mirror of
https://github.com/BeyondDimension/SteamTools.git
synced 2026-10-27 04:41:15 +08:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7366e2c904 | ||
|
|
5215e3d613 | ||
|
|
1c454123b8 | ||
|
|
18080285bf | ||
|
|
1049355476 | ||
|
|
dc74c2ae10 | ||
|
|
b32cb40949 | ||
|
|
98324d95a8 | ||
|
|
bed2fbb98d | ||
|
|
dd09e37048 | ||
|
|
d359392fc0 | ||
|
|
4601987cb4 | ||
|
|
7c21e8454e | ||
|
|
6f60f10fc8 | ||
|
|
41b51f4e0a | ||
|
|
83952d6d76 | ||
|
|
44b4a3b05e | ||
|
|
ac91872815 | ||
|
|
2795dbac5f | ||
|
|
4e2fe256ed | ||
|
|
83c515542c | ||
|
|
0c9f25a304 | ||
|
|
63be2ba037 | ||
|
|
4c464623b8 |
+8
-1
@@ -212,7 +212,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WinAuth", "ref\WinAuth\src\
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BD.WTTS.Client.Tools.OpenSourceLibraryList", "src\BD.WTTS.Client.Tools.OpenSourceLibraryList\BD.WTTS.Client.Tools.OpenSourceLibraryList.csproj", "{64F0101B-F54C-442E-BA70-738ACF72247F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BD.WTTS.Client.Plugins.SteamIdleCard", "src\BD.WTTS.Client.Plugins.SteamIdleCard\BD.WTTS.Client.Plugins.SteamIdleCard.csproj", "{80B51074-9473-4635-9B8F-4EB00B9ADCE7}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BD.WTTS.Client.Plugins.SteamIdleCard", "src\BD.WTTS.Client.Plugins.SteamIdleCard\BD.WTTS.Client.Plugins.SteamIdleCard.csproj", "{80B51074-9473-4635-9B8F-4EB00B9ADCE7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BD.WTTS.Client.Tools.HostsTest", "src\BD.WTTS.Client.Tools.HostsTest\BD.WTTS.Client.Tools.HostsTest.csproj", "{13A7EBC3-7574-41BF-9657-CEC2053E54EC}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -412,6 +414,10 @@ Global
|
||||
{80B51074-9473-4635-9B8F-4EB00B9ADCE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{80B51074-9473-4635-9B8F-4EB00B9ADCE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{80B51074-9473-4635-9B8F-4EB00B9ADCE7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{13A7EBC3-7574-41BF-9657-CEC2053E54EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{13A7EBC3-7574-41BF-9657-CEC2053E54EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{13A7EBC3-7574-41BF-9657-CEC2053E54EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{13A7EBC3-7574-41BF-9657-CEC2053E54EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -477,6 +483,7 @@ Global
|
||||
{3F67183A-CC29-4B60-B534-08C2FB1A6746} = {84FF822C-BF60-493F-A8C8-6A413167E3DA}
|
||||
{64F0101B-F54C-442E-BA70-738ACF72247F} = {D6C8A54D-878F-44A9-857F-6ED01491C7BD}
|
||||
{80B51074-9473-4635-9B8F-4EB00B9ADCE7} = {A5828819-62AB-4B03-A083-8B6ED4915AA3}
|
||||
{13A7EBC3-7574-41BF-9657-CEC2053E54EC} = {D6C8A54D-878F-44A9-857F-6ED01491C7BD}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {567F588F-E95C-4286-8B30-DB3B2200221C}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
run_path=$(dirname "$0")
|
||||
dotnet_path="$run_path/dotnet"
|
||||
dotnet_exec="$dotnet_path/dotnet"
|
||||
export DOTNET_ROOT="$dotnet_path"
|
||||
link_exec="$run_path/Steam++"
|
||||
# 判断符号链接是否存在
|
||||
if [ ! -L "$link_exec" ]; then
|
||||
echo "创建符号链接 $dotnet_exec 到 $link_exec"
|
||||
ln -s "$dotnet_exec" "$link_exec"
|
||||
else
|
||||
echo "符号链接 $link_exec 已存在"
|
||||
fi
|
||||
"$link_exec" "$run_path/assemblies/Steam++.dll" "$@"
|
||||
exit
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
base_path="$HOME/WattToolkit"
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
Install_certutil() {
|
||||
# 判断发行版类型
|
||||
if command -v certutil &>/dev/null; then
|
||||
echo "certutil 工具已安装。"
|
||||
else
|
||||
echo "证书导入以及验证需要使用 certutil 工具。"
|
||||
# 判断包管理器
|
||||
if command -v apt &>/dev/null; then
|
||||
# 使用 apt (Debian/Ubuntu)
|
||||
sudo apt update
|
||||
sudo apt install -y libnss3-tools
|
||||
elif command -v dnf &>/dev/null; then
|
||||
# 使用 dnf (Fedora)
|
||||
sudo dnf install -y nss-tools
|
||||
elif command -v yum &>/dev/null; then
|
||||
# 使用 yum (CentOS/Red Hat)
|
||||
sudo yum install -y nss-tools
|
||||
elif command -v pacman &>/dev/null; then
|
||||
# 使用 pacman (Arch Linux)
|
||||
# sudo pacman -S nss
|
||||
echo "请手动安装 certutil 工具。"
|
||||
exit 1
|
||||
else
|
||||
echo "请手动安装 certutil 工具。"
|
||||
exit 1
|
||||
fi
|
||||
echo "certutil 工具已安装。"
|
||||
fi
|
||||
}
|
||||
certutil_Init() {
|
||||
certutil -d sql:$HOME/.pki/nssdb -L
|
||||
ret=$?
|
||||
if [ $ret -ne 0 ]; then
|
||||
echo "即将初始化 certutil sql:\$HOME/.pki/nssdb"
|
||||
mkdir -p sql:$HOME/.pki
|
||||
mkdir -p sql:$HOME/.pki/nssdb
|
||||
chmod 700 sql:$HOME/.pki/nssdb
|
||||
certutil -d sql:$HOME/.pki/nssdb -N --empty-password
|
||||
else
|
||||
echo "certutil nssdb 正常"
|
||||
fi
|
||||
}
|
||||
Install_certutil
|
||||
certutil_Init
|
||||
+15
-18
@@ -40,6 +40,19 @@ Install_certutil() {
|
||||
echo "certutil 工具已安装。"
|
||||
fi
|
||||
}
|
||||
certutil_Init() {
|
||||
certutil -d sql:$HOME/.pki/nssdb -L
|
||||
ret=$?
|
||||
if [ $ret -ne 0 ]; then
|
||||
echo "即将初始化 certutil sql:\$HOME/.pki/nssdb"
|
||||
mkdir -p sql:$HOME/.pki
|
||||
mkdir -p sql:$HOME/.pki/nssdb
|
||||
chmod 700 sql:$HOME/.pki/nssdb
|
||||
certutil -d sql:$HOME/.pki/nssdb -N --empty-password
|
||||
else
|
||||
echo "certutil nssdb 正常"
|
||||
fi
|
||||
}
|
||||
Install_jq() {
|
||||
# Check if jq is already installed
|
||||
if command -v jq &>/dev/null; then
|
||||
@@ -139,7 +152,7 @@ Get_NewVer() {
|
||||
esac
|
||||
|
||||
# 获取发行版信息
|
||||
read -r os_version <<<"$(cat /etc/os-release | grep -E 'VERSION=' | awk -F'=' '{ print $2 }' | tr -d '"')"
|
||||
read -r os_version <<<"$(cat /etc/os-release | grep -E 'VERSION_ID=' | awk -F'=' '{ print $2 }' | tr -d '"')"
|
||||
|
||||
# 如果 VERSION 为空,则使用 BUILD_ID 填充
|
||||
if [ -z "$os_version" ]; then
|
||||
@@ -270,23 +283,6 @@ Decompression() {
|
||||
else
|
||||
chmod +x "$dotnet_exec"
|
||||
fi
|
||||
rm "$base_path/start.sh"
|
||||
rm "$base_path/Steam++"
|
||||
echo "#!/bin/bash
|
||||
run_path=\$(dirname \"\$0\")
|
||||
dotnet_path=\"\$run_path/dotnet\"
|
||||
dotnet_exec=\"\$dotnet_path/dotnet\"
|
||||
export DOTNET_ROOT=\"\$dotnet_path\"
|
||||
link_exec=\"\$run_path/Steam++\"
|
||||
# 判断符号链接是否存在
|
||||
if [ ! -L \"\$link_exec\" ]; then
|
||||
echo \"创建符号链接 \$dotnet_exec 到 \$link_exec\"
|
||||
ln -s \"\$dotnet_exec\" \"\$link_exec\"
|
||||
else
|
||||
echo \"符号链接 \$link_exec 已存在\"
|
||||
fi
|
||||
\"\$link_exec\" \"\$run_path/assemblies/Steam++.dll\" \"\$@\"
|
||||
exit" >"$base_path/$exec_name.sh"
|
||||
chmod +x "$base_path/$exec_name.sh"
|
||||
}
|
||||
|
||||
@@ -294,6 +290,7 @@ exit" >"$base_path/$exec_name.sh"
|
||||
Install_certutil
|
||||
Install_zenity
|
||||
Install_jq
|
||||
certutil_Init
|
||||
#版本检查更新;
|
||||
Get_NewVer
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
base_path="$HOME/WattToolkit"
|
||||
export LC_ALL=en_US.UTF-8
|
||||
exec_name="Steam++"
|
||||
app_name="Watt Toolkit"
|
||||
AppData="$XDG_DATA_HOME/Steam++"
|
||||
AppData_t="$HOME/.local/share/Steam++"
|
||||
Cache="$XDG_CACHE_HOME/Steam++"
|
||||
Cache_t="$HOME/.cache/Steam+"
|
||||
PROCESS_NAMES=("$exec_name" "$app_name")
|
||||
|
||||
Kill_Process() {
|
||||
# 尝试的次数
|
||||
Kill_MAX_RETRIES=3
|
||||
|
||||
# 循环尝试终止进程
|
||||
for process_name in "${PROCESS_NAMES[@]}"; do
|
||||
kill_retry=1
|
||||
while [ $kill_retry -le $Kill_MAX_RETRIES ]; do
|
||||
pid=$(pgrep "$process_name")
|
||||
if [ -n "$pid" ]; then
|
||||
echo "尝试 $kill_retry: 进程 $process_name 正在运行中。正在终止..."
|
||||
kill $pid
|
||||
sleep 2
|
||||
else
|
||||
break
|
||||
fi
|
||||
kill_retry=$((kill_retry + 1))
|
||||
done
|
||||
done
|
||||
|
||||
for process_name in "${PROCESS_NAMES[@]}"; do
|
||||
# 检查是否成功终止进程
|
||||
if pgrep -x "$process_name" >/dev/null; then
|
||||
echo "无法终止程序 $process_name。尝试次数已达上限。"
|
||||
exit 1
|
||||
else
|
||||
echo "程序 $process_name 已成功终止。"
|
||||
fi
|
||||
done
|
||||
|
||||
}
|
||||
Kill_Process
|
||||
|
||||
zenity --question --text="是否删除用户数据" --width=400
|
||||
# 获取上一个命令的退出码
|
||||
response=$?
|
||||
|
||||
if [ $response -eq 0 ]; then
|
||||
rm -rfq "$AppData" 2>/dev/null
|
||||
rm -rfq "$AppData_t" 2>/dev/null
|
||||
else
|
||||
echo "保留用户数据方便下次安装。"
|
||||
echo "如需删除可手动删除该目录:$AppData 或者 $AppData_t"
|
||||
fi
|
||||
rm -rfq "$Cache" 2>/dev/null
|
||||
rm -rfq "$Cache_t" 2>/dev/null
|
||||
rm -rfq "$base_path" 2>/dev/null
|
||||
rm -rfq "$HOME/Desktop/Watt Toolkit.desktop" 2>/dev/null
|
||||
zenity --info --text="卸载完成!" --width=300
|
||||
+1
-1
Submodule ref/WinAuth updated: e936bae0c9...5a75e1be00
Binary file not shown.
@@ -14,7 +14,7 @@ public static partial class AssemblyInfo
|
||||
/// <summary>
|
||||
/// 预览版本号,范围 1~9
|
||||
/// </summary>
|
||||
const string ver_for_preview = "5";
|
||||
const string ver_for_preview = "7";
|
||||
|
||||
/// <summary>
|
||||
/// RC 版本号,范围 0~9
|
||||
|
||||
@@ -153,7 +153,7 @@ static class Strings
|
||||
public static string Error_IncompatibleOS => GetString(l => l switch
|
||||
{
|
||||
Language.ChineseSimplified => "此应用程序仅兼容 Windows 11 与 Windows 10 版本 1809(OS 内部版本 17763)或更高版本",
|
||||
Language.ChineseTraditional => "此應用程序僅相容 Windows 11 與 Windows 10 版本 1809(OS 內部版本17763)或更高版本",
|
||||
Language.ChineseTraditional => "此應用程序僅相容 Windows 11 與 Windows 10 版本 1809(OS 內部版本 17763)或更高版本",
|
||||
Language.Spanish => "Esta aplicación solo es compatible con Windows 11 y Windows 10 versión 1809 (versión interna del sistema operativo 17763) o superior",
|
||||
Language.Italian => "Questa applicazione è compatibile solo con Windows 11 e Windows 10 versione 1809 (versione interna del sistema operativo 17763) o superiore",
|
||||
Language.Japanese => "このアプリケーションは、Windows 11およびWindows 10 バージョン1809(OS内部バージョン17763)以降にのみ対応しています",
|
||||
|
||||
@@ -200,7 +200,7 @@ sealed partial class Program : Startup
|
||||
WatchTrace.Record("ConfigureDemandServices.Notification");
|
||||
#endif
|
||||
|
||||
if (HasHosts)
|
||||
if (HasHosts || HasIPCRoot)
|
||||
{
|
||||
// hosts 文件助手服务
|
||||
services.AddHostsFileService();
|
||||
|
||||
@@ -269,11 +269,11 @@
|
||||
|
||||
<ScrollViewer
|
||||
Name="ShopsScrollViewer"
|
||||
Margin="0,3,0,20"
|
||||
Margin="0,0,0,20"
|
||||
Padding="0,0,0,15"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Shops}">
|
||||
<ItemsControl Padding="0,3,0,15" ItemsSource="{Binding Shops}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" />
|
||||
@@ -281,55 +281,57 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="spp:ShopRecommendGoodItem">
|
||||
<spp:AppItem
|
||||
Title="{Binding Name}"
|
||||
MaxWidth="260"
|
||||
Classes="Vertical Scale"
|
||||
ClickCommand="{Binding OpenBrowserCommand, Source={x:Static spp:App.Instance}}"
|
||||
Cursor="Hand"
|
||||
Tags="{Binding GoodsAllCategorysText}"
|
||||
ToolTip.Tip="{Binding Brief}">
|
||||
<spp:AppItem.ClickCommandParameter>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding Path="." Source="{x:Static spp:HomePageViewModel.WattGame_Goods_Detail_}" />
|
||||
<CompiledBinding Path="Id" />
|
||||
</MultiBinding>
|
||||
</spp:AppItem.ClickCommandParameter>
|
||||
<spp:AppItem.Image>
|
||||
<spp:Image2 RenderOptions.BitmapInterpolationMode="HighQuality" Source="{Binding Image}" />
|
||||
</spp:AppItem.Image>
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border spp:Animations.EnableAnimations="True">
|
||||
<spp:AppItem
|
||||
Title="{Binding Name}"
|
||||
MaxWidth="260"
|
||||
Classes="Vertical Scale"
|
||||
ClickCommand="{Binding OpenBrowserCommand, Source={x:Static spp:App.Instance}}"
|
||||
Cursor="Hand"
|
||||
Tags="{Binding GoodsAllCategorysText}"
|
||||
ToolTip.Tip="{Binding Brief}">
|
||||
<spp:AppItem.ClickCommandParameter>
|
||||
<MultiBinding Converter="{StaticResource StringFormatConverter}">
|
||||
<CompiledBinding Path="." Source="{x:Static spp:HomePageViewModel.WattGame_Goods_Detail_}" />
|
||||
<CompiledBinding Path="Id" />
|
||||
</MultiBinding>
|
||||
</spp:AppItem.ClickCommandParameter>
|
||||
<spp:AppItem.Image>
|
||||
<spp:Image2 RenderOptions.BitmapInterpolationMode="HighQuality" Source="{Binding Image}" />
|
||||
</spp:AppItem.Image>
|
||||
<spp:AppItem.Status>
|
||||
<Border Classes="Status">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{Binding !!DiscountValue}"
|
||||
Text="{Binding Mktprice, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}"
|
||||
TextDecorations="Strikethrough" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{Binding Price, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<spp:AppItem.ActionButton>
|
||||
<Border
|
||||
Height="24"
|
||||
Padding="8,0"
|
||||
Background="#4c6b22"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
IsVisible="{Binding !!DiscountValue}">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
IsVisible="{Binding !!DiscountValue}"
|
||||
Text="{Binding Mktprice, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}"
|
||||
TextDecorations="Strikethrough" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="14"
|
||||
Text="{Binding Price, Converter={StaticResource StringFormatConverter}, ConverterParameter={x:Static spp:CurrencyCode.CNY}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</spp:AppItem.Status>
|
||||
<spp:AppItem.ActionButton>
|
||||
<Border
|
||||
Height="24"
|
||||
Padding="8,0"
|
||||
Background="#4c6b22"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="4"
|
||||
IsVisible="{Binding !!DiscountValue}">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#beee11"
|
||||
Text="{Binding DiscountValue, StringFormat={}-{0}%}" />
|
||||
</Border>
|
||||
</spp:AppItem.ActionButton>
|
||||
</spp:AppItem>
|
||||
Foreground="#beee11"
|
||||
Text="{Binding DiscountValue, StringFormat={}-{0}%}" />
|
||||
</Border>
|
||||
</spp:AppItem.ActionButton>
|
||||
</spp:AppItem>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -80,7 +80,7 @@ public sealed class ScriptManager : GeneralHttpClientFactory, IScriptManager
|
||||
script.MatchDomainNames = string.IsNullOrEmpty(matchs) ? includes : matchs;
|
||||
// 忽略脚本 Enable 启动标签默认启动
|
||||
//var enable = Regex.Match(userScript, string.Format(DescRegex, "@Enable"), RegexOptions.IgnoreCase).GetValue(s => s.Success == true);
|
||||
script.Disable = true;
|
||||
script.Disable = false;
|
||||
//script.Enable = bool.TryParse(enable, out var e) && e;
|
||||
return script;
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ partial class ProxyService
|
||||
{
|
||||
hosts.TryAdd(IReverseProxyService.Constants.LocalDomain, localhost);
|
||||
}
|
||||
var updateHostsResult = hostsFileService.UpdateHosts(hosts);
|
||||
var updateHostsResult = await hostsFileService.UpdateHosts(hosts);
|
||||
if (updateHostsResult.ResultType != OperationResultType.Success)
|
||||
{
|
||||
if (OperatingSystem2.IsMacOS() || OperatingSystem2.IsLinux())
|
||||
@@ -265,7 +265,7 @@ partial class ProxyService
|
||||
var needClear = hostsFileService.ContainsHostsByTag();
|
||||
if (needClear)
|
||||
{
|
||||
var removeHostsResult = hostsFileService.RemoveHostsByTag();
|
||||
var removeHostsResult = await hostsFileService.RemoveHostsByTag();
|
||||
if (removeHostsResult.ResultType != OperationResultType.Success)
|
||||
{
|
||||
// 退出时候不打开浏览器
|
||||
|
||||
@@ -344,7 +344,7 @@ public sealed partial class ProxyService
|
||||
}
|
||||
}
|
||||
|
||||
public static void OnExitRestoreHosts()
|
||||
public static async ValueTask OnExitRestoreHosts()
|
||||
{
|
||||
var s = Ioc.Get_Nullable<IHostsFileService>();
|
||||
if (s != null)
|
||||
@@ -352,7 +352,7 @@ public sealed partial class ProxyService
|
||||
var needClear = s.ContainsHostsByTag();
|
||||
if (needClear)
|
||||
{
|
||||
s.OnExitRestoreHosts();
|
||||
await s.OnExitRestoreHosts();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,13 +560,9 @@ public sealed partial class ProxyService
|
||||
}
|
||||
#endregion
|
||||
|
||||
public
|
||||
#if WINDOWS
|
||||
async
|
||||
#endif
|
||||
void FixNetwork()
|
||||
public async void FixNetwork()
|
||||
{
|
||||
OnExitRestoreHosts();
|
||||
await OnExitRestoreHosts();
|
||||
|
||||
#if WINDOWS
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed partial class AcceleratorPageViewModel
|
||||
DeleteCertificateCommand = ReactiveCommand.Create(DeleteCertificate_OnClick);
|
||||
EditHostsFileCommand = ReactiveCommand.Create(hostsFileService.OpenFile);
|
||||
OpenHostsDirCommand = ReactiveCommand.Create(hostsFileService.OpenFileDir);
|
||||
ResetHostsFileCommand = ReactiveCommand.Create(hostsFileService.ResetFile);
|
||||
ResetHostsFileCommand = ReactiveCommand.CreateFromTask(hostsFileService.ResetFile);
|
||||
NetworkFixCommand = ReactiveCommand.Create(ProxyService.Current.FixNetwork);
|
||||
TrustCerCommand = ReactiveCommand.Create(TrustCer_OnClick);
|
||||
OpenCertificateDirCommand = ReactiveCommand.Create(() =>
|
||||
|
||||
@@ -542,9 +542,8 @@
|
||||
<spp:AdControl
|
||||
Grid.Column="1"
|
||||
Width="165"
|
||||
MaxHeight="390"
|
||||
Height="390"
|
||||
VerticalAlignment="Top"
|
||||
DockPanel.Dock="Right"
|
||||
Standard="Vertical" />
|
||||
</Grid>
|
||||
</spp:PageBase>
|
||||
|
||||
@@ -463,7 +463,7 @@ public static class AuthenticatorHelper
|
||||
{
|
||||
AvaloniaFilePickerFileTypeFilter fileTypes = new AvaloniaFilePickerFileTypeFilter.Item[] {
|
||||
new($"{fileExtension} Files") {
|
||||
Patterns = new[] { $"*.{fileExtension}", },
|
||||
Patterns = new[] { $"*{fileExtension}", },
|
||||
//MimeTypes
|
||||
//AppleUniformTypeIdentifiers =
|
||||
},
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ public partial class AuthenticatorGeneralImportPageViewModel
|
||||
|
||||
if (AuthType == AuthType.TOTP)
|
||||
{
|
||||
var auth = new GoogleAuthenticator();
|
||||
var auth = new TOTPAuthenticator();
|
||||
auth.Enroll(privateKey);
|
||||
|
||||
auth.HMACType = HMACType;
|
||||
|
||||
@@ -299,7 +299,7 @@ public sealed partial class GameListPageViewModel : TabItemViewModel
|
||||
try
|
||||
{
|
||||
GameLibrarySettings.HideGameList.Add(app.AppId, app.DisplayName);
|
||||
SteamConnectService.Current.SteamApps.Remove(app);
|
||||
SteamConnectService.Current.SteamApps.RemoveKey(app.AppId);
|
||||
|
||||
Toast.Show(ToastIcon.Success, Strings.GameList_HideAppsSuccess);
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@
|
||||
|
||||
<ItemsRepeater ItemsSource="{Binding App.SaveFiles}">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Orientation="Vertical" Spacing="10" />
|
||||
<StackLayout Orientation="Vertical" Spacing="{StaticResource SettingsSpacing}" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type spp:SteamAppSaveFile}">
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Steam++.HostsTest</AssemblyName>
|
||||
<OutputType>Exe</OutputType>
|
||||
<ApplicationIcon>..\..\res\icons\app\v3\Icon.ico</ApplicationIcon>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<DefineConstants>TOOL_HOSTSTEST;__NOT_IMPORT_COMMON__;$(DefineConstants)</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'">
|
||||
<AllowedReferenceRelatedFileExtensions>
|
||||
<!-- Prevent default XML and PDB files copied to output in RELEASE.
|
||||
Only *.allowedextension files will be included, which doesn't exist in my case.
|
||||
-->
|
||||
.allowedextension
|
||||
</AllowedReferenceRelatedFileExtensions>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\AssemblyInfo.Constants.cs">
|
||||
<LinkBase>Properties</LinkBase>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- PackageReference -->
|
||||
<PackageReference Include="PInvoke.AdvApi32" />
|
||||
<PackageReference Include="PInvoke.Kernel32" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\TFM_NETX_WITH_WINDOWS.props" />
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,133 @@
|
||||
// https://learn.microsoft.com/zh-cn/sysinternals/downloads/handle
|
||||
|
||||
var fColor = Console.ForegroundColor;
|
||||
var bColor = Console.BackgroundColor;
|
||||
try
|
||||
{
|
||||
var isProcessElevated = IsProcessElevated(Process.GetCurrentProcess());
|
||||
Console.WriteLine($"当前进程是否为管理员权限:{isProcessElevated}");
|
||||
|
||||
var hostsFilePath = Path.Combine(Environment.SystemDirectory, "drivers", "etc", "hosts");
|
||||
Console.WriteLine($"hosts 文件路径:{hostsFilePath}");
|
||||
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(hostsFilePath);
|
||||
Console.WriteLine("hosts 文件内容:");
|
||||
Console.WriteLine("--------------------------------------------------");
|
||||
Console.WriteLine(text);
|
||||
Console.WriteLine("--------------------------------------------------");
|
||||
|
||||
File.WriteAllText(hostsFilePath, text);
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.BackgroundColor = ConsoleColor.DarkGreen;
|
||||
Console.WriteLine("OK");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.White;
|
||||
Console.BackgroundColor = ConsoleColor.DarkRed;
|
||||
Console.WriteLine("错误:");
|
||||
Console.WriteLine(ex.ToString());
|
||||
|
||||
// https://learn.microsoft.com/zh-cn/sysinternals/downloads/handle
|
||||
Console.WriteLine("查找 hosts 文件被哪个进程占用的命令,需要安装工具:");
|
||||
Console.WriteLine("https://learn.microsoft.com/zh-cn/sysinternals/downloads/handle");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(
|
||||
$"""
|
||||
handle.exe "{hostsFilePath}" /accepteula
|
||||
""");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.ForegroundColor = fColor;
|
||||
Console.BackgroundColor = bColor;
|
||||
Console.WriteLine("键入回车键后退出此程序:");
|
||||
Console.ReadLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定的进程是否以管理员权限运行
|
||||
/// </summary>
|
||||
/// <param name="process"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static unsafe bool IsProcessElevated(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handle = process.Handle;
|
||||
|
||||
// IsPrivilegedProcess
|
||||
// https://github.com/dotnet/runtime/pull/77355/files#diff-1c6f0e5208d48036e96fcc9c0243d93595f3ce16f2d1a50f51ba604d930ca69dR87
|
||||
PInvoke.Kernel32.SafeObjectHandle? token = null;
|
||||
try
|
||||
{
|
||||
if (PInvoke.AdvApi32.OpenProcessToken(handle,
|
||||
PInvoke.AdvApi32.TokenAccessRights.TOKEN_READ,
|
||||
out token))
|
||||
{
|
||||
TOKEN_ELEVATION elevation = default;
|
||||
if (PInvoke.AdvApi32.GetTokenInformation(
|
||||
token,
|
||||
PInvoke.AdvApi32.TOKEN_INFORMATION_CLASS.TokenElevation,
|
||||
&elevation,
|
||||
sizeof(TOKEN_ELEVATION),
|
||||
out _))
|
||||
{
|
||||
return elevation.TokenIsElevated != BOOL.FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
token?.Dispose();
|
||||
}
|
||||
|
||||
var error = Marshal.GetLastPInvokeError();
|
||||
throw new Win32Exception(error);
|
||||
|
||||
//using WindowsIdentity identity = new(handle);
|
||||
//WindowsPrincipal principal = new(identity);
|
||||
//return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
/* “process.Handle”引发了类型“System.ComponentModel.Win32Exception”的异常
|
||||
* Data: {System.Collections.ListDictionaryInternal}
|
||||
* ErrorCode: -2147467259
|
||||
* HResult: -2147467259
|
||||
* HelpLink: null
|
||||
* InnerException: null
|
||||
* Message: "拒绝访问。"
|
||||
* NativeErrorCode: 5
|
||||
* Source: "System.Diagnostics.Process"
|
||||
*/
|
||||
if (ex.NativeErrorCode == 5)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb530717.aspx
|
||||
struct TOKEN_ELEVATION
|
||||
{
|
||||
public BOOL TokenIsElevated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Blittable version of Windows BOOL type. It is convenient in situations where
|
||||
/// manual marshalling is required, or to avoid overhead of regular bool marshalling.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Some Windows APIs return arbitrary integer values although the return type is defined
|
||||
/// as BOOL. It is best to never compare BOOL to TRUE. Always use bResult != BOOL.FALSE
|
||||
/// or bResult == BOOL.FALSE .
|
||||
/// </remarks>
|
||||
enum BOOL : int
|
||||
{
|
||||
FALSE = 0,
|
||||
TRUE = 1,
|
||||
}
|
||||
@@ -262,4 +262,19 @@ interface ICompressedPackageCommand : ICommand
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
static void CreateZipPack(string packPath, IEnumerable<AppPublishFileInfo> files)
|
||||
{
|
||||
using var archive = ZipFile.Open(packPath, ZipArchiveMode.Create);
|
||||
foreach (var file in files)
|
||||
{
|
||||
#if DEBUG
|
||||
Console.WriteLine($"正在压缩:{file.FilePath}");
|
||||
#endif
|
||||
var name = file.RelativePath;
|
||||
if (Path.DirectorySeparatorChar != IOPath.UnixDirectorySeparatorChar)
|
||||
name = name.Replace(Path.DirectorySeparatorChar, IOPath.UnixDirectorySeparatorChar);
|
||||
archive.CreateEntryFromFile(file.FilePath, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,12 @@ interface IDotNetPublishCommand : ICommand
|
||||
var force_sign = new Option<bool>("--force-sign", GetDefForceSign, "Mandatory verification must be digitally signed");
|
||||
var sha256 = new Option<bool>("--sha256", () => true, "Calculate file hash value");
|
||||
var sha384 = new Option<bool>("--sha384", () => true, "Calculate file hash value");
|
||||
var stm_upload = new Option<bool>("--stm-upload", "Steam upload zip file");
|
||||
var command = new Command(commandName, "DotNet publish app")
|
||||
{
|
||||
debug, rids, force_sign, sha256, sha384,
|
||||
debug, rids, force_sign, sha256, sha384, stm_upload,
|
||||
};
|
||||
command.SetHandler(Handler, debug, rids, force_sign, sha256, sha384);
|
||||
command.SetHandler(Handler, debug, rids, force_sign, sha256, sha384, stm_upload);
|
||||
return command;
|
||||
}
|
||||
|
||||
@@ -56,7 +57,7 @@ interface IDotNetPublishCommand : ICommand
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Handler(bool debug, string[] rids, bool force_sign, bool sha256, bool sha384)
|
||||
internal static void Handler(bool debug, string[] rids, bool force_sign, bool sha256, bool sha384, bool stm_upload)
|
||||
{
|
||||
var bgOriginalColor = Console.BackgroundColor;
|
||||
var fgOriginalColor = Console.ForegroundColor;
|
||||
@@ -166,7 +167,10 @@ interface IDotNetPublishCommand : ICommand
|
||||
// 不能使用下划线
|
||||
File.Copy(ico_path, Path.Combine(save_dir_path, "Watt-Toolkit.png"), true);
|
||||
}
|
||||
|
||||
Console.WriteLine("Linux 复制启动 环境检查 卸载脚本");
|
||||
var script_path = Path.Combine(ProjectUtils.ProjPath, "build", "linux");
|
||||
CopyDirectory(script_path, Path.Combine(rootPublishDir, "script"), true);
|
||||
File.Move(Path.Combine(rootPublishDir, "script", "Steam++.sh"), Path.Combine(rootPublishDir, "Steam++.sh"));
|
||||
}
|
||||
|
||||
SetConsoleColor(ConsoleColor.White, ConsoleColor.DarkGreen);
|
||||
@@ -377,6 +381,37 @@ interface IDotNetPublishCommand : ICommand
|
||||
ICompressedPackageCommand.CreateGZipPack(packPath = GetPackPathWithTryDelete(FileEx.TAR_GZ), appPublish.Files);
|
||||
break;
|
||||
}
|
||||
if (stm_upload)
|
||||
{
|
||||
List<AppPublishFileInfo> mainFiles = new();
|
||||
List<AppPublishFileInfo> modulesFiles = new();
|
||||
foreach (var item in appPublish.Files)
|
||||
{
|
||||
if (item.RelativePath.Contains("modules"))
|
||||
{
|
||||
modulesFiles.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
mainFiles.Add(item);
|
||||
}
|
||||
}
|
||||
ICompressedPackageCommand.CreateZipPack(GetPackPathWithTryDelete("_main.stmupload.zip"), mainFiles);
|
||||
|
||||
var query = from module in modulesFiles
|
||||
let split = module.RelativePath.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
let name = split.Length >= 2 ? split[1] : null
|
||||
where name != null
|
||||
group module by name;
|
||||
var items = query.ToArray();
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Key))
|
||||
continue;
|
||||
var files = item.ToArray();
|
||||
ICompressedPackageCommand.CreateZipPack(GetPackPathWithTryDelete($"_{item.Key}.stmupload.zip"), files);
|
||||
}
|
||||
}
|
||||
SetConsoleColor(ConsoleColor.White, ConsoleColor.DarkGreen);
|
||||
Console.Write("创建成功【压缩包】,文件大小:");
|
||||
if (packPath != null)
|
||||
@@ -759,21 +794,21 @@ publish -c {0} -p:OutputType={1} -p:PublishDir=bin\{0}\Publish\win-any -p:Publis
|
||||
const string appconfigFileName = "Steam++.exe.config";
|
||||
|
||||
var rootPublishDir = Path.Combine(publishDir, "..");
|
||||
var cacheFilePath = Path.Combine(ProjectUtils.ProjPath,
|
||||
"res", "windows", "Steam++.apphost");
|
||||
// 使用缓存文件
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
File.Copy(cacheFilePath, Path.Combine(rootPublishDir, "Steam++.exe"));
|
||||
var sourceFileName = Path.Combine(ProjectUtils.ProjPath, "src", "BD.WTTS.Client.AppHost", "App.config");
|
||||
var appconfigContent = File.ReadAllText(sourceFileName);
|
||||
//var cacheFilePath = Path.Combine(ProjectUtils.ProjPath,
|
||||
// "res", "windows", "Steam++.apphost");
|
||||
//// 使用缓存文件
|
||||
//if (File.Exists(cacheFilePath))
|
||||
//{
|
||||
// File.Copy(cacheFilePath, Path.Combine(rootPublishDir, "Steam++.exe"));
|
||||
// var sourceFileName = Path.Combine(ProjectUtils.ProjPath, "src", "BD.WTTS.Client.AppHost", "App.config");
|
||||
// var appconfigContent = File.ReadAllText(sourceFileName);
|
||||
|
||||
var xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(appconfigContent);
|
||||
appconfigContent = xmlDoc.InnerXml;
|
||||
File.WriteAllText(Path.Combine(rootPublishDir, appconfigFileName), appconfigContent);
|
||||
return;
|
||||
}
|
||||
// var xmlDoc = new XmlDocument();
|
||||
// xmlDoc.LoadXml(appconfigContent);
|
||||
// appconfigContent = xmlDoc.InnerXml;
|
||||
// File.WriteAllText(Path.Combine(rootPublishDir, appconfigFileName), appconfigContent);
|
||||
// return;
|
||||
//}
|
||||
|
||||
const string app_host_tfm = "net40"/*"net35"*/; // net35 在 Windows 10 LTSC 上即使 app.config 中配置了 4.x 兼容但依旧会打开设置窗口并且定位在可选功能
|
||||
var configuration = PublishCommandArg.GetConfiguration(debug);
|
||||
|
||||
@@ -3,9 +3,9 @@ namespace BD.WTTS.Client.Tools.Publish.Commands;
|
||||
interface IGenerateCaCertCommand : ICommand
|
||||
{
|
||||
const string commandName = "gcert";
|
||||
const string X500DistinguishedNameValue = $"C=CN, S=Hunan, L=Changsha, O=\u6C5F\u82CF\u84B8\u6C7D\u51E1\u661F\u79D1\u6280\u6709\u9650\u516C\u53F8, OU=\u6280\u672F\u90E8, CN=\u6C5F\u82CF\u84B8\u6C7D\u51E1\u661F\u79D1\u6280\u6709\u9650\u516C\u53F8";
|
||||
const string X500DistinguishedNameValue = $"C=CN, S=Hunan, L=Changsha, O=\u5F90\u5DDE\u7E41\u661F\u7F51\u7EDC\u79D1\u6280\u6709\u9650\u516C\u53F8, OU=\u6280\u672F\u90E8, CN=\u5F90\u5DDE\u7E41\u661F\u7F51\u7EDC\u79D1\u6280\u6709\u9650\u516C\u53F8";
|
||||
|
||||
public const int CertificateValidDays = 3650;
|
||||
public const int CertificateValidDays = 365;
|
||||
|
||||
public const int KEY_SIZE_BITS = 4096;
|
||||
|
||||
@@ -39,9 +39,10 @@ interface IGenerateCaCertCommand : ICommand
|
||||
/// </summary>
|
||||
const string _13Oid = "1.3.6.1.4.1.311.10.3.13";
|
||||
|
||||
static (byte[] pfx, byte[] cer) GenerateCodeSigningCert(string x500DistinguishedNameValue, string password)
|
||||
static (byte[] pfx, byte[] cer, string? csr) GenerateCodeSigningCert(string x500DistinguishedNameValue, string password, bool isCSR = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
string? csr = default;
|
||||
if (!isCSR && string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentOutOfRangeException(nameof(password), password, null);
|
||||
|
||||
// https://learn.microsoft.com/zh-cn/windows/win32/appxpkg/how-to-create-a-package-signing-certificate
|
||||
@@ -73,20 +74,33 @@ interface IGenerateCaCertCommand : ICommand
|
||||
using var cert = request.CreateSelfSigned(notBefore, notAfter);
|
||||
var pfx = cert.Export(X509ContentType.Pfx, password); // 私钥
|
||||
var cer = cert.Export(X509ContentType.Cert); // 公钥
|
||||
return (pfx, cer);
|
||||
|
||||
if (isCSR)
|
||||
{
|
||||
csr = request.CreateSigningRequestPem();
|
||||
}
|
||||
|
||||
return (pfx, cer, csr);
|
||||
}
|
||||
|
||||
internal static void Handler(int type, string path, string cn, string password)
|
||||
{
|
||||
(var pfx, var cer) = type switch
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentOutOfRangeException(nameof(path), path, null);
|
||||
|
||||
(var pfx, var cer, var csr) = type switch
|
||||
{
|
||||
// 生成 BeyondDimension 代码签名证书
|
||||
1 => GenerateCodeSigningCert(X500DistinguishedNameValue, password),
|
||||
1 => GenerateCodeSigningCert(X500DistinguishedNameValue, password, true),
|
||||
// 生成 上传应用商店的 代码签名证书
|
||||
2 => GenerateCodeSigningCert($"CN={cn}", password),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
|
||||
};
|
||||
File.WriteAllBytes(path, pfx);
|
||||
File.WriteAllBytes(path.TrimEnd(".pfx", StringComparison.OrdinalIgnoreCase) + ".cer", cer);
|
||||
if (csr != default)
|
||||
{
|
||||
File.WriteAllText(path.TrimEnd(".pfx", StringComparison.OrdinalIgnoreCase) + ".txt", csr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"BD.WTTS.Client.Tools.Publish": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "run --rids win-x64"
|
||||
"commandLineArgs": "run --rids win-x64 --stm-upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,38 @@ public static class PluginsCore
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
var assemblyLoadContext = DefaultAssemblyLoadContext;
|
||||
if (assemblyLoadContext != this)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = assemblyLoadContext.LoadFromAssemblyName(assemblyName);
|
||||
if (assembly != null)
|
||||
{
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
return base.Load(assemblyName);
|
||||
}
|
||||
}
|
||||
|
||||
static AssemblyLoadContext DefaultAssemblyLoadContext
|
||||
{
|
||||
get
|
||||
{
|
||||
// 在 Windows 上通过自定义 AppHost 程序集加载上下文为 IsolatedComponentLoadContext 而不是 Default
|
||||
var assemblyLoadContext = AssemblyLoadContext.GetLoadContext(typeof(PluginsCore).Assembly);
|
||||
assemblyLoadContext ??= AssemblyLoadContext.Default;
|
||||
return assemblyLoadContext;
|
||||
}
|
||||
}
|
||||
|
||||
static DisablePluginsAssemblyLoadContext disablePluginsAssemblyLoadContext = new();
|
||||
@@ -183,9 +215,7 @@ public static class PluginsCore
|
||||
}
|
||||
else
|
||||
{
|
||||
// 在 Windows 上通过自定义 AppHost 程序集加载上下文为 IsolatedComponentLoadContext 而不是 Default
|
||||
assemblyLoadContext = AssemblyLoadContext.GetLoadContext(typeof(PluginsCore).Assembly);
|
||||
assemblyLoadContext ??= AssemblyLoadContext.Default;
|
||||
assemblyLoadContext = DefaultAssemblyLoadContext;
|
||||
}
|
||||
var assembly = assemblyLoadContext.LoadFromAssemblyPath(assemblyPath);
|
||||
return assembly;
|
||||
@@ -208,7 +238,7 @@ public static class PluginsCore
|
||||
ArchiSteamFarmPlus,
|
||||
Authenticator,
|
||||
GameTools,
|
||||
SteamIdleCard
|
||||
SteamIdleCard,
|
||||
};
|
||||
foreach (var item in modules)
|
||||
{
|
||||
@@ -355,7 +385,8 @@ public static class PluginsCore
|
||||
try
|
||||
{
|
||||
using CompositionHost container = configuration.CreateContainer();
|
||||
foreach (var plugin in container.GetExports<IPlugin>())
|
||||
var exports = container.GetExports<IPlugin>().ToArray();
|
||||
foreach (var plugin in exports)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(plugin.UniqueEnglishName) ||
|
||||
string.Equals(plugin.UniqueEnglishName,
|
||||
|
||||
+2
-2
@@ -3304,7 +3304,7 @@ namespace BD.WTTS.Client.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 HOTP、TOTP通用令牌 的本地化字符串。
|
||||
/// 查找类似 HOTP、TOTP 通用令牌 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string HOTP {
|
||||
get {
|
||||
@@ -3863,7 +3863,7 @@ namespace BD.WTTS.Client.Resources {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找类似 {0}导入 的本地化字符串。
|
||||
/// 查找类似 {0} 导入 的本地化字符串。
|
||||
/// </summary>
|
||||
public static string LocalAuth_Import {
|
||||
get {
|
||||
|
||||
@@ -2314,7 +2314,7 @@
|
||||
<value>Mafile 文件</value>
|
||||
</data>
|
||||
<data name="LocalAuth_Import" xml:space="preserve">
|
||||
<value>{0}导入</value>
|
||||
<value>{0} 导入</value>
|
||||
</data>
|
||||
<data name="LocalAuth_SDAImport" xml:space="preserve">
|
||||
<value>从 Mafile 文件导入令牌</value>
|
||||
@@ -2347,7 +2347,7 @@
|
||||
<value>{0}密钥导入</value>
|
||||
</data>
|
||||
<data name="HOTP" xml:space="preserve">
|
||||
<value>HOTP、TOTP通用令牌</value>
|
||||
<value>HOTP、TOTP 通用令牌</value>
|
||||
</data>
|
||||
<data name="LocalAuth_HOTPImport" xml:space="preserve">
|
||||
<value>添加 HOTP、TOTP 令牌时生成的通用密钥导入令牌</value>
|
||||
|
||||
@@ -460,6 +460,11 @@ public sealed partial class IPCMainProcessServiceImpl : IPCMainProcessService
|
||||
void ConfigureServices()
|
||||
{
|
||||
RegisterService<IPCPlatformService, IPlatformService>();
|
||||
#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
|
||||
IHostsFileService? hostsFileService = Ioc.Get_Nullable<IHostsFileService>();
|
||||
if (hostsFileService != null)
|
||||
RegisterService(hostsFileService);
|
||||
#endif
|
||||
RegisterService<IPCToastService>(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,20 +15,29 @@ internal sealed class HostsFileServiceImpl
|
||||
|
||||
readonly IPlatformService s;
|
||||
readonly object lockObj = new();
|
||||
readonly IHostsFileService? privilegedThis;
|
||||
IHostsFileService? _privilegedThis;
|
||||
|
||||
const string TAG = "HostsFileS";
|
||||
|
||||
public HostsFileServiceImpl(IServiceProvider serviceProvider, IPlatformService s)
|
||||
async ValueTask<IHostsFileService?> GetPrivilegedThisAsync()
|
||||
{
|
||||
this.s = s;
|
||||
#if WINDOWS
|
||||
if (Startup.Instance.IsMainProcess && !WindowsPlatformServiceImpl.IsPrivilegedProcess)
|
||||
{
|
||||
var ipc = serviceProvider.GetService<IPCSubProcessService>();
|
||||
privilegedThis = ipc?.GetService<IHostsFileService>();
|
||||
if (_privilegedThis == null)
|
||||
{
|
||||
var ipc = IPCMainProcessService.Instance;
|
||||
_privilegedThis = await ipc.GetServiceAsync<IHostsFileService>(IPlatformService.IPCRoot.moduleName);
|
||||
}
|
||||
return _privilegedThis;
|
||||
}
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
|
||||
public HostsFileServiceImpl(IPlatformService s)
|
||||
{
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
#region Mark
|
||||
@@ -496,7 +505,7 @@ internal sealed class HostsFileServiceImpl
|
||||
|
||||
public void OpenFileDir() => s.OpenFolder(s.HostsFilePath);
|
||||
|
||||
public async void ResetFile()
|
||||
public async Task<bool> ResetFile()
|
||||
{
|
||||
if (Startup.Instance.IsMainProcess)
|
||||
{
|
||||
@@ -509,15 +518,21 @@ internal sealed class HostsFileServiceImpl
|
||||
{
|
||||
try
|
||||
{
|
||||
var isOk = true;
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
privilegedThis.ResetFile();
|
||||
isOk = await privilegedThis.ResetFile();
|
||||
}
|
||||
else
|
||||
{
|
||||
s.WriteDefaultHostsContent();
|
||||
}
|
||||
Toast.Show(ToastIcon.Success, AppResources.CommunityFix_ResetHostsFileOk);
|
||||
|
||||
if (isOk)
|
||||
Toast.Show(ToastIcon.Success, AppResources.CommunityFix_ResetHostsFileOk);
|
||||
else
|
||||
Toast.Show(ToastIcon.Error, AppResources.CommunityFix_ResetHostsFileCatchTip_.Format("IPC fail"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -530,6 +545,7 @@ internal sealed class HostsFileServiceImpl
|
||||
{
|
||||
s.WriteDefaultHostsContent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -541,7 +557,7 @@ internal sealed class HostsFileServiceImpl
|
||||
EncodingType.ANSICodePage => s.Default,
|
||||
EncodingType.UTF8 => Encoding.Default,
|
||||
EncodingType.UTF8WithBOM => Encoding.UTF8,
|
||||
_ => OperatingSystem2.IsWindows() ? Encoding.UTF8 : Encoding.Default,
|
||||
_ => OperatingSystem.IsWindows() ? Encoding.UTF8 : Encoding.Default,
|
||||
};
|
||||
|
||||
static Dictionary<string, string> ReadHostsAllLines(StreamReader? fileReader)
|
||||
@@ -598,7 +614,7 @@ internal sealed class HostsFileServiceImpl
|
||||
{
|
||||
AppendData = new(ReadHostsAllLines_(fileReader)),
|
||||
ResultType = OperationResultType.Success,
|
||||
Message = AppResources.Hosts_ReadSuccess
|
||||
Message = AppResources.Hosts_ReadSuccess,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -632,7 +648,7 @@ internal sealed class HostsFileServiceImpl
|
||||
{
|
||||
AppendData = ReadHostsAllLines(fileReader),
|
||||
ResultType = OperationResultType.Success,
|
||||
Message = AppResources.Hosts_ReadSuccess
|
||||
Message = AppResources.Hosts_ReadSuccess,
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -644,30 +660,47 @@ internal sealed class HostsFileServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
public OperationResult UpdateHosts(string ip, string domain)
|
||||
static OperationResult GetOperationResultByIpcResult(OperationResult? result)
|
||||
{
|
||||
if (result == null)
|
||||
return new OperationResult
|
||||
{
|
||||
ResultType = OperationResultType.Error,
|
||||
Message = "Ipc call error",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<OperationResult> UpdateHosts(string ip, string domain)
|
||||
{
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
return privilegedThis.UpdateHosts(ip, domain);
|
||||
var r = await privilegedThis.UpdateHosts(ip, domain);
|
||||
return GetOperationResultByIpcResult(r);
|
||||
}
|
||||
var dict = new Dictionary<string, string>
|
||||
{
|
||||
{ domain, ip },
|
||||
};
|
||||
return UpdateHosts(dict);
|
||||
var r2 = await UpdateHosts(dict);
|
||||
return r2;
|
||||
}
|
||||
|
||||
public OperationResult UpdateHosts(IEnumerable<(string ip, string domain)> hosts)
|
||||
public async Task<OperationResult> UpdateHosts(IEnumerable<(string ip, string domain)> hosts)
|
||||
{
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
return privilegedThis.UpdateHosts(hosts);
|
||||
var r = await privilegedThis.UpdateHosts(hosts);
|
||||
return GetOperationResultByIpcResult(r);
|
||||
}
|
||||
//var value = hosts.ToDictionary(k => k.domain, v => v.ip);
|
||||
var value = new Dictionary<string, string>();
|
||||
foreach (var (ip, domain) in hosts)
|
||||
value.TryAdd(domain, ip);
|
||||
return UpdateHosts(value);
|
||||
var r2 = await UpdateHosts(value);
|
||||
return r2;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -678,21 +711,25 @@ internal sealed class HostsFileServiceImpl
|
||||
return new OperationResult(result);
|
||||
}
|
||||
|
||||
public OperationResult UpdateHosts(IReadOnlyDictionary<string, string> hosts)
|
||||
public async Task<OperationResult> UpdateHosts(IReadOnlyDictionary<string, string> hosts)
|
||||
{
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
return privilegedThis.UpdateHosts(hosts);
|
||||
var r = await privilegedThis.UpdateHosts(hosts);
|
||||
return GetOperationResultByIpcResult(r);
|
||||
}
|
||||
var result = HandleHosts(isUpdateOrRemove: true, hosts);
|
||||
return Convert(result);
|
||||
}
|
||||
|
||||
public OperationResult RemoveHosts(string ip, string domain)
|
||||
public async Task<OperationResult> RemoveHosts(string ip, string domain)
|
||||
{
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
return privilegedThis.RemoveHosts(ip, domain);
|
||||
var r = await privilegedThis.RemoveHosts(ip, domain);
|
||||
return GetOperationResultByIpcResult(r);
|
||||
}
|
||||
var hosts = new Dictionary<string, string>
|
||||
{
|
||||
@@ -702,13 +739,15 @@ internal sealed class HostsFileServiceImpl
|
||||
return Convert(result);
|
||||
}
|
||||
|
||||
public OperationResult RemoveHosts(string domain) => RemoveHosts(string.Empty, domain);
|
||||
public async Task<OperationResult> RemoveHosts(string domain) => await RemoveHosts(string.Empty, domain);
|
||||
|
||||
public OperationResult RemoveHostsByTag()
|
||||
public async Task<OperationResult> RemoveHostsByTag()
|
||||
{
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
return privilegedThis.RemoveHostsByTag();
|
||||
var r = await privilegedThis.RemoveHostsByTag();
|
||||
return GetOperationResultByIpcResult(r);
|
||||
}
|
||||
var result = HandleHosts(isUpdateOrRemove: false);
|
||||
return Convert(result);
|
||||
@@ -760,14 +799,14 @@ internal sealed class HostsFileServiceImpl
|
||||
}
|
||||
|
||||
bool mOnExitRestoreHosts;
|
||||
readonly object mOnExitRestoreHostsLock = new();
|
||||
readonly AsyncLock mOnExitRestoreHostsLock = new();
|
||||
|
||||
public void OnExitRestoreHosts()
|
||||
public async Task OnExitRestoreHosts()
|
||||
{
|
||||
lock (mOnExitRestoreHostsLock)
|
||||
using (await mOnExitRestoreHostsLock.LockAsync())
|
||||
{
|
||||
if (mOnExitRestoreHosts) return;
|
||||
RemoveHostsByTag();
|
||||
await RemoveHostsByTag();
|
||||
mOnExitRestoreHosts = true;
|
||||
}
|
||||
}
|
||||
@@ -775,14 +814,17 @@ internal sealed class HostsFileServiceImpl
|
||||
#if DEBUG
|
||||
static FileStream? mOccupyHostsFileStream;
|
||||
|
||||
public void OccupyHosts()
|
||||
public async Task<bool> OccupyHosts()
|
||||
{
|
||||
var isOK = true;
|
||||
var privilegedThis = await GetPrivilegedThisAsync();
|
||||
if (privilegedThis != null)
|
||||
{
|
||||
privilegedThis.OccupyHosts();
|
||||
return;
|
||||
// Ipc 调用失败时将返回默认值
|
||||
isOK = await privilegedThis.OccupyHosts();
|
||||
}
|
||||
mOccupyHostsFileStream = new FileStream(s.HostsFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
|
||||
return isOK;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -12,14 +12,21 @@ static partial class Interop
|
||||
|
||||
const uint QueryLimitedInformation = 0x00001000;
|
||||
|
||||
public static string QueryFullProcessImageName(Process process)
|
||||
public static string? QueryFullProcessImageName(Process process)
|
||||
{
|
||||
int capacity = 1024;
|
||||
StringBuilder sb = new(capacity);
|
||||
var handle = Kernel32_.OpenProcess(new(QueryLimitedInformation), false, process.Id);
|
||||
Kernel32_.QueryFullProcessImageName(handle, 0, sb, ref capacity);
|
||||
string fullPath = sb.ToString(0, capacity);
|
||||
return fullPath;
|
||||
try
|
||||
{
|
||||
int capacity = 1024;
|
||||
StringBuilder sb = new(capacity);
|
||||
var handle = Kernel32_.OpenProcess(new(QueryLimitedInformation), false, process.Id);
|
||||
Kernel32_.QueryFullProcessImageName(handle, 0, sb, ref capacity);
|
||||
string fullPath = sb.ToString(0, capacity);
|
||||
return fullPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+110
-110
@@ -105,115 +105,115 @@ partial class WindowsPlatformServiceImpl
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 BypassUAC 以管理员权限启动进程
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="arguments"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static async Task<Process?> StartAsAdministratorByBypassUAC(string fileName, string? arguments = null)
|
||||
{
|
||||
/** 33. Author: winscripting.blog
|
||||
* https://github.com/hfiref0x/UACME
|
||||
* https://github.com/FatRodzianko/SharpBypassUAC/blob/67a9a3e4500731881f60863454e4e42ef46874e0/SharpBypassUAC/FodHelper.cs
|
||||
* Type: Shell API
|
||||
* Method: Registry key manipulation
|
||||
* Target(s): \system32\fodhelper.exe
|
||||
* Component(s): Attacker defined
|
||||
* Implementation: ucmShellRegModMethod
|
||||
* Works from: Windows 10 TH1 (10240)
|
||||
* Fixed in: unfixed 🙈
|
||||
* How: -
|
||||
* Code status: added in v2.7.2
|
||||
*/
|
||||
RegistryKey? classes = null;
|
||||
var targetName = Path.Combine(Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.System),
|
||||
"fodhelper.exe");
|
||||
try
|
||||
{
|
||||
if (!File.Exists(targetName))
|
||||
return null;
|
||||
// /// <summary>
|
||||
// /// 使用 BypassUAC 以管理员权限启动进程
|
||||
// /// </summary>
|
||||
// /// <param name="fileName"></param>
|
||||
// /// <param name="arguments"></param>
|
||||
// /// <returns></returns>
|
||||
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
// internal static async Task<Process?> StartAsAdministratorByBypassUAC(string fileName, string? arguments = null)
|
||||
// {
|
||||
// /** 33. Author: winscripting.blog
|
||||
// * https://github.com/hfiref0x/UACME
|
||||
// * https://github.com/FatRodzianko/SharpBypassUAC/blob/67a9a3e4500731881f60863454e4e42ef46874e0/SharpBypassUAC/FodHelper.cs
|
||||
// * Type: Shell API
|
||||
// * Method: Registry key manipulation
|
||||
// * Target(s): \system32\fodhelper.exe
|
||||
// * Component(s): Attacker defined
|
||||
// * Implementation: ucmShellRegModMethod
|
||||
// * Works from: Windows 10 TH1 (10240)
|
||||
// * Fixed in: unfixed 🙈
|
||||
// * How: -
|
||||
// * Code status: added in v2.7.2
|
||||
// */
|
||||
// RegistryKey? classes = null;
|
||||
// var targetName = Path.Combine(Environment.GetFolderPath(
|
||||
// Environment.SpecialFolder.System),
|
||||
// "fodhelper.exe");
|
||||
// try
|
||||
// {
|
||||
// if (!File.Exists(targetName))
|
||||
// return null;
|
||||
|
||||
var processPath = Environment.ProcessPath;
|
||||
processPath.ThrowIsNull();
|
||||
if (fileName != processPath) // 仅允许启动自己
|
||||
throw new ArgumentOutOfRangeException(nameof(fileName));
|
||||
// var processPath = Environment.ProcessPath;
|
||||
// processPath.ThrowIsNull();
|
||||
// if (fileName != processPath) // 仅允许启动自己
|
||||
// throw new ArgumentOutOfRangeException(nameof(fileName));
|
||||
|
||||
var processName = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (string.IsNullOrWhiteSpace(processName))
|
||||
throw new ArgumentNullException(nameof(processName));
|
||||
// var processName = Path.GetFileNameWithoutExtension(fileName);
|
||||
// if (string.IsNullOrWhiteSpace(processName))
|
||||
// throw new ArgumentNullException(nameof(processName));
|
||||
|
||||
classes = Registry.CurrentUser.OpenSubKey(@"Software\Classes", true);
|
||||
using var command = classes!.CreateSubKey(@"ms-settings\Shell\Open\command", true);
|
||||
command.SetValue("DelegateExecute", "");
|
||||
command.SetValue("", string.IsNullOrWhiteSpace(arguments) ?
|
||||
$"\"{fileName}\"" : $"\"{fileName}\" {arguments}");
|
||||
// classes = Registry.CurrentUser.OpenSubKey(@"Software\Classes", true);
|
||||
// using var command = classes!.CreateSubKey(@"ms-settings\Shell\Open\command", true);
|
||||
// command.SetValue("DelegateExecute", "");
|
||||
// command.SetValue("", string.IsNullOrWhiteSpace(arguments) ?
|
||||
// $"\"{fileName}\"" : $"\"{fileName}\" {arguments}");
|
||||
|
||||
ProcessStartInfo psi = new()
|
||||
{
|
||||
UseShellExecute = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
FileName = targetName,
|
||||
};
|
||||
var processs_start_before = new HashSet<int>(
|
||||
Process.GetProcessesByName(processName).Select(x => x.Id));
|
||||
var process = Process.Start(psi);
|
||||
process!.WaitForExit(6000);
|
||||
// ProcessStartInfo psi = new()
|
||||
// {
|
||||
// UseShellExecute = true,
|
||||
// WindowStyle = ProcessWindowStyle.Hidden,
|
||||
// FileName = targetName,
|
||||
// };
|
||||
// var processs_start_before = new HashSet<int>(
|
||||
// Process.GetProcessesByName(processName).Select(x => x.Id));
|
||||
// var process = Process.Start(psi);
|
||||
// process!.WaitForExit(6000);
|
||||
|
||||
Dictionary<int, bool> mIsProcessElevated = new();
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
var processs_start_after = Process.GetProcessesByName(processName);
|
||||
foreach (var item in processs_start_after)
|
||||
{
|
||||
if (item.Id == Environment.ProcessId)
|
||||
continue;
|
||||
if (processs_start_before.Contains(item.Id))
|
||||
continue;
|
||||
// Dictionary<int, bool> mIsProcessElevated = new();
|
||||
// for (int i = 0; i < 7; i++)
|
||||
// {
|
||||
// var processs_start_after = Process.GetProcessesByName(processName);
|
||||
// foreach (var item in processs_start_after)
|
||||
// {
|
||||
// if (item.Id == Environment.ProcessId)
|
||||
// continue;
|
||||
// if (processs_start_before.Contains(item.Id))
|
||||
// continue;
|
||||
|
||||
if (mIsProcessElevated.TryGetValue(item.Id, out var value))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
else
|
||||
{
|
||||
var isProcessElevated = IsProcessElevated(item);
|
||||
if (isProcessElevated)
|
||||
return item;
|
||||
mIsProcessElevated.Add(item.Id, isProcessElevated);
|
||||
}
|
||||
}
|
||||
// if (mIsProcessElevated.TryGetValue(item.Id, out var value))
|
||||
// {
|
||||
// return item;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var isProcessElevated = IsProcessElevated(item);
|
||||
// if (isProcessElevated)
|
||||
// return item;
|
||||
// mIsProcessElevated.Add(item.Id, isProcessElevated);
|
||||
// }
|
||||
// }
|
||||
|
||||
await Task.Delay(800);
|
||||
}
|
||||
// await Task.Delay(800);
|
||||
// }
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Log.Error(TAG, ex, "StartAsAdministratorByBypassUAC fail.");
|
||||
#endif
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (classes != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
classes.DeleteSubKeyTree("ms-settings");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// return null;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
//#if DEBUG
|
||||
// Log.Error(TAG, ex, "StartAsAdministratorByBypassUAC fail.");
|
||||
//#endif
|
||||
// }
|
||||
// finally
|
||||
// {
|
||||
// if (classes != null)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// classes.DeleteSubKeyTree("ms-settings");
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
|
||||
}
|
||||
classes.Dispose();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// }
|
||||
// classes.Dispose();
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 以管理员权限启动进程
|
||||
@@ -221,18 +221,18 @@ partial class WindowsPlatformServiceImpl
|
||||
/// <param name="fileName"></param>
|
||||
/// <param name="arguments"></param>
|
||||
/// <returns></returns>
|
||||
internal static async ValueTask<Process?> StartAsAdministrator(string fileName, string? arguments = null)
|
||||
internal static /*async*/ ValueTask<Process?> StartAsAdministrator(string fileName, string? arguments = null)
|
||||
{
|
||||
#if !DEBUG
|
||||
if (IsPrivilegedProcess)
|
||||
return Process2.Start(fileName, arguments);
|
||||
return ValueTask.FromResult(Process2.Start(fileName, arguments));
|
||||
#endif
|
||||
if (!DesktopBridge.IsRunningAsUwp)
|
||||
{
|
||||
var process = await StartAsAdministratorByBypassUAC(fileName, arguments);
|
||||
if (process != null)
|
||||
return process;
|
||||
}
|
||||
//if (!DesktopBridge.IsRunningAsUwp)
|
||||
//{
|
||||
// var process = await StartAsAdministratorByBypassUAC(fileName, arguments);
|
||||
// if (process != null)
|
||||
// return process;
|
||||
//}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -244,11 +244,11 @@ partial class WindowsPlatformServiceImpl
|
||||
Verb = "runas",
|
||||
};
|
||||
var process_runas = Process.Start(psi);
|
||||
return process_runas;
|
||||
return ValueTask.FromResult(process_runas);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
return ValueTask.FromResult<Process?>(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +46,15 @@ public sealed class UserService : ReactiveObject
|
||||
var cUser = await userManager.GetCurrentUserAsync();
|
||||
if (cUser.HasValue()) return;
|
||||
|
||||
if (Random2.Next(2) == 1)
|
||||
{
|
||||
var vm = new LoginOrRegisterWindowViewModel();
|
||||
await windowManager.ShowTaskDialogAsync(vm, vm.Title, isOkButton: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
INavigationService.Instance.Navigate(typeof(LoginOrRegisterWindowViewModel), NavigationTransitionEffect.DrillIn);
|
||||
}
|
||||
//if (Random2.Next(2) == 1)
|
||||
//{
|
||||
var vm = new LoginOrRegisterWindowViewModel();
|
||||
await windowManager.ShowTaskDialogAsync(vm, vm.Title, isOkButton: false);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// INavigationService.Instance.Navigate(typeof(LoginOrRegisterWindowViewModel), NavigationTransitionEffect.DrillIn);
|
||||
//}
|
||||
}
|
||||
|
||||
public void NavigateUserCenterPage()
|
||||
|
||||
@@ -12,7 +12,8 @@ public sealed class NoticeService : ReactiveObject
|
||||
|
||||
//readonly INotificationRepository notificationRepo = Ioc.Get<INotificationRepository>();
|
||||
|
||||
public ObservableCollection<OfficialMessageItemDTO> Notices { get; }
|
||||
[Reactive]
|
||||
public ObservableCollection<OfficialMessageItemDTO> Notices { get; set; }
|
||||
|
||||
[Reactive]
|
||||
public int UnreadNotificationsCount { get; set; }
|
||||
@@ -54,13 +55,21 @@ public sealed class NoticeService : ReactiveObject
|
||||
{
|
||||
item.Unread = true;
|
||||
|
||||
//只弹最新的一条通知
|
||||
if (UnreadNotificationsCount == 0 && GeneralSettings.MessagePopupNotification.Value)
|
||||
INotificationService.Instance.Notify(item.Content, NotificationType.Announcement, title: item.Title);
|
||||
|
||||
UnreadNotificationsCount++;
|
||||
}
|
||||
}
|
||||
|
||||
//只弹最新的一条通知
|
||||
if (UnreadNotificationsCount != 0 && GeneralSettings.MessagePopupNotification.Value)
|
||||
{
|
||||
//if (UnreadNotificationsCount > 1)
|
||||
//{
|
||||
// INotificationService.Instance.Notify($"{UnreadNotificationsCount}条通知未读", NotificationType.Announcement);
|
||||
// return;
|
||||
//}
|
||||
var notice = Notices.First(x => x.Unread == true);
|
||||
INotificationService.Instance.Notify(notice.Content, NotificationType.Announcement, title: notice.Title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ public sealed class SteamConnectService
|
||||
|
||||
public void RunAFKApps()
|
||||
{
|
||||
var aFKAppList = Ioc.Get<IPartialGameLibrarySettings>()?.AFKAppList;
|
||||
var aFKAppList = Ioc.Get_Nullable<IPartialGameLibrarySettings>()?.AFKAppList;
|
||||
if (aFKAppList?.Count > 0)
|
||||
{
|
||||
foreach (var item in aFKAppList)
|
||||
@@ -291,7 +291,7 @@ public sealed class SteamConnectService
|
||||
INotificationService.Instance.Notify($"{AppResources.Steam_CheckStarted}{(IsSteamChinaLauncher ? AppResources.Steam_SteamChina : AppResources.Steam_SteamWorld)}{Environment.NewLine}{AppResources.Steam_CurrentUser}{CurrentSteamUser.SteamNickName}{Environment.NewLine}{AppResources.Steam_CurrentIPCountry}{CurrentSteamUser.IPCountry}{Environment.NewLine}{AppResources.Steam_ServerTime}{steamServerTime:yyyy-MM-dd HH:mm:ss}", NotificationType.Message);
|
||||
}
|
||||
|
||||
var isAutoAFKApps = Ioc.Get<IPartialGameLibrarySettings>()?.IsAutoAFKApps ?? default;
|
||||
var isAutoAFKApps = Ioc.Get_Nullable<IPartialGameLibrarySettings>()?.IsAutoAFKApps ?? default;
|
||||
if (isAutoAFKApps)
|
||||
{
|
||||
RunAFKApps();
|
||||
|
||||
@@ -28,7 +28,7 @@ public interface IHostsFileService
|
||||
/// <summary>
|
||||
/// 重置 hosts 文件
|
||||
/// </summary>
|
||||
void ResetFile();
|
||||
Task<bool> ResetFile();
|
||||
|
||||
/// <summary>
|
||||
/// 读取 hosts 文件
|
||||
@@ -42,17 +42,17 @@ public interface IHostsFileService
|
||||
/// <param name="ip"></param>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
OperationResult UpdateHosts(string ip, string domain);
|
||||
Task<OperationResult> UpdateHosts(string ip, string domain);
|
||||
|
||||
/// <inheritdoc cref="UpdateHosts(IReadOnlyDictionary{string, string})"/>
|
||||
OperationResult UpdateHosts(IEnumerable<(string ip, string domain)> hosts);
|
||||
Task<OperationResult> UpdateHosts(IEnumerable<(string ip, string domain)> hosts);
|
||||
|
||||
/// <summary>
|
||||
/// 更新多条 hosts 纪录
|
||||
/// </summary>
|
||||
/// <param name="hosts"></param>
|
||||
/// <returns></returns>
|
||||
OperationResult UpdateHosts(IReadOnlyDictionary<string, string> hosts);
|
||||
Task<OperationResult> UpdateHosts(IReadOnlyDictionary<string, string> hosts);
|
||||
|
||||
/// <summary>
|
||||
/// 移除一条 hosts 纪录
|
||||
@@ -60,21 +60,21 @@ public interface IHostsFileService
|
||||
/// <param name="ip"></param>
|
||||
/// <param name="domain"></param>
|
||||
/// <returns></returns>
|
||||
OperationResult RemoveHosts(string ip, string domain);
|
||||
Task<OperationResult> RemoveHosts(string ip, string domain);
|
||||
|
||||
/// <inheritdoc cref="RemoveHosts(string, string)"/>
|
||||
OperationResult RemoveHosts(string domain);
|
||||
Task<OperationResult> RemoveHosts(string domain);
|
||||
|
||||
/// <summary>
|
||||
/// 移除当前程序写入的 hosts 纪录并还原写入时冲突的备份纪录
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
OperationResult RemoveHostsByTag();
|
||||
Task<OperationResult> RemoveHostsByTag();
|
||||
|
||||
/// <summary>
|
||||
/// 当程序退出时还原 hosts 文件
|
||||
/// </summary>
|
||||
void OnExitRestoreHosts();
|
||||
Task OnExitRestoreHosts();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前 hosts 文件是否包含修改过的标签,注意:产生异常时也将返回 <see langword="false"/>,并 <see cref="Toast"/> 显示 <see cref="Exception"/>
|
||||
@@ -83,7 +83,7 @@ public interface IHostsFileService
|
||||
bool ContainsHostsByTag();
|
||||
|
||||
#if DEBUG
|
||||
void OccupyHosts();
|
||||
Task<bool> OccupyHosts();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ partial interface IPlatformService
|
||||
/// </summary>
|
||||
void WriteDefaultHostsContent()
|
||||
{
|
||||
var fileStream = new FileStream(HostsFilePath,
|
||||
using var fileStream = new FileStream(HostsFilePath,
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.Write,
|
||||
FileShare.ReadWrite | FileShare.Delete);
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace BD.WTTS.Services;
|
||||
|
||||
partial interface IPlatformService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 获取 排除代理地址
|
||||
/// </summary>
|
||||
@@ -28,7 +27,7 @@ partial interface IPlatformService
|
||||
"172.31.*",
|
||||
"192.168.*",
|
||||
Constants.Urls.OfficialApiHostName,
|
||||
Constants.Urls.OfficialShopApiHostName
|
||||
Constants.Urls.OfficialShopApiHostName,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -89,6 +89,21 @@ partial class Startup // 全局异常处理
|
||||
#endif
|
||||
Logger.Error(ex, message, args);
|
||||
|
||||
#if WINDOWS
|
||||
Task2.InBackground(() =>
|
||||
{
|
||||
var mbText =
|
||||
$"""
|
||||
{string.Format(message, args)}
|
||||
{ex}
|
||||
""";
|
||||
const string mbTitle = $"Application Crash - {AssemblyInfo.Trademark}";
|
||||
WPFMessageBox.Show(mbText, mbTitle,
|
||||
WPFMessageBoxButton.OK,
|
||||
WPFMessageBoxImage.Error);
|
||||
});
|
||||
#endif
|
||||
|
||||
#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
|
||||
|
||||
Startup? s;
|
||||
|
||||
@@ -80,8 +80,6 @@ partial class Startup // OnStartup
|
||||
{
|
||||
Steamworks.Dispatch.OnException = (e) =>
|
||||
{
|
||||
Console.Error.WriteLine(e.Message);
|
||||
Console.Error.WriteLine(e.StackTrace);
|
||||
Log.Error(nameof(Steamworks), e, "Steamworks.SteamClient OnException.");
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#if WINDOWS
|
||||
using System.Security.AccessControl;
|
||||
#endif
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace BD.WTTS;
|
||||
|
||||
@@ -82,14 +86,31 @@ partial class Startup // 本应用程序单例管道
|
||||
}
|
||||
#endif
|
||||
var currentMainModule = current.TryGetMainModule();
|
||||
bool IsThisProgram(Process p, ProcessModule? pMainModule)
|
||||
{
|
||||
if (currentMainModule != null)
|
||||
{
|
||||
if (pMainModule != null)
|
||||
{
|
||||
return pMainModule.FileName == currentMainModule.FileName &&
|
||||
pMainModule.ModuleName == currentMainModule.ModuleName;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if WINDOWS
|
||||
var path = Interop.Kernel32.QueryFullProcessImageName(p);
|
||||
return path == currentMainModule.FileName;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
var query = from p in Process.GetProcessesByName(current.ProcessName)
|
||||
let pMainModule = p.TryGetMainModule()
|
||||
where p.Id != current.Id &&
|
||||
p.ProcessName == current.ProcessName &&
|
||||
(currentMainModule == null ||
|
||||
(pMainModule != null &&
|
||||
pMainModule.FileName == currentMainModule.FileName &&
|
||||
pMainModule.ModuleName == currentMainModule.ModuleName))
|
||||
p.ProcessName == current.ProcessName && IsThisProgram(p, pMainModule)
|
||||
select p;
|
||||
return query;
|
||||
}
|
||||
@@ -121,7 +142,7 @@ partial class Startup // 本应用程序单例管道
|
||||
static string GetPipeName()
|
||||
{
|
||||
var processPath = Environment.ProcessPath ?? string.Empty;
|
||||
return AssemblyInfo.APPLICATION_ID + "_v3_" + Hashs.String.Crc32(processPath);
|
||||
return AssemblyInfo.APPLICATION_ID + "_v" + AssemblyInfo.InformationalVersion + "_" + Hashs.String.Crc32(processPath);
|
||||
}
|
||||
|
||||
CancellationTokenSource? cts;
|
||||
@@ -131,9 +152,35 @@ partial class Startup // 本应用程序单例管道
|
||||
{
|
||||
cts = new CancellationTokenSource();
|
||||
var name = GetPipeName();
|
||||
|
||||
NamedPipeServerStream GetNamedPipeServerStream()
|
||||
{
|
||||
const PipeDirection direction = PipeDirection.In;
|
||||
const int maxNumberOfServerInstances = 5;
|
||||
const PipeTransmissionMode transmissionMode = PipeTransmissionMode.Byte;
|
||||
const PipeOptions options = PipeOptions.None;
|
||||
const int inBufferSize = 0;
|
||||
const int outBufferSize = 0;
|
||||
#if WINDOWS
|
||||
if (WindowsPlatformServiceImpl.IsPrivilegedProcess)
|
||||
{
|
||||
// https://github.com/dotnet-campus/dotnetCampus.Ipc/blob/2.0.0-alpha403/src/dotnetCampus.Ipc/Internals/IpcPipeServerMessageProvider.cs#L99
|
||||
SecurityIdentifier securityIdentifier = new(WellKnownSidType.AuthenticatedUserSid, null);
|
||||
PipeSecurity pipeSecurity = new();
|
||||
pipeSecurity.AddAccessRule(new PipeAccessRule(securityIdentifier,
|
||||
PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
|
||||
AccessControlType.Allow));
|
||||
|
||||
var result = NamedPipeServerStreamAcl.Create(name, direction, maxNumberOfServerInstances, transmissionMode, options, inBufferSize, outBufferSize, pipeSecurity);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
return new NamedPipeServerStream(name, direction, maxNumberOfServerInstances, transmissionMode, options);
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
using var pipeServer = new NamedPipeServerStream(name, PipeDirection.In, 1);
|
||||
using var pipeServer = GetNamedPipeServerStream();
|
||||
try
|
||||
{
|
||||
pipeServer.WaitForConnection();
|
||||
@@ -177,7 +224,7 @@ partial class Startup // 本应用程序单例管道
|
||||
using var pipeClient = new NamedPipeClientStream(".", name,
|
||||
PipeDirection.Out, PipeOptions.None,
|
||||
TokenImpersonationLevel.Impersonation);
|
||||
pipeClient.Connect(TimeSpan.FromSeconds(25));
|
||||
pipeClient.Connect(TimeSpan.FromSeconds(7.7D));
|
||||
using StreamWriter sw = new StreamWriter(pipeClient);
|
||||
sw.WriteLine(value);
|
||||
return true;
|
||||
|
||||
@@ -434,12 +434,6 @@ public abstract partial class Startup
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalExceptionHandler.Handler(ex, nameof(StartAsync));
|
||||
#if WINDOWS
|
||||
WPFMessageBox.Show(ex.ToString(),
|
||||
AssemblyInfo.Trademark,
|
||||
WPFMessageBoxButton.OK,
|
||||
WPFMessageBoxImage.Error);
|
||||
#endif
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -27,20 +27,20 @@ global using System.IO;
|
||||
global using System.Buffers;
|
||||
global using System.IO.Pipes;
|
||||
global using PipeOptions = System.IO.Pipes.PipeOptions;
|
||||
#if !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using System.IO.Pipelines;
|
||||
global using PipelineOptions = System.IO.Pipelines.PipeOptions;
|
||||
#endif
|
||||
#endif
|
||||
global using System.IO.Compression;
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using System.IO.FileFormats;
|
||||
#endif
|
||||
global using System.Linq;
|
||||
global using System.Linq.Expressions;
|
||||
global using System.Net;
|
||||
global using System.Net.Security;
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using System.Net.Http.Headers;
|
||||
global using System.Net.Http.Json;
|
||||
global using System.Net.Http.Client;
|
||||
@@ -66,7 +66,7 @@ global using System.Text.Unicode;
|
||||
#endif
|
||||
global using System.Text.RegularExpressions;
|
||||
global using System.Runtime;
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using DeploymentMode = System.Runtime.DeploymentMode;
|
||||
global using System.Runtime.Devices;
|
||||
#endif
|
||||
@@ -74,16 +74,16 @@ global using System.Runtime.InteropServices;
|
||||
global using System.Runtime.Versioning;
|
||||
global using System.Runtime.Serialization.Formatters;
|
||||
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
#endif
|
||||
|
||||
#if WINDOWS && !APP_HOST && !APP_REVERSE_PROXY && !LIB_CLIENT_IPC
|
||||
#if WINDOWS && !APP_HOST && !APP_REVERSE_PROXY && !LIB_CLIENT_IPC && !__NOT_IMPORT_COMMON__
|
||||
global using System.Management;
|
||||
#endif
|
||||
|
||||
#if !NONE_UI && (WINDOWS || APP_HOST)
|
||||
#if !NONE_UI && (WINDOWS || APP_HOST) && !__NOT_IMPORT_COMMON__
|
||||
global using WPFMessageBox = MS.Win32.MessageBox;
|
||||
global using WPFMessageBoxButton = MS.Win32.MessageBoxButton;
|
||||
global using WPFMessageBoxImage = MS.Win32.MessageBoxImage;
|
||||
@@ -94,14 +94,14 @@ global using System.Xml;
|
||||
global using System.Xml.Serialization;
|
||||
global using System.Security.Cryptography.X509Certificates;
|
||||
global using IPAddress = System.Net.IPAddress;
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using Ioc = System.Ioc;
|
||||
global using DateTimeFormat = System.DateTimeFormat;
|
||||
global using SerializationDateTimeFormat = System.Runtime.Serialization.DateTimeFormat;
|
||||
global using HttpMethod = System.Net.Http.HttpMethod;
|
||||
#endif
|
||||
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ
|
||||
#if !NETFRAMEWORK && !APP_HOST && !NETFRAMEWORK_SINGLE_FILE && !RES_PROJ && !__NOT_IMPORT_COMMON__
|
||||
global using HttpHandlerCategory = System.Net.Http.Client.HttpHandlerCategory;
|
||||
global using IHttpClientFactory = System.Net.Http.Client.IHttpClientFactory;
|
||||
#endif
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#if !APP_HOST
|
||||
#if !NETFRAMEWORK
|
||||
|
||||
#if !__NOT_IMPORT_COMMON_PRIMITIVES__
|
||||
#if !__NOT_IMPORT_COMMON_PRIMITIVES__ && !__NOT_IMPORT_COMMON__
|
||||
global using Polly;
|
||||
global using BD.Common;
|
||||
global using BD.Common.Columns;
|
||||
@@ -22,7 +22,7 @@ global using BD.Common.Identity;
|
||||
global using BD.Common.Identity.Abstractions;
|
||||
#endif
|
||||
|
||||
#if !__NOT_IMPORT_COMMON_SERVICES__
|
||||
#if !__NOT_IMPORT_COMMON_SERVICES__ && !__NOT_IMPORT_COMMON__
|
||||
global using BD.Common.Services;
|
||||
global using BD.Common.Services.Implementation;
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user