Compare commits

...

8 commits

Author SHA1 Message Date
DHR60
24a5847acc
Merge 224f9b1c95 into 22f0d04f01 2025-10-04 04:07:12 +00:00
DHR60
224f9b1c95 Remove unnecessary checks 2025-10-04 12:07:03 +08:00
DHR60
25f8730021 Refactor 2025-10-04 12:03:06 +08:00
2dust
22f0d04f01 Fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8060
2025-10-03 14:13:03 +08:00
2dust
d7c5161431 Optimize and improve
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-02 19:55:49 +08:00
2dust
12cc09d0c9 Bug fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-01 20:17:26 +08:00
2dust
5b12c36da5 Optimize and improve, encapsulate ProcessService 2025-10-01 19:49:28 +08:00
DHR60
e970372a9f
Fix some minor UI bugs (#8053) 2025-10-01 16:47:22 +08:00
32 changed files with 456 additions and 518 deletions

View file

@ -67,116 +67,4 @@ public static class ProcUtils
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
} }
public static async Task ProcessKill(int pid)
{
try
{
await ProcessKill(Process.GetProcessById(pid), false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public static async Task ProcessKill(Process? proc, bool review)
{
if (proc is null)
{
return;
}
GetProcessKeyInfo(proc, review, out var procId, out var fileName, out var processName);
try
{
if (Utils.IsNonWindows())
{
proc?.Kill(true);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Kill();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Close();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Dispose();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await Task.Delay(300);
await ProcessKillByKeyInfo(review, procId, fileName, processName);
}
private static void GetProcessKeyInfo(Process? proc, bool review, out int? procId, out string? fileName, out string? processName)
{
procId = null;
fileName = null;
processName = null;
if (!review)
{
return;
}
try
{
procId = proc?.Id;
fileName = proc?.MainModule?.FileName;
processName = proc?.ProcessName;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private static async Task ProcessKillByKeyInfo(bool review, int? procId, string? fileName, string? processName)
{
if (review && procId != null && fileName != null)
{
try
{
var lstProc = Process.GetProcessesByName(processName);
foreach (var proc2 in lstProc)
{
if (proc2.Id == procId)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job, procId");
await ProcessKill(proc2, false);
}
if (proc2.MainModule != null && proc2.MainModule?.FileName == fileName)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job, fileName");
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
}
} }

View file

@ -85,13 +85,19 @@ public class Utils
/// Base64 Encode /// Base64 Encode
/// </summary> /// </summary>
/// <param name="plainText"></param> /// <param name="plainText"></param>
/// <param name="removePadding"></param>
/// <returns></returns> /// <returns></returns>
public static string Base64Encode(string plainText) public static string Base64Encode(string plainText, bool removePadding = false)
{ {
try try
{ {
var plainTextBytes = Encoding.UTF8.GetBytes(plainText); var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes); var base64 = Convert.ToBase64String(plainTextBytes);
if (removePadding)
{
base64 = base64.TrimEnd('=');
}
return base64;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -112,7 +118,7 @@ public class Utils
{ {
if (plainText.IsNullOrEmpty()) if (plainText.IsNullOrEmpty())
{ {
return ""; return string.Empty;
} }
plainText = plainText.Trim() plainText = plainText.Trim()
@ -947,7 +953,7 @@ public class Utils
if (SetUnixFileMode(fileName)) if (SetUnixFileMode(fileName))
{ {
Logging.SaveLog($"Successfully set the file execution permission, {fileName}"); Logging.SaveLog($"Successfully set the file execution permission, {fileName}");
return ""; return string.Empty;
} }
if (fileName.Contains(' ')) if (fileName.Contains(' '))

View file

@ -7,11 +7,11 @@ namespace ServiceLib.Common;
* http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net * http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
*/ */
public sealed class Job : IDisposable public sealed class WindowsJob : IDisposable
{ {
private IntPtr handle = IntPtr.Zero; private IntPtr handle = IntPtr.Zero;
public Job() public WindowsJob()
{ {
handle = CreateJobObject(IntPtr.Zero, null); handle = CreateJobObject(IntPtr.Zero, null);
var extendedInfoPtr = IntPtr.Zero; var extendedInfoPtr = IntPtr.Zero;
@ -94,7 +94,7 @@ namespace ServiceLib.Common;
} }
} }
~Job() ~WindowsJob()
{ {
Dispose(false); Dispose(false);
} }

View file

@ -1257,35 +1257,7 @@ public static class ConfigHandler
var tun2SocksAddress = node.Address; var tun2SocksAddress = node.Address;
if (node.ConfigType > EConfigType.Group) if (node.ConfigType > EConfigType.Group)
{ {
static async Task<List<string>> GetChildNodeAddressesAsync(string parentIndexId) var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList();
{
var childAddresses = new List<string>();
if (!ProfileGroupItemManager.Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType > EConfigType.Group)
{
var subAddresses = await GetChildNodeAddressesAsync(childNode.IndexId);
childAddresses.AddRange(subAddresses);
}
}
return childAddresses;
}
var lstAddresses = await GetChildNodeAddressesAsync(node.IndexId);
if (lstAddresses.Count > 0) if (lstAddresses.Count > 0)
{ {
tun2SocksAddress = Utils.List2String(lstAddresses); tun2SocksAddress = Utils.List2String(lstAddresses);

View file

@ -27,7 +27,7 @@ public class FmtHandler
catch (Exception ex) catch (Exception ex)
{ {
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
return ""; return string.Empty;
} }
} }

View file

@ -42,7 +42,7 @@ public class ShadowsocksFmt : BaseFmt
// item.port); // item.port);
//url = Utile.Base64Encode(url); //url = Utile.Base64Encode(url);
//new Sip002 //new Sip002
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}"); var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark); return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark);
} }

View file

@ -33,7 +33,7 @@ public class SocksFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
//new //new
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}"); var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark); return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark);
} }

View file

@ -8,7 +8,6 @@ public sealed class AppManager
private Config _config; private Config _config;
private int? _statePort; private int? _statePort;
private int? _statePort2; private int? _statePort2;
private Job? _processJob;
public static AppManager Instance => _instance.Value; public static AppManager Instance => _instance.Value;
public Config Config => _config; public Config Config => _config;
@ -100,7 +99,6 @@ public sealed class AppManager
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo(); await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop(); await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close(); StatisticsManager.Instance.Close();
@ -138,21 +136,6 @@ public sealed class AppManager
return localPort + (int)protocol; return localPort + (int)protocol;
} }
public void AddProcess(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch
{
}
}
}
#endregion Config #endregion Config
#region SqliteHelper #region SqliteHelper

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Text; using System.Text;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
@ -31,7 +30,7 @@ public class CoreAdminManager
await _updateFunc?.Invoke(notify, msg); await _updateFunc?.Invoke(notify, msg);
} }
public async Task<Process?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath) public async Task<ProcessService?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("#!/bin/bash"); sb.AppendLine("#!/bin/bash");
@ -39,50 +38,25 @@ public class CoreAdminManager
sb.AppendLine($"sudo -S {cmdLine}"); sb.AppendLine($"sudo -S {cmdLine}");
var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
Process proc = new() var procService = new ProcessService(
{ fileName: shFilePath,
StartInfo = new() arguments: "",
{ workingDirectory: Utils.GetBinConfigPath(),
FileName = shFilePath, displayLog: true,
Arguments = "", redirectInput: true,
WorkingDirectory = Utils.GetBinConfigPath(), environmentVars: null,
UseShellExecute = false, updateFunc: _updateFunc
RedirectStandardInput = true, );
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
}
};
void dataHandler(object sender, DataReceivedEventArgs e) await procService.StartAsync(AppManager.Instance.LinuxSudoPwd);
{
if (e.Data.IsNotEmpty())
{
_ = UpdateFunc(false, e.Data + Environment.NewLine);
}
}
proc.OutputDataReceived += dataHandler; if (procService is null or { HasExited: true })
proc.ErrorDataReceived += dataHandler;
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
await Task.Delay(10);
await proc.StandardInput.WriteLineAsync(AppManager.Instance.LinuxSudoPwd);
await Task.Delay(100);
if (proc is null or { HasExited: true })
{ {
throw new Exception(ResUI.FailedToRunCore); throw new Exception(ResUI.FailedToRunCore);
} }
_linuxSudoPid = procService.Id;
_linuxSudoPid = proc.Id; return procService;
return proc;
} }
public async Task KillProcessAsLinuxSudo() public async Task KillProcessAsLinuxSudo()

