feat: add two-folder first-run playout setup
This commit is contained in:
@@ -70,7 +70,13 @@ public partial class App : Application
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Development Live bootstrap: {developmentLive.Code}");
|
||||
|
||||
_window = new MainWindow();
|
||||
// A fresh clone cannot become a full runtime inside the already compiled
|
||||
// source-only process. Show only the native two-folder setup window; the
|
||||
// reviewed initializer builds the verified runtime for the next F5.
|
||||
var showFirstRunSetup = isSourceOnlyBuild && isDebugBuild;
|
||||
_window = showFirstRunSetup
|
||||
? new FirstRunSetupWindow()
|
||||
: new MainWindow();
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
|
||||
123
src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
Normal file
123
src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
Normal file
@@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="MBN_STOCK_WEBVIEW.LegacyParityApp.FirstRunSetupWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="V-STOCK 첫 실행 설정">
|
||||
|
||||
<Grid Background="#FF08182B">
|
||||
<ScrollViewer>
|
||||
<StackPanel
|
||||
Width="720"
|
||||
Margin="36"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="20">
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
FontFamily="Segoe UI Variable Display"
|
||||
FontSize="30"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="첫 실행 설정" />
|
||||
<TextBlock
|
||||
FontSize="15"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="기존 코더의 Cuts 폴더와 여러 INI가 들어 있는 Res 폴더만 선택하면 됩니다."
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="#FF102640"
|
||||
BorderBrush="#334D79A7"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="1. Cuts 폴더" />
|
||||
<TextBlock
|
||||
x:Name="CutsPathText"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="선택되지 않음"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="ChooseCutsButton"
|
||||
HorizontalAlignment="Left"
|
||||
Click="OnChooseCutsClicked"
|
||||
Content="Cuts 폴더 선택" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="#FF102640"
|
||||
BorderBrush="#334D79A7"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Text="2. 설정/INI 폴더 (Res)" />
|
||||
<TextBlock
|
||||
Foreground="#FF8FAAC5"
|
||||
Text="MmoneyCoder.ini와 종목·업종·해외·환율·지수·종목비교 INI를 함께 확인합니다."
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Name="ResourcePathText"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="선택되지 않음"
|
||||
TextWrapping="Wrap" />
|
||||
<Button
|
||||
x:Name="ChooseResourceButton"
|
||||
HorizontalAlignment="Left"
|
||||
Click="OnChooseResourceClicked"
|
||||
Content="Res 폴더 선택" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<InfoBar
|
||||
x:Name="SetupStatusBar"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="두 폴더를 선택해 주세요."
|
||||
Severity="Informational"
|
||||
Title="준비" />
|
||||
|
||||
<ProgressBar
|
||||
x:Name="SetupProgress"
|
||||
Height="4"
|
||||
IsIndeterminate="True"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<Button
|
||||
x:Name="CloseButton"
|
||||
Click="OnCloseClicked"
|
||||
Content="닫기"
|
||||
Visibility="Collapsed" />
|
||||
<Button
|
||||
x:Name="InstallButton"
|
||||
Click="OnInstallClicked"
|
||||
Content="설정 시작"
|
||||
IsEnabled="False"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock
|
||||
Foreground="#FF8FAAC5"
|
||||
FontSize="13"
|
||||
Text="설정 중에는 DB나 Tornado2에 연결하지 않습니다. 완료 후 Visual Studio에서 F5를 한 번 더 누르면 실제 개발 송출 모드로 시작됩니다."
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,551 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Windows.Graphics;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
|
||||
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 string? _cutsDirectory;
|
||||
private string? _resourceDirectory;
|
||||
private bool _setupRunning;
|
||||
private readonly AppWindow _appWindow;
|
||||
|
||||
public FirstRunSetupWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_appWindow = ResolveAppWindow();
|
||||
_appWindow.Resize(new SizeInt32(820, 720));
|
||||
_appWindow.Closing += OnAppWindowClosing;
|
||||
}
|
||||
|
||||
private async void OnChooseCutsClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectedPath = await PickFolderAsync();
|
||||
if (selectedPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cutsDirectory = null;
|
||||
CutsPathText.Text = "선택되지 않음";
|
||||
InstallButton.IsEnabled = false;
|
||||
var validation = LegacyOperatorFolderValidator.Validate(
|
||||
LegacyOperatorSettingsFolderKind.Scene,
|
||||
selectedPath);
|
||||
if (!validation.IsValid || validation.Folder is null)
|
||||
{
|
||||
ShowWarning(
|
||||
validation.WarningMessage ??
|
||||
"선택한 Cuts 폴더를 검증할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
_cutsDirectory = validation.Folder.CanonicalPath;
|
||||
CutsPathText.Text = _cutsDirectory;
|
||||
RefreshReadyState();
|
||||
}
|
||||
|
||||
private async void OnChooseResourceClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var selectedPath = await PickFolderAsync();
|
||||
if (selectedPath is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_resourceDirectory = null;
|
||||
ResourcePathText.Text = "선택되지 않음";
|
||||
InstallButton.IsEnabled = false;
|
||||
var validation = LegacyOperatorFolderValidator.Validate(
|
||||
LegacyOperatorSettingsFolderKind.Resource,
|
||||
selectedPath);
|
||||
if (!validation.IsValid || validation.Folder is null)
|
||||
{
|
||||
ShowWarning(
|
||||
validation.WarningMessage ??
|
||||
"선택한 Res 폴더를 검증할 수 없습니다.");
|
||||
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)
|
||||
{
|
||||
if (_setupRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryResolveRuntimeRoot(
|
||||
_cutsDirectory,
|
||||
_resourceDirectory,
|
||||
out var runtimeRoot,
|
||||
out var warningMessage))
|
||||
{
|
||||
ShowWarning(warningMessage ?? "두 폴더를 다시 확인해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
string repositoryRoot;
|
||||
try
|
||||
{
|
||||
repositoryRoot = ResolveRepositoryRoot();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowError(
|
||||
"현재 Git 소스 위치를 확인할 수 없습니다. Visual Studio에서 이 솔루션을 다시 빌드해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetSetupRunning(true);
|
||||
SetupStatusBar.Title = "설정 중";
|
||||
SetupStatusBar.Message =
|
||||
"자산·DB 설정·K3D를 검증하고 있습니다. 완료될 때까지 Visual Studio 실행을 중지하지 마세요.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Informational;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await RunSetupAsync(
|
||||
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;
|
||||
}
|
||||
|
||||
SetupStatusBar.Title = "설정 완료";
|
||||
SetupStatusBar.Message =
|
||||
"이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요. Development Live로 실행됩니다.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Success;
|
||||
InstallButton.Visibility = Visibility.Collapsed;
|
||||
CloseButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowError(
|
||||
"첫 실행 설정을 시작하지 못했습니다. Visual Studio와 Windows PowerShell 설치 상태를 확인해 주세요.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetSetupRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCloseClicked(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnAppWindowClosing(
|
||||
AppWindow sender,
|
||||
AppWindowClosingEventArgs args)
|
||||
{
|
||||
if (!_setupRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Cancel = true;
|
||||
ShowWarning("설정이 끝날 때까지 창을 닫지 마세요.");
|
||||
}
|
||||
|
||||
private async Task<string?> PickFolderAsync()
|
||||
{
|
||||
if (_setupRunning)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var picker = new FolderPicker
|
||||
{
|
||||
SuggestedStartLocation = PickerLocationId.ComputerFolder,
|
||||
ViewMode = PickerViewMode.List
|
||||
};
|
||||
picker.FileTypeFilter.Add("*");
|
||||
InitializeWithWindow.Initialize(
|
||||
picker,
|
||||
WindowNative.GetWindowHandle(this));
|
||||
var selected = await picker.PickSingleFolderAsync();
|
||||
return selected?.Path;
|
||||
}
|
||||
catch
|
||||
{
|
||||
ShowError("폴더 선택 창을 열 수 없습니다.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshReadyState()
|
||||
{
|
||||
if (_cutsDirectory is null || _resourceDirectory is null)
|
||||
{
|
||||
InstallButton.IsEnabled = false;
|
||||
SetupStatusBar.Title = "준비";
|
||||
SetupStatusBar.Message = "두 폴더를 모두 선택해 주세요.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Informational;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryResolveRuntimeRoot(
|
||||
_cutsDirectory,
|
||||
_resourceDirectory,
|
||||
out _,
|
||||
out var warningMessage))
|
||||
{
|
||||
InstallButton.IsEnabled = false;
|
||||
ShowWarning(warningMessage ?? "두 폴더를 다시 확인해 주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
InstallButton.IsEnabled = true;
|
||||
SetupStatusBar.Title = "준비 완료";
|
||||
SetupStatusBar.Message =
|
||||
"설정 시작을 누르면 검증된 파일만 이 PC의 로컬 실행 영역에 구성합니다.";
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Success;
|
||||
}
|
||||
|
||||
private static bool TryResolveRuntimeRoot(
|
||||
string? cutsDirectory,
|
||||
string? resourceDirectory,
|
||||
out string? runtimeRoot,
|
||||
out string? warningMessage)
|
||||
{
|
||||
runtimeRoot = null;
|
||||
warningMessage = null;
|
||||
if (string.IsNullOrWhiteSpace(cutsDirectory) ||
|
||||
string.IsNullOrWhiteSpace(resourceDirectory))
|
||||
{
|
||||
warningMessage = "Cuts 폴더와 Res 폴더를 모두 선택해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var cuts = Path.TrimEndingDirectorySeparator(Path.GetFullPath(cutsDirectory));
|
||||
var resource = Path.TrimEndingDirectorySeparator(
|
||||
Path.GetFullPath(resourceDirectory));
|
||||
if (!string.Equals(
|
||||
Path.GetFileName(cuts),
|
||||
"Cuts",
|
||||
StringComparison.OrdinalIgnoreCase) ||
|
||||
!string.Equals(
|
||||
Path.GetFileName(resource),
|
||||
"Res",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
warningMessage =
|
||||
"폴더 이름이 각각 Cuts와 Res인 실제 실행 자산 폴더를 선택해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var cutsParent = Path.GetDirectoryName(cuts);
|
||||
var resourceParent = Path.GetDirectoryName(resource);
|
||||
if (string.IsNullOrWhiteSpace(cutsParent) ||
|
||||
!string.Equals(
|
||||
cutsParent,
|
||||
resourceParent,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
warningMessage =
|
||||
"같은 위치에 나란히 있는 Cuts 폴더와 Res 폴더를 선택해 주세요.";
|
||||
return false;
|
||||
}
|
||||
|
||||
runtimeRoot = cutsParent;
|
||||
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
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.Single(attribute => string.Equals(
|
||||
attribute.Key,
|
||||
RepositoryRootMetadataKey,
|
||||
StringComparison.Ordinal));
|
||||
if (string.IsNullOrWhiteSpace(metadata.Value))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The source repository metadata is empty.");
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The source repository metadata is not usable.");
|
||||
}
|
||||
|
||||
return repositoryRoot;
|
||||
}
|
||||
|
||||
private static async Task<FirstRunSetupResult> RunSetupAsync(
|
||||
string repositoryRoot,
|
||||
string runtimeRoot,
|
||||
string resourceDirectory,
|
||||
bool replaceExistingLocalConfiguration)
|
||||
{
|
||||
var setupScript = Path.Combine(
|
||||
repositoryRoot,
|
||||
SetupScriptRelativePath);
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The Windows system directory is unavailable.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadBoundedTailAsync(StreamReader reader)
|
||||
{
|
||||
var tail = new StringBuilder(MaximumDiagnosticCharacters);
|
||||
var buffer = new char[4096];
|
||||
int count;
|
||||
while ((count = await reader.ReadAsync(buffer)) > 0)
|
||||
{
|
||||
tail.Append(buffer, 0, count);
|
||||
if (tail.Length > MaximumDiagnosticCharacters)
|
||||
{
|
||||
tail.Remove(
|
||||
0,
|
||||
tail.Length - MaximumDiagnosticCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
return tail.ToString();
|
||||
}
|
||||
|
||||
private static string? LastDiagnosticLine(string diagnostic)
|
||||
{
|
||||
var line = diagnostic
|
||||
.Split(
|
||||
['\r', '\n'],
|
||||
StringSplitOptions.RemoveEmptyEntries |
|
||||
StringSplitOptions.TrimEntries)
|
||||
.LastOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return line.Length <= 400 ? line : string.Concat(line[..397], "...");
|
||||
}
|
||||
|
||||
private void SetSetupRunning(bool running)
|
||||
{
|
||||
_setupRunning = running;
|
||||
ChooseCutsButton.IsEnabled = !running;
|
||||
ChooseResourceButton.IsEnabled = !running;
|
||||
InstallButton.IsEnabled = !running &&
|
||||
_cutsDirectory is not null &&
|
||||
_resourceDirectory is not null;
|
||||
SetupProgress.Visibility = running
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void ShowWarning(string message)
|
||||
{
|
||||
SetupStatusBar.Title = "확인 필요";
|
||||
SetupStatusBar.Message = message;
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Warning;
|
||||
}
|
||||
|
||||
private void ShowError(string message)
|
||||
{
|
||||
SetupStatusBar.Title = "설정 실패";
|
||||
SetupStatusBar.Message = message;
|
||||
SetupStatusBar.Severity = InfoBarSeverity.Error;
|
||||
}
|
||||
|
||||
private AppWindow ResolveAppWindow()
|
||||
{
|
||||
var windowHandle = WindowNative.GetWindowHandle(this);
|
||||
var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(windowHandle);
|
||||
return AppWindow.GetFromWindowId(windowId);
|
||||
}
|
||||
|
||||
private sealed record FirstRunSetupResult(
|
||||
bool Succeeded,
|
||||
string? Diagnostic,
|
||||
bool LocalConfigurationReplacementRequired);
|
||||
}
|
||||
@@ -24,18 +24,24 @@
|
||||
<ApplicationDisplayVersion>0.1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
<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.
|
||||
-->
|
||||
<SourceRepositoryRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))</SourceRepositoryRoot>
|
||||
<!--
|
||||
Keep the legacy runtime assets outside this repository while reproducing the
|
||||
original executable-folder layout. A fresh source checkout builds in safe
|
||||
source-only DryRun mode when the sibling runtime tree is absent. Supplying an
|
||||
explicit root or creating a Release MSIX makes the assets required unless the
|
||||
caller explicitly selects Auto mode:
|
||||
original executable-folder layout. A fresh source checkout always builds the
|
||||
safe source-only first-run app until its generated local binding exists.
|
||||
Supplying an explicit root or creating a Release MSIX makes the assets
|
||||
required unless the caller explicitly selects Auto mode:
|
||||
/p:LegacyRuntimeSourceRoot="D:\path\to\MBN_STOCK_N\bin\Debug"
|
||||
/p:LegacyRuntimeAssetsMode=Auto
|
||||
-->
|
||||
<LegacyRuntimeSourceRootWasSpecified
|
||||
Condition="'$(LegacyRuntimeSourceRoot)' != ''">true</LegacyRuntimeSourceRootWasSpecified>
|
||||
<LegacyRuntimeSourceRoot Condition="'$(LegacyRuntimeSourceRoot)' == ''">$(MSBuildProjectDirectory)\..\..\..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug</LegacyRuntimeSourceRoot>
|
||||
<LegacyRuntimeSourceRoot Condition="'$(LegacyRuntimeSourceRoot)' == ''">$(MSBuildProjectDirectory)\.first-run-runtime-not-configured-0F72B3C8</LegacyRuntimeSourceRoot>
|
||||
<LegacyRuntimeSourceRoot>$([System.IO.Path]::GetFullPath('$(LegacyRuntimeSourceRoot)'))</LegacyRuntimeSourceRoot>
|
||||
<LegacyRuntimeAssetsMode
|
||||
Condition="'$(LegacyRuntimeAssetsMode)' == '' and
|
||||
@@ -69,6 +75,13 @@
|
||||
<LegacyResSourceRoot>$(LegacyRuntimeSourceRoot)\Res</LegacyResSourceRoot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyMetadata Include="SourceRepositoryRoot"
|
||||
Value="$(SourceRepositoryRoot)"
|
||||
Condition="'$(LegacyRuntimeAssetsEnabled)' != 'true' and
|
||||
'$(Configuration)' == 'Debug'" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Closed runtime contract: every active alias and non-external scene asset must exist. -->
|
||||
<LegacyRequiredScene Include="5001;N5001;5006;5011;5016;50160;5023;5024;5025;5026;5029;8018;8032;5032;5037;5074;5076;5077;5078;5079;5080;5081;5082;5083;5084;5085;5086;50860;5087;5088;6001;6067;8001;8002;8003;8035;8061;8040;8046;8051;8056;8067;5068;5070;5072" />
|
||||
@@ -193,7 +206,7 @@
|
||||
<Message Condition="'$(LegacyRuntimeAssetsMode)' == 'Auto' and
|
||||
'$(LegacyRuntimeAssetsEnabled)' != 'true'"
|
||||
Importance="high"
|
||||
Text="Legacy runtime assets were not found. Building the safe source-only DryRun app. Run scripts\Initialize-ExistingDevelopmentPc.ps1 before Development Live." />
|
||||
Text="Legacy runtime assets were not found. Building the safe first-run setup app. Start Debug x64 and select the existing Cuts and Res folders." />
|
||||
<Message Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'"
|
||||
Importance="high"
|
||||
Text="Verified full legacy runtime assets are enabled. Database credentials remain in the current user's LocalAppData overlay." />
|
||||
|
||||
Reference in New Issue
Block a user