feat: initialize existing development playout PCs

This commit is contained in:
2026-07-26 23:11:29 +09:00
parent 8b628908f0
commit 483b2785a0
18 changed files with 4029 additions and 74 deletions

View File

@@ -10,6 +10,10 @@ public partial class App : Application
private Window? _window;
private AppInstance? _mainInstance;
internal static bool IsDevelopmentLiveLaunch { get; private set; }
internal static bool IsDevelopmentLiveLaunchRequested { get; private set; }
public App()
{
InitializeComponent();
@@ -55,9 +59,14 @@ public partial class App : Application
: DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
args.Arguments,
Environment.GetCommandLineArgs());
IsDevelopmentLiveLaunchRequested = string.Equals(
launchArguments,
DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
StringComparison.Ordinal);
var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(
launchArguments,
isDebugBuild);
IsDevelopmentLiveLaunch = developmentLive.IsApplied;
System.Diagnostics.Debug.WriteLine(
$"Development Live bootstrap: {developmentLive.Code}");

View File

@@ -178,9 +178,10 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<!--
The active legacy DB INI contains credentials. It is available beside the
executable for local F5/unpackaged development only and is never MSIX Content.
Historical copies, backups, archives and afiedt.buf.txt are not included at all.
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
afiedt.buf.txt are not included at all.
-->
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
@@ -192,10 +193,13 @@
<Message Condition="'$(LegacyRuntimeAssetsMode)' == 'Auto' and
'$(LegacyRuntimeAssetsEnabled)' != 'true'"
Importance="high"
Text="Legacy runtime assets were not found. Building the safe source-only DryRun app. Full runtime use requires a Required build with the approved external root." />
Text="Legacy runtime assets were not found. Building the safe source-only DryRun app. Run scripts\Initialize-ExistingDevelopmentPc.ps1 before Development Live." />
<Message Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'"
Importance="high"
Text="Verified full legacy runtime assets are enabled. Database credentials remain in the current user's LocalAppData overlay." />
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyRuntimeSourceRoot)')"
Text="Legacy runtime source root was not found: $(LegacyRuntimeSourceRoot). Set /p:LegacyRuntimeSourceRoot to the read-only MBN_STOCK_N bin\Debug directory, or use /p:LegacyRuntimeAssetsMode=Auto for a source-only DryRun build." />
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 DryRun build." />
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyCutsSourceRoot)')"
Text="Legacy Cuts source directory was not found: $(LegacyCutsSourceRoot)." />

View File