View file

@ -1,6 +1,3 @@
using System.Diagnostics;
using System.Text;
namespace ServiceLib.Manager; namespace ServiceLib.Manager;
/// <summary> /// <summary>
@ -11,8 +8,9 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new()); private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value; public static CoreManager Instance => _instance.Value;
private Config _config; private Config _config;
private Process? _process; private WindowsJob? _processJob;
private Process? _processPre; private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false; private bool _linuxSudo = false;
private Func<bool, string, Task>? _updateFunc; private Func<bool, string, Task>? _updateFunc;
private const string _tag = "CoreHandler"; private const string _tag = "CoreHandler";
@ -89,13 +87,13 @@ public class CoreManager
await CoreStart(node); await CoreStart(node);
await CoreStartPreService(node); await CoreStartPreService(node);
if (_process != null) if (_processService != null)
{ {
await UpdateFunc(true, $"{node.GetSummary()}"); await UpdateFunc(true, $"{node.GetSummary()}");
} }
} }
public async Task<int> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds) public async Task<ProcessService?> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
{ {
var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray; var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray;
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
@ -104,28 +102,22 @@ public class CoreManager
await UpdateFunc(false, result.Msg); await UpdateFunc(false, result.Msg);
if (result.Success != true) if (result.Success != true)
{ {
return -1; return null;
} }
await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await UpdateFunc(false, configPath); await UpdateFunc(false, configPath);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false); return await RunProcess(coreInfo, fileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
} }
public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem) public async Task<ProcessService?> LoadCoreConfigSpeedtest(ServerTestItem testItem)
{ {
var node = await AppManager.Instance.GetProfileItem(testItem.IndexId); var node = await AppManager.Instance.GetProfileItem(testItem.IndexId);
if (node is null) if (node is null)
{ {
return -1; return null;
} }
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
@ -133,18 +125,12 @@ public class CoreManager
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath);
if (result.Success != true) if (result.Success != true)
{ {
return -1; return null;
} }
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false); return await RunProcess(coreInfo, fileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
} }
public async Task CoreStop() public async Task CoreStop()
@ -157,16 +143,18 @@ public class CoreManager
_linuxSudo = false; _linuxSudo = false;
} }
if (_process != null) if (_processService != null)
{ {
await ProcUtils.ProcessKill(_process, Utils.IsWindows()); await _processService.StopAsync();
_process = null; _processService.Dispose();
_processService = null;
} }
if (_processPre != null) if (_processPreService != null)
{ {
await ProcUtils.ProcessKill(_processPre, Utils.IsWindows()); await _processPreService.StopAsync();
_processPre = null; _processPreService.Dispose();
_processPreService = null;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -188,12 +176,12 @@ public class CoreManager
{ {
return; return;
} }
_process = proc; _processService = proc;
} }
private async Task CoreStartPreService(ProfileItem node) private async Task CoreStartPreService(ProfileItem node)
{ {
if (_process != null && !_process.HasExited) if (_processService != null && !_processService.HasExited)
{ {
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType); var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType);
@ -210,7 +198,7 @@ public class CoreManager
{ {
return; return;
} }
_processPre = proc; _processPreService = proc;
} }
} }
} }
@ -225,7 +213,7 @@ public class CoreManager
#region Process #region Process
private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
{ {
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
@ -256,55 +244,48 @@ public class CoreManager
} }
} }
private async Task<Process?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog) private async Task<ProcessService?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog)
{ {
Process proc = new() var environmentVars = new Dictionary<string, string>();
{
StartInfo = new()
{
FileName = fileName,
Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
WorkingDirectory = Utils.GetBinConfigPath(),
UseShellExecute = false,
RedirectStandardOutput = displayLog,
RedirectStandardError = displayLog,
CreateNoWindow = true,
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
}
};
foreach (var kv in coreInfo.Environment) foreach (var kv in coreInfo.Environment)
{ {
proc.StartInfo.Environment[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath); environmentVars[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath);
} }
if (displayLog) var procService = new ProcessService(
{ fileName: fileName,
void dataHandler(object sender, DataReceivedEventArgs e) arguments: string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
{ workingDirectory: Utils.GetBinConfigPath(),
if (e.Data.IsNotEmpty()) displayLog: displayLog,
{ redirectInput: false,
_ = UpdateFunc(false, e.Data + Environment.NewLine); environmentVars: environmentVars,
} updateFunc: _updateFunc
} );
proc.OutputDataReceived += dataHandler;
proc.ErrorDataReceived += dataHandler;
}
proc.Start();
if (displayLog) await procService.StartAsync();
{
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
}
await Task.Delay(100); await Task.Delay(100);
AppManager.Instance.AddProcess(proc.Handle);
if (proc is null or { HasExited: true }) if (procService is null or { HasExited: true })
{ {
throw new Exception(ResUI.FailedToRunCore); throw new Exception(ResUI.FailedToRunCore);
} }
return proc; AddProcessJob(procService.Handle);
return procService;
}
private void AddProcessJob(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch { }
}
} }
#endregion Process #endregion Process

