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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +