fix: persist first-run folder paths only

This commit is contained in:
2026-07-28 01:03:55 +09:00
parent 347900701b
commit 11d3849933
13 changed files with 240 additions and 666 deletions

View File

@@ -23,7 +23,7 @@
<TextBlock
FontSize="15"
Foreground="#FFBFD2E6"
Text="기존 코더의 Cuts 폴더와 여러 INI가 들어 있는 Res 폴더 선택하면 니다."
Text="기존 코더의 Cuts 폴더와 Res 폴더 선택하면 해당 경로만 저장합니다."
TextWrapping="Wrap" />
</StackPanel>
@@ -66,7 +66,7 @@
Text="2. 설정/INI 폴더 (Res)" />
<TextBlock
Foreground="#FF8FAAC5"
Text="MmoneyCoder.ini와 종목·업종·해외·환율·지수·종목비교 INI를 함께 확인합니다."
Text="폴더 안의 파일 전체를 검사하거나 복사하지 않고 선택한 경로를 그대로 사용합니다."
TextWrapping="Wrap" />
<TextBlock
x:Name="ResourcePathText"
@@ -115,7 +115,7 @@
<TextBlock
Foreground="#FF8FAAC5"
FontSize="13"
Text="설정 중에는 DB나 Tornado2에 연결하지 않습니다. 완료 후 Visual Studio에서 F5를 한 번 더 누르면 실제 개발 송출 모드로 시작니다."
Text="설정 시작은 경로만 저장합니다. 빌드, DB, K3D 또는 Tornado2 작업은 실행하지 않으며 완료 후 Visual Studio에서 F5를 한 번 더 누르면 앱을 빌드해 시작니다."
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>

View File

