fix: persist first-run folder paths only
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<!--
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user