mirror of
https://github.com/2dust/v2rayN.git
synced 2026-11-14 16:35:00 +03:00
Refactor UI config access and clean up code
Use ConfigTextManager instance methods for window sizes and grid heights in both desktop clients. Standardize formatting and remove redundant imports and casts across the Web API and tests.
This commit is contained in:
@@ -1,8 +1,4 @@
|
|||||||
using System.Reflection;
|
|
||||||
using ReactiveUI.Builder;
|
using ReactiveUI.Builder;
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Resx;
|
|
||||||
using ServiceLib.Tests.CoreConfig;
|
using ServiceLib.Tests.CoreConfig;
|
||||||
using ServiceLib.ViewModels;
|
using ServiceLib.ViewModels;
|
||||||
|
|
||||||
|
|||||||
@@ -67,12 +67,11 @@ public sealed class AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logging.Setup();
|
Logging.Setup();
|
||||||
var config = ConfigTextManager.Instance.Init();
|
_config = ConfigTextManager.Instance.Init();
|
||||||
if (config == null)
|
if (_config == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_config = config;
|
|
||||||
Thread.CurrentThread.CurrentUICulture = new(_config.UiItem.CurrentLanguage);
|
Thread.CurrentThread.CurrentUICulture = new(_config.UiItem.CurrentLanguage);
|
||||||
|
|
||||||
//Under Win10
|
//Under Win10
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ public static class ConfigDbManager
|
|||||||
return Task.FromResult(-1);
|
return Task.FromResult(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
int[] movingIndices = sourceIndices
|
var movingIndices = sourceIndices
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.OrderBy(index => index)
|
.OrderBy(index => index)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|||||||
@@ -259,9 +259,9 @@ public class ConfigTextManager
|
|||||||
|
|
||||||
#region UIItem
|
#region UIItem
|
||||||
|
|
||||||
public static WindowSizeItem? GetWindowSizeItem(Config config, string typeName)
|
public WindowSizeItem? GetWindowSizeItem(string typeName)
|
||||||
{
|
{
|
||||||
var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName);
|
var sizeItem = Config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName);
|
||||||
if (sizeItem == null || sizeItem.Width <= 0 || sizeItem.Height <= 0)
|
if (sizeItem == null || sizeItem.Width <= 0 || sizeItem.Height <= 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -270,13 +270,13 @@ public class ConfigTextManager
|
|||||||
return sizeItem;
|
return sizeItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int SaveWindowSizeItem(Config config, string typeName, double width, double height)
|
public int SaveWindowSizeItem(string typeName, double width, double height)
|
||||||
{
|
{
|
||||||
var sizeItem = config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName);
|
var sizeItem = Config?.UiItem?.WindowSizeItem?.FirstOrDefault(t => t.TypeName == typeName);
|
||||||
if (sizeItem == null)
|
if (sizeItem == null)
|
||||||
{
|
{
|
||||||
sizeItem = new WindowSizeItem { TypeName = typeName };
|
sizeItem = new WindowSizeItem { TypeName = typeName };
|
||||||
config.UiItem.WindowSizeItem.Add(sizeItem);
|
Config.UiItem.WindowSizeItem.Add(sizeItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
sizeItem.Width = (int)width;
|
sizeItem.Width = (int)width;
|
||||||
@@ -285,9 +285,9 @@ public class ConfigTextManager
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int SaveMainGirdHeight(Config config, double height1, double height2)
|
public int SaveMainGirdHeight(double height1, double height2)
|
||||||
{
|
{
|
||||||
var uiItem = config.UiItem ?? new();
|
var uiItem = Config.UiItem ?? new();
|
||||||
|
|
||||||
uiItem.MainGirdHeight1 = (int)(height1 + 0.1);
|
uiItem.MainGirdHeight1 = (int)(height1 + 0.1);
|
||||||
uiItem.MainGirdHeight2 = (int)(height2 + 0.1);
|
uiItem.MainGirdHeight2 = (int)(height2 + 0.1);
|
||||||
|
|||||||
@@ -618,30 +618,62 @@ public partial class AddServerViewModel : MyReactiveObject, ICloseable
|
|||||||
{
|
{
|
||||||
case "Cloudflare R2":
|
case "Cloudflare R2":
|
||||||
XdriveService = "s3";
|
XdriveService = "s3";
|
||||||
if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "auto";
|
if (XdriveS3Region.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
XdriveS3Region = "auto";
|
||||||
|
}
|
||||||
|
|
||||||
XdriveS3PathStyle = true;
|
XdriveS3PathStyle = true;
|
||||||
break;
|
break;
|
||||||
case "Google Drive":
|
case "Google Drive":
|
||||||
XdriveService = "google-drive";
|
XdriveService = "google-drive";
|
||||||
SelectedSource.StreamSecurity = Global.StreamSecurity;
|
SelectedSource.StreamSecurity = Global.StreamSecurity;
|
||||||
if (SelectedSource.Sni.IsNullOrEmpty()) SelectedSource.Sni = "www.google.com";
|
if (SelectedSource.Sni.IsNullOrEmpty())
|
||||||
if (SelectedSource.Alpn.IsNullOrEmpty()) SelectedSource.Alpn = "h2";
|
{
|
||||||
if (SelectedSource.Address.IsNullOrEmpty()) SelectedSource.Address = "www.google.com";
|
SelectedSource.Sni = "www.google.com";
|
||||||
if (SelectedSource.Port <= 0) SelectedSource.Port = 443;
|
}
|
||||||
|
|
||||||
|
if (SelectedSource.Alpn.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
SelectedSource.Alpn = "h2";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectedSource.Address.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
SelectedSource.Address = "www.google.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SelectedSource.Port <= 0)
|
||||||
|
{
|
||||||
|
SelectedSource.Port = 443;
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "AWS S3 (Global)":
|
case "AWS S3 (Global)":
|
||||||
XdriveService = "s3";
|
XdriveService = "s3";
|
||||||
if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ap-northeast-1";
|
if (XdriveS3Region.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
XdriveS3Region = "ap-northeast-1";
|
||||||
|
}
|
||||||
|
|
||||||
XdriveS3PathStyle = false;
|
XdriveS3PathStyle = false;
|
||||||
break;
|
break;
|
||||||
case "Yandex Cloud S3":
|
case "Yandex Cloud S3":
|
||||||
XdriveService = "s3";
|
XdriveService = "s3";
|
||||||
if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ru-central1";
|
if (XdriveS3Region.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
XdriveS3Region = "ru-central1";
|
||||||
|
}
|
||||||
|
|
||||||
XdriveS3PathStyle = true;
|
XdriveS3PathStyle = true;
|
||||||
break;
|
break;
|
||||||
case "ArvanCloud S3":
|
case "ArvanCloud S3":
|
||||||
XdriveService = "s3";
|
XdriveService = "s3";
|
||||||
if (XdriveS3Region.IsNullOrEmpty()) XdriveS3Region = "ir-tbz-sh1";
|
if (XdriveS3Region.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
XdriveS3Region = "ir-tbz-sh1";
|
||||||
|
}
|
||||||
|
|
||||||
XdriveS3PathStyle = true;
|
XdriveS3PathStyle = true;
|
||||||
break;
|
break;
|
||||||
case "Custom S3":
|
case "Custom S3":
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sizeItem = ConfigTextManager.GetWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name);
|
var sizeItem = ConfigTextManager.Instance.GetWindowSizeItem(GetType().Name);
|
||||||
if (sizeItem is null)
|
if (sizeItem is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -55,7 +55,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
|
|||||||
base.OnClosed(e);
|
base.OnClosed(e);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name, Width, Height);
|
ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,15 +396,15 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
|
|||||||
|
|
||||||
private void StorageUI()
|
private void StorageUI()
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveWindowSizeItem(_config, GetType().Name, Width, Height);
|
ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height);
|
||||||
|
|
||||||
if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal)
|
if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal)
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveMainGirdHeight(_config, gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth);
|
ConfigTextManager.Instance.SaveMainGirdHeight(gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth);
|
||||||
}
|
}
|
||||||
else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical)
|
else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical)
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveMainGirdHeight(_config, gridMain.RowDefinitions[0].ActualHeight, gridMain.RowDefinitions[2].ActualHeight);
|
ConfigTextManager.Instance.SaveMainGirdHeight(gridMain.RowDefinitions[0].ActualHeight, gridMain.RowDefinitions[2].ActualHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using ServiceLib.Base;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Adapters;
|
namespace v2rayN.WebAPI.Adapters;
|
||||||
|
|
||||||
public sealed class HeadlessWindowDialog : IWindowDialog
|
public sealed class HeadlessWindowDialog : IWindowDialog
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Security;
|
using v2rayN.WebAPI.Security;
|
||||||
@@ -206,7 +204,7 @@ public static class WebApiEndpoints
|
|||||||
{
|
{
|
||||||
var (result, filePath) = await runtime.CreateBackupArchiveAsync();
|
var (result, filePath) = await runtime.CreateBackupArchiveAsync();
|
||||||
return result.Success && filePath is not null
|
return result.Success && filePath is not null
|
||||||
? (IResult)new DeleteAfterFileResult(filePath)
|
? new DeleteAfterFileResult(filePath)
|
||||||
: ApiReplies.Operation(result);
|
: ApiReplies.Operation(result);
|
||||||
});
|
});
|
||||||
app.MapPost("/api/backup/restore", async (IFormFile file, V2rayRuntime runtime, CancellationToken cancellationToken) =>
|
app.MapPost("/api/backup/restore", async (IFormFile file, V2rayRuntime runtime, CancellationToken cancellationToken) =>
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"rid": "linux-x64",
|
"rid": "linux-x64",
|
||||||
"arch": "64",
|
"arch": "64",
|
||||||
"full": "v2rayN-linux-64-WebAPI.zip",
|
"full": "v2rayN-linux-64-WebAPI.zip",
|
||||||
"update": "v2rayN-linux-64-WebAPI-update.zip"
|
"update": "v2rayN-linux-64-WebAPI-update.zip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rid": "linux-arm64",
|
"rid": "linux-arm64",
|
||||||
"arch": "arm64",
|
"arch": "arm64",
|
||||||
"full": "v2rayN-linux-arm64-WebAPI.zip",
|
"full": "v2rayN-linux-arm64-WebAPI.zip",
|
||||||
"update": "v2rayN-linux-arm64-WebAPI-update.zip"
|
"update": "v2rayN-linux-arm64-WebAPI-update.zip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rid": "win-x64",
|
"rid": "win-x64",
|
||||||
"arch": "64",
|
"arch": "64",
|
||||||
"full": "v2rayN-windows-64-WebAPI.zip",
|
"full": "v2rayN-windows-64-WebAPI.zip",
|
||||||
"update": "v2rayN-windows-64-WebAPI-update.zip"
|
"update": "v2rayN-windows-64-WebAPI-update.zip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rid": "win-arm64",
|
"rid": "win-arm64",
|
||||||
"arch": "arm64",
|
"arch": "arm64",
|
||||||
"full": "v2rayN-windows-arm64-WebAPI.zip",
|
"full": "v2rayN-windows-arm64-WebAPI.zip",
|
||||||
"update": "v2rayN-windows-arm64-WebAPI-update.zip"
|
"update": "v2rayN-windows-arm64-WebAPI-update.zip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rid": "linux-riscv64",
|
"rid": "linux-riscv64",
|
||||||
"arch": "riscv64",
|
"arch": "riscv64",
|
||||||
"full": "v2rayN-linux-riscv64-WebAPI.zip",
|
"full": "v2rayN-linux-riscv64-WebAPI.zip",
|
||||||
"update": "v2rayN-linux-riscv64-WebAPI-update.zip"
|
"update": "v2rayN-linux-riscv64-WebAPI-update.zip"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rid": "linux-loongarch64",
|
"rid": "linux-loongarch64",
|
||||||
"arch": "loong64",
|
"arch": "loong64",
|
||||||
"full": "v2rayN-linux-loong64-WebAPI.zip",
|
"full": "v2rayN-linux-loong64-WebAPI.zip",
|
||||||
"update": "v2rayN-linux-loong64-WebAPI-update.zip"
|
"update": "v2rayN-linux-loong64-WebAPI-update.zip"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,24 +28,39 @@ internal static class NativeEnvironmentFile
|
|||||||
{
|
{
|
||||||
var path = Path.Combine(Path.GetFullPath(executableDirectory), ".env");
|
var path = Path.Combine(Path.GetFullPath(executableDirectory), ".env");
|
||||||
if (Directory.Exists(path))
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
throw new NativeEnvironmentConfigurationException($"Invalid environment configuration in '{path}': expected a regular file.");
|
throw new NativeEnvironmentConfigurationException($"Invalid environment configuration in '{path}': expected a regular file.");
|
||||||
if (!File.Exists(path)) return;
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Dictionary<string, string> values;
|
Dictionary<string, string> values;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
if (stream.Length > MaximumFileBytes)
|
if (stream.Length > MaximumFileBytes)
|
||||||
|
{
|
||||||
throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit.");
|
throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit.");
|
||||||
|
}
|
||||||
|
|
||||||
using var contents = new MemoryStream();
|
using var contents = new MemoryStream();
|
||||||
var buffer = new byte[4096];
|
var buffer = new byte[4096];
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var read = stream.Read(buffer, 0, buffer.Length);
|
var read = stream.Read(buffer, 0, buffer.Length);
|
||||||
if (read == 0) break;
|
if (read == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (contents.Length + read > MaximumFileBytes)
|
if (contents.Length + read > MaximumFileBytes)
|
||||||
|
{
|
||||||
throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit.");
|
throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit.");
|
||||||
|
}
|
||||||
|
|
||||||
contents.Write(buffer, 0, read);
|
contents.Write(buffer, 0, read);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +81,9 @@ internal static class NativeEnvironmentFile
|
|||||||
{
|
{
|
||||||
// Never replace a variable inherited from the OS, systemd, or the container runtime.
|
// Never replace a variable inherited from the OS, systemd, or the container runtime.
|
||||||
if (!processEnvironment.ContainsKey(key))
|
if (!processEnvironment.ContainsKey(key))
|
||||||
|
{
|
||||||
Environment.SetEnvironmentVariable(key, mergedValues[key]);
|
Environment.SetEnvironmentVariable(key, mergedValues[key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +91,19 @@ internal static class NativeEnvironmentFile
|
|||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(contents);
|
ArgumentNullException.ThrowIfNull(contents);
|
||||||
if (StrictUtf8.GetByteCount(contents) > MaximumFileBytes)
|
if (StrictUtf8.GetByteCount(contents) > MaximumFileBytes)
|
||||||
|
{
|
||||||
throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit.");
|
throw new NativeEnvironmentConfigurationException($"The file exceeds the {MaximumFileBytes}-byte size limit.");
|
||||||
|
}
|
||||||
|
|
||||||
if (contents.Contains('\0'))
|
if (contents.Contains('\0'))
|
||||||
|
{
|
||||||
throw new NativeEnvironmentConfigurationException("NUL characters are not allowed.");
|
throw new NativeEnvironmentConfigurationException("NUL characters are not allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
if (contents.Length > 0 && contents[0] == '\uFEFF')
|
if (contents.Length > 0 && contents[0] == '\uFEFF')
|
||||||
|
{
|
||||||
contents = contents[1..];
|
contents = contents[1..];
|
||||||
|
}
|
||||||
|
|
||||||
var parsed = new Dictionary<string, string>(StringComparer.Ordinal);
|
var parsed = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
using var reader = new StringReader(contents);
|
using var reader = new StringReader(contents);
|
||||||
@@ -88,19 +112,29 @@ internal static class NativeEnvironmentFile
|
|||||||
{
|
{
|
||||||
lineNumber++;
|
lineNumber++;
|
||||||
var line = rawLine.Trim();
|
var line = rawLine.Trim();
|
||||||
if (line.Length == 0 || line.StartsWith('#')) continue;
|
if (line.Length == 0 || line.StartsWith('#'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var separator = line.IndexOf('=');
|
var separator = line.IndexOf('=');
|
||||||
if (separator <= 0)
|
if (separator <= 0)
|
||||||
|
{
|
||||||
throw InvalidLine(lineNumber, "expected KEY=value");
|
throw InvalidLine(lineNumber, "expected KEY=value");
|
||||||
|
}
|
||||||
|
|
||||||
var key = line[..separator];
|
var key = line[..separator];
|
||||||
if (!IsValidKey(key))
|
if (!IsValidKey(key))
|
||||||
|
{
|
||||||
throw InvalidLine(lineNumber, "invalid key name");
|
throw InvalidLine(lineNumber, "invalid key name");
|
||||||
|
}
|
||||||
|
|
||||||
var value = ParseValue(line[(separator + 1)..], lineNumber);
|
var value = ParseValue(line[(separator + 1)..], lineNumber);
|
||||||
// Duplicate keys are valid but deterministic: the first occurrence wins.
|
// Duplicate keys are valid but deterministic: the first occurrence wins.
|
||||||
if (!parsed.ContainsKey(key)) parsed.Add(key, value);
|
if (!parsed.ContainsKey(key))
|
||||||
|
{
|
||||||
|
parsed.Add(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -125,7 +159,10 @@ internal static class NativeEnvironmentFile
|
|||||||
{
|
{
|
||||||
var values = new Dictionary<string, string?>(EnvironmentVariableComparer());
|
var values = new Dictionary<string, string?>(EnvironmentVariableComparer());
|
||||||
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
|
||||||
|
{
|
||||||
values[(string)entry.Key] = entry.Value?.ToString();
|
values[(string)entry.Key] = entry.Value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +172,15 @@ internal static class NativeEnvironmentFile
|
|||||||
{
|
{
|
||||||
var quote = value[0];
|
var quote = value[0];
|
||||||
if (value.Length < 2 || value[^1] != quote)
|
if (value.Length < 2 || value[^1] != quote)
|
||||||
|
{
|
||||||
throw InvalidLine(lineNumber, "malformed quoted value");
|
throw InvalidLine(lineNumber, "malformed quoted value");
|
||||||
|
}
|
||||||
|
|
||||||
value = value[1..^1];
|
value = value[1..^1];
|
||||||
if (value.Contains(quote))
|
if (value.Contains(quote))
|
||||||
|
{
|
||||||
throw InvalidLine(lineNumber, "quoted values do not support escapes or embedded matching quotes");
|
throw InvalidLine(lineNumber, "quoted values do not support escapes or embedded matching quotes");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (value.Contains('\'') || value.Contains('"'))
|
else if (value.Contains('\'') || value.Contains('"'))
|
||||||
{
|
{
|
||||||
@@ -147,28 +188,39 @@ internal static class NativeEnvironmentFile
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (StrictUtf8.GetByteCount(value) > MaximumValueBytes)
|
if (StrictUtf8.GetByteCount(value) > MaximumValueBytes)
|
||||||
|
{
|
||||||
throw InvalidLine(lineNumber, $"value exceeds the {MaximumValueBytes}-byte size limit");
|
throw InvalidLine(lineNumber, $"value exceeds the {MaximumValueBytes}-byte size limit");
|
||||||
|
}
|
||||||
|
|
||||||
// This is a plain-text format, not a shell: reject shell expansion/escape syntax rather
|
// This is a plain-text format, not a shell: reject shell expansion/escape syntax rather
|
||||||
// than interpreting it. No command or variable expansion is ever performed.
|
// than interpreting it. No command or variable expansion is ever performed.
|
||||||
if (value.Contains('$') || value.Contains('`') || value.Contains('\\'))
|
if (value.Contains('$') || value.Contains('`') || value.Contains('\\'))
|
||||||
|
{
|
||||||
throw InvalidLine(lineNumber, "shell expansion and escape syntax are not supported");
|
throw InvalidLine(lineNumber, "shell expansion and escape syntax are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidKey(string key)
|
private static bool IsValidKey(string key)
|
||||||
{
|
{
|
||||||
if (key.Length == 0 || !IsAsciiLetter(key[0]) && key[0] != '_') return false;
|
if (key.Length == 0 || (!IsAsciiLetter(key[0]) && key[0] != '_'))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (var index = 1; index < key.Length; index++)
|
for (var index = 1; index < key.Length; index++)
|
||||||
{
|
{
|
||||||
var character = key[index];
|
var character = key[index];
|
||||||
if (!IsAsciiLetter(character) && !char.IsAsciiDigit(character) && character != '_') return false;
|
if (!IsAsciiLetter(character) && !char.IsAsciiDigit(character) && character != '_')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsAsciiLetter(char value) => value is >= 'A' and <= 'Z' or >= 'a' and <= 'z';
|
private static bool IsAsciiLetter(char value) => value is (>= 'A' and <= 'Z') or (>= 'a' and <= 'z');
|
||||||
|
|
||||||
private static StringComparer EnvironmentVariableComparer() =>
|
private static StringComparer EnvironmentVariableComparer() =>
|
||||||
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using ServiceLib.Enums;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Contracts;
|
namespace v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
public sealed record SubscriptionInput(
|
public sealed record SubscriptionInput(
|
||||||
|
|||||||
@@ -1 +1,23 @@
|
|||||||
{"launcher":{"started":"v2rayN.WebAPI is running in the background.\nAPI health endpoint: {url}\nThe service is detached from this terminal; Ctrl+C will not stop it.\nStop it with: {command} --stop\nRun in the foreground with: {command} --foreground","alreadyRunningOpening":"v2rayN.WebAPI is already running.\nOpening the API health endpoint: {url}\nStop it with: {command} --stop","alreadyRunning":"v2rayN.WebAPI is already running.\nAPI health endpoint: {url}\nStop it with: {command} --stop","foregroundStarted":"v2rayN.WebAPI is running in the foreground.\nAPI health endpoint: {url}\nPress Ctrl+C to stop the service gracefully.","stopNotRunning":"v2rayN.WebAPI is not running for this data directory.","stopSucceeded":"v2rayN.WebAPI stopped.","stopIdentityUnverified":"Cannot safely stop v2rayN.WebAPI: instance identity could not be verified; no signal was sent.","stopUnprobeable":"Cannot safely stop v2rayN.WebAPI: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address); no signal was sent.","stopSupervisorManaged":"This instance is managed by systemd. Use systemctl stop for its unit; --stop will not compete with the supervisor.","stopCoreStillRunning":"The Web host stopped, but a tracked Core child process is still running. Check the service log before force-stopping it.","stopSignalFailed":"Could not send SIGTERM to v2rayN.WebAPI.","stopTimedOut":"v2rayN.WebAPI received SIGTERM but did not stop within the expected time.","stopTimedOutStage":"Last observed cleanup stage: {stage}.","stopUnsupported":"--stop is supported only on Linux.","startFailed":"v2rayN.WebAPI failed to start; run with --foreground to see details.","startUnprobeable":"v2rayN.WebAPI cannot start in the background: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address). Run with --foreground instead.","existingUnhealthy":"An instance for this data directory is starting but is not ready yet; try again shortly.","instanceInUse":"A v2rayN.WebAPI instance is already using this data directory.","help":"v2rayN.WebAPI - the headless Web API service of v2rayN\n\nUsage: v2rayN.WebAPI [options]\n\nOptions:\n --foreground Run in the foreground; Ctrl+C stops it safely and logs stay on the terminal\n --background Run in the background, detached from the terminal (Linux default in a terminal)\n --no-open Do not open the browser on startup\n --stop Stop the instance running for the same data directory (Linux only)\n --help, -h Show this help and exit\n\nOther arguments (for example --urls http://0.0.0.0:5080) are passed to the web host.\n\nCommon configuration (environment variables; can live in an .env file next to the executable):\n V2RAYN_WEB_API_KEY Management Key; required when listening beyond 127.0.0.1\n ASPNETCORE_URLS Listen address, default http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS Allowed cross-origin sources\n V2RAYN_WEB_AUTOSTART=true Start the Core together with the web service\n V2RAYN_WEB_PROXY_PORT Local proxy port (used by update checks)\n\nDocs: https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"}}
|
{
|
||||||
|
"launcher": {
|
||||||
|
"started": "v2rayN.WebAPI is running in the background.\nAPI health endpoint: {url}\nThe service is detached from this terminal; Ctrl+C will not stop it.\nStop it with: {command} --stop\nRun in the foreground with: {command} --foreground",
|
||||||
|
"alreadyRunningOpening": "v2rayN.WebAPI is already running.\nOpening the API health endpoint: {url}\nStop it with: {command} --stop",
|
||||||
|
"alreadyRunning": "v2rayN.WebAPI is already running.\nAPI health endpoint: {url}\nStop it with: {command} --stop",
|
||||||
|
"foregroundStarted": "v2rayN.WebAPI is running in the foreground.\nAPI health endpoint: {url}\nPress Ctrl+C to stop the service gracefully.",
|
||||||
|
"stopNotRunning": "v2rayN.WebAPI is not running for this data directory.",
|
||||||
|
"stopSucceeded": "v2rayN.WebAPI stopped.",
|
||||||
|
"stopIdentityUnverified": "Cannot safely stop v2rayN.WebAPI: instance identity could not be verified; no signal was sent.",
|
||||||
|
"stopUnprobeable": "Cannot safely stop v2rayN.WebAPI: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address); no signal was sent.",
|
||||||
|
"stopSupervisorManaged": "This instance is managed by systemd. Use systemctl stop for its unit; --stop will not compete with the supervisor.",
|
||||||
|
"stopCoreStillRunning": "The Web host stopped, but a tracked Core child process is still running. Check the service log before force-stopping it.",
|
||||||
|
"stopSignalFailed": "Could not send SIGTERM to v2rayN.WebAPI.",
|
||||||
|
"stopTimedOut": "v2rayN.WebAPI received SIGTERM but did not stop within the expected time.",
|
||||||
|
"stopTimedOutStage": "Last observed cleanup stage: {stage}.",
|
||||||
|
"stopUnsupported": "--stop is supported only on Linux.",
|
||||||
|
"startFailed": "v2rayN.WebAPI failed to start; run with --foreground to see details.",
|
||||||
|
"startUnprobeable": "v2rayN.WebAPI cannot start in the background: the configured listeners have no loopback HTTP health endpoint to verify the instance (for example HTTPS-only or an explicit non-loopback address). Run with --foreground instead.",
|
||||||
|
"existingUnhealthy": "An instance for this data directory is starting but is not ready yet; try again shortly.",
|
||||||
|
"instanceInUse": "A v2rayN.WebAPI instance is already using this data directory.",
|
||||||
|
"help": "v2rayN.WebAPI - the headless Web API service of v2rayN\n\nUsage: v2rayN.WebAPI [options]\n\nOptions:\n --foreground Run in the foreground; Ctrl+C stops it safely and logs stay on the terminal\n --background Run in the background, detached from the terminal (Linux default in a terminal)\n --no-open Do not open the browser on startup\n --stop Stop the instance running for the same data directory (Linux only)\n --help, -h Show this help and exit\n\nOther arguments (for example --urls http://0.0.0.0:5080) are passed to the web host.\n\nCommon configuration (environment variables; can live in an .env file next to the executable):\n V2RAYN_WEB_API_KEY Management Key; required when listening beyond 127.0.0.1\n ASPNETCORE_URLS Listen address, default http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS Allowed cross-origin sources\n V2RAYN_WEB_AUTOSTART=true Start the Core together with the web service\n V2RAYN_WEB_PROXY_PORT Local proxy port (used by update checks)\n\nDocs: https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,23 @@
|
|||||||
{"launcher":{"started":"v2rayN.WebAPI 已在后台启动。\nAPI 健康端点:{url}\n服务已脱离当前终端,Ctrl+C 不会停止它。\n停止服务:{command} --stop\n前台运行:{command} --foreground","alreadyRunningOpening":"v2rayN.WebAPI 已在运行。\n正在打开 API 健康端点:{url}\n停止服务:{command} --stop","alreadyRunning":"v2rayN.WebAPI 已在运行。\nAPI 健康端点:{url}\n停止服务:{command} --stop","foregroundStarted":"v2rayN.WebAPI 正在前台运行。\nAPI 健康端点:{url}\n按 Ctrl+C 可安全停止服务。","stopNotRunning":"v2rayN.WebAPI 未在此数据目录运行。","stopSucceeded":"v2rayN.WebAPI 已停止。","stopIdentityUnverified":"无法安全停止 v2rayN.WebAPI:无法确认实例身份,未发送信号。","stopUnprobeable":"无法安全停止 v2rayN.WebAPI:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址),未发送信号。","stopSupervisorManaged":"此实例由 systemd 管理,请使用 systemctl stop 停止对应服务;--stop 不会与服务管理器争抢生命周期。","stopCoreStillRunning":"Web 主进程已停止,但检测到 Core 子进程仍在运行。请先检查服务日志,再决定是否强制结束。","stopSignalFailed":"无法向 v2rayN.WebAPI 发送 SIGTERM。","stopTimedOut":"v2rayN.WebAPI 已收到 SIGTERM,但未能在预期时间内停止。","stopTimedOutStage":"最后观察到的清理阶段:{stage}。","stopUnsupported":"--stop 仅支持 Linux。","startFailed":"v2rayN.WebAPI 启动失败;请使用 --foreground 查看详细日志。","startUnprobeable":"v2rayN.WebAPI 无法在后台启动:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址)。请改用 --foreground 运行。","existingUnhealthy":"检测到同一数据目录的实例正在启动,但尚未就绪;请稍后重试。","instanceInUse":"同一数据目录下已有 v2rayN.WebAPI 实例正在运行。","help":"v2rayN.WebAPI - v2rayN 的 Web API 服务(无界面)\n\n用法:v2rayN.WebAPI [选项]\n\n选项:\n --foreground 前台运行;Ctrl+C 可安全停止,日志直接输出到终端\n --background 后台运行并脱离终端(Linux 终端下的默认行为)\n --no-open 启动时不自动打开浏览器\n --stop 停止同一数据目录下正在运行的实例(仅 Linux)\n --help, -h 显示本帮助并退出\n\n其余参数(例如 --urls http://0.0.0.0:5080)会原样传递给 Web 主机。\n\n常用配置(环境变量,可写入可执行文件同目录的 .env 文件):\n V2RAYN_WEB_API_KEY 管理密钥;监听非 127.0.0.1 地址时必填\n ASPNETCORE_URLS 监听地址,默认 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允许跨域的来源列表\n V2RAYN_WEB_AUTOSTART=true 启动 Web 服务时自动启动 Core\n V2RAYN_WEB_PROXY_PORT 本地代理端口(供更新检查等使用)\n\n文档:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"}}
|
{
|
||||||
|
"launcher": {
|
||||||
|
"started": "v2rayN.WebAPI 已在后台启动。\nAPI 健康端点:{url}\n服务已脱离当前终端,Ctrl+C 不会停止它。\n停止服务:{command} --stop\n前台运行:{command} --foreground",
|
||||||
|
"alreadyRunningOpening": "v2rayN.WebAPI 已在运行。\n正在打开 API 健康端点:{url}\n停止服务:{command} --stop",
|
||||||
|
"alreadyRunning": "v2rayN.WebAPI 已在运行。\nAPI 健康端点:{url}\n停止服务:{command} --stop",
|
||||||
|
"foregroundStarted": "v2rayN.WebAPI 正在前台运行。\nAPI 健康端点:{url}\n按 Ctrl+C 可安全停止服务。",
|
||||||
|
"stopNotRunning": "v2rayN.WebAPI 未在此数据目录运行。",
|
||||||
|
"stopSucceeded": "v2rayN.WebAPI 已停止。",
|
||||||
|
"stopIdentityUnverified": "无法安全停止 v2rayN.WebAPI:无法确认实例身份,未发送信号。",
|
||||||
|
"stopUnprobeable": "无法安全停止 v2rayN.WebAPI:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址),未发送信号。",
|
||||||
|
"stopSupervisorManaged": "此实例由 systemd 管理,请使用 systemctl stop 停止对应服务;--stop 不会与服务管理器争抢生命周期。",
|
||||||
|
"stopCoreStillRunning": "Web 主进程已停止,但检测到 Core 子进程仍在运行。请先检查服务日志,再决定是否强制结束。",
|
||||||
|
"stopSignalFailed": "无法向 v2rayN.WebAPI 发送 SIGTERM。",
|
||||||
|
"stopTimedOut": "v2rayN.WebAPI 已收到 SIGTERM,但未能在预期时间内停止。",
|
||||||
|
"stopTimedOutStage": "最后观察到的清理阶段:{stage}。",
|
||||||
|
"stopUnsupported": "--stop 仅支持 Linux。",
|
||||||
|
"startFailed": "v2rayN.WebAPI 启动失败;请使用 --foreground 查看详细日志。",
|
||||||
|
"startUnprobeable": "v2rayN.WebAPI 无法在后台启动:当前监听配置没有可供验证实例身份的回环 HTTP 健康端点(例如仅 HTTPS 或仅绑定非回环地址)。请改用 --foreground 运行。",
|
||||||
|
"existingUnhealthy": "检测到同一数据目录的实例正在启动,但尚未就绪;请稍后重试。",
|
||||||
|
"instanceInUse": "同一数据目录下已有 v2rayN.WebAPI 实例正在运行。",
|
||||||
|
"help": "v2rayN.WebAPI - v2rayN 的 Web API 服务(无界面)\n\n用法:v2rayN.WebAPI [选项]\n\n选项:\n --foreground 前台运行;Ctrl+C 可安全停止,日志直接输出到终端\n --background 后台运行并脱离终端(Linux 终端下的默认行为)\n --no-open 启动时不自动打开浏览器\n --stop 停止同一数据目录下正在运行的实例(仅 Linux)\n --help, -h 显示本帮助并退出\n\n其余参数(例如 --urls http://0.0.0.0:5080)会原样传递给 Web 主机。\n\n常用配置(环境变量,可写入可执行文件同目录的 .env 文件):\n V2RAYN_WEB_API_KEY 管理密钥;监听非 127.0.0.1 地址时必填\n ASPNETCORE_URLS 监听地址,默认 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允许跨域的来源列表\n V2RAYN_WEB_AUTOSTART=true 启动 Web 服务时自动启动 Core\n V2RAYN_WEB_PROXY_PORT 本地代理端口(供更新检查等使用)\n\n文档:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,23 @@
|
|||||||
{"launcher":{"started":"v2rayN.WebAPI 已在背景啟動。\nAPI 健康端點:{url}\n服務已脫離目前終端,Ctrl+C 不會停止它。\n停止服務:{command} --stop\n前台執行:{command} --foreground","alreadyRunningOpening":"v2rayN.WebAPI 已在執行。\n正在開啟 API 健康端點:{url}\n停止服務:{command} --stop","alreadyRunning":"v2rayN.WebAPI 已在執行。\nAPI 健康端點:{url}\n停止服務:{command} --stop","foregroundStarted":"v2rayN.WebAPI 正在前台執行。\nAPI 健康端點:{url}\n按 Ctrl+C 可安全停止服務。","stopNotRunning":"v2rayN.WebAPI 未在此資料目錄執行。","stopSucceeded":"v2rayN.WebAPI 已停止。","stopIdentityUnverified":"無法安全停止 v2rayN.WebAPI:無法確認執行個體身分,未傳送訊號。","stopUnprobeable":"無法安全停止 v2rayN.WebAPI:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址),未傳送訊號。","stopSupervisorManaged":"此執行個體由 systemd 管理,請使用 systemctl stop 停止對應服務;--stop 不會與服務管理器爭搶生命週期。","stopCoreStillRunning":"Web 主程序已停止,但偵測到 Core 子程序仍在執行。請先檢查服務記錄,再決定是否強制結束。","stopSignalFailed":"無法向 v2rayN.WebAPI 傳送 SIGTERM。","stopTimedOut":"v2rayN.WebAPI 已收到 SIGTERM,但未能在預期時間內停止。","stopTimedOutStage":"最後觀察到的清理階段:{stage}。","stopUnsupported":"--stop 僅支援 Linux。","startFailed":"v2rayN.WebAPI 啟動失敗;請使用 --foreground 查看詳細記錄。","startUnprobeable":"v2rayN.WebAPI 無法在背景啟動:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址)。請改用 --foreground 執行。","existingUnhealthy":"偵測到相同資料目錄的執行個體正在啟動,但尚未就緒;請稍後重試。","instanceInUse":"相同資料目錄下已有 v2rayN.WebAPI 執行個體正在執行。","help":"v2rayN.WebAPI - v2rayN 的 Web API 服務(無介面)\n\n用法:v2rayN.WebAPI [選項]\n\n選項:\n --foreground 前台執行;Ctrl+C 可安全停止,記錄直接輸出到終端\n --background 背景執行並脫離終端(Linux 終端下的預設行為)\n --no-open 啟動時不自動開啟瀏覽器\n --stop 停止同一資料目錄下正在執行的執行個體(僅 Linux)\n --help, -h 顯示本說明並結束\n\n其餘參數(例如 --urls http://0.0.0.0:5080)會原樣傳遞給 Web 主機。\n\n常用設定(環境變數,可寫入執行檔同目錄的 .env 檔案):\n V2RAYN_WEB_API_KEY 管理金鑰;監聽非 127.0.0.1 位址時必填\n ASPNETCORE_URLS 監聽位址,預設 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允許跨來源的來源清單\n V2RAYN_WEB_AUTOSTART=true 啟動 Web 服務時自動啟動 Core\n V2RAYN_WEB_PROXY_PORT 本機代理連接埠(供更新檢查等使用)\n\n文件:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"}}
|
{
|
||||||
|
"launcher": {
|
||||||
|
"started": "v2rayN.WebAPI 已在背景啟動。\nAPI 健康端點:{url}\n服務已脫離目前終端,Ctrl+C 不會停止它。\n停止服務:{command} --stop\n前台執行:{command} --foreground",
|
||||||
|
"alreadyRunningOpening": "v2rayN.WebAPI 已在執行。\n正在開啟 API 健康端點:{url}\n停止服務:{command} --stop",
|
||||||
|
"alreadyRunning": "v2rayN.WebAPI 已在執行。\nAPI 健康端點:{url}\n停止服務:{command} --stop",
|
||||||
|
"foregroundStarted": "v2rayN.WebAPI 正在前台執行。\nAPI 健康端點:{url}\n按 Ctrl+C 可安全停止服務。",
|
||||||
|
"stopNotRunning": "v2rayN.WebAPI 未在此資料目錄執行。",
|
||||||
|
"stopSucceeded": "v2rayN.WebAPI 已停止。",
|
||||||
|
"stopIdentityUnverified": "無法安全停止 v2rayN.WebAPI:無法確認執行個體身分,未傳送訊號。",
|
||||||
|
"stopUnprobeable": "無法安全停止 v2rayN.WebAPI:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址),未傳送訊號。",
|
||||||
|
"stopSupervisorManaged": "此執行個體由 systemd 管理,請使用 systemctl stop 停止對應服務;--stop 不會與服務管理器爭搶生命週期。",
|
||||||
|
"stopCoreStillRunning": "Web 主程序已停止,但偵測到 Core 子程序仍在執行。請先檢查服務記錄,再決定是否強制結束。",
|
||||||
|
"stopSignalFailed": "無法向 v2rayN.WebAPI 傳送 SIGTERM。",
|
||||||
|
"stopTimedOut": "v2rayN.WebAPI 已收到 SIGTERM,但未能在預期時間內停止。",
|
||||||
|
"stopTimedOutStage": "最後觀察到的清理階段:{stage}。",
|
||||||
|
"stopUnsupported": "--stop 僅支援 Linux。",
|
||||||
|
"startFailed": "v2rayN.WebAPI 啟動失敗;請使用 --foreground 查看詳細記錄。",
|
||||||
|
"startUnprobeable": "v2rayN.WebAPI 無法在背景啟動:目前監聽設定沒有可供驗證執行個體身分的回送 HTTP 健康端點(例如僅 HTTPS 或僅繫結非回送位址)。請改用 --foreground 執行。",
|
||||||
|
"existingUnhealthy": "偵測到相同資料目錄的執行個體正在啟動,但尚未就緒;請稍後重試。",
|
||||||
|
"instanceInUse": "相同資料目錄下已有 v2rayN.WebAPI 執行個體正在執行。",
|
||||||
|
"help": "v2rayN.WebAPI - v2rayN 的 Web API 服務(無介面)\n\n用法:v2rayN.WebAPI [選項]\n\n選項:\n --foreground 前台執行;Ctrl+C 可安全停止,記錄直接輸出到終端\n --background 背景執行並脫離終端(Linux 終端下的預設行為)\n --no-open 啟動時不自動開啟瀏覽器\n --stop 停止同一資料目錄下正在執行的執行個體(僅 Linux)\n --help, -h 顯示本說明並結束\n\n其餘參數(例如 --urls http://0.0.0.0:5080)會原樣傳遞給 Web 主機。\n\n常用設定(環境變數,可寫入執行檔同目錄的 .env 檔案):\n V2RAYN_WEB_API_KEY 管理金鑰;監聽非 127.0.0.1 位址時必填\n ASPNETCORE_URLS 監聽位址,預設 http://127.0.0.1:5080\n V2RAYN_WEB_ALLOWED_ORIGINS 允許跨來源的來源清單\n V2RAYN_WEB_AUTOSTART=true 啟動 Web 服務時自動啟動 Core\n V2RAYN_WEB_PROXY_PORT 本機代理連接埠(供更新檢查等使用)\n\n文件:https://github.com/2dust/v2rayN/tree/master/v2rayN/v2rayN.WebAPI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Launcher;
|
namespace v2rayN.WebAPI.Launcher;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using ServiceLib.Common;
|
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Launcher;
|
namespace v2rayN.WebAPI.Launcher;
|
||||||
@@ -78,8 +76,15 @@ internal static class NativeWebUpdateHelper
|
|||||||
"The update could not be prepared; restarting the unchanged application."));
|
"The update could not be prepared; restarting the unchanged application."));
|
||||||
using var oldProcess = StartWebInstance(plan);
|
using var oldProcess = StartWebInstance(plan);
|
||||||
previousRestarted = await WaitForExpectedHealthAsync(plan, oldProcess, plan.PreviousVersion, HealthTimeout);
|
previousRestarted = await WaitForExpectedHealthAsync(plan, oldProcess, plan.PreviousVersion, HealthTimeout);
|
||||||
if (!previousRestarted) StopOwnedProcess(oldProcess, plan.InstanceLockPath);
|
if (!previousRestarted)
|
||||||
if (previousRestarted) DeleteRuntimeIntent(plan.RuntimeIntentPath);
|
{
|
||||||
|
StopOwnedProcess(oldProcess, plan.InstanceLockPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousRestarted)
|
||||||
|
{
|
||||||
|
DeleteRuntimeIntent(plan.RuntimeIntentPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception restartException)
|
catch (Exception restartException)
|
||||||
{
|
{
|
||||||
@@ -150,7 +155,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
replacementProcess = StartWebInstance(plan);
|
replacementProcess = StartWebInstance(plan);
|
||||||
var healthy = await WaitForExpectedHealthAsync(plan, replacementProcess, plan.ExpectedVersion, HealthTimeout);
|
var healthy = await WaitForExpectedHealthAsync(plan, replacementProcess, plan.ExpectedVersion, HealthTimeout);
|
||||||
if (!healthy) StopOwnedProcess(replacementProcess, plan.InstanceLockPath);
|
if (!healthy)
|
||||||
|
{
|
||||||
|
StopOwnedProcess(replacementProcess, plan.InstanceLockPath);
|
||||||
|
}
|
||||||
|
|
||||||
return healthy;
|
return healthy;
|
||||||
},
|
},
|
||||||
() =>
|
() =>
|
||||||
@@ -163,7 +172,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
replacementProcess = StartWebInstance(plan);
|
replacementProcess = StartWebInstance(plan);
|
||||||
var healthy = await WaitForExpectedHealthAsync(plan, replacementProcess, plan.PreviousVersion, HealthTimeout);
|
var healthy = await WaitForExpectedHealthAsync(plan, replacementProcess, plan.PreviousVersion, HealthTimeout);
|
||||||
if (!healthy) StopOwnedProcess(replacementProcess, plan.InstanceLockPath);
|
if (!healthy)
|
||||||
|
{
|
||||||
|
StopOwnedProcess(replacementProcess, plan.InstanceLockPath);
|
||||||
|
}
|
||||||
|
|
||||||
return healthy;
|
return healthy;
|
||||||
},
|
},
|
||||||
phase => WriteProgress(plan, new WebUpdateProgressState(phase, false, false, plan.ExpectedVersion, null)),
|
phase => WriteProgress(plan, new WebUpdateProgressState(phase, false, false, plan.ExpectedVersion, null)),
|
||||||
@@ -175,7 +188,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (transaction.Success || transaction.RollbackSucceeded)
|
if (transaction.Success || transaction.RollbackSucceeded)
|
||||||
|
{
|
||||||
DeleteRuntimeIntent(plan.RuntimeIntentPath);
|
DeleteRuntimeIntent(plan.RuntimeIntentPath);
|
||||||
|
}
|
||||||
|
|
||||||
WriteProgress(plan, new WebUpdateProgressState(
|
WriteProgress(plan, new WebUpdateProgressState(
|
||||||
transaction.Success ? "completed" : "failed",
|
transaction.Success ? "completed" : "failed",
|
||||||
true,
|
true,
|
||||||
@@ -218,7 +234,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
var restoredProcess = StartWebInstance(plan);
|
var restoredProcess = StartWebInstance(plan);
|
||||||
rollbackSucceeded = await WaitForExpectedHealthAsync(plan, restoredProcess, plan.PreviousVersion, HealthTimeout);
|
rollbackSucceeded = await WaitForExpectedHealthAsync(plan, restoredProcess, plan.PreviousVersion, HealthTimeout);
|
||||||
if (rollbackSucceeded) DeleteRuntimeIntent(plan.RuntimeIntentPath);
|
if (rollbackSucceeded)
|
||||||
|
{
|
||||||
|
DeleteRuntimeIntent(plan.RuntimeIntentPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception restartException)
|
catch (Exception restartException)
|
||||||
{
|
{
|
||||||
@@ -274,7 +293,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
internal static string CreateIsolatedHelperExecutable(string executable)
|
internal static string CreateIsolatedHelperExecutable(string executable)
|
||||||
{
|
{
|
||||||
if (new FileInfo(executable).LinkTarget is not null)
|
if (new FileInfo(executable).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The native update helper cannot be copied from a symlink.");
|
throw new InvalidDataException("The native update helper cannot be copied from a symlink.");
|
||||||
|
}
|
||||||
|
|
||||||
var extension = OperatingSystem.IsWindows() ? ".exe" : string.Empty;
|
var extension = OperatingSystem.IsWindows() ? ".exe" : string.Empty;
|
||||||
var helperPath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(executable))!,
|
var helperPath = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(executable))!,
|
||||||
$"{IsolatedHelperPrefix}{Guid.NewGuid():N}{extension}");
|
$"{IsolatedHelperPrefix}{Guid.NewGuid():N}{extension}");
|
||||||
@@ -282,7 +304,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
File.Copy(executable, helperPath, overwrite: false);
|
File.Copy(executable, helperPath, overwrite: false);
|
||||||
if (OperatingSystem.IsLinux())
|
if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
File.SetUnixFileMode(helperPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
|
File.SetUnixFileMode(helperPath, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
|
||||||
|
}
|
||||||
|
|
||||||
return helperPath;
|
return helperPath;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -294,7 +319,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
|
|
||||||
internal static bool IsUpdateHelperExecutableName(string name)
|
internal static bool IsUpdateHelperExecutableName(string name)
|
||||||
{
|
{
|
||||||
if (name is "v2rayN.WebAPI" or "v2rayN.WebAPI.exe") return true;
|
if (name is "v2rayN.WebAPI" or "v2rayN.WebAPI.exe")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return IsIsolatedHelperExecutableName(name);
|
return IsIsolatedHelperExecutableName(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +444,8 @@ internal static class NativeWebUpdateHelper
|
|||||||
throw new InvalidDataException("The staged executable identity does not match the verified update plan.");
|
throw new InvalidDataException("The staged executable identity does not match the verified update plan.");
|
||||||
}
|
}
|
||||||
var writableProbe = Path.Combine(install, $".v2rayn-WebAPI-helper-write-probe-{Guid.NewGuid():N}");
|
var writableProbe = Path.Combine(install, $".v2rayn-WebAPI-helper-write-probe-{Guid.NewGuid():N}");
|
||||||
using (new FileStream(writableProbe, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { }
|
using (new FileStream(writableProbe, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||||
|
{ }
|
||||||
File.Delete(writableProbe);
|
File.Delete(writableProbe);
|
||||||
if (new DirectoryInfo(install).LinkTarget is not null
|
if (new DirectoryInfo(install).LinkTarget is not null
|
||||||
|| new FileInfo(Path.Combine(install, executableName)).LinkTarget is not null)
|
|| new FileInfo(Path.Combine(install, executableName)).LinkTarget is not null)
|
||||||
@@ -513,13 +543,19 @@ internal static class NativeWebUpdateHelper
|
|||||||
var executableName = WebUpdatePackageStager.ExecutableNameForRid(plan.Rid);
|
var executableName = WebUpdatePackageStager.ExecutableNameForRid(plan.Rid);
|
||||||
Directory.CreateDirectory(plan.BackupDirectory);
|
Directory.CreateDirectory(plan.BackupDirectory);
|
||||||
if (new FileInfo(Path.Combine(plan.InstallDirectory, executableName)).LinkTarget is not null)
|
if (new FileInfo(Path.Combine(plan.InstallDirectory, executableName)).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The installed Web executable is a symlink and cannot be transactionally replaced.");
|
throw new InvalidDataException("The installed Web executable is a symlink and cannot be transactionally replaced.");
|
||||||
|
}
|
||||||
|
|
||||||
File.Copy(Path.Combine(plan.InstallDirectory, executableName), Path.Combine(plan.BackupDirectory, executableName));
|
File.Copy(Path.Combine(plan.InstallDirectory, executableName), Path.Combine(plan.BackupDirectory, executableName));
|
||||||
var oldIdentity = Path.Combine(plan.InstallDirectory, "v2rayN.WebAPI.build.json");
|
var oldIdentity = Path.Combine(plan.InstallDirectory, "v2rayN.WebAPI.build.json");
|
||||||
if (File.Exists(oldIdentity))
|
if (File.Exists(oldIdentity))
|
||||||
{
|
{
|
||||||
if (new FileInfo(oldIdentity).LinkTarget is not null)
|
if (new FileInfo(oldIdentity).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The installed Web build identity is a symlink.");
|
throw new InvalidDataException("The installed Web build identity is a symlink.");
|
||||||
|
}
|
||||||
|
|
||||||
File.Copy(oldIdentity, Path.Combine(plan.BackupDirectory, "v2rayN.WebAPI.build.json"));
|
File.Copy(oldIdentity, Path.Combine(plan.BackupDirectory, "v2rayN.WebAPI.build.json"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -546,7 +582,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
var install = plan.InstallDirectory;
|
var install = plan.InstallDirectory;
|
||||||
var backup = plan.BackupDirectory;
|
var backup = plan.BackupDirectory;
|
||||||
var executableName = WebUpdatePackageStager.ExecutableNameForRid(plan.Rid);
|
var executableName = WebUpdatePackageStager.ExecutableNameForRid(plan.Rid);
|
||||||
if (!Directory.Exists(backup)) throw new DirectoryNotFoundException("The previous Web application backup is missing.");
|
if (!Directory.Exists(backup))
|
||||||
|
{
|
||||||
|
throw new DirectoryNotFoundException("The previous Web application backup is missing.");
|
||||||
|
}
|
||||||
|
|
||||||
var previousIdentity = Path.Combine(backup, "v2rayN.WebAPI.build.json");
|
var previousIdentity = Path.Combine(backup, "v2rayN.WebAPI.build.json");
|
||||||
var restoredExecutable = Path.Combine(install, $".v2rayn-WebAPI-restore-{Guid.NewGuid():N}{Path.GetExtension(executableName)}");
|
var restoredExecutable = Path.Combine(install, $".v2rayn-WebAPI-restore-{Guid.NewGuid():N}{Path.GetExtension(executableName)}");
|
||||||
@@ -622,7 +661,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
throw new PlatformNotSupportedException("Native Web self-update is supported on Linux and Windows x64.");
|
throw new PlatformNotSupportedException("Native Web self-update is supported on Linux and Windows x64.");
|
||||||
}
|
}
|
||||||
foreach (var argument in plan.HostArguments) startInfo.ArgumentList.Add(argument);
|
foreach (var argument in plan.HostArguments)
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add(argument);
|
||||||
|
}
|
||||||
|
|
||||||
return Process.Start(startInfo) ?? throw new InvalidOperationException("Could not start the updated Web application.");
|
return Process.Start(startInfo) ?? throw new InvalidOperationException("Could not start the updated Web application.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,7 +722,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(intentPath)) return false;
|
if (!File.Exists(intentPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var intent = JsonSerializer.Deserialize<RuntimeRestartIntent>(File.ReadAllText(intentPath), JsonOptions);
|
var intent = JsonSerializer.Deserialize<RuntimeRestartIntent>(File.ReadAllText(intentPath), JsonOptions);
|
||||||
return intent is not null && NativeWebUpdateWorkflow.RuntimeIntentIsSatisfied(intent, health);
|
return intent is not null && NativeWebUpdateWorkflow.RuntimeIntentIsSatisfied(intent, health);
|
||||||
}
|
}
|
||||||
@@ -694,7 +741,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
var deadline = DateTimeOffset.UtcNow + timeout;
|
var deadline = DateTimeOffset.UtcNow + timeout;
|
||||||
while (DateTimeOffset.UtcNow < deadline)
|
while (DateTimeOffset.UtcNow < deadline)
|
||||||
{
|
{
|
||||||
if (!WebInstanceLock.IsHeld(lockPath)) return true;
|
if (!WebInstanceLock.IsHeld(lockPath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(PollInterval);
|
await Task.Delay(PollInterval);
|
||||||
}
|
}
|
||||||
return !WebInstanceLock.IsHeld(lockPath);
|
return !WebInstanceLock.IsHeld(lockPath);
|
||||||
@@ -702,7 +753,11 @@ internal static class NativeWebUpdateHelper
|
|||||||
|
|
||||||
private static Process? CaptureWindowsOwnerProcess(NativeWebUpdatePlan plan)
|
private static Process? CaptureWindowsOwnerProcess(NativeWebUpdatePlan plan)
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsWindows() || !WebInstanceLock.IsHeld(plan.InstanceLockPath)) return null;
|
if (!OperatingSystem.IsWindows() || !WebInstanceLock.IsHeld(plan.InstanceLockPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var ownerProcessId = WebInstanceLock.ReadOwnerProcessId(plan.InstanceLockPath)
|
var ownerProcessId = WebInstanceLock.ReadOwnerProcessId(plan.InstanceLockPath)
|
||||||
?? throw new InvalidDataException("The running Windows Web process identity could not be read from its instance lock.");
|
?? throw new InvalidDataException("The running Windows Web process identity could not be read from its instance lock.");
|
||||||
try
|
try
|
||||||
@@ -722,7 +777,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (process.HasExited) return true;
|
if (process.HasExited)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
@@ -730,16 +788,25 @@ internal static class NativeWebUpdateHelper
|
|||||||
}
|
}
|
||||||
await Task.Delay(PollInterval);
|
await Task.Delay(PollInterval);
|
||||||
}
|
}
|
||||||
try { return process.HasExited; }
|
try
|
||||||
|
{ return process.HasExited; }
|
||||||
catch (InvalidOperationException) { return true; }
|
catch (InvalidOperationException) { return true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StopOwnedProcess(Process? process, string lockPath)
|
private static void StopOwnedProcess(Process? process, string lockPath)
|
||||||
{
|
{
|
||||||
if (process is null) return;
|
if (process is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (process.HasExited) return;
|
if (process.HasExited)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (WebInstanceLock.ReadOwnerProcessId(lockPath) == process.Id)
|
if (WebInstanceLock.ReadOwnerProcessId(lockPath) == process.Id)
|
||||||
{
|
{
|
||||||
process.Kill(entireProcessTree: true);
|
process.Kill(entireProcessTree: true);
|
||||||
@@ -766,16 +833,24 @@ internal static class NativeWebUpdateHelper
|
|||||||
private static void EnsureNoLinks(string directory)
|
private static void EnsureNoLinks(string directory)
|
||||||
{
|
{
|
||||||
if (new DirectoryInfo(directory).LinkTarget is not null)
|
if (new DirectoryInfo(directory).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The staged update directory is a symlink.");
|
throw new InvalidDataException("The staged update directory is a symlink.");
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly))
|
foreach (var file in Directory.EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly))
|
||||||
{
|
{
|
||||||
if (new FileInfo(file).LinkTarget is not null)
|
if (new FileInfo(file).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The staged Web update contains a symbolic link.");
|
throw new InvalidDataException("The staged Web update contains a symbolic link.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
foreach (var childDirectory in Directory.EnumerateDirectories(directory, "*", SearchOption.TopDirectoryOnly))
|
foreach (var childDirectory in Directory.EnumerateDirectories(directory, "*", SearchOption.TopDirectoryOnly))
|
||||||
{
|
{
|
||||||
if (new DirectoryInfo(childDirectory).LinkTarget is not null)
|
if (new DirectoryInfo(childDirectory).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The staged Web update contains a symbolic link.");
|
throw new InvalidDataException("The staged Web update contains a symbolic link.");
|
||||||
|
}
|
||||||
|
|
||||||
EnsureNoLinks(childDirectory);
|
EnsureNoLinks(childDirectory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -800,7 +875,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
Directory.Delete(path, recursive: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
@@ -812,7 +890,10 @@ internal static class NativeWebUpdateHelper
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(path)) File.Delete(path);
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ internal static class NativeWebUpdateWorkflow
|
|||||||
{
|
{
|
||||||
var hasCore = (health.CoreProcessIds?.Count ?? 0) > 0;
|
var hasCore = (health.CoreProcessIds?.Count ?? 0) > 0;
|
||||||
if (!intent.WasRunning)
|
if (!intent.WasRunning)
|
||||||
|
{
|
||||||
return string.Equals(health.CoreState, "stopped", StringComparison.OrdinalIgnoreCase) && !hasCore;
|
return string.Equals(health.CoreState, "stopped", StringComparison.OrdinalIgnoreCase) && !hasCore;
|
||||||
|
}
|
||||||
|
|
||||||
return string.Equals(health.CoreState, "running", StringComparison.OrdinalIgnoreCase)
|
return string.Equals(health.CoreState, "running", StringComparison.OrdinalIgnoreCase)
|
||||||
&& hasCore
|
&& hasCore
|
||||||
&& !string.IsNullOrWhiteSpace(health.CoreProfileId);
|
&& !string.IsNullOrWhiteSpace(health.CoreProfileId);
|
||||||
@@ -76,9 +79,15 @@ internal static class NativeWebUpdateWorkflow
|
|||||||
setPhase("restarting");
|
setPhase("restarting");
|
||||||
setPhase("verifying-health");
|
setPhase("verifying-health");
|
||||||
if (!await startPreviousAndVerifyHealthAsync())
|
if (!await startPreviousAndVerifyHealthAsync())
|
||||||
|
{
|
||||||
return new(false, false, $"{failure} The previous Web build did not become healthy. {restoreFailure}");
|
return new(false, false, $"{failure} The previous Web build did not become healthy. {restoreFailure}");
|
||||||
|
}
|
||||||
|
|
||||||
if (restoreFailure is not null)
|
if (restoreFailure is not null)
|
||||||
|
{
|
||||||
return new(false, false, $"{failure} The previous Web process is responding, but restoring all previous app files failed: {restoreFailure}");
|
return new(false, false, $"{failure} The previous Web process is responding, but restoring all previous app files failed: {restoreFailure}");
|
||||||
|
}
|
||||||
|
|
||||||
await cleanBackupsAsync();
|
await cleanBackupsAsync();
|
||||||
return new(false, true, $"{failure} The previous Web build was restored and is healthy.");
|
return new(false, true, $"{failure} The previous Web build was restored and is healthy.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public sealed class WebInstanceLock : IDisposable
|
|||||||
{
|
{
|
||||||
throw new IOException("Unable to open the v2rayN.WebAPI instance lock.", new Win32Exception(Marshal.GetLastPInvokeError()));
|
throw new IOException("Unable to open the v2rayN.WebAPI instance lock.", new Win32Exception(Marshal.GetLastPInvokeError()));
|
||||||
}
|
}
|
||||||
stream = new FileStream(new SafeFileHandle((IntPtr)descriptor, ownsHandle: true), FileAccess.ReadWrite);
|
stream = new FileStream(new SafeFileHandle(descriptor, ownsHandle: true), FileAccess.ReadWrite);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -97,7 +97,7 @@ public sealed class WebInstanceLock : IDisposable
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
using var stream = new FileStream(new SafeFileHandle((IntPtr)descriptor, ownsHandle: true), FileAccess.Read);
|
using var stream = new FileStream(new SafeFileHandle(descriptor, ownsHandle: true), FileAccess.Read);
|
||||||
using var reader = new StreamReader(stream, Encoding.ASCII);
|
using var reader = new StreamReader(stream, Encoding.ASCII);
|
||||||
ownerText = reader.ReadToEnd().Trim();
|
ownerText = reader.ReadToEnd().Trim();
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,8 @@ public sealed class WebInstanceLock : IDisposable
|
|||||||
}
|
}
|
||||||
else if (OperatingSystem.IsWindows())
|
else if (OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
try { _stream.Unlock(WindowsLockOffset, WindowsLockLength); }
|
try
|
||||||
|
{ _stream.Unlock(WindowsLockOffset, WindowsLockLength); }
|
||||||
catch (IOException) { }
|
catch (IOException) { }
|
||||||
}
|
}
|
||||||
_stream.Dispose();
|
_stream.Dispose();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Launcher;
|
namespace v2rayN.WebAPI.Launcher;
|
||||||
|
|
||||||
public interface IWebHealthProbe
|
public interface IWebHealthProbe
|
||||||
@@ -312,7 +311,10 @@ public sealed class WebLauncher
|
|||||||
processPath,
|
processPath,
|
||||||
WebReplacementCommand.GetManagedEntryPoint(processPath, commandLine),
|
WebReplacementCommand.GetManagedEntryPoint(processPath, commandLine),
|
||||||
hostArguments);
|
hostArguments);
|
||||||
if (command is null) return null;
|
if (command is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Launcher;
|
namespace v2rayN.WebAPI.Launcher;
|
||||||
|
|
||||||
@@ -140,6 +139,7 @@ public interface IWebReplacementProcess : IDisposable
|
|||||||
{
|
{
|
||||||
int Id { get; }
|
int Id { get; }
|
||||||
bool HasExited { get; }
|
bool HasExited { get; }
|
||||||
|
|
||||||
void KillTree();
|
void KillTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +151,7 @@ public interface IWebReplacementProcessStarter
|
|||||||
public interface IWebInstanceOwnershipProbe
|
public interface IWebInstanceOwnershipProbe
|
||||||
{
|
{
|
||||||
bool IsLockHeld(string lockPath);
|
bool IsLockHeld(string lockPath);
|
||||||
|
|
||||||
int? ReadOwnerProcessId(string lockPath);
|
int? ReadOwnerProcessId(string lockPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +187,8 @@ public sealed class NativeReplacementProcessStarter : IWebReplacementProcessStar
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
try { return process.HasExited; }
|
try
|
||||||
|
{ return process.HasExited; }
|
||||||
catch (InvalidOperationException) { return true; }
|
catch (InvalidOperationException) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,8 +243,15 @@ public sealed class NativeWebRestartCoordinator
|
|||||||
_pollInterval = pollInterval ?? DefaultPollInterval;
|
_pollInterval = pollInterval ?? DefaultPollInterval;
|
||||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
_delay = delay ?? ((duration, token) => Task.Delay(duration, _timeProvider, token));
|
_delay = delay ?? ((duration, token) => Task.Delay(duration, _timeProvider, token));
|
||||||
if (_timeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(timeout));
|
if (_timeout <= TimeSpan.Zero)
|
||||||
if (_pollInterval <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(pollInterval));
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pollInterval <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pollInterval));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<WebRestartHandoffResult> StartReplacementAfterOwnerReleaseAsync(
|
public async Task<WebRestartHandoffResult> StartReplacementAfterOwnerReleaseAsync(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.IO;
|
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Launcher;
|
namespace v2rayN.WebAPI.Launcher;
|
||||||
@@ -154,7 +153,11 @@ public sealed class WebStopper
|
|||||||
|
|
||||||
internal static bool IsManagedBySystemd(int processId)
|
internal static bool IsManagedBySystemd(int processId)
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return false;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return IsSystemdServiceCgroup(File.ReadAllText($"/proc/{processId}/cgroup"));
|
return IsSystemdServiceCgroup(File.ReadAllText($"/proc/{processId}/cgroup"));
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using System.Collections;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using v2rayN.WebAPI.Api;
|
using v2rayN.WebAPI.Api;
|
||||||
using v2rayN.WebAPI.Configuration;
|
using v2rayN.WebAPI.Configuration;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
@@ -348,7 +345,11 @@ internal static class Program
|
|||||||
var line = $"{DateTimeOffset.UtcNow:O} {(success ? "INFO" : "ERROR")} {message}"
|
var line = $"{DateTimeOffset.UtcNow:O} {(success ? "INFO" : "ERROR")} {message}"
|
||||||
+ (processId.HasValue ? $" ProcessId={processId.Value}." : string.Empty)
|
+ (processId.HasValue ? $" ProcessId={processId.Value}." : string.Empty)
|
||||||
+ Environment.NewLine;
|
+ Environment.NewLine;
|
||||||
if (!success) Console.Error.WriteLine(message);
|
if (!success)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(message);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Utils.GetLogPath("native-restore-restart.log");
|
var path = Utils.GetLogPath("native-restore-restart.log");
|
||||||
@@ -357,7 +358,10 @@ internal static class Program
|
|||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
if (!success) Console.Error.WriteLine($"Could not persist the native restart diagnostic: {exception.Message}");
|
if (!success)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Could not persist the native restart diagnostic: {exception.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"v2rayN.WebAPI": {
|
"v2rayN.WebAPI": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"applicationUrl": "https://localhost:4939;http://localhost:4940"
|
"applicationUrl": "https://localhost:4939;http://localhost:4940"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,11 @@ public sealed class WebCorsPolicy
|
|||||||
public WebCorsPolicy(string? configuration)
|
public WebCorsPolicy(string? configuration)
|
||||||
{
|
{
|
||||||
_origins = new HashSet<string>(StringComparer.Ordinal);
|
_origins = new HashSet<string>(StringComparer.Ordinal);
|
||||||
if (string.IsNullOrWhiteSpace(configuration)) return;
|
if (string.IsNullOrWhiteSpace(configuration))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var item in configuration.Split(','))
|
foreach (var item in configuration.Split(','))
|
||||||
{
|
{
|
||||||
_origins.Add(NormalizeOrigin(item.Trim()));
|
_origins.Add(NormalizeOrigin(item.Trim()));
|
||||||
@@ -46,7 +50,11 @@ public sealed class WebCorsPolicy
|
|||||||
{
|
{
|
||||||
if (request.Headers["Sec-Fetch-Site"].Count != 1
|
if (request.Headers["Sec-Fetch-Site"].Count != 1
|
||||||
|| request.Headers["Sec-Fetch-Site"] != "same-origin"
|
|| request.Headers["Sec-Fetch-Site"] != "same-origin"
|
||||||
|| request.Headers.Origin.Count != 1) return false;
|
|| request.Headers.Origin.Count != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var origin = request.Headers.Origin.ToString();
|
var origin = request.Headers.Origin.ToString();
|
||||||
@@ -57,8 +65,16 @@ public sealed class WebCorsPolicy
|
|||||||
|
|
||||||
public static bool IsSameOriginRequest(HttpRequest request)
|
public static bool IsSameOriginRequest(HttpRequest request)
|
||||||
{
|
{
|
||||||
if (!request.Headers.ContainsKey("Origin")) return true; // Native clients.
|
if (!request.Headers.ContainsKey("Origin"))
|
||||||
if (request.Headers.Origin.Count != 1) return false;
|
{
|
||||||
|
return true; // Native clients.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Headers.Origin.Count != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return string.Equals(request.Headers.Origin.ToString(),
|
return string.Equals(request.Headers.Origin.ToString(),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Security;
|
namespace v2rayN.WebAPI.Security;
|
||||||
|
|
||||||
@@ -7,6 +6,7 @@ namespace v2rayN.WebAPI.Security;
|
|||||||
public static class WebListenerSecurityPolicy
|
public static class WebListenerSecurityPolicy
|
||||||
{
|
{
|
||||||
public const string DefaultUrl = "http://127.0.0.1:5080";
|
public const string DefaultUrl = "http://127.0.0.1:5080";
|
||||||
|
|
||||||
public const string MissingManagementKeyMessage =
|
public const string MissingManagementKeyMessage =
|
||||||
"Management Key is required for non-loopback Web listeners. Set V2RAYN_WEB_API_KEY before starting v2rayN.WebAPI, or bind only to 127.0.0.1, localhost, or [::1] for local first-run setup.";
|
"Management Key is required for non-loopback Web listeners. Set V2RAYN_WEB_API_KEY before starting v2rayN.WebAPI, or bind only to 127.0.0.1, localhost, or [::1] for local first-run setup.";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Security;
|
namespace v2rayN.WebAPI.Security;
|
||||||
|
|
||||||
@@ -329,9 +328,9 @@ public sealed class WebSessionService : IDisposable
|
|||||||
{
|
{
|
||||||
digest = string.Empty;
|
digest = string.Empty;
|
||||||
if (token is null || token.Length != TokenEncodedLength
|
if (token is null || token.Length != TokenEncodedLength
|
||||||
|| token.Any(character => !(character is >= 'A' and <= 'Z'
|
|| token.Any(character => !(character is (>= 'A' and <= 'Z')
|
||||||
or >= 'a' and <= 'z'
|
or (>= 'a' and <= 'z')
|
||||||
or >= '0' and <= '9'
|
or (>= '0' and <= '9')
|
||||||
or '-' or '_')))
|
or '-' or '_')))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Security;
|
namespace v2rayN.WebAPI.Security;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
internal static class BuiltinDnsProfileBootstrap
|
internal static class BuiltinDnsProfileBootstrap
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler.Builder;
|
using ServiceLib.Handler.Builder;
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ internal static class CoreProcessTracker
|
|||||||
{
|
{
|
||||||
using (process)
|
using (process)
|
||||||
{
|
{
|
||||||
try { ids.Add(process.Id); }
|
try
|
||||||
|
{ ids.Add(process.Id); }
|
||||||
catch (InvalidOperationException) { }
|
catch (InvalidOperationException) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,10 @@ internal static class CoreProcessTracker
|
|||||||
.Where(path => path is not null)
|
.Where(path => path is not null)
|
||||||
.Select(path => path!)
|
.Select(path => path!)
|
||||||
.ToHashSet(PathComparer);
|
.ToHashSet(PathComparer);
|
||||||
if (expected.Count == 0) return [];
|
if (expected.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
var processes = new List<CoreProcessIdentity>();
|
var processes = new List<CoreProcessIdentity>();
|
||||||
foreach (var process in Process.GetProcesses())
|
foreach (var process in Process.GetProcesses())
|
||||||
@@ -50,7 +54,11 @@ internal static class CoreProcessTracker
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (existingProcessIds.Contains(process.Id) || process.HasExited) continue;
|
if (existingProcessIds.Contains(process.Id) || process.HasExited)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var startTimeTicks = GetStartTimeUtcTicks(process);
|
var startTimeTicks = GetStartTimeUtcTicks(process);
|
||||||
var executablePath = GetProcessExecutablePath(process);
|
var executablePath = GetProcessExecutablePath(process);
|
||||||
if (startTimeTicks is null || executablePath is null
|
if (startTimeTicks is null || executablePath is null
|
||||||
@@ -93,9 +101,16 @@ internal static class CoreProcessTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
var startTimeTicks = GetStartTimeUtcTicks(process);
|
var startTimeTicks = GetStartTimeUtcTicks(process);
|
||||||
if (startTimeTicks is long observedStartTime && observedStartTime != identity.StartTimeUtcTicks) continue;
|
if (startTimeTicks is long observedStartTime && observedStartTime != identity.StartTimeUtcTicks)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var executablePath = GetProcessExecutablePath(process);
|
var executablePath = GetProcessExecutablePath(process);
|
||||||
if (executablePath is not null && !PathComparer.Equals(executablePath, identity.ExecutablePath)) continue;
|
if (executablePath is not null && !PathComparer.Equals(executablePath, identity.ExecutablePath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// If an otherwise matching live PID becomes inaccessible, keep it as an
|
// If an otherwise matching live PID becomes inaccessible, keep it as an
|
||||||
// uncertain active identity. This may report a conservative stop failure,
|
// uncertain active identity. This may report a conservative stop failure,
|
||||||
@@ -125,7 +140,10 @@ internal static class CoreProcessTracker
|
|||||||
HashSet<string> expectedPaths,
|
HashSet<string> expectedPaths,
|
||||||
HashSet<string>? expectedConfigPaths)
|
HashSet<string>? expectedConfigPaths)
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return expectedPaths.Contains(executablePath);
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return expectedPaths.Contains(executablePath);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -157,7 +175,10 @@ internal static class CoreProcessTracker
|
|||||||
private static bool CommandLineReferencesConfig(int processId, IEnumerable<string> arguments, HashSet<string> expectedConfigPaths)
|
private static bool CommandLineReferencesConfig(int processId, IEnumerable<string> arguments, HashSet<string> expectedConfigPaths)
|
||||||
{
|
{
|
||||||
var workingDirectory = GetProcessWorkingDirectory(processId);
|
var workingDirectory = GetProcessWorkingDirectory(processId);
|
||||||
if (workingDirectory is null) return false;
|
if (workingDirectory is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var argument in arguments)
|
foreach (var argument in arguments)
|
||||||
{
|
{
|
||||||
@@ -166,11 +187,18 @@ internal static class CoreProcessTracker
|
|||||||
: argument.StartsWith("--config-file=", StringComparison.Ordinal)
|
: argument.StartsWith("--config-file=", StringComparison.Ordinal)
|
||||||
? argument["--config-file=".Length..]
|
? argument["--config-file=".Length..]
|
||||||
: argument;
|
: argument;
|
||||||
if (configArgument.Length == 0 || configArgument.StartsWith("-", StringComparison.Ordinal)) continue;
|
if (configArgument.Length == 0 || configArgument.StartsWith("-", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var fullPath = NormalizeAbsolutePath(Path.IsPathRooted(configArgument)
|
var fullPath = NormalizeAbsolutePath(Path.IsPathRooted(configArgument)
|
||||||
? configArgument
|
? configArgument
|
||||||
: Path.Combine(workingDirectory, configArgument));
|
: Path.Combine(workingDirectory, configArgument));
|
||||||
if (fullPath is not null && expectedConfigPaths.Contains(fullPath)) return true;
|
if (fullPath is not null && expectedConfigPaths.Contains(fullPath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -190,7 +218,8 @@ internal static class CoreProcessTracker
|
|||||||
|
|
||||||
private static long? GetStartTimeUtcTicks(Process process)
|
private static long? GetStartTimeUtcTicks(Process process)
|
||||||
{
|
{
|
||||||
try { return process.StartTime.ToUniversalTime().Ticks; }
|
try
|
||||||
|
{ return process.StartTime.ToUniversalTime().Ticks; }
|
||||||
catch (Exception exception) when (exception is InvalidOperationException or Win32Exception or NotSupportedException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is InvalidOperationException or Win32Exception or NotSupportedException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -215,7 +244,11 @@ internal static class CoreProcessTracker
|
|||||||
private static string? NormalizeExecutablePath(string? path)
|
private static string? NormalizeExecutablePath(string? path)
|
||||||
{
|
{
|
||||||
var fullPath = NormalizeAbsolutePath(path);
|
var fullPath = NormalizeAbsolutePath(path);
|
||||||
if (fullPath is null) return null;
|
if (fullPath is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var executablePath = fullPath;
|
var executablePath = fullPath;
|
||||||
@@ -237,7 +270,10 @@ internal static class CoreProcessTracker
|
|||||||
current = Path.Combine(current, segment);
|
current = Path.Combine(current, segment);
|
||||||
FileSystemInfo info = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current);
|
FileSystemInfo info = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current);
|
||||||
var linkTarget = info.ResolveLinkTarget(returnFinalTarget: false);
|
var linkTarget = info.ResolveLinkTarget(returnFinalTarget: false);
|
||||||
if (linkTarget is not null) current = linkTarget.FullName;
|
if (linkTarget is not null)
|
||||||
|
{
|
||||||
|
current = linkTarget.FullName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Path.GetFullPath(current);
|
return Path.GetFullPath(current);
|
||||||
}
|
}
|
||||||
@@ -249,8 +285,13 @@ internal static class CoreProcessTracker
|
|||||||
|
|
||||||
private static string? NormalizeAbsolutePath(string? path)
|
private static string? NormalizeAbsolutePath(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path)) return null;
|
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path))
|
||||||
try { return Path.GetFullPath(path.TrimEnd('\0')); }
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{ return Path.GetFullPath(path.TrimEnd('\0')); }
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -266,27 +307,49 @@ internal static class CoreProcessTracker
|
|||||||
var prefix = new byte[256];
|
var prefix = new byte[256];
|
||||||
var length = stream.Read(prefix);
|
var length = stream.Read(prefix);
|
||||||
var lineEnd = Array.IndexOf(prefix, (byte)'\n', 0, length);
|
var lineEnd = Array.IndexOf(prefix, (byte)'\n', 0, length);
|
||||||
if (lineEnd < 0) lineEnd = length;
|
if (lineEnd < 0)
|
||||||
|
{
|
||||||
|
lineEnd = length;
|
||||||
|
}
|
||||||
|
|
||||||
var firstLine = System.Text.Encoding.UTF8.GetString(prefix, 0, lineEnd).TrimEnd('\r');
|
var firstLine = System.Text.Encoding.UTF8.GetString(prefix, 0, lineEnd).TrimEnd('\r');
|
||||||
if (!firstLine.StartsWith("#!", StringComparison.Ordinal)) return interpreters;
|
if (!firstLine.StartsWith("#!", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return interpreters;
|
||||||
|
}
|
||||||
|
|
||||||
var tokens = firstLine[2..].Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
var tokens = firstLine[2..].Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (tokens.Length == 0) return interpreters;
|
if (tokens.Length == 0)
|
||||||
|
{
|
||||||
|
return interpreters;
|
||||||
|
}
|
||||||
|
|
||||||
var interpreter = tokens[0];
|
var interpreter = tokens[0];
|
||||||
if (string.Equals(Path.GetFileName(interpreter), "env", StringComparison.Ordinal))
|
if (string.Equals(Path.GetFileName(interpreter), "env", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
var index = 1;
|
var index = 1;
|
||||||
while (index < tokens.Length && tokens[index].StartsWith("-", StringComparison.Ordinal))
|
while (index < tokens.Length && tokens[index].StartsWith("-", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
if (tokens[index] is "-u" or "--unset" or "-C" or "--chdir" or "-a" or "--argv0") index++;
|
if (tokens[index] is "-u" or "--unset" or "-C" or "--chdir" or "-a" or "--argv0")
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
if (index >= tokens.Length) return interpreters;
|
if (index >= tokens.Length)
|
||||||
|
{
|
||||||
|
return interpreters;
|
||||||
|
}
|
||||||
|
|
||||||
interpreter = FindOnPath(tokens[index]) ?? string.Empty;
|
interpreter = FindOnPath(tokens[index]) ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized = NormalizeExecutablePath(interpreter);
|
var normalized = NormalizeExecutablePath(interpreter);
|
||||||
if (normalized is not null) interpreters.Add(normalized);
|
if (normalized is not null)
|
||||||
|
{
|
||||||
|
interpreters.Add(normalized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
|
||||||
{
|
{
|
||||||
@@ -298,13 +361,24 @@ internal static class CoreProcessTracker
|
|||||||
|
|
||||||
private static string? FindOnPath(string executableName)
|
private static string? FindOnPath(string executableName)
|
||||||
{
|
{
|
||||||
if (Path.IsPathRooted(executableName)) return executableName;
|
if (Path.IsPathRooted(executableName))
|
||||||
|
{
|
||||||
|
return executableName;
|
||||||
|
}
|
||||||
|
|
||||||
var path = Environment.GetEnvironmentVariable("PATH");
|
var path = Environment.GetEnvironmentVariable("PATH");
|
||||||
if (string.IsNullOrEmpty(path)) return null;
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
foreach (var directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||||
{
|
{
|
||||||
var candidate = Path.Combine(directory, executableName);
|
var candidate = Path.Combine(directory, executableName);
|
||||||
if (File.Exists(candidate)) return candidate;
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using ServiceLib.Enums;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
internal enum CoreRuntimeState
|
internal enum CoreRuntimeState
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
using System.Formats.Tar;
|
using System.Formats.Tar;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ internal static class GeoFilesUpdateTransaction
|
|||||||
|
|
||||||
Task RollBackFilesAsync()
|
Task RollBackFilesAsync()
|
||||||
{
|
{
|
||||||
if (rollbackCompleted) return Task.CompletedTask;
|
if (rollbackCompleted)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
var originallyExisting = backups.Keys.ToHashSet(PathComparer);
|
var originallyExisting = backups.Keys.ToHashSet(PathComparer);
|
||||||
foreach (var target in originalTargets)
|
foreach (var target in originalTargets)
|
||||||
@@ -44,7 +47,11 @@ internal static class GeoFilesUpdateTransaction
|
|||||||
foreach (var (target, backup) in backups)
|
foreach (var (target, backup) in backups)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||||
if (IsSymbolicLink(target)) File.Delete(target);
|
if (IsSymbolicLink(target))
|
||||||
|
{
|
||||||
|
File.Delete(target);
|
||||||
|
}
|
||||||
|
|
||||||
File.Copy(backup.BackupPath, target, overwrite: true);
|
File.Copy(backup.BackupPath, target, overwrite: true);
|
||||||
File.SetLastWriteTimeUtc(target, backup.LastWriteTimeUtc);
|
File.SetLastWriteTimeUtc(target, backup.LastWriteTimeUtc);
|
||||||
}
|
}
|
||||||
@@ -67,7 +74,11 @@ internal static class GeoFilesUpdateTransaction
|
|||||||
{
|
{
|
||||||
throw new IOException($"GeoFiles target is a symbolic link and cannot be updated safely: {target}");
|
throw new IOException($"GeoFiles target is a symbolic link and cannot be updated safely: {target}");
|
||||||
}
|
}
|
||||||
if (!File.Exists(target)) continue;
|
if (!File.Exists(target))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var backup = Path.Combine(backupRoot, index.ToString("D6", System.Globalization.CultureInfo.InvariantCulture));
|
var backup = Path.Combine(backupRoot, index.ToString("D6", System.Globalization.CultureInfo.InvariantCulture));
|
||||||
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(target);
|
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(target);
|
||||||
File.Copy(target, backup);
|
File.Copy(target, backup);
|
||||||
@@ -86,7 +97,11 @@ internal static class GeoFilesUpdateTransaction
|
|||||||
throw new IOException($"GeoFiles update did not produce a non-empty {Path.GetFileName(requiredFile)}.");
|
throw new IOException($"GeoFiles update did not produce a non-empty {Path.GetFileName(requiredFile)}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (beforeCommit is not null) await beforeCommit(RollBackFilesAsync, cancellationToken);
|
if (beforeCommit is not null)
|
||||||
|
{
|
||||||
|
await beforeCommit(RollBackFilesAsync, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
transactionCompleted = true;
|
transactionCompleted = true;
|
||||||
}
|
}
|
||||||
catch (Exception updateException)
|
catch (Exception updateException)
|
||||||
@@ -110,7 +125,8 @@ internal static class GeoFilesUpdateTransaction
|
|||||||
{
|
{
|
||||||
if ((transactionCompleted || rollbackCompleted || !updateStarted) && Directory.Exists(backupRoot))
|
if ((transactionCompleted || rollbackCompleted || !updateStarted) && Directory.Exists(backupRoot))
|
||||||
{
|
{
|
||||||
try { Directory.Delete(backupRoot, recursive: true); }
|
try
|
||||||
|
{ Directory.Delete(backupRoot, recursive: true); }
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
// A cleanup failure does not invalidate a successful update or rollback.
|
// A cleanup failure does not invalidate a successful update or rollback.
|
||||||
@@ -127,7 +143,11 @@ internal static class GeoFilesUpdateTransaction
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (new FileInfo(path).LinkTarget is not null) return true;
|
if (new FileInfo(path).LinkTarget is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReparsePoint);
|
return File.Exists(path) && File.GetAttributes(path).HasFlag(FileAttributes.ReparsePoint);
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException)
|
catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using ServiceLib.Common;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,4 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
@@ -351,7 +344,8 @@ internal static class RegionalPresetWorkflow
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class RegionalPresetStagingException(Exception innerException)
|
internal sealed class RegionalPresetStagingException(Exception innerException)
|
||||||
: Exception("Regional preset staging failed.", innerException) { }
|
: Exception("Regional preset staging failed.", innerException)
|
||||||
|
{ }
|
||||||
|
|
||||||
internal static class RegionalPresetApplyWorkflow
|
internal static class RegionalPresetApplyWorkflow
|
||||||
{
|
{
|
||||||
@@ -399,11 +393,13 @@ internal static class RegionalPresetApplyWorkflow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class RegionalPresetCoreApplyFailedException : Exception { }
|
internal sealed class RegionalPresetCoreApplyFailedException : Exception
|
||||||
|
{ }
|
||||||
|
|
||||||
internal sealed class RegionalPresetApplyRollbackException(Exception applyException, Exception rollbackException)
|
internal sealed class RegionalPresetApplyRollbackException(Exception applyException, Exception rollbackException)
|
||||||
: Exception("The regional preset failed and its configuration/runtime rollback also failed.",
|
: Exception("The regional preset failed and its configuration/runtime rollback also failed.",
|
||||||
new AggregateException(applyException, rollbackException)) { }
|
new AggregateException(applyException, rollbackException))
|
||||||
|
{ }
|
||||||
|
|
||||||
internal sealed record RegionalPresetApplyResult(bool Success, bool RollbackSucceeded, Exception? Error);
|
internal sealed record RegionalPresetApplyResult(bool Success, bool RollbackSucceeded, Exception? Error);
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,15 @@ public sealed class RuntimeOperationCoordinator
|
|||||||
private bool _stopping;
|
private bool _stopping;
|
||||||
|
|
||||||
public CancellationToken ShutdownToken => _shutdown.Token;
|
public CancellationToken ShutdownToken => _shutdown.Token;
|
||||||
|
|
||||||
public bool IsStopping
|
public bool IsStopping
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_sync) return _stopping;
|
lock (_sync)
|
||||||
|
{
|
||||||
|
return _stopping;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ internal static class RuntimeRequestOperationPolicy
|
|||||||
&& (normalizedPath is "/api/core/xray/update" or "/api/core/geo/update" or "/api/web-updates/update" or "/api/speedtests"
|
&& (normalizedPath is "/api/core/xray/update" or "/api/core/geo/update" or "/api/web-updates/update" or "/api/speedtests"
|
||||||
or "/api/subscriptions/update" or "/api/backup/restore" or "/api/backup/webdav/restore"
|
or "/api/subscriptions/update" or "/api/backup/restore" or "/api/backup/webdav/restore"
|
||||||
|| normalizedPath.StartsWith("/api/settings/regional-presets/", StringComparison.Ordinal)
|
|| normalizedPath.StartsWith("/api/settings/regional-presets/", StringComparison.Ordinal)
|
||||||
|| normalizedPath.StartsWith("/api/core-updates/", StringComparison.Ordinal)
|
|| (normalizedPath.StartsWith("/api/core-updates/", StringComparison.Ordinal)
|
||||||
&& normalizedPath.EndsWith("/update", StringComparison.Ordinal)
|
&& normalizedPath.EndsWith("/update", StringComparison.Ordinal))
|
||||||
|| (normalizedPath.StartsWith("/api/subscriptions/", StringComparison.Ordinal)
|
|| (normalizedPath.StartsWith("/api/subscriptions/", StringComparison.Ordinal)
|
||||||
&& normalizedPath.EndsWith("/update", StringComparison.Ordinal))))
|
&& normalizedPath.EndsWith("/update", StringComparison.Ordinal))))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ internal static class RuntimeShutdownBudgets
|
|||||||
// Runtime cleanup owns 20 seconds; the host retains 5 seconds, systemd is
|
// Runtime cleanup owns 20 seconds; the host retains 5 seconds, systemd is
|
||||||
// configured to 30 seconds, and the native --stop launcher waits 5 seconds more.
|
// configured to 30 seconds, and the native --stop launcher waits 5 seconds more.
|
||||||
public static readonly TimeSpan RuntimeCleanup = TimeSpan.FromSeconds(20);
|
public static readonly TimeSpan RuntimeCleanup = TimeSpan.FromSeconds(20);
|
||||||
|
|
||||||
public static readonly TimeSpan HostShutdown = TimeSpan.FromSeconds(25);
|
public static readonly TimeSpan HostShutdown = TimeSpan.FromSeconds(25);
|
||||||
public static readonly TimeSpan LauncherWait = TimeSpan.FromSeconds(35);
|
public static readonly TimeSpan LauncherWait = TimeSpan.FromSeconds(35);
|
||||||
|
|
||||||
// Used when the host shutdown budget was already exhausted before cleanup
|
// Used when the host shutdown budget was already exhausted before cleanup
|
||||||
// started, for example while the web server drained long-lived connections.
|
// started, for example while the web server drained long-lived connections.
|
||||||
public static readonly TimeSpan LateCleanup = TimeSpan.FromSeconds(5);
|
public static readonly TimeSpan LateCleanup = TimeSpan.FromSeconds(5);
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using SQLite;
|
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Helper;
|
using ServiceLib.Helper;
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Launcher;
|
using v2rayN.WebAPI.Launcher;
|
||||||
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
public sealed partial class V2rayRuntime
|
public sealed partial class V2rayRuntime
|
||||||
@@ -415,7 +410,10 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (File.Exists(temporary)) File.Delete(temporary);
|
if (File.Exists(temporary))
|
||||||
|
{
|
||||||
|
File.Delete(temporary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +425,11 @@ public sealed partial class V2rayRuntime
|
|||||||
RequeueAbandonedRestoreRuntimeClaim(path);
|
RequeueAbandonedRestoreRuntimeClaim(path);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(path)) return null;
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var json = await File.ReadAllTextAsync(path, cancellationToken);
|
var json = await File.ReadAllTextAsync(path, cancellationToken);
|
||||||
return JsonSerializer.Deserialize<RuntimeRestartIntent>(json);
|
return JsonSerializer.Deserialize<RuntimeRestartIntent>(json);
|
||||||
}
|
}
|
||||||
@@ -456,7 +458,11 @@ public sealed partial class V2rayRuntime
|
|||||||
// cleanup fails or the process crashes here, startup still has a recoverable
|
// cleanup fails or the process crashes here, startup still has a recoverable
|
||||||
// authoritative marker and can retry cleanup.
|
// authoritative marker and can retry cleanup.
|
||||||
DeleteRestoreRuntimeClaims(path, requireComplete: true);
|
DeleteRestoreRuntimeClaims(path, requireComplete: true);
|
||||||
if (File.Exists(path)) File.Delete(path);
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
throw new IOException("The committed restore runtime-state marker still exists.");
|
throw new IOException("The committed restore runtime-state marker still exists.");
|
||||||
@@ -473,7 +479,11 @@ public sealed partial class V2rayRuntime
|
|||||||
private static void RequeueAbandonedRestoreRuntimeClaim(string path)
|
private static void RequeueAbandonedRestoreRuntimeClaim(string path)
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(path);
|
var directory = Path.GetDirectoryName(path);
|
||||||
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return;
|
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pattern = Path.GetFileName(path) + ".consuming-*";
|
var pattern = Path.GetFileName(path) + ".consuming-*";
|
||||||
@@ -540,7 +550,11 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!File.Exists(path)) return null;
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return JsonSerializer.Deserialize<RuntimeRestartIntent>(File.ReadAllText(path));
|
return JsonSerializer.Deserialize<RuntimeRestartIntent>(File.ReadAllText(path));
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
|
||||||
@@ -562,7 +576,8 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
DateTime lastWriteTimeUtc;
|
DateTime lastWriteTimeUtc;
|
||||||
try { lastWriteTimeUtc = File.GetLastWriteTimeUtc(claimPath); }
|
try
|
||||||
|
{ lastWriteTimeUtc = File.GetLastWriteTimeUtc(claimPath); }
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
lastWriteTimeUtc = DateTime.MinValue;
|
lastWriteTimeUtc = DateTime.MinValue;
|
||||||
@@ -573,7 +588,11 @@ public sealed partial class V2rayRuntime
|
|||||||
private static void DeleteRestoreRuntimeClaims(string path, bool requireComplete = false)
|
private static void DeleteRestoreRuntimeClaims(string path, bool requireComplete = false)
|
||||||
{
|
{
|
||||||
var directory = Path.GetDirectoryName(path);
|
var directory = Path.GetDirectoryName(path);
|
||||||
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return;
|
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var pattern = Path.GetFileName(path) + ".consuming-*";
|
var pattern = Path.GetFileName(path) + ".consuming-*";
|
||||||
@@ -585,7 +604,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
if (requireComplete) throw;
|
if (requireComplete)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
Logging.SaveLog("Committed abandoned restore runtime-state claim could not be removed: " + exception.Message);
|
Logging.SaveLog("Committed abandoned restore runtime-state claim could not be removed: " + exception.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,7 +619,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
if (requireComplete) throw;
|
if (requireComplete)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
Logging.SaveLog("Committed abandoned restore runtime-state claims could not be removed: " + exception.Message);
|
Logging.SaveLog("Committed abandoned restore runtime-state claims could not be removed: " + exception.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,7 +637,10 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(path)) File.Delete(path);
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
|
|
||||||
using ServiceLib.Models.CoreConfigs;
|
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Resx;
|
|
||||||
using ServiceLib.Services;
|
using ServiceLib.Services;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
@@ -19,10 +10,12 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
private const string GeoFilesUpdateTarget = "GeoFiles";
|
private const string GeoFilesUpdateTarget = "GeoFiles";
|
||||||
private const string WebUpdateTarget = "v2rayN.WebAPI";
|
private const string WebUpdateTarget = "v2rayN.WebAPI";
|
||||||
|
|
||||||
internal static string NormalizeWebUpdateTarget(string target) => target == "v2rayN.Web" ? WebUpdateTarget : target;
|
internal static string NormalizeWebUpdateTarget(string target) => target == "v2rayN.Web" ? WebUpdateTarget : target;
|
||||||
|
|
||||||
internal static List<string>? NormalizeWebUpdateTargets(IEnumerable<string>? targets) =>
|
internal static List<string>? NormalizeWebUpdateTargets(IEnumerable<string>? targets) =>
|
||||||
targets?.Select(NormalizeWebUpdateTarget).Distinct(StringComparer.Ordinal).ToList();
|
targets?.Select(NormalizeWebUpdateTarget).Distinct(StringComparer.Ordinal).ToList();
|
||||||
|
|
||||||
private readonly object _updateTaskGate = new();
|
private readonly object _updateTaskGate = new();
|
||||||
private readonly GeoFilesUpdateGate _geoUpdateGate = new();
|
private readonly GeoFilesUpdateGate _geoUpdateGate = new();
|
||||||
private readonly ConcurrentDictionary<ECoreType, Task> _coreUpdateTasks = new();
|
private readonly ConcurrentDictionary<ECoreType, Task> _coreUpdateTasks = new();
|
||||||
@@ -31,7 +24,7 @@ public sealed partial class V2rayRuntime
|
|||||||
private Task? _coreUpdateBatchTask;
|
private Task? _coreUpdateBatchTask;
|
||||||
|
|
||||||
internal bool GetUpdatePreRelease(string target, bool? preRelease = null) =>
|
internal bool GetUpdatePreRelease(string target, bool? preRelease = null) =>
|
||||||
preRelease ?? (Config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(target, StringComparer.Ordinal) ?? false);
|
preRelease ?? Config.CheckUpdateItem.CheckPreReleaseCoreTypes?.Contains(target, StringComparer.Ordinal) ?? false;
|
||||||
|
|
||||||
public CoreUpdateSettingsView GetCoreUpdateSettings()
|
public CoreUpdateSettingsView GetCoreUpdateSettings()
|
||||||
{
|
{
|
||||||
@@ -236,8 +229,16 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
var targetNames = targets.Select(coreType => coreType.ToString()).ToList();
|
var targetNames = targets.Select(coreType => coreType.ToString()).ToList();
|
||||||
if (includeGeoFiles) targetNames.Add(GeoFilesUpdateTarget);
|
if (includeGeoFiles)
|
||||||
if (includeWeb) targetNames.Add(WebUpdateTarget);
|
{
|
||||||
|
targetNames.Add(GeoFilesUpdateTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeWeb)
|
||||||
|
{
|
||||||
|
targetNames.Add(WebUpdateTarget);
|
||||||
|
}
|
||||||
|
|
||||||
return OperationView.Ok(ApiMessageKeys.CoreUpdateBatchStarted, new { apply, targets = targetNames });
|
return OperationView.Ok(ApiMessageKeys.CoreUpdateBatchStarted, new { apply, targets = targetNames });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,7 +550,11 @@ public sealed partial class V2rayRuntime
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
CleanupWebUpdateStage(webStage);
|
CleanupWebUpdateStage(webStage);
|
||||||
foreach (var stage in staged) CleanupCoreUpdateStage(stage);
|
foreach (var stage in staged)
|
||||||
|
{
|
||||||
|
CleanupCoreUpdateStage(stage);
|
||||||
|
}
|
||||||
|
|
||||||
lock (_updateTaskGate)
|
lock (_updateTaskGate)
|
||||||
{
|
{
|
||||||
_coreUpdateBatchTask = null;
|
_coreUpdateBatchTask = null;
|
||||||
@@ -1268,7 +1273,11 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
private static void AddPrefixedItems(IEnumerable<string>? items, string prefix, ICollection<string> output)
|
private static void AddPrefixedItems(IEnumerable<string>? items, string prefix, ICollection<string> output)
|
||||||
{
|
{
|
||||||
if (items is null) return;
|
if (items is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (item.StartsWith(prefix, StringComparison.Ordinal))
|
if (item.StartsWith(prefix, StringComparison.Ordinal))
|
||||||
@@ -1280,7 +1289,11 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
private static void ExtractDnsRuleSets(string? dnsJson, ICollection<string> geoipRules, ICollection<string> geositeRules)
|
private static void ExtractDnsRuleSets(string? dnsJson, ICollection<string> geoipRules, ICollection<string> geositeRules)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(dnsJson)) return;
|
if (string.IsNullOrEmpty(dnsJson))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dns = JsonUtils.Deserialize<Dns4Sbox>(dnsJson);
|
var dns = JsonUtils.Deserialize<Dns4Sbox>(dnsJson);
|
||||||
@@ -1297,7 +1310,11 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
private static void ExtractSrsRuleSets(Rule4Sbox? rule, ICollection<string> geoipRules, ICollection<string> geositeRules)
|
private static void ExtractSrsRuleSets(Rule4Sbox? rule, ICollection<string> geoipRules, ICollection<string> geositeRules)
|
||||||
{
|
{
|
||||||
if (rule is null) return;
|
if (rule is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AddPrefixedItems(rule.rule_set, "geosite-", geositeRules);
|
AddPrefixedItems(rule.rule_set, "geosite-", geositeRules);
|
||||||
AddPrefixedItems(rule.rule_set, "geoip-", geoipRules);
|
AddPrefixedItems(rule.rule_set, "geoip-", geoipRules);
|
||||||
foreach (var nestedRule in rule.rules ?? [])
|
foreach (var nestedRule in rule.rules ?? [])
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler;
|
|
||||||
using ServiceLib.Handler.Builder;
|
using ServiceLib.Handler.Builder;
|
||||||
using ServiceLib.Handler.Fmt;
|
using ServiceLib.Handler.Fmt;
|
||||||
using ServiceLib.Manager;
|
|
||||||
|
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Services;
|
using ServiceLib.Services;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
@@ -437,16 +429,16 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
var (context, validation) = await CoreConfigContextBuilder.Build(Config, item);
|
var (context, validation) = await CoreConfigContextBuilder.Build(Config, item);
|
||||||
if (validation.Success)
|
if (validation.Success)
|
||||||
{
|
|
||||||
var generated = await CoreConfigHandler.GenerateClientConfig(context, null);
|
|
||||||
if (generated.Success && generated.Data is string configText)
|
|
||||||
{
|
{
|
||||||
output.Add(new(item.IndexId, item.Remarks, "client-config", configText));
|
var generated = await CoreConfigHandler.GenerateClientConfig(context, null);
|
||||||
|
if (generated.Success && generated.Data is string configText)
|
||||||
|
{
|
||||||
|
output.Add(new(item.IndexId, item.Remarks, "client-config", configText));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Services;
|
using ServiceLib.Services;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler;
|
|
||||||
using ServiceLib.Helper;
|
using ServiceLib.Helper;
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.CoreConfigs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Services;
|
using ServiceLib.Services;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
@@ -216,7 +208,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var coreFingerprintBefore = GetCoreConfigurationFingerprint();
|
var coreFingerprintBefore = GetCoreConfigurationFingerprint();
|
||||||
var oldConfig = applyContext.OldConfig;
|
var oldConfig = applyContext.OldConfig;
|
||||||
var statisticsChanged = Config.GuiItem.EnableStatistics != application.EnableStatistics
|
var statisticsChanged = Config.GuiItem.EnableStatistics != application.EnableStatistics
|
||||||
@@ -309,7 +305,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var fingerprint = GetCoreConfigurationFingerprint();
|
var fingerprint = GetCoreConfigurationFingerprint();
|
||||||
await _mutations.RunAsync(async () =>
|
await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -346,7 +346,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var fingerprint = GetCoreConfigurationFingerprint();
|
var fingerprint = GetCoreConfigurationFingerprint();
|
||||||
await _mutations.RunAsync(async () =>
|
await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -446,7 +450,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var fingerprint = GetCoreConfigurationFingerprint();
|
var fingerprint = GetCoreConfigurationFingerprint();
|
||||||
await _mutations.RunAsync(async () =>
|
await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -485,7 +493,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var fingerprint = GetCoreConfigurationFingerprint();
|
var fingerprint = GetCoreConfigurationFingerprint();
|
||||||
await _mutations.RunAsync(async () =>
|
await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -501,7 +513,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> UpdateSimpleDNSAsync(SimpleDNSItem input)
|
public async Task<OperationView> UpdateSimpleDNSAsync(SimpleDNSItem input)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var fingerprint = GetCoreConfigurationFingerprint();
|
var fingerprint = GetCoreConfigurationFingerprint();
|
||||||
await _mutations.RunAsync(async () =>
|
await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -527,7 +543,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _mutations.RunAsync(async () =>
|
var result = await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
var current = await AppManager.Instance.GetDNSItem(coreType);
|
var current = await AppManager.Instance.GetDNSItem(coreType);
|
||||||
@@ -582,7 +602,11 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
internal static bool IsValidCustomDns(ECoreType coreType, string? content)
|
internal static bool IsValidCustomDns(ECoreType coreType, string? content)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(content)) return true;
|
if (string.IsNullOrEmpty(content))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (coreType == ECoreType.sing_box)
|
if (coreType == ECoreType.sing_box)
|
||||||
{
|
{
|
||||||
var parsed = JsonUtils.Deserialize<Dns4Sbox>(content);
|
var parsed = JsonUtils.Deserialize<Dns4Sbox>(content);
|
||||||
@@ -618,7 +642,11 @@ public sealed partial class V2rayRuntime
|
|||||||
return OperationView.Fail("routing_rules_json_invalid", ApiMessageKeys.RoutingRulesInvalid);
|
return OperationView.Fail("routing_rules_json_invalid", ApiMessageKeys.RoutingRulesInvalid);
|
||||||
}
|
}
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var save = await _mutations.RunAsync(async () =>
|
var save = await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
RoutingItem item;
|
RoutingItem item;
|
||||||
@@ -636,7 +664,7 @@ public sealed partial class V2rayRuntime
|
|||||||
var existingItem = await AppManager.Instance.GetRoutingItem(routingId);
|
var existingItem = await AppManager.Instance.GetRoutingItem(routingId);
|
||||||
if (existingItem is null)
|
if (existingItem is null)
|
||||||
{
|
{
|
||||||
return (Result: -2, Item: (RoutingItem?)null, Changed: false);
|
return (Result: -2, Item: null, Changed: false);
|
||||||
}
|
}
|
||||||
item = existingItem;
|
item = existingItem;
|
||||||
changed = item.Remarks != input.Remarks
|
changed = item.Remarks != input.Remarks
|
||||||
@@ -688,7 +716,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> DeleteRoutingProfileAsync(string id)
|
public async Task<OperationView> DeleteRoutingProfileAsync(string id)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var removed = await _mutations.RunAsync(async () =>
|
var removed = await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
var item = await AppManager.Instance.GetRoutingItem(id);
|
var item = await AppManager.Instance.GetRoutingItem(id);
|
||||||
@@ -727,7 +759,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> ActivateRoutingProfileAsync(string id)
|
public async Task<OperationView> ActivateRoutingProfileAsync(string id)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _mutations.RunAsync(async () =>
|
var result = await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
var item = await AppManager.Instance.GetRoutingItem(id);
|
var item = await AppManager.Instance.GetRoutingItem(id);
|
||||||
@@ -759,7 +795,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> SaveRoutingRulesAsync(string routingId, IEnumerable<RulesItem> rules)
|
public async Task<OperationView> SaveRoutingRulesAsync(string routingId, IEnumerable<RulesItem> rules)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var items = rules.ToArray();
|
var items = rules.ToArray();
|
||||||
var before = await AppManager.Instance.GetRoutingItem(routingId);
|
var before = await AppManager.Instance.GetRoutingItem(routingId);
|
||||||
var changed = before is not null && before.IsActive
|
var changed = before is not null && before.IsActive
|
||||||
@@ -810,7 +850,11 @@ public sealed partial class V2rayRuntime
|
|||||||
return OperationView.Fail("routing_rules_json_invalid", ApiMessageKeys.RoutingRulesInvalid);
|
return OperationView.Fail("routing_rules_json_invalid", ApiMessageKeys.RoutingRulesInvalid);
|
||||||
}
|
}
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var activeBefore = await AppManager.Instance.GetRoutingItem(routingId);
|
var activeBefore = await AppManager.Instance.GetRoutingItem(routingId);
|
||||||
var beforeRuleSet = activeBefore?.RuleSet;
|
var beforeRuleSet = activeBefore?.RuleSet;
|
||||||
var saved = await _mutations.RunAsync(async () =>
|
var saved = await _mutations.RunAsync(async () =>
|
||||||
@@ -867,7 +911,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> MoveRoutingRuleAsync(string routingId, string ruleId, EMove direction, int position)
|
public async Task<OperationView> MoveRoutingRuleAsync(string routingId, string ruleId, EMove direction, int position)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var routeWasActive = (await AppManager.Instance.GetRoutingItem(routingId))?.IsActive == true;
|
var routeWasActive = (await AppManager.Instance.GetRoutingItem(routingId))?.IsActive == true;
|
||||||
var result = await _mutations.RunAsync(async () =>
|
var result = await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -904,7 +952,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> DeleteRoutingRuleAsync(string routingId, string ruleId)
|
public async Task<OperationView> DeleteRoutingRuleAsync(string routingId, string ruleId)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var routeWasActive = (await AppManager.Instance.GetRoutingItem(routingId))?.IsActive == true;
|
var routeWasActive = (await AppManager.Instance.GetRoutingItem(routingId))?.IsActive == true;
|
||||||
var result = await _mutations.RunAsync(async () =>
|
var result = await _mutations.RunAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -935,7 +987,11 @@ public sealed partial class V2rayRuntime
|
|||||||
public async Task<OperationView> ImportRoutingProfilesAsync()
|
public async Task<OperationView> ImportRoutingProfilesAsync()
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var before = await GetActiveRoutingSignatureAsync();
|
var before = await GetActiveRoutingSignatureAsync();
|
||||||
var result = await _mutations.RunAsync(() => ConfigDbManager.InitRouting(Config, true));
|
var result = await _mutations.RunAsync(() => ConfigDbManager.InitRouting(Config, true));
|
||||||
if (result != 0)
|
if (result != 0)
|
||||||
@@ -958,21 +1014,25 @@ public sealed partial class V2rayRuntime
|
|||||||
FullConfigTemplateItem current,
|
FullConfigTemplateItem current,
|
||||||
ECoreType coreType,
|
ECoreType coreType,
|
||||||
CoreConfigTemplateInput input) => new()
|
CoreConfigTemplateInput input) => new()
|
||||||
{
|
{
|
||||||
Id = current.Id,
|
Id = current.Id,
|
||||||
Remarks = input.Remarks ?? current.Remarks,
|
Remarks = input.Remarks ?? current.Remarks,
|
||||||
Enabled = input.Enabled,
|
Enabled = input.Enabled,
|
||||||
CoreType = coreType,
|
CoreType = coreType,
|
||||||
Config = input.Config,
|
Config = input.Config,
|
||||||
TunConfig = input.TunConfig,
|
TunConfig = input.TunConfig,
|
||||||
AddProxyOnly = input.AddProxyOnly,
|
AddProxyOnly = input.AddProxyOnly,
|
||||||
ProxyDetour = input.ProxyDetour,
|
ProxyDetour = input.ProxyDetour,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task<OperationView> SaveFullConfigTemplateAsync(ECoreType coreType, CoreConfigTemplateInput input)
|
public async Task<OperationView> SaveFullConfigTemplateAsync(ECoreType coreType, CoreConfigTemplateInput input)
|
||||||
{
|
{
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
await using var applyContext = await BeginCoreSettingsApplyAsync();
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
var existingBefore = await AppManager.Instance.GetFullConfigTemplateItem(coreType);
|
var existingBefore = await AppManager.Instance.GetFullConfigTemplateItem(coreType);
|
||||||
var changed = existingBefore is not null
|
var changed = existingBefore is not null
|
||||||
&& (existingBefore.Remarks != (input.Remarks ?? existingBefore.Remarks)
|
&& (existingBefore.Remarks != (input.Remarks ?? existingBefore.Remarks)
|
||||||
@@ -1073,7 +1133,10 @@ public sealed partial class V2rayRuntime
|
|||||||
allowReadOnlyObservations: true);
|
allowReadOnlyObservations: true);
|
||||||
var operationToken = maintenance.Token;
|
var operationToken = maintenance.Token;
|
||||||
await using var applyContext = await BeginCoreSettingsApplyAsync(operationToken);
|
await using var applyContext = await BeginCoreSettingsApplyAsync(operationToken);
|
||||||
if (applyContext.Failure is { } applyFailure) return applyFailure;
|
if (applyContext.Failure is { } applyFailure)
|
||||||
|
{
|
||||||
|
return applyFailure;
|
||||||
|
}
|
||||||
|
|
||||||
RegionalPresetApplyResult? applyResult = null;
|
RegionalPresetApplyResult? applyResult = null;
|
||||||
OperationView? coreApplyResult = null;
|
OperationView? coreApplyResult = null;
|
||||||
@@ -1194,7 +1257,6 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
throw new RegionalPresetConfigApplyException();
|
throw new RegionalPresetConfigApplyException();
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
updateGeoFilesTransaction: (beforeCommit, token) => ApplyGeoFilesUpdateAsync(
|
updateGeoFilesTransaction: (beforeCommit, token) => ApplyGeoFilesUpdateAsync(
|
||||||
Config.CheckUpdateItem.UpdateViaProxy,
|
Config.CheckUpdateItem.UpdateViaProxy,
|
||||||
@@ -1569,13 +1631,21 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
await SQLiteHelper.Instance.DeleteAllAsync<RoutingItem>();
|
await SQLiteHelper.Instance.DeleteAllAsync<RoutingItem>();
|
||||||
if (await SQLiteHelper.Instance.InsertAllAsync(context.OldRoutingItems) != context.OldRoutingItems.Length)
|
if (await SQLiteHelper.Instance.InsertAllAsync(context.OldRoutingItems) != context.OldRoutingItems.Length)
|
||||||
|
{
|
||||||
throw new IOException("Routing profiles could not be restored from the settings snapshot.");
|
throw new IOException("Routing profiles could not be restored from the settings snapshot.");
|
||||||
|
}
|
||||||
|
|
||||||
await SQLiteHelper.Instance.DeleteAllAsync<DNSItem>();
|
await SQLiteHelper.Instance.DeleteAllAsync<DNSItem>();
|
||||||
if (await SQLiteHelper.Instance.InsertAllAsync(context.OldDnsItems) != context.OldDnsItems.Length)
|
if (await SQLiteHelper.Instance.InsertAllAsync(context.OldDnsItems) != context.OldDnsItems.Length)
|
||||||
|
{
|
||||||
throw new IOException("DNS profiles could not be restored from the settings snapshot.");
|
throw new IOException("DNS profiles could not be restored from the settings snapshot.");
|
||||||
|
}
|
||||||
|
|
||||||
await SQLiteHelper.Instance.DeleteAllAsync<FullConfigTemplateItem>();
|
await SQLiteHelper.Instance.DeleteAllAsync<FullConfigTemplateItem>();
|
||||||
if (await SQLiteHelper.Instance.InsertAllAsync(context.OldFullConfigTemplates) != context.OldFullConfigTemplates.Length)
|
if (await SQLiteHelper.Instance.InsertAllAsync(context.OldFullConfigTemplates) != context.OldFullConfigTemplates.Length)
|
||||||
|
{
|
||||||
throw new IOException("Core templates could not be restored from the settings snapshot.");
|
throw new IOException("Core templates could not be restored from the settings snapshot.");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
AppManager.Instance.Reset();
|
AppManager.Instance.Reset();
|
||||||
|
|
||||||
@@ -1781,12 +1851,17 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _disposed, 1) == 0) gate.Release();
|
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||||
|
{
|
||||||
|
gate.Release();
|
||||||
|
}
|
||||||
|
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record CoreSettingsRollbackResult(bool RolledBack, bool OldRuntimeRestored, bool ConfigRestored, string? Detail);
|
private sealed record CoreSettingsRollbackResult(bool RolledBack, bool OldRuntimeRestored, bool ConfigRestored, string? Detail);
|
||||||
|
|
||||||
private sealed class RegionalPresetConfigApplyException : Exception { }
|
private sealed class RegionalPresetConfigApplyException : Exception
|
||||||
|
{ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
using ServiceLib.Services;
|
using ServiceLib.Services;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Launcher;
|
using v2rayN.WebAPI.Launcher;
|
||||||
@@ -78,7 +73,10 @@ public sealed partial class V2rayRuntime
|
|||||||
?? ApiMessageKeys.CoreUpdateUnsupported);
|
?? ApiMessageKeys.CoreUpdateUnsupported);
|
||||||
}
|
}
|
||||||
if (GetWebUpdateRuntimeInstallReason() is { } runtimeReason)
|
if (GetWebUpdateRuntimeInstallReason() is { } runtimeReason)
|
||||||
|
{
|
||||||
return OperationView.Fail("web_update_runtime_unavailable", runtimeReason);
|
return OperationView.Fail("web_update_runtime_unavailable", runtimeReason);
|
||||||
|
}
|
||||||
|
|
||||||
if (Config.CheckUpdateItem.SelectedCoreTypes is { } selected
|
if (Config.CheckUpdateItem.SelectedCoreTypes is { } selected
|
||||||
&& !selected.Contains(WebUpdateTarget, StringComparer.Ordinal))
|
&& !selected.Contains(WebUpdateTarget, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -101,13 +99,25 @@ public sealed partial class V2rayRuntime
|
|||||||
internal CoreUpdateProgressView? GetWebUpdateProgress()
|
internal CoreUpdateProgressView? GetWebUpdateProgress()
|
||||||
{
|
{
|
||||||
var inMemory = _updateProgress.GetValueOrDefault(WebUpdateTarget);
|
var inMemory = _updateProgress.GetValueOrDefault(WebUpdateTarget);
|
||||||
if (inMemory is { IsComplete: false } || inMemory?.Phase == "completed") return inMemory;
|
if (inMemory is { IsComplete: false } || inMemory?.Phase == "completed")
|
||||||
|
{
|
||||||
|
return inMemory;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var path = Utils.GetTempPath(WebUpdateProgressFile);
|
var path = Utils.GetTempPath(WebUpdateProgressFile);
|
||||||
if (!File.Exists(path)) return inMemory;
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return inMemory;
|
||||||
|
}
|
||||||
|
|
||||||
var persisted = JsonSerializer.Deserialize<WebUpdateProgressState>(File.ReadAllText(path), JsonOptions);
|
var persisted = JsonSerializer.Deserialize<WebUpdateProgressState>(File.ReadAllText(path), JsonOptions);
|
||||||
if (persisted is null) return inMemory;
|
if (persisted is null)
|
||||||
|
{
|
||||||
|
return inMemory;
|
||||||
|
}
|
||||||
|
|
||||||
var progress = new CoreUpdateProgressView(
|
var progress = new CoreUpdateProgressView(
|
||||||
WebUpdateTarget,
|
WebUpdateTarget,
|
||||||
persisted.Phase,
|
persisted.Phase,
|
||||||
@@ -120,7 +130,11 @@ public sealed partial class V2rayRuntime
|
|||||||
persisted.RollbackSucceeded);
|
persisted.RollbackSucceeded);
|
||||||
// In-flight persisted progress belongs to the detached helper. Caching
|
// In-flight persisted progress belongs to the detached helper. Caching
|
||||||
// it as local progress would prevent subsequent reads of its result.
|
// it as local progress would prevent subsequent reads of its result.
|
||||||
if (progress.IsComplete) _updateProgress[WebUpdateTarget] = progress;
|
if (progress.IsComplete)
|
||||||
|
{
|
||||||
|
_updateProgress[WebUpdateTarget] = progress;
|
||||||
|
}
|
||||||
|
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or JsonException)
|
||||||
@@ -167,7 +181,10 @@ public sealed partial class V2rayRuntime
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
CleanupWebUpdateStage(stage);
|
CleanupWebUpdateStage(stage);
|
||||||
lock (_updateTaskGate) _webUpdateTask = null;
|
lock (_updateTaskGate)
|
||||||
|
{
|
||||||
|
_webUpdateTask = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,25 +201,37 @@ public sealed partial class V2rayRuntime
|
|||||||
WebReleaseChannel.BuildReleaseIndexUrl(repository),
|
WebReleaseChannel.BuildReleaseIndexUrl(repository),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (string.IsNullOrWhiteSpace(releaseJson) || releaseJson.Length > 8 * 1024 * 1024)
|
if (string.IsNullOrWhiteSpace(releaseJson) || releaseJson.Length > 8 * 1024 * 1024)
|
||||||
|
{
|
||||||
return WebUpdateReleaseCheck.Failed("The GitHub Web release index could not be downloaded or exceeded its size limit.");
|
return WebUpdateReleaseCheck.Failed("The GitHub Web release index could not be downloaded or exceeded its size limit.");
|
||||||
|
}
|
||||||
|
|
||||||
var candidates = ParseWebReleaseCandidates(releaseJson, allowPrerelease);
|
var candidates = ParseWebReleaseCandidates(releaseJson, allowPrerelease);
|
||||||
if (candidates.Count == 0)
|
if (candidates.Count == 0)
|
||||||
|
{
|
||||||
return WebUpdateReleaseCheck.Failed("No v2rayN releases with Web update packages were found for this channel.");
|
return WebUpdateReleaseCheck.Failed("No v2rayN releases with Web update packages were found for this channel.");
|
||||||
|
}
|
||||||
|
|
||||||
candidates.Sort((left, right) => new ServiceLib.Models.Dto.SemanticVersion(right.Version)
|
candidates.Sort((left, right) => new ServiceLib.Models.Dto.SemanticVersion(right.Version)
|
||||||
.CompareTo(new ServiceLib.Models.Dto.SemanticVersion(left.Version)));
|
.CompareTo(new ServiceLib.Models.Dto.SemanticVersion(left.Version)));
|
||||||
var selected = candidates[0];
|
var selected = candidates[0];
|
||||||
if (!WebReleaseChannel.IsTrustedAssetUrl(selected.ManifestUrl, repository, selected.Tag, WebReleaseChannel.ManifestAssetName))
|
if (!WebReleaseChannel.IsTrustedAssetUrl(selected.ManifestUrl, repository, selected.Tag, WebReleaseChannel.ManifestAssetName))
|
||||||
|
{
|
||||||
return WebUpdateReleaseCheck.Failed("The Web release manifest URL did not match the trusted release channel.");
|
return WebUpdateReleaseCheck.Failed("The Web release manifest URL did not match the trusted release channel.");
|
||||||
|
}
|
||||||
|
|
||||||
var manifestJson = await downloader.TryDownloadString(
|
var manifestJson = await downloader.TryDownloadString(
|
||||||
selected.ManifestUrl,
|
selected.ManifestUrl,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
if (string.IsNullOrWhiteSpace(manifestJson) || manifestJson.Length > 1024 * 1024)
|
if (string.IsNullOrWhiteSpace(manifestJson) || manifestJson.Length > 1024 * 1024)
|
||||||
|
{
|
||||||
return WebUpdateReleaseCheck.Failed("The Web release manifest could not be downloaded or exceeded its size limit.");
|
return WebUpdateReleaseCheck.Failed("The Web release manifest could not be downloaded or exceeded its size limit.");
|
||||||
|
}
|
||||||
|
|
||||||
var manifest = WebUpdatePackageStager.ParseManifest(manifestJson);
|
var manifest = WebUpdatePackageStager.ParseManifest(manifestJson);
|
||||||
if (manifest.Version != selected.Version)
|
if (manifest.Version != selected.Version)
|
||||||
|
{
|
||||||
return WebUpdateReleaseCheck.Failed("The Web release manifest version does not match its release tag.");
|
return WebUpdateReleaseCheck.Failed("The Web release manifest version does not match its release tag.");
|
||||||
|
}
|
||||||
|
|
||||||
var currentRid = WebBuildIdentity.Current.Rid;
|
var currentRid = WebBuildIdentity.Current.Rid;
|
||||||
var package = manifest.Packages.FirstOrDefault(item => item.Rid == currentRid);
|
var package = manifest.Packages.FirstOrDefault(item => item.Rid == currentRid);
|
||||||
@@ -252,16 +281,30 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
using var document = JsonDocument.Parse(releaseJson);
|
using var document = JsonDocument.Parse(releaseJson);
|
||||||
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The GitHub Web release index has an invalid format.");
|
throw new InvalidDataException("The GitHub Web release index has an invalid format.");
|
||||||
|
}
|
||||||
|
|
||||||
var candidates = new List<WebReleaseCandidate>();
|
var candidates = new List<WebReleaseCandidate>();
|
||||||
foreach (var release in document.RootElement.EnumerateArray())
|
foreach (var release in document.RootElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (release.TryGetProperty("draft", out var draft) && draft.GetBoolean()) continue;
|
if (release.TryGetProperty("draft", out var draft) && draft.GetBoolean())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var isPrerelease = release.TryGetProperty("prerelease", out var prerelease) && prerelease.GetBoolean();
|
var isPrerelease = release.TryGetProperty("prerelease", out var prerelease) && prerelease.GetBoolean();
|
||||||
if (!WebUpdatePackageStager.ShouldConsiderRelease(isPrerelease, allowPrerelease)) continue;
|
if (!WebUpdatePackageStager.ShouldConsiderRelease(isPrerelease, allowPrerelease))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var tag = release.TryGetProperty("tag_name", out var tagElement) ? tagElement.GetString() : null;
|
var tag = release.TryGetProperty("tag_name", out var tagElement) ? tagElement.GetString() : null;
|
||||||
if (!WebReleaseChannel.TryParseReleaseTag(tag, out var version)) continue;
|
if (!WebReleaseChannel.TryParseReleaseTag(tag, out var version))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var assets = new Dictionary<string, (string Url, long Size)>(StringComparer.Ordinal);
|
var assets = new Dictionary<string, (string Url, long Size)>(StringComparer.Ordinal);
|
||||||
if (release.TryGetProperty("assets", out var assetsElement) && assetsElement.ValueKind == JsonValueKind.Array)
|
if (release.TryGetProperty("assets", out var assetsElement) && assetsElement.ValueKind == JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
@@ -271,11 +314,16 @@ public sealed partial class V2rayRuntime
|
|||||||
var url = asset.TryGetProperty("browser_download_url", out var urlElement) ? urlElement.GetString() : null;
|
var url = asset.TryGetProperty("browser_download_url", out var urlElement) ? urlElement.GetString() : null;
|
||||||
var size = asset.TryGetProperty("size", out var sizeElement) && sizeElement.TryGetInt64(out var parsedSize)
|
var size = asset.TryGetProperty("size", out var sizeElement) && sizeElement.TryGetInt64(out var parsedSize)
|
||||||
? parsedSize : 0;
|
? parsedSize : 0;
|
||||||
if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url)) assets[name] = (url, size);
|
if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
assets[name] = (url, size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (assets.TryGetValue(WebReleaseChannel.ManifestAssetName, out var manifestAsset))
|
if (assets.TryGetValue(WebReleaseChannel.ManifestAssetName, out var manifestAsset))
|
||||||
|
{
|
||||||
candidates.Add(new WebReleaseCandidate(tag!, version, isPrerelease, manifestAsset.Url, assets));
|
candidates.Add(new WebReleaseCandidate(tag!, version, isPrerelease, manifestAsset.Url, assets));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
@@ -287,9 +335,15 @@ public sealed partial class V2rayRuntime
|
|||||||
bool batch)
|
bool batch)
|
||||||
{
|
{
|
||||||
if (check.Manifest is null || check.Package is null)
|
if (check.Manifest is null || check.Package is null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The v2rayN.WebAPI update manifest or runtime package was not checked.");
|
throw new InvalidDataException("The v2rayN.WebAPI update manifest or runtime package was not checked.");
|
||||||
|
}
|
||||||
|
|
||||||
var deployment = GetWebUpdateDeployment();
|
var deployment = GetWebUpdateDeployment();
|
||||||
if (!deployment.CanInstall) throw new InvalidOperationException(deployment.InstallReasonKey ?? "Web update installation is unavailable.");
|
if (!deployment.CanInstall)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(deployment.InstallReasonKey ?? "Web update installation is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
var id = Guid.NewGuid().ToString("N");
|
var id = Guid.NewGuid().ToString("N");
|
||||||
var archivePath = Utils.GetTempPath($"web-update-{id}.zip");
|
var archivePath = Utils.GetTempPath($"web-update-{id}.zip");
|
||||||
@@ -318,14 +372,22 @@ public sealed partial class V2rayRuntime
|
|||||||
DisplayFileName = Path.GetFileName(archivePath),
|
DisplayFileName = Path.GetFileName(archivePath),
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
if (downloadFailure.Task.IsCompletedSuccessfully)
|
if (downloadFailure.Task.IsCompletedSuccessfully)
|
||||||
|
{
|
||||||
throw new IOException("The v2rayN.WebAPI package download failed.", await downloadFailure.Task);
|
throw new IOException("The v2rayN.WebAPI package download failed.", await downloadFailure.Task);
|
||||||
if (!File.Exists(archivePath)) throw new IOException("The v2rayN.WebAPI package download did not produce an archive.");
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(archivePath))
|
||||||
|
{
|
||||||
|
throw new IOException("The v2rayN.WebAPI package download did not produce an archive.");
|
||||||
|
}
|
||||||
|
|
||||||
PublishWebUpdateProgress("verifying", false, false, null, check.Manifest.Version, batch);
|
PublishWebUpdateProgress("verifying", false, false, null, check.Manifest.Version, batch);
|
||||||
var identity = await WebUpdatePackageStager.VerifyAndExtractAsync(
|
var identity = await WebUpdatePackageStager.VerifyAndExtractAsync(
|
||||||
archivePath, packageDirectory, check.Manifest, check.Package, cancellationToken);
|
archivePath, packageDirectory, check.Manifest, check.Package, cancellationToken);
|
||||||
if (identity.Rid != WebBuildIdentity.Current.Rid)
|
if (identity.Rid != WebBuildIdentity.Current.Rid)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The staged v2rayN.WebAPI package has the wrong runtime identifier.");
|
throw new InvalidDataException("The staged v2rayN.WebAPI package has the wrong runtime identifier.");
|
||||||
|
}
|
||||||
|
|
||||||
PublishWebUpdateProgress("staged", false, false, "The app-only package passed SHA-256, RID, identity, and archive checks.",
|
PublishWebUpdateProgress("staged", false, false, "The app-only package passed SHA-256, RID, identity, and archive checks.",
|
||||||
check.Manifest.Version, batch);
|
check.Manifest.Version, batch);
|
||||||
@@ -426,7 +488,11 @@ public sealed partial class V2rayRuntime
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
if (isolatedWindowsHelper is not null) TryDeleteFile(isolatedWindowsHelper);
|
if (isolatedWindowsHelper is not null)
|
||||||
|
{
|
||||||
|
TryDeleteFile(isolatedWindowsHelper);
|
||||||
|
}
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
using (helper)
|
using (helper)
|
||||||
@@ -488,12 +554,21 @@ public sealed partial class V2rayRuntime
|
|||||||
var snapshot = CurrentCoreRuntime;
|
var snapshot = CurrentCoreRuntime;
|
||||||
var activeChild = HasTrackedCoreProcesses;
|
var activeChild = HasTrackedCoreProcesses;
|
||||||
if (snapshot.State is CoreRuntimeState.Starting or CoreRuntimeState.Stopping or CoreRuntimeState.Restarting)
|
if (snapshot.State is CoreRuntimeState.Starting or CoreRuntimeState.Stopping or CoreRuntimeState.Restarting)
|
||||||
|
{
|
||||||
return ApiMessageKeys.CoreRuntimeBusy;
|
return ApiMessageKeys.CoreRuntimeBusy;
|
||||||
|
}
|
||||||
|
|
||||||
if (snapshot.State == CoreRuntimeState.Faulted && activeChild)
|
if (snapshot.State == CoreRuntimeState.Faulted && activeChild)
|
||||||
|
{
|
||||||
return "maintenance.webUpdateRuntimeFaulted";
|
return "maintenance.webUpdateRuntimeFaulted";
|
||||||
if (snapshot.State == CoreRuntimeState.Running && !activeChild
|
}
|
||||||
|| snapshot.State == CoreRuntimeState.Stopped && activeChild)
|
|
||||||
|
if ((snapshot.State == CoreRuntimeState.Running && !activeChild)
|
||||||
|
|| (snapshot.State == CoreRuntimeState.Stopped && activeChild))
|
||||||
|
{
|
||||||
return "maintenance.webUpdateRuntimeFaulted";
|
return "maintenance.webUpdateRuntimeFaulted";
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,9 +578,13 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
if (new DirectoryInfo(installDirectory).LinkTarget is not null
|
if (new DirectoryInfo(installDirectory).LinkTarget is not null
|
||||||
|| new FileInfo(executablePath).LinkTarget is not null)
|
|| new FileInfo(executablePath).LinkTarget is not null)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var probe = Path.Combine(installDirectory, $".v2rayn-WebAPI-write-probe-{Guid.NewGuid():N}");
|
var probe = Path.Combine(installDirectory, $".v2rayn-WebAPI-write-probe-{Guid.NewGuid():N}");
|
||||||
using (new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { }
|
using (new FileStream(probe, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||||
|
{ }
|
||||||
File.Delete(probe);
|
File.Delete(probe);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -567,7 +646,11 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
private static void CleanupWebUpdateStage(WebUpdateStage? stage)
|
private static void CleanupWebUpdateStage(WebUpdateStage? stage)
|
||||||
{
|
{
|
||||||
if (stage is null) return;
|
if (stage is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CleanupWebUpdateStageFiles(stage.ArchivePath, stage.PackageDirectory,
|
CleanupWebUpdateStageFiles(stage.ArchivePath, stage.PackageDirectory,
|
||||||
stage.TransferredToHelper ? null : stage.CandidateDirectory);
|
stage.TransferredToHelper ? null : stage.CandidateDirectory);
|
||||||
}
|
}
|
||||||
@@ -576,7 +659,10 @@ public sealed partial class V2rayRuntime
|
|||||||
{
|
{
|
||||||
TryDeleteFile(archive);
|
TryDeleteFile(archive);
|
||||||
TryDeleteDirectory(packageDirectory);
|
TryDeleteDirectory(packageDirectory);
|
||||||
if (candidateDirectory is not null) TryDeleteDirectory(candidateDirectory);
|
if (candidateDirectory is not null)
|
||||||
|
{
|
||||||
|
TryDeleteDirectory(candidateDirectory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PublishWebUpdateProgress(
|
private void PublishWebUpdateProgress(
|
||||||
@@ -607,13 +693,25 @@ public sealed partial class V2rayRuntime
|
|||||||
|
|
||||||
private static void TryDeleteFile(string path)
|
private static void TryDeleteFile(string path)
|
||||||
{
|
{
|
||||||
try { if (File.Exists(path)) File.Delete(path); }
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { }
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryDeleteDirectory(string path)
|
private static void TryDeleteDirectory(string path)
|
||||||
{
|
{
|
||||||
try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); }
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(path))
|
||||||
|
{
|
||||||
|
Directory.Delete(path, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { }
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
using System.Net.Sockets;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.NetworkInformation;
|
using System.Net.NetworkInformation;
|
||||||
using ServiceLib;
|
using System.Net.Sockets;
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler;
|
|
||||||
using ServiceLib.Handler.Builder;
|
|
||||||
using ServiceLib.Helper;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Services;
|
|
||||||
using NLog;
|
using NLog;
|
||||||
using NLog.Config;
|
using NLog.Config;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
|
using ServiceLib.Handler.Builder;
|
||||||
|
using ServiceLib.Helper;
|
||||||
|
using ServiceLib.Services;
|
||||||
using v2rayN.WebAPI.Adapters;
|
using v2rayN.WebAPI.Adapters;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
public sealed partial class V2rayRuntime(
|
public sealed partial class V2rayRuntime(
|
||||||
@@ -102,11 +94,16 @@ public sealed partial class V2rayRuntime(
|
|||||||
var task = _coreMonitorTask;
|
var task = _coreMonitorTask;
|
||||||
_coreMonitorCancellation = null;
|
_coreMonitorCancellation = null;
|
||||||
_coreMonitorTask = null;
|
_coreMonitorTask = null;
|
||||||
if (cancellation is null) return;
|
if (cancellation is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cancellation.Cancel();
|
cancellation.Cancel();
|
||||||
if (task is not null)
|
if (task is not null)
|
||||||
{
|
{
|
||||||
try { await task.WaitAsync(TimeSpan.FromSeconds(2)); }
|
try
|
||||||
|
{ await task.WaitAsync(TimeSpan.FromSeconds(2)); }
|
||||||
catch (OperationCanceledException) { }
|
catch (OperationCanceledException) { }
|
||||||
catch (TimeoutException) { AddLog("core", "Core runtime monitor did not stop within its cleanup budget."); }
|
catch (TimeoutException) { AddLog("core", "Core runtime monitor did not stop within its cleanup budget."); }
|
||||||
}
|
}
|
||||||
@@ -714,7 +711,6 @@ public sealed partial class V2rayRuntime(
|
|||||||
sniffingEnabled,
|
sniffingEnabled,
|
||||||
sniffingTypes,
|
sniffingTypes,
|
||||||
WebApiCapabilities.Current);
|
WebApiCapabilities.Current);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OperationView> SelectProfileAsync(string profileId, CancellationToken cancellationToken)
|
public async Task<OperationView> SelectProfileAsync(string profileId, CancellationToken cancellationToken)
|
||||||
@@ -828,7 +824,7 @@ public sealed partial class V2rayRuntime(
|
|||||||
|
|
||||||
if (preflight.Success)
|
if (preflight.Success)
|
||||||
{
|
{
|
||||||
RuntimeListenerSnapshot[] currentlyOwnedListeners = HasTrackedCoreProcesses
|
var currentlyOwnedListeners = HasTrackedCoreProcesses
|
||||||
? CurrentCoreRuntime.Listeners
|
? CurrentCoreRuntime.Listeners
|
||||||
: [];
|
: [];
|
||||||
foreach (var listener in GetConfiguredListenerSnapshots())
|
foreach (var listener in GetConfiguredListenerSnapshots())
|
||||||
@@ -1052,59 +1048,59 @@ public sealed partial class V2rayRuntime(
|
|||||||
private async Task<OperationView> RestartCoreLockedAsync(CancellationToken cancellationToken)
|
private async Task<OperationView> RestartCoreLockedAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var previous = CurrentCoreRuntime;
|
var previous = CurrentCoreRuntime;
|
||||||
if (previous.State is not (CoreRuntimeState.Running or CoreRuntimeState.Faulted)
|
if (previous.State is not (CoreRuntimeState.Running or CoreRuntimeState.Faulted)
|
||||||
|| string.IsNullOrEmpty(previous.ProfileId))
|
|| string.IsNullOrEmpty(previous.ProfileId))
|
||||||
{
|
{
|
||||||
return OperationView.Fail("core_not_running", ApiMessageKeys.CoreNotRunning,
|
return OperationView.Fail("core_not_running", ApiMessageKeys.CoreNotRunning,
|
||||||
new { runtimeState = previous.State.ToString(), processIds = GetActiveCoreProcessIds() });
|
new { runtimeState = previous.State.ToString(), processIds = GetActiveCoreProcessIds() });
|
||||||
}
|
}
|
||||||
|
|
||||||
var profile = await AppManager.Instance.GetProfileItem(previous.ProfileId);
|
var profile = await AppManager.Instance.GetProfileItem(previous.ProfileId);
|
||||||
if (profile is null)
|
if (profile is null)
|
||||||
{
|
{
|
||||||
return OperationView.Fail("profile_not_found", ApiMessageKeys.ProfileNotFound,
|
return OperationView.Fail("profile_not_found", ApiMessageKeys.ProfileNotFound,
|
||||||
new { profileId = previous.ProfileId });
|
new { profileId = previous.ProfileId });
|
||||||
}
|
}
|
||||||
|
|
||||||
AppManager.Instance.Reset();
|
AppManager.Instance.Reset();
|
||||||
var restartStage = "preflight";
|
var restartStage = "preflight";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await CoreRestartFlow.ExecuteAsync(
|
return await CoreRestartFlow.ExecuteAsync(
|
||||||
() => BuildAndValidateCoreLaunchAsync(profile),
|
() => BuildAndValidateCoreLaunchAsync(profile),
|
||||||
async () =>
|
async () =>
|
||||||
{
|
|
||||||
restartStage = "stop";
|
|
||||||
SetCoreRuntime(previous with { State = CoreRuntimeState.Restarting, LastFailure = null });
|
|
||||||
await StopCoreMonitorAsync();
|
|
||||||
await StopCoreAndConfirmAsync(cancellationToken);
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
SetCoreRuntime(CoreRuntimeSnapshot.Stopped);
|
|
||||||
restartStage = "start";
|
|
||||||
},
|
|
||||||
preflight => LaunchPreflightedCoreLockedAsync(preflight, cancellationToken));
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
var processIds = GetActiveCoreProcessIds();
|
|
||||||
if (restartStage != "preflight")
|
|
||||||
{
|
{
|
||||||
SetCoreRuntime(CurrentCoreRuntime with
|
restartStage = "stop";
|
||||||
{
|
SetCoreRuntime(previous with { State = CoreRuntimeState.Restarting, LastFailure = null });
|
||||||
State = CoreRuntimeState.Faulted,
|
await StopCoreMonitorAsync();
|
||||||
ProcessIds = processIds,
|
await StopCoreAndConfirmAsync(cancellationToken);
|
||||||
LastFailure = exception.Message,
|
},
|
||||||
});
|
() =>
|
||||||
}
|
{
|
||||||
AddLog("core", $"Core restart failed during {restartStage}: {exception}");
|
SetCoreRuntime(CoreRuntimeSnapshot.Stopped);
|
||||||
return restartStage == "stop"
|
restartStage = "start";
|
||||||
? OperationView.Fail("core_stop_failed", ApiMessageKeys.CoreStopFailed,
|
},
|
||||||
new { detail = exception.Message, processIds })
|
preflight => LaunchPreflightedCoreLockedAsync(preflight, cancellationToken));
|
||||||
: OperationView.Fail("core_restart_failed", ApiMessageKeys.CoreStartFailed,
|
}
|
||||||
new { stage = restartStage, detail = exception.Message, processIds });
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
var processIds = GetActiveCoreProcessIds();
|
||||||
|
if (restartStage != "preflight")
|
||||||
|
{
|
||||||
|
SetCoreRuntime(CurrentCoreRuntime with
|
||||||
|
{
|
||||||
|
State = CoreRuntimeState.Faulted,
|
||||||
|
ProcessIds = processIds,
|
||||||
|
LastFailure = exception.Message,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
AddLog("core", $"Core restart failed during {restartStage}: {exception}");
|
||||||
|
return restartStage == "stop"
|
||||||
|
? OperationView.Fail("core_stop_failed", ApiMessageKeys.CoreStopFailed,
|
||||||
|
new { detail = exception.Message, processIds })
|
||||||
|
: OperationView.Fail("core_restart_failed", ApiMessageKeys.CoreStartFailed,
|
||||||
|
new { stage = restartStage, detail = exception.Message, processIds });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OperationView> StartLatencyTestAsync(string profileId)
|
public async Task<OperationView> StartLatencyTestAsync(string profileId)
|
||||||
@@ -1476,7 +1472,11 @@ public sealed partial class V2rayRuntime(
|
|||||||
var addresses = new HashSet<IPAddress> { IPAddress.Loopback };
|
var addresses = new HashSet<IPAddress> { IPAddress.Loopback };
|
||||||
foreach (var network in NetworkInterface.GetAllNetworkInterfaces())
|
foreach (var network in NetworkInterface.GetAllNetworkInterfaces())
|
||||||
{
|
{
|
||||||
if (network.OperationalStatus != OperationalStatus.Up) continue;
|
if (network.OperationalStatus != OperationalStatus.Up)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var address in network.GetIPProperties().UnicastAddresses
|
foreach (var address in network.GetIPProperties().UnicastAddresses
|
||||||
.Select(item => item.Address)
|
.Select(item => item.Address)
|
||||||
.Where(address => address.AddressFamily == AddressFamily.InterNetwork))
|
.Where(address => address.AddressFamily == AddressFamily.InterNetwork))
|
||||||
@@ -1520,7 +1520,11 @@ public sealed partial class V2rayRuntime(
|
|||||||
ExclusiveAddressUse = true,
|
ExclusiveAddressUse = true,
|
||||||
};
|
};
|
||||||
socket.Bind(new IPEndPoint(address, port));
|
socket.Bind(new IPEndPoint(address, port));
|
||||||
if (socketType == SocketType.Stream) socket.Listen(1);
|
if (socketType == SocketType.Stream)
|
||||||
|
{
|
||||||
|
socket.Listen(1);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (SocketException)
|
catch (SocketException)
|
||||||
@@ -1532,5 +1536,4 @@ public sealed partial class V2rayRuntime(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ public sealed record WebBuildIdentity(string Version, string Commit, string Buil
|
|||||||
?? new Dictionary<string, string?>(StringComparer.Ordinal);
|
?? new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||||
var repository = metadata.GetValueOrDefault("WebRepository");
|
var repository = metadata.GetValueOrDefault("WebRepository");
|
||||||
if (!WebReleaseChannel.IsValidRepository(repository))
|
if (!WebReleaseChannel.IsValidRepository(repository))
|
||||||
|
{
|
||||||
repository = WebReleaseChannel.DefaultRepository;
|
repository = WebReleaseChannel.DefaultRepository;
|
||||||
|
}
|
||||||
|
|
||||||
return new WebBuildIdentity(
|
return new WebBuildIdentity(
|
||||||
metadata.GetValueOrDefault("WebVersion") ?? "0.0.0-dev",
|
metadata.GetValueOrDefault("WebVersion") ?? "0.0.0-dev",
|
||||||
metadata.GetValueOrDefault("WebCommit") ?? "unknown",
|
metadata.GetValueOrDefault("WebCommit") ?? "unknown",
|
||||||
|
|||||||
@@ -19,14 +19,21 @@ internal static partial class WebReleaseChannel
|
|||||||
public static string BuildReleaseIndexUrl(string repository)
|
public static string BuildReleaseIndexUrl(string repository)
|
||||||
{
|
{
|
||||||
if (!IsValidRepository(repository))
|
if (!IsValidRepository(repository))
|
||||||
|
{
|
||||||
throw new ArgumentException("The release repository identifier is invalid.", nameof(repository));
|
throw new ArgumentException("The release repository identifier is invalid.", nameof(repository));
|
||||||
|
}
|
||||||
|
|
||||||
return $"{ApiBase}/{repository}/releases?per_page=100";
|
return $"{ApiBase}/{repository}/releases?per_page=100";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParseReleaseTag(string? tag, out string version)
|
public static bool TryParseReleaseTag(string? tag, out string version)
|
||||||
{
|
{
|
||||||
version = string.Empty;
|
version = string.Empty;
|
||||||
if (string.IsNullOrWhiteSpace(tag) || !ReleaseTagPattern().IsMatch(tag)) return false;
|
if (string.IsNullOrWhiteSpace(tag) || !ReleaseTagPattern().IsMatch(tag))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
version = tag;
|
version = tag;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,25 @@ internal static class WebUpdateDeploymentPolicy
|
|||||||
bool installDirectoryWritable)
|
bool installDirectoryWritable)
|
||||||
{
|
{
|
||||||
if (isContainer)
|
if (isContainer)
|
||||||
|
{
|
||||||
return new(WebDeploymentKind.Container, true, false, "maintenance.webUpdateContainerImage");
|
return new(WebDeploymentKind.Container, true, false, "maintenance.webUpdateContainerImage");
|
||||||
|
}
|
||||||
|
|
||||||
if (isSystemdManaged)
|
if (isSystemdManaged)
|
||||||
|
{
|
||||||
return new(WebDeploymentKind.SystemdManaged, true, false, "maintenance.webUpdateSystemdAdmin");
|
return new(WebDeploymentKind.SystemdManaged, true, false, "maintenance.webUpdateSystemdAdmin");
|
||||||
|
}
|
||||||
|
|
||||||
if (!isSupportedNativePlatform || !isNativeSingleFile)
|
if (!isSupportedNativePlatform || !isNativeSingleFile)
|
||||||
|
{
|
||||||
return new(WebDeploymentKind.Unsupported, true, false, "maintenance.webUpdateUnsupported");
|
return new(WebDeploymentKind.Unsupported, true, false, "maintenance.webUpdateUnsupported");
|
||||||
|
}
|
||||||
|
|
||||||
if (!installDirectoryWritable)
|
if (!installDirectoryWritable)
|
||||||
|
{
|
||||||
return new(WebDeploymentKind.ReadOnly, true, false, "maintenance.webUpdateReadOnly");
|
return new(WebDeploymentKind.ReadOnly, true, false, "maintenance.webUpdateReadOnly");
|
||||||
|
}
|
||||||
|
|
||||||
return new(WebDeploymentKind.NativeWritable, true, true, null);
|
return new(WebDeploymentKind.NativeWritable, true, true, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Services;
|
namespace v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
@@ -36,11 +35,19 @@ internal static partial class WebUpdatePackageStager
|
|||||||
string? currentCommit = null,
|
string? currentCommit = null,
|
||||||
string? candidateCommit = null)
|
string? candidateCommit = null)
|
||||||
{
|
{
|
||||||
if (!IsValidVersion(currentVersion) || !IsValidVersion(candidateVersion)) return false;
|
if (!IsValidVersion(currentVersion) || !IsValidVersion(candidateVersion))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var current = new SemanticVersion(currentVersion);
|
var current = new SemanticVersion(currentVersion);
|
||||||
var candidate = new SemanticVersion(candidateVersion);
|
var candidate = new SemanticVersion(candidateVersion);
|
||||||
var comparison = candidate.CompareTo(current);
|
var comparison = candidate.CompareTo(current);
|
||||||
if (comparison > 0) return true;
|
if (comparison > 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return comparison == 0
|
return comparison == 0
|
||||||
&& !string.IsNullOrWhiteSpace(candidateCommit)
|
&& !string.IsNullOrWhiteSpace(candidateCommit)
|
||||||
&& !string.IsNullOrWhiteSpace(currentCommit)
|
&& !string.IsNullOrWhiteSpace(currentCommit)
|
||||||
@@ -55,7 +62,10 @@ internal static partial class WebUpdatePackageStager
|
|||||||
new JsonSerializerOptions(JsonSerializerDefaults.Web))
|
new JsonSerializerOptions(JsonSerializerDefaults.Web))
|
||||||
?? throw new InvalidDataException("The Web release manifest is empty.");
|
?? throw new InvalidDataException("The Web release manifest is empty.");
|
||||||
if (manifest.Product == "v2rayN.Web")
|
if (manifest.Product == "v2rayN.Web")
|
||||||
|
{
|
||||||
throw new InvalidDataException("Legacy v2rayN.Web packages require a one-time manual installation of v2rayN.WebAPI.");
|
throw new InvalidDataException("Legacy v2rayN.Web packages require a one-time manual installation of v2rayN.WebAPI.");
|
||||||
|
}
|
||||||
|
|
||||||
if (manifest.Product != "v2rayN.WebAPI" || !IsValidVersion(manifest.Version)
|
if (manifest.Product != "v2rayN.WebAPI" || !IsValidVersion(manifest.Version)
|
||||||
|| string.IsNullOrWhiteSpace(manifest.Commit) || manifest.Packages is null || manifest.Packages.Count is 0 or > 8)
|
|| string.IsNullOrWhiteSpace(manifest.Commit) || manifest.Packages is null || manifest.Packages.Count is 0 or > 8)
|
||||||
{
|
{
|
||||||
@@ -90,7 +100,10 @@ internal static partial class WebUpdatePackageStager
|
|||||||
public static WebUpdatePackage RequirePackage(WebUpdateManifest manifest, string rid)
|
public static WebUpdatePackage RequirePackage(WebUpdateManifest manifest, string rid)
|
||||||
{
|
{
|
||||||
if (!IsSupportedRid(rid))
|
if (!IsSupportedRid(rid))
|
||||||
|
{
|
||||||
throw new InvalidDataException($"The Web release does not support runtime identifier {rid}.");
|
throw new InvalidDataException($"The Web release does not support runtime identifier {rid}.");
|
||||||
|
}
|
||||||
|
|
||||||
return manifest.Packages.FirstOrDefault(package => package.Rid == rid)
|
return manifest.Packages.FirstOrDefault(package => package.Rid == rid)
|
||||||
?? throw new InvalidDataException($"The Web release has no package for runtime identifier {rid}.");
|
?? throw new InvalidDataException($"The Web release has no package for runtime identifier {rid}.");
|
||||||
}
|
}
|
||||||
@@ -114,22 +127,32 @@ internal static partial class WebUpdatePackageStager
|
|||||||
{
|
{
|
||||||
var archive = new FileInfo(archivePath);
|
var archive = new FileInfo(archivePath);
|
||||||
if (!archive.Exists || archive.Length != package.Size || archive.Length is <= 0 or > MaximumArchiveBytes)
|
if (!archive.Exists || archive.Length != package.Size || archive.Length is <= 0 or > MaximumArchiveBytes)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The downloaded Web package size does not match its manifest.");
|
throw new InvalidDataException("The downloaded Web package size does not match its manifest.");
|
||||||
|
}
|
||||||
|
|
||||||
await using (var stream = File.OpenRead(archivePath))
|
await using (var stream = File.OpenRead(archivePath))
|
||||||
{
|
{
|
||||||
var hash = await SHA256.HashDataAsync(stream, cancellationToken);
|
var hash = await SHA256.HashDataAsync(stream, cancellationToken);
|
||||||
var expectedHash = Convert.FromHexString(package.Sha256);
|
var expectedHash = Convert.FromHexString(package.Sha256);
|
||||||
if (!CryptographicOperations.FixedTimeEquals(hash, expectedHash))
|
if (!CryptographicOperations.FixedTimeEquals(hash, expectedHash))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The downloaded Web package SHA-256 does not match its manifest.");
|
throw new InvalidDataException("The downloaded Web package SHA-256 does not match its manifest.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Directory.Exists(destinationPath) || File.Exists(destinationPath))
|
if (Directory.Exists(destinationPath) || File.Exists(destinationPath))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package staging directory already exists.");
|
throw new InvalidDataException("The Web package staging directory already exists.");
|
||||||
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(destinationPath);
|
Directory.CreateDirectory(destinationPath);
|
||||||
var root = Path.GetFullPath(destinationPath);
|
var root = Path.GetFullPath(destinationPath);
|
||||||
if (new DirectoryInfo(root).LinkTarget is not null)
|
if (new DirectoryInfo(root).LinkTarget is not null)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package staging root must not be a symbolic link.");
|
throw new InvalidDataException("The Web package staging root must not be a symbolic link.");
|
||||||
|
}
|
||||||
|
|
||||||
var rootPrefix = root.EndsWith(Path.DirectorySeparatorChar) ? root : root + Path.DirectorySeparatorChar;
|
var rootPrefix = root.EndsWith(Path.DirectorySeparatorChar) ? root : root + Path.DirectorySeparatorChar;
|
||||||
long expandedBytes = 0;
|
long expandedBytes = 0;
|
||||||
var entryCount = 0;
|
var entryCount = 0;
|
||||||
@@ -140,28 +163,48 @@ internal static partial class WebUpdatePackageStager
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
if (++entryCount > MaximumEntries)
|
if (++entryCount > MaximumEntries)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package has too many archive entries.");
|
throw new InvalidDataException("The Web package has too many archive entries.");
|
||||||
|
}
|
||||||
|
|
||||||
var relative = NormalizeEntryName(entry.FullName);
|
var relative = NormalizeEntryName(entry.FullName);
|
||||||
var target = Path.GetFullPath(Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar)));
|
var target = Path.GetFullPath(Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar)));
|
||||||
if (!target.StartsWith(rootPrefix, StringComparison.Ordinal))
|
if (!target.StartsWith(rootPrefix, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package contains a path outside its staging directory.");
|
throw new InvalidDataException("The Web package contains a path outside its staging directory.");
|
||||||
|
}
|
||||||
|
|
||||||
if (IsEnvironmentConfigurationPath(relative))
|
if (IsEnvironmentConfigurationPath(relative))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package must not contain .env or .env.example files.");
|
throw new InvalidDataException("The Web package must not contain .env or .env.example files.");
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsAllowedPath(relative, package.Rid))
|
if (!IsAllowedPath(relative, package.Rid))
|
||||||
|
{
|
||||||
throw new InvalidDataException($"The Web package contains an unexpected file: {relative}.");
|
throw new InvalidDataException($"The Web package contains an unexpected file: {relative}.");
|
||||||
|
}
|
||||||
|
|
||||||
var unixType = (entry.ExternalAttributes >> 16) & 0xF000;
|
var unixType = (entry.ExternalAttributes >> 16) & 0xF000;
|
||||||
if (unixType is not (0 or 0x4000 or 0x8000))
|
if (unixType is not (0 or 0x4000 or 0x8000))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package contains a link or special archive entry.");
|
throw new InvalidDataException("The Web package contains a link or special archive entry.");
|
||||||
|
}
|
||||||
|
|
||||||
if (unixType == 0x4000 || entry.FullName.EndsWith('/'))
|
if (unixType == 0x4000 || entry.FullName.EndsWith('/'))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(target);
|
Directory.CreateDirectory(target);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!extractedFiles.Add(relative))
|
if (!extractedFiles.Add(relative))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package contains duplicate file paths.");
|
throw new InvalidDataException("The Web package contains duplicate file paths.");
|
||||||
|
}
|
||||||
|
|
||||||
if (entry.Length < 0 || entry.Length > MaximumExpandedBytes - expandedBytes)
|
if (entry.Length < 0 || entry.Length > MaximumExpandedBytes - expandedBytes)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The expanded Web package exceeds the configured size limit.");
|
throw new InvalidDataException("The expanded Web package exceeds the configured size limit.");
|
||||||
|
}
|
||||||
|
|
||||||
expandedBytes += entry.Length;
|
expandedBytes += entry.Length;
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||||
await using var input = entry.Open();
|
await using var input = entry.Open();
|
||||||
@@ -203,12 +246,22 @@ internal static partial class WebUpdatePackageStager
|
|||||||
private static string NormalizeEntryName(string name)
|
private static string NormalizeEntryName(string name)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name) || name.Contains('\\') || name.Contains('\0') || name.StartsWith('/'))
|
if (string.IsNullOrWhiteSpace(name) || name.Contains('\\') || name.Contains('\0') || name.StartsWith('/'))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package contains an invalid archive path.");
|
throw new InvalidDataException("The Web package contains an invalid archive path.");
|
||||||
|
}
|
||||||
|
|
||||||
var segments = name.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
var segments = name.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||||
if (segments.Any(segment => segment is "." or ".." || segment.Contains(':')))
|
if (segments.Any(segment => segment is "." or ".." || segment.Contains(':')))
|
||||||
|
{
|
||||||
throw new InvalidDataException("The Web package contains a path traversal entry.");
|
throw new InvalidDataException("The Web package contains a path traversal entry.");
|
||||||
|
}
|
||||||
|
|
||||||
var normalized = string.Join('/', segments);
|
var normalized = string.Join('/', segments);
|
||||||
if (normalized.Length == 0) throw new InvalidDataException("The Web package contains an empty archive path.");
|
if (normalized.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("The Web package contains an empty archive path.");
|
||||||
|
}
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +289,9 @@ internal static partial class WebUpdatePackageStager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rid is not ("linux-x64" or "linux-arm64" or "linux-riscv64" or "linux-loongarch64"))
|
if (rid is not ("linux-x64" or "linux-arm64" or "linux-riscv64" or "linux-loongarch64"))
|
||||||
|
{
|
||||||
throw new InvalidDataException($"Unsupported WebAPI runtime identifier: {rid}.");
|
throw new InvalidDataException($"Unsupported WebAPI runtime identifier: {rid}.");
|
||||||
|
}
|
||||||
|
|
||||||
var expectedMachine = rid switch
|
var expectedMachine = rid switch
|
||||||
{
|
{
|
||||||
@@ -253,13 +308,18 @@ internal static partial class WebUpdatePackageStager
|
|||||||
|| BinaryPrimitives.ReadUInt16LittleEndian(header[18..]) != expectedMachine
|
|| BinaryPrimitives.ReadUInt16LittleEndian(header[18..]) != expectedMachine
|
||||||
|| BinaryPrimitives.ReadUInt32LittleEndian(header[20..]) != 1
|
|| BinaryPrimitives.ReadUInt32LittleEndian(header[20..]) != 1
|
||||||
|| BinaryPrimitives.ReadUInt16LittleEndian(header[52..]) != 64)
|
|| BinaryPrimitives.ReadUInt16LittleEndian(header[52..]) != 64)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The WebAPI package executable must be a Linux ELF64 image for its declared runtime identifier.");
|
throw new InvalidDataException("The WebAPI package executable must be a Linux ELF64 image for its declared runtime identifier.");
|
||||||
|
}
|
||||||
|
|
||||||
var offset = BinaryPrimitives.ReadUInt64LittleEndian(header[32..]);
|
var offset = BinaryPrimitives.ReadUInt64LittleEndian(header[32..]);
|
||||||
var entrySize = BinaryPrimitives.ReadUInt16LittleEndian(header[54..]);
|
var entrySize = BinaryPrimitives.ReadUInt16LittleEndian(header[54..]);
|
||||||
var count = BinaryPrimitives.ReadUInt16LittleEndian(header[56..]);
|
var count = BinaryPrimitives.ReadUInt16LittleEndian(header[56..]);
|
||||||
if (offset < 64 || offset > (ulong)stream.Length || entrySize != 56 || count == 0
|
if (offset < 64 || offset > (ulong)stream.Length || entrySize != 56 || count == 0
|
||||||
|| (ulong)count * entrySize > (ulong)stream.Length - offset)
|
|| (ulong)count * entrySize > (ulong)stream.Length - offset)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The WebAPI package executable has a truncated or invalid ELF program table.");
|
throw new InvalidDataException("The WebAPI package executable has a truncated or invalid ELF program table.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RequireWindowsExecutable(Stream stream, string rid)
|
private static void RequireWindowsExecutable(Stream stream, string rid)
|
||||||
@@ -279,7 +339,9 @@ internal static partial class WebUpdatePackageStager
|
|||||||
|
|
||||||
var peOffset = BinaryPrimitives.ReadUInt32LittleEndian(dosHeader[0x3C..]);
|
var peOffset = BinaryPrimitives.ReadUInt32LittleEndian(dosHeader[0x3C..]);
|
||||||
if (peOffset < dosHeader.Length || peOffset > stream.Length - 26)
|
if (peOffset < dosHeader.Length || peOffset > stream.Length - 26)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The WebAPI package executable has a truncated Windows PE header.");
|
throw new InvalidDataException("The WebAPI package executable has a truncated Windows PE header.");
|
||||||
|
}
|
||||||
|
|
||||||
stream.Position = peOffset;
|
stream.Position = peOffset;
|
||||||
Span<byte> peHeader = stackalloc byte[26];
|
Span<byte> peHeader = stackalloc byte[26];
|
||||||
@@ -295,7 +357,10 @@ internal static partial class WebUpdatePackageStager
|
|||||||
|
|
||||||
var optionalHeaderSize = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[20..]);
|
var optionalHeaderSize = BinaryPrimitives.ReadUInt16LittleEndian(peHeader[20..]);
|
||||||
if ((long)peOffset + 24 + optionalHeaderSize > stream.Length)
|
if ((long)peOffset + 24 + optionalHeaderSize > stream.Length)
|
||||||
|
{
|
||||||
throw new InvalidDataException("The WebAPI package executable has a truncated Windows optional header.");
|
throw new InvalidDataException("The WebAPI package executable has a truncated Windows optional header.");
|
||||||
|
}
|
||||||
|
|
||||||
Span<byte> optionalHeaderMagic = stackalloc byte[2];
|
Span<byte> optionalHeaderMagic = stackalloc byte[2];
|
||||||
stream.Position = (long)peOffset + 24;
|
stream.Position = (long)peOffset + 24;
|
||||||
if (stream.Read(optionalHeaderMagic) != optionalHeaderMagic.Length
|
if (stream.Read(optionalHeaderMagic) != optionalHeaderMagic.Length
|
||||||
@@ -315,13 +380,23 @@ internal static partial class WebUpdatePackageStager
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var read = await input.ReadAsync(buffer, cancellationToken);
|
var read = await input.ReadAsync(buffer, cancellationToken);
|
||||||
if (read == 0) break;
|
if (read == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
copied += read;
|
copied += read;
|
||||||
if (copied > expectedLength || copied > MaximumExpandedBytes)
|
if (copied > expectedLength || copied > MaximumExpandedBytes)
|
||||||
|
{
|
||||||
throw new InvalidDataException("A Web package archive entry exceeded its declared size.");
|
throw new InvalidDataException("A Web package archive entry exceeded its declared size.");
|
||||||
|
}
|
||||||
|
|
||||||
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
|
||||||
}
|
}
|
||||||
if (copied != expectedLength) throw new InvalidDataException("A Web package archive entry was truncated.");
|
if (copied != expectedLength)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("A Web package archive entry was truncated.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$", RegexOptions.CultureInvariant)]
|
[GeneratedRegex("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$", RegexOptions.CultureInvariant)]
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class BackupRestorePreflightTests
|
|||||||
|
|
||||||
private static IConfigurationRoot Config(params (string Key, string Value)[] values) =>
|
private static IConfigurationRoot Config(params (string Key, string Value)[] values) =>
|
||||||
new ConfigurationBuilder()
|
new ConfigurationBuilder()
|
||||||
.AddInMemoryCollection(values.ToDictionary(item => item.Key, item => (string?)item.Value))
|
.AddInMemoryCollection(values.ToDictionary(item => item.Key, item => item.Value))
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
private sealed class TemporaryDirectory : IDisposable
|
private sealed class TemporaryDirectory : IDisposable
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Tests;
|
namespace v2rayN.WebAPI.Tests;
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ public class CoreProcessTrackerTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task WebTrackerCapturesNewCoreProcessesAndPrunesExitedIdentities()
|
public async Task WebTrackerCapturesNewCoreProcessesAndPrunesExitedIdentities()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var executable = CopySleepExecutable(directory.Path);
|
var executable = CopySleepExecutable(directory.Path);
|
||||||
@@ -39,7 +42,10 @@ public class CoreProcessTrackerTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task WebTrackerDoesNotClaimProcessesThatPredateItsLaunchSnapshot()
|
public async Task WebTrackerDoesNotClaimProcessesThatPredateItsLaunchSnapshot()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var executable = CopySleepExecutable(directory.Path);
|
var executable = CopySleepExecutable(directory.Path);
|
||||||
@@ -61,7 +67,11 @@ public class CoreProcessTrackerTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task WebTrackerRecognizesAnExecutableRunningThroughItsInterpreter()
|
public async Task WebTrackerRecognizesAnExecutableRunningThroughItsInterpreter()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var executable = Path.Combine(directory.Path, "test-core-shim");
|
var executable = Path.Combine(directory.Path, "test-core-shim");
|
||||||
await File.WriteAllTextAsync(executable, "#!/bin/sh\nsleep 60\n");
|
await File.WriteAllTextAsync(executable, "#!/bin/sh\nsleep 60\n");
|
||||||
@@ -88,7 +98,11 @@ public class CoreProcessTrackerTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task WebTrackerDoesNotClaimSameNamedExecutableFromAnotherPath()
|
public async Task WebTrackerDoesNotClaimSameNamedExecutableFromAnotherPath()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var unrelatedExecutable = Path.Combine(directory.Path, "sleep");
|
var unrelatedExecutable = Path.Combine(directory.Path, "sleep");
|
||||||
File.Copy("/bin/sleep", unrelatedExecutable);
|
File.Copy("/bin/sleep", unrelatedExecutable);
|
||||||
@@ -113,7 +127,11 @@ public class CoreProcessTrackerTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task LinuxTrackerRequiresTheWebCoreConfigPathWhenProvided()
|
public async Task LinuxTrackerRequiresTheWebCoreConfigPathWhenProvided()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var executable = Path.Combine(directory.Path, "test-core-shim");
|
var executable = Path.Combine(directory.Path, "test-core-shim");
|
||||||
var configPath = Path.Combine(directory.Path, "config.json");
|
var configPath = Path.Combine(directory.Path, "config.json");
|
||||||
@@ -144,7 +162,11 @@ public class CoreProcessTrackerTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task WebTrackerRejectsReusedPidWithDifferentStartTimeOrExecutable()
|
public async Task WebTrackerRejectsReusedPidWithDifferentStartTimeOrExecutable()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var executable = CopySleepExecutable(directory.Path);
|
var executable = CopySleepExecutable(directory.Path);
|
||||||
var existingProcessIds = CoreProcessTracker.CaptureExistingProcessIds();
|
var existingProcessIds = CoreProcessTracker.CaptureExistingProcessIds();
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler.Builder;
|
using ServiceLib.Handler.Builder;
|
||||||
using ServiceLib.Models.CoreConfigs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using System.Formats.Tar;
|
using System.Formats.Tar;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Resx;
|
using ServiceLib.Resx;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.CoreConfigs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Services.CoreConfig;
|
using ServiceLib.Services.CoreConfig;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ public class LogBufferTests
|
|||||||
{
|
{
|
||||||
var logs = new LogBuffer();
|
var logs = new LogBuffer();
|
||||||
var message = new string('x', 10_000);
|
var message = new string('x', 10_000);
|
||||||
for (var index = 0; index < 300; index++) logs.Add("core", message);
|
for (var index = 0; index < 300; index++)
|
||||||
|
{
|
||||||
|
logs.Add("core", message);
|
||||||
|
}
|
||||||
|
|
||||||
var retained = logs.Recent(2000);
|
var retained = logs.Recent(2000);
|
||||||
await retained.Count.Should().BeLessThan(2000);
|
await retained.Count.Should().BeLessThan(2000);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using v2rayN.WebAPI.Launcher;
|
using System.IO.MemoryMappedFiles;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.IO.MemoryMappedFiles;
|
using v2rayN.WebAPI.Launcher;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Tests;
|
namespace v2rayN.WebAPI.Tests;
|
||||||
|
|
||||||
@@ -13,7 +13,11 @@ public class NativeWebUpdateFileSwapTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task RollbackDoesNotModifyAnExecutableThatIsStillMemoryMapped()
|
public async Task RollbackDoesNotModifyAnExecutableThatIsStillMemoryMapped()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var plan = await CreatePlanAsync(directory.Path, includeNewIdentity: true);
|
var plan = await CreatePlanAsync(directory.Path, includeNewIdentity: true);
|
||||||
NativeWebUpdateHelper.CopyCurrentAppToBackup(plan);
|
NativeWebUpdateHelper.CopyCurrentAppToBackup(plan);
|
||||||
@@ -96,7 +100,10 @@ public class NativeWebUpdateFileSwapTests
|
|||||||
await File.WriteAllTextAsync(Path.Combine(install, ".env"), "V2RAYN_WEB_API_KEY=private-test-key\n");
|
await File.WriteAllTextAsync(Path.Combine(install, ".env"), "V2RAYN_WEB_API_KEY=private-test-key\n");
|
||||||
await File.WriteAllTextAsync(Path.Combine(candidate, ExecutableName), "new-web");
|
await File.WriteAllTextAsync(Path.Combine(candidate, ExecutableName), "new-web");
|
||||||
if (includeNewIdentity)
|
if (includeNewIdentity)
|
||||||
|
{
|
||||||
await File.WriteAllTextAsync(Path.Combine(candidate, "v2rayN.WebAPI.build.json"), "new-identity");
|
await File.WriteAllTextAsync(Path.Combine(candidate, "v2rayN.WebAPI.build.json"), "new-identity");
|
||||||
|
}
|
||||||
|
|
||||||
return new NativeWebUpdatePlan(
|
return new NativeWebUpdatePlan(
|
||||||
install,
|
install,
|
||||||
candidate,
|
candidate,
|
||||||
|
|||||||
@@ -82,8 +82,10 @@ public class NativeWebUpdateHelperTests
|
|||||||
await Path.GetDirectoryName(helper).Should().BeEqualTo(directory);
|
await Path.GetDirectoryName(helper).Should().BeEqualTo(directory);
|
||||||
await NativeWebUpdateHelper.IsUpdateHelperExecutableName(Path.GetFileName(helper)).Should().BeTrue();
|
await NativeWebUpdateHelper.IsUpdateHelperExecutableName(Path.GetFileName(helper)).Should().BeTrue();
|
||||||
if (OperatingSystem.IsLinux())
|
if (OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
await File.GetUnixFileMode(helper).Should().BeEqualTo(
|
await File.GetUnixFileMode(helper).Should().BeEqualTo(
|
||||||
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
|
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
|
||||||
|
}
|
||||||
|
|
||||||
var candidate = Path.Combine(directory, "candidate");
|
var candidate = Path.Combine(directory, "candidate");
|
||||||
await File.WriteAllTextAsync(candidate, "different-bundle-layout");
|
await File.WriteAllTextAsync(candidate, "different-bundle-layout");
|
||||||
@@ -100,7 +102,11 @@ public class NativeWebUpdateHelperTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task SymlinkManagedExecutablesCannotCreateWorkers()
|
public async Task SymlinkManagedExecutablesCannotCreateWorkers()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var directory = Path.Combine(Path.GetTempPath(), $"webapi-helper-link-{Guid.NewGuid():N}");
|
var directory = Path.Combine(Path.GetTempPath(), $"webapi-helper-link-{Guid.NewGuid():N}");
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
try
|
try
|
||||||
@@ -110,7 +116,8 @@ public class NativeWebUpdateHelperTests
|
|||||||
var link = Path.Combine(directory, "v2rayN.WebAPI");
|
var link = Path.Combine(directory, "v2rayN.WebAPI");
|
||||||
File.CreateSymbolicLink(link, target);
|
File.CreateSymbolicLink(link, target);
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { NativeWebUpdateHelper.CreateIsolatedHelperExecutable(link); }
|
try
|
||||||
|
{ NativeWebUpdateHelper.CreateIsolatedHelperExecutable(link); }
|
||||||
catch (InvalidDataException) { rejected = true; }
|
catch (InvalidDataException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
await Directory.GetFiles(directory, ".v2rayn-WebAPI-update-helper-*").Length.Should().BeEqualTo(0);
|
await Directory.GetFiles(directory, ".v2rayn-WebAPI-update-helper-*").Length.Should().BeEqualTo(0);
|
||||||
|
|||||||
@@ -29,8 +29,15 @@ public class NativeWebUpdateWorkflowTests
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (previous is null) File.Delete(path);
|
if (previous is null)
|
||||||
else await File.WriteAllBytesAsync(path, previous);
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await File.WriteAllBytesAsync(path, previous);
|
||||||
|
}
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable(ServiceLib.Global.LocalAppData, previousLocalData);
|
Environment.SetEnvironmentVariable(ServiceLib.Global.LocalAppData, previousLocalData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
@@ -113,7 +108,7 @@ public class ParityValidationTests
|
|||||||
EConfigType.Hysteria2,
|
EConfigType.Hysteria2,
|
||||||
EConfigType.WireGuard,
|
EConfigType.WireGuard,
|
||||||
};
|
};
|
||||||
await (editableTypes.SequenceEqual(expected)).Should().BeTrue();
|
await editableTypes.SequenceEqual(expected).Should().BeTrue();
|
||||||
|
|
||||||
var validMappings = expected.Select(configType => new CoreTypeMapping(configType, ECoreType.Xray)).ToArray();
|
var validMappings = expected.Select(configType => new CoreTypeMapping(configType, ECoreType.Xray)).ToArray();
|
||||||
await V2rayRuntime.AreWebCoreTypeMappingsValid(validMappings).Should().BeTrue();
|
await V2rayRuntime.AreWebCoreTypeMappingsValid(validMappings).Should().BeTrue();
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Dto;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Tests;
|
namespace v2rayN.WebAPI.Tests;
|
||||||
@@ -182,7 +174,11 @@ public class RegionalPresetStagingTests
|
|||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.Success) restartCalled = true;
|
if (result.Success)
|
||||||
|
{
|
||||||
|
restartCalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
await result.Success.Should().BeFalse();
|
await result.Success.Should().BeFalse();
|
||||||
await result.RollbackSucceeded.Should().BeTrue();
|
await result.RollbackSucceeded.Should().BeTrue();
|
||||||
await restartCalled.Should().BeFalse();
|
await restartCalled.Should().BeFalse();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
|
|
||||||
namespace v2rayN.WebAPI.Tests;
|
namespace v2rayN.WebAPI.Tests;
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json.Nodes;
|
using System.Text.Json.Nodes;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.CoreConfigs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Services.CoreConfig;
|
using ServiceLib.Services.CoreConfig;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Helper;
|
using ServiceLib.Helper;
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,3 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Net;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using ServiceLib.Services;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using ServiceLib;
|
|
||||||
using ServiceLib.Common;
|
|
||||||
using ServiceLib.Manager;
|
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Services;
|
using v2rayN.WebAPI.Services;
|
||||||
|
|
||||||
@@ -177,7 +173,10 @@ public class UpdatePreReleaseSettingsTests
|
|||||||
if (File.Exists(_configPath))
|
if (File.Exists(_configPath))
|
||||||
{
|
{
|
||||||
_previousConfigFile = File.ReadAllBytes(_configPath);
|
_previousConfigFile = File.ReadAllBytes(_configPath);
|
||||||
if (!OperatingSystem.IsWindows()) _previousConfigMode = File.GetUnixFileMode(_configPath);
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
_previousConfigMode = File.GetUnixFileMode(_configPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Environment.SetEnvironmentVariable(Global.LocalAppData, "0");
|
Environment.SetEnvironmentVariable(Global.LocalAppData, "0");
|
||||||
_configField.SetValue(AppManager.Instance, Config);
|
_configField.SetValue(AppManager.Instance, Config);
|
||||||
@@ -193,13 +192,18 @@ public class UpdatePreReleaseSettingsTests
|
|||||||
{
|
{
|
||||||
if (_previousConfigFile is null)
|
if (_previousConfigFile is null)
|
||||||
{
|
{
|
||||||
if (File.Exists(_configPath)) File.Delete(_configPath);
|
if (File.Exists(_configPath))
|
||||||
|
{
|
||||||
|
File.Delete(_configPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(_configPath, _previousConfigFile);
|
File.WriteAllBytes(_configPath, _previousConfigFile);
|
||||||
if (!OperatingSystem.IsWindows() && _previousConfigMode is { } mode)
|
if (!OperatingSystem.IsWindows() && _previousConfigMode is { } mode)
|
||||||
|
{
|
||||||
File.SetUnixFileMode(_configPath, mode);
|
File.SetUnixFileMode(_configPath, mode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Hosting;
|
|||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using v2rayN.WebAPI.Api;
|
using v2rayN.WebAPI.Api;
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ using System.Net.Http.Json;
|
|||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using v2rayN.WebAPI.Api;
|
using v2rayN.WebAPI.Api;
|
||||||
using v2rayN.WebAPI.Security;
|
using v2rayN.WebAPI.Security;
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ public class WebCorsTests
|
|||||||
"https://good.example.com?", "https://good.example.com#", "https://user:password@good.example.com", "https://good.example.com,", "https://good.example.com\\evil", "https://good.example.com/other/..", "https://good.example.com/%2e" })
|
"https://good.example.com?", "https://good.example.com#", "https://user:password@good.example.com", "https://good.example.com,", "https://good.example.com\\evil", "https://good.example.com/other/..", "https://good.example.com/%2e" })
|
||||||
{
|
{
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { _ = new WebCorsPolicy(invalid); } catch (ArgumentException) { rejected = true; }
|
try
|
||||||
|
{ _ = new WebCorsPolicy(invalid); }
|
||||||
|
catch (ArgumentException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
var policy = new WebCorsPolicy(" HTTPS://GOOD.EXAMPLE.COM:443, http://localhost:5173 ");
|
var policy = new WebCorsPolicy(" HTTPS://GOOD.EXAMPLE.COM:443, http://localhost:5173 ");
|
||||||
@@ -196,8 +198,16 @@ public class WebCorsTests
|
|||||||
private static HttpRequestMessage Request(HttpMethod method, string path, string? origin, string? token = null)
|
private static HttpRequestMessage Request(HttpMethod method, string path, string? origin, string? token = null)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(method, path);
|
var request = new HttpRequestMessage(method, path);
|
||||||
if (origin is not null) request.Headers.Add("Origin", origin);
|
if (origin is not null)
|
||||||
if (token is not null) request.Headers.Authorization = new("Bearer", token);
|
{
|
||||||
|
request.Headers.Add("Origin", origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token is not null)
|
||||||
|
{
|
||||||
|
request.Headers.Authorization = new("Bearer", token);
|
||||||
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ public class WebInstanceLockProcessTests
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_process.HasExited) _process.Kill(entireProcessTree: true);
|
if (!_process.HasExited)
|
||||||
|
{
|
||||||
|
_process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -145,7 +145,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task ExistingHealthyInstanceIsReusedAndBrowserAdapterIsInvoked()
|
public async Task ExistingHealthyInstanceIsReusedAndBrowserAdapterIsInvoked()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -171,7 +174,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task HealthyEndpointFromAnotherInstanceIsNotReusedForThisDataHome()
|
public async Task HealthyEndpointFromAnotherInstanceIsNotReusedForThisDataHome()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -205,7 +211,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task UnlockedStaleLockFileCanBeRecovered()
|
public async Task UnlockedStaleLockFileCanBeRecovered()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -228,7 +237,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopTreatsAnUnlockedStaleLockAsAlreadyStopped()
|
public async Task StopTreatsAnUnlockedStaleLockAsAlreadyStopped()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -245,7 +257,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopSendsOneSigTermToVerifiedOwnerAndWaitsForLockRelease()
|
public async Task StopSendsOneSigTermToVerifiedOwnerAndWaitsForLockRelease()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -282,7 +297,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopDoesNotReportSuccessWhileAKnownCoreChildRemainsAlive()
|
public async Task StopDoesNotReportSuccessWhileAKnownCoreChildRemainsAlive()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -304,7 +322,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopNeverSignalsWhenHealthPidDoesNotMatchLockOwner()
|
public async Task StopNeverSignalsWhenHealthPidDoesNotMatchLockOwner()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -321,7 +342,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopNeverSignalsWhenHealthCannotConfirmTheInstance()
|
public async Task StopNeverSignalsWhenHealthCannotConfirmTheInstance()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -338,7 +362,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopWithoutAProbeEndpointNeverSignals()
|
public async Task StopWithoutAProbeEndpointNeverSignals()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -359,7 +386,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopTimesOutWithoutEscalatingBeyondOneSigTerm()
|
public async Task StopTimesOutWithoutEscalatingBeyondOneSigTerm()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -381,7 +411,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task StopTimeoutReportsLastObservedCleanupStage()
|
public async Task StopTimeoutReportsLastObservedCleanupStage()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
var lockPath = Path.Combine(directory.Path, "instance.lock");
|
||||||
@@ -405,7 +438,10 @@ public class WebLauncherTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task LockIsReleasedWhenOwnerExitsAndDataHomesUseDistinctLocks()
|
public async Task LockIsReleasedWhenOwnerExitsAndDataHomesUseDistinctLocks()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using var directory = new TemporaryDirectory();
|
using var directory = new TemporaryDirectory();
|
||||||
var firstPath = Path.Combine(directory.Path, "one", "instance.lock");
|
var firstPath = Path.Combine(directory.Path, "one", "instance.lock");
|
||||||
@@ -435,8 +471,8 @@ public class WebLauncherTests
|
|||||||
await chinese.Contains("./v2rayN.WebAPI --stop", StringComparison.Ordinal).Should().BeTrue();
|
await chinese.Contains("./v2rayN.WebAPI --stop", StringComparison.Ordinal).Should().BeTrue();
|
||||||
await LauncherMessages.AlreadyRunning("http://127.0.0.1:5080", command, false, LauncherLocale.TraditionalChinese)
|
await LauncherMessages.AlreadyRunning("http://127.0.0.1:5080", command, false, LauncherLocale.TraditionalChinese)
|
||||||
.Contains("--stop", StringComparison.Ordinal).Should().BeTrue();
|
.Contains("--stop", StringComparison.Ordinal).Should().BeTrue();
|
||||||
await (LauncherMessages.Started("http://127.0.0.1:5080", command, LauncherLocale.TraditionalChinese)
|
await LauncherMessages.Started("http://127.0.0.1:5080", command, LauncherLocale.TraditionalChinese)
|
||||||
.Contains("--stop", StringComparison.Ordinal)).Should().BeTrue();
|
.Contains("--stop", StringComparison.Ordinal).Should().BeTrue();
|
||||||
await english.Contains("API health endpoint", StringComparison.Ordinal).Should().BeTrue();
|
await english.Contains("API health endpoint", StringComparison.Ordinal).Should().BeTrue();
|
||||||
await LauncherMessages.ForegroundStarted("http://127.0.0.1:5080", LauncherLocale.English)
|
await LauncherMessages.ForegroundStarted("http://127.0.0.1:5080", LauncherLocale.English)
|
||||||
.Contains("Ctrl+C", StringComparison.Ordinal).Should().BeTrue();
|
.Contains("Ctrl+C", StringComparison.Ordinal).Should().BeTrue();
|
||||||
@@ -498,7 +534,10 @@ public class WebLauncherTests
|
|||||||
{
|
{
|
||||||
_healthy = isHealthy;
|
_healthy = isHealthy;
|
||||||
_processId = instanceProcessId;
|
_processId = instanceProcessId;
|
||||||
if (coreProcessIds is not null) _coreProcessIds = coreProcessIds;
|
if (coreProcessIds is not null)
|
||||||
|
{
|
||||||
|
_coreProcessIds = coreProcessIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetShutdownStage(string? stage) => _shutdownStage = stage;
|
public void SetShutdownStage(string? stage) => _shutdownStage = stage;
|
||||||
|
|||||||
@@ -86,10 +86,14 @@ public class WebListenerSecurityPolicyTests
|
|||||||
.AddInMemoryCollection(new Dictionary<string, string?> { ["urls"] = "http://192.168.1.20:5080" })
|
.AddInMemoryCollection(new Dictionary<string, string?> { ["urls"] = "http://192.168.1.20:5080" })
|
||||||
.AddCommandLine(value is null ? [argument] : [argument, value]).Build();
|
.AddCommandLine(value is null ? [argument] : [argument, value]).Build();
|
||||||
// URLs win over port-only configuration, so remove the URL for the ports-only case.
|
// URLs win over port-only configuration, so remove the URL for the ports-only case.
|
||||||
if (argument == "--http_ports") configuration["urls"] = "";
|
if (argument == "--http_ports")
|
||||||
|
{
|
||||||
|
configuration["urls"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
await (WebListenerSecurityPolicy.GetStartupError(configuration, null) is not null).Should().BeEqualTo(requiresKey);
|
await (WebListenerSecurityPolicy.GetStartupError(configuration, null) is not null).Should().BeEqualTo(requiresKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IConfigurationRoot Config(params (string Key, string Value)[] values) =>
|
private static IConfigurationRoot Config(params (string Key, string Value)[] values) =>
|
||||||
new ConfigurationBuilder().AddInMemoryCollection(values.ToDictionary(item => item.Key, item => (string?)item.Value)).Build();
|
new ConfigurationBuilder().AddInMemoryCollection(values.ToDictionary(item => item.Key, item => item.Value)).Build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,6 @@ public class WebProbeUriResolverTests
|
|||||||
|
|
||||||
private static IConfigurationRoot Config(params (string Key, string Value)[] values) =>
|
private static IConfigurationRoot Config(params (string Key, string Value)[] values) =>
|
||||||
new ConfigurationBuilder()
|
new ConfigurationBuilder()
|
||||||
.AddInMemoryCollection(values.ToDictionary(item => item.Key, item => (string?)item.Value))
|
.AddInMemoryCollection(values.ToDictionary(item => item.Key, item => item.Value))
|
||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ public class WebReleaseChannelTests
|
|||||||
await WebReleaseChannel.DefaultRepository.Should().BeEqualTo("2dust/v2rayN");
|
await WebReleaseChannel.DefaultRepository.Should().BeEqualTo("2dust/v2rayN");
|
||||||
|
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { _ = WebReleaseChannel.BuildReleaseIndexUrl("2dust/v2rayN/../../evil"); }
|
try
|
||||||
|
{ _ = WebReleaseChannel.BuildReleaseIndexUrl("2dust/v2rayN/../../evil"); }
|
||||||
catch (ArgumentException) { rejected = true; }
|
catch (ArgumentException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
await WebReleaseChannel.IsValidRepository("community-fork/v2rayN").Should().BeTrue();
|
await WebReleaseChannel.IsValidRepository("community-fork/v2rayN").Should().BeTrue();
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ public class WebRestartCoordinatorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task NativeSelfContainedRestartUsesDetachedBackgroundChildCommand()
|
public async Task NativeSelfContainedRestartUsesDetachedBackgroundChildCommand()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var command = WebReplacementCommand.Create(
|
var command = WebReplacementCommand.Create(
|
||||||
"/usr/bin/setsid",
|
"/usr/bin/setsid",
|
||||||
@@ -29,7 +32,10 @@ public class WebRestartCoordinatorTests
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task FrameworkDependentRestartPreservesDotnetDllAndHostArguments()
|
public async Task FrameworkDependentRestartPreservesDotnetDllAndHostArguments()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsLinux()) return;
|
if (!OperatingSystem.IsLinux())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var processPath = "/usr/bin/dotnet";
|
var processPath = "/usr/bin/dotnet";
|
||||||
var entryPoint = WebReplacementCommand.GetManagedEntryPoint(processPath, ["/opt/v2rayN/v2rayN.WebAPI.dll", "--urls"]);
|
var entryPoint = WebReplacementCommand.GetManagedEntryPoint(processPath, ["/opt/v2rayN/v2rayN.WebAPI.dll", "--urls"]);
|
||||||
@@ -376,7 +382,11 @@ public class WebRestartCoordinatorTests
|
|||||||
|
|
||||||
public void Advance(TimeSpan duration)
|
public void Advance(TimeSpan duration)
|
||||||
{
|
{
|
||||||
if (duration < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(duration));
|
if (duration < TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(duration));
|
||||||
|
}
|
||||||
|
|
||||||
_timestamp += duration.Ticks;
|
_timestamp += duration.Ticks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,8 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Hosting.Server;
|
using Microsoft.AspNetCore.Hosting.Server;
|
||||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ServiceLib.Enums;
|
|
||||||
using ServiceLib.Handler.Builder;
|
using ServiceLib.Handler.Builder;
|
||||||
using ServiceLib.Models.Configs;
|
|
||||||
using ServiceLib.Models.CoreConfigs;
|
|
||||||
using ServiceLib.Models.Entities;
|
|
||||||
using v2rayN.WebAPI.Api;
|
using v2rayN.WebAPI.Api;
|
||||||
using v2rayN.WebAPI.Contracts;
|
using v2rayN.WebAPI.Contracts;
|
||||||
using v2rayN.WebAPI.Security;
|
using v2rayN.WebAPI.Security;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.IO.Compression;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -97,7 +97,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
.Should().BeTrue();
|
.Should().BeTrue();
|
||||||
|
|
||||||
var wrongRidRejected = false;
|
var wrongRidRejected = false;
|
||||||
try { _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-unknown64"); }
|
try
|
||||||
|
{ _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-unknown64"); }
|
||||||
catch (InvalidDataException) { wrongRidRejected = true; }
|
catch (InvalidDataException) { wrongRidRejected = true; }
|
||||||
await wrongRidRejected.Should().BeTrue();
|
await wrongRidRejected.Should().BeTrue();
|
||||||
|
|
||||||
@@ -131,7 +132,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
|
|
||||||
await WebUpdatePackageStager.RequirePackage(parsed, "linux-x64").Rid.Should().BeEqualTo("linux-x64");
|
await WebUpdatePackageStager.RequirePackage(parsed, "linux-x64").Rid.Should().BeEqualTo("linux-x64");
|
||||||
var unknownRejected = false;
|
var unknownRejected = false;
|
||||||
try { _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-future64"); }
|
try
|
||||||
|
{ _ = WebUpdatePackageStager.RequirePackage(parsed, "linux-future64"); }
|
||||||
catch (InvalidDataException) { unknownRejected = true; }
|
catch (InvalidDataException) { unknownRejected = true; }
|
||||||
await unknownRejected.Should().BeTrue();
|
await unknownRejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -152,7 +154,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
var mismatched = Path.Combine(directory.Path, $"elf-wrong-{rid}");
|
var mismatched = Path.Combine(directory.Path, $"elf-wrong-{rid}");
|
||||||
await File.WriteAllBytesAsync(mismatched, BuildElfImage(machine == 62 ? 183 : 62));
|
await File.WriteAllBytesAsync(mismatched, BuildElfImage(machine == 62 ? 183 : 62));
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); }
|
try
|
||||||
|
{ WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); }
|
||||||
catch (InvalidDataException) { rejected = true; }
|
catch (InvalidDataException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -166,7 +169,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
var mismatched = Path.Combine(directory.Path, $"pe-wrong-{rid}.exe");
|
var mismatched = Path.Combine(directory.Path, $"pe-wrong-{rid}.exe");
|
||||||
await File.WriteAllBytesAsync(mismatched, BuildPeImage((ushort)(machine == 0x8664 ? 0xAA64 : 0x8664)));
|
await File.WriteAllBytesAsync(mismatched, BuildPeImage((ushort)(machine == 0x8664 ? 0xAA64 : 0x8664)));
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); }
|
try
|
||||||
|
{ WebUpdatePackageStager.RequireNativeExecutable(mismatched, rid); }
|
||||||
catch (InvalidDataException) { rejected = true; }
|
catch (InvalidDataException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -345,7 +349,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
{
|
{
|
||||||
await File.WriteAllBytesAsync(path, image);
|
await File.WriteAllBytesAsync(path, image);
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { WebUpdatePackageStager.RequireNativeExecutable(path, "linux-x64"); }
|
try
|
||||||
|
{ WebUpdatePackageStager.RequireNativeExecutable(path, "linux-x64"); }
|
||||||
catch (InvalidDataException) { rejected = true; }
|
catch (InvalidDataException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -368,7 +373,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
{
|
{
|
||||||
await File.WriteAllBytesAsync(path, image);
|
await File.WriteAllBytesAsync(path, image);
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { WebUpdatePackageStager.RequireNativeExecutable(path, "win-x64"); }
|
try
|
||||||
|
{ WebUpdatePackageStager.RequireNativeExecutable(path, "win-x64"); }
|
||||||
catch (InvalidDataException) { rejected = true; }
|
catch (InvalidDataException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@@ -439,7 +445,8 @@ public class WebUpdatePackageStagerTests
|
|||||||
private static async Task ExpectInvalidDataAsync(Func<Task<WebUpdatePackageIdentity>> action)
|
private static async Task ExpectInvalidDataAsync(Func<Task<WebUpdatePackageIdentity>> action)
|
||||||
{
|
{
|
||||||
var rejected = false;
|
var rejected = false;
|
||||||
try { _ = await action(); }
|
try
|
||||||
|
{ _ = await action(); }
|
||||||
catch (InvalidDataException) { rejected = true; }
|
catch (InvalidDataException) { rejected = true; }
|
||||||
await rejected.Should().BeTrue();
|
await rejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,12 +90,14 @@ public class WebUpdateReleaseSelectionTests
|
|||||||
public async Task InvalidReleaseIndexesAreRejected()
|
public async Task InvalidReleaseIndexesAreRejected()
|
||||||
{
|
{
|
||||||
var malformedRejected = false;
|
var malformedRejected = false;
|
||||||
try { _ = V2rayRuntime.ParseWebReleaseCandidates("{ not json", allowPrerelease: false); }
|
try
|
||||||
|
{ _ = V2rayRuntime.ParseWebReleaseCandidates("{ not json", allowPrerelease: false); }
|
||||||
catch (JsonException) { malformedRejected = true; }
|
catch (JsonException) { malformedRejected = true; }
|
||||||
await malformedRejected.Should().BeTrue();
|
await malformedRejected.Should().BeTrue();
|
||||||
|
|
||||||
var nonArrayRejected = false;
|
var nonArrayRejected = false;
|
||||||
try { _ = V2rayRuntime.ParseWebReleaseCandidates("{}", allowPrerelease: false); }
|
try
|
||||||
|
{ _ = V2rayRuntime.ParseWebReleaseCandidates("{}", allowPrerelease: false); }
|
||||||
catch (InvalidDataException) { nonArrayRejected = true; }
|
catch (InvalidDataException) { nonArrayRejected = true; }
|
||||||
await nonArrayRejected.Should().BeTrue();
|
await nonArrayRejected.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sizeItem = ConfigTextManager.GetWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name);
|
var sizeItem = ConfigTextManager.Instance.GetWindowSizeItem(GetType().Name);
|
||||||
if (sizeItem == null)
|
if (sizeItem == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -32,7 +32,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveWindowSizeItem(ConfigTextManager.Instance.Config, GetType().Name, Width, Height);
|
ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -345,15 +345,15 @@ public partial class MainWindow
|
|||||||
|
|
||||||
private void StorageUI()
|
private void StorageUI()
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveWindowSizeItem(_config, GetType().Name, Width, Height);
|
ConfigTextManager.Instance.SaveWindowSizeItem(GetType().Name, Width, Height);
|
||||||
|
|
||||||
if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal)
|
if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Horizontal)
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveMainGirdHeight(_config, gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth);
|
ConfigTextManager.Instance.SaveMainGirdHeight(gridMain.ColumnDefinitions[0].ActualWidth, gridMain.ColumnDefinitions[2].ActualWidth);
|
||||||
}
|
}
|
||||||
else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical)
|
else if (_config.UiItem.MainGirdOrientation == EGirdOrientation.Vertical)
|
||||||
{
|
{
|
||||||
ConfigTextManager.SaveMainGirdHeight(_config, gridMain1.RowDefinitions[0].ActualHeight, gridMain1.RowDefinitions[2].ActualHeight);
|
ConfigTextManager.Instance.SaveMainGirdHeight(gridMain1.RowDefinitions[0].ActualHeight, gridMain1.RowDefinitions[2].ActualHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user