@@ -1,7 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Text;
using MBN_STOCK_WEBVIEW.Infrastructure;
using System.Xml;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using Microsoft.UI.Windowing;
using Windows.Graphics;
@@ -13,11 +12,8 @@ namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
public sealed partial class FirstRunSetupWindow : Window
{
private const string RepositoryRootMetadataKey = "SourceRepositoryRoot";
private const string SetupScriptRelativePath =
@"scripts\Initialize-ExistingDevelopmentPc.ps1";
private const string DefaultPlayoutHost = "127.0.0.1";
private const int DefaultPlayoutPort = 30001;
private const int MaximumDiagnosticCharacters = 32 * 1024;
private const string LocalBuildPropsFileName =
"Directory.Build.local.props";
private string? _cutsDirectory;
private string? _resourceDirectory;
@@ -81,20 +77,12 @@ public sealed partial class FirstRunSetupWindow : Window
return;
}
if (!validation.Folder.DatabaseIniDetected ||
!CanReadDatabaseIni(validation.Folder.CanonicalPath))
{
ShowWarning(
"선택한 Res 폴더의 MmoneyCoder.ini를 검증할 수 없습니다.");
return;
}
_resourceDirectory = validation.Folder.CanonicalPath;
ResourcePathText.Text = _resourceDirectory;
RefreshReadyState();
}
private async void OnInstallClicked(object sender, RoutedEventArgs e)
private void OnInstallClicked(object sender, RoutedEventArgs e)
{
if (_setupRunning)
{
@@ -124,52 +112,22 @@ public sealed partial class FirstRunSetupWindow : Window
}
SetSetupRunning(true);
SetupStatusBar.Title = "설정 중";
SetupStatusBar.Title = "경로 저장 중";
SetupStatusBar.Message =
"선택한 경로와 DB 설정·K3D를 적용하고 있습니다. 완료될 때까지 Visual Studio 실행을 중지하지 마세요.";
"선택한 Cuts/Res 폴더 경로를 저장하고 있습니다.";
SetupStatusBar.Severity = InfoBarSeverity.Informational;
try
{
var result = await RunSetupAsync(
SaveSelectedRuntimePaths(
repositoryRoot,
runtimeRoot!,
_resourceDirectory!,
replaceExistingLocalConfiguration: false);
if (!result.Succeeded &&
result.LocalConfigurationReplacementRequired)
{
var replaceConfirmed =
await ConfirmLocalConfigurationReplacementAsync();
if (!replaceConfirmed)
{
ShowWarning(
"기존 로컬 설정은 변경하지 않았습니다. 다시 진행하려면 설정 시작을 눌러 주세요.");
return;
}
SetupStatusBar.Title = "기존 설정 업데이트 중";
SetupStatusBar.Message =
"선택한 Cuts/Res와 기본 Tornado2 설정으로 이 앱의 로컬 설정을 갱신하고 있습니다.";
result = await RunSetupAsync(
repositoryRoot,
runtimeRoot!,
_resourceDirectory!,
replaceExistingLocalConfiguration: true);
}
if (!result.Succeeded)
{
ShowError(
string.IsNullOrWhiteSpace(result.Diagnostic)
? "첫 실행 설정에 실패했습니다. 선택한 폴더와 K3D 설치 상태를 확인해 주세요."
: $"첫 실행 설정에 실패했습니다. {result.Diagnostic}");
return;
}
_cutsDirectory!,
_resourceDirectory!);
SetupStatusBar.Title = "설정 완료";
SetupStatusBar.Message =
"이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요. Development Live로 실행됩니다.";
"폴더 경로를 저장했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
SetupStatusBar.Severity = InfoBarSeverity.Success;
InstallButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Visible;
@@ -177,7 +135,7 @@ public sealed partial class FirstRunSetupWindow : Window
catch
{
ShowError(
"첫 실행 설정을 시작하지 못했습니다. Visual Studio와 Windows PowerShell 설치 상태를 확인해 주세요.");
"폴더 경로를 저장하지 못했습니다. 폴더와 Git 소스 위치를 확인해 주세요.");
}
finally
{
@@ -313,20 +271,6 @@ public sealed partial class FirstRunSetupWindow : Window
return true;
}
private static bool CanReadDatabaseIni(string resourceDirectory)
{
try
{
_ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
Path.Combine(resourceDirectory, "MmoneyCoder.ini"));
return true;
}
catch
{
return false;
}
}
private static string ResolveRepositoryRoot()
{
var metadata = typeof(FirstRunSetupWindow).Assembly
@@ -343,15 +287,12 @@ public sealed partial class FirstRunSetupWindow : Window
var repositoryRoot = Path.TrimEndingDirectorySeparator(
Path.GetFullPath(metadata.Value));
var setupScript = Path.Combine(
repositoryRoot,
SetupScriptRelativePath);
var project = Path.Combine(
repositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MBN_STOCK_WEBVIEW.LegacyParityApp.csproj");
if (!File.Exists(setupScript) || !File.Exists(project))
if (!File.Exists(project))
{
throw new InvalidOperationException(
"The source repository metadata is not usable.");
@@ -360,161 +301,119 @@ public sealed partial class FirstRunSetupWindow : Window
return repositoryRoot;
}
private static async Task<FirstRunSetupResult> RunSetupAsync(
private static void SaveSelectedRuntimePaths(
string repositoryRoot,
string runtimeRoot,
string resourceDirectory,
bool replaceExistingLocalConfiguration)
string cutsDirectory,
string resourceDirectory)
{
var setupScript = Path.Combine(
repositoryRoot,
SetupScriptRelativePath);
var startInfo = new ProcessStartInfo
var store = new LegacyOperatorSettingsStore();
var loaded = store.Load();
var current = loaded.Failure == LegacyOperatorSettingsStoreFailure.None
? loaded.Settings
: LegacyOperatorSettings.Default;
var saved = store.Save(current with
{
FileName = ResolveWindowsPowerShellPath(),
WorkingDirectory = repositoryRoot,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
foreach (var argument in new[]
{
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
setupScript,
"-LegacyRuntimeSourceRoot",
runtimeRoot,
"-NoFolderPicker",
"-DatabaseIniPath",
Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
"-ConfigureDevelopmentLive",
"-PlayoutHost",
DefaultPlayoutHost,
"-PlayoutPort",
DefaultPlayoutPort.ToString(
System.Globalization.CultureInfo.InvariantCulture),
"-PinRegisteredK3D"
})
{
startInfo.ArgumentList.Add(argument);
}
if (replaceExistingLocalConfiguration)
{
startInfo.ArgumentList.Add("-ReplaceRuntimeBinding");
startInfo.ArgumentList.Add("-ReplaceDatabaseProfile");
startInfo.ArgumentList.Add("-ReplaceLiveConfig");
}
using var process = Process.Start(startInfo) ??
throw new InvalidOperationException("Windows PowerShell did not start.");
var standardOutput = ReadBoundedTailAsync(process.StandardOutput);
var standardError = ReadBoundedTailAsync(process.StandardError);
await process.WaitForExitAsync();
await Task.WhenAll(standardOutput, standardError);
var diagnosticText =
string.IsNullOrWhiteSpace(standardError.Result)
? standardOutput.Result
: standardError.Result;
return new FirstRunSetupResult(
process.ExitCode == 0,
LastDiagnosticLine(diagnosticText),
RequiresLocalConfigurationReplacement(diagnosticText));
}
private async Task<bool> ConfirmLocalConfigurationReplacementAsync()
{
var dialog = new ContentDialog
{
XamlRoot = Content.XamlRoot,
Title = "기존 로컬 설정 업데이트",
Content =
"이 PC에 이 앱의 이전 runtime, DB 또는 Development Live 설정이 있습니다. " +
"선택한 Cuts/Res와 기본 Tornado2(127.0.0.1:30001) 설정으로 갱신할까요? " +
"K3D DLL 기준값은 자동으로 바꾸지 않습니다.",
PrimaryButtonText = "업데이트",
CloseButtonText = "취소",
DefaultButton = ContentDialogButton.Primary
};
return await dialog.ShowAsync() == ContentDialogResult.Primary;
}
private static bool RequiresLocalConfigurationReplacement(
string? diagnostic) =>
!string.IsNullOrWhiteSpace(diagnostic) &&
(diagnostic.Contains(
"-ReplaceRuntimeBinding",
StringComparison.Ordinal) ||
diagnostic.Contains(
"-ReplaceDatabaseProfile",
StringComparison.Ordinal) ||
diagnostic.Contains(
"-ReplaceLiveConfig",
StringComparison.Ordinal));
private static string ResolveWindowsPowerShellPath()
{
var systemDirectory = Environment.GetFolderPath(
Environment.SpecialFolder.System);
if (string.IsNullOrWhiteSpace(systemDirectory))
SceneDirectory = cutsDirectory,
ResourceDirectory = resourceDirectory
});
if (!saved.Succeeded)
{
throw new InvalidOperationException(
"The Windows system directory is unavailable.");
"The selected runtime folders could not be saved.");
}
var path = Path.GetFullPath(Path.Combine(
systemDirectory,
"WindowsPowerShell",
"v1.0",
"powershell.exe"));
if (!File.Exists(path))
{
throw new FileNotFoundException(
"Windows PowerShell 5.1 was not found.",
path);
}
return path;
SaveRuntimeBinding(repositoryRoot, runtimeRoot);
}
private static async Task<string> ReadBoundedTailAsync(StreamReader reader)
private static void SaveRuntimeBinding(
string repositoryRoot,
string runtimeRoot)
{
var tail = new StringBuilder(MaximumDiagnosticCharacters);
var buffer = new char[4096];
int count;
while ((count = await reader.ReadAsync(buffer)) > 0)
var destination = Path.Combine(
repositoryRoot,
LocalBuildPropsFileName);
if (Directory.Exists(destination))
{
tail.Append(buffer, 0, count);
if (tail.Length > MaximumDiagnosticCharacters)
throw new InvalidOperationException(
"The local runtime binding path is not a file.");
}
if (File.Exists(destination))
{
var attributes = File.GetAttributes(destination);
if ((attributes & (FileAttributes.Directory |
FileAttributes.ReparsePoint |
FileAttributes.Device)) != 0)
{
tail.Remove(
0,
tail.Length - MaximumDiagnosticCharacters);
throw new InvalidOperationException(
"The local runtime binding file is unsafe.");
}
}
return tail.ToString();
var bytes = CreateRuntimeBinding(runtimeRoot);
var temporary = Path.Combine(
repositoryRoot,
$".{LocalBuildPropsFileName}.{Guid.NewGuid():N}.tmp");
try
{
using (var stream = new FileStream(
temporary,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
FileOptions.WriteThrough))
{
stream.Write(bytes);
stream.Flush(flushToDisk: true);
}
File.Move(temporary, destination, overwrite: true);
temporary = string.Empty;
if (!File.ReadAllBytes(destination).AsSpan().SequenceEqual(bytes))
{
throw new IOException(
"The local runtime binding failed verification.");
}
}
finally
{
if (!string.IsNullOrEmpty(temporary) &&
File.Exists(temporary))
{
File.Delete(temporary);
}
}
}
private static string? LastDiagnosticLine(string diagnostic)
private static byte[] CreateRuntimeBinding(string runtimeRoot)
{
var line = diagnostic
.Split(
['\r', '\n'],
StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries)
.LastOrDefault();
if (string.IsNullOrWhiteSpace(line))
using var stream = new MemoryStream(capacity: 512);
var settings = new XmlWriterSettings
{
return null;
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
Indent = true,
NewLineChars = Environment.NewLine,
NewLineHandling = NewLineHandling.Replace
};
using (var writer = XmlWriter.Create(stream, settings))
{
writer.WriteStartDocument();
writer.WriteStartElement("Project");
writer.WriteStartElement("PropertyGroup");
writer.WriteElementString(
"LegacyRuntimeSourceRoot",
runtimeRoot);
writer.WriteElementString(
"LegacyRuntimeAssetsMode",
"Required");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
return line.Length <= 400 ? line : string.Concat(line[..397], "...");
return stream.ToArray();
}
private void SetSetupRunning(bool running)
@@ -551,8 +450,4 @@ public sealed partial class FirstRunSetupWindow : Window
return AppWindow.GetFromWindowId(windowId);
}
private sealed record FirstRunSetupResult(
bool Succeeded,
string? Diagnostic,
bool LocalConfigurationReplacementRequired);
}