View file

@ -164,4 +164,113 @@ public class ProfileGroupItemManager
Logging.SaveLog(_tag, ex); Logging.SaveLog(_tag, ex);
} }
} }
#region Helper
public static bool HasCycle(string? indexId)
{
return HasCycle(indexId, new HashSet<string>(), new HashSet<string>());
}
public static bool HasCycle(string? indexId, HashSet<string> visited, HashSet<string> stack)
{
if (indexId.IsNullOrEmpty())
return false;
if (stack.Contains(indexId))
return true;
if (visited.Contains(indexId))
return false;
visited.Add(indexId);
stack.Add(indexId);
Instance.TryGet(indexId, out var groupItem);
if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty())
{
return false;
}
var childIds = Utils.String2List(groupItem.ChildItems)
.Where(p => !string.IsNullOrEmpty(p))
.ToList();
foreach (var child in childIds)
{
if (HasCycle(child, visited, stack))
{
return true;
}
}
stack.Remove(indexId);
return false;
}
public static async Task<(List<ProfileItem> Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId)
{
Instance.TryGet(indexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return (new List<ProfileItem>(), profileGroupItem);
}
var items = await GetChildProfileItems(profileGroupItem);
return (items, profileGroupItem);
}
public static async Task<List<ProfileItem>> GetChildProfileItems(ProfileGroupItem? group)
{
if (group == null || group.ChildItems.IsNullOrEmpty())
{
return new();
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null &&
p.IsValid() &&
p.ConfigType != EConfigType.Custom
)
.ToList();
return childProfiles;
}
public static async Task<HashSet<string>> GetAllChildDomainAddresses(string parentIndexId)
{
// include grand children
var childAddresses = new HashSet<string>();
if (!Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType > EConfigType.Group)
{
var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
}
}
return childAddresses;
}
#endregion Helper
} }

