Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build-and-test:
runs-on: windows-latest

steps:
- uses: actions/checkout@v4

- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Restore
run: dotnet restore W-Fix.sln

- name: Build
run: dotnet build W-Fix.sln --configuration Release --no-restore

- name: Test
run: dotnet test W-Fix.sln --configuration Release --no-build

- name: NuGet vulnerability report
run: dotnet list W-Fix.sln package --vulnerable --include-transitive
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 OneDeadMachine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ W-Fix/

**Стек:** WPF · .NET 8 · ModernWpfUI · CommunityToolkit.Mvvm · PowerShell SDK · Serilog

Подробности внутренних границ, выполнения PowerShell и правил для фиксеров:
[docs/architecture.md](docs/architecture.md).

---

## 📋 Системные требования
Expand Down
6 changes: 6 additions & 0 deletions W-Fix.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "W-Fix.Core", "src\W-Fix.Cor
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "W-Fix.App", "src\W-Fix.App\W-Fix.App.csproj", "{A1B2C3D4-0002-0002-0002-000000000002}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "W-Fix.Core.Tests", "tests\W-Fix.Core.Tests\W-Fix.Core.Tests.csproj", "{A1B2C3D4-0003-0003-0003-000000000003}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,5 +23,9 @@ Global
{A1B2C3D4-0002-0002-0002-000000000002}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-0002-0002-0002-000000000002}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-0002-0002-0002-000000000002}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-0003-0003-0003-000000000003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-0003-0003-0003-000000000003}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-0003-0003-0003-000000000003}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-0003-0003-0003-000000000003}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
40 changes: 40 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Архитектура W-Fix

## Границы компонентов

- `W-Fix.App` — WPF-интерфейс, выбор цели и сценария, отображение прогресса.
- `W-Fix.Core.Models` — данные принтеров, журнал и типизированные результаты операций.
- `W-Fix.Core.Services` — обнаружение принтеров, Active Directory и выполнение PowerShell.
- `W-Fix.Core.Fixers` — прикладные сценарии диагностики и исправления.

UI не должен самостоятельно определять успешность системной команды. Источником истины является
`PowerShellExecutionResult`, а итог многошагового фиксера вычисляется из результатов всех выполненных шагов.

## Выполнение PowerShell

`PowerShellEngine` поддерживает два режима:

1. Встроенный runspace PowerShell SDK — в том числе для `Invoke-Command` на удалённой машине.
2. Внешний Windows PowerShell 5.1 — для Windows-модулей, несовместимых со встроенным Core-runspace.

Оба режима имеют общий десятиминутный таймаут по умолчанию. Пользовательская отмена пробрасывается как
`OperationCanceledException`; таймаут возвращается как неуспешный `PowerShellExecutionResult` с
`TimedOut = true`. Внешний процесс при этом завершается вместе с дочерними процессами.

Скрипты используют префиксы `[OK]`, `[INFO]`, `[WARN]` и `[ERROR]`. Даже если PowerShell-скрипт перехватил
исключение, строка `[ERROR]` делает результат неуспешным. Служебный progress-CLIXML Windows PowerShell 5.1
в `stderr` не считается ошибкой.

## Правила для фиксеров

- Не сообщать `Success`, если обязательный шаг завершился неуспешно.
- Не скрывать ошибку обязательного шага за успешным перезапуском Spooler.
- Ограничивать удаление файлов только ресурсами подсистемы печати.
- Для потенциально опасных изменений реестра сохранять исходное состояние перед будущей реализацией rollback.
- Любой новый сценарий должен иметь тестируемую логику определения результата.

## Следующий архитектурный этап

