Process runner, save/restore flow, status script, listen-address IPv6

- Process: Add IProcessHandle, ProcessOutputLine, StartAsync; RunAsync via StartAsync+WaitForExitAsync. Wait for stdout/stderr stream close before reading to fix empty restart output. ProcessTimeouts; ReloadService/StatusController/LogsService use config timeouts.
- Effective config: Rename reload→restart in save/restore result types and UI. Save modal: show restart output, danger alert, Stop editing/Continue editing (yellow), footer one-line; fallback exit code when no output; ensure pre visibility (CSS). Restore failed: combined output helper.
- Status script: When dnsmasq dead, run dnsmasq --test and show config error in status block (Docker harness; DNSMASQ_CONF).
- listen-address: Allow IPv6 (ValidateDraftValue accepts InterNetworkV6).
- Docker test: RestartCommand uses DnsmasqOptions timeout; DnsmasqOptions RestartTimeoutSeconds.
This commit is contained in:
2026-03-01 22:23:17 +10:00
parent a776f445b6
commit 8ed66cdf48
29 changed files with 499 additions and 183 deletions
@@ -66,22 +66,22 @@ public sealed class EffectiveConfigSaveService : IEffectiveConfigSaveService
"Failed to write config.");
}
var reload = await _reloadService.ReloadAsync(ct);
var restartResult = await _reloadService.ReloadAsync(ct);
var backupCreated = File.Exists(backupPath);
if (!reload.Success)
_logger.LogWarning("Config saved but reload failed: exit {ExitCode}, stderr: {Stderr}", reload.ExitCode, reload.StdErr);
if (!restartResult.Success)
_logger.LogWarning("Config saved but restart command failed: exit {ExitCode}, stderr: {Stderr}", restartResult.ExitCode, restartResult.StdErr);
return new EffectiveConfigSaveResult(
BackupCreated: backupCreated,
BackupPath: backupCreated ? backupPath : null,
Saved: true,
Reloaded: reload.Success,
ReloadExitCode: reload.ExitCode,
ReloadStdOut: reload.StdOut,
ReloadStdErr: reload.StdErr,
ErrorCode: reload.Success ? null : "reload_failed",
UserMessage: reload.Success ? "Saved and reloaded." : "Saved, but reload failed.");
Restarted: restartResult.Success,
RestartExitCode: restartResult.ExitCode,
RestartStdOut: restartResult.StdOut,
RestartStdErr: restartResult.StdErr,
ErrorCode: restartResult.Success ? null : "restart_failed",
UserMessage: restartResult.Success ? "Saved and dnsmasq restarted." : "Saved, but the restart command failed.");
}
/// <inheritdoc />
@@ -106,19 +106,19 @@ public sealed class EffectiveConfigSaveService : IEffectiveConfigSaveService
File.Copy(backupPath, managedPath, overwrite: true);
_logger.LogInformation("Restored managed config from backup: {BackupPath}", backupPath);
var reload = await _reloadService.ReloadAsync(ct);
var restartResult = await _reloadService.ReloadAsync(ct);
if (!reload.Success)
_logger.LogWarning("Restore completed but reload failed: exit {ExitCode}, stderr: {Stderr}", reload.ExitCode, reload.StdErr);
if (!restartResult.Success)
_logger.LogWarning("Restore completed but restart command failed: exit {ExitCode}, stderr: {Stderr}", restartResult.ExitCode, restartResult.StdErr);
return new EffectiveConfigRestoreResult(
Restored: true,
Reloaded: reload.Success,
ReloadExitCode: reload.ExitCode,
ReloadStdErr: reload.StdErr,
UserMessage: reload.Success
? "Backup restored and dnsmasq reloaded."
: "Backup restored, but reload still failed.");
Restarted: restartResult.Success,
RestartExitCode: restartResult.ExitCode,
RestartStdErr: restartResult.StdErr,
UserMessage: restartResult.Success
? "Backup restored and dnsmasq restarted."
: "Backup restored, but the restart command still failed.");
}
private static string BuildBackupPath(string managedPath) =>