View File

@@ -26,8 +26,8 @@
<Version>0.1.0</Version>
<!--
Source-only F5 runs from an MSIX layout, not necessarily from the clone.
Embed this local, non-secret build path so the first-run window can invoke
the reviewed setup scripts in the exact clone that Visual Studio built.
Embed this local, non-secret build path so the first-run window can write
the direct local runtime binding into the exact clone Visual Studio built.
-->
<SourceRepositoryRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))</SourceRepositoryRoot>
<!--

View File

@@ -1,7 +1,6 @@
using MBN_STOCK_WEBVIEW.Infrastructure;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using MBN_STOCK_WEBVIEW.LegacyBridge;
using MMoneyCoderSharp.Data;
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
@@ -107,16 +106,6 @@ public sealed partial class MainWindow
return;
}
if (kind == LegacyOperatorFolderKind.Resource &&
validated.DatabaseIniDetected &&
!CanReadSelectedDatabaseProfile(validated.CanonicalPath))
{
SetOperatorSettingsMessage(
"선택한 설정 폴더의 DB 설정 파일을 검증할 수 없습니다.",
LegacyOperatorSettingsMessageKind.Warning);
return;
}
var next = kind switch
{
LegacyOperatorFolderKind.Design => _operatorSettings with
@@ -270,20 +259,6 @@ public sealed partial class MainWindow
LegacyOperatorSettingsMessageKind.Success);
}
private static bool CanReadSelectedDatabaseProfile(string resourceDirectory)
{
try
{
_ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
Path.Combine(resourceDirectory, "MmoneyCoder.ini"));
return true;
}
catch (DatabaseConfigurationException)
{
return false;
}
}
private void SetOperatorSettingsMessage(
string message,
LegacyOperatorSettingsMessageKind kind)