add console window

This commit is contained in:
Lightczx
2023-12-11 11:44:03 +08:00
parent 824fba89a8
commit e8762d658f
15 changed files with 117 additions and 232 deletions

View File

@@ -10,9 +10,11 @@ DwmSetWindowAttribute
GetDeviceCaps
// KERNEL32
AllocConsole
CloseHandle
CreateEventW
CreateRemoteThread
FreeConsole
GetModuleHandleW
GetProcAddress
K32EnumProcessModules

View File

@@ -24,7 +24,7 @@ internal static class DependencyInjection
ServiceProvider serviceProvider = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddUnconditionalDebug())
.AddLogging(builder => builder.AddConsoleWindow())
.AddMemoryCache()
// Hutao extensions
@@ -39,6 +39,7 @@ internal static class DependencyInjection
Ioc.Default.ConfigureServices(serviceProvider);
serviceProvider.InitializeConsoleWindow();
serviceProvider.InitializeCulture();
return serviceProvider;
@@ -61,4 +62,9 @@ internal static class DependencyInjection
SH.Culture = cultureInfo;
}
private static void InitializeConsoleWindow(this IServiceProvider serviceProvider)
{
_ = serviceProvider.GetRequiredService<ConsoleWindowLifeTime>();
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Setting;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Logging;
internal sealed class ConsoleWindowLifeTime : IDisposable
{
private readonly bool consoleWindowAllocated;
public ConsoleWindowLifeTime()
{
if (LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false))
{
consoleWindowAllocated = AllocConsole();
}
}
public void Dispose()
{
if (consoleWindowAllocated)
{
FreeConsole();
}
}
}

View File

