v2rayN/v2rayN/ServiceLib/Manager/AppManager.cs

668 lines
22 KiB
C#
Raw Normal View History

2025-08-17 08:52:51 +00:00
namespace ServiceLib.Manager;
2025-08-17 09:31:55 +00:00
public sealed class AppManager
2022-02-24 12:45:24 +00:00
{
#region Property
2025-08-17 09:31:55 +00:00
private static readonly Lazy<AppManager> _instance = new(() => new());
private Config _config;
private int? _statePort;
private int? _statePort2;
2025-08-17 09:31:55 +00:00
public static AppManager Instance => _instance.Value;
public Config Config => _config;
public int StatePort
2022-02-24 12:45:24 +00:00
{
get
{
_statePort ??= Utils.GetFreePort(GetLocalPort(EInboundProtocol.api));
return _statePort.Value;
}
}
public int StatePort2
{
get
{
_statePort2 ??= Utils.GetFreePort(GetLocalPort(EInboundProtocol.api2));
return _statePort2.Value + (_config.TunModeItem.EnableTun ? 1 : 0);
}
}
public string LinuxSudoPwd { get; set; }
public bool ShowInTaskbar { get; set; }
public ECoreType RunningCoreType { get; set; }
public bool IsRunningCore(ECoreType type)
{
switch (type)
{
case ECoreType.Xray when RunningCoreType is ECoreType.Xray or ECoreType.v2fly or ECoreType.v2fly_v5:
case ECoreType.sing_box when RunningCoreType is ECoreType.sing_box or ECoreType.mihomo:
return true;
default:
return false;
}
}
#endregion Property
2025-09-02 09:12:38 +00:00
#region App
2024-10-07 01:51:41 +00:00
public bool InitApp()
{
Tighten platform annotations and finalize SQLite native sync This commit refines the platform-aware bits of the .NET 10 dependency modernization stack added earlier in this PR. It introduces no new NuGet packages and does not change any pinned versions: dotnet list package --outdated against v2rayN.sln returns only the eight Avalonia 12.x packages, all of which are explicitly out of scope for this PR. ServiceLib/Common/Utils.cs: - Annotate IsLinux() with [SupportedOSPlatformGuard("linux")] and IsMacOS() with [SupportedOSPlatformGuard("macos")] so the analyzer can narrow the platform context inside Linux/macOS guards in the same way it already does for Utils.IsWindows(). - Rewrite GetSystemHosts() as a cross-platform helper. Previously it unconditionally tried to read C:\Windows\System32\drivers\etc\{hosts, hosts.ics}, which silently returned an empty dictionary on Linux/macOS. The new implementation reads /etc/hosts on those platforms, fixing UseSystemHosts=true being a silent no-op outside Windows. Behavior on Windows is unchanged (still merges hosts and hosts.ics). ServiceLib/Handler/AutoStartupHandler.cs: - Mark the Linux helpers (ClearTaskLinux, SetTaskLinux, GetHomePathLinux) with [SupportedOSPlatform("linux")] and the macOS helpers (ClearTaskOSX, SetTaskOSX, GetLaunchAgentPathMacOS, GenerateLaunchAgentPlist) with [SupportedOSPlatform("macos")] for symmetry with the Windows helpers in the same file. The dispatch in UpdateTask is already gated by Utils.IsWindows / IsLinux / IsMacOS, whose new SupportedOSPlatformGuard attributes make these annotations enforceable by the analyzer. ServiceLib/Handler/SysProxy/ProxySettingLinux.cs and ServiceLib/Handler/SysProxy/ProxySettingOSX.cs: - Mark the classes with [SupportedOSPlatform("linux")] and [SupportedOSPlatform("macos")] respectively. Both are reachable only through SysProxyHandler.UpdateSysProxy under the corresponding platform guard. ServiceLib/Manager/AppManager.cs: - Call SQLitePCL.Batteries_V2.Init() at the top of InitApp() in addition to the existing call in SQLiteHelper's static constructor. The call is idempotent, so it cooperates with that static constructor; the duplication is intentional defense-in-depth so that any future code path which reaches the database without going through SQLiteHelper (background services, tests, future utilities) will still find an initialized native provider rather than throwing "Library not initialized" at runtime. ServiceLib/Manager/CoreManager.cs: - Add a clarifying comment above the existing [SupportedOSPlatform("windows")] field-level attribute on _processJob, explaining that the attribute narrows the analyzer's platform context for every read/assign of the field and that runtime safety is preserved by the IsWindows() guard inside AddProcessJob. No code change. package-rhel-riscv.sh: - Promote the previously hard-coded SQLite amalgamation version (sqlite_year / sqlite_ver) inside build_sqlite_native_riscv64 to two env-overridable variables (SQLITE_AMALGAMATION_YEAR and SQLITE_AMALGAMATION_VER) declared at the top of the script next to SKIA_VER and HARFBUZZ_VER. - Set the defaults to 2025 / 3500400, which corresponds to SQLite 3.50.4 and matches SourceGear.sqlite3 3.50.4.5 used on the other RIDs (x64/arm64/macOS). Previously the script downloaded SQLite 3.53.0 amalgamation on RISC-V, which left RISC-V users on a different SQLite minor version than every other platform shipped from this repository. The accompanying comment block documents the mapping rule between SourceGear.sqlite3 X.Y.Z.W and the SQLITE_AMALGAMATION_* values, so the next bump can be done in two coordinated places (this script and Directory.Packages.props) without drift. The version can still be overridden per-build via environment variables for ad-hoc testing on RISC-V. Verification ------------ - dotnet restore + dotnet build v2rayN.sln -c Release: 0 errors, 1 warning (CS8625 in GlobalHotKeys submodule, pre-existing, out-of-scope of this PR). - dotnet test ServiceLib.Tests: 41/41 passed. - dotnet publish v2rayN.Desktop.csproj for linux-x64 and osx-x64, and v2rayN.csproj for win-x64: all succeed with no new warnings. - dotnet list package --outdated against v2rayN.sln: only the eight Avalonia 12.x packages appear; none are eligible while this PR stays on the Avalonia 11.x branch.
2026-05-02 22:28:51 +00:00
// Initialize the native SQLite provider explicitly before any database
// access. The call is idempotent, so it cooperates with the static
// constructor in SQLiteHelper. Having it here as well ensures that any
// future code path which reaches the database without going through
// SQLiteHelper still works (e.g. background services or tests).
SQLitePCL.Batteries_V2.Init();
if (Utils.HasWritePermission() == false)
2024-10-07 01:51:41 +00:00
{
Environment.SetEnvironmentVariable(Global.LocalAppData, "1", EnvironmentVariableTarget.Process);
2024-10-07 01:51:41 +00:00
}
2024-02-08 06:01:33 +00:00
Logging.Setup();
var config = ConfigHandler.LoadConfig();
if (config == null)
2023-01-01 11:42:01 +00:00
{
return false;
2023-01-01 11:42:01 +00:00
}
_config = config;
Thread.CurrentThread.CurrentUICulture = new(_config.UiItem.CurrentLanguage);
2023-01-01 11:42:01 +00:00
//Under Win10
if (Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)
{
Environment.SetEnvironmentVariable("DOTNET_EnableWriteXorExecute", "0", EnvironmentVariableTarget.User);
}
SQLiteHelper.Instance.CreateTable<SubItem>();
SQLiteHelper.Instance.CreateTable<ProfileItem>();
SQLiteHelper.Instance.CreateTable<ServerStatItem>();
SQLiteHelper.Instance.CreateTable<RoutingItem>();
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
SQLiteHelper.Instance.CreateTable<DNSItem>();
SQLiteHelper.Instance.CreateTable<FullConfigTemplateItem>();
#pragma warning disable CS0618
SQLiteHelper.Instance.CreateTable<ProfileGroupItem>();
#pragma warning restore CS0618
return true;
}
2022-03-21 12:20:29 +00:00
public bool InitComponents()
{
Logging.SaveLog($"v2rayN start up | {Utils.GetRuntimeInfo()}");
Logging.LoggingEnabled(_config.GuiItem.EnableLog);
//First determine the port value
_ = StatePort;
_ = StatePort2;
Task.Run(async () =>
{
await MigrateProfileExtra();
}).Wait();
return true;
}
2024-02-08 06:01:33 +00:00
public bool Reset()
{
_statePort = null;
_statePort2 = null;
return true;
}
2024-02-08 06:01:33 +00:00
2025-09-02 09:12:38 +00:00
public async Task AppExitAsync(bool needShutdown)
{
try
{
Logging.SaveLog("AppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
2025-09-25 02:56:10 +00:00
AppEvents.AppExitRequested.Publish();
2025-09-02 09:12:38 +00:00
await Task.Delay(50); //Wait for AppExitRequested to be processed
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close();
Logging.SaveLog("AppExitAsync End");
}
catch { }
finally
{
if (needShutdown)
{
Shutdown(false);
}
}
}
public void Shutdown(bool byUser)
{
2025-09-25 02:56:10 +00:00
AppEvents.ShutdownRequested.Publish(byUser);
2025-09-02 09:12:38 +00:00
}
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await AppManager.Instance.AppExitAsync(true);
}
2025-09-02 09:12:38 +00:00
#endregion App
2023-01-01 11:42:01 +00:00
#region Config
2023-04-14 12:49:36 +00:00
public int GetLocalPort(EInboundProtocol protocol)
{
var localPort = _config.Inbound.FirstOrDefault(t => t.Protocol == nameof(EInboundProtocol.socks))?.LocalPort ?? 10808;
return localPort + (int)protocol;
}
2023-01-01 11:42:01 +00:00
#endregion Config
#region SqliteHelper
public async Task<List<SubItem>?> SubItems()
{
return await SQLiteHelper.Instance.TableAsync<SubItem>().OrderBy(t => t.Sort).ToListAsync();
}
public async Task<SubItem?> GetSubItem(string? subid)
{
return await SQLiteHelper.Instance.TableAsync<SubItem>().FirstOrDefaultAsync(t => t.Id == subid);
}
public async Task<List<ProfileItem>?> ProfileItems(string subid)
{
if (subid.IsNullOrEmpty())
{
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().ToListAsync();
}
else
2022-03-21 12:20:29 +00:00
{
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid).ToListAsync();
}
}
public async Task<List<string>?> ProfileItemIndexes(string subid)
{
return (await ProfileItems(subid))?.Select(t => t.IndexId)?.ToList();
}
public async Task<List<ProfileItemModel>?> ProfileModels(string subid, string filter)
{
var sql = @$"select a.IndexId
,a.ConfigType
,a.Remarks
,a.Address
,a.Port
,a.Network
,a.StreamSecurity
,a.Subid
,b.remarks as subRemarks
2023-01-01 11:42:01 +00:00
from ProfileItem a
2023-04-14 12:49:36 +00:00
left join SubItem b on a.subid = b.id
2023-01-01 11:42:01 +00:00
where 1=1 ";
if (subid.IsNotEmpty())
2023-01-01 11:42:01 +00:00
{
sql += $" and a.subid = '{subid}'";
2023-01-01 11:42:01 +00:00
}
if (filter.IsNotEmpty())
2023-12-23 12:57:31 +00:00
{
if (filter.Contains('\''))
2023-12-23 12:57:31 +00:00
{
filter = filter.Replace("'", "");
2023-12-23 12:57:31 +00:00
}
sql += string.Format(" and (a.remarks like '%{0}%' or a.address like '%{0}%') ", filter);
2023-12-23 12:57:31 +00:00
}
return await SQLiteHelper.Instance.QueryAsync<ProfileItemModel>(sql);
}
2023-04-14 12:49:36 +00:00
public async Task<ProfileItem?> GetProfileItem(string indexId)
{
if (indexId.IsNullOrEmpty())
2023-01-01 11:42:01 +00:00
{
return null;
2023-01-01 11:42:01 +00:00
}
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.IndexId == indexId);
}
2023-01-01 11:42:01 +00:00
public async Task<List<ProfileItem>> GetProfileItemsByIndexIds(IEnumerable<string> indexIds)
{
var ids = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (ids.Count == 0)
{
return [];
}
return await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => ids.Contains(it.IndexId))
.ToListAsync();
}
public async Task<Dictionary<string, ProfileItem>> GetProfileItemsByIndexIdsAsMap(IEnumerable<string> indexIds)
{
var items = await GetProfileItemsByIndexIds(indexIds);
return items.ToDictionary(it => it.IndexId);
}
public async Task<List<ProfileItem>> GetProfileItemsOrderedByIndexIds(IEnumerable<string> indexIds)
{
var idList = indexIds.Where(id => !id.IsNullOrEmpty()).Distinct().ToList();
if (idList.Count == 0)
{
return [];
}
var items = await SQLiteHelper.Instance.TableAsync<ProfileItem>()
.Where(it => idList.Contains(it.IndexId))
.ToListAsync();
var itemMap = items.ToDictionary(it => it.IndexId);
return idList.Select(id => itemMap.GetValueOrDefault(id))
.Where(item => item != null)
.ToList();
}
public async Task<ProfileItem?> GetProfileItemViaRemarks(string? remarks)
{
if (remarks.IsNullOrEmpty())
{
return null;
}
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.Remarks == remarks);
}
public async Task<List<RoutingItem>?> RoutingItems()
{
return await SQLiteHelper.Instance.TableAsync<RoutingItem>().OrderBy(t => t.Sort).ToListAsync();
}
public async Task<RoutingItem?> GetRoutingItem(string id)
{
return await SQLiteHelper.Instance.TableAsync<RoutingItem>().FirstOrDefaultAsync(it => it.Id == id);
}
2023-01-01 11:42:01 +00:00
public async Task<List<DNSItem>?> DNSItems()
{
return await SQLiteHelper.Instance.TableAsync<DNSItem>().ToListAsync();
}
2023-01-01 11:42:01 +00:00
public async Task<DNSItem?> GetDNSItem(ECoreType eCoreType)
{
return await SQLiteHelper.Instance.TableAsync<DNSItem>().FirstOrDefaultAsync(it => it.CoreType == eCoreType);
}
public async Task<List<FullConfigTemplateItem>?> FullConfigTemplateItem()
{
return await SQLiteHelper.Instance.TableAsync<FullConfigTemplateItem>().ToListAsync();
}
public async Task<FullConfigTemplateItem?> GetFullConfigTemplateItem(ECoreType eCoreType)
{
return await SQLiteHelper.Instance.TableAsync<FullConfigTemplateItem>().FirstOrDefaultAsync(it => it.CoreType == eCoreType);
}
#pragma warning disable CS0618
2026-04-05 09:18:28 +00:00
public async Task MigrateProfileExtra()
{
await MigrateProfileExtraGroupV2ToV3();
await MigrateProfileExtraV2ToV3();
await MigrateProfileTransportV3ToV4();
}
private async Task MigrateProfileExtraV2ToV3()
{
const int pageSize = 100;
var offset = 0;
while (true)
{
var sql = $"SELECT * FROM ProfileItem " +
$"WHERE ConfigVersion < 3 " +
$"AND ConfigType NOT IN ({(int)EConfigType.PolicyGroup}, {(int)EConfigType.ProxyChain}) " +
$"LIMIT {pageSize} OFFSET {offset}";
var batch = await SQLiteHelper.Instance.QueryAsync<ProfileItem>(sql);
if (batch is null || batch.Count == 0)
{
break;
}
var batchSuccessCount = await MigrateProfileExtraV2ToV3Sub(batch);
// Only increment offset by the number of failed items that remain in the result set
// Successfully updated items are automatically excluded from future queries due to ConfigVersion = 3
offset += batch.Count - batchSuccessCount;
}
//await ProfileGroupItemManager.Instance.ClearAll();
}
private async Task MigrateProfileTransportV3ToV4()
{
const int pageSize = 100;
var offset = 0;
while (true)
{
var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion = 3 LIMIT {pageSize} OFFSET {offset}";
var batch = await SQLiteHelper.Instance.QueryAsync<ProfileItem>(sql);
if (batch is null || batch.Count == 0)
{
break;
}
var updateProfileItems = new List<ProfileItem>();
foreach (var item in batch)
{
try
{
if (item.Network == Global.RawNetworkAlias)
{
item.Network = nameof(ETransport.raw);
}
var transport = item.GetTransportExtra();
var network = item.GetNetwork();
switch (network)
{
case nameof(ETransport.raw):
transport = transport with
{
RawHeaderType = item.HeaderType.NullIfEmpty(),
Host = item.RequestHost.NullIfEmpty(),
Path = item.Path.NullIfEmpty(),
};
break;
case nameof(ETransport.ws):
case nameof(ETransport.httpupgrade):
transport = transport with
{
Host = item.RequestHost.NullIfEmpty(),
Path = item.Path.NullIfEmpty(),
};
break;
case nameof(ETransport.xhttp):
transport = transport with
{
Host = item.RequestHost.NullIfEmpty(),
Path = item.Path.NullIfEmpty(),
XhttpMode = item.HeaderType.NullIfEmpty(),
XhttpExtra = item.Extra.NullIfEmpty(),
};
break;
case nameof(ETransport.grpc):
transport = transport with
{
GrpcAuthority = item.RequestHost.NullIfEmpty(),
GrpcServiceName = item.Path.NullIfEmpty(),
GrpcMode = item.HeaderType.NullIfEmpty(),
};
break;
case nameof(ETransport.kcp):
transport = transport with
{
KcpHeaderType = item.HeaderType.NullIfEmpty(),
KcpSeed = item.Path.NullIfEmpty(),
};
break;
default:
item.Network = Global.DefaultNetwork;
transport = transport with
{
RawHeaderType = item.HeaderType.NullIfEmpty(),
Host = item.RequestHost.NullIfEmpty(),
};
break;
}
item.SetTransportExtra(transport);
item.ConfigVersion = 4;
updateProfileItems.Add(item);
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileTransportV3ToV4 Error: {ex}");
}
}
if (updateProfileItems.Count > 0)
{
try
{
var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems);
offset += batch.Count - count;
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileTransportV3ToV4 update error: {ex}");
offset += batch.Count;
}
}
else
{
offset += batch.Count;
}
}
}
private async Task<int> MigrateProfileExtraV2ToV3Sub(List<ProfileItem> batch)
{
var updateProfileItems = new List<ProfileItem>();
foreach (var item in batch)
{
try
{
var extra = item.GetProtocolExtra();
switch (item.ConfigType)
{
case EConfigType.Shadowsocks:
extra = extra with { SsMethod = item.Security.NullIfEmpty() };
break;
case EConfigType.VMess:
extra = extra with
{
AlterId = item.AlterId.ToString(),
VmessSecurity = item.Security.NullIfEmpty(),
};
break;
case EConfigType.VLESS:
extra = extra with
{
Flow = item.Flow.NullIfEmpty(),
VlessEncryption = item.Security,
};
break;
case EConfigType.Hysteria2:
extra = extra with
{
SalamanderPass = item.Path.NullIfEmpty(),
Ports = item.Ports.NullIfEmpty(),
UpMbps = _config.HysteriaItem.UpMbps,
DownMbps = _config.HysteriaItem.DownMbps,
HopInterval = _config.HysteriaItem.HopInterval.ToString(),
};
break;
case EConfigType.TUIC:
extra = extra with { CongestionControl = item.HeaderType.NullIfEmpty(), };
item.Username = item.Id;
item.Id = item.Security;
item.Password = item.Security;
break;
case EConfigType.HTTP:
case EConfigType.SOCKS:
item.Username = item.Security;
break;
case EConfigType.WireGuard:
extra = extra with
{
WgPublicKey = item.PublicKey.NullIfEmpty(),
WgInterfaceAddress = item.RequestHost.NullIfEmpty(),
WgReserved = item.Path.NullIfEmpty(),
WgMtu = int.TryParse(item.ShortId, out var mtu) ? mtu : 1280
};
break;
}
item.SetProtocolExtra(extra);
item.Password = item.Id;
item.ConfigVersion = 3;
updateProfileItems.Add(item);
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtra Error: {ex}");
}
}
if (updateProfileItems.Count > 0)
{
try
{
var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems);
return count;
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtraGroup update error: {ex}");
return 0;
}
}
else
{
return 0;
}
}
private async Task<bool> MigrateProfileExtraGroupV2ToV3()
{
var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
var groupItems = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!));
var sql = $"SELECT * FROM ProfileItem WHERE ConfigVersion < 3 AND ConfigType IN ({(int)EConfigType.PolicyGroup}, {(int)EConfigType.ProxyChain})";
var items = await SQLiteHelper.Instance.QueryAsync<ProfileItem>(sql);
if (items is null || items.Count == 0)
{
Logging.SaveLog("MigrateProfileExtraGroup: No items to migrate.");
return true;
}
Logging.SaveLog($"MigrateProfileExtraGroup: Found {items.Count} group items to migrate.");
var updateProfileItems = new List<ProfileItem>();
foreach (var item in items)
{
try
{
var extra = item.GetProtocolExtra();
extra = extra with { GroupType = nameof(item.ConfigType) };
groupItems.TryGetValue(item.IndexId, out var groupItem);
if (groupItem != null && !groupItem.NotHasChild())
{
extra = extra with
{
ChildItems = groupItem.ChildItems,
SubChildItems = groupItem.SubChildItems,
Filter = groupItem.Filter,
MultipleLoad = groupItem.MultipleLoad,
};
}
item.SetProtocolExtra(extra);
item.ConfigVersion = 3;
updateProfileItems.Add(item);
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtraGroup item error [{item.IndexId}]: {ex}");
}
}
if (updateProfileItems.Count > 0)
{
try
{
var count = await SQLiteHelper.Instance.UpdateAllAsync(updateProfileItems);
Logging.SaveLog($"MigrateProfileExtraGroup: Successfully updated {updateProfileItems.Count} items.");
return updateProfileItems.Count == count;
}
catch (Exception ex)
{
Logging.SaveLog($"MigrateProfileExtraGroup update error: {ex}");
return false;
}
}
return true;
//await ProfileGroupItemManager.Instance.ClearAll();
}
2026-04-05 09:18:28 +00:00
#pragma warning restore CS0618
#endregion SqliteHelper
#region Core Type
public List<string> GetShadowsocksSecurities(ProfileItem profileItem)
{
var coreType = GetCoreType(profileItem, EConfigType.Shadowsocks);
switch (coreType)
2023-01-01 11:42:01 +00:00
{
case ECoreType.v2fly:
return Global.SsSecurities;
2022-03-21 12:20:29 +00:00
case ECoreType.Xray:
return Global.SsSecuritiesInXray;
2024-01-23 04:30:11 +00:00
case ECoreType.sing_box:
return Global.SsSecuritiesInSingbox;
2022-03-21 12:20:29 +00:00
}
return Global.SsSecuritiesInSingbox;
}
2022-03-21 12:20:29 +00:00
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType)
{
if (profileItem?.CoreType != null)
2022-03-21 12:20:29 +00:00
{
return (ECoreType)profileItem.CoreType;
2022-03-21 12:20:29 +00:00
}
2022-03-28 10:54:05 +00:00
var item = _config.CoreTypeItem?.FirstOrDefault(it => it.ConfigType == eConfigType);
return item?.CoreType ?? ECoreType.Xray;
2022-02-24 12:45:24 +00:00
}
#endregion Core Type
}