feat: make LegacyParityApp playout live-only
This commit is contained in:
@@ -14,6 +14,10 @@ public partial class App : Application
|
||||
|
||||
internal static bool IsDevelopmentLiveLaunchRequested { get; private set; }
|
||||
|
||||
internal static string? DevelopmentLiveStartupFailureCode { get; private set; }
|
||||
|
||||
internal static string? DevelopmentLiveStartupFailureMessage { get; private set; }
|
||||
|
||||
public App()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -43,40 +47,31 @@ public partial class App : Application
|
||||
_mainInstance = registeredInstance;
|
||||
_mainInstance.Activated += OnMainInstanceActivated;
|
||||
|
||||
#if DEBUG
|
||||
const bool isDebugBuild = true;
|
||||
#else
|
||||
const bool isDebugBuild = false;
|
||||
#endif
|
||||
|
||||
#if SOURCE_ONLY_RUNTIME
|
||||
const bool isSourceOnlyBuild = true;
|
||||
#else
|
||||
const bool isSourceOnlyBuild = false;
|
||||
#endif
|
||||
var launchArguments = isSourceOnlyBuild
|
||||
? null
|
||||
: 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}");
|
||||
|
||||
// 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();
|
||||
#if SOURCE_ONLY_RUNTIME
|
||||
IsDevelopmentLiveLaunchRequested = false;
|
||||
IsDevelopmentLiveLaunch = false;
|
||||
DevelopmentLiveStartupFailureCode = null;
|
||||
DevelopmentLiveStartupFailureMessage = null;
|
||||
_window = new FirstRunSetupWindow();
|
||||
#else
|
||||
var launchArguments = DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
|
||||
args.Arguments,
|
||||
Environment.GetCommandLineArgs());
|
||||
var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(launchArguments);
|
||||
IsDevelopmentLiveLaunchRequested = developmentLive.IsRequested;
|
||||
IsDevelopmentLiveLaunch = developmentLive.IsApplied;
|
||||
DevelopmentLiveStartupFailureCode = developmentLive.IsLaunchAllowed
|
||||
? null
|
||||
: developmentLive.Code;
|
||||
DevelopmentLiveStartupFailureMessage = developmentLive.FailureMessage;
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Development Live bootstrap: {developmentLive.Code}");
|
||||
|
||||
_window = new MainWindow();
|
||||
#endif
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
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." />
|
||||
<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 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 setup build." />
|
||||
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
|
||||
!Exists('$(LegacyCutsSourceRoot)')"
|
||||
Text="Legacy Cuts source directory was not found: $(LegacyCutsSourceRoot)." />
|
||||
|
||||
@@ -32,21 +32,33 @@ public sealed partial class MainWindow
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_developmentLiveStartupBlocked)
|
||||
{
|
||||
_playoutInitializationError =
|
||||
App.DevelopmentLiveStartupFailureMessage ??
|
||||
"Live playout startup validation failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
var explicitLocalConfigurationExists = File.Exists(
|
||||
PlayoutOptionsLoader.DefaultPath);
|
||||
_playoutOptions =
|
||||
_isDevelopmentLiveLaunch &&
|
||||
!_developmentLiveStartupBlocked
|
||||
_isDevelopmentLiveLaunch
|
||||
? PlayoutOptionsLoader.LoadDevelopmentLive(
|
||||
baseDirectory: AppContext.BaseDirectory,
|
||||
operatorSceneDirectory:
|
||||
_appliedOperatorSettings.SceneDirectory)
|
||||
: PlayoutOptionsLoader.Load(
|
||||
operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory,
|
||||
operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory,
|
||||
forceSafeDryRun: IsSourceOnlyBuild ||
|
||||
_isDevelopmentLiveLaunchRequested ||
|
||||
_developmentLiveStartupBlocked);
|
||||
operatorBackgroundDirectory:
|
||||
_appliedOperatorSettings.BackgroundDirectory);
|
||||
if (_playoutOptions.Mode != PlayoutMode.Live)
|
||||
{
|
||||
throw new PlayoutConfigurationException(
|
||||
"The application requires a validated Live playout configuration. " +
|
||||
"DryRun fallback is disabled.");
|
||||
}
|
||||
|
||||
ResetRefreshState();
|
||||
var startupComposition = LegacyParityStartupCompositionResolver.Resolve(
|
||||
_playoutOptions,
|
||||
@@ -75,6 +87,7 @@ public sealed partial class MainWindow
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_developmentLiveStartupBlocked = true;
|
||||
_playoutInitializationWarning = null;
|
||||
_playoutInitializationError = exception is PlayoutConfigurationException
|
||||
? exception.Message
|
||||
|
||||
@@ -64,6 +64,9 @@ public sealed partial class MainWindow : Window
|
||||
{
|
||||
_isDevelopmentLiveLaunch = App.IsDevelopmentLiveLaunch;
|
||||
_isDevelopmentLiveLaunchRequested = App.IsDevelopmentLiveLaunchRequested;
|
||||
_developmentLiveStartupBlocked =
|
||||
_isDevelopmentLiveLaunchRequested &&
|
||||
!_isDevelopmentLiveLaunch;
|
||||
// Capture and remove Gate A bearer material before XAML can create WebView2.
|
||||
// Only the native process keeps the capability digest for this launch.
|
||||
_playoutLaunchAuthorization =
|
||||
@@ -119,9 +122,15 @@ public sealed partial class MainWindow : Window
|
||||
IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService;
|
||||
CorePagePlanProvider? fixedPagePlanProvider = null;
|
||||
string? initializationError = null;
|
||||
var databaseInitializationSucceeded = false;
|
||||
try
|
||||
{
|
||||
if (_developmentLiveStartupBlocked)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
App.DevelopmentLiveStartupFailureMessage ??
|
||||
"Live playout startup validation failed.");
|
||||
}
|
||||
|
||||
if (IsSourceOnlyBuild)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
@@ -198,14 +207,16 @@ 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에서는 데이터베이스 연결을 사용하지 않습니다."
|
||||
initializationError = _developmentLiveStartupBlocked
|
||||
? App.DevelopmentLiveStartupFailureMessage ??
|
||||
"Live playout startup validation failed."
|
||||
: IsSourceOnlyBuild
|
||||
? "소스 전용 설정 빌드에서는 데이터베이스 연결을 사용하지 않습니다."
|
||||
: "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
|
||||
stockLookup = new UnavailableStockLookup(initializationError);
|
||||
industrySelectionService = new UnavailableIndustrySelectionService(
|
||||
@@ -241,14 +252,6 @@ 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);
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)": {
|
||||
"commandName": "MsixPackage",
|
||||
"commandLineArgs": "--development-live"
|
||||
},
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)": {
|
||||
"commandName": "MsixPackage",
|
||||
"commandLineArgs": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Security;
|
||||
using System.Text.Json;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
|
||||
using MBN_STOCK_WEBVIEW.Playout.Safety;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.Playout.Configuration;
|
||||
|
||||
@@ -8,6 +9,7 @@ internal enum DevelopmentLiveBootstrapStatus
|
||||
{
|
||||
Skipped,
|
||||
Applied,
|
||||
GateABypassed,
|
||||
Rejected
|
||||
}
|
||||
|
||||
@@ -16,6 +18,31 @@ internal readonly record struct DevelopmentLiveBootstrapResult(
|
||||
string Code)
|
||||
{
|
||||
public bool IsApplied => Status == DevelopmentLiveBootstrapStatus.Applied;
|
||||
|
||||
public bool IsLaunchAllowed =>
|
||||
Status is DevelopmentLiveBootstrapStatus.Applied or
|
||||
DevelopmentLiveBootstrapStatus.GateABypassed;
|
||||
|
||||
public bool IsRequested =>
|
||||
Status is DevelopmentLiveBootstrapStatus.Applied or
|
||||
DevelopmentLiveBootstrapStatus.Rejected;
|
||||
|
||||
public string? FailureMessage => Status == DevelopmentLiveBootstrapStatus.Rejected
|
||||
? Code switch
|
||||
{
|
||||
"development-live-arguments-invalid" =>
|
||||
"Live startup was rejected because the launch arguments were not recognized.",
|
||||
"development-live-gate-a-inspection-failed" =>
|
||||
"Gate A launch authorization could not be inspected.",
|
||||
"development-live-authorization-unavailable" =>
|
||||
"Live startup authorization is unavailable.",
|
||||
"development-live-authorization-invalid" =>
|
||||
"Live startup authorization is invalid.",
|
||||
"development-live-environment-failed" =>
|
||||
"Live startup authorization could not be applied to this process.",
|
||||
_ => "Live startup authorization failed."
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
internal interface IDevelopmentLiveBootstrapPlatform
|
||||
@@ -28,10 +55,10 @@ internal interface IDevelopmentLiveBootstrapPlatform
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Arms the existing process-scoped Live and vendor-binary gates for one explicit
|
||||
/// Visual Studio Debug launch. The ordinary app startup path never calls this with
|
||||
/// a true debug-build capability, and the authorization file is deliberately kept
|
||||
/// outside the package and source tree.
|
||||
/// Arms the existing process-scoped Live and vendor-binary gates for a full-runtime
|
||||
/// application launch. Both ordinary no-argument launches and the retained exact
|
||||
/// development-live argument use the same strict local authorization file, which
|
||||
/// is deliberately kept outside the package and source tree.
|
||||
/// </summary>
|
||||
internal static class DevelopmentLiveLaunchBootstrap
|
||||
{
|
||||
@@ -46,6 +73,8 @@ internal static class DevelopmentLiveLaunchBootstrap
|
||||
private const string AuthorizationProperty = "authorization";
|
||||
private const string NativeSha256Property = "nativeSha256";
|
||||
private const string InteropSha256Property = "interopSha256";
|
||||
private const string InvalidLaunchArgumentsMarker =
|
||||
"\0MBN_STOCK_WEBVIEW_INVALID_LAUNCH_ARGUMENTS";
|
||||
|
||||
private static readonly IReadOnlySet<string> AllowedProperties =
|
||||
new HashSet<string>(StringComparer.Ordinal)
|
||||
@@ -65,6 +94,14 @@ internal static class DevelopmentLiveLaunchBootstrap
|
||||
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable
|
||||
];
|
||||
|
||||
private static readonly string[] GateAEnvironmentNames =
|
||||
[
|
||||
PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable,
|
||||
PlayoutLaunchAuthorization.GateAPgmProcessIdEnvironmentVariable,
|
||||
PlayoutLaunchAuthorization.GateAPgmStartTimeUtcTicksEnvironmentVariable,
|
||||
PlayoutLaunchAuthorization.GateAExpiresAtUtcTicksEnvironmentVariable
|
||||
];
|
||||
|
||||
internal static string DefaultPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
@@ -86,11 +123,22 @@ internal static class DevelopmentLiveLaunchBootstrap
|
||||
return activationArguments;
|
||||
}
|
||||
|
||||
return processArguments.Count == 2
|
||||
? processArguments[1]
|
||||
: null;
|
||||
return processArguments.Count switch
|
||||
{
|
||||
<= 1 => null,
|
||||
2 => processArguments[1],
|
||||
_ => InvalidLaunchArgumentsMarker
|
||||
};
|
||||
}
|
||||
|
||||
internal static DevelopmentLiveBootstrapResult TryApply(
|
||||
string? launchArguments) =>
|
||||
TryApply(
|
||||
launchArguments,
|
||||
isDebugBuild: true,
|
||||
DefaultPath,
|
||||
new SystemDevelopmentLiveBootstrapPlatform());
|
||||
|
||||
internal static DevelopmentLiveBootstrapResult TryApply(
|
||||
string? launchArguments,
|
||||
bool isDebugBuild) =>
|
||||
@@ -108,21 +156,32 @@ internal static class DevelopmentLiveLaunchBootstrap
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(platform);
|
||||
|
||||
if (!string.Equals(
|
||||
// Retain the isDebugBuild parameter for callers compiled against the
|
||||
// previous API. Release and Debug now intentionally share this exact gate.
|
||||
_ = isDebugBuild;
|
||||
|
||||
if (!string.IsNullOrEmpty(launchArguments) &&
|
||||
!string.Equals(
|
||||
launchArguments,
|
||||
ExactLaunchArgument,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
return new DevelopmentLiveBootstrapResult(
|
||||
DevelopmentLiveBootstrapStatus.Skipped,
|
||||
"development-live-not-requested");
|
||||
return Rejected("development-live-arguments-invalid");
|
||||
}
|
||||
|
||||
if (!isDebugBuild)
|
||||
if (!TryHasGateAEnvironment(platform, out var hasGateAEnvironment))
|
||||
{
|
||||
return Rejected("development-live-gate-a-inspection-failed");
|
||||
}
|
||||
|
||||
// Gate A owns and consumes its process-scoped bearer values in
|
||||
// PlayoutLaunchAuthorization. Do not read the ordinary authorization file
|
||||
// and do not set, clear or roll back any environment value in this branch.
|
||||
if (hasGateAEnvironment)
|
||||
{
|
||||
return new DevelopmentLiveBootstrapResult(
|
||||
DevelopmentLiveBootstrapStatus.Skipped,
|
||||
"development-live-debug-only");
|
||||
DevelopmentLiveBootstrapStatus.GateABypassed,
|
||||
"development-live-gate-a-bypassed");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authorizationFilePath))
|
||||
@@ -305,13 +364,37 @@ internal static class DevelopmentLiveLaunchBootstrap
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryHasGateAEnvironment(
|
||||
IDevelopmentLiveBootstrapPlatform platform,
|
||||
out bool hasGateAEnvironment)
|
||||
{
|
||||
hasGateAEnvironment = false;
|
||||
try
|
||||
{
|
||||
foreach (var name in GateAEnvironmentNames)
|
||||
{
|
||||
if (platform.GetProcessEnvironmentVariable(name) is not null)
|
||||
{
|
||||
hasGateAEnvironment = true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (IsExpectedEnvironmentFailure(exception))
|
||||
{
|
||||
hasGateAEnvironment = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static DevelopmentLiveBootstrapResult RejectedAndDisarm(
|
||||
IDevelopmentLiveBootstrapPlatform platform,
|
||||
string code)
|
||||
{
|
||||
// An exact Debug request that failed validation must not fall through to
|
||||
// A required Live request that failed validation must not fall through to
|
||||
// inherited process state from an earlier shell or Visual Studio session.
|
||||
// The app also forces this requested-but-not-applied state to DryRun.
|
||||
// The app treats this requested-but-not-applied state as a startup failure.
|
||||
for (var index = ArmedEnvironmentNames.Length - 1; index >= 0; index--)
|
||||
{
|
||||
var name = ArmedEnvironmentNames[index];
|
||||
@@ -354,7 +437,7 @@ internal static class DevelopmentLiveLaunchBootstrap
|
||||
|
||||
private static bool IsExpectedFileFailure(Exception exception) =>
|
||||
exception is IOException or UnauthorizedAccessException or SecurityException or
|
||||
ArgumentException or NotSupportedException;
|
||||
InvalidDataException or ArgumentException or NotSupportedException;
|
||||
|
||||
private static bool IsExpectedEnvironmentFailure(Exception exception) =>
|
||||
exception is ArgumentException or SecurityException or InvalidOperationException;
|
||||
|
||||
@@ -88,11 +88,10 @@ public static class PlayoutOptionsLoader
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the inert LocalAppData base profile for an already-applied exact
|
||||
/// Debug Development Live bootstrap. This path deliberately ignores process
|
||||
/// environment and ordinary playout-profile scene overrides. Live mode is
|
||||
/// armed from bootstrap provenance, and the scene root is pinned to the
|
||||
/// native folder selected during first-run setup.
|
||||
/// Loads the protected LocalAppData Live profile for an already-applied
|
||||
/// Development Live bootstrap. This path deliberately ignores process
|
||||
/// environment and ordinary playout-profile scene overrides. The scene root
|
||||
/// is pinned to the native folder selected during first-run setup.
|
||||
/// </summary>
|
||||
internal static PlayoutOptions LoadDevelopmentLive(
|
||||
string? path = null,
|
||||
@@ -188,7 +187,7 @@ public static class PlayoutOptionsLoader
|
||||
|
||||
private static void EnsureDevelopmentLiveBaseContract(PlayoutOptions options)
|
||||
{
|
||||
if (options.Mode != PlayoutMode.DryRun ||
|
||||
if (options.Mode != PlayoutMode.Live ||
|
||||
!IsLiteralLoopback(options.Host) ||
|
||||
options.Port is < 1 or > 65_535 ||
|
||||
options.TcpMode != 1 ||
|
||||
@@ -285,7 +284,7 @@ public static class PlayoutOptionsLoader
|
||||
}
|
||||
|
||||
if (seen.Count != DevelopmentLiveProperties.Count ||
|
||||
!IsExactString(root, "mode", "DryRun") ||
|
||||
!IsExactString(root, "mode", "Live") ||
|
||||
!IsLoopbackString(root, "host") ||
|
||||
!IsIntegerInRange(root, "port", 1, 65_535) ||
|
||||
!IsExactInteger(root, "tcpMode", 1) ||
|
||||
|
||||
Reference in New Issue
Block a user