Compare commits

...
9 Commits
Author SHA1 Message Date
Alexandru Macocian 071007c3e5 Create application manifest.
Require highest available rights.
Return dialog message when registry is failed to be set.
2021-04-12 19:35:08 +02:00
Alexandru Macocian 57192d5043 Experimental multi-launch support 2021-04-12 19:09:00 +02:00
amacocianandGitHub 6af72683a8 Update README.md 2021-04-12 13:26:54 +02:00
amacocianandGitHub 3af9567506 Update README.md 2021-04-12 13:09:02 +02:00
Alexandru Macocian 1d7adf3944 Merge branch 'master' of https://github.com/AlexMacocian/Daybreak 2021-04-10 19:00:46 +02:00
Alexandru Macocian fb7399adf8 Add support for experimental features. 2021-04-10 19:00:39 +02:00
amacocianandGitHub 6ebbffdda1 Update README.md 2021-04-10 11:25:13 +02:00
Alexandru Macocian 2c35333cac Handle missing dependency on webview2 browser. 2021-04-10 11:18:46 +02:00
Alexandru Macocian fe5e38b216 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.
2021-04-10 10:31:36 +02:00
17 changed files with 703 additions and 44 deletions
@@ -20,5 +20,7 @@ namespace Daybreak.Configuration
public string ProtectedPassword { get; set; }
[JsonProperty("AddressBarReadonly")]
public bool AddressBarReadonly { get; set; } = true;
[JsonProperty("ExperimentalFeatures")]
public ExperimentalFeatures ExperimentalFeatures { get; set; } = new();
}
}
@@ -0,0 +1,7 @@
namespace Daybreak.Configuration
{
public sealed class ExperimentalFeatures
{
public bool MultiLaunchSupport { get; set; }
}
}
@@ -4,6 +4,7 @@ using Daybreak.Services.Bloogum;
using Daybreak.Services.Configuration;
using Daybreak.Services.Credentials;
using Daybreak.Services.Logging;
using Daybreak.Services.Mutex;
using Daybreak.Services.Screenshots;
using Daybreak.Services.Updater;
using Daybreak.Services.ViewManagement;
@@ -31,7 +32,7 @@ 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)));
serviceProducer.RegisterSingleton<IMutexHandler, MutexHandler>();
}
public static void RegisterLifetimeServices(IApplicationLifetimeProducer applicationLifetimeProducer)
{
@@ -39,6 +40,7 @@ namespace Daybreak.Configuration
applicationLifetimeProducer.RegisterService<ILoggingDatabase>();
applicationLifetimeProducer.RegisterService<IScreenshotProvider>();
applicationLifetimeProducer.RegisterService<IApplicationUpdater>();
}
public static void RegisterViews(IViewProducer viewProducer)
{
+21 -3
View File
@@ -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)
+2 -1
View File
@@ -9,7 +9,8 @@
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<Version>0.2.1</Version>
<Version>0.4.1</Version>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
+13
View File
@@ -0,0 +1,13 @@
namespace Daybreak.Models
{
public enum ExecutionPolicies
{
AllSigned,
Bypass,
Default,
RemoteSigned,
Restricted,
Undefined,
Unrestricted
}
}
@@ -1,11 +1,20 @@
using Daybreak.Services.Configuration;
using Daybreak.Models;
using Daybreak.Services.Configuration;
using Daybreak.Services.Credentials;
using Daybreak.Services.Logging;
using Daybreak.Services.Mutex;
using Daybreak.Utils;
using Microsoft.Win32;
using Pepa.Wpf.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Extensions;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
namespace Daybreak.Services.ApplicationDetection
{
@@ -13,17 +22,24 @@ namespace Daybreak.Services.ApplicationDetection
{
private const string ToolboxProcessName = "GWToolbox";
private const string ProcessName = "gw";
private const string ArenaNetMutex = "AN-Mute";
private readonly IConfigurationManager configurationManager;
private readonly ICredentialManager credentialManager;
private readonly IMutexHandler mutexHandler;
private readonly ILogger logger;
public bool IsGuildwarsRunning => GuildwarsProcessDetected();
public bool IsGuildwarsRunning => this.GuildwarsProcessDetected();
public bool IsToolboxRunning => GuildwarsToolboxProcessDetected();
public ApplicationDetector(
IConfigurationManager configurationManager,
ICredentialManager credentialManager)
ICredentialManager credentialManager,
IMutexHandler mutexHandler,
ILogger logger)
{
this.logger = logger.ThrowIfNull(nameof(logger));
this.mutexHandler = mutexHandler.ThrowIfNull(nameof(mutexHandler));
this.credentialManager = credentialManager.ThrowIfNull(nameof(credentialManager));
this.configurationManager = configurationManager.ThrowIfNull(nameof(configurationManager));
}
@@ -31,12 +47,6 @@ namespace Daybreak.Services.ApplicationDetection
public void LaunchGuildwars()
{
var configuration = this.configurationManager.GetConfiguration();
var executable = configuration.GamePath;
if (File.Exists(executable) is false)
{
throw new InvalidOperationException($"Guildwars executable doesn't exist at {executable}");
}
if (string.IsNullOrEmpty(configuration.CharacterName))
{
throw new InvalidOperationException($"No character name set");
@@ -46,10 +56,12 @@ namespace Daybreak.Services.ApplicationDetection
auth.Do(
onSome: (credentials) =>
{
if (Process.Start(executable, new List<string> { "-email", credentials.Username, "-password", credentials.Password, "-character", configuration.CharacterName }) is null)
if (configuration.ExperimentalFeatures.MultiLaunchSupport is true)
{
throw new InvalidOperationException($"Unable to launch {executable}");
ClearGwLocks();
}
LaunchGuildwarsProcess(credentials.Username, credentials.Password, configuration.CharacterName);
},
onNone: () =>
{
@@ -72,11 +84,92 @@ namespace Daybreak.Services.ApplicationDetection
}
}
private static bool GuildwarsProcessDetected()
private void LaunchGuildwarsProcess(string email, Models.SecureString password, string character)
{
var executable = this.configurationManager.GetConfiguration().GamePath;
if (File.Exists(executable) is false)
{
throw new InvalidOperationException($"Guildwars executable doesn't exist at {executable}");
}
if (Process.Start(executable, new List<string> { "-email", email, "-password", password, "-character", character }) is null)
{
throw new InvalidOperationException($"Unable to launch {executable}");
}
}
private bool GuildwarsProcessDetected()
{
if (this.configurationManager.GetConfiguration().ExperimentalFeatures.MultiLaunchSupport is true)
{
try
{
using var stream = File.OpenWrite(this.configurationManager.GetConfiguration().GamePath);
return false;
}
catch
{
return true;
}
}
return Process.GetProcessesByName(ProcessName).FirstOrDefault() is not null;
}
private void ClearGwLocks()
{
this.SetRegistryGuildwarsPath();
foreach (var process in Process.GetProcessesByName(ProcessName))
{
this.mutexHandler.CloseMutex(process, ArenaNetMutex);
}
}
private void SetRegistryGuildwarsPath()
{
var gamePath = this.configurationManager.GetConfiguration().GamePath;
try
{
var registryKey = GetGuildwarsRegistryKey(true);
registryKey.SetValue("Path", gamePath);
registryKey.SetValue("Src", gamePath);
registryKey.Close();
}
catch (SecurityException ex)
{
this.logger.LogCritical($"Multi-launch requires administrator rights. Details: {ex}");
}
}
private RegistryKey GetGuildwarsRegistryKey(bool write)
{
var gwKey = Registry.CurrentUser.OpenSubKey("SOFTWARE")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}
gwKey = Registry.CurrentUser.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}
gwKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}
gwKey = Registry.LocalMachine.OpenSubKey("SOFTWARE")?.OpenSubKey("WOW6432Node")?.OpenSubKey("ArenaNet")?.OpenSubKey("Guild Wars", write);
if (gwKey is not null)
{
return gwKey;
}
throw new InvalidOperationException("Could not find registry key for guildwars.");
}
private static bool GuildwarsToolboxProcessDetected()
{
return Process.GetProcessesByName(ToolboxProcessName).FirstOrDefault() is not null;
+9
View File
@@ -0,0 +1,9 @@
using System.Diagnostics;
namespace Daybreak.Services.Mutex
{
public interface IMutexHandler
{
void CloseMutex(Process process, string mutexName);
}
}
+139
View File
@@ -0,0 +1,139 @@
using Pepa.Wpf.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Daybreak.Services.Mutex
{
public sealed class MutexHandler : IMutexHandler
{
public void CloseMutex(Process process, string mutexName)
{
CloseHandle(process, mutexName);
}
private static List<NativeMethods.SystemHandleInformation> GetHandles(Process targetProcess, IntPtr systemHandle)
{
var processHandles = new List<NativeMethods.SystemHandleInformation>();
var basePointer = systemHandle.ToInt64();
NativeMethods.SystemHandleInformation currentHandleInfo;
for (int i = 0; i < Marshal.ReadInt32(systemHandle); i++)
{
var currentOffset = IntPtr.Size + i * Marshal.SizeOf(typeof(NativeMethods.SystemHandleInformation));
currentHandleInfo = (NativeMethods.SystemHandleInformation)Marshal.PtrToStructure(new IntPtr(basePointer + currentOffset), typeof(NativeMethods.SystemHandleInformation));
if (currentHandleInfo.OwnerPID == (uint)targetProcess.Id)
{
processHandles.Add(currentHandleInfo);
}
}
return processHandles;
}
private static void CloseHandle(Process targetProcess, string handleName)
{
var systemHandles = GetAllHandles();
if (systemHandles == IntPtr.Zero)
{
return;
}
List<NativeMethods.SystemHandleInformation> processHandles = GetHandles(targetProcess, systemHandles);
Marshal.FreeHGlobal(systemHandles);
var processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.DupHandle, false, (uint)targetProcess.Id);
foreach (var handleInfo in processHandles)
{
if (GetHandleName(handleInfo, processHandle).Contains(handleName))
{
if (CloseOwnedHandle(handleInfo.OwnerPID, new IntPtr(handleInfo.HandleValue)))
{
NativeMethods.CloseHandle(processHandle);
return;
}
}
}
NativeMethods.CloseHandle(processHandle);
return;
}
private static string GetHandleName(NativeMethods.SystemHandleInformation targetHandleInfo, IntPtr processHandle)
{
if (targetHandleInfo.AccessMask.ToInt64() == 0x0012019F)
{
return string.Empty;
}
var thisProcess = Process.GetCurrentProcess().Handle;
NativeMethods.DuplicateHandle(processHandle, new IntPtr(targetHandleInfo.HandleValue), thisProcess, out var handle, 0, false, NativeMethods.DuplicateOptions.DUPLICATE_SAME_ACCESS);
var bufferSize = GetHandleNameLength(handle);
var stringBuffer = Marshal.AllocHGlobal(bufferSize);
NativeMethods.NtQueryObject(handle, NativeMethods.ObjectInformationClass.ObjectNameInformation, stringBuffer, bufferSize, out _);
NativeMethods.CloseHandle(handle);
var handleName = ConvertToString(stringBuffer);
Marshal.FreeHGlobal(stringBuffer);
return handleName;
}
private static IntPtr GetAllHandles()
{
int bufferSize = 0x10000;
var pSysInfoBuffer = Marshal.AllocHGlobal(bufferSize);
var queryResult = NativeMethods.NtQuerySystemInformation(NativeMethods.SystemInformationClass.SystemHandleInformation,
pSysInfoBuffer, bufferSize, out _);
while (queryResult == NativeMethods.NtStatus.STATUS_INFO_LENGTH_MISMATCH)
{
Marshal.FreeHGlobal(pSysInfoBuffer);
bufferSize *= 2;
pSysInfoBuffer = Marshal.AllocHGlobal(bufferSize);
queryResult = NativeMethods.NtQuerySystemInformation(NativeMethods.SystemInformationClass.SystemHandleInformation,
pSysInfoBuffer, bufferSize, out _);
}
if (queryResult == NativeMethods.NtStatus.STATUS_SUCCESS)
{
return pSysInfoBuffer;
}
else
{
Marshal.FreeHGlobal(pSysInfoBuffer);
return IntPtr.Zero;
}
}
private static int GetHandleNameLength(IntPtr handle)
{
var infoBufferSize = Marshal.SizeOf(typeof(NativeMethods.ObjectBasicInformation));
var pInfoBuffer = Marshal.AllocHGlobal(infoBufferSize);
NativeMethods.NtQueryObject(handle, NativeMethods.ObjectInformationClass.ObjectBasicInformation, pInfoBuffer, infoBufferSize, out _);
NativeMethods.ObjectBasicInformation objInfo = (NativeMethods.ObjectBasicInformation)Marshal.PtrToStructure(pInfoBuffer, typeof(NativeMethods.ObjectBasicInformation));
Marshal.FreeHGlobal(pInfoBuffer);
if (objInfo.NameInformationLength == 0)
{
return 0x100;
}
else
{
return (int)objInfo.NameInformationLength;
}
}
private static string ConvertToString(IntPtr stringBuffer)
{
var baseAddress = stringBuffer.ToInt64();
var offset = IntPtr.Size * 2;
var handleName = Marshal.PtrToStringUni(new IntPtr(baseAddress + offset));
return handleName;
}
private static bool CloseOwnedHandle(uint processId, IntPtr handleToClose)
{
var processHandle = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.All, false, processId);
var success = NativeMethods.DuplicateHandle(processHandle, handleToClose, IntPtr.Zero, out _, 0, false, NativeMethods.DuplicateOptions.DUPLICATE_CLOSE_SOURCE);
NativeMethods.CloseHandle(processHandle);
return success;
}
}
}
+193 -12
View File
@@ -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,15 +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 SetExecutionPolicy = $"Set-ExecutionPolicy RemoteSigned -Scope CurrentUser";
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}";
@@ -90,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;
@@ -98,11 +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>()
{
SetExecutionPolicy,
DelayCommand,
WaitCommand.Replace(ProcessIdTag, Environment.ProcessId.ToString()),
ExtractCommandTemplate
.Replace(InputFileTag, Path.GetFullPath(TempFile))
.Replace(OutputPathTag, Directory.GetCurrentDirectory()),
@@ -124,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();
+94 -1
View File
@@ -1,13 +1,106 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
namespace Pepa.Wpf.Utilities
{
static class NativeMethods
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SystemHandleInformation
{
public uint OwnerPID;
public byte ObjectType;
public byte HandleFlags;
public ushort HandleValue;
public UIntPtr ObjectPointer;
public IntPtr AccessMask;
}
[StructLayout(LayoutKind.Sequential)]
public struct ObjectBasicInformation
{
public uint Attributes;
public uint GrantedAccess;
public uint HandleCount;
public uint PointerCount;
public uint PagedPoolUsage;
public uint NonPagedPoolUsage;
public uint Reserved1;
public uint Reserved2;
public uint Reserved3;
public uint NameInformationLength;
public uint TypeInformationLength;
public uint SecurityDescriptorLength;
public FILETIME CreateTime;
}
[StructLayout(LayoutKind.Sequential)]
public struct IoStatusBlock
{
public uint Status;
public ulong Information;
}
[Flags]
public enum DuplicateOptions : uint
{
DUPLICATE_CLOSE_SOURCE = 0x00000001,
DUPLICATE_SAME_ACCESS = 0x00000002
}
[Flags]
public enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VMOperation = 0x00000008,
VMRead = 0x00000010,
VMWrite = 0x00000020,
DupHandle = 0x00000040,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
Synchronize = 0x00100000
}
[Flags]
public enum NtStatus : uint
{
STATUS_SUCCESS = 0x00000000,
STATUS_INFO_LENGTH_MISMATCH = 0xC0000004
}
[Flags]
public enum ObjectInformationClass : uint
{
ObjectBasicInformation = 0,
ObjectNameInformation = 1,
ObjectTypeInformation = 2,
ObjectAllTypesInformation = 3,
ObjectHandleInformation = 4
}
[Flags]
public enum SystemInformationClass : uint
{
SystemHandleInformation = 16
}
[Flags]
public enum FileInformationClass
{
FileNameInformation = 9
}
public const int WM_SYSCOMMAND = 0x112;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle, IntPtr hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, DuplicateOptions dwOptions);
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, uint dwProcessID);
[DllImport("ntdll.dll", SetLastError = true)]
public static extern NtStatus NtQueryInformationFile(IntPtr FileHandle, ref IoStatusBlock IoStatusBlock, IntPtr FileInformation, int FileInformationLength, FileInformationClass FileInformationClass);
[DllImport("ntdll.dll")]
public static extern NtStatus NtQueryObject(IntPtr ObjectHandle, ObjectInformationClass ObjectInformationClass, IntPtr ObjectInformation, int ObjectInformationLength, out int ReturnLength);
[DllImport("ntdll.dll")]
public static extern NtStatus NtQuerySystemInformation(SystemInformationClass SystemInformationClass, IntPtr SystemInformation, int SystemInformationLength, out int ReturnLength);
}
}
}
+6
View File
@@ -17,6 +17,8 @@
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
@@ -50,5 +52,9 @@
<TextBlock Text="Address bar readonly: " FontSize="22" Foreground="White" Grid.Row="6"/>
<TextBox Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.Row="6"
x:Name="AddressBarReadonlyTextbox" FontSize="22" Background="Transparent" Foreground="White"></TextBox>
<TextBlock Text="Experimental" FontSize="26" Foreground="White" Grid.Row="7" Grid.ColumnSpan="2" HorizontalAlignment="Center"/>
<TextBlock Text="Multi-launch support: " FontSize="22" Foreground="White" Grid.Row="8"/>
<TextBox Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Grid.Row="8"
x:Name="MultiLaunchSupportTextbox" FontSize="22" Background="Transparent" Foreground="White"></TextBox>
</Grid>
</UserControl>
+6
View File
@@ -44,6 +44,7 @@ namespace Daybreak.Views
this.CharacterTextbox.Text = config.CharacterName;
this.GamePathTextbox.Text = config.GamePath;
this.ToolboxPathTextbox.Text = config.ToolboxPath;
this.MultiLaunchSupportTextbox.Text = config.ExperimentalFeatures.MultiLaunchSupport.ToString();
}
private void SaveButton_Clicked(object sender, System.EventArgs e)
@@ -57,6 +58,11 @@ namespace Daybreak.Views
currentConfig.AddressBarReadonly = addressBarReadonly;
}
if (bool.TryParse(this.MultiLaunchSupportTextbox.Text, out var multiLaunchSupport))
{
currentConfig.ExperimentalFeatures.MultiLaunchSupport = multiLaunchSupport;
}
this.configurationManager.SaveConfiguration(currentConfig);
this.credentialManager.StoreCredentials(new LoginCredentials { Username = this.UsernameTextbox.Text, Password = this.PasswordBox.Password });
this.viewManager.ShowView<MainView>();
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</windowsSettings>
</application>
</assembly>
+19 -5
View File
@@ -1,12 +1,26 @@
# Daybreak
Custom client for Guildwars.
Requires standalone version https://developer.microsoft.com/microsoft-edge/webview2.
Requires webview2 runtime https://go.microsoft.com/fwlink/p/?LinkId=2124703.
![Alt Text](https://media1.giphy.com/media/Z32o0OZ5pZHDOIodzD/giphy.gif)
![Alt Text](https://media0.giphy.com/media/aQ8Wl7lsuhT0AblCPI/giphy.gif)
![Alt Text](https://media2.giphy.com/media/s06PtxgeAAZtoJhTx6/giphy.gif)
![Showcase 1](https://media1.giphy.com/media/Z32o0OZ5pZHDOIodzD/giphy.gif)
![Showcase 2](https://media0.giphy.com/media/aQ8Wl7lsuhT0AblCPI/giphy.gif)
![Showcase 3](https://media2.giphy.com/media/s06PtxgeAAZtoJhTx6/giphy.gif)
## Features
# Examples/Usage
To modify any settings, press the settings button on the titlebar
![Settings button](https://i.imgur.com/0QSTvNF.png)
When launching, if any of the required settings are not valid (missing username/password/character name), the launcher will open the settings page.
![Settings page](https://i.imgur.com/Pzs8N6S.png)
By default, the browser address bar is set to readonly. To allow any link to be typed in the address bar, change the "Address bar readonly" setting from settings page to "True".
When in the main view, the browsers open the default/prefferred page. To change the prefferred page, navigate to it using one of the browsers and press the star button. The current loaded page will become the default for the selected browser.
![Browser default selection](https://i.imgur.com/nDnyIIL.png)
To display other images than the ones retrieved from "http://bloogum.net/guildwars", place images in the Screenshots folder, next to the Daybreak.exe executable. If the folder doesn't exist yet, either create it or run the launcher once so that it gets created automatically.
# Features
Automatically detect if guildwars is running or not. Includes the ability to launch guildwars from the client.
Manages username and password combination.