NewLife/LuckyClover

[feat] 增加安装助手,包括net20/net40版本,用于安装主流dotNet运行时
大石头 authored at 2024-01-21 19:22:47
fa5c97d
Tree
1 Parent(s) 639fc98
Summary: 24 changed files with 4125 additions and 22 deletions.
Added +250 -0
Added +79 -0
Added +85 -0
Added +745 -0
Added +687 -0
Added +126 -0
Added +42 -0
Added +110 -0
Added +14 -0
Added +360 -0
Added +805 -0
Added +21 -0
Added +36 -0
Added +71 -0
Added +117 -0
Added +30 -0
Added +7 -0
Added +14 -0
Added +290 -0
Added +79 -0
Added +61 -0
Added +23 -0
Modified +65 -21
Modified +8 -1
Added +250 -0
diff --git a/Installer/Advapi32.cs b/Installer/Advapi32.cs
new file mode 100644
index 0000000..6d568f7
--- /dev/null
+++ b/Installer/Advapi32.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Installer;
+
+internal class SafeServiceHandle : SafeHandle
+{
+    public override Boolean IsInvalid
+    {
+        get
+        {
+            if (!(DangerousGetHandle() == IntPtr.Zero))
+            {
+                return DangerousGetHandle() == new IntPtr(-1);
+            }
+            return true;
+        }
+    }
+
+    internal SafeServiceHandle(IntPtr handle)
+        : base(IntPtr.Zero, true) => SetHandle(handle);
+
+    protected override Boolean ReleaseHandle() => Advapi32.CloseServiceHandle(handle);
+}
+
+internal class Advapi32
+{
+    [Flags]
+    public enum ServiceType
+    {
+        Adapter = 0x4,
+        FileSystemDriver = 0x2,
+        InteractiveProcess = 0x100,
+        KernelDriver = 0x1,
+        RecognizerDriver = 0x8,
+        Win32OwnProcess = 0x10,
+        Win32ShareProcess = 0x20
+    }
+
+    internal enum ControlOptions
+    {
+        Stop = 1,
+        Pause = 2,
+        Continue = 3,
+        Interrogate = 4,
+        Shutdown = 5,
+
+        PowerEvent = 13,
+        SessionChange = 14,
+        TimeChange = 16,
+    }
+
+    internal class ServiceOptions
+    {
+        internal const Int32 SERVICE_QUERY_CONFIG = 1;
+
+        internal const Int32 SERVICE_CHANGE_CONFIG = 2;
+
+        internal const Int32 SERVICE_QUERY_STATUS = 4;
+
+        internal const Int32 SERVICE_ENUMERATE_DEPENDENTS = 8;
+
+        internal const Int32 SERVICE_START = 16;
+
+        internal const Int32 SERVICE_STOP = 32;
+
+        internal const Int32 SERVICE_PAUSE_CONTINUE = 64;
+
+        internal const Int32 SERVICE_INTERROGATE = 128;
+
+        internal const Int32 SERVICE_USER_DEFINED_CONTROL = 256;
+
+        internal const Int32 SERVICE_ALL_ACCESS = 983551;
+
+        internal const Int32 STANDARD_RIGHTS_DELETE = 65536;
+
+        internal const Int32 STANDARD_RIGHTS_REQUIRED = 983040;
+    }
+
+    internal class ServiceControllerOptions
+    {
+        internal const Int32 SC_ENUM_PROCESS_INFO = 0;
+
+        internal const Int32 SC_MANAGER_CONNECT = 1;
+
+        internal const Int32 SC_MANAGER_CREATE_SERVICE = 2;
+
+        internal const Int32 SC_MANAGER_ENUMERATE_SERVICE = 4;
+
+        internal const Int32 SC_MANAGER_LOCK = 8;
+
+        internal const Int32 SC_MANAGER_MODIFY_BOOT_CONFIG = 32;
+
+        internal const Int32 SC_MANAGER_QUERY_LOCK_STATUS = 16;
+
+        internal const Int32 SC_MANAGER_ALL = 983103;
+    }
+
+    internal struct SERVICE_STATUS
+    {
+        public ServiceType serviceType;
+
+        public ServiceControllerStatus currentState;
+
+        public ControlsAccepted controlsAccepted;
+
+        public Int32 win32ExitCode;
+
+        public Int32 serviceSpecificExitCode;
+
+        public Int32 checkPoint;
+
+        public Int32 waitHint;
+    }
+
+    public enum ServiceControllerStatus : Int32
+    {
+        ContinuePending = 5,
+        Paused = 7,
+        PausePending = 6,
+        Running = 4,
+        StartPending = 2,
+        Stopped = 1,
+        StopPending = 3
+    }
+
+    [Flags]
+    public enum ControlsAccepted : Int32
+    {
+        NetBindChange = 0x00000010,
+        ParamChange = 0x00000008,
+        CanPauseAndContinue = 0x00000002,
+        /// <summary>系统将关闭</summary>
+        PreShutdown = 0x00000100,
+        CanShutdown = 0x00000004,
+        CanStop = 0x00000001,
+
+        //supported only by HandlerEx
+        HardwareProfileChange = 0x00000020,
+        /// <summary>电源状态更改</summary>
+        CanHandlePowerEvent = 0x00000040,
+        /// <summary>会话状态发生更改</summary>
+        CanHandleSessionChangeEvent = 0x00000080,
+        /// <summary>系统时间已更改</summary>
+        TimeChange = 0x00000200,
+        /// <summary>注册为已发生事件的服务触发事件</summary>
+        TriggerEvent = 0x00000400,
+        /// <summary>用户已开始重新启动</summary>
+        UserModeReboot = 0x00000800
+    }
+
+    public delegate void ServiceMainCallback(Int32 argCount, IntPtr argPointer);
+
+    [StructLayout(LayoutKind.Sequential)]
+    public class SERVICE_TABLE_ENTRY
+    {
+        public IntPtr name;
+
+        public ServiceMainCallback callback;
+    }
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    public struct SERVICE_DESCRIPTION
+    {
+        public IntPtr Description;
+    }
+
+    public enum PowerBroadcastStatus
+    {
+        BatteryLow = 9,
+        OemEvent = 11,
+        PowerStatusChange = 10,
+        QuerySuspend = 0,
+        QuerySuspendFailed = 2,
+        ResumeAutomatic = 18,
+        ResumeCritical = 6,
+        ResumeSuspend = 7,
+        Suspend = 4
+    }
+
+    public enum SessionChangeReason
+    {
+        ConsoleConnect = 1,
+        ConsoleDisconnect,
+        RemoteConnect,
+        RemoteDisconnect,
+        SessionLogon,
+        SessionLogoff,
+        SessionLock,
+        SessionUnlock,
+        SessionRemoteControl
+    }
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    public class WTSSESSION_NOTIFICATION
+    {
+        public Int32 size;
+
+        public Int32 sessionId;
+    }
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    public class SERVICE_TIMECHANGE_INFO
+    {
+        public Int64 NewTime;
+        public Int64 OldTime;
+    }
+
+    public delegate Int32 ServiceControlCallbackEx(ControlOptions control, Int32 eventType, IntPtr eventData, IntPtr eventContext);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    internal static extern Boolean CloseServiceHandle(IntPtr handle);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    internal static extern unsafe Boolean ControlService(SafeServiceHandle serviceHandle, ControlOptions control, SERVICE_STATUS* pStatus);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "OpenSCManagerW", SetLastError = true)]
+    internal static extern IntPtr OpenSCManager(String machineName, String databaseName, Int32 access);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "OpenServiceW", SetLastError = true)]
+    internal static extern IntPtr OpenService(SafeServiceHandle databaseHandle, String serviceName, Int32 access);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    internal static extern IntPtr CreateService(SafeServiceHandle databaseHandle, String lpSvcName, String lpDisplayName,
+                                                Int32 dwDesiredAccess, Int32 dwServiceType, Int32 dwStartType,
+                                                Int32 dwErrorControl, String lpPathName, String lpLoadOrderGroup,
+                                                Int32 lpdwTagId, String lpDependencies, String lpServiceStartName,
+                                                String lpPassword);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    public static extern Int32 DeleteService(SafeServiceHandle serviceHandle);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    internal static extern unsafe Boolean QueryServiceStatus(SafeServiceHandle serviceHandle, SERVICE_STATUS* pStatus);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, EntryPoint = "StartServiceW", SetLastError = true)]
+    internal static extern Boolean StartService(SafeServiceHandle serviceHandle, Int32 argNum, IntPtr argPtrs);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    public static extern unsafe Boolean SetServiceStatus(IntPtr serviceStatusHandle, SERVICE_STATUS* status);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    public static extern IntPtr RegisterServiceCtrlHandlerEx(String serviceName, ServiceControlCallbackEx callback, IntPtr userData);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    public static extern Boolean StartServiceCtrlDispatcher(IntPtr entry);
+
+    [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    public static extern Boolean ChangeServiceConfig2(SafeServiceHandle serviceHandle, Int32 dwInfoLevel, IntPtr pInfo);
+}
\ No newline at end of file
Added +79 -0
diff --git a/Installer/app.manifest b/Installer/app.manifest
new file mode 100644
index 0000000..2e0ad7a
--- /dev/null
+++ b/Installer/app.manifest
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+    <security>
+      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+        <!-- UAC 清单选项
+             如果想要更改 Windows 用户帐户控制级别,请使用
+             以下节点之一替换 requestedExecutionLevel 节点。
+
+        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
+        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
+        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />
+
+            指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
+            如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
+            元素。
+        -->
+        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
+      </requestedPrivileges>
+    </security>
+  </trustInfo>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
+           Windows 版本的列表。取消评论适当的元素,
+           Windows 将自动选择最兼容的环境。 -->
+
+      <!-- Windows Vista -->
+      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
+
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
+
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
+
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
+
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+
+    </application>
+  </compatibility>
+
+  <!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
+       自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
+       选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
+       在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
+       
+       将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
+  
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings>
+      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
+      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
+    </windowsSettings>
+  </application>
+  
+
+  <!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
+  <!--
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+          type="win32"
+          name="Microsoft.Windows.Common-Controls"
+          version="6.0.0.0"
+          processorArchitecture="*"
+          publicKeyToken="6595b64144ccf1df"
+          language="*"
+        />
+    </dependentAssembly>
+  </dependency>
+  -->
+
+</assembly>
Added +85 -0
diff --git a/Installer/DownloadHelper.cs b/Installer/DownloadHelper.cs
new file mode 100644
index 0000000..704c697
--- /dev/null
+++ b/Installer/DownloadHelper.cs
@@ -0,0 +1,85 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using NewLife;
+using NewLife.Http;
+using NewLife.Log;
+
+namespace BuildTool;
+
+public class DownloadHelper
+{
+    public String BaseUrl { get; set; } = "http://x.newlifex.com/dotnet/";
+
+    public String CachePath { get; set; }
+
+    public async Task DownloadAllAsync()
+    {
+        XTrace.WriteLine("开始检测下载所有组件");
+
+        await Check("dotnet-runtime-7.0.9-win-x64.exe");
+        await Check("dotnet-runtime-7.0.9-win-x86.exe");
+        await Check("dotnet-runtime-6.0.19-win-x64.exe");
+        await Check("dotnet-runtime-6.0.19-win-x86.exe");
+
+        await Check("ndp481-x86-x64-allos-enu.exe");
+        await Check("ndp481-x86-x64-allos-chs.exe");
+        await Check("ndp48-x86-x64-allos-enu.exe");
+        await Check("ndp48-x86-x64-allos-chs.exe");
+
+        await Check("dotNetFx40_Full_x86_x64.exe");
+        await Check("NetFx20SP2_x86.exe");
+
+        await Check("vc2019/VC_redist.x64.exe");
+        await Check("vc2019/VC_redist.x86.exe");
+        await Check("win7/Windows6.1-KB3063858-x64.msu");
+        await Check("win7/Windows6.1-KB3063858-x86.msu");
+        await Check("win7/MsRootCert.zip");
+
+        await Check("aspnetcore-runtime-6.0.20-1-loongarch64.deb");
+        await Check("aspnetcore-runtime-7.0.9-linux-arm64.tar.gz");
+        await Check("aspnetcore-runtime-7.0.9-linux-x64.tar.gz");
+        await Check("dotnet-sdk-3.1.7-rc1-mips64el.deb");
+
+        XTrace.WriteLine("所有组件下载完成!");
+    }
+
+    public async Task DownloadAppsAsync()
+    {
+        XTrace.WriteLine("开始检测下载所有应用");
+
+        await Check("star/staragent80.zip");
+        await Check("star/staragent60.zip");
+        await Check("star/staragent45.zip");
+
+        XTrace.WriteLine("所有应用下载完成!");
+    }
+
+    async Task Check(String file, String url = null)
+    {
+        XTrace.WriteLine("检查:{0}", file);
+
+        var fi = CachePath.CombinePath(file);
+        if (File.Exists(fi)) return;
+
+        var p = Path.GetDirectoryName(fi);
+        if (!Directory.Exists(p)) Directory.CreateDirectory(p);
+
+        if (String.IsNullOrEmpty(url)) url = file;
+        if (!url.StartsWith("http")) url = BaseUrl.EnsureEnd("/") + url;
+
+        XTrace.WriteLine("下载:{0}", url);
+
+#if NET40_OR_GREATER || NETCOREAPP
+        using var client = new HttpClient();
+        await client.DownloadFileAsync(url, fi);
+#else
+        using var client = new WebClient();
+        client.DownloadFile(url, fi);
+#endif
+
+        XTrace.WriteLine("下载完成!");
+    }
+}
Added +745 -0
diff --git a/Installer/FrmMain.cs b/Installer/FrmMain.cs
new file mode 100644
index 0000000..ab186b1
--- /dev/null
+++ b/Installer/FrmMain.cs
@@ -0,0 +1,745 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Web;
+using System.Windows.Forms;
+using BuildTool;
+using NewLife;
+using NewLife.Log;
+using NewLife.Reflection;
+using NewLife.Serialization;
+using NewLife.Threading;
+using NewLife.Web;
+using Timer = System.Threading.Timer;
+#if !NET40
+using TaskEx = System.Threading.Tasks.Task;
+#endif
+
+namespace Installer;
+
+#if NET40
+#else
+//delegate void Func();
+delegate void Action();
+#endif
+
+public partial class FrmMain : Form
+{
+    private MachineInfo _info;
+    private Boolean _x64;
+    private String _installPath;
+    private String _serviceName = "StarAgent";
+    private ITracer _tracer;
+
+    public FrmMain()
+    {
+        InitializeComponent();
+
+#if NET45
+        _tracer = DefaultTracer.Instance;
+#endif
+    }
+
+    private void FrmMain_Load(Object sender, EventArgs e)
+    {
+        rtbLog.UseWinFormControl();
+
+        using var span = _tracer?.NewSpan(nameof(FrmMain_Load));
+
+        var asm = AssemblyX.Create(Assembly.GetExecutingAssembly());
+        Text = String.Format("{2} v{0} {1:HH:mm:ss}", asm.FileVersion, asm.Compile, Text);
+
+        var mi = new MachineInfo();
+        mi.Init();
+        span?.SetTag(mi);
+        XTrace.WriteLine(mi.ToJson(true));
+        _info = mi;
+
+        txtMachince.Text = Environment.MachineName;
+        txtUser.Text = Environment.UserName;
+
+        txtOS.Text = mi.OSName;
+        txtVersion.Text = mi.OSVersion;
+
+        txtProduct.Text = mi.Product;
+        txtSerial.Text = mi.Serial;
+
+        _x64 = IntPtr.Size == 8;
+
+        var f = "server.txt";
+        if (File.Exists(f))
+        {
+            var url = File.ReadAllText(f)?.Trim();
+            if (!url.IsNullOrWhiteSpace()) txtServer.Text = url;
+        }
+
+        _timer = new Timer(Detect, null, 0, 30_000);
+
+        ThreadPoolX.QueueUserWorkItem(DetectTarget);
+    }
+
+    #region 运行时环境
+    Timer _timer;
+    private Boolean _firstDetect;
+    private void Detect(Object state)
+    {
+        using var span = _tracer?.NewSpan(nameof(Detect));
+
+        var net = new NetRuntime();
+        var vers = net.GetVers();
+        span?.SetTag(vers);
+
+        if (!_firstDetect)
+        {
+            _firstDetect = true;
+
+            XTrace.WriteLine("已安装NET运行时版本:");
+            foreach (var item in vers)
+            {
+                if (String.IsNullOrEmpty(item.Sp))
+                    XTrace.WriteLine("{0,-10} {1}", item.Name, item.Version);
+                else
+                    XTrace.WriteLine("{0,-10} {1} Sp{2}", item.Name, item.Version, item.Sp);
+            }
+        }
+
+        this.Invoke(new Action(() =>
+        {
+            if (vers.Any(e => e.Name.StartsWith("v2.0")))
+                cbNET2.Checked = true;
+
+            if (vers.Any(e => e.Name.StartsWith("v4.0")))
+                cbNET4.Checked = true;
+            else
+                btnNET4.Enabled = true;
+
+            if (vers.Any(e => e.Name.StartsWith("v4.8")))
+                cbNET48.Checked = true;
+            else
+                btnNET48.Enabled = true;
+
+            if (vers.Any(e => e.Name.StartsWith("v6.")))
+                cbNET6.Checked = true;
+            else
+                btnNET6.Enabled = true;
+
+            if (vers.Any(e => e.Name.StartsWith("v7.")))
+                cbNET7.Checked = true;
+            else
+                btnNET7.Enabled = true;
+        }));
+
+        var svc = new WindowsService();
+        if (svc.IsInstalled(_serviceName))
+        {
+            this.Invoke(new Action(() =>
+            {
+                btnUninstall.Enabled = true;
+            }));
+
+            var bRunning = svc.IsRunning(_serviceName);
+            XTrace.WriteLine("软件{0}已安装!{1}", _serviceName, bRunning ? "正在运行!" : "未运行!");
+        }
+        else
+        {
+            XTrace.WriteLine("软件{0}未安装!", _serviceName);
+        }
+    }
+
+    NetRuntime GetNetRuntime(Boolean silent = false)
+    {
+        var net = new NetRuntime
+        {
+            BaseUrl = txtServer.Text.EnsureEnd("/") + "dotnet",
+            Silent = silent,
+            InstallPath = _installPath,
+            Hashs = NetRuntime.LoadMD5s(),
+        };
+#if NET45
+        net.Tracer = DefaultTracer.Instance;
+#endif
+
+        return net;
+    }
+
+    void ShowResult(Boolean rs)
+    {
+        if (rs)
+            MessageBox.Show("安装成功!", "", MessageBoxButtons.OK, MessageBoxIcon.Information);
+        else
+            MessageBox.Show("安装失败!", "", MessageBoxButtons.OK, MessageBoxIcon.Error);
+    }
+
+    private void btnNET4_Click(Object sender, EventArgs e)
+    {
+        var net = GetNetRuntime();
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            var rs = net.InstallNet40();
+
+            _timer.Change(0, 30_000);
+
+            ShowResult(rs);
+        });
+    }
+
+    private void btnNET48_Click(Object sender, EventArgs e)
+    {
+        var net = GetNetRuntime();
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            var rs = net.InstallNet48();
+
+            _timer.Change(0, 30_000);
+
+            ShowResult(rs);
+        });
+    }
+
+    private void btnNET6_Click(Object sender, EventArgs e)
+    {
+        var net = GetNetRuntime();
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            var rs = net.InstallNet6();
+
+            _timer.Change(0, 30_000);
+
+            ShowResult(rs);
+        });
+    }
+
+    private void btnNET7_Click(Object sender, EventArgs e)
+    {
+        var net = GetNetRuntime();
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            var rs = net.InstallNet7();
+
+            _timer.Change(0, 30_000);
+
+            ShowResult(rs);
+        });
+    }
+
+    private void btnFinal_Click(Object sender, EventArgs e)
+    {
+        var rs = MessageBox.Show("实在安装不上NET4时的终极大招,可能对系统文件有损坏!\r\n修复过程中可能自动重启系统,是否确定继续?", "危险提示", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning);
+        if (rs == DialogResult.Cancel) return;
+
+        var net = GetNetRuntime();
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            net.Install("netfxrepairtool.exe", null, "/repair /passive");
+
+            _timer.Change(0, 30_000);
+
+            MessageBox.Show("专家模式修复完成,请重新安装.NET运行时!");
+        });
+    }
+    #endregion
+
+    #region 服务通信
+    private void btnNetwork_Click(Object sender, EventArgs e)
+    {
+        var url = txtServer.Text;
+        if (url.IsNullOrEmpty()) return;
+
+        using var span = _tracer?.NewSpan(nameof(btnNetwork_Click));
+        try
+        {
+            url = url.EnsureEnd("/") + "api";
+            var client = new WebClientX();
+            var html = client.GetHtml(url);
+            if (html.IsNullOrEmpty()) return;
+
+            XTrace.WriteLine(html);
+
+            MessageBox.Show("测试通过");
+        }
+        catch (Exception ex)
+        {
+            span?.SetError(ex, null);
+            throw;
+        }
+    }
+
+    private void btnDownloadAll_Click(Object sender, EventArgs e)
+    {
+        var net = GetNetRuntime();
+        XTrace.WriteLine("目标目录:{0}", net.CachePath);
+
+        // 检测缓存目录所有文件,如果哈希不对,直接删除
+        foreach (var item in net.CachePath.AsDirectory().GetFiles("*.*", SearchOption.AllDirectories))
+        {
+            if (net.Hashs.TryGetValue(item.Name, out var hash))
+            {
+                var md5 = NetRuntime.GetMD5(item.FullName);
+                if (hash != md5)
+                {
+                    XTrace.WriteLine("文件MD5校验失败,删除:{0}", item.FullName);
+                    File.Delete(item.FullName);
+                }
+            }
+        }
+
+        TaskEx.Run(async () =>
+        {
+            var helper = new DownloadHelper { CachePath = net.CachePath };
+            await helper.DownloadAllAsync();
+
+            helper.BaseUrl = net.BaseUrl;
+            await helper.DownloadAppsAsync();
+        });
+    }
+    #endregion
+
+    #region 自动安装软件
+    void DetectTarget()
+    {
+        using var span = _tracer?.NewSpan(nameof(DetectTarget));
+
+        var osVer = Environment.OSVersion.Version;
+        var str = "";
+
+        //A方案,NET7
+        //B方案,NET6
+        //C方案,NET4
+        //D方案,NET2
+
+        Invoke(new Action(() =>
+        {
+            var btns = new[] { btnInstallA, btnInstallB, btnInstallC, btnInstallD };
+            foreach (var btn in btns)
+            {
+                btn.Enabled = false;
+            }
+
+            // WinXP
+            if (osVer.Major <= 5)
+            {
+                btnInstallC.Enabled = true;
+                btnInstallD.Enabled = true;
+                str = "WinXP";
+            }
+            // Vista
+            else if (osVer.Major == 6 && osVer.Minor == 0)
+            {
+                btnInstallC.Enabled = true;
+                btnInstallD.Enabled = true;
+                str = "Vista";
+            }
+            else if (osVer.Major == 6 && osVer.Minor == 1)
+            {
+                // Win7
+                if (osVer.Revision <= 7600)
+                {
+                    btnInstallC.Enabled = true;
+                    btnInstallD.Enabled = true;
+                    str = "Win7";
+                }
+                else
+                // Win7Sp1
+                {
+                    btnInstallB.Enabled = true;
+                    btnInstallC.Enabled = true;
+                    btnInstallD.Enabled = true;
+                    str = "Win7Sp1";
+                }
+            }
+            // Win10/Win11
+            else if (osVer.Major >= 10)
+            {
+                btnInstallA.Enabled = true;
+                btnInstallB.Enabled = true;
+                //btnInstallC.Enabled = true;
+                str = "Win10/Win11";
+            }
+            else
+            {
+                btnInstallA.Enabled = true;
+                btnInstallB.Enabled = true;
+                //btnInstallC.Enabled = true;
+                str = txtOS.Text;
+            }
+
+            // 在.NET4.0中,禁止安装D方案(.NET2.0)
+#if NET40
+            btnInstallD.Enabled = false;
+#endif
+
+            if (_x64)
+                str += "(64位)";
+            else
+                str += "(32位)";
+            txtTarget.Text = str;
+
+            span?.SetTag(str);
+        }));
+
+        // 检测磁盘,决定安装目录
+        var dis = DriveInfo.GetDrives();
+        if (dis.Any(e => e.Name == @"D:\" && e.DriveType == DriveType.Fixed))
+            _installPath = @"D:\agent\";
+        else if (dis.Any(e => e.Name == @"C:\" && e.DriveType == DriveType.Fixed))
+            _installPath = @"C:\agent\";
+        else
+            _installPath = dis[0].Name.CombinePath("agent");
+
+        XTrace.WriteLine("安装路径:{0}", _installPath);
+    }
+
+    /// <summary>安装服务。启动进程</summary>
+    /// <param name="svc"></param>
+    /// <returns></returns>
+    Boolean InstallService(WindowsService svc, String url)
+    {
+        var file = @"C:\agent\StarAgent.exe";
+        if (!File.Exists(file))
+        {
+            XTrace.WriteLine("找不到客户端文件");
+            return false;
+        }
+
+        // 强制使用外部url地址控制内部文件
+        var txt = Path.GetDirectoryName(file).CombinePath("server.txt");
+        //if (!File.Exists(txt) && !url.IsNullOrEmpty())
+        if (!url.IsNullOrEmpty())
+        {
+            File.WriteAllText(txt, url);
+        }
+
+        var si = new ProcessStartInfo
+        {
+            FileName = file,
+            Arguments = "-install",
+
+            WindowStyle = ProcessWindowStyle.Hidden,
+            CreateNoWindow = true,
+
+            UseShellExecute = true,
+            //RedirectStandardOutput = true,
+            //RedirectStandardError = true,
+        };
+
+        var p = new Process { StartInfo = si };
+        //p.OutputDataReceived += (s, e) => { if (e.Data != null) XTrace.WriteLine(e.Data); };
+        //p.ErrorDataReceived += (s, e) => { if (e.Data != null) XTrace.Log.Error(e.Data); };
+
+        p.Start();
+        //p.BeginOutputReadLine();
+        //p.BeginErrorReadLine();
+
+        if (!p.WaitForExit(15_000)) return false;
+
+        return true;
+    }
+
+    private void btnInstallA_Click(Object sender, EventArgs e)
+    {
+        var svc = new WindowsService();
+        if (!svc.IsAdministrator())
+        {
+            MessageBox.Show("需要管理员权限运行本软件!");
+            return;
+        }
+
+        groupBox3.Enabled = false;
+        var url = txtServer.Text;
+
+        var server = txtServer.Text;
+        var net = GetNetRuntime(true);
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            net.InstallNet7();
+            _timer.Change(1000, 30_000);
+            //net.InstallApp("gbt-agent7.zip");
+
+            try
+            {
+                if (svc.IsRunning(_serviceName)) svc.Stop(_serviceName);
+            }
+            catch (Exception ex)
+            {
+                XTrace.WriteException(ex);
+            }
+
+            var rs = net.Install("gbt-win10-mini.exe", null);
+            if (rs) rs = InstallService(svc, url);
+            _timer.Change(1000, 30_000);
+
+            PostLog(server, nameof(btnInstallA_Click));
+
+            if (!rs)
+                MessageBox.Show("安装失败!");
+            else
+                Invoke(new Action(() =>
+                {
+                    groupBox3.Enabled = true;
+
+                    MessageBox.Show("安装完成!");
+                }));
+        });
+    }
+
+    private void btnInstallB_Click(Object sender, EventArgs e)
+    {
+        var svc = new WindowsService();
+        if (!svc.IsAdministrator())
+        {
+            MessageBox.Show("需要管理员权限运行本软件!");
+            return;
+        }
+
+        groupBox3.Enabled = false;
+        var url = txtServer.Text;
+
+        var server = txtServer.Text;
+        var net = GetNetRuntime(true);
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            net.InstallNet6();
+            _timer.Change(1000, 30_000);
+            //net.InstallApp("gbt-agent6.zip");
+
+            try
+            {
+                if (svc.IsRunning(_serviceName)) svc.Stop(_serviceName);
+            }
+            catch (Exception ex)
+            {
+                XTrace.WriteException(ex);
+            }
+
+            var rs = false;
+            if (_x64)
+                rs = net.Install("gbt-win7-mini.exe", null);
+            else
+                rs = net.Install("gbt-win7x86-mini.exe", null);
+
+            if (rs) rs = InstallService(svc, url);
+            _timer.Change(1000, 30_000);
+
+            PostLog(server, nameof(btnInstallB_Click));
+
+            if (!rs)
+                MessageBox.Show("安装失败!");
+            else
+                Invoke(new Action(() =>
+                {
+                    groupBox3.Enabled = true;
+
+                    MessageBox.Show("安装完成!");
+                }));
+        });
+    }
+
+    private void btnInstallC_Click(Object sender, EventArgs e)
+    {
+        var svc = new WindowsService();
+        if (!svc.IsAdministrator())
+        {
+            MessageBox.Show("需要管理员权限运行本软件!");
+            return;
+        }
+
+        groupBox3.Enabled = false;
+        var url = txtServer.Text;
+
+        var server = txtServer.Text;
+        var net = GetNetRuntime(true);
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            net.InstallNet40();
+            _timer.Change(1000, 30_000);
+            //net.InstallApp("gbt-agent4.zip");
+
+            try
+            {
+                if (svc.IsRunning(_serviceName)) svc.Stop(_serviceName);
+            }
+            catch (Exception ex)
+            {
+                XTrace.WriteException(ex);
+            }
+
+            var rs = net.Install("gbt-winxp-mini.exe", null);
+            if (rs) rs = InstallService(svc, url);
+            _timer.Change(1000, 30_000);
+
+            PostLog(server, nameof(btnInstallC_Click));
+
+            if (!rs)
+                MessageBox.Show("安装失败!");
+            else
+                Invoke(new Action(() =>
+                {
+                    groupBox3.Enabled = true;
+
+                    MessageBox.Show("安装完成!");
+                }));
+        });
+    }
+
+    private void btnInstallD_Click(Object sender, EventArgs e)
+    {
+        var svc = new WindowsService();
+        if (!svc.IsAdministrator())
+        {
+            MessageBox.Show("需要管理员权限运行本软件!");
+            return;
+        }
+
+        groupBox3.Enabled = false;
+        var url = txtServer.Text;
+
+        var server = txtServer.Text;
+        var net = GetNetRuntime(true);
+        ThreadPoolX.QueueUserWorkItem(() =>
+        {
+            //net.InstallNet40();
+            //_timer.Change(1000, 30_000);
+
+            try
+            {
+                if (svc.IsRunning(_serviceName)) svc.Stop(_serviceName);
+            }
+            catch (Exception ex)
+            {
+                XTrace.WriteException(ex);
+            }
+
+            var rs = net.Install("gbt-agent20-mini.exe", null);
+            if (rs) rs = InstallService(svc, url);
+            _timer.Change(1000, 30_000);
+
+            PostLog(server, nameof(btnInstallD_Click));
+
+            if (!rs)
+                MessageBox.Show("安装失败!");
+            else
+                Invoke(new Action(() =>
+                {
+                    groupBox3.Enabled = true;
+
+                    MessageBox.Show("安装完成!");
+                }));
+        });
+    }
+
+    private void btnUninstall_Click(Object sender, EventArgs e)
+    {
+        var svc = new WindowsService();
+        if (!svc.IsAdministrator())
+        {
+            MessageBox.Show("需要管理员权限运行本软件!");
+            return;
+        }
+
+        var server = txtServer.Text;
+        using var span = _tracer?.NewSpan(nameof(btnUninstall_Click));
+        try
+        {
+            try
+            {
+                svc.Stop(_serviceName);
+            }
+            catch (Exception ex)
+            {
+                XTrace.WriteException(ex);
+            }
+            try
+            {
+                svc.Remove(_serviceName);
+            }
+            catch (Exception ex)
+            {
+                XTrace.WriteException(ex);
+            }
+
+            _timer.Change(1000, 30_000);
+
+            PostLog(server, nameof(btnUninstall_Click));
+        }
+        catch (Exception ex)
+        {
+            span?.SetError(ex, null);
+            //XTrace.WriteException(ex);
+            throw;
+        }
+    }
+    #endregion
+
+    #region 辅助
+    private void btnReport_Click(Object sender, EventArgs e)
+    {
+        PostLog(txtServer.Text, nameof(btnReport_Click));
+    }
+
+    void PostLog(String server, String action)
+    {
+        // 上传客户端日志
+        var di = @"C:\agent\Log".AsDirectory();
+        if (di.Exists)
+        {
+            var fi = di.GetFiles("*.log").OrderByDescending(e => e.LastWriteTime).FirstOrDefault();
+            if (fi != null) PostLog(server, fi, action);
+        }
+
+        // 上传自己的日志
+        di = @".\Log".AsDirectory();
+        if (di.Exists)
+        {
+            var fi = di.GetFiles("*.log").OrderByDescending(e => e.LastWriteTime).FirstOrDefault();
+            if (fi != null) PostLog(server, fi, action);
+        }
+    }
+
+    void PostLog(String server, FileInfo fi, String action)
+    {
+        using var fs = new FileStream(fi.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+        // 取最后100K
+        var retain = fs.Length - 100 * 1024;
+        if (retain > 0) fs.Seek(retain, SeekOrigin.Begin);
+
+        XTrace.WriteLine("发现日志:{0}", fi.FullName);
+        XTrace.WriteLine("日志大小:{0}", fs.Length);
+
+        var buf = new Byte[fs.Length - fs.Position];
+        var count = fs.Read(buf, 0, buf.Length);
+
+        buf = buf.Compress();
+        XTrace.WriteLine("压缩大小:{0}", buf.Length);
+
+#if NET40_OR_GREATER
+        var http = new HttpClient { BaseAddress = new Uri(server) };
+        var headers = http.DefaultRequestHeaders;
+#else
+        var http = new WebClientX { BaseAddress = server };
+        var headers = http.Headers;
+#endif
+        headers.Add("X-Action", action);
+        headers.Add("X-ClientId", _info.UUID);
+        headers.Add("X-MachineName", Environment.MachineName);
+        headers.Add("X-UserName", Environment.UserName);
+        headers.Add("X-IP", NetHelper.MyIP() + "");
+
+#if NET40_OR_GREATER
+        var content = new ByteArrayContent(buf);
+        var rs = http.PostAsync("/log", content).Result;
+        rs.EnsureSuccessStatusCode();
+        var html = rs.Content.ReadAsStringAsync().Result;
+#else
+        var rs = http.UploadData("/log", buf);
+        var html = rs.ToStr();
+#endif
+
+        XTrace.WriteLine("上传完成!{0}", html);
+    }
+    #endregion
+}
\ No newline at end of file
Added +687 -0
diff --git a/Installer/FrmMain.Designer.cs b/Installer/FrmMain.Designer.cs
new file mode 100644
index 0000000..73529ad
--- /dev/null
+++ b/Installer/FrmMain.Designer.cs
@@ -0,0 +1,687 @@
+namespace Installer
+{
+    partial class FrmMain
+    {
+        /// <summary>
+        /// 必需的设计器变量。
+        /// </summary>
+        private System.ComponentModel.IContainer components = null;
+
+        /// <summary>
+        /// 清理所有正在使用的资源。
+        /// </summary>
+        /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing && (components != null))
+            {
+                components.Dispose();
+            }
+            base.Dispose(disposing);
+        }
+
+        #region Windows 窗体设计器生成的代码
+
+        /// <summary>
+        /// 设计器支持所需的方法 - 不要修改
+        /// 使用代码编辑器修改此方法的内容。
+        /// </summary>
+        private void InitializeComponent()
+        {
+            this.components = new System.ComponentModel.Container();
+            this.groupBox1 = new System.Windows.Forms.GroupBox();
+            this.txtSerial = new System.Windows.Forms.TextBox();
+            this.label5 = new System.Windows.Forms.Label();
+            this.txtProduct = new System.Windows.Forms.TextBox();
+            this.label6 = new System.Windows.Forms.Label();
+            this.txtVersion = new System.Windows.Forms.TextBox();
+            this.label3 = new System.Windows.Forms.Label();
+            this.txtOS = new System.Windows.Forms.TextBox();
+            this.label4 = new System.Windows.Forms.Label();
+            this.txtUser = new System.Windows.Forms.TextBox();
+            this.label2 = new System.Windows.Forms.Label();
+            this.txtMachince = new System.Windows.Forms.TextBox();
+            this.label1 = new System.Windows.Forms.Label();
+            this.groupBox2 = new System.Windows.Forms.GroupBox();
+            this.btnFinal = new System.Windows.Forms.Button();
+            this.cbNET48 = new System.Windows.Forms.CheckBox();
+            this.lbNET48 = new System.Windows.Forms.Label();
+            this.btnNET48 = new System.Windows.Forms.Button();
+            this.label9 = new System.Windows.Forms.Label();
+            this.btnNET7 = new System.Windows.Forms.Button();
+            this.btnNET6 = new System.Windows.Forms.Button();
+            this.btnNET4 = new System.Windows.Forms.Button();
+            this.cbNET7 = new System.Windows.Forms.CheckBox();
+            this.lbNET7 = new System.Windows.Forms.Label();
+            this.cbNET6 = new System.Windows.Forms.CheckBox();
+            this.lbNET6 = new System.Windows.Forms.Label();
+            this.cbNET4 = new System.Windows.Forms.CheckBox();
+            this.lbNET4 = new System.Windows.Forms.Label();
+            this.cbNET2 = new System.Windows.Forms.CheckBox();
+            this.lbNET2 = new System.Windows.Forms.Label();
+            this.groupBox3 = new System.Windows.Forms.GroupBox();
+            this.btnDownloadAll = new System.Windows.Forms.Button();
+            this.btnInstallD = new System.Windows.Forms.Button();
+            this.btnReport = new System.Windows.Forms.Button();
+            this.label10 = new System.Windows.Forms.Label();
+            this.btnUninstall = new System.Windows.Forms.Button();
+            this.btnInstallC = new System.Windows.Forms.Button();
+            this.btnInstallB = new System.Windows.Forms.Button();
+            this.btnInstallA = new System.Windows.Forms.Button();
+            this.txtTarget = new System.Windows.Forms.TextBox();
+            this.label8 = new System.Windows.Forms.Label();
+            this.btnNetwork = new System.Windows.Forms.Button();
+            this.txtServer = new System.Windows.Forms.TextBox();
+            this.label7 = new System.Windows.Forms.Label();
+            this.groupBox4 = new System.Windows.Forms.GroupBox();
+            this.rtbLog = new System.Windows.Forms.RichTextBox();
+            this.toolTip1 = new System.Windows.Forms.ToolTip(this.components);
+            this.groupBox1.SuspendLayout();
+            this.groupBox2.SuspendLayout();
+            this.groupBox3.SuspendLayout();
+            this.groupBox4.SuspendLayout();
+            this.SuspendLayout();
+            // 
+            // groupBox1
+            // 
+            this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.groupBox1.Controls.Add(this.txtSerial);
+            this.groupBox1.Controls.Add(this.label5);
+            this.groupBox1.Controls.Add(this.txtProduct);
+            this.groupBox1.Controls.Add(this.label6);
+            this.groupBox1.Controls.Add(this.txtVersion);
+            this.groupBox1.Controls.Add(this.label3);
+            this.groupBox1.Controls.Add(this.txtOS);
+            this.groupBox1.Controls.Add(this.label4);
+            this.groupBox1.Controls.Add(this.txtUser);
+            this.groupBox1.Controls.Add(this.label2);
+            this.groupBox1.Controls.Add(this.txtMachince);
+            this.groupBox1.Controls.Add(this.label1);
+            this.groupBox1.Location = new System.Drawing.Point(14, 14);
+            this.groupBox1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox1.Name = "groupBox1";
+            this.groupBox1.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox1.Size = new System.Drawing.Size(1005, 157);
+            this.groupBox1.TabIndex = 0;
+            this.groupBox1.TabStop = false;
+            this.groupBox1.Text = "系统信息";
+            // 
+            // txtSerial
+            // 
+            this.txtSerial.Location = new System.Drawing.Point(623, 103);
+            this.txtSerial.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtSerial.Name = "txtSerial";
+            this.txtSerial.ReadOnly = true;
+            this.txtSerial.Size = new System.Drawing.Size(360, 28);
+            this.txtSerial.TabIndex = 11;
+            // 
+            // label5
+            // 
+            this.label5.AutoSize = true;
+            this.label5.Location = new System.Drawing.Point(541, 109);
+            this.label5.Name = "label5";
+            this.label5.Size = new System.Drawing.Size(80, 18);
+            this.label5.TabIndex = 10;
+            this.label5.Text = "序列号:";
+            // 
+            // txtProduct
+            // 
+            this.txtProduct.Location = new System.Drawing.Point(127, 103);
+            this.txtProduct.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtProduct.Name = "txtProduct";
+            this.txtProduct.ReadOnly = true;
+            this.txtProduct.Size = new System.Drawing.Size(360, 28);
+            this.txtProduct.TabIndex = 9;
+            // 
+            // label6
+            // 
+            this.label6.AutoSize = true;
+            this.label6.Location = new System.Drawing.Point(62, 109);
+            this.label6.Name = "label6";
+            this.label6.Size = new System.Drawing.Size(62, 18);
+            this.label6.TabIndex = 8;
+            this.label6.Text = "产品:";
+            // 
+            // txtVersion
+            // 
+            this.txtVersion.Location = new System.Drawing.Point(623, 66);
+            this.txtVersion.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtVersion.Name = "txtVersion";
+            this.txtVersion.ReadOnly = true;
+            this.txtVersion.Size = new System.Drawing.Size(360, 28);
+            this.txtVersion.TabIndex = 7;
+            // 
+            // label3
+            // 
+            this.label3.AutoSize = true;
+            this.label3.Location = new System.Drawing.Point(558, 72);
+            this.label3.Name = "label3";
+            this.label3.Size = new System.Drawing.Size(62, 18);
+            this.label3.TabIndex = 6;
+            this.label3.Text = "版本:";
+            // 
+            // txtOS
+            // 
+            this.txtOS.Location = new System.Drawing.Point(127, 66);
+            this.txtOS.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtOS.Name = "txtOS";
+            this.txtOS.ReadOnly = true;
+            this.txtOS.Size = new System.Drawing.Size(360, 28);
+            this.txtOS.TabIndex = 5;
+            // 
+            // label4
+            // 
+            this.label4.AutoSize = true;
+            this.label4.Location = new System.Drawing.Point(28, 72);
+            this.label4.Name = "label4";
+            this.label4.Size = new System.Drawing.Size(98, 18);
+            this.label4.TabIndex = 4;
+            this.label4.Text = "操作系统:";
+            // 
+            // txtUser
+            // 
+            this.txtUser.Location = new System.Drawing.Point(623, 29);
+            this.txtUser.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtUser.Name = "txtUser";
+            this.txtUser.ReadOnly = true;
+            this.txtUser.Size = new System.Drawing.Size(360, 28);
+            this.txtUser.TabIndex = 3;
+            // 
+            // label2
+            // 
+            this.label2.AutoSize = true;
+            this.label2.Location = new System.Drawing.Point(558, 35);
+            this.label2.Name = "label2";
+            this.label2.Size = new System.Drawing.Size(62, 18);
+            this.label2.TabIndex = 2;
+            this.label2.Text = "用户:";
+            // 
+            // txtMachince
+            // 
+            this.txtMachince.Location = new System.Drawing.Point(127, 29);
+            this.txtMachince.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtMachince.Name = "txtMachince";
+            this.txtMachince.ReadOnly = true;
+            this.txtMachince.Size = new System.Drawing.Size(360, 28);
+            this.txtMachince.TabIndex = 1;
+            // 
+            // label1
+            // 
+            this.label1.AutoSize = true;
+            this.label1.Location = new System.Drawing.Point(45, 35);
+            this.label1.Name = "label1";
+            this.label1.Size = new System.Drawing.Size(80, 18);
+            this.label1.TabIndex = 0;
+            this.label1.Text = "机器名:";
+            // 
+            // groupBox2
+            // 
+            this.groupBox2.Controls.Add(this.btnFinal);
+            this.groupBox2.Controls.Add(this.cbNET48);
+            this.groupBox2.Controls.Add(this.lbNET48);
+            this.groupBox2.Controls.Add(this.btnNET48);
+            this.groupBox2.Controls.Add(this.label9);
+            this.groupBox2.Controls.Add(this.btnNET7);
+            this.groupBox2.Controls.Add(this.btnNET6);
+            this.groupBox2.Controls.Add(this.btnNET4);
+            this.groupBox2.Controls.Add(this.cbNET7);
+            this.groupBox2.Controls.Add(this.lbNET7);
+            this.groupBox2.Controls.Add(this.cbNET6);
+            this.groupBox2.Controls.Add(this.lbNET6);
+            this.groupBox2.Controls.Add(this.cbNET4);
+            this.groupBox2.Controls.Add(this.lbNET4);
+            this.groupBox2.Controls.Add(this.cbNET2);
+            this.groupBox2.Controls.Add(this.lbNET2);
+            this.groupBox2.Location = new System.Drawing.Point(755, 179);
+            this.groupBox2.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox2.Name = "groupBox2";
+            this.groupBox2.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox2.Size = new System.Drawing.Size(263, 274);
+            this.groupBox2.TabIndex = 1;
+            this.groupBox2.TabStop = false;
+            this.groupBox2.Text = "运行时环境(专家级)";
+            // 
+            // btnFinal
+            // 
+            this.btnFinal.Location = new System.Drawing.Point(162, 49);
+            this.btnFinal.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnFinal.Name = "btnFinal";
+            this.btnFinal.Size = new System.Drawing.Size(82, 36);
+            this.btnFinal.TabIndex = 19;
+            this.btnFinal.Text = "补天";
+            this.toolTip1.SetToolTip(this.btnFinal, "实在安装不上NET4时的终极大招,可能对系统文件有损坏!");
+            this.btnFinal.UseVisualStyleBackColor = true;
+            this.btnFinal.Click += new System.EventHandler(this.btnFinal_Click);
+            // 
+            // cbNET48
+            // 
+            this.cbNET48.AutoSize = true;
+            this.cbNET48.Enabled = false;
+            this.cbNET48.Location = new System.Drawing.Point(114, 144);
+            this.cbNET48.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.cbNET48.Name = "cbNET48";
+            this.cbNET48.Size = new System.Drawing.Size(22, 21);
+            this.cbNET48.TabIndex = 18;
+            this.cbNET48.UseVisualStyleBackColor = true;
+            // 
+            // lbNET48
+            // 
+            this.lbNET48.AutoSize = true;
+            this.lbNET48.Location = new System.Drawing.Point(18, 145);
+            this.lbNET48.Name = "lbNET48";
+            this.lbNET48.Size = new System.Drawing.Size(71, 18);
+            this.lbNET48.TabIndex = 17;
+            this.lbNET48.Text = ".NET4.8";
+            // 
+            // btnNET48
+            // 
+            this.btnNET48.Enabled = false;
+            this.btnNET48.Location = new System.Drawing.Point(162, 136);
+            this.btnNET48.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnNET48.Name = "btnNET48";
+            this.btnNET48.Size = new System.Drawing.Size(82, 36);
+            this.btnNET48.TabIndex = 16;
+            this.btnNET48.Text = "安装";
+            this.toolTip1.SetToolTip(this.btnNET48, "安装.NET4.8");
+            this.btnNET48.UseVisualStyleBackColor = true;
+            this.btnNET48.Click += new System.EventHandler(this.btnNET48_Click);
+            // 
+            // label9
+            // 
+            this.label9.AutoSize = true;
+            this.label9.Location = new System.Drawing.Point(18, 25);
+            this.label9.Name = "label9";
+            this.label9.Size = new System.Drawing.Size(152, 18);
+            this.label9.TabIndex = 15;
+            this.label9.Text = "打勾表示已安装!";
+            // 
+            // btnNET7
+            // 
+            this.btnNET7.Enabled = false;
+            this.btnNET7.Location = new System.Drawing.Point(162, 222);
+            this.btnNET7.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnNET7.Name = "btnNET7";
+            this.btnNET7.Size = new System.Drawing.Size(82, 36);
+            this.btnNET7.TabIndex = 14;
+            this.btnNET7.Text = "安装";
+            this.toolTip1.SetToolTip(this.btnNET7, "安装.NET7.0");
+            this.btnNET7.UseVisualStyleBackColor = true;
+            this.btnNET7.Click += new System.EventHandler(this.btnNET7_Click);
+            // 
+            // btnNET6
+            // 
+            this.btnNET6.Enabled = false;
+            this.btnNET6.Location = new System.Drawing.Point(162, 179);
+            this.btnNET6.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnNET6.Name = "btnNET6";
+            this.btnNET6.Size = new System.Drawing.Size(82, 36);
+            this.btnNET6.TabIndex = 13;
+            this.btnNET6.Text = "安装";
+            this.toolTip1.SetToolTip(this.btnNET6, "安装.NET6.0");
+            this.btnNET6.UseVisualStyleBackColor = true;
+            this.btnNET6.Click += new System.EventHandler(this.btnNET6_Click);
+            // 
+            // btnNET4
+            // 
+            this.btnNET4.Enabled = false;
+            this.btnNET4.Location = new System.Drawing.Point(162, 92);
+            this.btnNET4.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnNET4.Name = "btnNET4";
+            this.btnNET4.Size = new System.Drawing.Size(82, 36);
+            this.btnNET4.TabIndex = 12;
+            this.btnNET4.Text = "安装";
+            this.toolTip1.SetToolTip(this.btnNET4, "安装.NET4.0");
+            this.btnNET4.UseVisualStyleBackColor = true;
+            this.btnNET4.Click += new System.EventHandler(this.btnNET4_Click);
+            // 
+            // cbNET7
+            // 
+            this.cbNET7.AutoSize = true;
+            this.cbNET7.Enabled = false;
+            this.cbNET7.Location = new System.Drawing.Point(114, 230);
+            this.cbNET7.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.cbNET7.Name = "cbNET7";
+            this.cbNET7.Size = new System.Drawing.Size(22, 21);
+            this.cbNET7.TabIndex = 7;
+            this.cbNET7.UseVisualStyleBackColor = true;
+            // 
+            // lbNET7
+            // 
+            this.lbNET7.AutoSize = true;
+            this.lbNET7.Location = new System.Drawing.Point(18, 232);
+            this.lbNET7.Name = "lbNET7";
+            this.lbNET7.Size = new System.Drawing.Size(71, 18);
+            this.lbNET7.TabIndex = 6;
+            this.lbNET7.Text = ".NET7.0";
+            // 
+            // cbNET6
+            // 
+            this.cbNET6.AutoSize = true;
+            this.cbNET6.Enabled = false;
+            this.cbNET6.Location = new System.Drawing.Point(114, 187);
+            this.cbNET6.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.cbNET6.Name = "cbNET6";
+            this.cbNET6.Size = new System.Drawing.Size(22, 21);
+            this.cbNET6.TabIndex = 5;
+            this.cbNET6.UseVisualStyleBackColor = true;
+            // 
+            // lbNET6
+            // 
+            this.lbNET6.AutoSize = true;
+            this.lbNET6.Location = new System.Drawing.Point(18, 188);
+            this.lbNET6.Name = "lbNET6";
+            this.lbNET6.Size = new System.Drawing.Size(71, 18);
+            this.lbNET6.TabIndex = 4;
+            this.lbNET6.Text = ".NET6.0";
+            // 
+            // cbNET4
+            // 
+            this.cbNET4.AutoSize = true;
+            this.cbNET4.Enabled = false;
+            this.cbNET4.Location = new System.Drawing.Point(114, 101);
+            this.cbNET4.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.cbNET4.Name = "cbNET4";
+            this.cbNET4.Size = new System.Drawing.Size(22, 21);
+            this.cbNET4.TabIndex = 3;
+            this.cbNET4.UseVisualStyleBackColor = true;
+            // 
+            // lbNET4
+            // 
+            this.lbNET4.AutoSize = true;
+            this.lbNET4.Location = new System.Drawing.Point(18, 102);
+            this.lbNET4.Name = "lbNET4";
+            this.lbNET4.Size = new System.Drawing.Size(71, 18);
+            this.lbNET4.TabIndex = 2;
+            this.lbNET4.Text = ".NET4.0";
+            // 
+            // cbNET2
+            // 
+            this.cbNET2.AutoSize = true;
+            this.cbNET2.Enabled = false;
+            this.cbNET2.Location = new System.Drawing.Point(114, 58);
+            this.cbNET2.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.cbNET2.Name = "cbNET2";
+            this.cbNET2.Size = new System.Drawing.Size(22, 21);
+            this.cbNET2.TabIndex = 1;
+            this.cbNET2.UseVisualStyleBackColor = true;
+            // 
+            // lbNET2
+            // 
+            this.lbNET2.AutoSize = true;
+            this.lbNET2.Location = new System.Drawing.Point(18, 59);
+            this.lbNET2.Name = "lbNET2";
+            this.lbNET2.Size = new System.Drawing.Size(71, 18);
+            this.lbNET2.TabIndex = 0;
+            this.lbNET2.Text = ".NET2.0";
+            // 
+            // groupBox3
+            // 
+            this.groupBox3.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.groupBox3.Controls.Add(this.btnDownloadAll);
+            this.groupBox3.Controls.Add(this.btnInstallD);
+            this.groupBox3.Controls.Add(this.btnReport);
+            this.groupBox3.Controls.Add(this.label10);
+            this.groupBox3.Controls.Add(this.btnUninstall);
+            this.groupBox3.Controls.Add(this.btnInstallC);
+            this.groupBox3.Controls.Add(this.btnInstallB);
+            this.groupBox3.Controls.Add(this.btnInstallA);
+            this.groupBox3.Controls.Add(this.txtTarget);
+            this.groupBox3.Controls.Add(this.label8);
+            this.groupBox3.Controls.Add(this.btnNetwork);
+            this.groupBox3.Controls.Add(this.txtServer);
+            this.groupBox3.Controls.Add(this.label7);
+            this.groupBox3.Location = new System.Drawing.Point(14, 179);
+            this.groupBox3.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox3.Name = "groupBox3";
+            this.groupBox3.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox3.Size = new System.Drawing.Size(735, 272);
+            this.groupBox3.TabIndex = 2;
+            this.groupBox3.TabStop = false;
+            this.groupBox3.Text = "软件安装(普通用户)";
+            // 
+            // btnDownloadAll
+            // 
+            this.btnDownloadAll.Location = new System.Drawing.Point(369, 85);
+            this.btnDownloadAll.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnDownloadAll.Name = "btnDownloadAll";
+            this.btnDownloadAll.Size = new System.Drawing.Size(102, 52);
+            this.btnDownloadAll.TabIndex = 23;
+            this.btnDownloadAll.Text = "下载缓存";
+            this.toolTip1.SetToolTip(this.btnDownloadAll, "在网络较好的电脑上把所需要使用的所有组件下载下来");
+            this.btnDownloadAll.UseVisualStyleBackColor = true;
+            this.btnDownloadAll.Click += new System.EventHandler(this.btnDownloadAll_Click);
+            // 
+            // btnInstallD
+            // 
+            this.btnInstallD.Enabled = false;
+            this.btnInstallD.Location = new System.Drawing.Point(454, 191);
+            this.btnInstallD.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnInstallD.Name = "btnInstallD";
+            this.btnInstallD.Size = new System.Drawing.Size(146, 68);
+            this.btnInstallD.TabIndex = 22;
+            this.btnInstallD.Text = "安装精简版\r\n(WinXP/Win7)";
+            this.btnInstallD.UseVisualStyleBackColor = true;
+            this.btnInstallD.Click += new System.EventHandler(this.btnInstallD_Click);
+            // 
+            // btnReport
+            // 
+            this.btnReport.Location = new System.Drawing.Point(587, 85);
+            this.btnReport.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnReport.Name = "btnReport";
+            this.btnReport.Size = new System.Drawing.Size(102, 52);
+            this.btnReport.TabIndex = 21;
+            this.btnReport.Text = "上传日志";
+            this.toolTip1.SetToolTip(this.btnReport, "直接向平台注册登记本计算机,仅用于无法安装客户端时使用");
+            this.btnReport.UseVisualStyleBackColor = true;
+            this.btnReport.Click += new System.EventHandler(this.btnReport_Click);
+            // 
+            // label10
+            // 
+            this.label10.AutoSize = true;
+            this.label10.ForeColor = System.Drawing.Color.Blue;
+            this.label10.Location = new System.Drawing.Point(6, 154);
+            this.label10.Name = "label10";
+            this.label10.Size = new System.Drawing.Size(710, 18);
+            this.label10.TabIndex = 16;
+            this.label10.Text = "以下按钮亮起即表示可以安装,优先左边,某个版本安装失败时可以尝试安装其它版本。";
+            // 
+            // btnUninstall
+            // 
+            this.btnUninstall.Enabled = false;
+            this.btnUninstall.Location = new System.Drawing.Point(615, 191);
+            this.btnUninstall.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnUninstall.Name = "btnUninstall";
+            this.btnUninstall.Size = new System.Drawing.Size(111, 68);
+            this.btnUninstall.TabIndex = 20;
+            this.btnUninstall.Text = "卸载软件";
+            this.btnUninstall.UseVisualStyleBackColor = true;
+            this.btnUninstall.Click += new System.EventHandler(this.btnUninstall_Click);
+            // 
+            // btnInstallC
+            // 
+            this.btnInstallC.Enabled = false;
+            this.btnInstallC.Location = new System.Drawing.Point(306, 191);
+            this.btnInstallC.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnInstallC.Name = "btnInstallC";
+            this.btnInstallC.Size = new System.Drawing.Size(135, 68);
+            this.btnInstallC.TabIndex = 19;
+            this.btnInstallC.Text = "安装WinXP版\r\n(兼容Win7)";
+            this.btnInstallC.UseVisualStyleBackColor = true;
+            this.btnInstallC.Click += new System.EventHandler(this.btnInstallC_Click);
+            // 
+            // btnInstallB
+            // 
+            this.btnInstallB.Enabled = false;
+            this.btnInstallB.Location = new System.Drawing.Point(158, 191);
+            this.btnInstallB.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnInstallB.Name = "btnInstallB";
+            this.btnInstallB.Size = new System.Drawing.Size(135, 68);
+            this.btnInstallB.TabIndex = 18;
+            this.btnInstallB.Text = "安装Win7版\r\n(兼容Win10)";
+            this.btnInstallB.UseVisualStyleBackColor = true;
+            this.btnInstallB.Click += new System.EventHandler(this.btnInstallB_Click);
+            // 
+            // btnInstallA
+            // 
+            this.btnInstallA.Enabled = false;
+            this.btnInstallA.Location = new System.Drawing.Point(8, 191);
+            this.btnInstallA.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnInstallA.Name = "btnInstallA";
+            this.btnInstallA.Size = new System.Drawing.Size(135, 68);
+            this.btnInstallA.TabIndex = 13;
+            this.btnInstallA.Text = "安装Win10版\r\n(兼容Win11)";
+            this.btnInstallA.UseVisualStyleBackColor = true;
+            this.btnInstallA.Click += new System.EventHandler(this.btnInstallA_Click);
+            // 
+            // txtTarget
+            // 
+            this.txtTarget.Location = new System.Drawing.Point(124, 92);
+            this.txtTarget.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtTarget.Name = "txtTarget";
+            this.txtTarget.ReadOnly = true;
+            this.txtTarget.Size = new System.Drawing.Size(222, 28);
+            this.txtTarget.TabIndex = 12;
+            // 
+            // label8
+            // 
+            this.label8.AutoSize = true;
+            this.label8.Location = new System.Drawing.Point(21, 98);
+            this.label8.Name = "label8";
+            this.label8.Size = new System.Drawing.Size(98, 18);
+            this.label8.TabIndex = 17;
+            this.label8.Text = "当前系统:";
+            // 
+            // btnNetwork
+            // 
+            this.btnNetwork.Location = new System.Drawing.Point(587, 25);
+            this.btnNetwork.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.btnNetwork.Name = "btnNetwork";
+            this.btnNetwork.Size = new System.Drawing.Size(102, 52);
+            this.btnNetwork.TabIndex = 16;
+            this.btnNetwork.Text = "网络检测";
+            this.toolTip1.SetToolTip(this.btnNetwork, "检测本机到平台的网络畅通情况");
+            this.btnNetwork.UseVisualStyleBackColor = true;
+            this.btnNetwork.Click += new System.EventHandler(this.btnNetwork_Click);
+            // 
+            // txtServer
+            // 
+            this.txtServer.Location = new System.Drawing.Point(117, 36);
+            this.txtServer.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.txtServer.Name = "txtServer";
+            this.txtServer.Size = new System.Drawing.Size(427, 28);
+            this.txtServer.TabIndex = 1;
+            this.txtServer.Text = "http://s.newlifex.com:6600";
+            // 
+            // label7
+            // 
+            this.label7.AutoSize = true;
+            this.label7.Location = new System.Drawing.Point(21, 42);
+            this.label7.Name = "label7";
+            this.label7.Size = new System.Drawing.Size(80, 18);
+            this.label7.TabIndex = 0;
+            this.label7.Text = "服务器:";
+            // 
+            // groupBox4
+            // 
+            this.groupBox4.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
+            | System.Windows.Forms.AnchorStyles.Left) 
+            | System.Windows.Forms.AnchorStyles.Right)));
+            this.groupBox4.Controls.Add(this.rtbLog);
+            this.groupBox4.Location = new System.Drawing.Point(14, 460);
+            this.groupBox4.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox4.Name = "groupBox4";
+            this.groupBox4.Padding = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.groupBox4.Size = new System.Drawing.Size(1005, 409);
+            this.groupBox4.TabIndex = 3;
+            this.groupBox4.TabStop = false;
+            this.groupBox4.Text = "日志";
+            // 
+            // rtbLog
+            // 
+            this.rtbLog.Dock = System.Windows.Forms.DockStyle.Fill;
+            this.rtbLog.Location = new System.Drawing.Point(3, 25);
+            this.rtbLog.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.rtbLog.Name = "rtbLog";
+            this.rtbLog.Size = new System.Drawing.Size(999, 380);
+            this.rtbLog.TabIndex = 0;
+            this.rtbLog.Text = "";
+            // 
+            // FrmMain
+            // 
+            this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
+            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+            this.ClientSize = new System.Drawing.Size(1032, 883);
+            this.Controls.Add(this.groupBox4);
+            this.Controls.Add(this.groupBox3);
+            this.Controls.Add(this.groupBox2);
+            this.Controls.Add(this.groupBox1);
+            this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
+            this.Name = "FrmMain";
+            this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+            this.Text = "dotNet安装助手(新生命团队)";
+            this.Load += new System.EventHandler(this.FrmMain_Load);
+            this.groupBox1.ResumeLayout(false);
+            this.groupBox1.PerformLayout();
+            this.groupBox2.ResumeLayout(false);
+            this.groupBox2.PerformLayout();
+            this.groupBox3.ResumeLayout(false);
+            this.groupBox3.PerformLayout();
+            this.groupBox4.ResumeLayout(false);
+            this.ResumeLayout(false);
+
+        }
+
+        #endregion
+
+        private System.Windows.Forms.GroupBox groupBox1;
+        private System.Windows.Forms.GroupBox groupBox2;
+        private System.Windows.Forms.Label label1;
+        private System.Windows.Forms.TextBox txtUser;
+        private System.Windows.Forms.Label label2;
+        private System.Windows.Forms.TextBox txtMachince;
+        private System.Windows.Forms.TextBox txtSerial;
+        private System.Windows.Forms.Label label5;
+        private System.Windows.Forms.TextBox txtProduct;
+        private System.Windows.Forms.Label label6;
+        private System.Windows.Forms.TextBox txtVersion;
+        private System.Windows.Forms.Label label3;
+        private System.Windows.Forms.TextBox txtOS;
+        private System.Windows.Forms.Label label4;
+        private System.Windows.Forms.CheckBox cbNET2;
+        private System.Windows.Forms.Label lbNET2;
+        private System.Windows.Forms.CheckBox cbNET7;
+        private System.Windows.Forms.Label lbNET7;
+        private System.Windows.Forms.CheckBox cbNET6;
+        private System.Windows.Forms.Label lbNET6;
+        private System.Windows.Forms.CheckBox cbNET4;
+        private System.Windows.Forms.Label lbNET4;
+        private System.Windows.Forms.Button btnNET4;
+        private System.Windows.Forms.Button btnNET6;
+        private System.Windows.Forms.Button btnNET7;
+        private System.Windows.Forms.GroupBox groupBox3;
+        private System.Windows.Forms.TextBox txtServer;
+        private System.Windows.Forms.Label label7;
+        private System.Windows.Forms.Button btnNetwork;
+        private System.Windows.Forms.GroupBox groupBox4;
+        private System.Windows.Forms.RichTextBox rtbLog;
+        private System.Windows.Forms.TextBox txtTarget;
+        private System.Windows.Forms.Label label8;
+        private System.Windows.Forms.Button btnInstallB;
+        private System.Windows.Forms.Button btnInstallA;
+        private System.Windows.Forms.Button btnInstallC;
+        private System.Windows.Forms.Button btnUninstall;
+        private System.Windows.Forms.Label label9;
+        private System.Windows.Forms.Label label10;
+        private System.Windows.Forms.ToolTip toolTip1;
+        private System.Windows.Forms.Button btnNET48;
+        private System.Windows.Forms.CheckBox cbNET48;
+        private System.Windows.Forms.Label lbNET48;
+        private System.Windows.Forms.Button btnReport;
+        private System.Windows.Forms.Button btnFinal;
+        private System.Windows.Forms.Button btnInstallD;
+        private System.Windows.Forms.Button btnDownloadAll;
+    }
+}
+
Added +126 -0
diff --git a/Installer/FrmMain.resx b/Installer/FrmMain.resx
new file mode 100644
index 0000000..66e18f4
--- /dev/null
+++ b/Installer/FrmMain.resx
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <metadata name="toolTip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+    <value>17, 17</value>
+  </metadata>
+  <metadata name="toolTip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
+    <value>17, 17</value>
+  </metadata>
+</root>
\ No newline at end of file
Added +42 -0
diff --git a/Installer/Helper.cs b/Installer/Helper.cs
new file mode 100644
index 0000000..9fa1e1b
--- /dev/null
+++ b/Installer/Helper.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Text;
+
+namespace Installer;
+
+public static class Helper
+{
+    /// <summary>去掉两头的0字节</summary>
+    /// <param name="value"></param>
+    /// <returns></returns>
+    public static String TrimZero(this String value) => value?.Trim().Trim('\0').Trim().Replace("\0", null);
+
+    /// <summary>
+    /// 获取可见字符串
+    /// </summary>
+    /// <param name="value"></param>
+    /// <returns></returns>
+    public static String GetInvisibleChar(this String value, Boolean isFirst = true)
+    {
+        if (String.IsNullOrEmpty(value)) return value;
+
+        var builder = new StringBuilder();
+
+        for (var i = 0; i < value.Length; i++)
+        {
+            if (value[i].IsInvisible())
+                builder.Append(value[i]);
+            else if (isFirst)
+                break;
+        }
+
+        return builder.ToString();
+    }
+
+    /// <summary>
+    /// 是否可见字符串
+    /// ASCII码中,第0~31号及第127号(共33个)是控制字符或通讯专用字符
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    public static Boolean IsInvisible(this Char ch) => ch is > (Char)31 and not (Char)127;
+}
\ No newline at end of file
Added +110 -0
diff --git a/Installer/Installer.csproj b/Installer/Installer.csproj
new file mode 100644
index 0000000..dd9676b
--- /dev/null
+++ b/Installer/Installer.csproj
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProjectGuid>{12503268-DD9B-4A68-B53C-EDCD71F2D485}</ProjectGuid>
+    <OutputType>WinExe</OutputType>
+    <RootNamespace>Installer</RootNamespace>
+    <AssemblyName>Installer</AssemblyName>
+    <TargetFrameworkVersion>v2.0</TargetFrameworkVersion>
+    <FileAlignment>512</FileAlignment>
+    <Deterministic>false</Deterministic>
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>..\Bin\Installer\</OutputPath>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <DebugType>pdbonly</DebugType>
+    <Optimize>true</Optimize>
+    <OutputPath>..\Bin\Installer\</OutputPath>
+    <DefineConstants>TRACE</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+  <PropertyGroup>
+    <ApplicationManifest>app.manifest</ApplicationManifest>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="Microsoft.VisualBasic" />
+    <Reference Include="System" />
+    <Reference Include="System.Data" />
+    <Reference Include="System.Deployment" />
+    <Reference Include="System.Drawing" />
+    <Reference Include="System.Management" />
+    <Reference Include="System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>C:\Windows\Microsoft.NET\Framework64\v2.0.50727\System.Web.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Windows.Forms" />
+    <Reference Include="System.Xml" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="Advapi32.cs" />
+    <Compile Include="DownloadHelper.cs" />
+    <Compile Include="FrmMain.cs">
+      <SubType>Form</SubType>
+    </Compile>
+    <Compile Include="FrmMain.Designer.cs">
+      <DependentUpon>FrmMain.cs</DependentUpon>
+    </Compile>
+    <Compile Include="Helper.cs" />
+    <Compile Include="ITracer.cs" />
+    <Compile Include="NetRuntime.cs" />
+    <Compile Include="Program.cs" />
+    <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="VerInfo.cs" />
+    <Compile Include="WindowsService.cs" />
+    <EmbeddedResource Include="FrmMain.resx">
+      <DependentUpon>FrmMain.cs</DependentUpon>
+    </EmbeddedResource>
+    <EmbeddedResource Include="Properties\Resources.resx">
+      <Generator>ResXFileCodeGenerator</Generator>
+      <LastGenOutput>Resources.Designer.cs</LastGenOutput>
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
+    <Compile Include="Properties\Resources.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Resources.resx</DependentUpon>
+    </Compile>
+    <EmbeddedResource Include="..\LuckyClover\res\MicrosoftRootCertificateAuthority2011.cer">
+      <Link>res\MicrosoftRootCertificateAuthority2011.cer</Link>
+    </EmbeddedResource>
+    <None Include="app.manifest" />
+    <None Include="Properties\Settings.settings">
+      <Generator>SettingsSingleFileGenerator</Generator>
+      <LastGenOutput>Settings.Designer.cs</LastGenOutput>
+    </None>
+    <Compile Include="Properties\Settings.Designer.cs">
+      <AutoGen>True</AutoGen>
+      <DependentUpon>Settings.settings</DependentUpon>
+      <DesignTimeSharedInput>True</DesignTimeSharedInput>
+    </Compile>
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="NewLife.Core">
+      <Version>10.6.2024.101-net20</Version>
+    </PackageReference>
+  </ItemGroup>
+  <ItemGroup>
+    <EmbeddedResource Include="..\LuckyClover\res\CertMgr.Exe">
+      <Link>res\CertMgr.Exe</Link>
+    </EmbeddedResource>
+    <EmbeddedResource Include="..\LuckyClover\res\md5.txt">
+      <Link>res\md5.txt</Link>
+    </EmbeddedResource>
+  </ItemGroup>
+  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+</Project>
\ No newline at end of file
Added +14 -0
diff --git a/Installer/ITracer.cs b/Installer/ITracer.cs
new file mode 100644
index 0000000..b4c1bc2
--- /dev/null
+++ b/Installer/ITracer.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Installer;
+
+public interface ITracer
+{
+    ISpan NewSpan(String name, Object tag = null);
+}
+
+public interface ISpan : IDisposable
+{
+    void SetTag(Object tag);
+    void SetError(Exception ex, Object tag = null);
+}
Added +360 -0
diff --git a/Installer/MachineInfo.cs b/Installer/MachineInfo.cs
new file mode 100644
index 0000000..f097cd7
--- /dev/null
+++ b/Installer/MachineInfo.cs
@@ -0,0 +1,360 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Management;
+using System.Runtime.InteropServices;
+using System.Security;
+using Microsoft.VisualBasic.Devices;
+using Microsoft.Win32;
+using NewLife.Log;
+
+namespace NewLife;
+
+/// <summary>机器信息</summary>
+/// <remarks>
+/// 文档 https://newlifex.com/core/machine_info
+/// 
+/// 刷新信息成本较高,建议采用单例模式
+/// </remarks>
+public class MachineInfo
+{
+    #region 属性
+    /// <summary>系统名称</summary>
+    [DisplayName("系统名称")]
+    public String OSName { get; set; }
+
+    /// <summary>系统版本</summary>
+    [DisplayName("系统版本")]
+    public String OSVersion { get; set; }
+
+    /// <summary>产品名称。制造商</summary>
+    [DisplayName("产品名称")]
+    public String Product { get; set; }
+
+    /// <summary>处理器型号</summary>
+    [DisplayName("处理器型号")]
+    public String Processor { get; set; }
+
+    ///// <summary>处理器序列号。PC处理器序列号绝大部分重复,实际存储处理器的其它信息</summary>
+    //public String CpuID { get; set; }
+
+    /// <summary>硬件唯一标识。取主板编码,部分品牌存在重复</summary>
+    [DisplayName("硬件唯一标识")]
+    public String UUID { get; set; }
+
+    /// <summary>软件唯一标识。系统标识,操作系统重装后更新,Linux系统的machine_id,Android的android_id,Ghost系统存在重复</summary>
+    [DisplayName("软件唯一标识")]
+    public String Guid { get; set; }
+
+    /// <summary>计算机序列号。适用于品牌机,跟笔记本标签显示一致</summary>
+    [DisplayName("计算机序列号")]
+    public String Serial { get; set; }
+
+    /// <summary>主板。序列号或家族信息</summary>
+    [DisplayName("主板")]
+    public String Board { get; set; }
+
+    /// <summary>磁盘序列号</summary>
+    [DisplayName("磁盘序列号")]
+    public String DiskID { get; set; }
+
+    /// <summary>内存总量。单位Byte</summary>
+    [DisplayName("内存总量")]
+    public UInt64 Memory { get; set; }
+
+    /// <summary>可用内存。单位Byte</summary>
+    [DisplayName("可用内存")]
+    public UInt64 AvailableMemory { get; set; }
+
+    /// <summary>CPU占用率</summary>
+    [DisplayName("CPU占用率")]
+    public Single CpuRate { get; set; }
+
+    ///// <summary>网络上行速度。字节每秒,初始化后首次读取为0</summary>
+    //[DisplayName("网络上行速度")]
+    //public UInt64 UplinkSpeed { get; set; }
+
+    ///// <summary>网络下行速度。字节每秒,初始化后首次读取为0</summary>
+    //[DisplayName("网络下行速度")]
+    //public UInt64 DownlinkSpeed { get; set; }
+
+    ///// <summary>温度。单位度</summary>
+    //[DisplayName("温度")]
+    //public Double Temperature { get; set; }
+
+    ///// <summary>电池剩余。小于1的小数,常用百分比表示</summary>
+    //[DisplayName("电池剩余")]
+    //public Double Battery { get; set; }
+    #endregion
+
+    #region 构造
+    /// <summary>当前机器信息。默认null,在RegisterAsync后才能使用</summary>
+    public static MachineInfo Current { get; set; } = new MachineInfo();
+    #endregion
+
+    #region 方法
+    /// <summary>刷新</summary>
+    public void Init()
+    {
+        var osv = Environment.OSVersion;
+        if (OSVersion.IsNullOrEmpty()) OSVersion = osv.Version + "";
+        if (OSName.IsNullOrEmpty()) OSName = (osv + "").TrimStart("Microsoft").TrimEnd(OSVersion).Trim();
+        if (Guid.IsNullOrEmpty()) Guid = "";
+
+        try
+        {
+            LoadWindowsInfo();
+        }
+        catch (Exception ex)
+        {
+            if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
+        }
+
+        // window+netcore 不方便读取注册表,随机生成一个guid,借助文件缓存确保其不变
+        if (Guid.IsNullOrEmpty()) Guid = "0-" + System.Guid.NewGuid().ToString();
+        if (UUID.IsNullOrEmpty()) UUID = "0-" + System.Guid.NewGuid().ToString();
+
+        try
+        {
+            Refresh();
+        }
+        catch (Exception ex)
+        {
+            if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
+        }
+    }
+
+    private void LoadWindowsInfo()
+    {
+        var machine_guid = "";
+
+        var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
+        if (reg != null) machine_guid = reg.GetValue("MachineGuid") + "";
+        if (machine_guid.IsNullOrEmpty())
+        {
+            //reg = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
+            //if (reg != null) machine_guid = reg.GetValue("MachineGuid") + "";
+        }
+
+        var ci = new ComputerInfo();
+        try
+        {
+            Memory = ci.TotalPhysicalMemory;
+
+            // 系统名取WMI可能出错
+            OSName = ci.OSFullName.TrimStart("Microsoft").Trim();
+            OSVersion = ci.OSVersion;
+        }
+        catch
+        {
+            try
+            {
+                var reg2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
+                if (reg2 != null)
+                {
+                    OSName = reg2.GetValue("ProductName") + "";
+                    OSVersion = reg2.GetValue("ReleaseId") + "";
+                }
+            }
+            catch (Exception ex)
+            {
+                if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
+            }
+        }
+
+        Processor = GetInfo("Win32_Processor", "Name");
+        //CpuID = GetInfo("Win32_Processor", "ProcessorId");
+        var uuid = GetInfo("Win32_ComputerSystemProduct", "UUID");
+        Product = GetInfo("Win32_ComputerSystemProduct", "Name");
+        DiskID = GetInfo("Win32_DiskDrive", "SerialNumber");
+
+        var sn = GetInfo("Win32_BIOS", "SerialNumber");
+        if (!sn.IsNullOrEmpty() && !sn.EqualIgnoreCase("System Serial Number")) Serial = sn;
+        Board = GetInfo("Win32_BaseBoard", "SerialNumber");
+
+        // UUID取不到时返回 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
+        if (!uuid.IsNullOrEmpty() && !uuid.EqualIgnoreCase("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")) UUID = uuid;
+
+        if (!machine_guid.IsNullOrEmpty()) Guid = machine_guid;
+    }
+
+    /// <summary>获取实时数据,如CPU、内存、温度</summary>
+    public void Refresh()
+    {
+        RefreshWindows();
+    }
+
+    private void RefreshWindows()
+    {
+        MEMORYSTATUSEX ms = default;
+        ms.Init();
+        if (GlobalMemoryStatusEx(ref ms))
+        {
+            Memory = ms.ullTotalPhys;
+            AvailableMemory = ms.ullAvailPhys;
+        }
+
+        GetSystemTimes(out var idleTime, out var kernelTime, out var userTime);
+
+        var current = new SystemTime
+        {
+            IdleTime = idleTime.ToLong(),
+            TotalTime = kernelTime.ToLong() + userTime.ToLong(),
+        };
+
+        var idle = current.IdleTime - (_systemTime?.IdleTime ?? 0);
+        var total = current.TotalTime - (_systemTime?.TotalTime ?? 0);
+        _systemTime = current;
+
+        CpuRate = total == 0 ? 0 : (Single)Math.Round((Single)(total - idle) / total, 4);
+    }
+    #endregion
+
+    #region 内存
+    [DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+    [SecurityCritical]
+    [return: MarshalAs(UnmanagedType.Bool)]
+    internal static extern Boolean GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
+
+    internal struct MEMORYSTATUSEX
+    {
+        internal UInt32 dwLength;
+
+        internal UInt32 dwMemoryLoad;
+
+        internal UInt64 ullTotalPhys;
+
+        internal UInt64 ullAvailPhys;
+
+        internal UInt64 ullTotalPageFile;
+
+        internal UInt64 ullAvailPageFile;
+
+        internal UInt64 ullTotalVirtual;
+
+        internal UInt64 ullAvailVirtual;
+
+        internal UInt64 ullAvailExtendedVirtual;
+
+        internal void Init() => dwLength = checked((UInt32)Marshal.SizeOf(typeof(MEMORYSTATUSEX)));
+    }
+    #endregion
+
+    #region 磁盘
+    /// <summary>获取指定目录所在盘可用空间,默认当前目录</summary>
+    /// <param name="path"></param>
+    /// <returns>返回可用空间,字节,获取失败返回-1</returns>
+    public static Int64 GetFreeSpace(String path = null)
+    {
+        if (path.IsNullOrEmpty()) path = ".";
+
+        var driveInfo = new DriveInfo(Path.GetPathRoot(path.GetFullPath()));
+        if (driveInfo == null || !driveInfo.IsReady) return -1;
+
+        try
+        {
+            return driveInfo.AvailableFreeSpace;
+        }
+        catch
+        {
+            return -1;
+        }
+    }
+
+    /// <summary>获取指定目录下文件名,支持去掉后缀的去重,主要用于Linux</summary>
+    /// <param name="path"></param>
+    /// <param name="trimSuffix"></param>
+    /// <returns></returns>
+    public static ICollection<String> GetFiles(String path, Boolean trimSuffix = false)
+    {
+        var list = new List<String>();
+        if (path.IsNullOrEmpty()) return list;
+
+        var di = path.AsDirectory();
+        if (!di.Exists) return list;
+
+        var list2 = di.GetFiles().Select(e => e.Name).ToList();
+        foreach (var item in list2)
+        {
+            var line = item?.Trim();
+            if (!line.IsNullOrEmpty())
+            {
+                if (trimSuffix)
+                {
+                    if (!list2.Any(e => e != line && line.StartsWith(e))) list.Add(line);
+                }
+                else
+                {
+                    list.Add(line);
+                }
+            }
+        }
+
+        return list;
+    }
+    #endregion
+
+    #region Windows辅助
+    [DllImport("kernel32.dll", SetLastError = true)]
+    private static extern Boolean GetSystemTimes(out FILETIME idleTime, out FILETIME kernelTime, out FILETIME userTime);
+
+    private struct FILETIME
+    {
+        public UInt32 Low;
+
+        public UInt32 High;
+
+        public FILETIME(Int64 time)
+        {
+            Low = (UInt32)time;
+            High = (UInt32)(time >> 32);
+        }
+
+        public Int64 ToLong() => (Int64)(((UInt64)High << 32) | Low);
+    }
+
+    private class SystemTime
+    {
+        public Int64 IdleTime;
+        public Int64 TotalTime;
+    }
+
+    private SystemTime _systemTime;
+
+    /// <summary>获取WMI信息</summary>
+    /// <param name="path"></param>
+    /// <param name="property"></param>
+    /// <param name="nameSpace"></param>
+    /// <returns></returns>
+    public static String GetInfo(String path, String property, String nameSpace = null)
+    {
+        // Linux Mono不支持WMI
+        if (Runtime.Mono) return "";
+
+        var bbs = new List<String>();
+        try
+        {
+            var wql = $"Select {property} From {path}";
+            var cimobject = new ManagementObjectSearcher(nameSpace, wql);
+            var moc = cimobject.Get();
+            foreach (var mo in moc)
+            {
+                var val = mo?.Properties?[property]?.Value;
+                if (val != null) bbs.Add(val.ToString().Trim());
+            }
+        }
+        catch (Exception ex)
+        {
+            if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteLine("WMI.GetInfo({0})失败!{1}", path, ex.Message);
+            return "";
+        }
+
+        bbs.Sort();
+
+        return bbs.Distinct().Join();
+    }
+    #endregion
+}
\ No newline at end of file
Added +805 -0
diff --git a/Installer/NetRuntime.cs b/Installer/NetRuntime.cs
new file mode 100644
index 0000000..74038e3
--- /dev/null
+++ b/Installer/NetRuntime.cs
@@ -0,0 +1,805 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Security.Cryptography;
+using Microsoft.Win32;
+using NewLife;
+using NewLife.Log;
+
+namespace Installer;
+
+public class NetRuntime
+{
+    #region 属性
+    public String BaseUrl { get; set; }
+
+    public Boolean Silent { get; set; }
+
+    /// <summary>应用安装目录</summary>
+    public String InstallPath { get; set; }
+
+    /// <summary>缓存目录</summary>
+    public String CachePath { get; set; } = "../Cache";
+
+    public IDictionary<String, String> Hashs { get; set; }
+
+    public ITracer Tracer { get; set; }
+    #endregion
+
+    #region 核心方法
+    public void AutoInstallNet()
+    {
+        var osVer = Environment.OSVersion.Version;
+
+        // WinXP
+        if (osVer.Major <= 5)
+            InstallNet40();
+        // Vista
+        else if (osVer.Major == 6 && osVer.Minor == 0)
+            InstallNet45();
+        else if (osVer.Major == 6 && osVer.Minor == 1)
+        {
+            // Win7
+            if (osVer.Revision <= 7600)
+                InstallNet45();
+            else
+            // Win7Sp1
+            {
+                InstallNet48();
+                InstallNet6();
+            }
+        }
+        // Win10/Win11
+        else if (osVer.Major >= 10)
+        {
+            InstallNet7();
+        }
+        else
+        {
+            InstallNet48();
+            InstallNet7();
+        }
+    }
+
+    public Boolean Install(String fileName, String baseUrl = null, String arg = null)
+    {
+        using var span = Tracer?.NewSpan($"Install-{Path.GetFileNameWithoutExtension(fileName)}", new { fileName, baseUrl });
+
+        XTrace.WriteLine("下载 {0}", fileName);
+
+        var fullFile = fileName;
+        if (!String.IsNullOrEmpty(CachePath)) fullFile = Path.Combine(CachePath, fileName);
+
+        var hash = "";
+        if (Hashs != null && !Hashs.TryGetValue(fileName, out hash)) hash = null;
+
+        // 检查已存在文件的MD5哈希,不正确则重新下载
+        var fi = new FileInfo(fullFile);
+        if (fi.Exists && fi.Length < 1024 && !String.IsNullOrEmpty(hash) && GetMD5(fullFile) != hash)
+        {
+            fi.Delete();
+            fi = null;
+        }
+        if (fi == null || !fi.Exists)
+        {
+            if (String.IsNullOrEmpty(baseUrl))
+                baseUrl = BaseUrl;
+            else
+                baseUrl = BaseUrl + baseUrl;
+
+            var url = $"{baseUrl}/{fileName}";
+            XTrace.WriteLine("正在下载:{0}", url);
+
+            fullFile.EnsureDirectory(true);
+
+            var http = new WebClient();
+            http.DownloadFile(url, fullFile);
+            XTrace.WriteLine("MD5: {0}", GetMD5(fullFile));
+        }
+
+        if (String.IsNullOrEmpty(arg)) arg = "/passive /promptrestart";
+        if (!Silent) arg = null;
+
+        XTrace.WriteLine("正在安装:{0} {1}", fullFile, arg);
+        var p = Process.Start(fullFile, arg);
+        if (p.WaitForExit(600_000))
+        {
+            if (p.ExitCode == 0)
+                XTrace.WriteLine("安装完成!");
+            else
+                XTrace.WriteLine("安装失败!ExitCode={0}", p.ExitCode);
+            Environment.ExitCode = p.ExitCode;
+            return p.ExitCode == 0;
+        }
+        else
+        {
+            XTrace.WriteLine("安装超时!");
+            Environment.ExitCode = 400;
+            return false;
+        }
+    }
+
+    static Version GetLast(IList<VerInfo> vers, String prefix = null, String suffix = null)
+    {
+        var ver = new Version();
+        if (vers.Count > 0)
+        {
+            //XTrace.WriteLine("已安装版本:");
+            foreach (var item in vers)
+            {
+                if ((String.IsNullOrEmpty(prefix) || item.Name.StartsWith(prefix)) &&
+                    (String.IsNullOrEmpty(suffix) || item.Name.EndsWith(suffix)))
+                {
+                    var str = item.Name.Trim('v');
+                    var p = str.IndexOf('-');
+                    if (p > 0) str = str.Substring(0, p);
+
+                    var v = new Version(str);
+                    if (v > ver) ver = v;
+                }
+
+                //XTrace.WriteLine(item.Name);
+            }
+            //XTrace.WriteLine("");
+        }
+
+        return ver;
+    }
+
+    public Boolean InstallNet40()
+    {
+        using var span = Tracer?.NewSpan(nameof(InstallNet40), null);
+
+        var vers = new List<VerInfo>();
+        vers.AddRange(Get1To45VersionFromRegistry());
+        vers.AddRange(Get45PlusFromRegistry());
+
+        var ver = GetLast(vers, null);
+
+        // 目标版本
+        var target = new Version("4.0");
+        if (ver >= target)
+        {
+            XTrace.WriteLine("已安装最新版 v{0}", ver);
+            return true;
+        }
+
+
+        var rs = Install("dotNetFx40_Full_x86_x64.exe", null);
+        if (!rs)
+        {
+            XTrace.WriteLine("安装NET4失败,准备清理环境后重新安装!");
+
+            CleanForNet();
+
+            rs = Install("dotNetFx40_Full_x86_x64.exe", null);
+        }
+
+        return rs;
+    }
+
+    public Boolean InstallNet45()
+    {
+        using var span = Tracer?.NewSpan(nameof(InstallNet45), null);
+
+        var vers = new List<VerInfo>();
+        vers.AddRange(Get1To45VersionFromRegistry());
+        vers.AddRange(Get45PlusFromRegistry());
+
+        var ver = GetLast(vers, null);
+
+        // 目标版本
+        var target = new Version("4.5");
+        if (ver >= target)
+        {
+            XTrace.WriteLine("已安装最新版 v{0}", ver);
+            return false;
+        }
+
+        var rs = Install("NDP452-KB2901907-x86-x64-AllOS-ENU.exe");
+        if (rs) Install("NDP452-KB2901907-x86-x64-AllOS-CHS.exe");
+
+        if (!rs)
+        {
+            XTrace.WriteLine("安装NET45失败,准备清理环境后重新安装!");
+
+            CleanForNet();
+
+            rs = Install("NDP452-KB2901907-x86-x64-AllOS-ENU.exe");
+            Install("NDP452-KB2901907-x86-x64-AllOS-CHS.exe");
+        }
+
+        return rs;
+    }
+
+    public Boolean InstallNet48()
+    {
+        using var span = Tracer?.NewSpan(nameof(InstallNet48), null);
+
+        var vers = new List<VerInfo>();
+        vers.AddRange(Get1To45VersionFromRegistry());
+        vers.AddRange(Get45PlusFromRegistry());
+
+        var ver = GetLast(vers, null);
+
+        // 目标版本。win10起支持4.8.1
+        var osVer = Environment.OSVersion.Version;
+        var target = osVer.Major >= 10 ? new Version("4.8.1") : new Version("4.8");
+        if (ver >= target)
+        {
+            XTrace.WriteLine("已安装最新版 v{0}", ver);
+            return true;
+        }
+
+        var is64 = IntPtr.Size == 8;
+
+        var isWin7 = osVer.Major == 6 && osVer.Minor == 1;
+        if (isWin7)
+        {
+            //if (is64)
+            //{
+            //    Install("Windows6.1-KB3063858-x64.msu", "/win7", "/quiet /norestart");
+            //}
+            //else
+            //{
+            //    Install("Windows6.1-KB3063858-x86.msu", "/win7", "/quiet /norestart");
+            //}
+            InstallCert();
+        }
+
+        // win10/win11 中安装 .NET4.8.1
+        var rs = false;
+        if (osVer.Major >= 10)
+        {
+            rs = Install("ndp481-x86-x64-allos-enu.exe", null, "/passive /promptrestart /showfinalerror");
+            if (rs) Install("ndp481-x86-x64-allos-chs.exe", null, "/passive /promptrestart /showfinalerror");
+        }
+        else
+        {
+            rs = Install("ndp48-x86-x64-allos-enu.exe", null, "/passive /promptrestart /showfinalerror");
+            if (rs) Install("ndp48-x86-x64-allos-chs.exe", null, "/passive /promptrestart /showfinalerror");
+        }
+
+        return rs;
+    }
+
+    public Boolean InstallNet6(String kind = null)
+    {
+        using var span = Tracer?.NewSpan(nameof(InstallNet6), null);
+
+        var vers = GetNetCore();
+
+        var suffix = "";
+        if (!String.IsNullOrEmpty(kind)) suffix = "-" + kind;
+        var ver = GetLast(vers, "v6.0", suffix);
+
+        // 目标版本
+        var target = new Version("6.0");
+        if (ver >= target)
+        {
+            XTrace.WriteLine("已安装最新版 v{0}", ver);
+            return false;
+        }
+
+        var is64 = IntPtr.Size == 8;
+
+        // win7需要vc2019运行时
+        var osVer = Environment.OSVersion.Version;
+        var isWin7 = osVer.Major == 6 && osVer.Minor == 1;
+        if (isWin7)
+        {
+            if (is64)
+            {
+                Install("Windows6.1-KB3063858-x64.msu", "/win7", "/quiet /norestart");
+                Install("VC_redist.x64.exe", "/vc2019", "/passive");
+            }
+            else
+            {
+                Install("Windows6.1-KB3063858-x86.msu", "/win7", "/quiet /norestart");
+                Install("VC_redist.x86.exe", "/vc2019", "/passive");
+            }
+        }
+
+        var rs = false;
+        if (is64)
+        {
+            switch (kind)
+            {
+                case "aspnet":
+                    rs = Install("dotnet-runtime-6.0.16-win-x64.exe");
+                    Install("aspnetcore-runtime-6.0.16-win-x64.exe");
+                    break;
+                case "desktop":
+                    rs = Install("windowsdesktop-runtime-6.0.16-win-x64.exe");
+                    break;
+                case "host":
+                    rs = Install("dotnet-hosting-6.0.16-win.exe");
+                    break;
+                default:
+                    rs = Install("dotnet-runtime-6.0.16-win-x64.exe");
+                    break;
+            }
+        }
+        else
+        {
+            switch (kind)
+            {
+                case "aspnet":
+                    rs = Install("dotnet-runtime-6.0.16-win-x86.exe");
+                    rs = Install("aspnetcore-runtime-6.0.16-win-x86.exe");
+                    break;
+                case "desktop":
+                    rs = Install("windowsdesktop-runtime-6.0.16-win-x86.exe");
+                    break;
+                case "host":
+                    rs = Install("dotnet-hosting-6.0.16-win.exe");
+                    break;
+                default:
+                    rs = Install("dotnet-runtime-6.0.16-win-x86.exe");
+                    break;
+            }
+        }
+
+        return rs;
+    }
+
+    public Boolean InstallNet7(String kind = null)
+    {
+        using var span = Tracer?.NewSpan(nameof(InstallNet7), null);
+
+        var vers = GetNetCore();
+
+        var suffix = "";
+        if (!String.IsNullOrEmpty(kind)) suffix = "-" + kind;
+        var ver = GetLast(vers, "v7.0", suffix);
+
+        // 目标版本
+        var target = new Version("7.0");
+        if (ver >= target)
+        {
+            XTrace.WriteLine("已安装最新版 v{0}", ver);
+            return false;
+        }
+
+        var is64 = IntPtr.Size == 8;
+
+        // win7需要vc2019运行时
+        var osVer = Environment.OSVersion.Version;
+        var isWin7 = osVer.Major == 6 && osVer.Minor == 1;
+        if (isWin7)
+        {
+            if (is64)
+            {
+                Install("Windows6.1-KB3063858-x64.msu", "/win7", "/quiet /norestart");
+                Install("VC_redist.x64.exe", "/vc2019", "/passive");
+            }
+            else
+            {
+                Install("Windows6.1-KB3063858-x86.msu", "/win7", "/quiet /norestart");
+                Install("VC_redist.x86.exe", "/vc2019", "/passive");
+            }
+        }
+
+        var rs = false;
+        if (is64)
+        {
+            switch (kind)
+            {
+                case "aspnet":
+                    rs = Install("dotnet-runtime-7.0.9-win-x64.exe");
+                    rs = Install("aspnetcore-runtime-7.0.9-win-x64.exe");
+                    break;
+                case "desktop":
+                    rs = Install("windowsdesktop-runtime-7.0.9-win-x64.exe");
+                    break;
+                case "host":
+                    rs = Install("dotnet-hosting-7.0.9-win.exe");
+                    break;
+                default:
+                    rs = Install("dotnet-runtime-7.0.9-win-x64.exe");
+                    break;
+            }
+        }
+        else
+        {
+            switch (kind)
+            {
+                case "aspnet":
+                    rs = Install("dotnet-runtime-7.0.9-win-x86.exe");
+                    rs = Install("aspnetcore-runtime-7.0.9-win-x86.exe");
+                    break;
+                case "desktop":
+                    rs = Install("windowsdesktop-runtime-7.0.9-win-x86.exe");
+                    break;
+                case "host":
+                    rs = Install("dotnet-hosting-7.0.9-win.exe");
+                    break;
+                default:
+                    rs = Install("dotnet-runtime-7.0.9-win-x86.exe");
+                    break;
+            }
+        }
+
+        return rs;
+    }
+
+    /// <summary>获取所有已安装版本</summary>
+    /// <returns></returns>
+    public IList<VerInfo> GetVers()
+    {
+        var vers = new List<VerInfo>();
+        vers.AddRange(Get1To45VersionFromRegistry());
+        vers.AddRange(Get45PlusFromRegistry());
+        vers.AddRange(GetNetCore());
+
+        return vers;
+    }
+
+    public static IList<VerInfo> Get1To45VersionFromRegistry()
+    {
+        var list = new List<VerInfo>();
+#if !NETCOREAPP3_1
+        // 注册表查找 .NET Framework
+        using var ndpKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\NET Framework Setup\NDP\");
+
+        foreach (var versionKeyName in ndpKey.GetSubKeyNames())
+        {
+            // 跳过 .NET Framework 4.5
+            if (versionKeyName == "v4") continue;
+            if (!versionKeyName.StartsWith("v")) continue;
+
+            var versionKey = ndpKey.OpenSubKey(versionKeyName);
+            // 获取 .NET Framework 版本
+            var ver = (String)versionKey.GetValue("Version", "");
+            // 获取SP数字
+            var sp = versionKey.GetValue("SP", "").ToString();
+
+            if (!String.IsNullOrEmpty(ver))
+            {
+                // 获取 installation flag, or an empty string if there is none.
+                var install = versionKey.GetValue("Install", "").ToString();
+                if (String.IsNullOrEmpty(install)) // No install info; it must be in a child subkey.
+                    list.Add(new VerInfo { Name = versionKeyName, Version = ver, Sp = sp });
+                else if (!String.IsNullOrEmpty(sp) && install == "1")
+                    list.Add(new VerInfo { Name = versionKeyName, Version = ver, Sp = sp });
+            }
+            else
+            {
+                foreach (var subKeyName in versionKey.GetSubKeyNames())
+                {
+                    var subKey = versionKey.OpenSubKey(subKeyName);
+                    ver = (String)subKey.GetValue("Version", "");
+                    if (!String.IsNullOrEmpty(ver))
+                    {
+                        var name = ver;
+                        while (name.Length > 3 && name.Substring(name.Length - 2) == ".0")
+                            name = name.Substring(0, name.Length - 2);
+                        if (name[0] != 'v') name = 'v' + name;
+                        sp = subKey.GetValue("SP", "").ToString();
+
+                        var install = subKey.GetValue("Install", "").ToString();
+                        if (String.IsNullOrEmpty(install)) //No install info; it must be later.
+                            list.Add(new VerInfo { Name = name, Version = ver, Sp = sp });
+                        else if (!String.IsNullOrEmpty(sp) && install == "1")
+                            list.Add(new VerInfo { Name = name, Version = ver, Sp = sp });
+                        else if (install == "1")
+                            list.Add(new VerInfo { Name = name, Version = ver, Sp = sp });
+                    }
+                }
+            }
+        }
+#endif
+
+        return list;
+    }
+
+    public static IList<VerInfo> Get45PlusFromRegistry()
+    {
+        var list = new List<VerInfo>();
+#if !NETCOREAPP3_1
+        const String subkey = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\";
+
+        using var ndpKey = Registry.LocalMachine.OpenSubKey(subkey);
+
+        if (ndpKey == null) return list;
+
+        //First check if there's an specific version indicated
+        var name = "";
+        var value = "";
+        var ver = ndpKey.GetValue("Version");
+        if (ver != null) name = ver.ToString();
+        var release = ndpKey.GetValue("Release");
+        if (release != null)
+            value = CheckFor45PlusVersion((Int32)ndpKey.GetValue("Release"));
+
+        if (String.IsNullOrEmpty(name)) name = value;
+        if (String.IsNullOrEmpty(value)) value = name;
+        if (!String.IsNullOrEmpty(name)) list.Add(new VerInfo { Name = "v" + value, Version = name });
+
+        // Checking the version using >= enables forward compatibility.
+        static String CheckFor45PlusVersion(Int32 releaseKey) => releaseKey switch
+        {
+            >= 533325 => "4.8.1",
+            >= 528040 => "4.8",
+            >= 461808 => "4.7.2",
+            >= 461308 => "4.7.1",
+            >= 460798 => "4.7",
+            >= 394802 => "4.6.2",
+            >= 394254 => "4.6.1",
+            >= 393295 => "4.6",
+            >= 379893 => "4.5.2",
+            >= 378675 => "4.5.1",
+            >= 378389 => "4.5",
+            _ => ""
+        };
+#endif
+
+        return list;
+    }
+
+    public static IList<VerInfo> GetNetCore()
+    {
+        var list = new List<VerInfo>();
+
+        var dir = "";
+        if (Environment.OSVersion.Platform <= PlatformID.WinCE)
+        {
+            dir = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+            if (String.IsNullOrEmpty(dir)) return null;
+            dir += "\\dotnet\\shared";
+        }
+        else if (Environment.OSVersion.Platform == PlatformID.Unix)
+            dir = "/usr/share/dotnet/shared";
+
+        var dic = new SortedDictionary<String, VerInfo>();
+        var di = new DirectoryInfo(dir);
+        if (di.Exists)
+        {
+            foreach (var item in di.GetDirectories())
+            {
+                foreach (var elm in item.GetDirectories())
+                {
+                    var name = "v" + elm.Name;
+                    if (item.Name.Contains("AspNet"))
+                        name += "-aspnet";
+                    else if (item.Name.Contains("Desktop"))
+                        name += "-desktop";
+                    if (!dic.ContainsKey(name))
+                    {
+                        dic.Add(name, new VerInfo { Name = name, Version = item.Name + " " + elm.Name });
+                    }
+                }
+            }
+        }
+
+        foreach (var item in dic)
+        {
+            list.Add(item.Value);
+        }
+
+        // 通用处理
+        {
+            var infs = Execute("dotnet", "--list-runtimes")?.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+            if (infs != null)
+            {
+                foreach (var line in infs)
+                {
+                    var ss = line.Split(' ');
+                    if (ss.Length >= 2)
+                    {
+                        var name = "v" + ss[1];
+                        var ver = $"{ss[0]} {ss[1]}";
+                        if (ver.Contains("AspNet"))
+                            name += "-aspnet";
+                        else if (ver.Contains("Desktop"))
+                            name += "-desktop";
+
+                        VerInfo vi = null;
+                        foreach (var item in list)
+                        {
+                            if (item.Name == name)
+                            {
+                                vi = item;
+                                break;
+                            }
+                        }
+                        if (vi == null)
+                        {
+                            vi = new VerInfo { Name = name, Version = ver };
+                            list.Add(vi);
+                        }
+
+                        if (vi.Version.Length < ver.Length) vi.Version = ver;
+                    }
+                }
+            }
+        }
+
+        return list;
+    }
+
+    private static String Execute(String cmd, String arguments = null)
+    {
+        try
+        {
+            var psi = new ProcessStartInfo(cmd, arguments)
+            {
+                // UseShellExecute 必须 false,以便于后续重定向输出流
+                UseShellExecute = false,
+                CreateNoWindow = true,
+                WindowStyle = ProcessWindowStyle.Hidden,
+                RedirectStandardOutput = true,
+                //RedirectStandardError = true,
+            };
+            var process = Process.Start(psi);
+            if (!process.WaitForExit(3_000))
+            {
+                process.Kill();
+                return null;
+            }
+
+            return process.StandardOutput.ReadToEnd();
+        }
+        catch { return null; }
+    }
+    #endregion
+
+    #region 辅助
+    public static String GetMD5(String fileName)
+    {
+        var fi = new FileInfo(fileName);
+        var md5 = MD5.Create();
+        using var fs = fi.OpenRead();
+        var buf = md5.ComputeHash(fs);
+        var hex = BitConverter.ToString(buf).Replace("-", null);
+
+        return hex;
+    }
+
+    /// <summary>加载内嵌的文件MD5信息</summary>
+    /// <returns></returns>
+    public static IDictionary<String, String> LoadMD5s()
+    {
+        var asm = Assembly.GetExecutingAssembly();
+        var ms = asm.GetManifestResourceStream(typeof(NetRuntime).Namespace + ".res.md5.txt");
+
+        var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
+        using var reader = new StreamReader(ms);
+        while (!reader.EndOfStream)
+        {
+            var line = reader.ReadLine()?.Trim();
+            if (String.IsNullOrEmpty(line)) continue;
+
+            var ss = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
+            if (ss.Length >= 2)
+            {
+                dic[ss[0]] = ss[1];
+            }
+        }
+
+        return dic;
+    }
+
+    /// <summary>为安装NET清理环境</summary>
+    public static void CleanForNet()
+    {
+        {
+            // 解决“一般信任关系失败”问题
+
+            Process.Start("regsvr32", "/s Softpub.dll").WaitForExit(15_000);
+            Process.Start("regsvr32", "/s Wintrust.dll").WaitForExit(15_000);
+            Process.Start("regsvr32", "/s Initpki.dll").WaitForExit(15_000);
+            Process.Start("regsvr32", "/s Mssip32.dll").WaitForExit(15_000);
+        }
+
+        {
+#if !NETCOREAPP3_1
+            using var reg = Registry.CurrentUser.OpenSubKey(@"\Software\Microsoft\Windows\CurrentVersion\WinTrust\Trust Providers\Software Publishing", true);
+            if (reg != null)
+            {
+                var v = (Int32)reg.GetValue("State");
+                if (v != 0x23c00) reg.SetValue("State", 0x23c00);
+            }
+#endif
+        }
+
+        {
+            // https://support.microsoft.com/zh-cn/sbs/windows/%E4%BF%AE%E5%A4%8D-windows-%E6%9B%B4%E6%96%B0%E9%94%99%E8%AF%AF-18b693b5-7818-5825-8a7e-2a4a37d6d787
+            Process.Start("net", "stop bits").WaitForExit(15_000);
+            Process.Start("net", "stop wuauserv").WaitForExit(15_000);
+
+            var dirs = new[] {
+                @"C:\Windows\SoftwareDistribution",
+                @"C:\Windows\System32\catroot2",
+            };
+            foreach (var dir in dirs)
+            {
+                if (Directory.Exists(dir))
+                {
+                    try
+                    {
+                        Directory.Move(dir, dir + "_bak");
+                    }
+                    catch (Exception ex)
+                    {
+                        XTrace.WriteLine(ex.Message);
+                    }
+                }
+            }
+
+            Process.Start("net", "start bits").WaitForExit(15_000);
+            Process.Start("net", "start wuauserv").WaitForExit(15_000);
+        }
+
+        {
+            //!!! 以下代码,能够破坏掉已经安装好的.NET4.0
+            //var files = new[] {
+            //    @"C:\Windows\System32\msvcr100_clr0400.dll",
+            //    @"C:\Windows\SysWOW64\msvcr100_clr0400.dll",
+            //    @"C:\Windows\System32\msvcr110_clr0400.dll",
+            //    @"C:\Windows\SysWOW64\msvcr110_clr0400.dll",
+            //    @"C:\Windows\System32\msvcp110_clr0400.dll",
+            //    @"C:\Windows\SysWOW64\msvcp110_clr0400.dll",
+            //};
+            //foreach (var file in files)
+            //{
+            //    if (File.Exists(file))
+            //    {
+            //        try
+            //        {
+            //            XTrace.WriteLine("清理:{0}", file);
+            //            File.Move(file, file + ".bak");
+            //        }
+            //        catch
+            //        {
+            //            XTrace.WriteLine("重命名文件失败,请手工重命名:{0}", file);
+            //        }
+            //    }
+            //}
+        }
+
+        InstallCert();
+    }
+
+    public static Boolean InstallCert()
+    {
+        XTrace.WriteLine("准备安装微软根证书");
+
+        // 释放文件
+        var asm = Assembly.GetExecutingAssembly();
+        var names = new[] { "CertMgr.Exe", "MicrosoftRootCertificateAuthority2011.cer" };
+        foreach (var name in names)
+        {
+            var ms = asm.GetManifestResourceStream(typeof(NetRuntime).Namespace + ".res." + name);
+            var buf = ms.ReadBytes(-1);
+
+            File.WriteAllBytes(name.GetFullPath(), buf);
+        }
+
+        var exe = names[0];
+        var cert = names[1];
+        if (!File.Exists(exe) || !File.Exists(cert)) return false;
+
+        // 执行
+        try
+        {
+            var p = Process.Start(exe, $"-add \"{cert}\" -s -r localMachine AuthRoot");
+
+            return p.WaitForExit(30_000) && p.ExitCode == 0;
+        }
+        catch (Exception ex)
+        {
+            XTrace.WriteLine(ex.Message);
+            return false;
+        }
+        finally
+        {
+            if (File.Exists(cert)) File.Delete(cert);
+            if (File.Exists(exe)) File.Delete(exe);
+        }
+    }
+    #endregion
+}
\ No newline at end of file
Added +21 -0
diff --git a/Installer/Program.cs b/Installer/Program.cs
new file mode 100644
index 0000000..a05fa32
--- /dev/null
+++ b/Installer/Program.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Windows.Forms;
+using NewLife.Log;
+
+namespace Installer;
+
+internal static class Program
+{
+    /// <summary>
+    /// 应用程序的主入口点。
+    /// </summary>
+    [STAThread]
+    static void Main()
+    {
+        XTrace.UseWinForm();
+
+        Application.EnableVisualStyles();
+        Application.SetCompatibleTextRenderingDefault(false);
+        Application.Run(new FrmMain());
+    }
+}
Added +36 -0
diff --git a/Installer/Properties/AssemblyInfo.cs b/Installer/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..bc5c59c
--- /dev/null
+++ b/Installer/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("Installer")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Installer")]
+[assembly: AssemblyCopyright("Copyright © NewLife 2023")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
+[assembly: Guid("12503268-dd9b-4a68-b53c-edcd71f2d485")]
+
+// 程序集的版本信息由下列四个值组成: 
+//
+//      主版本
+//      次版本
+//      生成号
+//      修订号
+//
+//可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值
+//通过使用 "*",如下所示:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.7.*")]
+[assembly: AssemblyFileVersion("1.7.2023.0531")]
Added +71 -0
diff --git a/Installer/Properties/Resources.Designer.cs b/Installer/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..badea14
--- /dev/null
+++ b/Installer/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     此代码由工具生成。
+//     运行时版本: 4.0.30319.42000
+//
+//     对此文件的更改可能导致不正确的行为,如果
+//     重新生成代码,则所做更改将丢失。
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Installer.Properties
+{
+
+
+    /// <summary>
+    ///   强类型资源类,用于查找本地化字符串等。
+    /// </summary>
+    // 此类是由 StronglyTypedResourceBuilder
+    // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
+    // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
+    // (以 /str 作为命令选项),或重新生成 VS 项目。
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    internal class Resources
+    {
+
+        private static global::System.Resources.ResourceManager resourceMan;
+
+        private static global::System.Globalization.CultureInfo resourceCulture;
+
+        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        internal Resources()
+        {
+        }
+
+        /// <summary>
+        ///   返回此类使用的缓存 ResourceManager 实例。
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Resources.ResourceManager ResourceManager
+        {
+            get
+            {
+                if ((resourceMan == null))
+                {
+                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Installer.Properties.Resources", typeof(Resources).Assembly);
+                    resourceMan = temp;
+                }
+                return resourceMan;
+            }
+        }
+
+        /// <summary>
+        ///   重写当前线程的 CurrentUICulture 属性,对
+        ///   使用此强类型资源类的所有资源查找执行重写。
+        /// </summary>
+        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+        internal static global::System.Globalization.CultureInfo Culture
+        {
+            get
+            {
+                return resourceCulture;
+            }
+            set
+            {
+                resourceCulture = value;
+            }
+        }
+    }
+}
Added +117 -0
diff --git a/Installer/Properties/Resources.resx b/Installer/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/Installer/Properties/Resources.resx
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+</root>
\ No newline at end of file
Added +30 -0
diff --git a/Installer/Properties/Settings.Designer.cs b/Installer/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..a25727d
--- /dev/null
+++ b/Installer/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+//     This code was generated by a tool.
+//     Runtime Version:4.0.30319.42000
+//
+//     Changes to this file may cause incorrect behavior and will be lost if
+//     the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace Installer.Properties
+{
+
+
+    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+    {
+
+        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+        public static Settings Default
+        {
+            get
+            {
+                return defaultInstance;
+            }
+        }
+    }
+}
Added +7 -0
diff --git a/Installer/Properties/Settings.settings b/Installer/Properties/Settings.settings
new file mode 100644
index 0000000..3964565
--- /dev/null
+++ b/Installer/Properties/Settings.settings
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
+  <Profiles>
+    <Profile Name="(Default)" />
+  </Profiles>
+  <Settings />
+</SettingsFile>
Added +14 -0
diff --git a/Installer/VerInfo.cs b/Installer/VerInfo.cs
new file mode 100644
index 0000000..b20c7ae
--- /dev/null
+++ b/Installer/VerInfo.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Installer;
+
+public class VerInfo
+{
+    public String Name { get; set; }
+
+    public String Version { get; set; }
+
+    public String Sp { get; set; }
+
+    public override String ToString() => String.IsNullOrEmpty(Sp) ? $"{Name} {Version}" : $"{Name} {Version} Sp{Sp}";
+}
Added +290 -0
diff --git a/Installer/WindowsService.cs b/Installer/WindowsService.cs
new file mode 100644
index 0000000..4c66879
--- /dev/null
+++ b/Installer/WindowsService.cs
@@ -0,0 +1,290 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Security.Principal;
+using System.Text;
+using NewLife;
+using NewLife.Log;
+using static Installer.Advapi32;
+
+namespace Installer;
+
+public class WindowsService
+{
+    #region 服务状态和控制
+    /// <summary>服务是否已安装</summary>
+    /// <param name="serviceName">服务名</param>
+    /// <returns></returns>
+    public Boolean IsInstalled(String serviceName)
+    {
+        using var manager = new SafeServiceHandle(OpenSCManager(null, null, ServiceControllerOptions.SC_MANAGER_CONNECT));
+        if (manager == null || manager.IsInvalid) return false;
+
+        using var service = new SafeServiceHandle(OpenService(manager, serviceName, ServiceOptions.SERVICE_QUERY_CONFIG));
+        return service != null && !service.IsInvalid;
+    }
+
+    /// <summary>服务是否已启动</summary>
+    /// <param name="serviceName">服务名</param>
+    /// <returns></returns>
+    public unsafe Boolean IsRunning(String serviceName)
+    {
+        using var manager = new SafeServiceHandle(OpenSCManager(null, null, ServiceControllerOptions.SC_MANAGER_CONNECT));
+        if (manager == null || manager.IsInvalid) return false;
+
+        using var service = new SafeServiceHandle(OpenService(manager, serviceName, ServiceOptions.SERVICE_QUERY_STATUS));
+        if (service == null || service.IsInvalid) return false;
+
+        SERVICE_STATUS status = default;
+        return !QueryServiceStatus(service, &status)
+            ? throw new Win32Exception(Marshal.GetLastWin32Error())
+            : status.currentState == ServiceControllerStatus.Running;
+    }
+
+    /// <summary>安装服务</summary>
+    /// <param name="serviceName">服务名</param>
+    /// <param name="displayName"></param>
+    /// <param name="binPath"></param>
+    /// <param name="description"></param>
+    /// <returns></returns>
+    public Boolean Install(String serviceName, String displayName, String binPath, String description)
+    {
+        XTrace.WriteLine("{0}.Install {1}, {2}, {3}, {4}", GetType().Name, serviceName, displayName, binPath, description);
+
+        using var manager = new SafeServiceHandle(OpenSCManager(null, null, ServiceControllerOptions.SC_MANAGER_CREATE_SERVICE));
+        if (manager.IsInvalid)
+        {
+            XTrace.WriteLine("安装Windows服务要求以管理员运行");
+            throw new Win32Exception(Marshal.GetLastWin32Error());
+        }
+
+        using var service = new SafeServiceHandle(CreateService(manager, serviceName, displayName, ServiceOptions.SERVICE_ALL_ACCESS, 0x10, 2, 1, binPath, null, 0, null, null, null));
+        if (service.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
+
+        // 设置描述信息
+        if (!description.IsNullOrEmpty())
+        {
+            SERVICE_DESCRIPTION sd;
+            sd.Description = Marshal.StringToHGlobalUni(description);
+            var lpInfo = Marshal.AllocHGlobal(Marshal.SizeOf(sd));
+
+            try
+            {
+                Marshal.StructureToPtr(sd, lpInfo, false);
+
+                const Int32 SERVICE_CONFIG_DESCRIPTION = 1;
+                ChangeServiceConfig2(service, SERVICE_CONFIG_DESCRIPTION, lpInfo);
+            }
+            finally
+            {
+                Marshal.FreeHGlobal(lpInfo);
+                Marshal.FreeHGlobal(sd.Description);
+            }
+        }
+
+        return true;
+    }
+
+    /// <summary>卸载服务</summary>
+    /// <param name="serviceName">服务名</param>
+    /// <returns></returns>
+    public unsafe Boolean Remove(String serviceName)
+    {
+        XTrace.WriteLine("{0}.Remove {1}", GetType().Name, serviceName);
+
+        if (!IsAdministrator()) return RunAsAdministrator("-uninstall");
+
+        using var manager = new SafeServiceHandle(OpenSCManager(null, null, ServiceControllerOptions.SC_MANAGER_ALL));
+        if (manager.IsInvalid)
+        {
+            XTrace.WriteLine("卸载Windows服务要求以管理员运行");
+            throw new Win32Exception(Marshal.GetLastWin32Error());
+        }
+
+        using var service = new SafeServiceHandle(OpenService(manager, serviceName, ServiceOptions.SERVICE_STOP | ServiceOptions.STANDARD_RIGHTS_DELETE));
+        if (service.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
+
+        SERVICE_STATUS status = default;
+        ControlService(service, ControlOptions.Stop, &status);
+
+        return DeleteService(service) == 0 ? throw new Win32Exception(Marshal.GetLastWin32Error()) : true;
+    }
+
+    /// <summary>启动服务</summary>
+    /// <param name="serviceName">服务名</param>
+    /// <returns></returns>
+    public Boolean Start(String serviceName)
+    {
+        XTrace.WriteLine("{0}.Start {1}", GetType().Name, serviceName);
+
+        if (!IsAdministrator()) return RunAsAdministrator("-start");
+
+        using var manager = new SafeServiceHandle(OpenSCManager(null, null, ServiceControllerOptions.SC_MANAGER_CONNECT));
+        if (manager.IsInvalid)
+        {
+            XTrace.WriteLine("启动Windows服务要求以管理员运行");
+            throw new Win32Exception(Marshal.GetLastWin32Error());
+        }
+
+        using var service = new SafeServiceHandle(OpenService(manager, serviceName, ServiceOptions.SERVICE_START));
+        return service.IsInvalid
+            ? throw new Win32Exception(Marshal.GetLastWin32Error())
+            : !StartService(service, 0, IntPtr.Zero) ? throw new Win32Exception(Marshal.GetLastWin32Error()) : true;
+    }
+
+    /// <summary>停止服务</summary>
+    /// <param name="serviceName">服务名</param>
+    /// <returns></returns>
+    public unsafe Boolean Stop(String serviceName)
+    {
+        XTrace.WriteLine("{0}.Stop {1}", GetType().Name, serviceName);
+
+        if (!IsAdministrator()) return RunAsAdministrator("-stop");
+
+        using var manager = new SafeServiceHandle(OpenSCManager(null, null, ServiceControllerOptions.SC_MANAGER_ALL));
+        if (manager.IsInvalid)
+        {
+            XTrace.WriteLine("停止Windows服务要求以管理员运行");
+            throw new Win32Exception(Marshal.GetLastWin32Error());
+        }
+
+        using var service = new SafeServiceHandle(OpenService(manager, serviceName, ServiceOptions.SERVICE_STOP));
+        if (service.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
+
+        SERVICE_STATUS status = default;
+        return !ControlService(service, ControlOptions.Stop, &status) ? throw new Win32Exception(Marshal.GetLastWin32Error()) : true;
+    }
+
+    /// <summary>重启服务</summary>
+    /// <param name="serviceName">服务名</param>
+    public Boolean Restart(String serviceName)
+    {
+        XTrace.WriteLine("{0}.Restart {1}", GetType().Name, serviceName);
+
+        if (!IsAdministrator()) return RunAsAdministrator("-restart");
+
+        //if (InService)
+        {
+            var cmd = $"/c net stop {serviceName} & ping 127.0.0.1 -n 5 & net start {serviceName}";
+            Process.Start("cmd.exe", cmd);
+        }
+        //else
+        //{
+        //    Process.Start(Service.GetExeName(), "-run -delay");
+        //}
+
+        //// 在临时目录生成重启服务的批处理文件
+        //var filename = "重启.bat".GetFullPath();
+        //if (File.Exists(filename)) File.Delete(filename);
+
+        //File.AppendAllText(filename, "net stop " + serviceName);
+        //File.AppendAllText(filename, Environment.NewLine);
+        //File.AppendAllText(filename, "ping 127.0.0.1 -n 5 > nul ");
+        //File.AppendAllText(filename, Environment.NewLine);
+        //File.AppendAllText(filename, "net start " + serviceName);
+
+        ////执行重启服务的批处理
+        ////RunCmd(filename, false, false);
+        //var p = new Process();
+        //var si = new ProcessStartInfo
+        //{
+        //    FileName = filename,
+        //    UseShellExecute = true,
+        //    CreateNoWindow = true
+        //};
+        //p.StartInfo = si;
+
+        //p.Start();
+
+        ////if (File.Exists(filename)) File.Delete(filename);
+
+        return true;
+    }
+
+    static Boolean RunAsAdministrator(String argument)
+    {
+        var exe = ExecutablePath;
+        if (exe.IsNullOrEmpty()) return false;
+
+        if (exe.EndsWithIgnoreCase(".dll"))
+        {
+            var exe2 = Path.ChangeExtension(exe, ".exe");
+            if (File.Exists(exe2)) exe = exe2;
+        }
+
+        var startInfo = exe.EndsWithIgnoreCase(".dll") ?
+            new ProcessStartInfo
+            {
+                FileName = "dotnet",
+                Arguments = $"{Path.GetFileName(exe)} {argument}",
+                WorkingDirectory = Path.GetDirectoryName(exe),
+                Verb = "runas",
+                UseShellExecute = true,
+            } :
+            new ProcessStartInfo
+            {
+                FileName = exe,
+                Arguments = argument,
+                Verb = "runas",
+                UseShellExecute = true,
+            };
+
+        var p = Process.Start(startInfo);
+        return !p.WaitForExit(5_000) || p.ExitCode == 0;
+    }
+
+    static String _executablePath;
+    static String ExecutablePath
+    {
+        get
+        {
+            if (_executablePath == null)
+            {
+                var entryAssembly = Assembly.GetEntryAssembly();
+                if (entryAssembly != null)
+                {
+                    var codeBase = entryAssembly.CodeBase;
+                    var uri = new Uri(codeBase);
+                    _executablePath = uri.IsFile ? uri.LocalPath + Uri.UnescapeDataString(uri.Fragment) : uri.ToString();
+                }
+                else
+                {
+                    var moduleFileNameLongPath = GetModuleFileNameLongPath(new HandleRef(null, IntPtr.Zero));
+                    _executablePath = moduleFileNameLongPath.ToString().GetFullPath();
+                }
+            }
+
+            return _executablePath;
+        }
+    }
+
+    static StringBuilder GetModuleFileNameLongPath(HandleRef hModule)
+    {
+        var sb = new StringBuilder(260);
+        var num = 1;
+        var num2 = 0;
+        while ((num2 = GetModuleFileName(hModule, sb, sb.Capacity)) == sb.Capacity && Marshal.GetLastWin32Error() == 122 && sb.Capacity < 32767)
+        {
+            num += 2;
+            var capacity = (num * 260 < 32767) ? (num * 260) : 32767;
+            sb.EnsureCapacity(capacity);
+        }
+        sb.Length = num2;
+        return sb;
+    }
+
+    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
+    static extern Int32 GetModuleFileName(HandleRef hModule, StringBuilder buffer, Int32 length);
+
+    public Boolean IsAdministrator()
+    {
+        var current = WindowsIdentity.GetCurrent();
+        var windowsPrincipal = new WindowsPrincipal(current);
+        return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
+    }
+    #endregion
+}
Added +79 -0
diff --git a/Installer4/app.manifest b/Installer4/app.manifest
new file mode 100644
index 0000000..2e0ad7a
--- /dev/null
+++ b/Installer4/app.manifest
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
+  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+    <security>
+      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+        <!-- UAC 清单选项
+             如果想要更改 Windows 用户帐户控制级别,请使用
+             以下节点之一替换 requestedExecutionLevel 节点。
+
+        <requestedExecutionLevel  level="asInvoker" uiAccess="false" />
+        <requestedExecutionLevel  level="requireAdministrator" uiAccess="false" />
+        <requestedExecutionLevel  level="highestAvailable" uiAccess="false" />
+
+            指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
+            如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
+            元素。
+        -->
+        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
+      </requestedPrivileges>
+    </security>
+  </trustInfo>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
+           Windows 版本的列表。取消评论适当的元素,
+           Windows 将自动选择最兼容的环境。 -->
+
+      <!-- Windows Vista -->
+      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
+
+      <!-- Windows 7 -->
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
+
+      <!-- Windows 8 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
+
+      <!-- Windows 8.1 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
+
+      <!-- Windows 10 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
+
+    </application>
+  </compatibility>
+
+  <!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
+       自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
+       选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
+       在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
+       
+       将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
+  
+  <application xmlns="urn:schemas-microsoft-com:asm.v3">
+    <windowsSettings>
+      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
+      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
+    </windowsSettings>
+  </application>
+  
+
+  <!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
+  <!--
+  <dependency>
+    <dependentAssembly>
+      <assemblyIdentity
+          type="win32"
+          name="Microsoft.Windows.Common-Controls"
+          version="6.0.0.0"
+          processorArchitecture="*"
+          publicKeyToken="6595b64144ccf1df"
+          language="*"
+        />
+    </dependentAssembly>
+  </dependency>
+  -->
+
+</assembly>
Added +61 -0
diff --git a/Installer4/Installer4.csproj b/Installer4/Installer4.csproj
new file mode 100644
index 0000000..6eb22ff
--- /dev/null
+++ b/Installer4/Installer4.csproj
@@ -0,0 +1,61 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>WinExe</OutputType>
+    <TargetFramework>net4.0</TargetFramework>
+    <AssemblyName>Installer</AssemblyName>
+    <RootNamespace>Installer</RootNamespace>
+    <AssemblyTitle>安装助手</AssemblyTitle>
+    <Description>辅助客户端打包,指定下载需要的文件</Description>
+    <Company>新生命开发团队</Company>
+    <Copyright>©2002-2023 NewLife</Copyright>
+    <VersionPrefix>1.7</VersionPrefix>
+    <VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
+    <Version>$(VersionPrefix).$(VersionSuffix)</Version>
+    <FileVersion>$(Version)</FileVersion>
+    <AssemblyVersion>$(VersionPrefix).*</AssemblyVersion>
+    <Deterministic>false</Deterministic>
+    <OutputPath>..\Bin\Installer4</OutputPath>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <LangVersion>latest</LangVersion>
+    <UseWindowsForms>true</UseWindowsForms>
+    <PlatformTarget>AnyCPU</PlatformTarget>
+    <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
+    <ApplicationManifest>app.manifest</ApplicationManifest>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="..\Installer\Advapi32.cs" Link="Advapi32.cs" />
+    <Compile Include="..\Installer\DownloadHelper.cs" Link="DownloadHelper.cs" />
+    <Compile Include="..\Installer\FrmMain.cs" Link="FrmMain.cs" />
+    <Compile Include="..\Installer\FrmMain.Designer.cs" Link="FrmMain.Designer.cs" />
+    <Compile Include="..\Installer\Helper.cs" Link="Helper.cs" />
+    <Compile Include="..\Installer\NetRuntime.cs" Link="NetRuntime.cs" />
+    <Compile Include="..\Installer\VerInfo.cs" Link="VerInfo.cs" />
+    <Compile Include="..\Installer\WindowsService.cs" Link="WindowsService.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="..\LuckyClover\res\md5.txt" Link="res\md5.txt" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="..\Installer\FrmMain.resx" Link="FrmMain.resx" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="NewLife.Core" Version="10.6.2024.101-net40" />
+    <PackageReference Include="NewLife.Stardust" Version="2.9.2024.101" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Folder Include="res\" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="..\LuckyClover\res\CertMgr.Exe" Link="res\CertMgr.Exe" />
+    <EmbeddedResource Include="..\LuckyClover\res\MicrosoftRootCertificateAuthority2011.cer" Link="res\MicrosoftRootCertificateAuthority2011.cer" />
+  </ItemGroup>
+
+</Project>
\ No newline at end of file
Added +23 -0
diff --git a/Installer4/Program.cs b/Installer4/Program.cs
new file mode 100644
index 0000000..79ee8d2
--- /dev/null
+++ b/Installer4/Program.cs
@@ -0,0 +1,23 @@
+using NewLife.Log;
+using Stardust;
+
+namespace Installer;
+
+internal static class Program
+{
+    /// <summary>
+    /// 应用程序的主入口点。
+    /// </summary>
+    [STAThread]
+    static void Main()
+    {
+        XTrace.UseWinForm();
+
+        var star = new StarFactory("http://s.newlifex.com:6600", null, null);
+        DefaultTracer.Instance = star.Tracer;
+
+        Application.EnableVisualStyles();
+        Application.SetCompatibleTextRenderingDefault(false);
+        Application.Run(new FrmMain());
+    }
+}
\ No newline at end of file
Modified +65 -21
diff --git a/LuckyClover.sln b/LuckyClover.sln
index 245613f..1d1f1c5 100644
--- a/LuckyClover.sln
+++ b/LuckyClover.sln
@@ -10,10 +10,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{52E5A5
 		Readme.MD = Readme.MD
 	EndProjectSection
 EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Clover", "Clover\Clover.vcxproj", "{85063414-B2A7-4AC8-B7EF-AB17237A96FC}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Clover", "Clover\Clover.vcxproj", "{8A06ED40-1CEA-4217-868E-050970F7ACEB}"
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CloverLinux", "CloverLinux\CloverLinux.vcxproj", "{9F1E9996-58AA-4148-A809-2E12D67D236B}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Installer", "Installer\Installer.csproj", "{12503268-DD9B-4A68-B53C-EDCD71F2D485}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Installer4", "Installer4\Installer4.csproj", "{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -48,26 +52,26 @@ Global
 		{F1A1DD43-A06A-4ADD-8303-D33310CB0ED3}.Release|x64.Build.0 = Release|Any CPU
 		{F1A1DD43-A06A-4ADD-8303-D33310CB0ED3}.Release|x86.ActiveCfg = Release|Any CPU
 		{F1A1DD43-A06A-4ADD-8303-D33310CB0ED3}.Release|x86.Build.0 = Release|Any CPU
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|Any CPU.ActiveCfg = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|Any CPU.Build.0 = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|ARM.ActiveCfg = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|ARM.Build.0 = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|ARM64.ActiveCfg = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|ARM64.Build.0 = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|x64.ActiveCfg = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|x64.Build.0 = Debug|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|x86.ActiveCfg = Debug|Win32
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Debug|x86.Build.0 = Debug|Win32
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|Any CPU.ActiveCfg = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|Any CPU.Build.0 = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|ARM.ActiveCfg = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|ARM.Build.0 = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|ARM64.ActiveCfg = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|ARM64.Build.0 = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|x64.ActiveCfg = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|x64.Build.0 = Release|x64
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|x86.ActiveCfg = Release|Win32
-		{85063414-B2A7-4AC8-B7EF-AB17237A96FC}.Release|x86.Build.0 = Release|Win32
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|Any CPU.ActiveCfg = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|Any CPU.Build.0 = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|ARM.ActiveCfg = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|ARM.Build.0 = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|ARM64.ActiveCfg = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|ARM64.Build.0 = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|x64.ActiveCfg = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|x64.Build.0 = Debug|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|x86.ActiveCfg = Debug|Win32
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Debug|x86.Build.0 = Debug|Win32
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|Any CPU.ActiveCfg = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|Any CPU.Build.0 = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|ARM.ActiveCfg = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|ARM.Build.0 = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|ARM64.ActiveCfg = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|ARM64.Build.0 = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|x64.ActiveCfg = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|x64.Build.0 = Release|x64
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|x86.ActiveCfg = Release|Win32
+		{8A06ED40-1CEA-4217-868E-050970F7ACEB}.Release|x86.Build.0 = Release|Win32
 		{9F1E9996-58AA-4148-A809-2E12D67D236B}.Debug|Any CPU.ActiveCfg = Debug|x64
 		{9F1E9996-58AA-4148-A809-2E12D67D236B}.Debug|Any CPU.Build.0 = Debug|x64
 		{9F1E9996-58AA-4148-A809-2E12D67D236B}.Debug|Any CPU.Deploy.0 = Debug|x64
@@ -98,6 +102,46 @@ Global
 		{9F1E9996-58AA-4148-A809-2E12D67D236B}.Release|x86.ActiveCfg = Release|x86
 		{9F1E9996-58AA-4148-A809-2E12D67D236B}.Release|x86.Build.0 = Release|x86
 		{9F1E9996-58AA-4148-A809-2E12D67D236B}.Release|x86.Deploy.0 = Release|x86
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|ARM.Build.0 = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|x64.Build.0 = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Debug|x86.Build.0 = Debug|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|Any CPU.Build.0 = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|ARM.ActiveCfg = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|ARM.Build.0 = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|ARM64.Build.0 = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|x64.ActiveCfg = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|x64.Build.0 = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|x86.ActiveCfg = Release|Any CPU
+		{12503268-DD9B-4A68-B53C-EDCD71F2D485}.Release|x86.Build.0 = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|ARM.ActiveCfg = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|ARM.Build.0 = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|x64.Build.0 = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Debug|x86.Build.0 = Debug|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|ARM.ActiveCfg = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|ARM.Build.0 = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|ARM64.Build.0 = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|x64.ActiveCfg = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|x64.Build.0 = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|x86.ActiveCfg = Release|Any CPU
+		{6E9F5946-AA92-4B5C-9FBD-5DCB68D6B47D}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
Modified +8 -1
diff --git a/LuckyClover/LuckyClover.csproj b/LuckyClover/LuckyClover.csproj
index b5469ab..b11259b 100644
--- a/LuckyClover/LuckyClover.csproj
+++ b/LuckyClover/LuckyClover.csproj
@@ -46,10 +46,17 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="PublishAotCompressed" Version="1.0.1" />
+    <None Remove="res\CertMgr.Exe" />
+    <None Remove="res\MicrosoftRootCertificateAuthority2011.cer" />
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="PublishAotCompressed" Version="1.0.3" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="res\CertMgr.Exe" />
     <EmbeddedResource Include="res\md5.txt" />
+    <EmbeddedResource Include="res\MicrosoftRootCertificateAuthority2011.cer" />
   </ItemGroup>
 </Project>