@@ -34,10 +34,17 @@ public sealed partial class MainWindow
{
var explicitLocalConfigurationExists = File.Exists(
PlayoutOptionsLoader.DefaultPath);
_playoutOptions = PlayoutOptionsLoader.Load(
operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory,
operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory,
forceSafeDryRun: IsSourceOnlyBuild);
_playoutOptions =
_isDevelopmentLiveLaunch &&
!_developmentLiveStartupBlocked
? PlayoutOptionsLoader.LoadDevelopmentLive(
baseDirectory: AppContext.BaseDirectory)
: PlayoutOptionsLoader.Load(
operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory,
operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory,
forceSafeDryRun: IsSourceOnlyBuild ||
_isDevelopmentLiveLaunchRequested ||
_developmentLiveStartupBlocked);
ResetRefreshState();
var startupComposition = LegacyParityStartupCompositionResolver.Resolve(
_playoutOptions,

View File

@@ -35,6 +35,8 @@ public sealed partial class MainWindow : Window
}
private readonly CancellationTokenSource _lifetimeCancellation = new();
private readonly bool _isDevelopmentLiveLaunch;
private readonly bool _isDevelopmentLiveLaunchRequested;
private readonly SemaphoreSlim _intentGate = new(1, 1);
private readonly SemaphoreSlim _orderedLocalSelectionIntentGate = new(1, 1);
private readonly SemaphoreSlim _orderedPlayoutIntentGate = new(1, 1);
@@ -49,6 +51,7 @@ public sealed partial class MainWindow : Window
private LegacyOperatorSettings _operatorSettings;
private DatabaseRuntime? _databaseRuntime;
private bool _closing;
private bool _developmentLiveStartupBlocked;
private bool _interactionMetricsRefreshQueued;
private bool _intentBusy;
private bool _windowSubclassAttached;
@@ -59,6 +62,8 @@ public sealed partial class MainWindow : Window
public MainWindow()
{
_isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch;
_isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested;
// Capture and remove Gate A bearer material before XAML can create WebView2.
// Only the native process keeps the capability digest for this launch.
_playoutLaunchAuthorization =
@@ -73,15 +78,22 @@ public sealed partial class MainWindow : Window
EnsureCloseConfirmationAttached();
var localApplicationData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
var runtimeCutMenuOverridePath = string.IsNullOrWhiteSpace(localApplicationData)
var runtimeCutMenuOverridePath =
_isDevelopmentLiveLaunch ||
string.IsNullOrWhiteSpace(localApplicationData)
? null
: Path.Combine(
localApplicationData,
"MBN_STOCK_WEBVIEW",
"Res",
"종목.ini");
var selectedResourceDirectory = _appliedOperatorSettings.ResourceDirectory;
var cutMenuComposition = selectedResourceDirectory is null
var selectedResourceDirectory = _isDevelopmentLiveLaunch
? null
: _appliedOperatorSettings.ResourceDirectory;
var cutMenuComposition = _isDevelopmentLiveLaunch
? LegacyRuntimeStockCutMenuLoader.ResolveFromBaseDirectory(
AppContext.BaseDirectory)
: selectedResourceDirectory is null
? LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates(
runtimeCutMenuOverridePath,
AppContext.BaseDirectory)
@@ -111,6 +123,7 @@ public sealed partial class MainWindow : Window
IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService;
CorePagePlanProvider? fixedPagePlanProvider = null;
string? initializationError = null;
var databaseInitializationSucceeded = false;
try
{
if (IsSourceOnlyBuild)
@@ -127,6 +140,8 @@ public sealed partial class MainWindow : Window
: Path.Combine(selectedResourceDirectory, "MmoneyCoder.ini");
_databaseRuntime = _playoutLaunchAuthorization.IsGateA
? DatabaseRuntime.CreateDefault()
: _isDevelopmentLiveLaunch
? DatabaseRuntime.CreateDevelopmentLiveLocalOverride()
: DatabaseRuntime.CreateLegacyExecutableDefault(
AppContext.BaseDirectory,
legacyOverridePath: selectedLegacyDatabaseIni,
@@ -185,9 +200,12 @@ public sealed partial class MainWindow : Window
new LegacyStockMasterIdentityValidationService(_databaseRuntime.Executor);
operatorCatalogSchemaValidationService =
new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor);
databaseInitializationSucceeded = true;
}
catch
{
_databaseRuntime = null;
fixedPagePlanProvider = null;
initializationError = IsSourceOnlyBuild
? "소스 전용 DryRun에서는 데이터베이스 연결을 사용하지 않습니다."
: "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
@@ -225,6 +243,14 @@ public sealed partial class MainWindow : Window
DataQueryExecutor.Reset();
}
if (_isDevelopmentLiveLaunch && !databaseInitializationSucceeded)
{
// Development Live is one fail-closed startup transaction. A
// missing or partially initialized protected DB runtime must not
// leave the independently armed playout gate eligible for use.
_developmentLiveStartupBlocked = true;
}
_industryWorkflow = new LegacyIndustrySelectionWorkflow(
industrySelectionService,
actionLayout: runtimeUiCatalogs.IndustryLayout);
@@ -310,7 +336,10 @@ public sealed partial class MainWindow : Window
if (!_closing)
{
StartDatabaseHealthMonitor();
await ConnectPlayoutAsync(_lifetimeCancellation.Token);
if (!_developmentLiveStartupBlocked)
{
await ConnectPlayoutAsync(_lifetimeCancellation.Token);
}
}
}