diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index 99ed607..39894e6 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -20,6 +20,10 @@ jobs: with: dotnet-version: '8.0.x' + - name: Run tests + shell: pwsh + run: dotnet test BASpark.sln -c Release --verbosity normal + - name: Build Single EXE shell: pwsh run: | @@ -51,4 +55,4 @@ jobs: with: name: BASpark-Executable-Preview path: src/publish_single/ - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/BASpark.sln b/BASpark.sln index 478c761..dafab2b 100644 --- a/BASpark.sln +++ b/BASpark.sln @@ -6,6 +6,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BASpark", "src\BASpark.csproj", "{F43895B0-1503-A7FF-2A92-DDB1081972A3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BASpark.Tests", "tests\BASpark.Tests\BASpark.Tests.csproj", "{2B0161F9-72DB-431B-8E9A-5D192D62995B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -16,12 +20,17 @@ Global {F43895B0-1503-A7FF-2A92-DDB1081972A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {F43895B0-1503-A7FF-2A92-DDB1081972A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {F43895B0-1503-A7FF-2A92-DDB1081972A3}.Release|Any CPU.Build.0 = Release|Any CPU + {2B0161F9-72DB-431B-8E9A-5D192D62995B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B0161F9-72DB-431B-8E9A-5D192D62995B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B0161F9-72DB-431B-8E9A-5D192D62995B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B0161F9-72DB-431B-8E9A-5D192D62995B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {F43895B0-1503-A7FF-2A92-DDB1081972A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {2B0161F9-72DB-431B-8E9A-5D192D62995B} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B27D4FF-6E40-4998-BBE8-EE8897314A93} diff --git a/src/AutoStartManager.cs b/src/AutoStartManager.cs new file mode 100644 index 0000000..8e98ac2 --- /dev/null +++ b/src/AutoStartManager.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; + +namespace BASpark +{ + public readonly record struct AutoStartPlan(bool RegistryRunEnabled, bool ScheduledTaskEnabled); + + public static class AutoStartManager + { + public const string RunValueName = "BASpark"; + public const string TaskName = "BASparkAutoStart"; + + public static AutoStartPlan CreatePlan(bool autoStart, bool runAsAdmin) + { + if (!autoStart) + { + return new AutoStartPlan(false, false); + } + + return runAsAdmin + ? new AutoStartPlan(false, true) + : new AutoStartPlan(true, false); + } + + public static string BuildRunCommand(string exePath) + { + return $"\"{exePath}\" --autostart"; + } + + public static string? ResolveExecutablePath( + string? processPath, + string? mainModulePath, + string? assemblyLocation, + string baseDirectory) + { + foreach (string? candidate in new[] { processPath, mainModulePath, assemblyLocation }) + { + if (IsExecutablePath(candidate)) + { + return candidate; + } + } + + return string.IsNullOrWhiteSpace(baseDirectory) + ? null + : Path.Combine(baseDirectory, "BASpark.exe"); + } + + private static bool IsExecutablePath(string? path) + { + return !string.IsNullOrWhiteSpace(path) && + path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/ControlPanelWindow.xaml.cs b/src/ControlPanelWindow.xaml.cs index 5400e44..8993773 100644 --- a/src/ControlPanelWindow.xaml.cs +++ b/src/ControlPanelWindow.xaml.cs @@ -856,40 +856,31 @@ private void ApplyAutoStartSettings() bool autoStart = CheckAutoStart.IsChecked == true; bool runAsAdmin = CheckRunAsAdmin.IsChecked == true; - string? exePath = Assembly.GetExecutingAssembly().Location; - if (string.IsNullOrEmpty(exePath)) - { - exePath = Process.GetCurrentProcess().MainModule?.FileName; - } + string? exePath = AutoStartManager.ResolveExecutablePath( + Environment.ProcessPath, + Process.GetCurrentProcess().MainModule?.FileName, + Assembly.GetExecutingAssembly().Location, + AppDomain.CurrentDomain.BaseDirectory); if (string.IsNullOrEmpty(exePath)) return; - string taskName = "BASparkAutoStart"; string regKeyPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"; + AutoStartPlan plan = AutoStartManager.CreatePlan(autoStart, runAsAdmin); try { - using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(regKeyPath, true)) + using (RegistryKey? key = Registry.CurrentUser.CreateSubKey(regKeyPath, true)) { - if (autoStart) + if (plan.RegistryRunEnabled) { - if (runAsAdmin) - { - if (key?.GetValue("BASpark") != null) - { - key.DeleteValue("BASpark", false); - } - ManageTaskScheduler(taskName, exePath, true); - } - else - { - ManageTaskScheduler(taskName, exePath, false); - } + key?.SetValue(AutoStartManager.RunValueName, AutoStartManager.BuildRunCommand(exePath)); } else { - ManageTaskScheduler(taskName, exePath, false); + key?.DeleteValue(AutoStartManager.RunValueName, false); } + + ManageTaskScheduler(AutoStartManager.TaskName, exePath, plan.ScheduledTaskEnabled); } } catch (Exception ex) diff --git a/tests/BASpark.Tests/AutoStartManagerTests.cs b/tests/BASpark.Tests/AutoStartManagerTests.cs new file mode 100644 index 0000000..9a5918d --- /dev/null +++ b/tests/BASpark.Tests/AutoStartManagerTests.cs @@ -0,0 +1,51 @@ +namespace BASpark.Tests; + +public class AutoStartManagerTests +{ + [Fact] + public void CreatePlan_UsesRegistryForNormalAutoStart() + { + AutoStartPlan plan = AutoStartManager.CreatePlan(autoStart: true, runAsAdmin: false); + + Assert.True(plan.RegistryRunEnabled); + Assert.False(plan.ScheduledTaskEnabled); + } + + [Fact] + public void CreatePlan_UsesScheduledTaskForElevatedAutoStart() + { + AutoStartPlan plan = AutoStartManager.CreatePlan(autoStart: true, runAsAdmin: true); + + Assert.False(plan.RegistryRunEnabled); + Assert.True(plan.ScheduledTaskEnabled); + } + + [Fact] + public void CreatePlan_DisablesBothStartupMechanismsWhenAutoStartIsOff() + { + AutoStartPlan plan = AutoStartManager.CreatePlan(autoStart: false, runAsAdmin: true); + + Assert.False(plan.RegistryRunEnabled); + Assert.False(plan.ScheduledTaskEnabled); + } + + [Fact] + public void BuildRunCommand_AddsAutostartArgumentToQuotedExecutablePath() + { + string command = AutoStartManager.BuildRunCommand(@"C:\Program Files\BASpark\BASpark.exe"); + + Assert.Equal(@"""C:\Program Files\BASpark\BASpark.exe"" --autostart", command); + } + + [Fact] + public void ResolveExecutablePath_PrefersProcessExeOverAssemblyDll() + { + string? resolved = AutoStartManager.ResolveExecutablePath( + processPath: @"C:\Apps\BASpark\BASpark.exe", + mainModulePath: @"C:\Apps\BASpark\BASpark.exe", + assemblyLocation: @"C:\Apps\BASpark\BASpark.dll", + baseDirectory: @"C:\Apps\BASpark"); + + Assert.Equal(@"C:\Apps\BASpark\BASpark.exe", resolved); + } +} diff --git a/tests/BASpark.Tests/BASpark.Tests.csproj b/tests/BASpark.Tests/BASpark.Tests.csproj new file mode 100644 index 0000000..3adf019 --- /dev/null +++ b/tests/BASpark.Tests/BASpark.Tests.csproj @@ -0,0 +1,25 @@ + + + + net8.0-windows10.0.19041.0 + enable + enable + false + + + + + + + + + + + + + + + + + +