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)