🎨 msix publish

This commit is contained in:
Aigio Liu
2023-07-14 21:13:24 +08:00
parent e8a3f7fd6f
commit eafaab9bab
17 changed files with 628 additions and 207 deletions
+1
View File
@@ -87,6 +87,7 @@ false
public const string GameListId = "00000000-0000-0000-0000-000000000003";
public const string ArchiSteamFarmPlus = "ArchiSteamFarmPlus";
public const string ArchiSteamFarmPlusId = "00000000-0000-0000-0000-000000000004";
public const string Authenticator = "Authenticator";
public const string AuthenticatorId = "00000000-0000-0000-0000-000000000005";
@@ -18,7 +18,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
public sealed override string Description => "提供一些游戏相关网站服务的加速及脚本注入功能。";
public sealed override string Author => "Steam++ 官方";
protected sealed override string? AuthorOriginalString => null;
public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.Accelerator/UI/Assets/accelerator.ico";
@@ -17,7 +17,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
public sealed override string Description => "提供多平台多账号令牌管理、加密、确认交易、云同步等功能";
public sealed override string Author => "Steam++ 官方";
protected sealed override string? AuthorOriginalString => null;
public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.Authenticator/UI/Assets/authenticator.ico";
@@ -18,7 +18,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
public sealed override string Description => "可支持自行添加多平台账号快速切换功能,Steam 可自动读取账号信息,其它平台请手动添加账号信息。";
public sealed override string Author => "Steam++ 官方";
protected sealed override string? AuthorOriginalString => null;
public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.GameAccount/UI/Assets/userswitcher.ico";
@@ -17,7 +17,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
public sealed override string Description => "管理库存游戏";
public sealed override string Author => "Steam++ 官方";
protected sealed override string? AuthorOriginalString => null;
public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.GameList/UI/Assets/game.ico";
@@ -15,7 +15,7 @@ public sealed class Plugin : PluginBase<Plugin>, IPlugin
public sealed override string Description => "游戏工具";
public sealed override string Author => "Steam++ 官方";
protected sealed override string? AuthorOriginalString => null;
public sealed override string? Icon => "avares://BD.WTTS.Client.Plugins.GameTools/UI/Assets/toolbox.ico";
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>pub</AssemblyName>
@@ -1,3 +1,4 @@
using BD.WTTS.Client.Tools.Publish.Helpers;
using static BD.WTTS.Client.Tools.Publish.Helpers.DotNetCLIHelper;
using static BD.WTTS.GlobalDllImportResolver;
@@ -65,7 +66,7 @@ interface IDotNetPublishCommand : ICommand
Console.WriteLine(string.Join(' ', psi.ArgumentList));
var publishDir = Path.Combine(projRootPath, arg.PublishDir);
var rootPublishDir = Path.Combine(publishDir, "..");
var rootPublishDir = Path.GetFullPath(Path.Combine(publishDir, ".."));
DirTryDelete(rootPublishDir);
// 发布主体
@@ -100,12 +101,29 @@ interface IDotNetPublishCommand : ICommand
// 复制运行时
CopyRuntime(rootPublishDir, isWindows, info.Architecture);
if (OperatingSystem.IsWindows() && isWindows)
{
// 打包资源 images
MSIXHelper.MakePri.Start(rootPublishDir);
// 生成 msix 包
MSIXHelper.MakeAppx.Start(rootPublishDir, GetVersion(), info.Architecture);
// 签名 msix 包
MSIXHelper.SignTool.Start(rootPublishDir);
}
Console.WriteLine(rootPublishDir);
}
Console.WriteLine("OK");
}
static string GetVersion()
{
var v = new Version(AssemblyInfo.Version);
static int GetInt32(int value) => value < 0 ? 0 : value;
return $"{GetInt32(v.Major)}.{GetInt32(v.Minor)}.{GetInt32(v.Build)}.{GetInt32(v.Revision)}";
}
/// <summary>
/// 将运行时复制到发布根目录下
/// </summary>
@@ -630,37 +648,4 @@ interface IDotNetPublishCommand : ICommand
}
}
}
static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) // https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
// Check if the source directory exists
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
// Cache directories before we start copying
DirectoryInfo[] dirs = dir.GetDirectories();
// Create the destination directory
Directory.CreateDirectory(destinationDir);
// Get the files in the source directory and copy to the destination directory
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
file.CopyTo(targetFilePath, true);
}
// If recursive and copying subdirectories, recursively call this method
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}
}
@@ -0,0 +1,90 @@
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\u4EE3\u7801\u7B7E\u540D\u8BC1\u4E66";
public const int CertificateValidDays = 3650;
public const int KEY_SIZE_BITS = 4096;
static Command ICommand.GetCommand()
{
var type = new Option<int>("--type",
"Generate certificate type(1 code, 2 store-code)");
var path = new Option<string>("--path",
"Directory where certificate files need to be saved");
var cn = new Option<string>("--cn", "X500DistinguishedNameValue");
var password = new Option<string>("--pwd");
var command = new Command(commandName, "Generate certificate")
{
type, path, cn, password,
};
command.SetHandler(Handler, type, path, cn, password);
return command;
}
// New-SelfSignedCertificate -Type Custom -Subject "CN=Contoso Software, O=Contoso Corporation, C=US" -KeyUsage DigitalSignature -FriendlyName "Your friendly name goes here" -CertStoreLocation "Cert:\CurrentUser\My" -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
/// <summary>
/// 表示证书对代码签名有效。始终指定此值以限制证书的预期用途。
/// https://oidref.com/1.3.6.1.5.5.7.3.3
/// </summary>
const string codeSigningOid = "1.3.6.1.5.5.7.3.3";
/// <summary>
/// 表示证书遵循生存期签名。通常,如果签名带有时间戳,只要证书在时间戳时有效,即使证书过期,签名也仍然有效。此 EKU 强制签名过期,无论签名是否带有时间戳。
/// https://oidref.com/1.3.6.1.4.1.311.10.3.13
/// </summary>
const string _13Oid = "1.3.6.1.4.1.311.10.3.13";
static byte[] GenerateCodeSigningCert(string x500DistinguishedNameValue, string password)
{
if (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
using var rsa = RSA.Create(KEY_SIZE_BITS);
X500DistinguishedName subjectName = new(x500DistinguishedNameValue);
CertificateRequest request = new(subjectName, rsa,
HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
// 基本约束:此扩展指示证书是否为证书颁发机构 (CA) 。 对于自签名证书,
// 此参数应包含扩展字符串 “2.5.29.19={text}”,指示证书是最终实体 (不是 CA) 。
X509BasicConstraintsExtension basicConstraints = new(false, false, 0, true);
request.CertificateExtensions.Add(basicConstraints);
X509KeyUsageExtension keyUsage = new(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, true);
request.CertificateExtensions.Add(keyUsage);
var oids = new OidCollection { new(codeSigningOid), new(_13Oid), };
var enhancedKeyUsage = new X509EnhancedKeyUsageExtension(oids, true);
request.CertificateExtensions.Add(enhancedKeyUsage);
X509SubjectKeyIdentifierExtension subjectKeyId;
subjectKeyId = new(request.PublicKey, false);
request.CertificateExtensions.Add(subjectKeyId);
var notBefore = DateTimeOffset.UtcNow;
var notAfter = notBefore.AddDays(CertificateValidDays);
using var cert = request.CreateSelfSigned(notBefore, notAfter);
var pfx = cert.Export(X509ContentType.Pfx, password);
return pfx;
}
internal static void Handler(int type, string path, string cn, string password)
{
var bytes = type switch
{
// 生成 BeyondDimension 代码签名证书
1 => GenerateCodeSigningCert(X500DistinguishedNameValue, password),
// 生成 上传应用商店的 代码签名证书
2 => GenerateCodeSigningCert($"CN={cn}", password),
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
File.WriteAllBytes(path, bytes);
}
}
@@ -181,4 +181,37 @@ interface Constants
var download_link = $"{(no_cdn.Value ? feed_no_cdn : feed)}/aspnetcore/Runtime/{specific_version}/aspnetcore-runtime-{specific_version}-{osname}-{normalized_architecture}.{(osname == "win" ? "zip" : "tar.gz")}";
return download_link;
}
static void CopyDirectory(string sourceDir, string destinationDir, bool recursive) // https://learn.microsoft.com/zh-cn/dotnet/standard/io/how-to-copy-directories
{
// Get information about the source directory
var dir = new DirectoryInfo(sourceDir);
// Check if the source directory exists
if (!dir.Exists)
throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}");
// Cache directories before we start copying
DirectoryInfo[] dirs = dir.GetDirectories();
// Create the destination directory
IOPath.DirCreateByNotExists(destinationDir);
// Get the files in the source directory and copy to the destination directory
foreach (FileInfo file in dir.GetFiles())
{
string targetFilePath = Path.Combine(destinationDir, file.Name);
file.CopyTo(targetFilePath, true);
}
// If recursive and copying subdirectories, recursively call this method
if (recursive)
{
foreach (DirectoryInfo subDir in dirs)
{
string newDestinationDir = Path.Combine(destinationDir, subDir.Name);
CopyDirectory(subDir.FullName, newDestinationDir, true);
}
}
}
}
@@ -2,84 +2,6 @@ namespace BD.WTTS.Client.Tools.Publish.Helpers;
static class MSIXHelper
{
static void W(Stream stream, string version)
{
// https://learn.microsoft.com/zh-cn/windows/msix/desktop/desktop-to-uwp-manual-conversion
var appxmanifest =
"""
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
IgnorableNamespaces="uap rescap desktop desktop2">
<Identity
Name="4651ED44255E.47979655102CE"
Publisher="CN=A90E406B-B2D3-4A23-B061-0FA1D65C4F66"
Version="{0}"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>Watt Toolkit</DisplayName>
<PublisherDisplayName>软妹币玩家</PublisherDisplayName>
<Logo>Images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Watt Toolkit"
Description="「Watt Toolkit」是一个开源跨平台的多功能游戏工具箱,此工具的大部分功能都是需要您下载安装 Steam 才能使用。"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png" ShortName="Steam++">
<uap:ShowNameOnTiles>
<uap:ShowOn Tile="square150x150Logo"/>
<uap:ShowOn Tile="wide310x150Logo"/>
<uap:ShowOn Tile="square310x310Logo"/>
</uap:ShowNameOnTiles>
</uap:DefaultTile >
<uap:SplashScreen Image="Images\SplashScreen.png" />
<uap:InitialRotationPreference>
<uap:Rotation Preference="landscape"/>
</uap:InitialRotationPreference>
<uap:LockScreen BadgeLogo="Images\BadgeLogo.png" Notification="badgeAndTileText"/>
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="Steam++\Steam++.exe" />
<desktop:Extension Category="windows.startupTask" Executable="Steam++\Steam++.exe" EntryPoint="Windows.FullTrustApplication">
<desktop:StartupTask TaskId="BootAutoStartTask" Enabled="true" DisplayName="Steam++ System Boot Run" />
</desktop:Extension>
</Extensions>
</Application>
</Applications>
<Extensions>
<desktop2:Extension Category="windows.firewallRules">
<desktop2:FirewallRules Executable="Steam++\Steam++.exe">
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all" />
<desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all" />
</desktop2:FirewallRules>
</desktop2:Extension>
</Extensions>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="allowElevation" />
</Capabilities>
</Package>
"""u8;
}
enum WinVersion : ushort
{
W10_20H1 = 19041,
@@ -87,7 +9,69 @@ static class MSIXHelper
W11_22H2 = 22621,
}
static class MakeAppx
public static class MakePri
{
const string makepri_exe = "makepri.exe";
static string GetMakePriPath(WinVersion version)
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
path = Path.Combine(path, "Windows Kits", "10", "bin", $"10.0.{(ushort)version}.0", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), makepri_exe);
return path;
}
static string GetMakePriPath()
{
var query = from m in Enum.GetValues<WinVersion>().OrderByDescending(x => (ushort)x)
let p = GetMakePriPath(m)
let exists = File.Exists(p)
where exists
select p;
var path = query.FirstOrDefault();
if (string.IsNullOrWhiteSpace(path))
throw new FileNotFoundException(makepri_exe);
return path;
}
public static void Start(string rootPublicPath)
{
// https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options
var xmlPath = @$"{rootPublicPath}\priconfig.xml";
IOPath.FileIfExistsItDelete(xmlPath);
var fileName = GetMakePriPath();
var psi = new ProcessStartInfo
{
FileName = fileName,
UseShellExecute = false,
Arguments =
$"""
createconfig /cf "{xmlPath}" /dq lang-en-US /o /pv 10.0.0
""",
WorkingDirectory = rootPublicPath,
};
DotNetCLIHelper.StartProcessAndWaitForExit(psi);
var prPath = $@"{ProjectUtils.ProjPath}\build\windows\makepri";
CopyDirectory(prPath, rootPublicPath, true);
psi = new ProcessStartInfo
{
FileName = fileName,
UseShellExecute = false,
Arguments =
$"""
new /cf "{xmlPath}" /pr "{prPath}"
""",
WorkingDirectory = rootPublicPath,
};
DotNetCLIHelper.StartProcessAndWaitForExit(psi);
IOPath.FileIfExistsItDelete(xmlPath);
}
}
public static class MakeAppx
{
// https://learn.microsoft.com/zh-cn/windows/msix/package/create-app-package-with-makeappx-tool
// 使用 MakeAppx.exe 创建 MSIX 包或捆绑包
@@ -114,13 +98,176 @@ static class MSIXHelper
return path;
}
public static void Start(string arguments)
public static void Start(
string rootPublicPath,
string version4,
Architecture processorArchitecture)
{
GenerateAppxManifestXml(rootPublicPath, version4, processorArchitecture);
var msixPath = $"{rootPublicPath}.msix";
IOPath.FileIfExistsItDelete(msixPath);
var psi = new ProcessStartInfo
{
FileName = GetMakeAppxPath(),
UseShellExecute = false,
Arguments = arguments,
Arguments =
$"""
pack /v /h SHA256 /d "{rootPublicPath}" /p "{msixPath}"
""",
};
DotNetCLIHelper.StartProcessAndWaitForExit(psi);
}
/// <summary>
/// 生成位于根目录的 AppxManifest.xml
/// </summary>
/// <param name="rootPublicPath"></param>
/// <param name="version4"></param>
/// <param name="processorArchitecture"></param>
static void GenerateAppxManifestXml(
string rootPublicPath,
string version4,
Architecture processorArchitecture)
{
// https://learn.microsoft.com/zh-cn/windows/msix/desktop/desktop-to-uwp-manual-conversion
var xmlString =
$"""
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Package IgnorableNamespaces="uap rescap desktop desktop2 build" xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2" xmlns:build="http://schemas.microsoft.com/developer/appx/2015/build">
<Identity Name="4651ED44255E.47979655102CE" Publisher="CN=A90E406B-B2D3-4A23-B061-0FA1D65C4F66"
Version="{version4}" ProcessorArchitecture="{processorArchitecture.ToString().ToLowerInvariant()}"/>
<Properties>
<DisplayName>Watt Toolkit</DisplayName>
<PublisherDisplayName>软妹币玩家</PublisherDisplayName>
<Logo>images\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0"/>
</Dependencies>
<Resources>
<Resource Language="ZH-CN"/>
<Resource uap:Scale="200"/>
</Resources>
<Applications>
<Application Id="App" Executable="Steam++.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="Watt Toolkit" Description="「Watt Toolkit」是一个开源跨平台的多功能游戏工具箱,此工具的大部分功能都是需要您下载安装 Steam 才能使用。" BackgroundColor="transparent" Square150x150Logo="images\Square150x150Logo.png" Square44x44Logo="images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="images\Wide310x150Logo.png" Square71x71Logo="images\SmallTile.png" Square310x310Logo="images\LargeTile.png" ShortName="Steam++">
<uap:ShowNameOnTiles>
<uap:ShowOn Tile="square150x150Logo"/>
<uap:ShowOn Tile="wide310x150Logo"/>
<uap:ShowOn Tile="square310x310Logo"/>
</uap:ShowNameOnTiles>
</uap:DefaultTile>
<uap:SplashScreen Image="images\SplashScreen.png"/>
<uap:InitialRotationPreference>
<uap:Rotation Preference="landscape"/>
</uap:InitialRotationPreference>
<uap:LockScreen BadgeLogo="images\BadgeLogo.png" Notification="badgeAndTileText"/>
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="Steam++.exe"/>
<desktop:Extension Category="windows.startupTask" Executable="Steam++.exe" EntryPoint="Windows.FullTrustApplication">
<desktop:StartupTask TaskId="BootAutoStartTask" Enabled="true" DisplayName="Steam++ System Boot Run"/>
</desktop:Extension>
</Extensions>
</Application>
</Applications>
<Extensions>
<desktop2:Extension Category="windows.firewallRules">
<desktop2:FirewallRules Executable="Steam++.exe">
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all"/>
<desktop2:Rule Direction="in" IPProtocol="UDP" Profile="all"/>
</desktop2:FirewallRules>
</desktop2:Extension>
</Extensions>
<Capabilities>
<Capability Name="internetClient"/>
<rescap:Capability Name="runFullTrust"/>
<rescap:Capability Name="allowElevation"/>
</Capabilities>
</Package>
""";
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(xmlString);
var xmlStringMini = xmlDoc.InnerXml;
var xmlFilePath = Path.Combine(rootPublicPath, "AppxManifest.xml");
File.WriteAllText(xmlFilePath, xmlStringMini);
}
// /// <summary>
// /// 生成打包布局
// /// </summary>
// static void GeneratePackagingLayoutXml(
// string rootPublicPath,
// string appxManifestXmlPath,
// Architecture processorArchitecture)
// {
// // 使用打包布局创建包
// // https://learn.microsoft.com/zh-cn/windows/msix/package/packaging-layout
// var xmlString =
//"""
//<PackagingLayout xmlns="http://schemas.microsoft.com/appx/makeappx/2017">
// <PackageFamily ID="MyGame" FlatBundle="true" ManifestPath="{0}" ResourceManager="false">
// <!-- {1} code package-->
// <Package ID="{1}" ProcessorArchitecture="{1}">
// <Files>
// <File DestinationPath="*" SourcePath="{2}"/>
// </Files>
// </Package>
// </PackageFamily>
//</PackagingLayout>
//"""u8;
// Stream stream = new MemoryStream();
// stream.WriteFormat(xmlString, new object?[]
// {
// appxManifestXmlPath,
// processorArchitecture.ToString().ToLowerInvariant(),
// rootPublicPath,
// });
// }
}
public static class SignTool
{
const string SignTool_exe = "SignTool.exe";
static string GetSignToolPath(WinVersion version)
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
path = Path.Combine(path, "Windows Kits", "10", "bin", $"10.0.{(ushort)version}.0", RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(), SignTool_exe);
return path;
}
static string GetSignToolPath()
{
var query = from m in Enum.GetValues<WinVersion>().OrderByDescending(x => (ushort)x)
let p = GetSignToolPath(m)
let exists = File.Exists(p)
where exists
select p;
var path = query.FirstOrDefault();
if (string.IsNullOrWhiteSpace(path))
throw new FileNotFoundException(SignTool_exe);
return path;
}
public static void Start(
string rootPublicPath)
{
var pwd = File.ReadAllText(@"C:\MSStore_CodeSigning.txt");
var psi = new ProcessStartInfo
{
FileName = GetSignToolPath(),
UseShellExecute = false,
Arguments =
$"""
sign /a /fd SHA384 /f "C:\MSStore_CodeSigning.pfx" /p {pwd} "{rootPublicPath}.msix"
""",
};
DotNetCLIHelper.StartProcessAndWaitForExit(psi);
}
@@ -1,3 +1,5 @@
IGenerateCaCertCommand.GenerateCodeSigningCert(IGenerateCaCertCommand.X500DistinguishedNameValue, "aaa");
var rootCommand = new RootCommand($"{AssemblyInfo.Product} Publish Tools");
var interfaceType = typeof(ICommand);
var addMethod = interfaceType.GetMethod(nameof(ICommand.AddCommand), BindingFlags.Static | BindingFlags.Public);
@@ -0,0 +1,43 @@
// ReSharper disable once CheckNamespace
namespace BD.WTTS;
static class ReflectionHelper
{
/// <summary>
/// 是否为内部调用,防止动态反射调用
/// </summary>
/// <param name="thisAssembly"></param>
/// <param name="stackTrace"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInternalCall(Assembly thisAssembly, StackTrace stackTrace)
{
if (stackTrace.FrameCount >= 1)
{
for (int i = 1; i < stackTrace.FrameCount; i++)
{
try
{
var method = stackTrace.GetFrame(i)?.GetMethod();
var assembly = method?.Module.Assembly;
if (assembly == thisAssembly)
{
return true;
}
}
catch
{
}
if (i == 2)
break;
}
}
return false;
}
/// <inheritdoc cref="IsInternalCall(Assembly, StackTrace)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInternalCall<T>(StackTrace stackTrace)
=> IsInternalCall(typeof(T).Assembly, stackTrace);
}
@@ -81,4 +81,9 @@ partial interface IPlugin
/// 插件安装时间
/// </summary>
DateTimeOffset InstallTime { get; }
/// <summary>
/// 插件发布时间
/// </summary>
DateTimeOffset ReleaseTime { get; }
}
@@ -23,7 +23,51 @@ partial class PluginBase
public virtual Type? SettingsPageViewType { get; }
public virtual string? Author { get; }
const string OfficialAuthor = "Steam++ 官方";
const string IllegalAuthor = "存在非法字符";
const string UnknownAuthor = "未知";
/// <summary>
/// 作者名字符串中是否存在非法字符
/// </summary>
/// <param name="author"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool IsIllegalAuthor(string author)
{
if (author.Contains("Steam++", StringComparison.OrdinalIgnoreCase) ||
author.Contains("Steam", StringComparison.OrdinalIgnoreCase) ||
author.Contains("steampp", StringComparison.OrdinalIgnoreCase) ||
author.Contains("Watt", StringComparison.OrdinalIgnoreCase) ||
author.Contains("WattToolkit", StringComparison.OrdinalIgnoreCase) ||
author.Contains("\u6B21\u5143\u8D85\u8D8A", StringComparison.OrdinalIgnoreCase) ||
author.Contains("\u51E1\u661F", StringComparison.OrdinalIgnoreCase) ||
author.Contains("\u7E41\u661F", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public string? Author
{
get
{
if (IsOfficial)
return OfficialAuthor;
var author = AuthorOriginalString;
if (string.IsNullOrWhiteSpace(author))
return UnknownAuthor;
if (IsIllegalAuthor(author))
return IllegalAuthor;
return author;
}
}
/// <summary>
/// 插件作者名(由插件程序集重写此属性填写)
/// </summary>
protected virtual string? AuthorOriginalString { get; }
public virtual string AuthorStoreUrl =>
$"https://steampp.net/store/plugins/authors/{WebUtility.UrlEncode(Author)}";
@@ -36,63 +80,109 @@ partial class PluginBase
public string CacheDirectory => mCacheDirectory.Value;
//static byte[][] OfficialPluginHashDatas => Array.Empty<byte[]>();
///// <summary>
///// 官方插件的哈希值 SHA256 数据,可使用 API 调用服务端获取
///// </summary>
//internal static SecurityCriticalOfficialPluginHashDatas OfficialPluginHashDatas { private get; set; }
bool? mIsOfficial;
//internal readonly record struct SecurityCriticalOfficialPluginHashDatas
//{
// internal SecurityCriticalOfficialPluginHashDatas(ImmutableArray<byte[]> value)
// {
// StackTrace stackTrace = new();
// var isInternalCall = ReflectionHelper.IsInternalCall<SecurityCriticalOfficialPluginHashDatas>(stackTrace);
// if (!isInternalCall)
// throw new ApplicationException("Disable reflection calls to this constructor.");
// Value = value;
// }
// internal ImmutableArray<byte[]> Value { get; init; }
// [MethodImpl(MethodImplOptions.AggressiveInlining)]
// internal bool IsDefault() => this == default || Value == default || Value.IsEmpty;
//}
/// <summary>
/// 是否为嵌入式插件,嵌入在程序安装目录中的
/// </summary>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool IsEmbeddedPlugin()
{
try
{
var rootPath = Path.GetFullPath(Path.Combine(AssemblyLocation,
#if DEBUG
"..", "..", "..", "..", ".."
#else
"..", ".."
#endif
));
var value = Environment.ProcessPath!.StartsWith(rootPath);
return value;
}
catch
{
}
return false;
}
public bool IsOfficial
{
get
get // 实时计算,不缓存结果
{
//#if DEBUG
//switch (Name)
//{
// case AssemblyInfo.Accelerator:
// case AssemblyInfo.ArchiSteamFarmPlus:
// case AssemblyInfo.Authenticator:
// case AssemblyInfo.GameAccount:
// case AssemblyInfo.GameList:
// case AssemblyInfo.GameTools:
// return true;
//}
//#endif
if (mIsOfficial.HasValue)
return mIsOfficial.Value;
//try
//{
// byte[] hashData;
// using (var fileStream = new FileStream(AssemblyLocation,
// FileMode.Open,
// FileAccess.Read,
// FileShare.ReadWrite | FileShare.Delete))
// {
// hashData = SHA256.HashData(fileStream);
// }
// if (PluginBase<TPlugin>.OfficialPluginHashDatas.Contains(hashData))
// {
// mIsOfficial = true;
// return true;
// }
//}
//catch
//{
// if (!OfficialPluginHashDatas.IsDefault())
// {
// try
// {
// byte[] hashData;
// using (var fileStream = new FileStream(AssemblyLocation,
// FileMode.Open,
// FileAccess.Read,
// FileShare.ReadWrite | FileShare.Delete))
// {
// hashData = SHA256.HashData(fileStream);
// }
// if (OfficialPluginHashDatas.Value.Contains(hashData))
// {
// return true;
// }
// }
// catch
// {
//}
// }
// }
//#if DEBUG
var thisType = GetType();
mIsOfficial = thisType.Name == "BD.WTTS.Plugins.Plugin" &&
var value = thisType.FullName == "BD.WTTS.Plugins.Plugin" &&
thisType.Assembly.GetName().Name?.TrimStart("BD.WTTS.Client.Plugins.") switch
{
AssemblyInfo.Accelerator or
AssemblyInfo.ArchiSteamFarmPlus or
AssemblyInfo.Authenticator or
AssemblyInfo.GameAccount or
AssemblyInfo.GameList or
AssemblyInfo.GameTools => true,
AssemblyInfo.Accelerator => IsEmbeddedPlugin() &&
UniqueEnglishName == AssemblyInfo.Accelerator &&
Id.ToString() == AssemblyInfo.AcceleratorId,
AssemblyInfo.ArchiSteamFarmPlus => IsEmbeddedPlugin() &&
UniqueEnglishName == AssemblyInfo.ArchiSteamFarmPlus &&
Id.ToString() == AssemblyInfo.ArchiSteamFarmPlusId,
AssemblyInfo.Authenticator => IsEmbeddedPlugin() &&
UniqueEnglishName == AssemblyInfo.Authenticator &&
Id.ToString() == AssemblyInfo.AuthenticatorId,
AssemblyInfo.GameAccount => IsEmbeddedPlugin() &&
UniqueEnglishName == AssemblyInfo.GameAccount &&
Id.ToString() == AssemblyInfo.GameAccountId,
AssemblyInfo.GameList => IsEmbeddedPlugin() &&
UniqueEnglishName == AssemblyInfo.GameList &&
Id.ToString() == AssemblyInfo.GameListId,
AssemblyInfo.GameTools => IsEmbeddedPlugin() &&
UniqueEnglishName == AssemblyInfo.GameTools &&
Id.ToString() == AssemblyInfo.GameToolsId,
_ => false,
};
return mIsOfficial.Value;
//mIsOfficial = false;
//return false;
return value;
//#else
// return false;
//#endif
}
}
@@ -114,6 +204,8 @@ partial class PluginBase
}
public DateTimeOffset InstallTime => mInstallTime.Value;
public DateTimeOffset ReleaseTime { get; internal set; }
}
partial class PluginBase<TPlugin>
+61 -39
View File
@@ -21,42 +21,16 @@ public static class PluginsCore
static DisablePluginsAssemblyLoadContext disablePluginsAssemblyLoadContext = new();
sealed class DisablePlugin : IPlugin
[DebuggerDisplay("{DebuggerDisplay,nq}")]
sealed record class DisablePlugin : IPlugin
{
public Guid Id { get; private set; }
public string Name { get; private set; } = null!;
public string UniqueEnglishName { get; private set; } = null!;
public string Version { get; private set; } = null!;
public string? Description { get; private set; }
public string StoreUrl { get; private set; } = null!;
public string HelpUrl { get; private set; } = null!;
public Type? SettingsPageViewType => null;
public string? Author { get; private set; }
public string AuthorStoreUrl { get; private set; } = null!;
public string AssemblyLocation { get; private set; } = null!;
public string AppDataDirectory { get; private set; } = null!;
public string CacheDirectory { get; private set; } = null!;
public bool IsOfficial { get; private set; }
public string? Icon { get; private set; }
public DateTimeOffset InstallTime { get; private set; }
public void SetValue(IPlugin plugin)
public DisablePlugin(IPlugin plugin)
{
StackTrace stackTrace = new();
var isInternalCall = ReflectionHelper.IsInternalCall<DisablePlugin>(stackTrace);
if (!isInternalCall)
throw new ApplicationException("Disable reflection calls to this constructor.");
Id = plugin.Id;
Name = plugin.Name;
UniqueEnglishName = plugin.UniqueEnglishName;
@@ -72,8 +46,47 @@ public static class PluginsCore
IsOfficial = plugin.IsOfficial;
Icon = plugin.Icon;
InstallTime = plugin.InstallTime;
ReleaseTime = plugin.ReleaseTime;
}
string DebuggerDisplay => $"{UniqueEnglishName} v{Version}";
public override string ToString() => DebuggerDisplay;
public Guid Id { get; init; }
public string Name { get; init; }
public string UniqueEnglishName { get; init; }
public string Version { get; init; }
public string? Description { get; init; }
public string StoreUrl { get; init; }
public string HelpUrl { get; init; }
public Type? SettingsPageViewType => null;
public string? Author { get; init; }
public string AuthorStoreUrl { get; init; }
public string AssemblyLocation { get; init; }
public string AppDataDirectory { get; init; }
public string CacheDirectory { get; init; }
public bool IsOfficial { get; init; }
public string? Icon { get; init; }
public DateTimeOffset InstallTime { get; init; }
public DateTimeOffset ReleaseTime { get; init; }
public void ConfigureDemandServices(IServiceCollection services, Startup startup)
{
}
@@ -309,9 +322,12 @@ public static class PluginsCore
using CompositionHost container = configuration.CreateContainer();
foreach (var plugin in container.GetExports<IPlugin>())
{
if (plugin.HasValue() && plugin is PluginBase)
if (plugin.HasValue() && plugin is PluginBase pluginBase)
{
plugins.Add(plugin);
#if DEBUG
var isOfficial = pluginBase.IsOfficial;
#endif
plugins.Add(pluginBase);
}
else
{
@@ -408,9 +424,15 @@ public static class PluginsCore
{
foreach (var plugin in disablePlugins)
{
var plugin_ = new DisablePlugin();
plugin_.SetValue(plugin);
pluginResults.Add(new(true, plugin_));
try
{
var plugin_ = new DisablePlugin(plugin);
pluginResults.Add(new(true, plugin_));
}
catch (Exception e)
{
Log.Error(TAG, e, "Failed to initialize disabled plugin.");
}
}
disablePluginsAssemblyLoadContext.Unload();
disablePluginsAssemblyLoadContext = null!;
+1
View File
@@ -115,6 +115,7 @@ public abstract partial class Startup
throw new ArgumentOutOfRangeException("CurrentThread != ApartmentState.STA");
}
}
#endif
try