View file

@ -35,7 +35,6 @@ public class TaskManager
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo(); await ProfileExManager.Instance.SaveTo();
await ProfileGroupItemManager.Instance.SaveTo();
} }
//Execute once 1 hour //Execute once 1 hour

View file

@ -11,48 +11,4 @@ public class ProfileGroupItem
public string ChildItems { get; set; } public string ChildItems { get; set; }
public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing; public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing;
public bool HasCycle()
{
return HasCycle(new HashSet<string>(), new HashSet<string>());
}
public bool HasCycle(HashSet<string> visited, HashSet<string> stack)
{
if (string.IsNullOrEmpty(ParentIndexId))
return false;
if (stack.Contains(ParentIndexId))
return true;
if (visited.Contains(ParentIndexId))
return false;
visited.Add(ParentIndexId);
stack.Add(ParentIndexId);
if (string.IsNullOrEmpty(ChildItems))
{
return false;
}
var childIds = Utils.String2List(ChildItems)
.Where(p => !string.IsNullOrEmpty(p))
.ToList();
var childProfiles = childIds.Select(ProfileGroupItemManager.Instance.GetOrDefault)//这里是内存访问
.Where(p => p != null)
.ToList();
foreach (var child in childProfiles)
{
if (child.HasCycle(visited, stack))
{
return true;
}
}
stack.Remove(ParentIndexId);
return false;
}
} }

