feat: restore legacy drag events and Tornado launch integration
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user