fix: initialize selected runtime for live playout

This commit is contained in:
2026-07-28 20:44:20 +09:00
parent 18d40e892f
commit 818acad646
24 changed files with 906 additions and 391 deletions

View File

@@ -23,7 +23,7 @@
<TextBlock
FontSize="15"
Foreground="#FFBFD2E6"
Text="기존 코더의 Cuts 폴더와 Res 폴더를 선택하면 해당 경로만 저장합니다."
Text="기존 코더의 Cuts 폴더와 Res 폴더를 선택하면 이후 실행에서도 그 경로를 그대로 사용합니다."
TextWrapping="Wrap" />
</StackPanel>
@@ -66,7 +66,7 @@
Text="2. 설정/INI 폴더 (Res)" />
<TextBlock
Foreground="#FF8FAAC5"
Text="폴더 안의 파일 전체를 검사하거나 복사하지 않고 선택한 경로를 그대로 사용합니다."
Text="선택한 폴더의 MmoneyCoder.ini를 DB 설정으로 검증하고 계속 직접 사용합니다. 자격증명 파일은 복사하지 않습니다."
TextWrapping="Wrap" />
<TextBlock
x:Name="ResourcePathText"
@@ -115,7 +115,7 @@
<TextBlock
Foreground="#FF8FAAC5"
FontSize="13"
Text="설정 시작은 경로만 저장합니다. 빌드, DB, K3D 또는 Tornado2 작업은 실행하지 않으며 완료 후 Visual Studio에서 F5를 한 번 더 누르면 앱을 빌드해 시작합니다."
Text="설정 시작은 선택 경로, C:\K3DAsyncEngine의 x64 등록/파일, 보호된 Live 설정과 Debug x64 빌드를 검증합니다. 이 과정에서는 DB에 연결하거나 Tornado2/PGM 송출 명령을 보내지 않습니다."
TextWrapping="Wrap" />
</StackPanel>
</ScrollViewer>

View File

