using Microsoft.Extensions.Configuration;
using v2rayN.WebAPI.Contracts;
namespace v2rayN.WebAPI.Launcher;
public sealed record NativeWebRestoreReplacement(
Uri HealthUri,
WebReplacementCommand Command,
bool CoreWasRunning);
public sealed record NativeWebRestorePreflightResult(
bool Allowed,
WebLifecycleOwner Owner,
NativeWebRestoreReplacement? Replacement,
string? FailureCode = null,
string? MessageKey = null)
{
public OperationView ToFailureOperation()
{
if (Allowed || string.IsNullOrWhiteSpace(FailureCode) || string.IsNullOrWhiteSpace(MessageKey))
{
throw new InvalidOperationException("A successful native restore preflight has no failure response.");
}
return OperationView.Fail(FailureCode, MessageKey);
}
}
///
/// Resolves everything needed to safely restart a native restore before Core or config state
/// is touched. Supervised deployments deliberately leave restart ownership to their supervisor.
///
public static class NativeWebRestorePreflight
{
public static NativeWebRestorePreflightResult Evaluate(
IConfiguration configuration,
bool isLinux,
bool isWindows,
bool externalSupervisor,
bool containerEnvironment,
string? setsidPath,
string? processPath,
string? managedEntryPoint,
IEnumerable hostArguments,
Func? fileExists = null,
Func? isUnixExecutable = null,
bool coreWasRunning = false)
{
var lifecycle = WebLifecyclePlanner.Create(
WebLifecycleAction.RestoreAndRestart,
supportsNativeRestart: isLinux || isWindows,
daemonEnvironment: externalSupervisor,
containerEnvironment);
if (lifecycle.Owner != WebLifecycleOwner.NativeProcess)
{
return new(true, lifecycle.Owner, null);
}
if (!WebProbeUriResolver.TryResolve(configuration, out var endpoints))
{
return Refused(
WebLifecycleOwner.NativeProcess,
"backup_restore_health_probe_unavailable",
ApiMessageKeys.BackupRestoreProbeUnavailable);
}
var exists = fileExists ?? File.Exists;
if (string.IsNullOrWhiteSpace(processPath) || !exists(processPath))
{
return Refused(
WebLifecycleOwner.NativeProcess,
"backup_restore_executable_unavailable",
ApiMessageKeys.BackupRestoreExecutableUnavailable);
}
var isDotnet = Path.GetFileNameWithoutExtension(processPath)
.Equals("dotnet", StringComparison.OrdinalIgnoreCase);
if (isDotnet
&& (string.IsNullOrWhiteSpace(managedEntryPoint)
|| !managedEntryPoint.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|| !exists(managedEntryPoint)))
{
return Refused(
WebLifecycleOwner.NativeProcess,
"backup_restore_executable_unavailable",
ApiMessageKeys.BackupRestoreExecutableUnavailable);
}
if (isLinux)
{
var executable = isUnixExecutable ?? IsUnixExecutable;
if (string.IsNullOrWhiteSpace(setsidPath)
|| !exists(setsidPath)
|| !executable(setsidPath))
{
return Refused(
WebLifecycleOwner.NativeProcess,
"backup_restore_setsid_unavailable",
ApiMessageKeys.BackupRestoreSetSidUnavailable);
}
}
WebReplacementCommand? command;
try
{
command = WebReplacementCommand.CreateNative(
isLinux,
isWindows,
setsidPath,
processPath,
managedEntryPoint,
hostArguments);
}
catch (Exception exception) when (exception is ArgumentException or IOException or NotSupportedException)
{
command = null;
}
if (command is null)
{
return Refused(
WebLifecycleOwner.NativeProcess,
"backup_restore_executable_unavailable",
ApiMessageKeys.BackupRestoreExecutableUnavailable);
}
return new(true, WebLifecycleOwner.NativeProcess,
new NativeWebRestoreReplacement(endpoints.HealthUri, command, coreWasRunning));
}
private static NativeWebRestorePreflightResult Refused(
WebLifecycleOwner owner,
string code,
string messageKey) => new(false, owner, null, code, messageKey);
private static bool IsUnixExecutable(string path)
{
if (!OperatingSystem.IsLinux())
{
return false;
}
try
{
const UnixFileMode executeBits = UnixFileMode.UserExecute
| UnixFileMode.GroupExecute
| UnixFileMode.OtherExecute;
return (File.GetUnixFileMode(path) & executeBits) != 0;
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or PlatformNotSupportedException)
{
return false;
}
}
}