Skip to content

Commit fe5e38b

Browse files
author
Alexandru Macocian
committed
Change update process to modify and restore execution policy before and after update.
Change update process to wait for the client to close instead of a static wait.
1 parent 9fd7d8b commit fe5e38b

File tree

5 files changed

+210
-14
lines changed

5 files changed

+210
-14
lines changed

Daybreak/Configuration/ProjectConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public static void RegisterLifetimeServices(IApplicationLifetimeProducer applica
3939

4040
applicationLifetimeProducer.RegisterService<ILoggingDatabase>();
4141
applicationLifetimeProducer.RegisterService<IScreenshotProvider>();
42+
applicationLifetimeProducer.RegisterService<IApplicationUpdater>();
4243
}
4344
public static void RegisterViews(IViewProducer viewProducer)
4445
{

Daybreak/Daybreak.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
1010
<LangVersion>preview</LangVersion>
1111
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
12-
<Version>0.2.1</Version>
12+
<Version>0.2.2</Version>
1313
</PropertyGroup>
1414

1515
<ItemGroup>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Daybreak.Models
2+
{
3+
public enum ExecutionPolicies
4+
{
5+
AllSigned,
6+
Bypass,
7+
Default,
8+
RemoteSigned,
9+
Restricted,
10+
Undefined,
11+
Unrestricted
12+
}
13+
}

Daybreak/Services/Updater/ApplicationUpdater.cs

Lines changed: 193 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Daybreak.Models;
22
using Daybreak.Services.Logging;
33
using Daybreak.Utils;
4+
using Microsoft.Win32;
45
using System;
56
using System.Collections.Generic;
67
using 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
}

Daybreak/Services/Updater/IApplicationUpdater.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
using Daybreak.Models;
2+
using Daybreak.Services.ApplicationLifetime;
23
using System.Threading.Tasks;
34

45
namespace Daybreak.Services.Updater
56
{
6-
public interface IApplicationUpdater
7+
public interface IApplicationUpdater : IApplicationLifetimeService
78
{
89
string CurrentVersion { get; }
910
void FinalizeUpdate();

0 commit comments

Comments
 (0)