View file

@ -109,42 +109,6 @@ public class ProfileItem : ReactiveObject
return true; return true;
} }
public async Task<bool> HasCycle(HashSet<string> visited, HashSet<string> stack)
{
if (ConfigType < EConfigType.Group)
return false;
if (stack.Contains(IndexId))
return true;
if (visited.Contains(IndexId))
return false;
visited.Add(IndexId);
stack.Add(IndexId);
if (ProfileGroupItemManager.Instance.TryGet(IndexId, out var group)
&& !group.ChildItems.IsNullOrEmpty())
{
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p => p != null)
.ToList();
foreach (var child in childProfiles)
{
if (await child.HasCycle(visited, stack))
return true;
}
}
stack.Remove(IndexId);
return false;
}
#endregion function #endregion function
[PrimaryKey] [PrimaryKey]

View file

@ -3057,6 +3057,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Bootstrap DNS (sing-box) 的本地化字符串。
/// </summary>
public static string TbSBBootstrapDNS {
get {
return ResourceManager.GetString("TbSBBootstrapDNS", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。 /// 查找类似 sing-box Direct Resolution Strategy 的本地化字符串。
/// </summary> /// </summary>
@ -3076,16 +3085,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 sing-box DoH Resolver Server 的本地化字符串。 /// 查找类似 Fallback DNS Resolution, Require IP 的本地化字符串。
/// </summary>
public static string TbSBDoHResolverServer {
get {
return ResourceManager.GetString("TbSBDoHResolverServer", resourceCulture);
}
}
/// <summary>
/// 查找类似 Fallback DNS Resolution, Suggest IP 的本地化字符串。
/// </summary> /// </summary>
public static string TbSBFallbackDNSResolve { public static string TbSBFallbackDNSResolve {
get { get {

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Fallback DNS Resolution, Require IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Fallback DNS Resolution, Require IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Fallback DNS Resolution, Require IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>

View file

@ -1425,11 +1425,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Разрешать домены для исходящих соединений</value> <value>Разрешать домены для исходящих соединений</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>Сервер DoH-резолвера (sing-box)</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Резервное DNS-разрешение (рекомендуется указывать IP)</value> <value>Fallback DNS Resolution, Require IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>Стратегия резолвинга Freedom (Xray)</value> <value>Стратегия резолвинга Freedom (Xray)</value>

View file

@ -1422,11 +1422,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>解析出站域名</value> <value>解析出站域名</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH 解析服务器</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>兜底解析其他 DNS 域名,建议设为 ip</value> <value>回退 DNS 解析,需指定为 IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray freedom 解析策略</value> <value>xray freedom 解析策略</value>

View file

@ -1422,11 +1422,11 @@
<data name="TbSBOutboundDomainResolve" xml:space="preserve"> <data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value> <value>Resolve Outbound Domains</value>
</data> </data>
<data name="TbSBDoHResolverServer" xml:space="preserve"> <data name="TbSBBootstrapDNS" xml:space="preserve">
<value>sing-box DoH Resolver Server</value> <value>Bootstrap DNS (sing-box)</value>
</data> </data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve"> <data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value> <value>Fallback DNS Resolution, Require IP</value>
</data> </data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve"> <data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value> <value>xray Freedom Resolution Strategy</value>

View file

@ -212,33 +212,13 @@ public partial class CoreConfigSingboxService
{ {
return -1; return -1;
} }
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle) if (hasCycle)
{ {
return -1; return -1;
} }
// remove custom nodes var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
// remove group nodes for proxy chain
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null
&& p.IsValid()
&& p.ConfigType != EConfigType.Custom
&& (node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group)
)
.ToList();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
return -1; return -1;
@ -514,16 +494,7 @@ public partial class CoreConfigSingboxService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{ {
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
continue; continue;
@ -698,16 +669,7 @@ public partial class CoreConfigSingboxService
continue; continue;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{ {
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
continue; continue;

View file

@ -1,5 +1,3 @@
using ServiceLib.Models;
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
@ -490,34 +488,13 @@ public partial class CoreConfigV2rayService
{ {
return -1; return -1;
} }
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (profileGroupItem is null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return -1;
}
var hasCycle = profileGroupItem.HasCycle();
//var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle) if (hasCycle)
{ {
return -1; return -1;
} }
// remove custom nodes var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
// remove group nodes for proxy chain
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null &&
p.IsValid() &&
p.ConfigType != EConfigType.Custom &&
(node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group)
)
.ToList();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
return -1; return -1;
@ -654,16 +631,7 @@ public partial class CoreConfigV2rayService
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{ {
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
continue; continue;
@ -814,16 +782,7 @@ public partial class CoreConfigV2rayService
continue; continue;
if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain) if (node.ConfigType is EConfigType.PolicyGroup or EConfigType.ProxyChain)
{ {
ProfileGroupItemManager.Instance.TryGet(node.IndexId, out var profileGroupItem); var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
continue;
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
)).Where(p => p != null).ToList();
if (childProfiles.Count <= 0) if (childProfiles.Count <= 0)
{ {
continue; continue;

View file

@ -0,0 +1,183 @@
using System.Diagnostics;
using System.Text;
namespace ServiceLib.Services;
public class ProcessService : IDisposable
{
private readonly Process _process;
private readonly Func<bool, string, Task>? _updateFunc;
private bool _isDisposed;
public int Id => _process.Id;
public IntPtr Handle => _process.Handle;
public bool HasExited => _process.HasExited;
public ProcessService(
string fileName,
string arguments,
string workingDirectory,
bool displayLog,
bool redirectInput,
Dictionary<string, string>? environmentVars,
Func<bool, string, Task>? updateFunc)
{
_updateFunc = updateFunc;
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardInput = redirectInput,
RedirectStandardOutput = displayLog,
RedirectStandardError = displayLog,
CreateNoWindow = true,
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
},
EnableRaisingEvents = true
};
if (environmentVars != null)
{
foreach (var kv in environmentVars)
{
_process.StartInfo.Environment[kv.Key] = kv.Value;
}
}
if (displayLog)
{
RegisterEventHandlers();
}
}
public async Task StartAsync(string pwd = null)
{
_process.Start();
if (_process.StartInfo.RedirectStandardOutput)
{
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
}
if (_process.StartInfo.RedirectStandardInput)
{
await Task.Delay(10);
await _process.StandardInput.WriteLineAsync(pwd);
}
}
public async Task StopAsync()
{
if (_process.HasExited)
{
return;
}
try
{
if (_process.StartInfo.RedirectStandardOutput)
{
try
{
_process.CancelOutputRead();
}
catch { }
try
{
_process.CancelErrorRead();
}
catch { }
}
try
{
if (Utils.IsNonWindows())
{
_process.Kill(true);
}
}
catch { }
try
{
_process.Kill();
}
catch { }
await Task.Delay(100);
}
catch (Exception ex)
{
await _updateFunc?.Invoke(true, ex.Message);
}
}
private void RegisterEventHandlers()
{
void dataHandler(object sender, DataReceivedEventArgs e)
{
if (e.Data.IsNotEmpty())
{
_ = _updateFunc?.Invoke(false, e.Data + Environment.NewLine);
}
}
_process.OutputDataReceived += dataHandler;
_process.ErrorDataReceived += dataHandler;
_process.Exited += (s, e) =>
{
try
{
_process.OutputDataReceived -= dataHandler;
_process.ErrorDataReceived -= dataHandler;
}
catch
{
}
};
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
try
{
if (!_process.HasExited)
{
try
{
_process.CancelOutputRead();
}
catch { }
try
{
_process.CancelErrorRead();
}
catch { }
_process.Kill();
}
_process.Dispose();
}
catch (Exception ex)
{
_updateFunc?.Invoke(true, ex.Message);
}
_isDisposed = true;
GC.SuppressFinalize(this);
}
}

View file

@ -182,11 +182,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey) private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
{ {
var pid = -1; ProcessService processService = null;
try try
{ {
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds); processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
if (pid < 0) if (processService is null)
{ {
return false; return false;
} }
@ -216,10 +216,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
finally finally
{ {
if (pid > 0) await processService?.StopAsync();
{
await ProcUtils.ProcessKill(pid);
}
} }
return true; return true;
} }
@ -244,11 +241,11 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
tasks.Add(Task.Run(async () => tasks.Add(Task.Run(async () =>
{ {
var pid = -1; ProcessService processService = null;
try try
{ {
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it); processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (pid < 0) if (processService is null)
{ {
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore); await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
} }
@ -275,10 +272,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
} }
finally finally
{ {
if (pid > 0) await processService?.StopAsync();
{
await ProcUtils.ProcessKill(pid);
}
concurrencySemaphore.Release(); concurrencySemaphore.Release();
} }
})); }));

View file

@ -1,4 +1,5 @@
using System.Reactive; using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Fody.Helpers; using ReactiveUI.Fody.Helpers;
@ -32,6 +33,8 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public bool SBCustomDNSEnableCompatible { get; set; } [Reactive] public bool SBCustomDNSEnableCompatible { get; set; }
[ObservableAsProperty] public bool IsSimpleDNSEnabled { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; }
@ -55,6 +58,10 @@ public class DNSSettingViewModel : MyReactiveObject
await Task.CompletedTask; await Task.CompletedTask;
}); });
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Select(x => !(x.Item1 && x.Item2))
.ToPropertyEx(this, x => x.IsSimpleDNSEnabled);
_ = Init(); _ = Init();
} }

View file

@ -37,6 +37,7 @@
<TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid <Grid
x:Name="gridBasicDNSSettings"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
ColumnDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
@ -103,7 +104,7 @@
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbSBDoHResolverServer}" /> Text="{x:Static resx:ResUI.TbSBBootstrapDNS}" />
<ctrls:AutoCompleteBox <ctrls:AutoCompleteBox
x:Name="cmbSBFinalResolverDNS" x:Name="cmbSBFinalResolverDNS"
Grid.Row="4" Grid.Row="4"
@ -187,6 +188,7 @@
<TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid <Grid
x:Name="gridAdvancedDNSSettings"
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
ColumnDefinitions="Auto,Auto,*" ColumnDefinitions="Auto,Auto,*"
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*"> RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,*">