@@ -1,6 +1,6 @@
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Xml;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using Microsoft.UI.Windowing;
using Windows.Graphics;
@@ -12,8 +12,8 @@ namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
public sealed partial class FirstRunSetupWindow : Window
{
private const string RepositoryRootMetadataKey = "SourceRepositoryRoot";
private const string LocalBuildPropsFileName =
"Directory.Build.local.props";
private const string SetupScriptRelativePath =
@"scripts\Initialize-ExistingDevelopmentPc.ps1";
private string? _cutsDirectory;
private string? _resourceDirectory;
@@ -82,7 +82,7 @@ public sealed partial class FirstRunSetupWindow : Window
RefreshReadyState();
}
private void OnInstallClicked(object sender, RoutedEventArgs e)
private async void OnInstallClicked(object sender, RoutedEventArgs e)
{
if (_setupRunning)
{
@@ -112,14 +112,14 @@ public sealed partial class FirstRunSetupWindow : Window
}
SetSetupRunning(true);
SetupStatusBar.Title = "경로 저장 중";
SetupStatusBar.Title = "개발 PC 설정 중";
SetupStatusBar.Message =
"선택한 Cuts/Res 폴더 경로를 저장하고 있습니다.";
"DB 설정, 표준 K3D 설치, Live 승인과 Debug x64 빌드를 검증하고 있습니다.";
SetupStatusBar.Severity = InfoBarSeverity.Informational;
try
{
SaveSelectedRuntimePaths(
await RunVerifiedSetupAsync(
repositoryRoot,
runtimeRoot!,
_cutsDirectory!,
@@ -127,7 +127,7 @@ public sealed partial class FirstRunSetupWindow : Window
SetupStatusBar.Title = "설정 완료";
SetupStatusBar.Message =
"폴더 경로를 저장했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
"Cuts/Res, DB, K3D 및 Live 설정을 검증했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
SetupStatusBar.Severity = InfoBarSeverity.Success;
InstallButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Visible;
@@ -135,7 +135,7 @@ public sealed partial class FirstRunSetupWindow : Window
catch
{
ShowError(
"폴더 경로를 저장하지 못했습니다. 폴더와 Git 소스 위치를 확인해 주세요.");
"개발 PC 자동 설정에 실패했습니다. Res\\MmoneyCoder.ini, C:\\K3DAsyncEngine x64 등록, Tornado2의 127.0.0.1:30001 설정을 확인해 주세요.");
}
finally
{
@@ -211,7 +211,7 @@ public sealed partial class FirstRunSetupWindow : Window
InstallButton.IsEnabled = true;
SetupStatusBar.Title = "준비 완료";
SetupStatusBar.Message =
"설정 시작을 누르면 선택한 Cuts/Res 경로를 이 PC에 적용합니다.";
"설정 시작을 누르면 Cuts/Res, DB, 표준 K3D 및 Development Live를 검증합니다.";
SetupStatusBar.Severity = InfoBarSeverity.Success;
}
@@ -267,6 +267,18 @@ public sealed partial class FirstRunSetupWindow : Window
return false;
}
try
{
_ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
Path.Combine(resource, "MmoneyCoder.ini"));
}
catch (DatabaseConfigurationException)
{
warningMessage =
"선택한 Res 폴더의 MmoneyCoder.ini에 유효한 Oracle/MariaDB 설정이 모두 필요합니다.";
return false;
}
runtimeRoot = cutsParent;
return true;
}
@@ -301,119 +313,141 @@ public sealed partial class FirstRunSetupWindow : Window
return repositoryRoot;
}
private static void SaveSelectedRuntimePaths(
private static async Task RunVerifiedSetupAsync(
string repositoryRoot,
string runtimeRoot,
string cutsDirectory,
string resourceDirectory)
{
var store = new LegacyOperatorSettingsStore();
var loaded = store.Load();
var current = loaded.Failure == LegacyOperatorSettingsStoreFailure.None
? loaded.Settings
: LegacyOperatorSettings.Default;
var saved = store.Save(current with
var powershellPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.System),
"WindowsPowerShell",
"v1.0",
"powershell.exe");
var setupScriptPath = Path.Combine(
repositoryRoot,
SetupScriptRelativePath);
EnsureOrdinaryFile(
powershellPath,
"Windows PowerShell을 찾을 수 없습니다.");
EnsureOrdinaryFile(
setupScriptPath,
"검증된 개발 PC 초기화 스크립트를 찾을 수 없습니다.");
var startInfo = new ProcessStartInfo
{
SceneDirectory = cutsDirectory,
ResourceDirectory = resourceDirectory
});
if (!saved.Succeeded)
FileName = powershellPath,
WorkingDirectory = repositoryRoot,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
AddArgument("-NoLogo");
AddArgument("-NoProfile");
AddArgument("-NonInteractive");
AddArgument("-ExecutionPolicy");
AddArgument("Bypass");
AddArgument("-File");
AddArgument(setupScriptPath);
AddArgument("-LegacyRuntimeSourceRoot");
AddArgument(runtimeRoot);
AddArgument("-NoFolderPicker");
AddArgument("-SkipDatabaseProfile");
AddArgument("-ConfigureDevelopmentLive");
AddArgument("-PlayoutHost");
AddArgument("127.0.0.1");
AddArgument("-PlayoutPort");
AddArgument("30001");
AddArgument("-PinRegisteredK3D");
using var process = new Process
{
StartInfo = startInfo
};
if (!process.Start())
{
throw new InvalidOperationException(
"The selected runtime folders could not be saved.");
"The verified development-PC initializer could not be started.");
}
SaveRuntimeBinding(repositoryRoot, runtimeRoot);
var standardOutput = DrainOutputAsync(process.StandardOutput);
var standardError = DrainOutputAsync(process.StandardError);
await process.WaitForExitAsync();
await Task.WhenAll(standardOutput, standardError);
if (process.ExitCode != 0)
{
throw new InvalidOperationException(
$"The verified development-PC initializer failed with exit code {process.ExitCode}.");
}
var loaded = new LegacyOperatorSettingsStore().Load();
if (loaded.Failure != LegacyOperatorSettingsStoreFailure.None ||
!string.Equals(
loaded.Settings.SceneDirectory,
cutsDirectory,
StringComparison.OrdinalIgnoreCase) ||
!string.Equals(
loaded.Settings.ResourceDirectory,
resourceDirectory,
StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"The verified initializer did not retain the selected runtime folders.");
}
EnsureOrdinaryFile(
Path.Combine(repositoryRoot, "Directory.Build.local.props"),
"The verified initializer did not retain the local build binding.");
var localApplicationData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
var configurationRoot = Path.Combine(
localApplicationData,
"MBN_STOCK_WEBVIEW",
"Config");
foreach (var fileName in new[]
{
"k3d-pins.local.json",
"playout.local.json",
"playout.development-live.local.json"
})
{
EnsureOrdinaryFile(
Path.Combine(configurationRoot, fileName),
"The verified initializer did not retain every protected Live setting.");
}
void AddArgument(string value) => startInfo.ArgumentList.Add(value);
}
private static void SaveRuntimeBinding(
string repositoryRoot,
string runtimeRoot)
private static async Task DrainOutputAsync(StreamReader reader)
{
var destination = Path.Combine(
repositoryRoot,
LocalBuildPropsFileName);
if (Directory.Exists(destination))
var buffer = new char[4096];
while (await reader.ReadAsync(buffer.AsMemory()) > 0)
{
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)
{
throw new InvalidOperationException(
"The local runtime binding file is unsafe.");
}
}
var bytes = CreateRuntimeBinding(runtimeRoot);
var temporary = Path.Combine(
repositoryRoot,
$".{LocalBuildPropsFileName}.{Guid.NewGuid():N}.tmp");
private static void EnsureOrdinaryFile(string path, string message)
{
try
{
using (var stream = new FileStream(
temporary,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
FileOptions.WriteThrough))
var attributes = File.GetAttributes(Path.GetFullPath(path));
if ((attributes & (FileAttributes.Directory |
FileAttributes.ReparsePoint |
FileAttributes.Device)) == 0)
{
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.");
return;
}
}
finally
catch (Exception exception) when (
exception is ArgumentException or IOException or
UnauthorizedAccessException or NotSupportedException or
System.Security.SecurityException)
{
if (!string.IsNullOrEmpty(temporary) &&
File.Exists(temporary))
{
File.Delete(temporary);
}
}
}
private static byte[] CreateRuntimeBinding(string runtimeRoot)
{
using var stream = new MemoryStream(capacity: 512);
var settings = new XmlWriterSettings
{
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 stream.ToArray();
throw new InvalidOperationException(message);
}
private void SetSetupRunning(bool running)

View File

@@ -185,8 +185,8 @@
</Content>
<!--
The active legacy DB INI contains credentials and is never a build input or
MSIX Content. Local F5 and packaged development use the current user's
LocalAppData overlay. Historical copies, backups, archives and
MSIX Content. Development Live reads the exact selected external
Res\MmoneyCoder.ini at runtime. Historical copies, backups, archives and
afiedt.buf.txt are not included at all.
-->
<Manifest Include="$(ApplicationManifest)" />
@@ -202,7 +202,7 @@
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="External legacy runtime paths are enabled. Cuts files remain in the selected folder and database credentials remain in the current user's LocalAppData overlay." />
Text="External legacy runtime paths are enabled. Cuts and the credential-bearing Res\MmoneyCoder.ini remain in the selected external folders." />
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyRuntimeSourceRoot)')"
Text="Legacy runtime source root was not found: $(LegacyRuntimeSourceRoot). Run scripts\Initialize-ExistingDevelopmentPc.ps1 to create the verified local Required binding, or use /p:LegacyRuntimeAssetsMode=Auto for a source-only setup build." />
@@ -242,8 +242,8 @@
</Target>
<!--
Database credentials are never build inputs. Full Debug, publish and MSIX
builds use the same user-only LocalAppData overlay as a clean handoff PC.
Database credentials are never build inputs. Development Live resolves the
selected external Res\MmoneyCoder.ini only at runtime.
-->
<Target Name="ScrubLegacyDatabaseIniFromBuildOutput"
BeforeTargets="PrepareForBuild">

