fix: clover 补齐架构识别、SP/WIC 预检与退出码提示 实测暴露的问题集中修正: 1) 架构:新增 PE Machine 解析,x64 安装包装到 32 位系统时直接跳过并报错(此前只得到「不是有效的 Win32 应用程序」);.NET2.0 在 64 位系统改下载 NetFx20SP2_x64.exe(x86 包会在会话 0 挂起);32 位进程跑在 64 位系统上不再用 IntPtr.Size 判断系统架构。 2) SP 预检:.NET2.0 要求 XP SP2 / 2003 SP1,低于该版本直接给结论,避免安装器 300s 无响应;4.0 补充 XP SP3 / 2003 SP2 提示。 3) WIC:XP/2003 装 4.0 前自动尝试 wic_x86/wic_x64,装不上时明确说明 1603 的原因。 4) 退出码翻译:1603/5100/1638/1618/5/216,失败时直接给出可执行建议。 5) net11 的支持性判定此前恒为 false,已修正(Shared 与 C++ 同步)。 6) 横幅新增「系统/进程位数」,便于区分 x86/x64 产物。 7) 无参数且无交互输入时打印用法,修掉 Linux 下的 ArgumentNullException。 8) AOT 产物在输入/输出被重定向时跳过 ReadKey,修掉非交互环境 0xC0000409。大石头 authored at 2026-11-02 09:01:16
diff --git a/.github/skills/vm-compat-matrix/SKILL.md b/.github/skills/vm-compat-matrix/SKILL.md
index 5262f08..c69cc6e 100644
--- a/.github/skills/vm-compat-matrix/SKILL.md
+++ b/.github/skills/vm-compat-matrix/SKILL.md
@@ -84,10 +84,14 @@ dotnet Tools/VmTest.cs stop "<虚拟机名>"
| `-998` | vmrun/VIX 调用失败(Tools 掉线/未运行) | `net start vmtools` 后按 `--filter` 单独重跑 |
| `-999` | 客机命令超时(多半缺运行时弹安装框) | 看 `.tmp\vmtest\shots\*_timeout.png` |
-**AOT 教训(2026-11-02)**:`cloverAot` 末尾有 `Console.ReadKey()`,只要 stdout/stdin 不是真控制台就抛 `InvalidOperation_ConsoleReadKeyOnFile` 并以 `0xC0000409` 退出——宿主 Windows 11 同样复现,**与客机系统版本无关**。`Classify` 已见到该异常直接判 ✅。别再把「出 banner 后异常」写成系统兼容性结论。
+**AOT 教训(2026-11-02)**:`cloverAot` 末尾有 `Console.ReadKey()`,只要 stdout/stdin 不是真控制台就抛 `InvalidOperation_ConsoleReadKeyOnFile` 并以 `0xC0000409` 退出——宿主 Windows 11 同样复现,**与客机系统版本无关**。v1.2 起产物已改为「重定向时跳过按键」,但 `Classify` 仍保留该识别(旧产物、旧快照里拷出来的二进制还会遇到)。别再把「出 banner 后异常」写成系统兼容性结论。
**自包含 vs 框架依赖**:`clover10-x86.exe` 是自包含单文件,跑通只说明该系统的 Win32 接口够用,**不代表装了 .NET 运行时**;`clover10.exe`(x64 框架依赖)才验证运行时。两者不一致时以 x64 为准并在备注写清。
+**测前必须重建产物**:`Bin\vmtest` 里可能残留 `Bin\publish` 的旧版本(曾出现 clover 只认 `net2|net35|net4|net45|net48` 的 v1.1 老二进制被当成"不支持 net6/8/10")。跑矩阵前先 `dotnet Tools/VmTest.cs build`,再用 `clover.exe list` 确认 banner 版本与自动计划符合预期。
+
+**clover 已具备的预检(v1.2)**:架构识别(PE Machine 校验,x64 包装到 32 位系统直接跳过并报错)、SP 预检(.NET 2.0 要求 XP SP2 / 2003 SP1,避免安装器长时间无响应)、WIC 预检(XP/2003 装 4.0 前自动尝试 `wic_x86/x64.exe`)、退出码翻译(1603/5100/1638/1618/5)。
+
## 回写 `Doc/os_net.md` 的硬要求
- 一行一结论,改动只碰对应行;新增系统插到同族末尾
diff --git a/Clover/CGetOSVersion.cpp b/Clover/CGetOSVersion.cpp
index 1565562..03265d0 100644
--- a/Clover/CGetOSVersion.cpp
+++ b/Clover/CGetOSVersion.cpp
@@ -34,6 +34,7 @@ CGetOSVersion::CGetOSVersion()
m_Info.Build = 0;
m_Info.IsServer = false;
m_Info.Is64Bit = false;
+ m_Info.ServicePack = 0;
}
CGetOSVersion::~CGetOSVersion()
@@ -71,6 +72,7 @@ bool CGetOSVersion::CheckOSVer()
m_Info.Major = 0;
m_Info.Minor = 0;
m_Info.Build = 0;
+ m_Info.ServicePack = 0;
m_Info.IsServer = IsServerOS();
m_Info.Desc = "未知的系统版本";
@@ -131,6 +133,10 @@ bool CGetOSVersion::CheckOSVer()
if (bExcute) m_Info.Desc = FormatDesc(m_Info.Major, m_Info.Minor, m_Info.Build, m_Info.IsServer, csd);
+ // 从 CSDVersion(如 "Service Pack 3")解析 Service Pack 序号
+ auto ps = csd.find("Service Pack");
+ if (ps != std::string::npos) m_Info.ServicePack = (DWORD)atoi(csd.c_str() + ps + 12);
+
return bExcute;
}
diff --git a/Clover/CGetOSVersion.h b/Clover/CGetOSVersion.h
index 1bfbd75..dc5bbf1 100644
--- a/Clover/CGetOSVersion.h
+++ b/Clover/CGetOSVersion.h
@@ -14,6 +14,8 @@ struct OSVersionInfo
bool IsServer;
/// <summary>是否64位系统</summary>
bool Is64Bit;
+ /// <summary>Service Pack 序号,如 3;取不到时为 0</summary>
+ DWORD ServicePack;
/// <summary>版本描述,如 Windows 7 SP1</summary>
std::string Desc;
};
diff --git a/Clover/Clover.cpp b/Clover/Clover.cpp
index f23aac7..d57a122 100644
--- a/Clover/Clover.cpp
+++ b/Clover/Clover.cpp
@@ -22,6 +22,10 @@ static string _server = "http://x.newlifex.com";
static string _serverPath = "/dotnet/";
static OSVersionInfo _os;
+// 前置声明:这两个函数在很多位置被调用,定义见文件后部
+static bool InstallPackage(const string& fileName, const string& args);
+static string ExplainExitCode(DWORD code);
+
/// <summary>获取程序所在目录</summary>
static string GetExeDir()
{
@@ -161,6 +165,83 @@ static bool RunProcess(const string& file, const string& args)
cout << "安装失败!退出码:" << code << endl;
+ auto tip = ExplainExitCode(code);
+ if (!tip.empty()) cout << "原因:" << tip << endl;
+
+ return false;
+}
+
+/// <summary>把安装器退出码翻译成可执行的建议</summary>
+static string ExplainExitCode(DWORD code)
+{
+ switch (code)
+ {
+ case 5: return "拒绝访问,需要以管理员身份运行";
+ case 1603: return "致命错误:XP/2003 常见原因是缺少 WIC(Windows Imaging Component),也可能是权限不足、源文件损坏或已存在更高版本";
+ case 5100: return "系统中已存在更高版本的 .NET";
+ case 1638: return "已安装同版本或更高版本";
+ case 1618: return "另一个安装正在进行,请等待其完成后重试";
+ default: return "";
+ }
+}
+
+/// <summary>读取 PE 文件的目标架构(IMAGE_FILE_MACHINE_*),不是 PE 文件时返回 0</summary>
+static DWORD GetPeMachine(const string& file)
+{
+ auto h = CreateFileA(file.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (h == INVALID_HANDLE_VALUE) return 0;
+
+ DWORD machine = 0;
+ BYTE head[1024];
+ DWORD read = 0;
+ if (ReadFile(h, head, sizeof(head), &read, NULL) && read >= 0x40 && head[0] == 'M' && head[1] == 'Z')
+ {
+ auto pe = *(DWORD*)(head + 0x3C);
+ if (pe > 0 && pe + 6 <= read && head[pe] == 'P' && head[pe + 1] == 'E') machine = *(WORD*)(head + pe + 4);
+ }
+ CloseHandle(h);
+
+ return machine;
+}
+
+/// <summary>校验安装包架构与本机是否匹配</summary>
+static bool CheckArch(const string& file)
+{
+ auto machine = GetPeMachine(file);
+ if (machine == 0) return true;
+
+ auto is64 = machine == 0x8664 || machine == 0xAA64 || machine == 0x0200;
+ auto name = machine == 0x014C ? "x86" : machine == 0x8664 ? "x64" : machine == 0xAA64 ? "arm64" : "ia64";
+
+ // 架构不符时尽早报错,否则只会得到“不是有效的 Win32 应用程序”这类无效信息
+ if (is64 && !_os.Is64Bit)
+ {
+ cout << "跳过 " << name << " 版安装包,当前系统只有 32 位:" << file << endl;
+
+ return false;
+ }
+
+ return true;
+}
+
+/// <summary>XP/2003 安装 .NET4.0 的前置:Windows Imaging Component</summary>
+static bool EnsureWic()
+{
+ char win[MAX_PATH] = { 0 };
+ if (GetWindowsDirectoryA(win, MAX_PATH) > 0)
+ {
+ auto dll = string(win) + "\\System32\\WindowsCodecs.dll";
+ if (GetFileAttributesA(dll.c_str()) != INVALID_FILE_ATTRIBUTES) return true;
+ }
+
+ auto file = _os.Is64Bit ? "wic_x64.exe" : "wic_x86.exe";
+ cout << "缺少 WIC(Windows Imaging Component),先安装 " << file << endl;
+
+ auto args = _silent ? string("/quiet /norestart") : string();
+ if (InstallPackage(file, args)) return true;
+
+ cout << "WIC 未能自动安装,.NET4.0 会以 1603 失败;请先手动安装 WIC 后重试" << endl;
+
return false;
}
@@ -174,6 +255,8 @@ static bool InstallPackage(const string& fileName, const string& args)
return false;
}
+ if (!CheckArch(file)) return false;
+
return RunProcess(file, args);
}
@@ -197,8 +280,8 @@ static bool IsSupported(const string& version)
return true;
}
- // .NET8/10 仅支持 Server 2012/2012 R2 及以上
- if (major == 8 || major == 10) return _os.IsServer && _os.Major == 6 && _os.Minor >= 2;
+ // .NET8/10/11 仅支持 Server 2012/2012 R2 及以上
+ if (major == 8 || major == 10 || major == 11) return _os.IsServer && _os.Major == 6 && _os.Minor >= 2;
return false;
}
@@ -282,7 +365,21 @@ static bool InstallNet20(CRegistryVisit& reg)
return true;
}
- return InstallPackage("NetFx20SP2_x86.exe", _silent ? "/quiet /norestart" : "");
+ // .NET2.0 SP2 要求 XP SP2 / 2003 SP1 及以上;低于该版本时安装器会长时间无响应,先给出结论
+ if (_os.Major == 5)
+ {
+ auto need = _os.Minor == 1 ? 2 : 1;
+ if (_os.ServicePack < need && !_force)
+ {
+ cout << "当前系统 Service Pack " << _os.ServicePack << " 低于 .NET2.0 要求的 SP" << need << ",请先安装系统补丁包,或加 -force 强制尝试" << endl;
+ return false;
+ }
+ }
+
+ // 64 位系统装 x64 包:x86 包会挂起在会话 0(实测 2003 R2 x64)
+ auto file = _os.Is64Bit ? "NetFx20SP2_x64.exe" : "NetFx20SP2_x86.exe";
+
+ return InstallPackage(file, _silent ? "/quiet /norestart" : "");
}
/// <summary>安装 .NET Framework 4.0</summary>
@@ -300,6 +397,16 @@ static bool InstallNet40(CRegistryVisit& reg)
return true;
}
+ if (_os.Major <= 5)
+ {
+ // .NET4.0 官方要求 XP SP3 / 2003 SP2,低版本安装器会以 1603 失败
+ auto need = _os.Minor == 1 ? 3 : 2;
+ if (_os.ServicePack < need && !_force) cout << "注意:.NET4.0 要求 SP" << need << ",当前为 SP" << _os.ServicePack << ",安装可能失败" << endl;
+
+ // XP/2003 上 .NET4.0 依赖 WIC,缺了必然 1603
+ EnsureWic();
+ }
+
return InstallPackage("dotNetFx40_Full_x86_x64.exe", _silent ? "/passive /norestart" : "");
}
diff --git a/Doc/os_net.md b/Doc/os_net.md
index 657168f..0a02d62 100644
--- a/Doc/os_net.md
+++ b/Doc/os_net.md
@@ -194,8 +194,8 @@
| 编号 | 现象 | 范围 | 说明 |
|---|---|---|---|
-| FIX-1 | Linux 下无参数运行 `clover` 会抛 `ArgumentNullException`(`Program.cs:155`) | 全部 Linux 架构(x64/arm64 均复现) | stdin 为空时 `ShowMenu()` 返回 null,随后 `_menus.TryGetValue(null)` 抛异常。带任意参数运行正常,退出码 0 |
-| FIX-2 | `cloverAot` 在非交互环境(stdout/stdin 被重定向、无控制台)退出码 `0xC0000409` | 全部 Windows(宿主 Win11 与各客机同样复现,与系统版本无关) | 产物末尾 `Console.ReadKey()` 等按键,无控制台时抛 `InvalidOperation_ConsoleReadKeyOnFile`。**交互式双击/命令行运行退出码为 0**,因此早期"各系统 AOT 出 banner 后异常"的结论是测试夹具误判,已按可运行修正 |
+| FIX-1 | Linux 下无参数运行 `clover` 会抛 `ArgumentNullException`(`Program.cs:155`) | 全部 Linux 架构(x64/arm64 均复现) | stdin 为空时 `ShowMenu()` 返回 null,随后 `_menus.TryGetValue(null)` 抛异常。带任意参数运行正常,退出码 0。**v1.2 已修复**:`cmd` 为空时打印用法,退出码 0 |
+| FIX-2 | `cloverAot` 在非交互环境(stdout/stdin 被重定向、无控制台)退出码 `0xC0000409` | 全部 Windows(宿主 Win11 与各客机同样复现,与系统版本无关) | 产物末尾 `Console.ReadKey()` 等按键,无控制台时抛 `InvalidOperation_ConsoleReadKeyOnFile`。**交互式双击/命令行运行退出码为 0**,因此早期"各系统 AOT 出 banner 后异常"的结论是测试夹具误判,已按可运行修正。**v1.2 已修复**:输入/输出被重定向时跳过按键等待,退出码 0 |
---
diff --git a/LuckyAOT/Program.cs b/LuckyAOT/Program.cs
index 5b17321..116e7a8 100644
--- a/LuckyAOT/Program.cs
+++ b/LuckyAOT/Program.cs
@@ -37,7 +37,16 @@ internal class Program
Console.WriteLine("发布:{0:yyyy-MM-dd HH:mm:ss}", GetCompileTime(ver));
Console.WriteLine();
- Console.ReadKey();
+ // 非交互环境(输入/输出被重定向、无控制台)不能 ReadKey,否则抛
+ // InvalidOperation_ConsoleReadKeyOnFile 并以 0xC0000409 退出,看起来像兼容性崩溃
+ if (!Console.IsInputRedirected && !Console.IsOutputRedirected)
+ {
+ try
+ {
+ Console.ReadKey();
+ }
+ catch (InvalidOperationException) { }
+ }
}
/// <summary>根据版本号计算得到编译时间</summary>
diff --git a/LuckyClover/NetRuntime.cs b/LuckyClover/NetRuntime.cs
index b4b05bd..6c8fd34 100644
--- a/LuckyClover/NetRuntime.cs
+++ b/LuckyClover/NetRuntime.cs
@@ -45,6 +45,81 @@ public class NetRuntime
/// <summary>文件哈希。用于校验下载文件的完整性</summary>
public IDictionary<String, String> Hashs { get; set; }
+
+ /// <summary>是否 64 位操作系统</summary>
+ /// <remarks>32 位进程跑在 64 位系统上时,IntPtr.Size/Is64BitProcess 都只反映进程,必须看 PROCESSOR_ARCHITEW6432</remarks>
+ public static Boolean Is64BitOS
+ {
+ get
+ {
+#if NET20
+ var arch = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432");
+ if (String.IsNullOrEmpty(arch)) arch = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
+ return "AMD64".Equals(arch, StringComparison.OrdinalIgnoreCase)
+ || "IA64".Equals(arch, StringComparison.OrdinalIgnoreCase)
+ || "ARM64".Equals(arch, StringComparison.OrdinalIgnoreCase);
+#else
+ return Environment.Is64BitOperatingSystem;
+#endif
+ }
+ }
+
+ /// <summary>当前进程位数</summary>
+ public static Int32 ProcessBits => IntPtr.Size * 8;
+
+ /// <summary>操作系统位数</summary>
+ public static Int32 OSBits => Is64BitOS ? 64 : 32;
+
+ /// <summary>获取系统 Service Pack 序号,取不到时为 0</summary>
+ public static Int32 GetServicePack()
+ {
+ var sp = Environment.OSVersion.ServicePack + "";
+ var p = sp.LastIndexOf(' ');
+ if (p >= 0 && Int32.TryParse(sp.Substring(p + 1).Trim(), out var n)) return n;
+
+ return 0;
+ }
+
+ /// <summary>读取 PE 文件的目标架构(IMAGE_FILE_MACHINE_*)。不是 PE 文件时返回 0</summary>
+ /// <param name="file">文件路径</param>
+ /// <returns>目标机器码,0x14C=x86、0x8664=x64、0xAA64=ARM64、0x200=IA64</returns>
+ public static Int32 GetPeMachine(String file)
+ {
+ try
+ {
+ var head = new Byte[1024];
+ using (var fs = File.OpenRead(file))
+ {
+ if (fs.Read(head, 0, head.Length) < 0x40) return 0;
+ }
+ if (head[0] != (Byte)'M' || head[1] != (Byte)'Z') return 0;
+
+ var pe = BitConverter.ToInt32(head, 0x3C);
+ if (pe <= 0 || pe + 6 > head.Length) return 0;
+ if (head[pe] != (Byte)'P' || head[pe + 1] != (Byte)'E') return 0;
+
+ return BitConverter.ToUInt16(head, pe + 4);
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ /// <summary>把 PE 机器码翻译成架构名称</summary>
+ /// <param name="machine">PE 机器码</param>
+ /// <returns>架构名称,未知时返回空</returns>
+ public static String GetMachineName(Int32 machine)
+ {
+ switch (machine)
+ {
+ case 0x014C: return "x86";
+ case 0x8664: return "x64";
+ case 0xAA64: return "arm64";
+ case 0x0200: return "ia64";
+ default: return "";
+ }
+ }
#endregion
#region 方法
@@ -63,7 +138,9 @@ public class NetRuntime
case "net45": InstallNet45(); break;
case "net48": InstallNet48(); break;
case "net6": InstallNet("v6.0", Version6, kind); break;
+ case "net8": InstallNet("v8.0", Version8, kind); break;
case "net10": InstallNet("v10.0", Version10, kind); break;
+ case "net11": InstallNet("v11.0", Version11, kind); break;
}
}
}
@@ -177,6 +254,13 @@ public class NetRuntime
var fullFile = Download(fileName, baseUrl);
if (String.IsNullOrEmpty(fullFile)) return false;
+ // 安装包架构与本机不符时尽早报错,否则只会得到“不是有效的 Win32 应用程序”这类无效信息
+ if (IsWindows && !CheckArch(fullFile))
+ {
+ Environment.ExitCode = 216; // ERROR_EXE_MACHINE_TYPE_MISMATCH
+ return false;
+ }
+
WriteLog("正在安装:{0} {1}", fullFile, arg);
if (IsWindows)
@@ -185,6 +269,60 @@ public class NetRuntime
return InstallOnLinux(fullFile, arg);
}
+ /// <summary>校验安装包架构与本机是否匹配</summary>
+ /// <param name="fullFile">安装包路径</param>
+ /// <returns>可安装返回 true;架构不符返回 false</returns>
+ Boolean CheckArch(String fullFile)
+ {
+ var machine = GetPeMachine(fullFile);
+ if (machine == 0) return true;
+
+ var name = GetMachineName(machine);
+ var is64 = machine == 0x8664 || machine == 0xAA64 || machine == 0x0200;
+
+ if (is64 && !Is64BitOS)
+ {
+ WriteLog("跳过 {0}:安装包是 {1} 版,当前系统只有 32 位", Path.GetFileName(fullFile), name);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <summary>把安装器退出码翻译成可执行的建议</summary>
+ /// <param name="code">退出码</param>
+ /// <returns>建议文案,无建议时返回空</returns>
+ static String ExplainExitCode(Int32 code)
+ {
+ switch (code)
+ {
+ case 5: return "拒绝访问,需要以管理员身份运行";
+ case 1603: return "致命错误:XP/2003 常见原因是缺少 WIC(Windows Imaging Component),也可能是权限不足、源文件损坏或已存在更高版本";
+ case 5100: return "系统中已存在更高版本的 .NET";
+ case 1638: return "已安装同版本或更高版本";
+ case 1618: return "另一个安装正在进行,请等待其完成后重试";
+ case 216: return "安装包架构与系统不符(如把 x64 包装到 32 位系统)";
+ default: return "";
+ }
+ }
+
+ /// <summary>XP/2003 上安装 .NET4.0 的前置:Windows Imaging Component</summary>
+ /// <returns>已安装或安装成功返回 true</returns>
+ Boolean EnsureWic()
+ {
+ var dll = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsCodecs.dll");
+ if (File.Exists(dll)) return true;
+
+ var file = Is64BitOS ? "wic_x64.exe" : "wic_x86.exe";
+ WriteLog("缺少 WIC(Windows Imaging Component),先安装 {0}", file);
+ if (Install(file, null, Silent ? "/quiet /norestart" : null)) return true;
+
+ WriteLog("WIC 未能自动安装({0}),.NET4.0 会以 1603 失败;请先手动安装 WIC 后重试", file);
+
+ return false;
+ }
+
Boolean InstallOnWindows(String fullFile, String? arg)
{
var p = Process.Start(fullFile, arg + "");
@@ -197,7 +335,11 @@ public class NetRuntime
if (rs)
WriteLog(code == 0 ? "安装完成!" : $"安装完成,需要重启系统!ExitCode={code}");
else
+ {
WriteLog("安装失败!ExitCode={0}", code);
+ var tip = ExplainExitCode(code);
+ if (!String.IsNullOrEmpty(tip)) WriteLog("原因:{0}", tip);
+ }
Environment.ExitCode = code;
return rs;
@@ -299,7 +441,24 @@ public class NetRuntime
return true;
}
- return Install("NetFx20SP2_x86.exe");
+ // .NET2.0 SP2 要求 XP SP2 / 2003 SP1 及以上;低于该版本时安装器会长时间无响应,先给出结论
+ var osVer = Environment.OSVersion.Version;
+ if (osVer.Major == 5)
+ {
+ var sp = GetServicePack();
+ var need = osVer.Minor == 1 ? 2 : 1;
+ if (sp < need && !Force)
+ {
+ WriteLog("当前系统 Service Pack {0} 低于 .NET2.0 要求的 SP{1},请先安装系统补丁包,或加 -force 强制尝试", sp, need);
+
+ return false;
+ }
+ }
+
+ // 64 位系统装 x64 包:x86 包会挂起在会话 0(实测 2003 R2 x64)
+ var file = Is64BitOS ? "NetFx20SP2_x64.exe" : "NetFx20SP2_x86.exe";
+
+ return Install(file);
}
/// <summary>安装.NET4.0</summary>
@@ -322,6 +481,19 @@ public class NetRuntime
return true;
}
+ var osVer = Environment.OSVersion.Version;
+
+ // .NET4.0 官方要求 XP SP3 / 2003 SP2,低版本安装器会以 1603 失败
+ if (osVer.Major == 5 && !Force)
+ {
+ var sp = GetServicePack();
+ var need = osVer.Minor == 1 ? 3 : 2;
+ if (sp < need) WriteLog("注意:.NET4.0 要求 SP{0},当前为 SP{1},安装可能失败", need, sp);
+ }
+
+ // XP/2003 上 .NET4.0 依赖 WIC,缺了必然 1603
+ if (osVer.Major <= 5) EnsureWic();
+
var rs = Install("dotNetFx40_Full_x86_x64.exe", null);
if (!rs)
{
@@ -450,7 +622,7 @@ public class NetRuntime
}
#if NET20
- var is64 = IntPtr.Size == 8;
+ var is64 = Is64BitOS;
#else
var is64 = Environment.Is64BitOperatingSystem;
#endif
diff --git a/LuckyClover/Program.cs b/LuckyClover/Program.cs
index 1b696b8..81ea6da 100644
--- a/LuckyClover/Program.cs
+++ b/LuckyClover/Program.cs
@@ -49,6 +49,7 @@ internal class Program
Console.WriteLine(asm.GetCustomAttribute<AssemblyDescriptionAttribute>()?.Description);
#endif
Console.WriteLine("{0}", Environment.OSVersion);
+ Console.WriteLine("架构:系统 {0} 位,进程 {1} 位", NetRuntime.OSBits, NetRuntime.ProcessBits);
Console.WriteLine("运行时:\e[34;1m{0}\e[0m", Environment.Version);
Console.WriteLine("发布:{0:yyyy-MM-dd HH:mm:ss}", GetCompileTime(ver));
Console.WriteLine();
@@ -150,7 +151,15 @@ internal class Program
_menus["untar"] = () => new TarCommand().Extract(args);
#endif
- if (String.IsNullOrEmpty(cmd)) cmd = ShowMenu();
+ if (String.IsNullOrEmpty(cmd)) cmd = ShowMenu() ?? "";
+
+ // 无参数且没有交互输入时 cmd 为空,直接给帮助,避免用 null 去查菜单字典
+ if (String.IsNullOrEmpty(cmd))
+ {
+ PrintUsage();
+
+ return;
+ }
if (_menus.TryGetValue(cmd, out var func))
func();
@@ -158,6 +167,18 @@ internal class Program
Console.WriteLine("无法识别命令:{0}", cmd);
}
+ private static void PrintUsage()
+ {
+ Console.WriteLine("用法:clover <命令> [选项]");
+ Console.WriteLine(" net20|net40|net45|net48|net6|net8|net10|net11 安装指定运行时");
+ Console.WriteLine(" net6-desktop|net8-desktop|net10-desktop 安装桌面运行时");
+ Console.WriteLine(" net6-aspnet|net8-aspnet|net10-aspnet 安装 ASP.NET Core 运行时");
+ Console.WriteLine(" auto|net 按操作系统自动选择");
+ Console.WriteLine(" md5 [pattern] 列出文件校验值");
+ Console.WriteLine(" zip|unzip|tar|untar 压缩解压");
+ Console.WriteLine("选项:-silent 静默安装;-force 强制安装;-dryrun 只打印计划;<url> 指定下载服务器");
+ }
+
private static String ShowMenu()
{
var vers = new List<VerInfo>();
diff --git a/Shared/NetVersions.cs b/Shared/NetVersions.cs
index ced5c06..082c7aa 100644
--- a/Shared/NetVersions.cs
+++ b/Shared/NetVersions.cs
@@ -111,8 +111,8 @@ public static class NetVersions
return true;
}
- // .NET8/10 仅支持 Server 2012/2012 R2 及以上
- if (major == 8 || major == 10) return isServer && osVer.Major == 6 && osVer.Minor >= 2;
+ // .NET8/10/11 仅支持 Server 2012/2012 R2 及以上
+ if (major is 8 or 10 or 11) return isServer && osVer.Major == 6 && osVer.Minor >= 2;
return false;
}