mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-15 21:09:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c35333cac | ||
|
|
fe5e38b216 | ||
|
|
9fd7d8b62a |
@@ -31,7 +31,6 @@ namespace Daybreak.Configuration
|
||||
serviceProducer.RegisterSingleton<IConfigurationManager, ConfigurationManager>();
|
||||
serviceProducer.RegisterSingleton<IBloogumClient, BloogumClient>();
|
||||
serviceProducer.RegisterSingleton<IApplicationUpdater, ApplicationUpdater>();
|
||||
serviceProducer.RegisterSingleton<CoreWebView2Environment, CoreWebView2Environment>((sp) => TaskExtensions.RunSync(() => CoreWebView2Environment.CreateAsync(null, "BrowserData", null)));
|
||||
}
|
||||
public static void RegisterLifetimeServices(IApplicationLifetimeProducer applicationLifetimeProducer)
|
||||
{
|
||||
@@ -39,6 +38,7 @@ namespace Daybreak.Configuration
|
||||
|
||||
applicationLifetimeProducer.RegisterService<ILoggingDatabase>();
|
||||
applicationLifetimeProducer.RegisterService<IScreenshotProvider>();
|
||||
applicationLifetimeProducer.RegisterService<IApplicationUpdater>();
|
||||
}
|
||||
public static void RegisterViews(IViewProducer viewProducer)
|
||||
{
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<wv2:WebView2 x:Name="WebBrowser" Source="{Binding ElementName=_this, Path=Address, Mode=TwoWay}"></wv2:WebView2>
|
||||
<Grid Grid.Row="1" Background="#80808080">
|
||||
<wv2:WebView2 x:Name="WebBrowser" Source="{Binding ElementName=_this, Path=Address, Mode=TwoWay}"
|
||||
IsEnabled="{Binding ElementName=_this, Path=BrowserSupported, Mode=OneWay}"></wv2:WebView2>
|
||||
<Grid Grid.Row="1" Background="#80808080" IsEnabled="{Binding ElementName=_this, Path=BrowserSupported, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
@@ -51,15 +52,32 @@
|
||||
Grid.Column="1" IsReadOnly="{Binding ElementName=_this, Path=AddressBarReadonly, Mode=OneWay}" Background="Transparent"
|
||||
BorderThickness="1" VerticalAlignment="Center" VerticalContentAlignment="Center"
|
||||
BorderBrush="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
PreviewKeyDown="TextBox_PreviewKeyDown"></TextBox>
|
||||
PreviewKeyDown="TextBox_PreviewKeyDown"
|
||||
IsEnabled="{Binding ElementName=_this, Path=BrowserSupported, Mode=OneWay}"></TextBox>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<local:StarGlyph Height="30" Width="30" Margin="5"
|
||||
Foreground="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
IsEnabled="{Binding ElementName=_this, Path=BrowserSupported, Mode=OneWay}"
|
||||
Clicked="StarGlyph_Clicked" x:Name="FavoriteButton"></local:StarGlyph>
|
||||
<local:MaximizeButton Height="30" Width="30" Margin="5"
|
||||
Foreground="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
|
||||
IsEnabled="{Binding ElementName=_this, Path=BrowserSupported, Mode=OneWay}"
|
||||
Clicked="MaximizeButton_Clicked"></local:MaximizeButton>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Grid Grid.RowSpan="2" Background="Gray"
|
||||
Visibility="{Binding ElementName=_this, Path=BrowserSupported, Mode=OneWay,Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Foreground="White" Text="Browser not supported." FontSize="26" TextWrapping="Wrap"></TextBlock>
|
||||
<TextBlock Foreground="White" Text="Download the Evergreen Bootstrapper from here:" FontSize="26" TextWrapping="Wrap" />
|
||||
<TextBox Background="Transparent" BorderBrush="Transparent" BorderThickness="0"
|
||||
Text="https://go.microsoft.com/fwlink/p/?LinkId=2124703"
|
||||
IsReadOnly="True" Margin="0, 0, 0, 30" FontSize="22" Foreground="Blue"
|
||||
PreviewMouseLeftButtonDown="Hyperlink_PreviewMouseLeftButtonDown" Cursor="Hand"
|
||||
TextWrapping="Wrap"></TextBox>
|
||||
<TextBlock Foreground="White" Text="Restart after the installation." FontSize="26" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using Daybreak.Launch;
|
||||
using Daybreak.Services.Configuration;
|
||||
using Daybreak.Services.Logging;
|
||||
using Daybreak.Utils;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Extensions;
|
||||
@@ -14,16 +18,20 @@ namespace Daybreak.Controls
|
||||
/// </summary>
|
||||
public partial class ChromiumBrowserWrapper : UserControl
|
||||
{
|
||||
private const string BrowserDownloadLink = "https://developer.microsoft.com/en-us/microsoft-edge/webview2/";
|
||||
|
||||
public readonly static DependencyProperty AddressProperty = DependencyPropertyExtensions.Register<ChromiumBrowserWrapper, string>(nameof(Address));
|
||||
public readonly static DependencyProperty FavoriteAddressProperty = DependencyPropertyExtensions.Register<ChromiumBrowserWrapper, string>(nameof(FavoriteAddress));
|
||||
public readonly static DependencyProperty NavigatingProperty = DependencyPropertyExtensions.Register<ChromiumBrowserWrapper, bool>(nameof(Navigating));
|
||||
public readonly static DependencyProperty AddressBarReadonlyProperty = DependencyPropertyExtensions.Register<ChromiumBrowserWrapper, bool>(nameof(AddressBarReadonly));
|
||||
public readonly static DependencyProperty BrowserSupportedProperty = DependencyPropertyExtensions.Register<ChromiumBrowserWrapper, bool>(nameof(BrowserSupported), new PropertyMetadata(true));
|
||||
|
||||
public event EventHandler<string> FavoriteUriChanged;
|
||||
public event EventHandler MaximizeClicked;
|
||||
|
||||
private readonly CoreWebView2Environment coreWebView2Environment;
|
||||
private readonly IConfigurationManager configurationManager;
|
||||
private readonly ILogger logger;
|
||||
private CoreWebView2Environment coreWebView2Environment;
|
||||
|
||||
public string Address
|
||||
{
|
||||
@@ -45,12 +53,18 @@ namespace Daybreak.Controls
|
||||
get => this.GetTypedValue<bool>(AddressBarReadonlyProperty);
|
||||
private set => this.SetTypedValue<bool>(AddressBarReadonlyProperty, value);
|
||||
}
|
||||
public bool BrowserSupported
|
||||
{
|
||||
get => this.GetTypedValue<bool>(BrowserSupportedProperty);
|
||||
private set => this.SetTypedValue<bool>(BrowserSupportedProperty, value);
|
||||
}
|
||||
|
||||
public ChromiumBrowserWrapper()
|
||||
{
|
||||
this.coreWebView2Environment = Launcher.ApplicationServiceManager.GetService<CoreWebView2Environment>();
|
||||
this.configurationManager = Launcher.ApplicationServiceManager.GetService<IConfigurationManager>();
|
||||
this.logger = Launcher.ApplicationServiceManager.GetService<ILogger>();
|
||||
this.InitializeComponent();
|
||||
this.InitializeEnvironment();
|
||||
this.InitializeBrowser();
|
||||
}
|
||||
|
||||
@@ -68,13 +82,51 @@ namespace Daybreak.Controls
|
||||
this.InitializeBrowser();
|
||||
}
|
||||
|
||||
private async void InitializeBrowser()
|
||||
private void InitializeEnvironment()
|
||||
{
|
||||
await this.WebBrowser.EnsureCoreWebView2Async(this.coreWebView2Environment);
|
||||
this.AddressBarReadonly = this.configurationManager.GetConfiguration().AddressBarReadonly;
|
||||
this.WebBrowser.CoreWebView2.NewWindowRequested += (browser, args) => args.Handled = true;
|
||||
this.WebBrowser.NavigationStarting += (browser, args) => this.Navigating = true;
|
||||
this.WebBrowser.NavigationCompleted += (browser, args) => this.Navigating = false;
|
||||
try
|
||||
{
|
||||
this.coreWebView2Environment = System.Extensions.TaskExtensions.RunSync(() => CoreWebView2Environment.CreateAsync(null, "BrowserData", null));
|
||||
this.BrowserSupported = true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
this.logger.LogWarning($"Browser initialization failed. Details: {e}");
|
||||
this.BrowserSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeBrowser()
|
||||
{
|
||||
if (this.BrowserSupported is true)
|
||||
{
|
||||
await this.WebBrowser.EnsureCoreWebView2Async(this.coreWebView2Environment);
|
||||
this.AddressBarReadonly = this.configurationManager.GetConfiguration().AddressBarReadonly;
|
||||
this.WebBrowser.CoreWebView2.NewWindowRequested += (browser, args) => args.Handled = true;
|
||||
this.WebBrowser.NavigationStarting += (browser, args) => this.Navigating = true;
|
||||
this.WebBrowser.NavigationCompleted += (browser, args) => this.Navigating = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void RetryInitializeButton_Clicked(object sender, EventArgs e)
|
||||
{
|
||||
this.InitializeEnvironment();
|
||||
try
|
||||
{
|
||||
await this.InitializeBrowser();
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.BrowserSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Hyperlink_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (Uri.TryCreate(BrowserDownloadLink, UriKind.Absolute, out var uri))
|
||||
{
|
||||
Process.Start("explorer.exe", uri.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void TextBox_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
|
||||
<Version>0.2</Version>
|
||||
<Version>0.3.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace Daybreak.Models
|
||||
{
|
||||
public enum ExecutionPolicies
|
||||
{
|
||||
AllSigned,
|
||||
Bypass,
|
||||
Default,
|
||||
RemoteSigned,
|
||||
Restricted,
|
||||
Undefined,
|
||||
Unrestricted
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Daybreak.Models;
|
||||
using Daybreak.Services.Logging;
|
||||
using Daybreak.Utils;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -15,14 +16,21 @@ namespace Daybreak.Services.Updater
|
||||
{
|
||||
public sealed class ApplicationUpdater : IApplicationUpdater
|
||||
{
|
||||
private const string ExecutionPolicyKey = "ExecutionPolicy";
|
||||
private const string UpdatedKey = "Updating";
|
||||
private const string RegistryKey = "Daybreak";
|
||||
private const string ExtractAndRunPs1 = "ExtractAndRun.ps1";
|
||||
private const string TempFile = "tempfile.zip";
|
||||
private const string VersionTag = "{VERSION}";
|
||||
private const string InputFileTag = "{INPUTFILE}";
|
||||
private const string OutputPathTag = "{OUTPUTPATh}";
|
||||
private const string OutputPathTag = "{OUTPUTPATH}";
|
||||
private const string ExecutionPolicyTag = "{EXECUTIONPOLICY}";
|
||||
private const string ProcessIdTag = "{PROCESSID}";
|
||||
private const string Url = "https://github.com/AlexMacocian/Daybreak/releases/latest";
|
||||
private const string DownloadUrl = $"https://github.com/AlexMacocian/Daybreak/releases/download/v{VersionTag}/Daybreakv{VersionTag}.zip";
|
||||
private const string DelayCommand = "Start-Sleep -m 3000";
|
||||
private const string GetExecutionPolicyCommand = "Get-ExecutionPolicy -Scope CurrentUser";
|
||||
private const string SetExecutionPolicyCommand = $"Set-ExecutionPolicy {ExecutionPolicyTag} -Scope CurrentUser";
|
||||
private const string WaitCommand = $"Wait-Process -Id {ProcessIdTag}";
|
||||
private const string ExtractCommandTemplate = $"Expand-Archive -Path '{InputFileTag}' -DestinationPath '{OutputPathTag}' -Force";
|
||||
private const string RunClientCommand = @".\Daybreak.exe";
|
||||
private const string RemoveTempFile = $"Remove-item {TempFile}";
|
||||
@@ -89,7 +97,7 @@ namespace Daybreak.Services.Updater
|
||||
var maybeLatestVersion = await this.GetLatestVersion();
|
||||
return maybeLatestVersion.Switch(
|
||||
onSome: latestVersion => string.Compare(version, latestVersion, true) < 0,
|
||||
onNone: () =>
|
||||
onNone: () =>
|
||||
{
|
||||
this.logger.LogWarning("Failed to retrieve latest version");
|
||||
return false;
|
||||
@@ -97,10 +105,117 @@ namespace Daybreak.Services.Updater
|
||||
}
|
||||
|
||||
public void FinalizeUpdate()
|
||||
{
|
||||
var maybeExecutionPolicy = this.RetrieveExecutionPolicy();
|
||||
maybeExecutionPolicy.DoAny(
|
||||
onNone: () =>
|
||||
{
|
||||
throw new InvalidOperationException("Failed to retrieve execution policy");
|
||||
});
|
||||
|
||||
var executionPolicy = maybeExecutionPolicy.ExtractValue();
|
||||
if (executionPolicy is not ExecutionPolicies.Bypass ||
|
||||
executionPolicy is not ExecutionPolicies.Unrestricted)
|
||||
{
|
||||
this.logger.LogInformation($"Execution policy is set to {executionPolicy}. Setting to {ExecutionPolicies.Bypass}");
|
||||
}
|
||||
|
||||
SaveExecutionPolicyValueToRegistry(executionPolicy);
|
||||
MarkUpdateInRegistry();
|
||||
this.SetExecutionPolicy(ExecutionPolicies.Bypass);
|
||||
this.LaunchExtractor();
|
||||
}
|
||||
|
||||
public void OnStartup()
|
||||
{
|
||||
if (UpdateMarkedInRegistry())
|
||||
{
|
||||
UnmarkUpdateInRegistry();
|
||||
var maybeExecutionPolicy = LoadExecutionPolicyValueFromRegistry();
|
||||
maybeExecutionPolicy.Do(
|
||||
onSome: policy =>
|
||||
{
|
||||
SetExecutionPolicy(policy);
|
||||
},
|
||||
onNone: () =>
|
||||
{
|
||||
throw new InvalidOperationException("Found update marked in registry but no execution policy");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void OnClosing()
|
||||
{
|
||||
}
|
||||
|
||||
private async Task<Optional<string>> GetLatestVersion()
|
||||
{
|
||||
using var response = await this.httpClient.GetAsync(Url);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var versionTag = response.RequestMessage.RequestUri.ToString().Split('/').Last().TrimStart('v');
|
||||
return versionTag;
|
||||
}
|
||||
|
||||
return Optional.None<string>();
|
||||
}
|
||||
|
||||
private Optional<ExecutionPolicies> RetrieveExecutionPolicy()
|
||||
{
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = GetExecutionPolicyCommand,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
this.logger.LogInformation("Checking current execution policy");
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
if (!Enum.TryParse(typeof(ExecutionPolicies), output, out var executionPolicy))
|
||||
{
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
this.logger.LogError($"Failed to retrieve current user execution policy. Stdout: {output}. Stderr: {error}");
|
||||
return Optional.None<ExecutionPolicies>();
|
||||
}
|
||||
|
||||
return executionPolicy.Cast<ExecutionPolicies>();
|
||||
}
|
||||
|
||||
private void SetExecutionPolicy(ExecutionPolicies executionPolicy)
|
||||
{
|
||||
var process = new Process()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell",
|
||||
Arguments = SetExecutionPolicyCommand.Replace(ExecutionPolicyTag, executionPolicy.ToString()),
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
this.logger.LogInformation($"Setting execution policy to {executionPolicy}");
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
if (!string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
throw new InvalidOperationException($"Failed to set execution policy to {executionPolicy}. Stdout: {output}. Stderr: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchExtractor()
|
||||
{
|
||||
File.WriteAllLines(ExtractAndRunPs1, new List<string>()
|
||||
{
|
||||
DelayCommand,
|
||||
{
|
||||
WaitCommand.Replace(ProcessIdTag, Environment.ProcessId.ToString()),
|
||||
ExtractCommandTemplate
|
||||
.Replace(InputFileTag, Path.GetFullPath(TempFile))
|
||||
.Replace(OutputPathTag, Directory.GetCurrentDirectory()),
|
||||
@@ -122,22 +237,90 @@ namespace Daybreak.Services.Updater
|
||||
WorkingDirectory = Directory.GetCurrentDirectory()
|
||||
},
|
||||
};
|
||||
this.logger.LogInformation("Created extractor script. Attempting to launch powershell");
|
||||
if (process.Start() is false)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create and start powershell script");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Optional<string>> GetLatestVersion()
|
||||
private static void MarkUpdateInRegistry()
|
||||
{
|
||||
using var response = await this.httpClient.GetAsync(Url);
|
||||
if (response.IsSuccessStatusCode)
|
||||
var homeRegistryKey = GetOrCreateHomeKey();
|
||||
homeRegistryKey.SetValue(UpdatedKey, true);
|
||||
homeRegistryKey.Close();
|
||||
}
|
||||
|
||||
private static void UnmarkUpdateInRegistry()
|
||||
{
|
||||
var homeRegistryKey = GetOrCreateHomeKey();
|
||||
homeRegistryKey.SetValue(UpdatedKey, false);
|
||||
homeRegistryKey.Close();
|
||||
}
|
||||
|
||||
private static bool UpdateMarkedInRegistry()
|
||||
{
|
||||
var homeRegistryKey = GetOrCreateHomeKey();
|
||||
var update = homeRegistryKey.GetValue(UpdatedKey);
|
||||
homeRegistryKey.Close();
|
||||
if (update is string updateString)
|
||||
{
|
||||
var versionTag = response.RequestMessage.RequestUri.ToString().Split('/').Last().TrimStart('v');
|
||||
return versionTag;
|
||||
if (bool.TryParse(updateString, out var updateValue))
|
||||
{
|
||||
return updateValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Found update value {updateString} in registry");
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.None<string>();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void SaveExecutionPolicyValueToRegistry(ExecutionPolicies executionPolicy)
|
||||
{
|
||||
var homeRegistryKey = GetOrCreateHomeKey();
|
||||
homeRegistryKey.SetValue(ExecutionPolicyKey, executionPolicy.ToString());
|
||||
homeRegistryKey.Close();
|
||||
}
|
||||
|
||||
private static Optional<ExecutionPolicies> LoadExecutionPolicyValueFromRegistry()
|
||||
{
|
||||
var homeRegistryKey = GetOrCreateHomeKey();
|
||||
var executionPolicy = homeRegistryKey.GetValue(ExecutionPolicyKey);
|
||||
homeRegistryKey.Close();
|
||||
|
||||
if (executionPolicy is null)
|
||||
{
|
||||
return Optional.None<ExecutionPolicies>();
|
||||
}
|
||||
else if (executionPolicy is string executionPolicyString)
|
||||
{
|
||||
if (Enum.TryParse<ExecutionPolicies>(executionPolicyString, out var executionPolicyValue))
|
||||
{
|
||||
return executionPolicyValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Found execution policy with value {executionPolicy}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Found execution policy of type {executionPolicy.GetType()}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static RegistryKey GetOrCreateHomeKey()
|
||||
{
|
||||
var homeRegistryKey = Registry.CurrentUser.OpenSubKey("Software", true).OpenSubKey(RegistryKey, true);
|
||||
if (homeRegistryKey is null)
|
||||
{
|
||||
homeRegistryKey = Registry.CurrentUser.OpenSubKey("Software", true).CreateSubKey(RegistryKey, true);
|
||||
}
|
||||
|
||||
return homeRegistryKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Daybreak.Models;
|
||||
using Daybreak.Services.ApplicationLifetime;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Daybreak.Services.Updater
|
||||
{
|
||||
public interface IApplicationUpdater
|
||||
public interface IApplicationUpdater : IApplicationLifetimeService
|
||||
{
|
||||
string CurrentVersion { get; }
|
||||
void FinalizeUpdate();
|
||||
|
||||
Reference in New Issue
Block a user