Вынести создание сервисов из `MainWindowViewModel` в composition root и ввести интерфейсы исполнителя,
обнаружения принтеров и диалогов. Это позволит тестировать фиксеры без реального изменения Windows и добавить
режим предварительного просмотра/отката системных настроек.
6 changes: 6 additions & 0 deletions src/W-Fix.Core/Fixers/DriverFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,14 @@ exit 1
# Шаг 1: Установить драйвер через pnputil
Write-Output ""[INFO] Добавляем INF в хранилище драйверов через pnputil...""
$pnpResult = pnputil.exe /add-driver ""$infPath"" /install 2>&1
$pnpExitCode = $LASTEXITCODE
foreach ($line in $pnpResult) {
Write-Output ""[INFO] pnputil: $line""
}
if ($pnpExitCode -ne 0) {
Write-Output ""[ERROR] pnputil завершился с кодом $pnpExitCode""
exit $pnpExitCode
}

# Шаг 2: Если есть конкретный принтер — попытаемся обновить его драйвер
if ($printerName -ne '' -and $driverName -ne 'Unknown') {
Expand Down Expand Up @@ -249,6 +254,7 @@ exit 1
}
}
Write-Output ""[INFO] Установите драйвер вручную через INF-файл (режим INF)""
exit 1
}
";

Expand Down
17 changes: 12 additions & 5 deletions src/W-Fix.Core/Fixers/Error02Fixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ public override async Task<FixResult> ApplyAsync(
""";

using var engine = new PowerShellEngine(remoteMachine);
var (_, out1, _) = await engine.RunAsync(step1, ct: ct);
var result1 = await engine.RunAsync(step1, ct: ct);
var out1 = result1.Output;
ReportOutput(out1, Report);

// ── Шаг 2: Очистка Print Environments в реестре ─────────────────────────
Expand Down Expand Up @@ -114,7 +115,8 @@ public override async Task<FixResult> ApplyAsync(
}
""";

var (_, out2, _) = await engine.RunAsync(step2, ct: ct);
var result2 = await engine.RunAsync(step2, ct: ct);
var out2 = result2.Output;
ReportOutput(out2, Report);

// ── Шаг 3: PendingFileRenameOperations ──────────────────────────────────
Expand Down Expand Up @@ -151,7 +153,8 @@ public override async Task<FixResult> ApplyAsync(
}
""";

var (_, out3, _) = await engine.RunAsync(step3, ct: ct);
var result3 = await engine.RunAsync(step3, ct: ct);
var out3 = result3.Output;
ReportOutput(out3, Report);

// ── Шаг 4: Point and Print + RPC ────────────────────────────────────────
Expand All @@ -176,7 +179,8 @@ public override async Task<FixResult> ApplyAsync(
}
""";

var (_, out4, _) = await engine.RunAsync(step4, ct: ct);
var result4 = await engine.RunAsync(step4, ct: ct);
var out4 = result4.Output;
ReportOutput(out4, Report);

// ── Шаг 5: Перезапуск Spooler ───────────────────────────────────────────
Expand All @@ -187,7 +191,10 @@ public override async Task<FixResult> ApplyAsync(

Report(Info("Рекомендация: перезагрузите компьютер, затем повторите подключение к принтеру."));

return spoolerResult.Status == FixStatus.Success
var allStepsSucceeded = new[] { result1, result2, result3, result4 }
.All(result => result.Success);

return spoolerResult.Status == FixStatus.Success && allStepsSucceeded
? FixResult.Ok("Фикс 0x00000002 выполнен", steps)
: FixResult.Warn("Фикс 0x00000002 частично выполнен", steps);
}
Expand Down
16 changes: 13 additions & 3 deletions src/W-Fix.Core/Fixers/Error4005Fixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public override async Task<FixResult> ApplyAsync(
IProgress<LogEntry>? progress, CancellationToken ct)
{
var steps = new List<LogEntry>();
var executionResults = new List<PowerShellExecutionResult>();
void Report(LogEntry e) { steps.Add(e); progress?.Report(e); }

Report(Info("Диагностика и исправление ошибки 0x00004005..."));
Expand All @@ -53,8 +54,14 @@ public override async Task<FixResult> ApplyAsync(
""";

using var engine = remoteMachine != null ? new PowerShellEngine(remoteMachine) : null;
async Task<(bool, IReadOnlyList<string>, string?)> RunPS(string script) =>
engine != null ? await engine.RunAsync(script, ct: ct) : await PowerShellEngine.RunExternalAsync(script, ct);
async Task<PowerShellExecutionResult> RunPS(string script)
{
var result = engine != null
? await engine.RunAsync(script, ct: ct)
: await PowerShellEngine.RunExternalAsync(script, ct);
executionResults.Add(result);
return result;
}

var (_, out1, _) = await RunPS(step1);
ReportOutput(out1, Report);
Expand Down Expand Up @@ -187,7 +194,10 @@ public override async Task<FixResult> ApplyAsync(
Report(Info("═══════════════════════════════════════════════════"));
Report(Info("Рекомендация: перезагрузите компьютер для применения всех изменений реестра."));

return spoolerResult.Status == FixStatus.Success
var allStepsSucceeded = executionResults.All(result => result.Success) &&
steps.All(step => step.Level != Models.LogLevel.Error);

return spoolerResult.Status == FixStatus.Success && allStepsSucceeded
? FixResult.Ok("Фикс 0x00004005 выполнен. Рекомендуется перезагрузка.", steps)
: FixResult.Warn("Фикс 0x00004005 частично выполнен — проверьте логи", steps);
}
Expand Down
22 changes: 17 additions & 5 deletions src/W-Fix.Core/Fixers/Error7eFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public override async Task<FixResult> ApplyAsync(
IProgress<LogEntry>? progress, CancellationToken ct)
{
var steps = new List<LogEntry>();
var executionResults = new List<PowerShellExecutionResult>();
void Report(LogEntry e) { steps.Add(e); progress?.Report(e); }

Report(Info("Диагностика и исправление ошибки 0x0000007e..."));
Expand Down Expand Up @@ -72,7 +73,9 @@ public override async Task<FixResult> ApplyAsync(
""";

using var engine = new PowerShellEngine(remoteMachine);
var (_, out1, _) = await engine.RunAsync(step1, ct: ct);
var result1 = await engine.RunAsync(step1, ct: ct);
executionResults.Add(result1);
var out1 = result1.Output;
ReportOutput(out1, Report);

// ── Шаг 2: Удаление BIDI-ключа реестра ──────────────────────────────────
Expand Down Expand Up @@ -104,7 +107,9 @@ reg export ""HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\$printerName\C
}
";

var (_, out2, _) = await engine.RunAsync(step2, ct: ct);
var result2 = await engine.RunAsync(step2, ct: ct);
executionResults.Add(result2);
var out2 = result2.Output;
ReportOutput(out2, Report);
}
else
Expand Down Expand Up @@ -146,7 +151,9 @@ reg export ""HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\$printerName\C
}
""";

var (_, out3, _) = await engine.RunAsync(step3, ct: ct);
var result3 = await engine.RunAsync(step3, ct: ct);
executionResults.Add(result3);
var out3 = result3.Output;
ReportOutput(out3, Report);

// ── Шаг 4: RPC + Spooler ───────────────────────────────────────────────
Expand All @@ -165,12 +172,17 @@ reg export ""HKLM\SYSTEM\CurrentControlSet\Control\Print\Printers\$printerName\C
Write-Output "[OK] Spooler: $status"
""";

