diff --git a/WireSockUI.Tests/Program.cs b/WireSockUI.Tests/Program.cs index 55d1090..6550de7 100644 --- a/WireSockUI.Tests/Program.cs +++ b/WireSockUI.Tests/Program.cs @@ -287,6 +287,8 @@ private static int Main(string[] args) { "WinForms dialogs use readable responsive layouts", WinFormsDialogsUseReadableResponsiveLayouts }, { "Main window profile details retain visual order after scaling", MainWindowProfileDetailsRetainVisualOrderAfterScaling }, { "Main window action rows remain visible after scaling", MainWindowActionRowsRemainVisibleAfterScaling }, + { "Main window concealment hides before handle recreation", MainWindowConcealmentHidesBeforeHandleRecreation }, + { "Main window activation is suppressed during shutdown", MainWindowActivationIsSuppressedDuringShutdown }, { "Settings copies the secured profiles path without shell activation", SettingsCopiesSecuredProfilesPathWithoutShellActivation }, { "Editor bounds synchronous syntax highlighting", EditorBoundsSynchronousSyntaxHighlighting }, { "Editor application-rule insertion is section aware", EditorApplicationRuleInsertionIsSectionAware }, @@ -305,6 +307,7 @@ private static int Main(string[] args) { "Program rejects non-local application paths", ProgramRejectsNonLocalApplicationPaths }, { "Autorun task name is path and user seeded", AutoRunTaskNameIsPathAndUserSeeded }, { "Autorun validates the complete task definition", AutoRunValidatesCompleteTaskDefinition }, + { "Autorun recognizes only the historical WireSock UI task shape", AutoRunRecognizesHistoricalTaskShape }, { "Process picker preserves executable match names", ProcessPickerPreservesExecutableMatchNames }, { "Process snapshots are cached serialized and SID based", ProcessSnapshotsAreCachedSerializedAndSidBased }, { "Process picker loads executable icons and main title shows version", ProcessPickerLoadsExecutableIconsAndMainTitleShowsVersion }, @@ -4377,6 +4380,15 @@ private static void AutoRunHelperBoundsHangsAndRecoversVerifiedState() AssertTrue( failedMutation.OperationStarted, "Expected a failed helper mutation to require state verification."); + var verifiedFailureDiagnostic = FrmSettings.BuildVerifiedIncompleteAutoRunDiagnostic( + AutoRunHelperOperation.Enable, + failedMutation.Diagnostic); + AssertTrue( + verifiedFailureDiagnostic.Contains("simulated autorun helper failure"), + "Expected verified helper failures to retain their original diagnostic."); + AssertFalse( + verifiedFailureDiagnostic.Contains("timed-out"), + "Expected a non-timeout helper failure not to be mislabeled as a timeout."); Environment.SetEnvironmentVariable(behaviorVariable, "success"); var blockedAfterFailure = service.ExecuteAsync( @@ -4989,6 +5001,88 @@ private static void AssertActionButtonFits( $"Expected the action button to retain its right inset at {scenario}."); } + private static void MainWindowConcealmentHidesBeforeHandleRecreation() + { + using (var form = new Form + { + Location = new Point(-32000, -32000), + ShowInTaskbar = true, + Size = new Size(320, 200), + StartPosition = FormStartPosition.Manual + }) + { + var handleDestroyedWhileVisible = false; + var handleCreatedWhileVisible = false; + var recreatedHandleCount = 0; + form.HandleDestroyed += (sender, args) => + handleDestroyedWhileVisible |= form.Visible; + + form.Show(); + AssertTrue(form.Visible, + "Expected the test window to be visible before concealment."); + AssertTrue(form.IsHandleCreated, + "Expected the test window to own a native handle before concealment."); + form.HandleCreated += (sender, args) => + { + recreatedHandleCount++; + handleCreatedWhileVisible |= form.Visible; + }; + + FrmMain.ConcealFromTaskbar(form); + + AssertFalse(form.Visible, + "Expected concealment to hide the window."); + AssertFalse(form.ShowInTaskbar, + "Expected concealment to remove the window from the taskbar."); + AssertFalse(handleDestroyedWhileVisible, + "Expected any ShowInTaskbar handle recreation to occur only after the window was hidden."); + AssertTrue(recreatedHandleCount > 0, + "Expected changing ShowInTaskbar on .NET Framework to recreate the native window handle."); + AssertFalse(handleCreatedWhileVisible, + "Expected the recreated native window never to become visible during concealment."); + } + } + + private static void MainWindowActivationIsSuppressedDuringShutdown() + { + using (var form = new Form + { + Location = new Point(-32000, -32000), + ShowInTaskbar = false, + Size = new Size(320, 200), + StartPosition = FormStartPosition.Manual + }) + { + AssertFalse(FrmMain.TryShowMainWindow(form, true, false), + "Expected an exit request to suppress tray activation."); + AssertFalse(form.Visible, + "Expected the exit-suppressed window to remain hidden."); + AssertFalse(form.TopMost, + "Expected suppressed activation not to alter topmost state."); + + AssertFalse(FrmMain.TryShowMainWindow(form, false, true), + "Expected completed shutdown to suppress tray activation."); + AssertFalse(form.Visible, + "Expected the shutdown-suppressed window to remain hidden."); + AssertFalse(form.TopMost, + "Expected shutdown suppression not to alter topmost state."); + + AssertTrue(FrmMain.TryShowMainWindow(form, false, false), + "Expected tray activation to show a live main window."); + AssertTrue(form.Visible, + "Expected a live main window to become visible."); + AssertTrue(form.ShowInTaskbar, + "Expected a restored main window to return to the taskbar."); + + FrmMain.ConcealFromTaskbar(form); + } + + var disposedForm = new Form(); + disposedForm.Dispose(); + AssertFalse(FrmMain.TryShowMainWindow(disposedForm, false, false), + "Expected a disposed main window to reject activation."); + } + private static void SettingsCopiesSecuredProfilesPathWithoutShellActivation() { var folder = Path.Combine( @@ -9058,46 +9152,29 @@ private static void AutoRunValidatesCompleteTaskDefinition() using (var taskService = new Microsoft.Win32.TaskScheduler.TaskService()) using (var definition = taskService.NewTask()) { - definition.Principal.UserId = currentUserId; - definition.Principal.LogonType = Microsoft.Win32.TaskScheduler.TaskLogonType.InteractiveToken; - definition.Principal.RunLevel = Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest; - definition.Principal.ProcessTokenSidType = - Microsoft.Win32.TaskScheduler.TaskProcessTokenSidType.Default; - definition.Settings.ExecutionTimeLimit = TimeSpan.Zero; - definition.Settings.DisallowStartIfOnBatteries = false; - definition.Settings.StopIfGoingOnBatteries = false; - definition.Settings.WakeToRun = true; - definition.Settings.IdleSettings.StopOnIdleEnd = false; - definition.Settings.RunOnlyIfIdle = false; - definition.Settings.RunOnlyIfNetworkAvailable = false; - definition.Settings.RestartCount = 0; - definition.Settings.RestartInterval = TimeSpan.Zero; - definition.Settings.MultipleInstances = - Microsoft.Win32.TaskScheduler.TaskInstancesPolicy.IgnoreNew; - definition.Settings.StartWhenAvailable = true; - definition.Settings.Enabled = true; - definition.Settings.Hidden = false; - definition.Settings.AllowDemandStart = true; - definition.Settings.DeleteExpiredTaskAfter = TimeSpan.Zero; - definition.Settings.Priority = FrmSettings.AutoRunTaskPriorityClass; - definition.Settings.Volatile = false; - definition.Settings.DisallowStartOnRemoteAppSession = false; - var logonTrigger = new Microsoft.Win32.TaskScheduler.LogonTrigger - { - UserId = currentUserId, - Delay = TimeSpan.Zero, - Enabled = true, - StartBoundary = DateTime.MinValue, - EndBoundary = DateTime.MaxValue, - ExecutionTimeLimit = TimeSpan.Zero - }; - definition.Triggers.Add(logonTrigger); - definition.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction(executablePath)); + FrmSettings.ConfigureAutoRunTaskDefinition( + definition, + "WireSockUI", + currentUserId, + executablePath); + var logonTrigger = + (Microsoft.Win32.TaskScheduler.LogonTrigger)definition.Triggers[0]; AssertTrue(FrmSettings.IsTaskDefinitionOwnedByExecutable( definition, true, executablePath), "Expected the exact elevated logon task shape to be recognized."); - var serializedPriority = XDocument.Parse(definition.XmlText) + var serializedDefinition = XDocument.Parse(definition.XmlText); + AssertEqual("1.3", serializedDefinition.Root?.Attribute("version")?.Value); + AssertFalse( + serializedDefinition.Descendants() + .Any(element => element.Name.LocalName == "Volatile"), + "Expected the Windows 7 definition not to emit the Windows 8 Volatile setting."); + AssertEqual( + (int)Microsoft.Win32.TaskScheduler.TaskCompatibility.V2_1, + (int)definition.Settings.Compatibility); + AssertFalse(FrmSettings.IsAutoRunTaskVolatile(definition.Settings), + "Expected a Windows 7 task definition to be inherently non-volatile."); + var serializedPriority = serializedDefinition .Descendants() .Single(element => element.Name.LocalName == "Priority"); AssertEqual(7, XmlConvert.ToInt32(serializedPriority.Value)); @@ -9226,6 +9303,183 @@ private static void AutoRunValidatesCompleteTaskDefinition() "Expected an inherit-only SYSTEM ACE not to satisfy the task DACL."); } + private static void AutoRunRecognizesHistoricalTaskShape() + { + string currentUserId; + using (var identity = WindowsIdentity.GetCurrent()) + currentUserId = identity.User?.Value ?? + throw new InvalidOperationException("Current user SID unavailable."); + + var otherUserId = new SecurityIdentifier( + WellKnownSidType.LocalSystemSid, + null).Value; + if (FrmSettings.IsSameTaskUser(currentUserId, otherUserId)) + otherUserId = new SecurityIdentifier( + WellKnownSidType.LocalServiceSid, + null).Value; + + using (var taskService = new Microsoft.Win32.TaskScheduler.TaskService()) + using (var definition = taskService.NewTask()) + { + definition.RegistrationInfo.Description = "Auto start for WireSockUI"; + definition.Principal.UserId = currentUserId; + definition.Principal.LogonType = + Microsoft.Win32.TaskScheduler.TaskLogonType.InteractiveToken; + definition.Principal.RunLevel = + Microsoft.Win32.TaskScheduler.TaskRunLevel.Highest; + definition.Triggers.Add(new Microsoft.Win32.TaskScheduler.LogonTrigger + { + UserId = currentUserId, + Enabled = true + }); + definition.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction( + @"C:\Former WireSock UI\WireSockUI.exe")); + + AssertTrue( + FrmSettings.IsLegacyAutoRunTaskDefinitionMigratable( + definition, + "WireSockUI.exe", + "Auto start for WireSockUI"), + "Expected the exact historical elevated WireSock UI task to remain migratable after its install path changes."); + AssertFalse( + FrmSettings.ShouldApplyAutoRunChange( + FrmSettings.AutoRunStatus.LegacyEnabled, + true, + false, + false, + false, + true, + false), + "Expected historical-task migration to remain blocked until the user approves it."); + AssertTrue( + FrmSettings.ShouldApplyAutoRunChange( + FrmSettings.AutoRunStatus.LegacyEnabled, + true, + false, + false, + false, + true, + true), + "Expected approval to migrate an enabled historical task to the current protected task."); + AssertFalse( + FrmSettings.ShouldOfferLegacyProductTaskMigration( + true, + false, + FrmSettings.AutoRunStatus.Conflict, + false, + false), + "Expected a conflicting autorun status not to offer a migration that cannot run."); + AssertFalse( + FrmSettings.ShouldOfferLegacyProductTaskMigration( + true, + false, + FrmSettings.AutoRunStatus.LegacyEnabled, + true, + false), + "Expected legacy-task migration to wait until opaque Startup cleanup is approved."); + AssertTrue( + FrmSettings.ShouldOfferLegacyProductTaskMigration( + true, + false, + FrmSettings.AutoRunStatus.LegacyEnabled, + true, + true), + "Expected legacy-task migration to be offered after all mutation prerequisites are approved."); + AssertEqual( + (int)AutoRunHelperOperation.Enable, + (int)FrmSettings.GetAutoRunMutationOperation(true, false)); + AssertEqual( + (int)AutoRunHelperOperation.EnableMigratingLegacyTask, + (int)FrmSettings.GetAutoRunMutationOperation(true, true)); + AssertEqual( + (int)AutoRunHelperOperation.Disable, + (int)FrmSettings.GetAutoRunMutationOperation(false, false)); + AssertEqual( + (int)AutoRunHelperOperation.DisableMigratingLegacyTask, + (int)FrmSettings.GetAutoRunMutationOperation(false, true)); + + AssertTrue( + FrmSettings.TryResolveMutationOutcome( + AutoRunHelperOperation.EnableMigratingLegacyTask, + FrmSettings.AutoRunStatus.Enabled, + true, + false, + true, + out var partialEnableMigrationSucceeded), + "Expected the canonical current task plus a remaining historical task to be a verifiable partial migration."); + AssertFalse( + partialEnableMigrationSucceeded, + "Expected a remaining historical task to prevent successful enable-migration verification."); + AssertTrue( + FrmSettings.TryResolveMutationOutcome( + AutoRunHelperOperation.DisableMigratingLegacyTask, + FrmSettings.AutoRunStatus.Disabled, + false, + false, + true, + out var partialDisableMigrationSucceeded), + "Expected disabled autorun plus a remaining historical task to be a verifiable partial migration."); + AssertFalse( + partialDisableMigrationSucceeded, + "Expected a remaining historical task to prevent successful disable-migration verification."); + AssertTrue( + FrmSettings.TryResolveMutationOutcome( + AutoRunHelperOperation.EnableMigratingLegacyTask, + FrmSettings.AutoRunStatus.Enabled, + true, + false, + false, + out var completedEnableMigrationSucceeded) && + completedEnableMigrationSucceeded, + "Expected canonical autorun with no historical task to verify completed migration."); + + var action = (Microsoft.Win32.TaskScheduler.ExecAction)definition.Actions[0]; + action.Path = @"C:\Former WireSock UI\not-wiresock.exe"; + AssertFalse( + FrmSettings.IsLegacyAutoRunTaskDefinitionMigratable( + definition, + "WireSockUI.exe", + "Auto start for WireSockUI"), + "Expected a different executable name to remain a conflict."); + action.Path = @"C:\Former WireSock UI\WireSockUI.exe"; + + action.Arguments = "--unexpected"; + AssertFalse( + FrmSettings.IsLegacyAutoRunTaskDefinitionMigratable( + definition, + "WireSockUI.exe", + "Auto start for WireSockUI"), + "Expected an argument-bearing task not to be migrated."); + action.Arguments = null; + + definition.RegistrationInfo.Description = "Another application"; + AssertFalse( + FrmSettings.IsLegacyAutoRunTaskDefinitionMigratable( + definition, + "WireSockUI.exe", + "Auto start for WireSockUI"), + "Expected a task without the historical product description to remain a conflict."); + definition.RegistrationInfo.Description = "Auto start for WireSockUI"; + + definition.Principal.UserId = otherUserId; + AssertFalse( + FrmSettings.IsLegacyAutoRunTaskDefinitionMigratable( + definition, + "WireSockUI.exe", + "Auto start for WireSockUI"), + "Expected another user's historical task not to be migrated."); + definition.Principal.UserId = currentUserId; + + definition.Actions.Add(new Microsoft.Win32.TaskScheduler.ExecAction("cmd.exe")); + AssertFalse( + FrmSettings.IsLegacyAutoRunTaskDefinitionMigratable( + definition, + "WireSockUI.exe", + "Auto start for WireSockUI"), + "Expected a task with an additional action to remain a conflict."); + } + } + private static void ShellLinkHresultValidationUsesSignedFailureSemantics() { ShellLink.VerifySucceeded(0); diff --git a/WireSockUI/Forms/AutoRunOperationService.cs b/WireSockUI/Forms/AutoRunOperationService.cs index 06be354..a631046 100644 --- a/WireSockUI/Forms/AutoRunOperationService.cs +++ b/WireSockUI/Forms/AutoRunOperationService.cs @@ -11,7 +11,9 @@ internal enum AutoRunHelperOperation { Inspect, Enable, + EnableMigratingLegacyTask, Disable, + DisableMigratingLegacyTask, DeleteLegacyShortcut } @@ -433,8 +435,12 @@ private static string GetOperationArgument(AutoRunHelperOperation operation) return "inspect"; case AutoRunHelperOperation.Enable: return "enable"; + case AutoRunHelperOperation.EnableMigratingLegacyTask: + return "enable-migrating-legacy-task"; case AutoRunHelperOperation.Disable: return "disable"; + case AutoRunHelperOperation.DisableMigratingLegacyTask: + return "disable-migrating-legacy-task"; case AutoRunHelperOperation.DeleteLegacyShortcut: return "delete-legacy-shortcut"; default: @@ -452,9 +458,15 @@ private static bool TryParseOperation(string value, out AutoRunHelperOperation o case "enable": operation = AutoRunHelperOperation.Enable; return true; + case "enable-migrating-legacy-task": + operation = AutoRunHelperOperation.EnableMigratingLegacyTask; + return true; case "disable": operation = AutoRunHelperOperation.Disable; return true; + case "disable-migrating-legacy-task": + operation = AutoRunHelperOperation.DisableMigratingLegacyTask; + return true; case "delete-legacy-shortcut": operation = AutoRunHelperOperation.DeleteLegacyShortcut; return true; diff --git a/WireSockUI/Forms/frmMain.cs b/WireSockUI/Forms/frmMain.cs index 153969a..de49a69 100644 --- a/WireSockUI/Forms/frmMain.cs +++ b/WireSockUI/Forms/frmMain.cs @@ -212,6 +212,43 @@ internal static void ConfigureBottomActionRow( actions.WrapContents = false; } + internal static void ConcealFromTaskbar(Form form) + { + if (form == null) + throw new ArgumentNullException(nameof(form)); + + // ShowInTaskbar recreates an existing WinForms top-level handle. + // Hide first so the replacement handle can never be painted as an + // empty frame while the window is being minimized or closed. + form.Hide(); + form.ShowInTaskbar = false; + } + + internal static bool TryShowMainWindow( + Form form, + bool exitRequested, + bool shutdownComplete) + { + if (form == null) + throw new ArgumentNullException(nameof(form)); + if (exitRequested || shutdownComplete || form.IsDisposed || form.Disposing) + return false; + + form.TopMost = true; + form.ShowInTaskbar = true; + form.Show(); + form.WindowState = FormWindowState.Normal; + form.BringToFront(); + form.Activate(); + form.TopMost = false; + return true; + } + + internal bool TryShowMainWindow() + { + return TryShowMainWindow(this, _exitRequested, _shutdownComplete); + } + internal static void ArrangeProfileDetails( Panel host, Control state, @@ -2211,8 +2248,7 @@ protected override async void OnLoad(EventArgs e) if (Settings.Default.AutoMinimize) { WindowState = FormWindowState.Minimized; - ShowInTaskbar = false; - Hide(); + ConcealFromTaskbar(this); } if (lstProfiles.Items.ContainsKey(PrivilegedSettingsStore.LastProfile)) @@ -2332,8 +2368,7 @@ private void OnFormClosing(object sender, FormClosingEventArgs e) if (e.CloseReason == CloseReason.UserClosing && !_exitRequested) { e.Cancel = true; - ShowInTaskbar = false; - Hide(); + ConcealFromTaskbar(this); return; } @@ -2342,8 +2377,11 @@ private void OnFormClosing(object sender, FormClosingEventArgs e) e.Cancel = true; _exitRequested = true; + trayIcon.Visible = false; Enabled = false; - ShowInTaskbar = false; + // The form is about to be disposed, so hiding it is sufficient to + // remove its taskbar button. Avoid changing ShowInTaskbar here: it + // would recreate the handle and briefly restart handle-bound work. Hide(); CloseOwnedFormsForShutdown(); BeginShutdownAndClose(); @@ -2404,21 +2442,14 @@ private async void CompleteShutdownAndCloseAsync() /// An EventArgs that contains the event data. private void OnFormShow(object sender, EventArgs e) { - TopMost = true; - ShowInTaskbar = true; - Show(); - WindowState = FormWindowState.Normal; - BringToFront(); - Activate(); - TopMost = false; + TryShowMainWindow(); } private void OnFormMinimize(object sender, EventArgs e) { if (WindowState == FormWindowState.Minimized) { - ShowInTaskbar = false; - Hide(); + ConcealFromTaskbar(this); } } diff --git a/WireSockUI/Forms/frmSettings.cs b/WireSockUI/Forms/frmSettings.cs index 95241ca..849a63c 100644 --- a/WireSockUI/Forms/frmSettings.cs +++ b/WireSockUI/Forms/frmSettings.cs @@ -32,6 +32,8 @@ public partial class FrmSettings : Form private bool _initialAutoRunUsesPathScopedTask; private bool _hasUnverifiedLegacyShortcut; private bool _legacyShortcutMigrationApproved; + private bool _hasLegacyProductTaskToMigrate; + private bool _legacyProductTaskMigrationApproved; private string _legacyStartupShortcutPath; private System.Threading.Tasks.Task _autoRunInspectionTask; private bool _managedResourcesDisposed; @@ -60,12 +62,14 @@ internal AutoRunInspection( AutoRunStatus status, bool usesPathScopedTask, bool hasUnverifiedLegacyShortcut, + bool hasLegacyProductTaskToMigrate, string legacyStartupShortcutPath, string diagnostic = null) { Status = status; UsesPathScopedTask = usesPathScopedTask; HasUnverifiedLegacyShortcut = hasUnverifiedLegacyShortcut; + HasLegacyProductTaskToMigrate = hasLegacyProductTaskToMigrate; LegacyStartupShortcutPath = legacyStartupShortcutPath; Diagnostic = diagnostic; } @@ -73,6 +77,7 @@ internal AutoRunInspection( internal AutoRunStatus Status { get; } internal bool UsesPathScopedTask { get; } internal bool HasUnverifiedLegacyShortcut { get; } + internal bool HasLegacyProductTaskToMigrate { get; } internal string LegacyStartupShortcutPath { get; } internal string Diagnostic { get; } } @@ -82,6 +87,7 @@ private sealed class AutoRunTaskInspection internal bool EnabledForCurrentExecutable { get; set; } internal bool Canonical { get; set; } internal bool Conflict { get; set; } + internal bool RequiresLegacyProductTaskMigration { get; set; } } public FrmSettings() @@ -175,6 +181,7 @@ private async void OnSettingsShown(object sender, EventArgs e) _initialAutoRunStatus = inspection.Status; _initialAutoRunUsesPathScopedTask = inspection.UsesPathScopedTask; _hasUnverifiedLegacyShortcut = inspection.HasUnverifiedLegacyShortcut; + _hasLegacyProductTaskToMigrate = inspection.HasLegacyProductTaskToMigrate; _legacyStartupShortcutPath = inspection.LegacyStartupShortcutPath; chkAutorun.Checked = ResolveRequestedAutoRun( _initialAutoRunStatus, @@ -416,7 +423,11 @@ private static AutoRunInspection InspectAutoRun() AutoRunTaskInspection legacyInspection; using (var legacyTask = FindRootAutoRunTask(taskService, GetLegacyAutoRunTaskName())) - legacyInspection = InspectAutoRunTask(legacyTask, false, true); + legacyInspection = InspectAutoRunTask( + legacyTask, + false, + true, + allowLegacyProductTask: true); var shortcutStatus = InspectLegacyStartupShortcutPath( legacyStartupShortcutPath, @@ -443,6 +454,7 @@ private static AutoRunInspection InspectAutoRun() status, usesPathScopedTask, RequiresLegacyStartupShortcutMigrationConsent(shortcutStatus), + legacyInspection.RequiresLegacyProductTaskMigration, legacyStartupShortcutPath, diagnostic); } @@ -472,18 +484,29 @@ internal static bool RequiresLegacyStartupShortcutMigrationConsent( private static AutoRunTaskInspection InspectAutoRunTask( Microsoft.Win32.TaskScheduler.Task task, bool pathScopedCandidate, - bool ignoreTaskScopedToAnotherUser) + bool ignoreTaskScopedToAnotherUser, + bool allowLegacyProductTask = false) { var inspection = new AutoRunTaskInspection(); if (task == null) return inspection; - var replaceable = IsTaskDefinitionReplaceableByExecutable( - task.Definition, Program.ApplicationLauncherPath); + var replaceableByCurrentExecutable = IsTaskDefinitionReplaceableByExecutable( + task.Definition, + Program.ApplicationLauncherPath); + var migratableLegacyProductTask = + !replaceableByCurrentExecutable && + allowLegacyProductTask && + IsLegacyAutoRunTaskDefinitionMigratable( + task.Definition, + Program.NativeLauncherFileName, + "Auto start for " + GetAppName()); + var replaceable = replaceableByCurrentExecutable || migratableLegacyProductTask; inspection.Conflict = !replaceable && !(ignoreTaskScopedToAnotherUser && IsTaskScopedToAnotherUser(task.Definition)); inspection.EnabledForCurrentExecutable = replaceable && task.Enabled; + inspection.RequiresLegacyProductTaskMigration = migratableLegacyProductTask; inspection.Canonical = pathScopedCandidate && IsTaskDefinitionOwnedByExecutable( task.Definition, task.Enabled, Program.ApplicationLauncherPath) && @@ -589,7 +612,8 @@ private static async System.Threading.Tasks.Task ExecuteAutoRunMutationAsy return true; if (result.Outcome == AutoRunOperationOutcome.Failed) throw new InvalidOperationException( - result.Diagnostic ?? $"The autorun {operation} operation failed."); + result.Diagnostic ?? + $"The autorun {GetAutoRunOperationDisplayName(operation)} operation failed."); if (result.Outcome == AutoRunOperationOutcome.StateUncertain) { @@ -612,53 +636,112 @@ private static async System.Threading.Tasks.Task ExecuteAutoRunMutationAsy return true; throw new InvalidOperationException( - $"The timed-out autorun {operation} operation was verified not to have completed."); + BuildVerifiedIncompleteAutoRunDiagnostic(operation, result.Diagnostic)); } } } throw new AutoRunOperationUncertainException( result.Diagnostic ?? - $"The autorun {operation} operation did not complete within its safety limit, and its final state could not be verified. No compensating autorun mutation will be started."); + $"The autorun {GetAutoRunOperationDisplayName(operation)} operation did not complete within its safety limit, and its final state could not be verified. No compensating autorun mutation will be started."); + } + + internal static string BuildVerifiedIncompleteAutoRunDiagnostic( + AutoRunHelperOperation operation, + string helperDiagnostic) + { + var verifiedDiagnostic = + $"The autorun {GetAutoRunOperationDisplayName(operation)} operation was verified not to have completed."; + return string.IsNullOrWhiteSpace(helperDiagnostic) + ? verifiedDiagnostic + : $"{verifiedDiagnostic} {helperDiagnostic.Trim()}"; + } + + private static string GetAutoRunOperationDisplayName(AutoRunHelperOperation operation) + { + switch (operation) + { + case AutoRunHelperOperation.Enable: + case AutoRunHelperOperation.EnableMigratingLegacyTask: + return "Enable"; + case AutoRunHelperOperation.Disable: + case AutoRunHelperOperation.DisableMigratingLegacyTask: + return "Disable"; + case AutoRunHelperOperation.DeleteLegacyShortcut: + return "legacy shortcut cleanup"; + default: + return operation.ToString(); + } } private static bool TryResolveMutationOutcome( AutoRunHelperOperation operation, AutoRunInspection inspection, out bool succeeded) + { + if (inspection == null) + { + succeeded = false; + return false; + } + + return TryResolveMutationOutcome( + operation, + inspection.Status, + inspection.UsesPathScopedTask, + inspection.HasUnverifiedLegacyShortcut, + inspection.HasLegacyProductTaskToMigrate, + out succeeded); + } + + internal static bool TryResolveMutationOutcome( + AutoRunHelperOperation operation, + AutoRunStatus status, + bool usesPathScopedTask, + bool hasUnverifiedLegacyShortcut, + bool hasLegacyProductTaskToMigrate, + out bool succeeded) { succeeded = false; - if (inspection == null || - inspection.Status == AutoRunStatus.Unknown || - inspection.Status == AutoRunStatus.Conflict) + if (status == AutoRunStatus.Unknown || status == AutoRunStatus.Conflict) return false; + if ((operation == AutoRunHelperOperation.EnableMigratingLegacyTask || + operation == AutoRunHelperOperation.DisableMigratingLegacyTask) && + hasLegacyProductTaskToMigrate) + { + // The requested migration explicitly includes removal of the historical + // product task. A canonical current state alone is only partial success. + return true; + } + switch (operation) { case AutoRunHelperOperation.Enable: - if (inspection.Status == AutoRunStatus.Enabled && - inspection.UsesPathScopedTask) + case AutoRunHelperOperation.EnableMigratingLegacyTask: + if (status == AutoRunStatus.Enabled && usesPathScopedTask) { succeeded = true; return true; } - return inspection.Status == AutoRunStatus.Disabled || - inspection.Status == AutoRunStatus.LegacyShortcutMigrationRequired; + return status == AutoRunStatus.Disabled || + status == AutoRunStatus.LegacyShortcutMigrationRequired; case AutoRunHelperOperation.Disable: - if (inspection.Status == AutoRunStatus.Disabled || - inspection.Status == AutoRunStatus.LegacyShortcutMigrationRequired) + case AutoRunHelperOperation.DisableMigratingLegacyTask: + if (status == AutoRunStatus.Disabled || + status == AutoRunStatus.LegacyShortcutMigrationRequired) { succeeded = true; return true; } - return inspection.Status == AutoRunStatus.Enabled || - inspection.Status == AutoRunStatus.LegacyEnabled; + return status == AutoRunStatus.Enabled || + status == AutoRunStatus.LegacyEnabled; case AutoRunHelperOperation.DeleteLegacyShortcut: - succeeded = !inspection.HasUnverifiedLegacyShortcut; + succeeded = !hasUnverifiedLegacyShortcut; return true; default: @@ -673,10 +756,11 @@ private static string SerializeAutoRunInspection(AutoRunInspection inspection) return string.Join( "|", - "1", + "2", ((int)inspection.Status).ToString(System.Globalization.CultureInfo.InvariantCulture), inspection.UsesPathScopedTask ? "1" : "0", inspection.HasUnverifiedLegacyShortcut ? "1" : "0", + inspection.HasLegacyProductTaskToMigrate ? "1" : "0", EncodeHelperField(inspection.LegacyStartupShortcutPath), EncodeHelperField(inspection.Diagnostic)); } @@ -684,7 +768,7 @@ private static string SerializeAutoRunInspection(AutoRunInspection inspection) private static AutoRunInspection DeserializeAutoRunInspection(string payload) { var fields = (payload ?? string.Empty).Split('|'); - if (fields.Length != 6 || fields[0] != "1" || + if (fields.Length != 7 || fields[0] != "2" || !int.TryParse( fields[1], System.Globalization.NumberStyles.None, @@ -692,7 +776,8 @@ private static AutoRunInspection DeserializeAutoRunInspection(string payload) out var statusValue) || !Enum.IsDefined(typeof(AutoRunStatus), statusValue) || !TryParseHelperBoolean(fields[2], out var usesPathScopedTask) || - !TryParseHelperBoolean(fields[3], out var hasUnverifiedLegacyShortcut)) + !TryParseHelperBoolean(fields[3], out var hasUnverifiedLegacyShortcut) || + !TryParseHelperBoolean(fields[4], out var hasLegacyProductTaskToMigrate)) throw new InvalidDataException("The autorun helper returned an invalid inspection result."); try @@ -701,8 +786,9 @@ private static AutoRunInspection DeserializeAutoRunInspection(string payload) (AutoRunStatus)statusValue, usesPathScopedTask, hasUnverifiedLegacyShortcut, - DecodeHelperField(fields[4]), - DecodeHelperField(fields[5])); + hasLegacyProductTaskToMigrate, + DecodeHelperField(fields[5]), + DecodeHelperField(fields[6])); } catch (FormatException ex) { @@ -751,10 +837,16 @@ private static AutoRunHelperExecution ExecuteAutoRunHelperOperation( return AutoRunHelperExecution.Success( SerializeAutoRunInspection(InspectAutoRun())); case AutoRunHelperOperation.Enable: - EnableAutoRun(); + EnableAutoRun(false); + return AutoRunHelperExecution.Success(); + case AutoRunHelperOperation.EnableMigratingLegacyTask: + EnableAutoRun(true); return AutoRunHelperExecution.Success(); case AutoRunHelperOperation.Disable: - DisableAutoRun(); + DisableAutoRun(false); + return AutoRunHelperExecution.Success(); + case AutoRunHelperOperation.DisableMigratingLegacyTask: + DisableAutoRun(true); return AutoRunHelperExecution.Success(); case AutoRunHelperOperation.DeleteLegacyShortcut: DeleteLegacyStartupShortcutIfPresent(GetLegacyStartupShortcutPath()); @@ -776,7 +868,7 @@ private static AutoRunHelperExecution ExecuteAutoRunHelperOperation( /// switches to battery power, to wake the computer if needed, and to not stop when the computer ceases to be idle. /// If an error occurs while enabling auto-run, a contextual exception is propagated to the settings transaction. /// - private static void EnableAutoRun() + private static void EnableAutoRun(bool allowLegacyProductTask) { var registrationCompleted = false; var pathScopedTaskExisted = false; @@ -786,59 +878,31 @@ private static void EnableAutoRun() using (var ts = new TaskService()) using (var td = ts.NewTask()) { - td.RegistrationInfo.Description = "Auto start for " + GetAppName(); - var currentUserId = GetCurrentUserId(); - td.Principal.UserId = currentUserId; - td.Principal.LogonType = TaskLogonType.InteractiveToken; - td.Principal.RunLevel = TaskRunLevel.Highest; // Run with the highest privileges - td.Principal.ProcessTokenSidType = TaskProcessTokenSidType.Default; - - var logonTrigger = new LogonTrigger - { - UserId = currentUserId, - Delay = TimeSpan.Zero, - Enabled = true, - StartBoundary = DateTime.MinValue, - EndBoundary = DateTime.MaxValue, - ExecutionTimeLimit = TimeSpan.Zero - }; - td.Triggers.Add(logonTrigger); // Trigger for this user only - var appPath = Program.ApplicationLauncherPath; if (!IsExecutablePathTrustedForAutoRun(appPath, out var trustDiagnostic)) throw new InvalidOperationException(trustDiagnostic); - td.Actions.Add(new ExecAction(appPath)); // Path to the executable - - // Set power and idle options - td.Settings.DisallowStartIfOnBatteries = - false; // Allow the task to start if the computer is running on batteries - td.Settings.StopIfGoingOnBatteries = - false; // Do not stop the task if the computer switches to battery power - td.Settings.WakeToRun = true; // Allow the task to wake the computer if needed - td.Settings.ExecutionTimeLimit = TimeSpan.Zero; // The VPN must not be terminated after 72 hours - td.Settings.IdleSettings.StopOnIdleEnd = - false; // Do not stop the task when the computer ceases to be idle - td.Settings.RunOnlyIfIdle = false; - td.Settings.RunOnlyIfNetworkAvailable = false; - td.Settings.RestartCount = 0; - td.Settings.RestartInterval = TimeSpan.Zero; - td.Settings.MultipleInstances = TaskInstancesPolicy.IgnoreNew; - td.Settings.StartWhenAvailable = true; - td.Settings.Enabled = true; - td.Settings.Hidden = false; - td.Settings.AllowDemandStart = true; - td.Settings.DeleteExpiredTaskAfter = TimeSpan.Zero; - td.Settings.Priority = AutoRunTaskPriorityClass; - td.Settings.Volatile = false; - td.Settings.DisallowStartOnRemoteAppSession = false; + ConfigureAutoRunTaskDefinition( + td, + GetAppName(), + currentUserId, + appPath); if (!IsExecutablePathTrustedForAutoRun(appPath, out trustDiagnostic)) throw new InvalidOperationException(trustDiagnostic); var autoRunTaskName = GetAutoRunTaskName(); pathScopedTaskExisted = EnsureAutoRunTaskCanBeReplaced(ts, autoRunTaskName); + EnsureAutoRunTaskCanBeRemoved( + ts, + GetLegacyPathScopedAutoRunTaskName(), + ignoreTaskScopedToAnotherUser: true); + EnsureAutoRunTaskCanBeRemoved( + ts, + GetLegacyAutoRunTaskName(), + ignoreTaskScopedToAnotherUser: true, + allowLegacyProductTask: allowLegacyProductTask); using (var registeredTask = ts.RootFolder.RegisterTaskDefinition( autoRunTaskName, td, @@ -858,7 +922,11 @@ private static void EnableAutoRun() } legacyCleanupStarted = true; DeleteAutoRunTaskIfReplaceable(ts, GetLegacyPathScopedAutoRunTaskName(), true); - DeleteAutoRunTaskIfReplaceable(ts, GetLegacyAutoRunTaskName(), true); + DeleteAutoRunTaskIfReplaceable( + ts, + GetLegacyAutoRunTaskName(), + ignoreTaskScopedToAnotherUser: true, + allowLegacyProductTask: allowLegacyProductTask); } } catch (Exception ex) @@ -877,6 +945,67 @@ private static void EnableAutoRun() } } + internal static void ConfigureAutoRunTaskDefinition( + TaskDefinition definition, + string appName, + string currentUserId, + string appPath) + { + if (definition == null) throw new ArgumentNullException(nameof(definition)); + if (string.IsNullOrWhiteSpace(appName)) + throw new ArgumentException("An application name is required.", nameof(appName)); + if (string.IsNullOrWhiteSpace(currentUserId)) + throw new ArgumentException("A current-user identifier is required.", nameof(currentUserId)); + if (string.IsNullOrWhiteSpace(appPath)) + throw new ArgumentException("An application path is required.", nameof(appPath)); + + definition.RegistrationInfo.Description = "Auto start for " + appName; + + // Windows 7 exposes Task Scheduler 2.1 (task XML schema 1.3). Explicitly + // stay on that schema so a definition authored on newer Windows remains + // portable to every supported OS. + definition.Settings.Compatibility = TaskCompatibility.V2_1; + + definition.Principal.UserId = currentUserId; + definition.Principal.LogonType = TaskLogonType.InteractiveToken; + definition.Principal.RunLevel = TaskRunLevel.Highest; + definition.Principal.ProcessTokenSidType = TaskProcessTokenSidType.Default; + + definition.Triggers.Add(new LogonTrigger + { + UserId = currentUserId, + Delay = TimeSpan.Zero, + Enabled = true, + StartBoundary = DateTime.MinValue, + EndBoundary = DateTime.MaxValue, + ExecutionTimeLimit = TimeSpan.Zero + }); + definition.Actions.Add(new ExecAction(appPath)); + + definition.Settings.DisallowStartIfOnBatteries = false; + definition.Settings.StopIfGoingOnBatteries = false; + definition.Settings.WakeToRun = true; + definition.Settings.ExecutionTimeLimit = TimeSpan.Zero; + definition.Settings.IdleSettings.StopOnIdleEnd = false; + definition.Settings.RunOnlyIfIdle = false; + definition.Settings.RunOnlyIfNetworkAvailable = false; + definition.Settings.RestartCount = 0; + definition.Settings.RestartInterval = TimeSpan.Zero; + definition.Settings.MultipleInstances = TaskInstancesPolicy.IgnoreNew; + definition.Settings.StartWhenAvailable = true; + definition.Settings.Enabled = true; + definition.Settings.Hidden = false; + definition.Settings.AllowDemandStart = true; + definition.Settings.DeleteExpiredTaskAfter = TimeSpan.Zero; + definition.Settings.Priority = AutoRunTaskPriorityClass; + definition.Settings.DisallowStartOnRemoteAppSession = false; + + // Do not assign TaskSettings.Volatile, even to false. The TaskScheduler + // wrapper promotes the definition to Task Scheduler 2.2/schema 1.4 on + // assignment, which Windows 7 cannot register. Its pre-2.2 value is + // inherently false. + } + /// /// Disables the auto-run feature for the current application with administrative privileges. /// @@ -884,15 +1013,29 @@ private static void EnableAutoRun() /// This method deletes only tasks that point to the current executable. /// If an error occurs while disabling auto-run, a contextual exception is propagated to the settings transaction. /// - private static void DisableAutoRun() + private static void DisableAutoRun(bool allowLegacyProductTask) { try { using (var ts = new TaskService()) { + EnsureAutoRunTaskCanBeRemoved(ts, GetAutoRunTaskName()); + EnsureAutoRunTaskCanBeRemoved( + ts, + GetLegacyPathScopedAutoRunTaskName(), + ignoreTaskScopedToAnotherUser: true); + EnsureAutoRunTaskCanBeRemoved( + ts, + GetLegacyAutoRunTaskName(), + ignoreTaskScopedToAnotherUser: true, + allowLegacyProductTask: allowLegacyProductTask); DeleteAutoRunTaskIfReplaceable(ts, GetAutoRunTaskName()); DeleteAutoRunTaskIfReplaceable(ts, GetLegacyPathScopedAutoRunTaskName(), true); - DeleteAutoRunTaskIfReplaceable(ts, GetLegacyAutoRunTaskName(), true); + DeleteAutoRunTaskIfReplaceable( + ts, + GetLegacyAutoRunTaskName(), + ignoreTaskScopedToAnotherUser: true, + allowLegacyProductTask: allowLegacyProductTask); } } catch (Exception ex) @@ -981,14 +1124,15 @@ private static bool IsReparsePointOrUnreadable(string path, string label, out st private static void DeleteAutoRunTaskIfReplaceable( TaskService ts, string taskName, - bool ignoreTaskScopedToAnotherUser = false) + bool ignoreTaskScopedToAnotherUser = false, + bool allowLegacyProductTask = false) { using (var task = FindRootAutoRunTask(ts, taskName)) { if (task == null) return; - if (!IsTaskReplaceableByCurrentExecutable(task)) + if (!IsTaskReplaceableByCurrentExecutable(task, allowLegacyProductTask)) { if (ignoreTaskScopedToAnotherUser && IsTaskScopedToAnotherUser(task.Definition)) return; @@ -1001,6 +1145,24 @@ private static void DeleteAutoRunTaskIfReplaceable( ts.RootFolder.DeleteTask(taskName, false); } + private static void EnsureAutoRunTaskCanBeRemoved( + TaskService taskService, + string taskName, + bool ignoreTaskScopedToAnotherUser = false, + bool allowLegacyProductTask = false) + { + using (var task = FindRootAutoRunTask(taskService, taskName)) + { + if (task == null || + IsTaskReplaceableByCurrentExecutable(task, allowLegacyProductTask) || + ignoreTaskScopedToAnotherUser && IsTaskScopedToAnotherUser(task.Definition)) + return; + + throw new InvalidOperationException( + $"Autorun task '{taskName}' changed or belongs to another executable and cannot be modified safely."); + } + } + private static string TryDeleteNewAutoRunTaskAfterMigrationFailure() { try @@ -1062,12 +1224,19 @@ internal static bool IsRootAutoRunTaskPath(string taskPath, string taskName) string.Equals(taskPath, $@"\{taskName}", StringComparison.OrdinalIgnoreCase); } - private static bool IsTaskReplaceableByCurrentExecutable(Microsoft.Win32.TaskScheduler.Task task) + private static bool IsTaskReplaceableByCurrentExecutable( + Microsoft.Win32.TaskScheduler.Task task, + bool allowLegacyProductTask = false) { return task != null && - IsTaskDefinitionReplaceableByExecutable( - task.Definition, - Program.ApplicationLauncherPath); + (IsTaskDefinitionReplaceableByExecutable( + task.Definition, + Program.ApplicationLauncherPath) || + allowLegacyProductTask && + IsLegacyAutoRunTaskDefinitionMigratable( + task.Definition, + Program.NativeLauncherFileName, + "Auto start for " + GetAppName())); } internal static bool IsTaskScopedToAnotherUser(TaskDefinition definition) @@ -1111,7 +1280,7 @@ private static bool IsTaskDefinitionOwnedByExecutable(TaskDefinition definition, !settings.AllowDemandStart || settings.DeleteExpiredTaskAfter != TimeSpan.Zero || settings.Priority != AutoRunTaskPriorityClass || - settings.Volatile || + IsAutoRunTaskVolatile(settings) || settings.DisallowStartOnRemoteAppSession || !settings.RunOnlyIfLoggedOn || settings.IdleSettings == null || @@ -1139,6 +1308,16 @@ private static bool IsTaskDefinitionOwnedByExecutable(TaskDefinition definition, !logonTrigger.Repetition.StopAtDurationEnd; } + internal static bool IsAutoRunTaskVolatile(TaskSettings settings) + { + if (settings == null) throw new ArgumentNullException(nameof(settings)); + + // ITaskSettings3.Volatile is unavailable before Task Scheduler 2.2. + // Older schemas cannot express a volatile task, so avoid touching that + // interface on Windows 7 and treat the effective value as false. + return settings.Compatibility >= TaskCompatibility.V2_2 && settings.Volatile; + } + private static bool IsAutoRunTaskSecurityCanonical( Microsoft.Win32.TaskScheduler.Task task) { @@ -1216,6 +1395,55 @@ internal static bool IsTaskDefinitionReplaceableByExecutable(TaskDefinition defi IsTaskUserReplaceable(logonTrigger.UserId, currentUserId); } + internal static bool IsLegacyAutoRunTaskDefinitionMigratable( + TaskDefinition definition, + string expectedExecutableFileName, + string expectedDescription) + { + if (definition?.Actions == null || definition.Actions.Count != 1 || + definition.Triggers == null || definition.Triggers.Count != 1 || + definition.Principal == null || + definition.Principal.RunLevel != TaskRunLevel.Highest || + definition.Principal.LogonType != TaskLogonType.InteractiveToken || + definition.RegistrationInfo == null || + !string.Equals( + definition.RegistrationInfo.Description, + expectedDescription, + StringComparison.Ordinal)) + return false; + + var execAction = definition.Actions[0] as ExecAction; + if (execAction == null || + !string.IsNullOrWhiteSpace(execAction.Arguments) || + !string.IsNullOrWhiteSpace(execAction.WorkingDirectory) || + string.IsNullOrWhiteSpace(execAction.Path)) + return false; + + try + { + var legacyPath = execAction.Path.Trim().Trim('"'); + if (!Path.IsPathRooted(legacyPath) || + !string.Equals( + Path.GetFileName(legacyPath), + expectedExecutableFileName, + StringComparison.OrdinalIgnoreCase)) + return false; + } + catch (Exception ex) when (ex is ArgumentException || + ex is NotSupportedException || + ex is PathTooLongException) + { + return false; + } + + if (!(definition.Triggers[0] is LogonTrigger logonTrigger) || !logonTrigger.Enabled) + return false; + + var currentUserId = GetCurrentUserId(); + return IsTaskUserReplaceable(definition.Principal.UserId, currentUserId) && + IsTaskUserReplaceable(logonTrigger.UserId, currentUserId); + } + private static string GetCurrentUserId() { using (var identity = WindowsIdentity.GetCurrent()) @@ -1312,9 +1540,10 @@ internal async System.Threading.Tasks.Task ApplyAutoRunChangeAsync() return true; return await ExecuteAutoRunMutationAsync( - requestedAutoRun - ? AutoRunHelperOperation.Enable - : AutoRunHelperOperation.Disable, + GetAutoRunMutationOperation( + requestedAutoRun, + _hasLegacyProductTaskToMigrate && + _legacyProductTaskMigrationApproved), _lifetimeCancellation.Token) .ConfigureAwait(false); } @@ -1330,13 +1559,28 @@ internal async System.Threading.Tasks.Task RollbackAutoRunChangeAsync() return true; return await ExecuteAutoRunMutationAsync( - initialAutoRun - ? AutoRunHelperOperation.Enable - : AutoRunHelperOperation.Disable, + GetAutoRunMutationOperation( + initialAutoRun, + _hasLegacyProductTaskToMigrate && + _legacyProductTaskMigrationApproved), _lifetimeCancellation.Token) .ConfigureAwait(false); } + internal static AutoRunHelperOperation GetAutoRunMutationOperation( + bool enable, + bool migrateLegacyProductTask) + { + if (enable) + return migrateLegacyProductTask + ? AutoRunHelperOperation.EnableMigratingLegacyTask + : AutoRunHelperOperation.Enable; + + return migrateLegacyProductTask + ? AutoRunHelperOperation.DisableMigratingLegacyTask + : AutoRunHelperOperation.Disable; + } + internal async System.Threading.Tasks.Task CommitAutoRunChangeAsync() { var shortcutPath = GetLegacyStartupShortcutPathForCommit( @@ -1412,7 +1656,9 @@ private bool TryCaptureAutoRunChange(out bool initialAutoRun, out bool requested requestedAutoRun, _initialAutoRunUsesPathScopedTask, _hasUnverifiedLegacyShortcut, - _legacyShortcutMigrationApproved); + _legacyShortcutMigrationApproved, + _hasLegacyProductTaskToMigrate, + _legacyProductTaskMigrationApproved); } internal static bool ShouldApplyAutoRunChange( @@ -1420,15 +1666,32 @@ internal static bool ShouldApplyAutoRunChange( bool requestedAutoRun, bool initialUsesPathScopedTask, bool hasUnverifiedLegacyShortcut, - bool migrationApproved) + bool migrationApproved, + bool hasLegacyProductTaskToMigrate = false, + bool legacyProductTaskMigrationApproved = false) { if (!IsKnownAutoRunStatus(initialStatus) || - hasUnverifiedLegacyShortcut && !migrationApproved) + hasUnverifiedLegacyShortcut && !migrationApproved || + hasLegacyProductTaskToMigrate && !legacyProductTaskMigrationApproved) return false; var initialAutoRun = IsEnabledAutoRunStatus(initialStatus); return initialAutoRun != requestedAutoRun || - requestedAutoRun && !initialUsesPathScopedTask; + requestedAutoRun && !initialUsesPathScopedTask || + hasLegacyProductTaskToMigrate && legacyProductTaskMigrationApproved; + } + + internal static bool ShouldOfferLegacyProductTaskMigration( + bool hasLegacyProductTaskToMigrate, + bool legacyProductTaskMigrationApproved, + AutoRunStatus initialStatus, + bool hasUnverifiedLegacyShortcut, + bool legacyShortcutMigrationApproved) + { + return hasLegacyProductTaskToMigrate && + !legacyProductTaskMigrationApproved && + IsKnownAutoRunStatus(initialStatus) && + (!hasUnverifiedLegacyShortcut || legacyShortcutMigrationApproved); } private void OnSaveClick(object sender, EventArgs e) @@ -1441,6 +1704,9 @@ private void OnSaveClick(object sender, EventArgs e) if (!IsKnownAutoRunStatus(_initialAutoRunStatus)) requestedAction = "remove only the opaque legacy file after all settings commit; conflicting or unreadable task entries will remain unchanged"; + else if (_hasLegacyProductTaskToMigrate) + requestedAction = + "approve removal of the opaque legacy file after all settings commit; any required elevated-task migration will be confirmed separately"; else if (chkAutorun.Checked != initialAutoRun) requestedAction = chkAutorun.Checked ? "enable WireSock UI autorun with a protected highest-privilege task and remove the opaque legacy file after all settings commit" @@ -1474,6 +1740,38 @@ private void OnSaveClick(object sender, EventArgs e) } } + if (ShouldOfferLegacyProductTaskMigration( + _hasLegacyProductTaskToMigrate, + _legacyProductTaskMigrationApproved, + _initialAutoRunStatus, + _hasUnverifiedLegacyShortcut, + _legacyShortcutMigrationApproved)) + { + var initialAutoRun = IsEnabledAutoRunStatus(_initialAutoRunStatus); + var result = MessageBox.Show( + "WireSock UI found an older elevated autorun task for this user. It has the historical WireSock UI definition but launches WireSockUI.exe from another installation path." + + $"{Environment.NewLine}{Environment.NewLine}Select Yes to apply the selected autorun setting to this installation and remove the older task when settings are saved." + + $"{Environment.NewLine}Select No to leave the older task unchanged and save the other settings." + + $"{Environment.NewLine}Select Cancel to return to Settings.", + Resources.TunnelErrorTitle, + MessageBoxButtons.YesNoCancel, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button3); + if (result == DialogResult.Cancel) + return; + + if (result == DialogResult.Yes) + { + _legacyProductTaskMigrationApproved = true; + } + else + { + // Do not persist a checkbox change whose corresponding legacy + // task mutation was declined. + chkAutorun.Checked = initialAutoRun; + } + } + DialogResult = DialogResult.OK; Close(); } diff --git a/WireSockUI/Notifications/Notifications.cs b/WireSockUI/Notifications/Notifications.cs index 093d8d9..d64adfc 100644 --- a/WireSockUI/Notifications/Notifications.cs +++ b/WireSockUI/Notifications/Notifications.cs @@ -7,6 +7,7 @@ using System.Windows.Forms; using Windows.Data.Xml.Dom; using Windows.UI.Notifications; +using WireSockUI.Forms; using WireSockUI.Native; using WireSockUI.Properties; @@ -295,6 +296,12 @@ private static void Notification_Activated(ToastNotification sender, object args if (form.IsDisposed || form.Disposing) return; + if (form is FrmMain mainForm) + { + mainForm.TryShowMainWindow(); + return; + } + form.ShowInTaskbar = true; form.Show(); form.WindowState = FormWindowState.Normal; diff --git a/docs/release-notes/release-v0.3.13.md b/docs/release-notes/release-v0.3.13.md new file mode 100644 index 0000000..9362cff --- /dev/null +++ b/docs/release-notes/release-v0.3.13.md @@ -0,0 +1,41 @@ +## WireSock UI 0.3.13 + +This maintenance release fixes elevated autorun on Windows 7, safely handles autorun tasks left by older WireSockUI installations, and removes a brief blank-window flash during shutdown. + +### Install or upgrade + +1. Install or update the matching-architecture [WireSock Secure Connect CLI/SDK](https://www.wiresock.net/). +2. Download the MSI below that matches your Windows version and architecture. +3. Close WireSockUI, then run the installer as an administrator. It upgrades an existing installation in place. + +> [!IMPORTANT] +> WireSockUI releases are intentionally unsigned. Windows may display **Unknown publisher**. Verify the MSI with its adjacent `.sha256` file before installation. + +### Choose a package + +| System | Package | +| --- | --- | +| Windows 7 SP1, x86 or x64 | Matching `no-uwp` MSI | +| Windows 8.1 or later, x86 or x64 | Matching `uwp` MSI for notifications and update checks, or `no-uwp` | +| Windows 11 on Arm | `win-arm64-uwp` or `win-arm64-no-uwp` | + +### What’s fixed + +- Restored **Run when Windows starts** on Windows 7 by generating a Task Scheduler 2.1/schema 1.3 definition instead of using Windows 8-only task settings. +- Recognized the exact historical WireSockUI autorun-task shape when it points to an older installation path, and offered an explicit, safety-checked migration to the current installation. +- Continued to leave foreign or modified scheduled tasks untouched. +- Preserved the real Task Scheduler error when an elevated helper fails, rather than misreporting every verified failure as a timeout. +- Prevented a partially completed legacy-task migration from being reported as successful. +- Prevented the main window or a notification activation from briefly recreating a blank window while WireSockUI is closing. +- Added regression coverage for Windows 7 task definitions, legacy-task migration and consent, timeout verification, and shutdown activation guards. + +The Windows 7 x64 `no-uwp` package was manually validated against both reported autorun scenarios before release. + +### Notes + +- WireSockUI requires the WireSock Secure Connect CLI/SDK and administrator privileges. +- The `no-uwp` x86/x64 packages support Windows 7 SP1; UWP packages require Windows 8.1 or later. +- All six MSIs and the modules inside them are unsigned by policy. +- Each MSI is accompanied by validation metadata and an SPDX SBOM. Every MSI, validation document, and SBOM has a SHA-256 sidecar and a GitHub provenance attestation. + +**Full changelog:** [release-v0.3.12...release-v0.3.13](https://github.com/wiresock/WireSockUI/compare/release-v0.3.12...release-v0.3.13)