View file

@ -1,4 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using ReactiveUI; using ReactiveUI;
@ -67,16 +68,14 @@ public partial class DNSSettingWindow : WindowBase<DNSSettingViewModel>
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables);
this.WhenAnyValue( this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
x => x.ViewModel.RayCustomDNSEnableCompatible, .Select(b => !b)
x => x.ViewModel.SBCustomDNSEnableCompatible, .BindTo(this.FindControl<TextBlock>("txtBasicDNSSettingsInvalid"), t => t.IsVisible);
(ray, sb) => ray && sb this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
).BindTo(this.FindControl<TextBlock>("txtBasicDNSSettingsInvalid"), t => t.IsVisible); .Select(b => !b)
this.WhenAnyValue( .BindTo(this.FindControl<TextBlock>("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible);
x => x.ViewModel.RayCustomDNSEnableCompatible, this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
x => x.ViewModel.SBCustomDNSEnableCompatible, this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
(ray, sb) => ray && sb
).BindTo(this.FindControl<TextBlock>("txtAdvancedDNSSettingsInvalid"), t => t.IsVisible);
}); });
} }

View file

@ -75,6 +75,7 @@
Width="{StaticResource IconButtonWidth}" Width="{StaticResource IconButtonWidth}"
Height="{StaticResource IconButtonHeight}" Height="{StaticResource IconButtonHeight}"
Margin="{StaticResource MarginLr8}" Margin="{StaticResource MarginLr8}"
HorizontalAlignment="Left"
Theme="{DynamicResource BorderlessButton}"> Theme="{DynamicResource BorderlessButton}">
<Button.Content> <Button.Content>
<PathIcon Data="{StaticResource building_more}" Foreground="{DynamicResource ButtonDefaultTertiaryForeground}" /> <PathIcon Data="{StaticResource building_more}" Foreground="{DynamicResource ButtonDefaultTertiaryForeground}" />
@ -208,8 +209,8 @@
Grid.Row="9" Grid.Row="9"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" Click="BtnSelectPrevProfile_Click"
Click="BtnSelectPrevProfile_Click" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="10" Grid.Row="10"
@ -228,8 +229,8 @@
Grid.Row="10" Grid.Row="10"
Grid.Column="2" Grid.Column="2"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
Content="{x:Static resx:ResUI.TbSelectProfile}" Click="BtnSelectNextProfile_Click"
Click="BtnSelectNextProfile_Click" /> Content="{x:Static resx:ResUI.TbSelectProfile}" />
<TextBlock <TextBlock
Grid.Row="11" Grid.Row="11"