@@ -1,70 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// A logger that writes messages in the debug output window only when a debugger is attached.
/// </summary>
internal sealed class DebugLogger : ILogger
{
private readonly string name;
/// <summary>
/// Initializes a new instance of the <see cref="DebugLogger"/> class.
/// </summary>
/// <param name="name">The name of the logger.</param>
public DebugLogger(string name)
{
this.name = name;
}
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return NullScope.Instance;
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
// If the filter is null, everything is enabled
return logLevel != LogLevel.None;
}
/// <inheritdoc />
[SuppressMessage("", "SH002")]
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
ArgumentNullException.ThrowIfNull(formatter);
string message = formatter(state, exception);
if (string.IsNullOrEmpty(message))
{
return;
}
message = $"{logLevel}: {message}";
if (exception is not null)
{
message += Environment.NewLine + Environment.NewLine + exception;
}
DebugWriteLine(message, name);
}
private static void DebugWriteLine(string message, string name)
{
Debug.WriteLine(message, category: name);
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// Extension methods for the <see cref="ILoggerFactory"/> class.
/// </summary>
internal static class DebugLoggerFactoryExtensions
{
/// <summary>
/// Adds a debug logger named 'Debug' to the factory.
/// </summary>
/// <param name="builder">The extension method argument.</param>
/// <returns>builder</returns>
public static ILoggingBuilder AddUnconditionalDebug(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DebugLoggerProvider>());
return builder;
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// The provider for the <see cref="DebugLogger"/>.
/// </summary>
[ProviderAlias("Debug")]
internal sealed class DebugLoggerProvider : ILoggerProvider
{
/// <inheritdoc />
public ILogger CreateLogger(string name)
{
return new DebugLogger(name);
}
/// <inheritdoc />
public void Dispose()
{
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
internal static class LoggerFactoryExtensions
{
public static ILoggingBuilder AddConsoleWindow(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ConsoleWindowLifeTime>();
builder.AddSimpleConsole();
return builder;
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// An empty scope without any logic
/// </summary>
internal sealed class NullScope : IDisposable
{
private NullScope()
{
}
/// <summary>
/// 实例
/// </summary>
public static NullScope Instance { get; } = new NullScope();
/// <inheritdoc />
public void Dispose()
{
}
}

View File

@@ -9,59 +9,26 @@ namespace Snap.Hutao.Core.Setting;
[HighQuality]
internal static class SettingKeys
{
/// <summary>
/// 窗体矩形
/// </summary>
public const string WindowRect = "WindowRect";
/// <summary>
/// 导航侧栏是否展开
/// </summary>
public const string IsNavPaneOpen = "IsNavPaneOpen";
/// <summary>
/// 启动次数
/// </summary>
public const string LaunchTimes = "LaunchTimes";
/// <summary>
/// 数据文件夹
/// </summary>
public const string DataFolderPath = "DataFolderPath";
/// <summary>
/// 通行证用户名(邮箱)
/// </summary>
public const string PassportUserName = "PassportUserName";
/// <summary>
/// 通行证密码
/// </summary>
public const string PassportPassword = "PassportPassword";
/// <summary>
/// 消息是否显示
/// </summary>
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
/// <summary>
/// 1.7.0 版本指引状态
/// </summary>
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
/// <summary>
/// 排除的系统公告
/// </summary>
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
/// <summary>
/// 禁用元数据更新检查
/// </summary>
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
/// <summary>
/// 覆盖管理员权限执行命令
/// </summary>
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
@@ -83,4 +50,6 @@ internal static class SettingKeys
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
}

View File

@@ -2528,6 +2528,12 @@
<data name="ViewServiceHutaoUserLoginOrRegisterHint" xml:space="preserve">
<value>立即登录或注册</value>
</data>
<data name="ViewSettingAllocConsoleDescription" xml:space="preserve">
<value>控制胡桃启动时是否开启控制台,重启后生效</value>
</data>
<data name="ViewSettingAllocConsoleHeader" xml:space="preserve">
<value>调试控制台</value>
</data>
<data name="ViewSettingFolderViewOpenFolderAction" xml:space="preserve">
<value>打开文件夹</value>
</data>

View File

@@ -297,6 +297,7 @@
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.Graphics.Win2D" Version="1.1.1" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />

View File

@@ -402,6 +402,13 @@
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Style="{StaticResource SettingsSectionHeaderTextBlockStyle}"
Text="{shcm:ResourceString Name=ViewPageSettingDangerousHeader}"/>
<cwc:SettingsCard
Description="{shcm:ResourceString Name=ViewSettingAllocConsoleDescription}"
Header="{shcm:ResourceString Name=ViewSettingAllocConsoleHeader}"
HeaderIcon="{cw:FontIcon Glyph=&#xE756;}">
<ToggleSwitch Width="120" IsOn="{Binding IsAllocConsoleDebugModeEnabled, Mode=TwoWay}"/>
</cwc:SettingsCard>
<cwc:SettingsCard
Header="{shcm:ResourceString Name=ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE730;}"

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Core.ExceptionService;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.ViewModel.Abstraction;
@@ -14,26 +15,16 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
{
private bool isInitialized;
/// <summary>
/// 是否初始化完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <inheritdoc/>
public CancellationToken CancellationToken { get; set; }
/// <inheritdoc/>
public SemaphoreSlim DisposeLock { get; set; } = new(1);
/// <inheritdoc/>
public bool IsViewDisposed { get; set; }
protected TaskCompletionSource<bool> Initialization { get; } = new();
/// <summary>
/// 异步初始化UI
/// </summary>
/// <returns>任务</returns>
[Command("OpenUICommand")]
protected virtual async Task OpenUIAsync()
{
@@ -42,20 +33,11 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
Initialization.TrySetResult(IsInitialized);
}
/// <summary>
/// 异步初始化界面数据
/// </summary>
/// <returns>初始化是否成功</returns>
protected virtual ValueTask<bool> InitializeUIAsync()
{
return ValueTask.FromResult(true);
}
/// <summary>
/// 保证 using scope 内的代码运行完成
/// 防止 视图资源被回收
/// </summary>
/// <returns>解除执行限制</returns>
protected async ValueTask<IDisposable> EnterCriticalExecutionAsync()
{
ThrowIfViewDisposed();
@@ -64,10 +46,28 @@ internal abstract partial class ViewModel : ObservableObject, IViewModel
return disposable;
}
/// <summary>
/// 当页面被释放后抛出异常
/// </summary>
/// <exception cref="OperationCanceledException">操作被用户取消</exception>
protected bool SetProperty<T>(ref T storage, T value, Action<T> changedCallback, [CallerMemberName] string? propertyName = null)
{
if (SetProperty(ref storage, value, propertyName))
{
changedCallback(value);
return true;
}
return false;
}
protected bool SetProperty<T>(ref T storage, T value, Func<T, ValueTask> changedAsyncCallback, [CallerMemberName] string? propertyName = null)
{
if (SetProperty(ref storage, value, propertyName))
{
changedAsyncCallback(value).SafeForget();
return true;
}
return false;
}
private void ThrowIfViewDisposed()
{
if (IsViewDisposed)

View File

@@ -55,37 +55,38 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
private GameAccount? selectedGameAccount;
private GameResource? gameResource;
/// <summary>
/// 已知的服务器方案
/// </summary>
[SuppressMessage("", "CA1822")]
public List<LaunchScheme> KnownSchemes { get => KnownLaunchSchemes.Get(); }
public List<LaunchScheme> KnownSchemes { get; } = KnownLaunchSchemes.Get();
/// <summary>
/// 当前选择的服务器方案
/// </summary>
public LaunchScheme? SelectedScheme
{
get => selectedScheme; set
get => selectedScheme;
set
{
if (SetProperty(ref selectedScheme, value))
SetProperty(ref selectedScheme, value, UpdateGameResourceAsync);
async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme)
{
if (value is not null)
if (scheme is null)
{
UpdateGameResourceAsync(value).SafeForget();
return;
}
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
}
}
}
}
/// <summary>
/// 游戏账号集合
/// </summary>
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
/// <summary>
/// 选中的账号
/// </summary>
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
public LaunchOptions LaunchOptions { get => launchOptions; }
@@ -96,9 +97,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
public AppOptions AppOptions { get => appOptions; }
/// <summary>
/// 游戏资源
/// </summary>
public GameResource? GameResource { get => gameResource; set => SetProperty(ref gameResource, value); }
protected override async ValueTask<bool> InitializeUIAsync()
@@ -165,20 +163,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
return true;
}
private async ValueTask UpdateGameResourceAsync(LaunchScheme scheme)
{
await taskContext.SwitchToBackgroundAsync();
Web.Response.Response<GameResource> response = await resourceClient
.GetResourceAsync(scheme)
.ConfigureAwait(false);
if (response.IsOk())
{
await taskContext.SwitchToMainThreadAsync();
GameResource = response.Data;
}
}
[Command("LaunchCommand")]
private async Task LaunchAsync()
{

View File

@@ -107,6 +107,13 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel
public IPInformation? IPInformation { get => ipInformation; private set => SetProperty(ref ipInformation, value); }
[SuppressMessage("", "CA1822")]
public bool IsAllocConsoleDebugModeEnabled
{
get => LocalSetting.Get(SettingKeys.IsAllocConsoleDebugModeEnabled, false);
set => LocalSetting.Set(SettingKeys.IsAllocConsoleDebugModeEnabled, value);
}
protected override async ValueTask<bool> InitializeUIAsync()
{
CacheFolderView = new(taskContext, runtimeOptions.LocalCache);