11using Daybreak . Models ;
22using Daybreak . Services . Logging ;
33using Daybreak . Utils ;
4+ using Microsoft . Win32 ;
45using System ;
56using System . Collections . Generic ;
67using System . Diagnostics ;
@@ -15,15 +16,21 @@ namespace Daybreak.Services.Updater
1516{
1617 public sealed class ApplicationUpdater : IApplicationUpdater
1718 {
19+ private const string ExecutionPolicyKey = "ExecutionPolicy" ;
20+ private const string UpdatedKey = "Updating" ;
21+ private const string RegistryKey = "Daybreak" ;
1822 private const string ExtractAndRunPs1 = "ExtractAndRun.ps1" ;
1923 private const string TempFile = "tempfile.zip" ;
2024 private const string VersionTag = "{VERSION}" ;
2125 private const string InputFileTag = "{INPUTFILE}" ;
22- private const string OutputPathTag = "{OUTPUTPATh}" ;
26+ private const string OutputPathTag = "{OUTPUTPATH}" ;
27+ private const string ExecutionPolicyTag = "{EXECUTIONPOLICY}" ;
28+ private const string ProcessIdTag = "{PROCESSID}" ;
2329 private const string Url = "https://github.com/AlexMacocian/Daybreak/releases/latest" ;
2430 private const string DownloadUrl = $ "https://github.com/AlexMacocian/Daybreak/releases/download/v{ VersionTag } /Daybreakv{ VersionTag } .zip";
25- private const string SetExecutionPolicy = $ "Set-ExecutionPolicy RemoteSigned -Scope CurrentUser";
26- private const string DelayCommand = "Start-Sleep -m 3000" ;
31+ private const string GetExecutionPolicyCommand = "Get-ExecutionPolicy -Scope CurrentUser" ;
32+ private const string SetExecutionPolicyCommand = $ "Set-ExecutionPolicy { ExecutionPolicyTag } -Scope CurrentUser";
33+ private const string WaitCommand = $ "Wait-Process -Id { ProcessIdTag } ";
2734 private const string ExtractCommandTemplate = $ "Expand-Archive -Path '{ InputFileTag } ' -DestinationPath '{ OutputPathTag } ' -Force";
2835 private const string RunClientCommand = @".\Daybreak.exe" ;
2936 private const string RemoveTempFile = $ "Remove-item { TempFile } ";
@@ -90,19 +97,125 @@ public async Task<bool> UpdateAvailable()
9097 var maybeLatestVersion = await this . GetLatestVersion ( ) ;
9198 return maybeLatestVersion . Switch (
9299 onSome : latestVersion => string . Compare ( version , latestVersion , true ) < 0 ,
93- onNone : ( ) =>
100+ onNone : ( ) =>
94101 {
95102 this . logger . LogWarning ( "Failed to retrieve latest version" ) ;
96103 return false ;
97104 } ) . ExtractValue ( ) ;
98105 }
99106
100107 public void FinalizeUpdate ( )
108+ {
109+ var maybeExecutionPolicy = this . RetrieveExecutionPolicy ( ) ;
110+ maybeExecutionPolicy . DoAny (
111+ onNone : ( ) =>
112+ {
113+ throw new InvalidOperationException ( "Failed to retrieve execution policy" ) ;
114+ } ) ;
115+
116+ var executionPolicy = maybeExecutionPolicy . ExtractValue ( ) ;
117+ if ( executionPolicy is not ExecutionPolicies . Bypass ||
118+ executionPolicy is not ExecutionPolicies . Unrestricted )
119+ {
120+ this . logger . LogInformation ( $ "Execution policy is set to { executionPolicy } . Setting to { ExecutionPolicies . Bypass } ") ;
121+ }
122+
123+ SaveExecutionPolicyValueToRegistry ( executionPolicy ) ;
124+ MarkUpdateInRegistry ( ) ;
125+ this . SetExecutionPolicy ( ExecutionPolicies . Bypass ) ;
126+ this . LaunchExtractor ( ) ;
127+ }
128+
129+ public void OnStartup ( )
130+ {
131+ if ( UpdateMarkedInRegistry ( ) )
132+ {
133+ UnmarkUpdateInRegistry ( ) ;
134+ var maybeExecutionPolicy = LoadExecutionPolicyValueFromRegistry ( ) ;
135+ maybeExecutionPolicy . Do (
136+ onSome : policy =>
137+ {
138+ SetExecutionPolicy ( policy ) ;
139+ } ,
140+ onNone : ( ) =>
141+ {
142+ throw new InvalidOperationException ( "Found update marked in registry but no execution policy" ) ;
143+ } ) ;
144+ }
145+ }
146+
147+ public void OnClosing ( )
148+ {
149+ }
150+
151+ private async Task < Optional < string > > GetLatestVersion ( )
152+ {
153+ using var response = await this . httpClient . GetAsync ( Url ) ;
154+ if ( response . IsSuccessStatusCode )
155+ {
156+ var versionTag = response . RequestMessage . RequestUri . ToString ( ) . Split ( '/' ) . Last ( ) . TrimStart ( 'v' ) ;
157+ return versionTag ;
158+ }
159+
160+ return Optional . None < string > ( ) ;
161+ }
162+
163+ private Optional < ExecutionPolicies > RetrieveExecutionPolicy ( )
164+ {
165+ var process = new Process ( )
166+ {
167+ StartInfo = new ProcessStartInfo
168+ {
169+ FileName = "powershell" ,
170+ Arguments = GetExecutionPolicyCommand ,
171+ UseShellExecute = false ,
172+ RedirectStandardError = true ,
173+ RedirectStandardInput = true ,
174+ RedirectStandardOutput = true
175+ }
176+ } ;
177+ process . Start ( ) ;
178+ this . logger . LogInformation ( "Checking current execution policy" ) ;
179+ var output = process . StandardOutput . ReadToEnd ( ) ;
180+ if ( ! Enum . TryParse ( typeof ( ExecutionPolicies ) , output , out var executionPolicy ) )
181+ {
182+ var error = process . StandardError . ReadToEnd ( ) ;
183+ this . logger . LogError ( $ "Failed to retrieve current user execution policy. Stdout: { output } . Stderr: { error } ") ;
184+ return Optional . None < ExecutionPolicies > ( ) ;
185+ }
186+
187+ return executionPolicy . Cast < ExecutionPolicies > ( ) ;
188+ }
189+
190+ private void SetExecutionPolicy ( ExecutionPolicies executionPolicy )
191+ {
192+ var process = new Process ( )
193+ {
194+ StartInfo = new ProcessStartInfo
195+ {
196+ FileName = "powershell" ,
197+ Arguments = SetExecutionPolicyCommand . Replace ( ExecutionPolicyTag , executionPolicy . ToString ( ) ) ,
198+ UseShellExecute = false ,
199+ RedirectStandardError = true ,
200+ RedirectStandardInput = true ,
201+ RedirectStandardOutput = true
202+ }
203+ } ;
204+ process . Start ( ) ;
205+ this . logger . LogInformation ( $ "Setting execution policy to { executionPolicy } ") ;
206+ var output = process . StandardOutput . ReadToEnd ( ) ;
207+ if ( ! string . IsNullOrWhiteSpace ( output ) )
208+ {
209+ var error = process . StandardError . ReadToEnd ( ) ;
210+ throw new InvalidOperationException ( $ "Failed to set execution policy to { executionPolicy } . Stdout: { output } . Stderr: { error } ") ;
211+ }
212+ }
213+
214+ private void LaunchExtractor ( )
101215 {
102216 File . WriteAllLines ( ExtractAndRunPs1 , new List < string > ( )
103217 {
104- SetExecutionPolicy ,
105- DelayCommand ,
218+ WaitCommand . Replace ( ProcessIdTag , Environment . ProcessId . ToString ( ) ) ,
106219 ExtractCommandTemplate
107220 . Replace ( InputFileTag , Path . GetFullPath ( TempFile ) )
108221 . Replace ( OutputPathTag , Directory . GetCurrentDirectory ( ) ) ,
@@ -124,22 +237,90 @@ public void FinalizeUpdate()
124237 WorkingDirectory = Directory . GetCurrentDirectory ( )
125238 } ,
126239 } ;
240+ this . logger . LogInformation ( "Created extractor script. Attempting to launch powershell" ) ;
127241 if ( process . Start ( ) is false )
128242 {
129243 throw new InvalidOperationException ( "Failed to create and start powershell script" ) ;
130244 }
131245 }
132246
133- private async Task < Optional < string > > GetLatestVersion ( )
247+ private static void MarkUpdateInRegistry ( )
134248 {
135- using var response = await this . httpClient . GetAsync ( Url ) ;
136- if ( response . IsSuccessStatusCode )
249+ var homeRegistryKey = GetOrCreateHomeKey ( ) ;
250+ homeRegistryKey . SetValue ( UpdatedKey , true ) ;
251+ homeRegistryKey . Close ( ) ;
252+ }
253+
254+ private static void UnmarkUpdateInRegistry ( )
255+ {
256+ var homeRegistryKey = GetOrCreateHomeKey ( ) ;
257+ homeRegistryKey . SetValue ( UpdatedKey , false ) ;
258+ homeRegistryKey . Close ( ) ;
259+ }
260+
261+ private static bool UpdateMarkedInRegistry ( )
262+ {
263+ var homeRegistryKey = GetOrCreateHomeKey ( ) ;
264+ var update = homeRegistryKey . GetValue ( UpdatedKey ) ;
265+ homeRegistryKey . Close ( ) ;
266+ if ( update is string updateString )
137267 {
138- var versionTag = response . RequestMessage . RequestUri . ToString ( ) . Split ( '/' ) . Last ( ) . TrimStart ( 'v' ) ;
139- return versionTag ;
268+ if ( bool . TryParse ( updateString , out var updateValue ) )
269+ {
270+ return updateValue ;
271+ }
272+ else
273+ {
274+ throw new InvalidOperationException ( $ "Found update value { updateString } in registry") ;
275+ }
140276 }
141277
142- return Optional . None < string > ( ) ;
278+ return false ;
279+ }
280+
281+ private static void SaveExecutionPolicyValueToRegistry ( ExecutionPolicies executionPolicy )
282+ {
283+ var homeRegistryKey = GetOrCreateHomeKey ( ) ;
284+ homeRegistryKey . SetValue ( ExecutionPolicyKey , executionPolicy . ToString ( ) ) ;
285+ homeRegistryKey . Close ( ) ;
286+ }
287+
288+ private static Optional < ExecutionPolicies > LoadExecutionPolicyValueFromRegistry ( )
289+ {
290+ var homeRegistryKey = GetOrCreateHomeKey ( ) ;
291+ var executionPolicy = homeRegistryKey . GetValue ( ExecutionPolicyKey ) ;
292+ homeRegistryKey . Close ( ) ;
293+
294+ if ( executionPolicy is null )
295+ {
296+ return Optional . None < ExecutionPolicies > ( ) ;
297+ }
298+ else if ( executionPolicy is string executionPolicyString )
299+ {
300+ if ( Enum . TryParse < ExecutionPolicies > ( executionPolicyString , out var executionPolicyValue ) )
301+ {
302+ return executionPolicyValue ;
303+ }
304+ else
305+ {
306+ throw new InvalidOperationException ( $ "Found execution policy with value { executionPolicy } ") ;
307+ }
308+ }
309+ else
310+ {
311+ throw new InvalidOperationException ( $ "Found execution policy of type { executionPolicy . GetType ( ) } .") ;
312+ }
313+ }
314+
315+ private static RegistryKey GetOrCreateHomeKey ( )
316+ {
317+ var homeRegistryKey = Registry . CurrentUser . OpenSubKey ( "Software" , true ) . OpenSubKey ( RegistryKey , true ) ;
318+ if ( homeRegistryKey is null )
319+ {
320+ homeRegistryKey = Registry . CurrentUser . OpenSubKey ( "Software" , true ) . CreateSubKey ( RegistryKey , true ) ;
321+ }
322+
323+ return homeRegistryKey ;
143324 }
144325 }
145326}
0 commit comments