var (success, out4, error) = await engine.RunAsync(step4, ct: ct);
var result4 = await engine.RunAsync(step4, ct: ct);
executionResults.Add(result4);
var out4 = result4.Output;
ReportOutput(out4, Report);

Report(Info("Рекомендация: попробуйте переподключить принтер."));

return success
var allStepsSucceeded = executionResults.All(result => result.Success);
var error = executionResults.FirstOrDefault(result => !result.Success)?.Error;

return allStepsSucceeded
? FixResult.Ok("Фикс 0x0000007e выполнен — mscms.dll на месте, BIDI очищен", steps)
: FixResult.Warn($"Фикс 0x0000007e частично: {error}", steps);
}
Expand Down
33 changes: 4 additions & 29 deletions src/W-Fix.Core/Fixers/Error8Fixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public override async Task<FixResult> ApplyAsync(PrinterInfo? printer, string? r
""";

using var engine = new PowerShellEngine(remoteMachine);
var (_, diagOutput, _) = await engine.RunAsync(diagScript, ct: ct);
var diagnosticResult = await engine.RunAsync(diagScript, ct: ct);
var diagOutput = diagnosticResult.Output;
foreach (var line in diagOutput)
{
var level = line.StartsWith("[OK]") ? Models.LogLevel.Success
Expand All @@ -68,34 +69,8 @@ public override async Task<FixResult> ApplyAsync(PrinterInfo? printer, string? r
var spoolerResult = await spoolerFixer.ApplyAsync(printer, remoteMachine, progress, ct);
steps.AddRange(spoolerResult.Steps);

// Дополнительно: очистка временных файлов Windows
var cleanupScript = """
Write-Output "[INFO] Очистка временных файлов Windows (помогает при нехватке места)..."
$tempPaths = @($env:TEMP, $env:TMP, "$env:SystemRoot\Temp")
$totalDeleted = 0
foreach ($p in $tempPaths) {
if (Test-Path $p) {
$items = Get-ChildItem -Path $p -Recurse -ErrorAction SilentlyContinue
$items | Where-Object { -not $_.PSIsContainer } |
Remove-Item -Force -ErrorAction SilentlyContinue
$totalDeleted++
}
}
Write-Output "[OK] Очистка временных папок завершена"
""";

var (cleanSuccess, cleanOutput, cleanError) = await engine.RunAsync(cleanupScript, ct: ct);
foreach (var line in cleanOutput)
{
var level = line.StartsWith("[OK]") ? Models.LogLevel.Success
: line.StartsWith("[WARN]") ? Models.LogLevel.Warning
: line.StartsWith("[ERROR]") ? Models.LogLevel.Error
: Models.LogLevel.Info;
Report(new LogEntry(level, line));
}

return spoolerResult.Status == FixStatus.Success
? FixResult.Ok("Фикс 0x00000008 выполнен: очередь очищена, временные файлы удалены", steps)
return spoolerResult.Status == FixStatus.Success && diagnosticResult.Success
? FixResult.Ok("Фикс 0x00000008 выполнен: очередь очищена, Spooler перезапущен", steps)
: FixResult.Warn("Фикс 0x00000008 частично выполнен — проверьте логи", steps);
}
}
14 changes: 11 additions & 3 deletions src/W-Fix.Core/Fixers/IppFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public override async Task<FixResult> ApplyAsync(
IProgress<LogEntry>? progress, CancellationToken ct)
{
var steps = new List<LogEntry>();
var executionResults = new List<PowerShellExecutionResult>();
void Report(LogEntry e) { steps.Add(e); progress?.Report(e); }

Report(Info("Диагностика и исправление сбоя IPP..."));
Expand Down Expand Up @@ -79,8 +80,14 @@ public override async Task<FixResult> ApplyAsync(
""";

using var engine = remoteMachine != null ? new PowerShellEngine(remoteMachine) : null;
async Task<(bool, IReadOnlyList<string>, string?)> RunPS(string script) =>
engine != null ? await engine.RunAsync(script, ct: ct) : await PowerShellEngine.RunExternalAsync(script, ct);
async Task<PowerShellExecutionResult> RunPS(string script)
{
var result = engine != null
? await engine.RunAsync(script, ct: ct)
: await PowerShellEngine.RunExternalAsync(script, ct);
executionResults.Add(result);
return result;
}

var (_, out1, _) = await RunPS(step1);
ReportOutput(out1, Report);
Expand Down Expand Up @@ -262,7 +269,8 @@ public override async Task<FixResult> ApplyAsync(
Report(Info("IPP требует включённый компонент Windows и MS IPP Class Driver."));
Report(Info("При сохранении проблем — перезагрузите ПК."));

var allOk = steps.All(s => s.Level != Models.LogLevel.Error);
var allOk = executionResults.All(result => result.Success) &&
steps.All(s => s.Level != Models.LogLevel.Error);
return allOk
? FixResult.Ok("Фикс IPP выполнен", steps)
: FixResult.Warn("Фикс IPP частично выполнен — проверьте логи", steps);
Expand Down
Loading
Loading