@@ -2,12 +2,23 @@ namespace SwitchifyPc.Core.Startup;
22
33public sealed record StartupRegistration ( string ExpectedCommand , string ? RegisteredCommand , string StartupApproved ) ;
44
5+ public sealed record StartupTaskRegistration (
6+ string TaskName ,
7+ bool Exists ,
8+ bool Enabled ,
9+ string ExpectedExecutablePath ,
10+ string ? RegisteredExecutablePath ,
11+ IReadOnlyList < string > ExpectedArguments ,
12+ IReadOnlyList < string > RegisteredArguments ,
13+ string ? LastRunResult ) ;
14+
515public sealed record SystemStartupSettings (
616 bool Supported ,
717 bool StartWithSystem ,
818 bool StartsHidden ,
919 string ? Reason ,
10- StartupRegistration ? Registration = null ) ;
20+ StartupRegistration ? Registration = null ,
21+ StartupTaskRegistration ? TaskRegistration = null ) ;
1122
1223public interface IStartupRegistry
1324{
@@ -18,6 +29,20 @@ public interface IStartupRegistry
1829
1930public sealed record StartupRegistrySnapshot ( string ? Command , string StartupApproved ) ;
2031
32+ public sealed record StartupTaskSnapshot (
33+ bool Exists ,
34+ bool Enabled ,
35+ string ? ExecutablePath ,
36+ IReadOnlyList < string > Arguments ,
37+ string ? LastRunResult ) ;
38+
39+ public interface IStartupTask
40+ {
41+ Task < StartupTaskSnapshot > GetAsync ( string taskName ) ;
42+ Task SetAsync ( string taskName , string executablePath , IReadOnlyList < string > args ) ;
43+ Task DeleteAsync ( string taskName ) ;
44+ }
45+
2146public interface ISystemStartupSettingsService
2247{
2348 Task < SystemStartupSettings > GetSettingsAsync ( ) ;
@@ -29,26 +54,30 @@ public sealed class SystemStartupService : ISystemStartupSettingsService
2954{
3055 public const string StartHiddenArg = "--start-hidden" ;
3156 public const string StartupValueName = "app.switchify.pc" ;
57+ public const string StartupTaskName = "Switchify PC" ;
3258
3359 private readonly string platform ;
3460 private readonly bool isPackaged ;
3561 private readonly string executablePath ;
36- private readonly IStartupRegistry startupRegistry ;
62+ private readonly IStartupTask startupTask ;
63+ private readonly IStartupRegistry legacyStartupRegistry ;
3764 private readonly Func < string , IReadOnlyList < string > , string > startupCommandFor ;
3865 private readonly Action < string > warn ;
3966
4067 public SystemStartupService (
4168 string platform ,
4269 bool isPackaged ,
4370 string executablePath ,
44- IStartupRegistry startupRegistry ,
71+ IStartupTask startupTask ,
72+ IStartupRegistry legacyStartupRegistry ,
4573 Func < string , IReadOnlyList < string > , string > startupCommandFor ,
4674 Action < string > ? warn = null )
4775 {
4876 this . platform = platform ;
4977 this . isPackaged = isPackaged ;
5078 this . executablePath = executablePath ;
51- this . startupRegistry = startupRegistry ;
79+ this . startupTask = startupTask ;
80+ this . legacyStartupRegistry = legacyStartupRegistry ;
5281 this . startupCommandFor = startupCommandFor ;
5382 this . warn = warn ?? Console . WriteLine ;
5483 }
@@ -63,30 +92,66 @@ public async Task<SystemStartupSettings> GetSettingsAsync()
6392 if ( ! IsSupported ( ) ) return UnsupportedSettings ( ) ;
6493
6594 string expectedCommand = ExpectedCommand ( ) ;
95+ IReadOnlyList < string > expectedArguments = [ StartHiddenArg ] ;
96+ StartupTaskSnapshot task = await GetTaskSafelyAsync ( ) ;
6697 StartupRegistrySnapshot entry = await GetRegistryEntrySafelyAsync ( ) ;
98+ bool taskMatches = IsHealthyTask ( task , expectedArguments ) ;
99+
67100 return new SystemStartupSettings (
68101 Supported : true ,
69- StartWithSystem : entry . Command == expectedCommand && entry . StartupApproved != "disabled" ,
102+ StartWithSystem : taskMatches ,
70103 StartsHidden : true ,
71104 Reason : null ,
72- Registration : new StartupRegistration ( expectedCommand , entry . Command , entry . StartupApproved ) ) ;
105+ Registration : new StartupRegistration ( expectedCommand , entry . Command , entry . StartupApproved ) ,
106+ TaskRegistration : new StartupTaskRegistration (
107+ StartupTaskName ,
108+ task . Exists ,
109+ task . Enabled ,
110+ executablePath ,
111+ task . ExecutablePath ,
112+ expectedArguments ,
113+ task . Arguments ,
114+ task . LastRunResult ) ) ;
73115 }
74116
75117 public async Task < SystemStartupSettings > SetStartWithSystemAsync ( bool enabled )
76118 {
77119 if ( ! IsSupported ( ) ) return UnsupportedSettings ( ) ;
78120 if ( enabled )
79121 {
80- await startupRegistry . SetEntryAsync ( StartupValueName , ExpectedCommand ( ) ) ;
122+ await startupTask . SetAsync ( StartupTaskName , executablePath , [ StartHiddenArg ] ) ;
123+ await DeleteLegacyRegistrySafelyAsync ( ) ;
81124 }
82125 else
83126 {
84- await startupRegistry . DeleteEntryAsync ( StartupValueName ) ;
127+ await startupTask . DeleteAsync ( StartupTaskName ) ;
128+ await DeleteLegacyRegistrySafelyAsync ( ) ;
85129 }
86130
87131 return await GetSettingsAsync ( ) ;
88132 }
89133
134+ public async Task RepairLegacyStartupRegistrationAsync ( )
135+ {
136+ if ( ! IsSupported ( ) ) return ;
137+
138+ IReadOnlyList < string > expectedArguments = [ StartHiddenArg ] ;
139+ StartupTaskSnapshot task = await GetTaskSafelyAsync ( ) ;
140+ StartupRegistrySnapshot legacy = await GetRegistryEntrySafelyAsync ( ) ;
141+
142+ if ( IsHealthyTask ( task , expectedArguments ) )
143+ {
144+ await DeleteLegacyRegistrySafelyAsync ( ) ;
145+ return ;
146+ }
147+
148+ if ( legacy . Command == ExpectedCommand ( ) && legacy . StartupApproved != "disabled" )
149+ {
150+ await startupTask . SetAsync ( StartupTaskName , executablePath , expectedArguments ) ;
151+ await DeleteLegacyRegistrySafelyAsync ( ) ;
152+ }
153+ }
154+
90155 private bool IsSupported ( )
91156 {
92157 return platform == "win32" && isPackaged ;
@@ -97,11 +162,32 @@ private string ExpectedCommand()
97162 return startupCommandFor ( executablePath , [ StartHiddenArg ] ) ;
98163 }
99164
165+ private bool IsHealthyTask ( StartupTaskSnapshot task , IReadOnlyList < string > expectedArguments )
166+ {
167+ return task . Exists &&
168+ task . Enabled &&
169+ string . Equals ( task . ExecutablePath , executablePath , StringComparison . Ordinal ) &&
170+ task . Arguments . SequenceEqual ( expectedArguments , StringComparer . Ordinal ) ;
171+ }
172+
173+ private async Task < StartupTaskSnapshot > GetTaskSafelyAsync ( )
174+ {
175+ try
176+ {
177+ return await startupTask . GetAsync ( StartupTaskName ) ;
178+ }
179+ catch ( Exception error )
180+ {
181+ warn ( error . Message ) ;
182+ return new StartupTaskSnapshot ( false , false , null , [ ] , null ) ;
183+ }
184+ }
185+
100186 private async Task < StartupRegistrySnapshot > GetRegistryEntrySafelyAsync ( )
101187 {
102188 try
103189 {
104- return await startupRegistry . GetEntryAsync ( StartupValueName ) ;
190+ return await legacyStartupRegistry . GetEntryAsync ( StartupValueName ) ;
105191 }
106192 catch ( Exception error )
107193 {
@@ -110,6 +196,18 @@ private async Task<StartupRegistrySnapshot> GetRegistryEntrySafelyAsync()
110196 }
111197 }
112198
199+ private async Task DeleteLegacyRegistrySafelyAsync ( )
200+ {
201+ try
202+ {
203+ await legacyStartupRegistry . DeleteEntryAsync ( StartupValueName ) ;
204+ }
205+ catch ( Exception error )
206+ {
207+ warn ( error . Message ) ;
208+ }
209+ }
210+
113211 private SystemStartupSettings UnsupportedSettings ( )
114212 {
115213 return new SystemStartupSettings (
0 commit comments