feat: restore legacy drag events and Tornado launch integration

This commit is contained in:
2026-07-15 15:35:25 +09:00
parent 6e0c275fdd
commit 06a16e5741
32 changed files with 3096 additions and 101 deletions

View File

@@ -0,0 +1,399 @@
using System.Security;
using System.Text.Json;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
namespace MBN_STOCK_WEBVIEW.Playout.Configuration;
internal enum DevelopmentLiveBootstrapStatus
{
Skipped,
Applied,
Rejected
}
internal readonly record struct DevelopmentLiveBootstrapResult(
DevelopmentLiveBootstrapStatus Status,
string Code)
{
public bool IsApplied => Status == DevelopmentLiveBootstrapStatus.Applied;
}
internal interface IDevelopmentLiveBootstrapPlatform
{
byte[] ReadAuthorizationFile(string path, int maximumBytes);
string? GetProcessEnvironmentVariable(string name);
void SetProcessEnvironmentVariable(string name, string? value);
}
/// <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.
/// </summary>
internal static class DevelopmentLiveLaunchBootstrap
{
internal const string ExactLaunchArgument = "--development-live";
internal const string ModeEnvironmentVariable = "MBN_STOCK_PLAYOUT_MODE";
internal const string RequiredMode = "Live";
internal const int SchemaVersion = 1;
internal const int MaximumAuthorizationFileBytes = 4 * 1024;
private const string SchemaVersionProperty = "schemaVersion";
private const string ModeProperty = "mode";
private const string AuthorizationProperty = "authorization";
private const string NativeSha256Property = "nativeSha256";
private const string InteropSha256Property = "interopSha256";
private static readonly IReadOnlySet<string> AllowedProperties =
new HashSet<string>(StringComparer.Ordinal)
{
SchemaVersionProperty,
ModeProperty,
AuthorizationProperty,
NativeSha256Property,
InteropSha256Property
};
internal static string DefaultPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MBN_STOCK_WEBVIEW",
"Config",
"playout.development-live.local.json");
internal static string? ResolveLaunchArguments(
string? activationArguments,
IReadOnlyList<string> processArguments)
{
ArgumentNullException.ThrowIfNull(processArguments);
// Packaged WinUI launches can expose an empty LaunchActivatedEventArgs.Arguments
// even though the full-trust process received the Visual Studio command-line
// argument. Fall back only to one exact process argument; never concatenate,
// trim, or ignore additional command-line input.
if (!string.IsNullOrEmpty(activationArguments))
{
return activationArguments;
}
return processArguments.Count == 2
? processArguments[1]
: null;
}
internal static DevelopmentLiveBootstrapResult TryApply(
string? launchArguments,
bool isDebugBuild) =>
TryApply(
launchArguments,
isDebugBuild,
DefaultPath,
new SystemDevelopmentLiveBootstrapPlatform());
internal static DevelopmentLiveBootstrapResult TryApply(
string? launchArguments,
bool isDebugBuild,
string authorizationFilePath,
IDevelopmentLiveBootstrapPlatform platform)
{
ArgumentNullException.ThrowIfNull(platform);
if (!string.Equals(
launchArguments,
ExactLaunchArgument,
StringComparison.Ordinal))
{
return new DevelopmentLiveBootstrapResult(
DevelopmentLiveBootstrapStatus.Skipped,
"development-live-not-requested");
}
if (!isDebugBuild)
{
return new DevelopmentLiveBootstrapResult(
DevelopmentLiveBootstrapStatus.Skipped,
"development-live-debug-only");
}
if (string.IsNullOrWhiteSpace(authorizationFilePath))
{
return Rejected("development-live-authorization-unavailable");
}
byte[] json;
try
{
json = platform.ReadAuthorizationFile(
authorizationFilePath,
MaximumAuthorizationFileBytes);
}
catch (Exception exception) when (IsExpectedFileFailure(exception))
{
return Rejected("development-live-authorization-unavailable");
}
if (!TryParseAuthorization(json, out var nativeSha256, out var interopSha256))
{
return Rejected("development-live-authorization-invalid");
}
return TryApplyEnvironment(platform, nativeSha256, interopSha256)
? new DevelopmentLiveBootstrapResult(
DevelopmentLiveBootstrapStatus.Applied,
"development-live-applied")
: Rejected("development-live-environment-failed");
}
private static bool TryParseAuthorization(
byte[] json,
out string nativeSha256,
out string interopSha256)
{
nativeSha256 = string.Empty;
interopSha256 = string.Empty;
if (json.Length is 0 or > MaximumAuthorizationFileBytes)
{
return false;
}
try
{
using var document = JsonDocument.Parse(
json,
new JsonDocumentOptions
{
AllowTrailingCommas = false,
CommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 4
});
if (document.RootElement.ValueKind != JsonValueKind.Object)
{
return false;
}
var seen = new HashSet<string>(StringComparer.Ordinal);
int? schemaVersion = null;
string? mode = null;
string? authorization = null;
string? native = null;
string? interop = null;
foreach (var property in document.RootElement.EnumerateObject())
{
if (!AllowedProperties.Contains(property.Name) || !seen.Add(property.Name))
{
return false;
}
switch (property.Name)
{
case SchemaVersionProperty:
if (property.Value.ValueKind != JsonValueKind.Number ||
!property.Value.TryGetInt32(out var parsedVersion))
{
return false;
}
schemaVersion = parsedVersion;
break;
case ModeProperty:
mode = StrictString(property.Value);
break;
case AuthorizationProperty:
authorization = StrictString(property.Value);
break;
case NativeSha256Property:
native = StrictString(property.Value);
break;
case InteropSha256Property:
interop = StrictString(property.Value);
break;
}
}
if (seen.Count != AllowedProperties.Count ||
schemaVersion != SchemaVersion ||
!string.Equals(mode, RequiredMode, StringComparison.Ordinal) ||
!string.Equals(
authorization,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
StringComparison.Ordinal) ||
!IsSha256(native) ||
!IsSha256(interop))
{
return false;
}
nativeSha256 = native!.ToUpperInvariant();
interopSha256 = interop!.ToUpperInvariant();
return true;
}
catch (JsonException)
{
return false;
}
}
private static bool TryApplyEnvironment(
IDevelopmentLiveBootstrapPlatform platform,
string nativeSha256,
string interopSha256)
{
string[] names =
[
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable,
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable,
ModeEnvironmentVariable,
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable
];
var originals = new Dictionary<string, string?>(StringComparer.Ordinal);
try
{
foreach (var name in names)
{
originals.Add(name, platform.GetProcessEnvironmentVariable(name));
}
// The authorization value is the final write. A partially applied setup
// therefore cannot newly satisfy the two-part Live gate.
platform.SetProcessEnvironmentVariable(
InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable,
nativeSha256);
platform.SetProcessEnvironmentVariable(
InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable,
interopSha256);
platform.SetProcessEnvironmentVariable(ModeEnvironmentVariable, RequiredMode);
platform.SetProcessEnvironmentVariable(
PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable,
PlayoutOptionsLoader.RequiredLiveAuthorizationValue);
return true;
}
catch (Exception exception) when (IsExpectedEnvironmentFailure(exception))
{
for (var index = names.Length - 1; index >= 0; index--)
{
var name = names[index];
if (!originals.TryGetValue(name, out var original))
{
continue;
}
try
{
platform.SetProcessEnvironmentVariable(name, original);
}
catch (Exception rollbackException) when (
IsExpectedEnvironmentFailure(rollbackException))
{
// The real process environment API is expected to be all-or-nothing
// for these bounded names and values. Never turn a rollback failure
// into a second startup attempt or expose a value in diagnostics.
}
}
return false;
}
}
private static string? StrictString(JsonElement value) =>
value.ValueKind == JsonValueKind.String ? value.GetString() : null;
private static bool IsSha256(string? value)
{
if (value is null || value.Length != 64)
{
return false;
}
foreach (var character in value)
{
if (!((character >= '0' && character <= '9') ||
(character >= 'A' && character <= 'F') ||
(character >= 'a' && character <= 'f')))
{
return false;
}
}
return true;
}
private static bool IsExpectedFileFailure(Exception exception) =>
exception is IOException or UnauthorizedAccessException or SecurityException or
ArgumentException or NotSupportedException;
private static bool IsExpectedEnvironmentFailure(Exception exception) =>
exception is ArgumentException or SecurityException or InvalidOperationException;
private static DevelopmentLiveBootstrapResult Rejected(string code) =>
new(DevelopmentLiveBootstrapStatus.Rejected, code);
}
internal sealed class SystemDevelopmentLiveBootstrapPlatform :
IDevelopmentLiveBootstrapPlatform
{
public byte[] ReadAuthorizationFile(string path, int maximumBytes)
{
var fullPath = Path.GetFullPath(path);
EnsureTrustedLocalPath(fullPath);
var attributes = File.GetAttributes(fullPath);
if ((attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0)
{
throw new InvalidDataException();
}
using var stream = new FileStream(
fullPath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize: 4096,
FileOptions.SequentialScan);
if (stream.Length is <= 0 || stream.Length > maximumBytes)
{
throw new InvalidDataException();
}
var bytes = new byte[checked((int)stream.Length)];
stream.ReadExactly(bytes);
return bytes;
}
public string? GetProcessEnvironmentVariable(string name) =>
Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process);
public void SetProcessEnvironmentVariable(string name, string? value) =>
Environment.SetEnvironmentVariable(
name,
value,
EnvironmentVariableTarget.Process);
private static void EnsureTrustedLocalPath(string fullPath)
{
var localRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)));
var rootPrefix = localRoot + Path.DirectorySeparatorChar;
if (!fullPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException();
}
var directory = Path.GetDirectoryName(fullPath);
while (!string.IsNullOrEmpty(directory) &&
!string.Equals(directory, localRoot, StringComparison.OrdinalIgnoreCase))
{
if ((File.GetAttributes(directory) & FileAttributes.ReparsePoint) != 0)
{
throw new InvalidDataException();
}
directory = Path.GetDirectoryName(directory);
}
if (!string.Equals(directory, localRoot, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException();
}
}
}

View File

@@ -2,3 +2,4 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MBN_STOCK_WEBVIEW.Playout.Tests")]
[assembly: InternalsVisibleTo("MBN_STOCK_WEBVIEW.PlayoutSmoke")]
[assembly: InternalsVisibleTo("MBN_STOCK_WEBVIEW.LegacyParityApp")]