View file

@ -41,7 +41,7 @@
<TabControl HorizontalContentAlignment="Left"> <TabControl HorizontalContentAlignment="Left">
<TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThBasicDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid Margin="{StaticResource Margin8}"> <Grid x:Name="gridBasicDNSSettings" Margin="{StaticResource Margin8}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -131,7 +131,7 @@
Margin="{StaticResource Margin8}" Margin="{StaticResource Margin8}"
VerticalAlignment="Center" VerticalAlignment="Center"
Style="{StaticResource ToolbarTextBlock}" Style="{StaticResource ToolbarTextBlock}"
Text="{x:Static resx:ResUI.TbSBDoHResolverServer}" /> Text="{x:Static resx:ResUI.TbSBBootstrapDNS}" />
<ComboBox <ComboBox
x:Name="cmbSBFinalResolverDNS" x:Name="cmbSBFinalResolverDNS"
Grid.Row="4" Grid.Row="4"
@ -222,7 +222,7 @@
</TabItem> </TabItem>
<TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}"> <TabItem Header="{x:Static resx:ResUI.ThAdvancedDNSSettings}">
<ScrollViewer VerticalScrollBarVisibility="Visible"> <ScrollViewer VerticalScrollBarVisibility="Visible">
<Grid Margin="{StaticResource Margin8}"> <Grid x:Name="gridAdvancedDNSSettings" Margin="{StaticResource Margin8}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />

View file

@ -1,4 +1,5 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Windows; using System.Windows;
using ReactiveUI; using ReactiveUI;
@ -65,18 +66,16 @@ public partial class DNSSettingWindow
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4V2rayCompatibleCmd, v => v.btnImportDefConfig4V2rayCompatible).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ImportDefConfig4SingboxCompatibleCmd, v => v.btnImportDefConfig4SingboxCompatible).DisposeWith(disposables);
this.WhenAnyValue( this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
x => x.ViewModel.RayCustomDNSEnableCompatible, .Select(b => b ? Visibility.Collapsed : Visibility.Visible)
x => x.ViewModel.SBCustomDNSEnableCompatible,
(ray, sb) => ray && sb ? Visibility.Visible : Visibility.Collapsed)
.BindTo(this, x => x.txtBasicDNSSettingsInvalid.Visibility) .BindTo(this, x => x.txtBasicDNSSettingsInvalid.Visibility)
.DisposeWith(disposables); .DisposeWith(disposables);
this.WhenAnyValue( this.WhenAnyValue(x => x.ViewModel.IsSimpleDNSEnabled)
x => x.ViewModel.RayCustomDNSEnableCompatible, .Select(b => b ? Visibility.Collapsed : Visibility.Visible)
x => x.ViewModel.SBCustomDNSEnableCompatible,
(ray, sb) => ray && sb ? Visibility.Visible : Visibility.Collapsed)
.BindTo(this, x => x.txtAdvancedDNSSettingsInvalid.Visibility) .BindTo(this, x => x.txtAdvancedDNSSettingsInvalid.Visibility)
.DisposeWith(disposables); .DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridBasicDNSSettings.IsEnabled).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSimpleDNSEnabled, v => v.gridAdvancedDNSSettings.IsEnabled).DisposeWith(disposables);
}); });
WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme); WindowsUtils.SetDarkBorder(this, AppManager.Instance.Config.UiItem.CurrentTheme);
} }

View file

@ -117,7 +117,7 @@
<materialDesign:PopupBox <materialDesign:PopupBox
Grid.Row="2" Grid.Row="2"
Grid.Column="2" Grid.Column="2"
HorizontalAlignment="Right" HorizontalAlignment="Left"
StaysOpen="True" StaysOpen="True"
Style="{StaticResource MaterialDesignToolForegroundPopupBox}"> Style="{StaticResource MaterialDesignToolForegroundPopupBox}">
<StackPanel> <StackPanel>