View File

@@ -139,16 +139,17 @@ public sealed partial class MainWindow : Window
// A Gate A plan pins database.local.json by path, size and SHA-256.
// Never inspect either packaged or operator-selected legacy INI files
// during that one-shot validation process.
// during that one-shot validation process. Ordinary Development Live
// uses the selected Res\MmoneyCoder.ini as its exact DB authority.
var selectedLegacyDatabaseIni =
_isDevelopmentLiveLaunch ||
selectedResourceDirectory is null
? null
: Path.Combine(selectedResourceDirectory, "MmoneyCoder.ini");
_databaseRuntime = _playoutLaunchAuthorization.IsGateA
? DatabaseRuntime.CreateDefault()
: _isDevelopmentLiveLaunch
? DatabaseRuntime.CreateDevelopmentLiveLocalOverride()
? DatabaseRuntime.CreateDevelopmentLiveLocalOverride(
selectedResourceDirectory)
: DatabaseRuntime.CreateLegacyExecutableDefault(
AppContext.BaseDirectory,
legacyOverridePath: selectedLegacyDatabaseIni,
@@ -208,11 +209,21 @@ public sealed partial class MainWindow : Window
operatorCatalogSchemaValidationService =
new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor);
}
catch
catch (Exception exception)
{
var developmentLiveDatabaseConfigurationFailed =
_isDevelopmentLiveLaunch &&
exception is DatabaseConfigurationException;
if (developmentLiveDatabaseConfigurationFailed)
{
_developmentLiveStartupBlocked = true;
}
_databaseRuntime = null;
fixedPagePlanProvider = null;
initializationError = _developmentLiveStartupBlocked
initializationError = developmentLiveDatabaseConfigurationFailed
? "Development Live 데이터베이스 설정을 확인할 수 없습니다."
: _developmentLiveStartupBlocked
? App.DevelopmentLiveStartupFailureMessage ??
"Live playout startup validation failed."
: IsSourceOnlyBuild