From 48f9f93bd6514f7f777b8d9846144e3e10848065 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 6 Jul 2026 15:03:27 +0000
Subject: [PATCH 1/4] Initial plan
From f422e1f16e98cfd8e6301735484e656df245bda1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 6 Jul 2026 15:10:30 +0000
Subject: [PATCH 2/4] Warn when host power scheme is not high performance
before Sandbox launch
---
.../Implementations/SandboxLauncher.cs | 3 +
src/TableCloth.Core/Helpers.cs | 72 +++++++++++++++++++
.../Resources/InfoStrings.Designer.cs | 9 +++
.../Resources/InfoStrings.ko.resx | 3 +
.../Resources/InfoStrings.resx | 3 +
src/TableCloth.Test/PowerSchemeTests.cs | 55 ++++++++++++++
6 files changed, 145 insertions(+)
create mode 100644 src/TableCloth.Test/PowerSchemeTests.cs
diff --git a/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs b/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs
index c5eb278f..9209c76a 100644
--- a/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs
+++ b/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs
@@ -49,6 +49,9 @@ public async Task RunSandboxAsync(TableClothConfiguration config, CancellationTo
return;
}
+ if (Helpers.IsHighPerformancePowerSchemeActive() == false)
+ appMessageBox.DisplayInfo(InfoStrings.Info_PowerScheme_NotHighPerformance);
+
if (config.CertPair != null)
{
var now = DateTime.Now;
diff --git a/src/TableCloth.Core/Helpers.cs b/src/TableCloth.Core/Helpers.cs
index db84c16e..0bf55546 100644
--- a/src/TableCloth.Core/Helpers.cs
+++ b/src/TableCloth.Core/Helpers.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Runtime.InteropServices;
using System.Text;
#nullable enable
@@ -11,6 +12,77 @@ namespace TableCloth
{
public static class Helpers
{
+ ///
+ /// Windows 기본 제공 "고성능(High performance)" 전원 관리 옵션의 GUID.
+ ///
+ private static readonly Guid HighPerformancePowerSchemeGuid =
+ new Guid("8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c");
+
+ ///
+ /// Windows 기본 제공 "최고의 성능(Ultimate Performance)" 전원 관리 옵션의 GUID.
+ ///
+ private static readonly Guid UltimatePerformancePowerSchemeGuid =
+ new Guid("e9a42b02-d5df-448d-aa00-03f14749eb61");
+
+ [DllImport("powrprof.dll", SetLastError = true)]
+ private static extern uint PowerGetActiveScheme(IntPtr userRootPowerKey, out IntPtr activePolicyGuid);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr LocalFree(IntPtr hMem);
+
+ private const uint ERROR_SUCCESS = 0u;
+
+ ///
+ /// 지정한 전원 관리 옵션 GUID가 "고성능" 또는 "최고의 성능" 구성표인지 여부를 반환한다.
+ /// Windows Sandbox의 시작 성능에 유리한 구성표를 판별하는 데 사용한다.
+ ///
+ public static bool IsHighPerformancePowerScheme(Guid schemeGuid)
+ => schemeGuid == HighPerformancePowerSchemeGuid
+ || schemeGuid == UltimatePerformancePowerSchemeGuid;
+
+ ///
+ /// 호스트에서 현재 활성화된 전원 관리 옵션의 GUID를 PowerGetActiveScheme로 읽는다.
+ /// 읽기에 실패하면 을 반환한다.
+ ///
+ public static Guid? GetActivePowerSchemeGuid()
+ {
+ var activePolicyGuidPtr = IntPtr.Zero;
+
+ try
+ {
+ if (PowerGetActiveScheme(IntPtr.Zero, out activePolicyGuidPtr) != ERROR_SUCCESS)
+ return null;
+
+ if (activePolicyGuidPtr == IntPtr.Zero)
+ return null;
+
+ return (Guid)Marshal.PtrToStructure(activePolicyGuidPtr, typeof(Guid));
+ }
+ catch
+ {
+ return null;
+ }
+ finally
+ {
+ if (activePolicyGuidPtr != IntPtr.Zero)
+ LocalFree(activePolicyGuidPtr);
+ }
+ }
+
+ ///
+ /// 호스트의 활성 전원 관리 옵션이 고성능 계열인지 여부를 반환한다.
+ /// 판별에 실패하면(예: 지원되지 않는 환경) 을 반환한다.
+ ///
+ public static bool? IsHighPerformancePowerSchemeActive()
+ {
+ var activeSchemeGuid = GetActivePowerSchemeGuid();
+
+ if (!activeSchemeGuid.HasValue)
+ return null;
+
+ return IsHighPerformancePowerScheme(activeSchemeGuid.Value);
+ }
+
public static bool IsDevelopmentBuild =>
#if DEBUG
true
diff --git a/src/TableCloth.Core/Resources/InfoStrings.Designer.cs b/src/TableCloth.Core/Resources/InfoStrings.Designer.cs
index ed0e1166..cef934e1 100644
--- a/src/TableCloth.Core/Resources/InfoStrings.Designer.cs
+++ b/src/TableCloth.Core/Resources/InfoStrings.Designer.cs
@@ -104,5 +104,14 @@ public static string Info_WillCreateSingleSiteShortcut {
return ResourceManager.GetString("Info_WillCreateSingleSiteShortcut", resourceCulture);
}
}
+
+ ///
+ /// The current power plan on this PC is not set to 'High performance'. Windows Sandbox may start slowly. For a faster experience, consider switching to the 'High performance' power plan in Control Panel.과(와) 유사한 지역화된 문자열을 찾습니다.
+ ///
+ public static string Info_PowerScheme_NotHighPerformance {
+ get {
+ return ResourceManager.GetString("Info_PowerScheme_NotHighPerformance", resourceCulture);
+ }
+ }
}
}
diff --git a/src/TableCloth.Core/Resources/InfoStrings.ko.resx b/src/TableCloth.Core/Resources/InfoStrings.ko.resx
index 83f7f6d3..51f200c0 100644
--- a/src/TableCloth.Core/Resources/InfoStrings.ko.resx
+++ b/src/TableCloth.Core/Resources/InfoStrings.ko.resx
@@ -132,4 +132,7 @@
바로 가기에 지정할 수 있는 최대 명령어 길이 제한으로 처음 선택한 사이트만 바로 가기로 만듭니다.
+
+ 현재 이 PC의 전원 관리 옵션이 '고성능'으로 설정되어 있지 않습니다. Windows Sandbox 시작이 느려질 수 있습니다. 더 빠른 실행 경험을 위해 제어판에서 '고성능' 전원 관리 옵션으로 변경하는 것을 권장합니다.
+
\ No newline at end of file
diff --git a/src/TableCloth.Core/Resources/InfoStrings.resx b/src/TableCloth.Core/Resources/InfoStrings.resx
index cfcafe58..53691d10 100644
--- a/src/TableCloth.Core/Resources/InfoStrings.resx
+++ b/src/TableCloth.Core/Resources/InfoStrings.resx
@@ -132,4 +132,7 @@
Because of the maximum command length limit that can be specified for a shortcut, only the first selected site is made a shortcut.
+
+ The current power plan on this PC is not set to 'High performance'. Windows Sandbox may start slowly. For a faster experience, consider switching to the 'High performance' power plan in Control Panel.
+
\ No newline at end of file
diff --git a/src/TableCloth.Test/PowerSchemeTests.cs b/src/TableCloth.Test/PowerSchemeTests.cs
new file mode 100644
index 00000000..b48e8f0d
--- /dev/null
+++ b/src/TableCloth.Test/PowerSchemeTests.cs
@@ -0,0 +1,55 @@
+using System;
+using TableCloth;
+
+namespace TableCloth.Test
+{
+ [TestClass]
+ public sealed class PowerSchemeTests
+ {
+ // Windows built-in "High performance" power scheme GUID.
+ private static readonly Guid HighPerformanceGuid =
+ new Guid("8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c");
+
+ // Windows built-in "Ultimate Performance" power scheme GUID.
+ private static readonly Guid UltimatePerformanceGuid =
+ new Guid("e9a42b02-d5df-448d-aa00-03f14749eb61");
+
+ // Windows built-in "Balanced" power scheme GUID.
+ private static readonly Guid BalancedGuid =
+ new Guid("381b4222-f694-41f0-9685-ff5bb260df2e");
+
+ // Windows built-in "Power saver" power scheme GUID.
+ private static readonly Guid PowerSaverGuid =
+ new Guid("a1841308-3541-4fab-bc81-f71556f20b4a");
+
+ [TestMethod]
+ public void IsHighPerformancePowerScheme_HighPerformance_ReturnsTrue()
+ {
+ Assert.IsTrue(Helpers.IsHighPerformancePowerScheme(HighPerformanceGuid));
+ }
+
+ [TestMethod]
+ public void IsHighPerformancePowerScheme_UltimatePerformance_ReturnsTrue()
+ {
+ Assert.IsTrue(Helpers.IsHighPerformancePowerScheme(UltimatePerformanceGuid));
+ }
+
+ [TestMethod]
+ public void IsHighPerformancePowerScheme_Balanced_ReturnsFalse()
+ {
+ Assert.IsFalse(Helpers.IsHighPerformancePowerScheme(BalancedGuid));
+ }
+
+ [TestMethod]
+ public void IsHighPerformancePowerScheme_PowerSaver_ReturnsFalse()
+ {
+ Assert.IsFalse(Helpers.IsHighPerformancePowerScheme(PowerSaverGuid));
+ }
+
+ [TestMethod]
+ public void IsHighPerformancePowerScheme_EmptyGuid_ReturnsFalse()
+ {
+ Assert.IsFalse(Helpers.IsHighPerformancePowerScheme(Guid.Empty));
+ }
+ }
+}
From 79f540e19cc5a5fbabb78f83f6aed351594df10a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 6 Jul 2026 15:12:41 +0000
Subject: [PATCH 3/4] Use type-safe Marshal.PtrToStructure generic overload for
power scheme GUID
---
src/TableCloth.Core/Helpers.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/TableCloth.Core/Helpers.cs b/src/TableCloth.Core/Helpers.cs
index 0bf55546..11cc6c7e 100644
--- a/src/TableCloth.Core/Helpers.cs
+++ b/src/TableCloth.Core/Helpers.cs
@@ -56,7 +56,7 @@ public static bool IsHighPerformancePowerScheme(Guid schemeGuid)
if (activePolicyGuidPtr == IntPtr.Zero)
return null;
- return (Guid)Marshal.PtrToStructure(activePolicyGuidPtr, typeof(Guid));
+ return Marshal.PtrToStructure(activePolicyGuidPtr);
}
catch
{
From b6daa3e30fbcf66c5d746ac990b82d747b19a62d Mon Sep 17 00:00:00 2001
From: "Jung Hyun, Nam"
Date: Wed, 8 Jul 2026 23:48:09 +0900
Subject: [PATCH 4/4] =?UTF-8?q?feat(power):=20=EC=A0=84=EC=9B=90=20?=
=?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=95=88=EB=82=B4=EB=A5=BC=20=EC=8B=A4?=
=?UTF-8?q?=EC=A0=9C=20CPU=20=EC=83=81=ED=95=9C=20=EA=B8=B0=EB=B0=98=20?=
=?UTF-8?q?=ED=8C=90=EC=A0=95=20+=20=EC=A0=84=EC=9A=A9=20=EC=B0=BD?=
=?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B0=9C=EC=84=A0=20(#295)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
전원 프로필 "이름(고성능 GUID)" 매칭은 복제/OEM 계획을 오탐하고 AC의 균형 조정처럼
실제로는 100%로 동작하는 경우도 잘못 경고했다. 실제 성능에 직접적인 CPU 최대 성능 상한
(최대 프로세서 상태)을 읽어 판정하도록 바꾸고, 안내도 전용 창으로 친절하게 재구성한다.
판정 (Helpers)
- IsHighPerformancePowerScheme(이름 매칭) 제거. 현재 전원 소스(AC/DC)에 적용되는
최대 프로세서 상태(GUID_PROCESSOR_THROTTLE_MAXIMUM)를 PowerReadAC/DCValueIndex로 읽어,
100% 미만이면 스로틀로 판정(IsSandboxCpuLikelyThrottled). 판별 실패 시 null → 경고 억제.
- 순수 분류기 IsProcessorStateThrottled(int)로 값→판정 로직을 단위 테스트(ProcessorThrottleTests).
안내 UI
- 시스템 메시지 박스 → 전용 PowerSchemeGuideWindow(MVVM). 상황을 설명하고 클래식 제어판
(powercfg.cpl)과 모던 설정(전원 및 절전) 중 하나를 골라 열 수 있다.
- 열기 버튼은 해당 설정만 열고 모달은 유지, '나중에'로만 닫는다. 버튼 내부 텍스트는
전용 템플릿으로 왼쪽 정렬(테마 색은 DynamicResource 유지).
- IAppUserInterface 팩토리 + DI 등록. SandboxLauncher는 UI 스레드로 마샬링해 모달 표시.
- InfoStrings.Info_Sandbox_ProcessorThrottled 제거, UIStringResources에 PowerSchemeGuide_* 추가(한/영).
빌드 경고 0, 테스트 통과, 스로틀 시뮬레이션·스모크 실행으로 검증.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../Components/IAppUserInterface.cs | 1 +
.../Implementations/AppUserInterface.cs | 3 +
.../Implementations/SandboxLauncher.cs | 9 +-
.../UseTableClothExtensions.cs | 1 +
.../Dialogs/PowerSchemeGuideWindow.xaml | 99 +++++++++++++++++
.../Dialogs/PowerSchemeGuideWindow.xaml.cs | 22 ++++
.../PowerSchemeGuideWindowViewModel.cs | 66 +++++++++++
src/TableCloth.Core/Helpers.cs | 105 ++++++++++++++----
.../Resources/InfoStrings.Designer.cs | 9 --
.../Resources/InfoStrings.ko.resx | 3 -
.../Resources/InfoStrings.resx | 3 -
.../Resources/UIStringResources.Designer.cs | 48 ++++++++
.../Resources/UIStringResources.ko.resx | 24 ++++
.../Resources/UIStringResources.resx | 24 ++++
src/TableCloth.Test/PowerSchemeTests.cs | 55 ---------
src/TableCloth.Test/ProcessorThrottleTests.cs | 45 ++++++++
16 files changed, 424 insertions(+), 93 deletions(-)
create mode 100644 src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml
create mode 100644 src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml.cs
create mode 100644 src/TableCloth.App/ViewModels/PowerSchemeGuideWindowViewModel.cs
delete mode 100644 src/TableCloth.Test/PowerSchemeTests.cs
create mode 100644 src/TableCloth.Test/ProcessorThrottleTests.cs
diff --git a/src/TableCloth.App/Components/IAppUserInterface.cs b/src/TableCloth.App/Components/IAppUserInterface.cs
index f80afd3a..baa25180 100644
--- a/src/TableCloth.App/Components/IAppUserInterface.cs
+++ b/src/TableCloth.App/Components/IAppUserInterface.cs
@@ -11,6 +11,7 @@ public interface IAppUserInterface
{
AboutWindow CreateAboutWindow();
OptionsWindow CreateOptionsWindow(string? initialTabKey = null);
+ PowerSchemeGuideWindow CreatePowerSchemeGuideWindow();
CatalogPage CreateCatalogPage(string searchKeyword);
CatalogPageViewModel CreateCatalogPageViewModel(string searchKeyword);
QuickStartPage CreateQuickStartPage();
diff --git a/src/TableCloth.App/Components/Implementations/AppUserInterface.cs b/src/TableCloth.App/Components/Implementations/AppUserInterface.cs
index 27d01186..9da8b174 100644
--- a/src/TableCloth.App/Components/Implementations/AppUserInterface.cs
+++ b/src/TableCloth.App/Components/Implementations/AppUserInterface.cs
@@ -53,6 +53,9 @@ public InputPasswordWindow CreateInputPasswordWindow()
public DisclaimerWindow CreateDisclaimerWindow()
=> SetOwnerIfAvailable(serviceProvider.GetRequiredService());
+ public PowerSchemeGuideWindow CreatePowerSchemeGuideWindow()
+ => SetOwnerIfAvailable(serviceProvider.GetRequiredService());
+
public SplashScreen CreateSplashScreen()
=> SetOwnerIfAvailable(serviceProvider.GetRequiredService());
diff --git a/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs b/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs
index 9209c76a..b4a74dd5 100644
--- a/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs
+++ b/src/TableCloth.App/Components/Implementations/SandboxLauncher.cs
@@ -14,6 +14,8 @@ namespace TableCloth.Components.Implementations;
public sealed class SandboxLauncher(
IAppMessageBox appMessageBox,
+ IAppUserInterface appUserInterface,
+ IApplicationService applicationService,
ISharedLocations sharedLocations,
ISandboxBuilder sandboxBuilder,
ISandboxCleanupManager sandboxCleanupManager,
@@ -49,8 +51,11 @@ public async Task RunSandboxAsync(TableClothConfiguration config, CancellationTo
return;
}
- if (Helpers.IsHighPerformancePowerSchemeActive() == false)
- appMessageBox.DisplayInfo(InfoStrings.Info_PowerScheme_NotHighPerformance);
+ // CPU 최대 성능이 제한(스로틀)되어 있으면, 시스템 메시지 박스 대신 전용 안내 창을 띄워
+ // 클래식(powercfg.cpl)/모던(전원 및 절전) 전원 설정 중 하나를 골라 열 수 있게 안내한다.
+ // 백그라운드에서 호출될 수 있으므로 UI 스레드로 마샬링해 모달로 표시한다(런치는 계속 진행).
+ if (Helpers.IsSandboxCpuLikelyThrottled() == true)
+ applicationService.DispatchInvoke(() => appUserInterface.CreatePowerSchemeGuideWindow().ShowDialog(), []);
if (config.CertPair != null)
{
diff --git a/src/TableCloth.App/DependencyInjection/UseTableClothExtensions.cs b/src/TableCloth.App/DependencyInjection/UseTableClothExtensions.cs
index 0441f8d9..0709157a 100644
--- a/src/TableCloth.App/DependencyInjection/UseTableClothExtensions.cs
+++ b/src/TableCloth.App/DependencyInjection/UseTableClothExtensions.cs
@@ -73,6 +73,7 @@ public static IHostApplicationBuilder UseTableCloth(this IHostApplicationBuilder
.AddWindow()
.AddWindow()
.AddWindow()
+ .AddWindow()
.AddWindow()
.AddWindow()
.AddPage(addPageAsSingleton: true)
diff --git a/src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml b/src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml
new file mode 100644
index 00000000..b7051af6
--- /dev/null
+++ b/src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml.cs b/src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml.cs
new file mode 100644
index 00000000..8f6af55f
--- /dev/null
+++ b/src/TableCloth.App/Dialogs/PowerSchemeGuideWindow.xaml.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Windows;
+using TableCloth.ViewModels;
+
+namespace TableCloth.Dialogs;
+
+public partial class PowerSchemeGuideWindow : Window
+{
+ public PowerSchemeGuideWindow(
+ PowerSchemeGuideWindowViewModel viewModel)
+ {
+ InitializeComponent();
+ DataContext = viewModel;
+ viewModel.CloseRequested += ViewModel_CloseRequested;
+ }
+
+ public PowerSchemeGuideWindowViewModel ViewModel
+ => (PowerSchemeGuideWindowViewModel)DataContext;
+
+ private void ViewModel_CloseRequested(object? sender, EventArgs e)
+ => Close();
+}
diff --git a/src/TableCloth.App/ViewModels/PowerSchemeGuideWindowViewModel.cs b/src/TableCloth.App/ViewModels/PowerSchemeGuideWindowViewModel.cs
new file mode 100644
index 00000000..541fcba1
--- /dev/null
+++ b/src/TableCloth.App/ViewModels/PowerSchemeGuideWindowViewModel.cs
@@ -0,0 +1,66 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace TableCloth.ViewModels;
+
+[Obsolete("This class is reserved for design-time usage.", false)]
+public partial class PowerSchemeGuideWindowViewModelForDesigner : PowerSchemeGuideWindowViewModel { }
+
+///
+/// CPU 최대 성능(최대 프로세서 상태)이 제한되어 Windows Sandbox가 느려질 수 있음을 안내하고,
+/// 사용자가 클래식 제어판(powercfg.cpl) 또는 모던 설정(전원 및 절전) 중 하나를 골라 열 수 있게 하는
+/// 안내 창의 뷰모델. 전원 옵션을 여는 커맨드는 창을 닫지 않고 그대로 유지하며(사용자가 확인 후 직접 닫음),
+/// '나중에'만 창을 닫는다. 여는 데 실패해도 무해하다(베스트에포트).
+///
+public partial class PowerSchemeGuideWindowViewModel : ObservableObject
+{
+ protected PowerSchemeGuideWindowViewModel() { }
+
+ [ActivatorUtilitiesConstructor]
+ public PowerSchemeGuideWindowViewModel(
+ TaskFactory taskFactory)
+ {
+ _taskFactory = taskFactory;
+ }
+
+ private readonly TaskFactory _taskFactory = default!;
+
+ /// 안내 창을 닫아 달라는 요청. 코드 비하인드가 구독해 Close()를 호출한다.
+ public event EventHandler? CloseRequested;
+
+ private async Task RequestCloseAsync()
+ => await _taskFactory.StartNew(() => CloseRequested?.Invoke(this, EventArgs.Empty)).ConfigureAwait(false);
+
+ /// 클래식 제어판의 전원 옵션(powercfg.cpl)을 연다. 안내 창은 닫지 않고 그대로 둔다.
+ [RelayCommand]
+ private void OpenClassicPowerOptions()
+ // control.exe 로 .cpl 을 여는 것이 셸 연결에 의존하지 않아 안정적이다.
+ => TryStart(new ProcessStartInfo("control.exe", "powercfg.cpl") { UseShellExecute = true });
+
+ /// 모던 Windows 설정의 전원 페이지(전원 및 절전)를 연다. 안내 창은 닫지 않고 그대로 둔다.
+ [RelayCommand]
+ private void OpenModernPowerSettings()
+ => TryStart(new ProcessStartInfo("ms-settings:powersleep") { UseShellExecute = true });
+
+ /// 아무 것도 열지 않고 안내 창을 닫는다(나중에).
+ [RelayCommand]
+ private async Task Dismiss()
+ => await RequestCloseAsync();
+
+ private static void TryStart(ProcessStartInfo startInfo)
+ {
+ try
+ {
+ Process.Start(startInfo);
+ }
+ catch
+ {
+ // 외부 설정 페이지 열기는 보조 동작이므로 실패해도 조용히 무시한다.
+ }
+ }
+}
diff --git a/src/TableCloth.Core/Helpers.cs b/src/TableCloth.Core/Helpers.cs
index 11cc6c7e..4f9b726b 100644
--- a/src/TableCloth.Core/Helpers.cs
+++ b/src/TableCloth.Core/Helpers.cs
@@ -12,33 +12,61 @@ namespace TableCloth
{
public static class Helpers
{
- ///
- /// Windows 기본 제공 "고성능(High performance)" 전원 관리 옵션의 GUID.
- ///
- private static readonly Guid HighPerformancePowerSchemeGuid =
- new Guid("8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c");
+ // 전원 관리 옵션 "이름(GUID)"이 아니라, 활성 옵션의 실제 CPU 성능 상한(최대 프로세서 상태)을 읽어
+ // 판단한다. 이름 기반 판별은 복제/OEM 커스텀 옵션을 오탐하고, AC의 '균형 조정'처럼 실제로는
+ // 100%로 동작하는 경우도 잘못 경고했다. 여기서는 현재 전원 소스(AC/DC)의 최대 프로세서 상태를
+ // 그대로 읽어, CPU가 실제로 제한(스로틀)될 때만(=100% 미만) Windows Sandbox 성능 저하로 판정한다.
- ///
- /// Windows 기본 제공 "최고의 성능(Ultimate Performance)" 전원 관리 옵션의 GUID.
- ///
- private static readonly Guid UltimatePerformancePowerSchemeGuid =
- new Guid("e9a42b02-d5df-448d-aa00-03f14749eb61");
+ /// Windows 프로세서 전원 관리 하위 그룹 GUID(GUID_PROCESSOR_SETTINGS_SUBGROUP).
+ private static readonly Guid ProcessorSettingsSubgroupGuid =
+ new Guid("54533251-82be-4824-96c1-47b60b740d00");
+
+ /// "최대 프로세서 상태" 설정 GUID(GUID_PROCESSOR_THROTTLE_MAXIMUM). 값은 0~100(%).
+ private static readonly Guid ProcessorThrottleMaximumSettingGuid =
+ new Guid("bc5038f7-23e0-4960-96da-33abaf5935ec");
+
+ private const uint ERROR_SUCCESS = 0u;
+
+ /// 제한이 없는(=최고 성능) 최대 프로세서 상태 값(%).
+ private const int FullProcessorStatePercent = 100;
[DllImport("powrprof.dll", SetLastError = true)]
private static extern uint PowerGetActiveScheme(IntPtr userRootPowerKey, out IntPtr activePolicyGuid);
+ [DllImport("powrprof.dll")]
+ private static extern uint PowerReadACValueIndex(
+ IntPtr rootPowerKey, in Guid schemeGuid, in Guid subGroupOfPowerSettingsGuid,
+ in Guid powerSettingGuid, out uint acValueIndex);
+
+ [DllImport("powrprof.dll")]
+ private static extern uint PowerReadDCValueIndex(
+ IntPtr rootPowerKey, in Guid schemeGuid, in Guid subGroupOfPowerSettingsGuid,
+ in Guid powerSettingGuid, out uint dcValueIndex);
+
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr hMem);
- private const uint ERROR_SUCCESS = 0u;
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool GetSystemPowerStatus(out SYSTEM_POWER_STATUS lpSystemPowerStatus);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct SYSTEM_POWER_STATUS
+ {
+ public byte ACLineStatus; // 0 = 배터리(오프라인), 1 = AC(온라인), 255 = 알 수 없음
+ public byte BatteryFlag;
+ public byte BatteryLifePercent;
+ public byte SystemStatusFlag;
+ public int BatteryLifeTime;
+ public int BatteryFullLifeTime;
+ }
///
- /// 지정한 전원 관리 옵션 GUID가 "고성능" 또는 "최고의 성능" 구성표인지 여부를 반환한다.
- /// Windows Sandbox의 시작 성능에 유리한 구성표를 판별하는 데 사용한다.
+ /// 최대 프로세서 상태 값(%)이 "제한됨(스로틀)"에 해당하는지 여부를 반환하는 순수 함수.
+ /// 100% 미만이면 CPU 성능이 상한으로 제한된 것으로 본다.
///
- public static bool IsHighPerformancePowerScheme(Guid schemeGuid)
- => schemeGuid == HighPerformancePowerSchemeGuid
- || schemeGuid == UltimatePerformancePowerSchemeGuid;
+ public static bool IsProcessorStateThrottled(int maxProcessorStatePercent)
+ => maxProcessorStatePercent < FullProcessorStatePercent;
///
/// 호스트에서 현재 활성화된 전원 관리 옵션의 GUID를 PowerGetActiveScheme로 읽는다.
@@ -69,18 +97,53 @@ public static bool IsHighPerformancePowerScheme(Guid schemeGuid)
}
}
+ /// 현재 배터리(DC)로 실행 중인지 여부. 판별 불가/AC면 .
+ private static bool IsRunningOnBattery()
+ {
+ if (!GetSystemPowerStatus(out var status))
+ return false;
+
+ // 0 = 배터리. 1(AC)/255(알 수 없음)은 AC로 간주해 AC 설정을 읽는다(보수적).
+ return status.ACLineStatus == 0;
+ }
+
///
- /// 호스트의 활성 전원 관리 옵션이 고성능 계열인지 여부를 반환한다.
- /// 판별에 실패하면(예: 지원되지 않는 환경) 을 반환한다.
+ /// 현재 전원 소스(AC 또는 배터리)에 적용되는 활성 옵션의 "최대 프로세서 상태"(%)를 읽는다.
+ /// 읽기에 실패하면 을 반환한다.
///
- public static bool? IsHighPerformancePowerSchemeActive()
+ public static int? GetActiveMaxProcessorStatePercent()
{
var activeSchemeGuid = GetActivePowerSchemeGuid();
-
if (!activeSchemeGuid.HasValue)
return null;
- return IsHighPerformancePowerScheme(activeSchemeGuid.Value);
+ var schemeGuid = activeSchemeGuid.Value;
+
+ var result = IsRunningOnBattery()
+ ? PowerReadDCValueIndex(IntPtr.Zero, in schemeGuid, in ProcessorSettingsSubgroupGuid,
+ in ProcessorThrottleMaximumSettingGuid, out var value)
+ : PowerReadACValueIndex(IntPtr.Zero, in schemeGuid, in ProcessorSettingsSubgroupGuid,
+ in ProcessorThrottleMaximumSettingGuid, out value);
+
+ if (result != ERROR_SUCCESS)
+ return null;
+
+ return (int)value;
+ }
+
+ ///
+ /// 호스트의 CPU 최대 성능이 제한(스로틀)되어 Windows Sandbox가 느려질 가능성이 높은지 여부를 반환한다.
+ /// 현재 전원 소스의 최대 프로세서 상태가 100% 미만이면 . 판별에 실패하면
+ /// (예: 지원되지 않는 환경) 을 반환해, 확신할 때만 경고하도록 한다.
+ ///
+ public static bool? IsSandboxCpuLikelyThrottled()
+ {
+ var maxProcessorStatePercent = GetActiveMaxProcessorStatePercent();
+
+ if (!maxProcessorStatePercent.HasValue)
+ return null;
+
+ return IsProcessorStateThrottled(maxProcessorStatePercent.Value);
}
public static bool IsDevelopmentBuild =>
diff --git a/src/TableCloth.Core/Resources/InfoStrings.Designer.cs b/src/TableCloth.Core/Resources/InfoStrings.Designer.cs
index cef934e1..ed0e1166 100644
--- a/src/TableCloth.Core/Resources/InfoStrings.Designer.cs
+++ b/src/TableCloth.Core/Resources/InfoStrings.Designer.cs
@@ -104,14 +104,5 @@ public static string Info_WillCreateSingleSiteShortcut {
return ResourceManager.GetString("Info_WillCreateSingleSiteShortcut", resourceCulture);
}
}
-
- ///
- /// The current power plan on this PC is not set to 'High performance'. Windows Sandbox may start slowly. For a faster experience, consider switching to the 'High performance' power plan in Control Panel.과(와) 유사한 지역화된 문자열을 찾습니다.
- ///
- public static string Info_PowerScheme_NotHighPerformance {
- get {
- return ResourceManager.GetString("Info_PowerScheme_NotHighPerformance", resourceCulture);
- }
- }
}
}
diff --git a/src/TableCloth.Core/Resources/InfoStrings.ko.resx b/src/TableCloth.Core/Resources/InfoStrings.ko.resx
index 51f200c0..83f7f6d3 100644
--- a/src/TableCloth.Core/Resources/InfoStrings.ko.resx
+++ b/src/TableCloth.Core/Resources/InfoStrings.ko.resx
@@ -132,7 +132,4 @@
바로 가기에 지정할 수 있는 최대 명령어 길이 제한으로 처음 선택한 사이트만 바로 가기로 만듭니다.
-
- 현재 이 PC의 전원 관리 옵션이 '고성능'으로 설정되어 있지 않습니다. Windows Sandbox 시작이 느려질 수 있습니다. 더 빠른 실행 경험을 위해 제어판에서 '고성능' 전원 관리 옵션으로 변경하는 것을 권장합니다.
-
\ No newline at end of file
diff --git a/src/TableCloth.Core/Resources/InfoStrings.resx b/src/TableCloth.Core/Resources/InfoStrings.resx
index 53691d10..cfcafe58 100644
--- a/src/TableCloth.Core/Resources/InfoStrings.resx
+++ b/src/TableCloth.Core/Resources/InfoStrings.resx
@@ -132,7 +132,4 @@
Because of the maximum command length limit that can be specified for a shortcut, only the first selected site is made a shortcut.
-
- The current power plan on this PC is not set to 'High performance'. Windows Sandbox may start slowly. For a faster experience, consider switching to the 'High performance' power plan in Control Panel.
-
\ No newline at end of file
diff --git a/src/TableCloth.Core/Resources/UIStringResources.Designer.cs b/src/TableCloth.Core/Resources/UIStringResources.Designer.cs
index 58cf6985..aedcb7be 100644
--- a/src/TableCloth.Core/Resources/UIStringResources.Designer.cs
+++ b/src/TableCloth.Core/Resources/UIStringResources.Designer.cs
@@ -1984,6 +1984,54 @@ public static string QuickStart_DataDirectory_OpenMissingPrompt {
}
}
+ public static string PowerSchemeGuide_Title {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_Title", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_Heading {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_Heading", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_Body {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_Body", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_OpenClassicButton {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_OpenClassicButton", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_OpenClassicDescription {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_OpenClassicDescription", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_OpenModernButton {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_OpenModernButton", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_OpenModernDescription {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_OpenModernDescription", resourceCulture);
+ }
+ }
+
+ public static string PowerSchemeGuide_CloseButton {
+ get {
+ return ResourceManager.GetString("PowerSchemeGuide_CloseButton", resourceCulture);
+ }
+ }
+
public static string Options_Title {
get {
return ResourceManager.GetString("Options_Title", resourceCulture);
diff --git a/src/TableCloth.Core/Resources/UIStringResources.ko.resx b/src/TableCloth.Core/Resources/UIStringResources.ko.resx
index e433e6b6..c04bfbf3 100644
--- a/src/TableCloth.Core/Resources/UIStringResources.ko.resx
+++ b/src/TableCloth.Core/Resources/UIStringResources.ko.resx
@@ -865,6 +865,30 @@ https://github.com/yourtablecloth/TableCloth
지금 만들어서 열까요?
+
+ Windows Sandbox 성능 안내
+
+
+ 현재 CPU 최대 성능이 제한되어 있습니다
+
+
+ 최대 프로세서 상태가 100% 미만으로 설정되어 있어(절전 모드이거나 배터리로 실행 중일 수 있습니다) Windows Sandbox 시작과 실행이 느려질 수 있습니다. 더 쾌적하게 사용하려면 전원(AC)에 연결하거나 최대 프로세서 상태를 높여 주세요. 아래에서 전원 설정을 바로 열 수 있습니다.
+
+
+ 제어판 전원 옵션 열기 (powercfg.cpl)
+
+
+ 전원 관리 옵션을 변경하거나 사용자 지정할 수 있는 기존 제어판 창을 엽니다.
+
+
+ 설정 열기 (전원 및 절전)
+
+
+ Windows 설정의 최신 전원 페이지를 엽니다.
+
+
+ 나중에
+
옵션
diff --git a/src/TableCloth.Core/Resources/UIStringResources.resx b/src/TableCloth.Core/Resources/UIStringResources.resx
index 4e123112..cff439c7 100644
--- a/src/TableCloth.Core/Resources/UIStringResources.resx
+++ b/src/TableCloth.Core/Resources/UIStringResources.resx
@@ -866,6 +866,30 @@ Create it now and continue?
Create it and open it now?
+
+ Windows Sandbox performance notice
+
+
+ Your CPU's maximum performance is currently limited
+
+
+ The maximum processor state is set below 100% (for example, a power-saver plan or running on battery), so Windows Sandbox may start and run slowly. For a smoother experience, connect to AC power or raise the maximum processor state. You can open the power settings below.
+
+
+ Open Control Panel power options (powercfg.cpl)
+
+
+ Opens the classic Power Options window, where you can switch or customize your power plan.
+
+
+ Open Settings (Power & sleep)
+
+
+ Opens the modern Windows Settings power page.
+
+
+ Later
+
Options
diff --git a/src/TableCloth.Test/PowerSchemeTests.cs b/src/TableCloth.Test/PowerSchemeTests.cs
deleted file mode 100644
index b48e8f0d..00000000
--- a/src/TableCloth.Test/PowerSchemeTests.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using System;
-using TableCloth;
-
-namespace TableCloth.Test
-{
- [TestClass]
- public sealed class PowerSchemeTests
- {
- // Windows built-in "High performance" power scheme GUID.
- private static readonly Guid HighPerformanceGuid =
- new Guid("8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c");
-
- // Windows built-in "Ultimate Performance" power scheme GUID.
- private static readonly Guid UltimatePerformanceGuid =
- new Guid("e9a42b02-d5df-448d-aa00-03f14749eb61");
-
- // Windows built-in "Balanced" power scheme GUID.
- private static readonly Guid BalancedGuid =
- new Guid("381b4222-f694-41f0-9685-ff5bb260df2e");
-
- // Windows built-in "Power saver" power scheme GUID.
- private static readonly Guid PowerSaverGuid =
- new Guid("a1841308-3541-4fab-bc81-f71556f20b4a");
-
- [TestMethod]
- public void IsHighPerformancePowerScheme_HighPerformance_ReturnsTrue()
- {
- Assert.IsTrue(Helpers.IsHighPerformancePowerScheme(HighPerformanceGuid));
- }
-
- [TestMethod]
- public void IsHighPerformancePowerScheme_UltimatePerformance_ReturnsTrue()
- {
- Assert.IsTrue(Helpers.IsHighPerformancePowerScheme(UltimatePerformanceGuid));
- }
-
- [TestMethod]
- public void IsHighPerformancePowerScheme_Balanced_ReturnsFalse()
- {
- Assert.IsFalse(Helpers.IsHighPerformancePowerScheme(BalancedGuid));
- }
-
- [TestMethod]
- public void IsHighPerformancePowerScheme_PowerSaver_ReturnsFalse()
- {
- Assert.IsFalse(Helpers.IsHighPerformancePowerScheme(PowerSaverGuid));
- }
-
- [TestMethod]
- public void IsHighPerformancePowerScheme_EmptyGuid_ReturnsFalse()
- {
- Assert.IsFalse(Helpers.IsHighPerformancePowerScheme(Guid.Empty));
- }
- }
-}
diff --git a/src/TableCloth.Test/ProcessorThrottleTests.cs b/src/TableCloth.Test/ProcessorThrottleTests.cs
new file mode 100644
index 00000000..1da98afc
--- /dev/null
+++ b/src/TableCloth.Test/ProcessorThrottleTests.cs
@@ -0,0 +1,45 @@
+using TableCloth;
+
+namespace TableCloth.Test
+{
+ ///
+ /// 전원 옵션 "이름"이 아니라 실제 CPU 최대 성능 상한(최대 프로세서 상태, %)으로 스로틀 여부를 판정하는
+ /// 순수 분류기()를 검증한다. 실제 P/Invoke 판독은
+ /// 환경 의존적이라 여기서는 다루지 않고, 값 → 판정 로직만 확인한다.
+ ///
+ [TestClass]
+ public sealed class ProcessorThrottleTests
+ {
+ [TestMethod]
+ public void IsProcessorStateThrottled_FullHundredPercent_ReturnsFalse()
+ {
+ // AC의 '균형 조정'을 포함해 최대 프로세서 상태가 100%면 제한되지 않은 것으로 본다(경고 안 함).
+ Assert.IsFalse(Helpers.IsProcessorStateThrottled(100));
+ }
+
+ [TestMethod]
+ public void IsProcessorStateThrottled_JustBelowHundred_ReturnsTrue()
+ {
+ Assert.IsTrue(Helpers.IsProcessorStateThrottled(99));
+ }
+
+ [TestMethod]
+ public void IsProcessorStateThrottled_Half_ReturnsTrue()
+ {
+ Assert.IsTrue(Helpers.IsProcessorStateThrottled(50));
+ }
+
+ [TestMethod]
+ public void IsProcessorStateThrottled_Zero_ReturnsTrue()
+ {
+ Assert.IsTrue(Helpers.IsProcessorStateThrottled(0));
+ }
+
+ [TestMethod]
+ public void IsProcessorStateThrottled_AboveHundred_ReturnsFalse()
+ {
+ // 100을 초과하는 값은 발생하지 않지만, 방어적으로 "제한 아님"으로 처리한다.
+ Assert.IsFalse(Helpers.IsProcessorStateThrottled(101));
+ }
+ }
+}