将调度内核标识从 SimpleLite 全面重命名为 Simple3。

配置段/环境变量、Launcher、健康检查 API、OpenAPI 与前后端文案同步;兼容探测旧 SimpleLite 进程名。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
黄兆尉
2026-08-26 17:46:52 +08:00
co-authored by Cursor
parent 912c44c9bb
commit 3686abdc78
79 changed files with 922 additions and 832 deletions
@@ -4,45 +4,46 @@ using Microsoft.Extensions.Logging;
namespace MiGu.Server.Launcher;
/// <summary>
/// 将 <c>obj/Debug</c> 下最新编译的 SimpleLite 同步到 <c>bin/Debug</c>(仅当 SimpleLite 未运行时)。
/// 将 <c>obj/Debug</c> 下最新编译的 Simple3 同步到 <c>bin/Debug</c>(仅当 Simple3 未运行时)。
/// </summary>
public static class SimpleLiteBuildSync
public static class Simple3BuildSync
{
public static bool TrySyncFromObjToBin(string contentRoot, ILogger? log = null)
{
if (!TryResolvePaths(contentRoot, out var objDll, out var objExe, out var binDir))
return false;
var binDll = Path.Combine(binDir, "SimpleLite.dll");
var binExe = Path.Combine(binDir, "SimpleLite.exe");
var binDll = Path.Combine(binDir, "Simple3.dll");
var binExe = Path.Combine(binDir, "Simple3.exe");
if (!File.Exists(objDll))
{
log?.LogDebug("[SimpleLiteBuildSync] obj DLL 不存在: {Path}", objDll);
log?.LogDebug("[Simple3BuildSync] obj DLL 不存在: {Path}", objDll);
return false;
}
if (Process.GetProcessesByName("SimpleLite").Any(p => !p.HasExited))
if (Process.GetProcessesByName("Simple3").Any(p => !p.HasExited)
|| Process.GetProcessesByName("SimpleLite").Any(p => !p.HasExited))
{
log?.LogWarning("[SimpleLiteBuildSync] SimpleLite 仍在运行,跳过 DLL 同步。请先关闭 SimpleLite 窗口。");
log?.LogWarning("[Simple3BuildSync] Simple3 仍在运行,跳过 DLL 同步。请先关闭内核窗口。");
return false;
}
var objTime = File.GetLastWriteTimeUtc(objDll);
if (File.Exists(binDll) && File.GetLastWriteTimeUtc(binDll) >= objTime)
{
log?.LogDebug("[SimpleLiteBuildSync] bin 已是最新,无需同步");
log?.LogDebug("[Simple3BuildSync] bin 已是最新,无需同步");
return false;
}
Directory.CreateDirectory(binDir);
File.Copy(objDll, binDll, true);
log?.LogInformation("[SimpleLiteBuildSync] 已同步 {Src} → {Dst}", objDll, binDll);
log?.LogInformation("[Simple3BuildSync] 已同步 {Src} → {Dst}", objDll, binDll);
if (File.Exists(objExe))
{
File.Copy(objExe, binExe, true);
log?.LogInformation("[SimpleLiteBuildSync] 已同步 {Src} → {Dst}", objExe, binExe);
log?.LogInformation("[Simple3BuildSync] 已同步 {Src} → {Dst}", objExe, binExe);
}
SyncRuntimeDeps(objDll, binDir, log);
@@ -51,7 +52,7 @@ public static class SimpleLiteBuildSync
}
/// <summary>
/// 探测 SimpleLite 是否带「前往站点」路由(区分新旧 DLL)。
/// 探测 Simple3 是否带「前往站点」路由(区分新旧 DLL)。
/// 必须用不存在的对象 id(-1):路由存在时后端进 handler 找不到对象,返回 JSON
/// <c>success:false</c>(无任何副作用);路由不存在时 EmbedIO 返回 HTML 404。
/// 早期版本误用 car/0 + siteId=0 —— 一旦场景里真有 id=0 的车和站点,每次健康
@@ -96,7 +97,7 @@ public static class SimpleLiteBuildSync
var dst = Path.Combine(binDir, name);
File.Copy(src, dst, true);
log?.LogInformation("[SimpleLiteBuildSync] 已同步依赖 {Name}", name);
log?.LogInformation("[Simple3BuildSync] 已同步依赖 {Name}", name);
}
}
@@ -106,11 +107,11 @@ public static class SimpleLiteBuildSync
binDir = "";
var repo = FindRepoRoot(contentRoot);
if (repo == null) return false;
var sl = Path.Combine(repo, "Simple", "SimpleLite");
objDll = Path.Combine(sl, "obj", "Debug", "SimpleLite.dll");
objExe = Path.Combine(sl, "obj", "Debug", "SimpleLite.exe");
var sl = Path.Combine(repo, "Simple", "Simple3");
objDll = Path.Combine(sl, "obj", "Debug", "Simple3.dll");
objExe = Path.Combine(sl, "obj", "Debug", "Simple3.exe");
binDir = Path.Combine(sl, "bin", "Debug");
return true;
return Directory.Exists(sl);
}
private static string? FindRepoRoot(string contentRoot)
@@ -118,7 +119,8 @@ public static class SimpleLiteBuildSync
var dir = new DirectoryInfo(contentRoot);
while (dir != null)
{
if (Directory.Exists(Path.Combine(dir.FullName, "Simple", "SimpleLite")))
if (Directory.Exists(Path.Combine(dir.FullName, "Simple", "Simple3"))
|| Directory.Exists(Path.Combine(dir.FullName, "Simple", "SimpleLite")))
return dir.FullName;
dir = dir.Parent;
}
@@ -7,21 +7,21 @@ using Microsoft.Extensions.Options;
namespace MiGu.Server.Launcher;
/// <summary>
/// 平台登录成功后按 LaunchMode 把 SimpleLite.exe 作为子进程拉起。
/// 平台登录成功后按 LaunchMode 把 Simple3.exe 作为子进程拉起。
///
/// 设计要点(与原 <c>SimpleLite/Platform/PlatformLauncher.cs</c> 镜像):
/// 设计要点(与原 <c>Simple3/Platform/PlatformLauncher.cs</c> 镜像):
/// - 幂等:第一次登录拉起,后续登录复用同一个子进程(不会重复启)。
/// - 子进程退出后下一次登录可以重新拉起(不阻塞用户重试)。
/// - 默认独立(<see cref="SimpleLiteOptions.FollowParent"/> = false):SimpleLite 与 MiGu.Server 互不影响,
/// - 默认独立(<see cref="Simple3Options.FollowParent"/> = false):Simple3 与 MiGu.Server 互不影响,
/// 关闭任一方不会 kill 另一方;Windows 上用 <c>cmd /c start</c> 脱离父进程组/控制台。
/// - 可选跟随(FollowParent = true):Windows JobObject 绑定,MiGu.Server 退出时一并结束 SimpleLite
/// - 命令行透传:把 LaunchMode 翻成 SimpleLite 的 <c>--display-mode=web</c> / <c>--display-mode=web+local</c>。
/// - 启动后阻塞等待 SimpleLite Projection :8222 端口可达(最长 ReadinessTimeoutMs),让前端 /api/sl/* 不再立刻 502。
/// - 可选跟随(FollowParent = true):Windows JobObject 绑定,MiGu.Server 退出时一并结束 Simple3
/// - 命令行透传:把 LaunchMode 翻成 Simple3 的 <c>--display-mode=web</c> / <c>--display-mode=web+local</c>。
/// - 启动后阻塞等待 Simple3 Projection :8222 端口可达(最长 ReadinessTimeoutMs),让前端 /api/sl/* 不再立刻 502。
/// </summary>
public sealed class SimpleLiteLauncher : IDisposable
public sealed class Simple3Launcher : IDisposable
{
private readonly SimpleLiteOptions _opts;
private readonly ILogger<SimpleLiteLauncher> _log;
private readonly Simple3Options _opts;
private readonly ILogger<Simple3Launcher> _log;
private readonly IHostEnvironment _env;
private readonly object _sync = new();
@@ -30,13 +30,13 @@ public sealed class SimpleLiteLauncher : IDisposable
private string? _lastLaunchMode;
private bool _disposed;
public SimpleLiteLauncher(IOptions<SimpleLiteOptions> opts, ILogger<SimpleLiteLauncher> log, IHostEnvironment env)
public Simple3Launcher(IOptions<Simple3Options> opts, ILogger<Simple3Launcher> log, IHostEnvironment env)
{
_opts = opts.Value;
_log = log;
_env = env;
// 会话 N+2:默认不再在 ProcessExit 时清理子进程 —— SimpleLite 是「独立程序」,MiGu.Server 关掉
// 不应该带走 SimpleLite。仅当用户显式 opt-in FollowParent=true 时才挂软关闭兜底。
// 会话 N+2:默认不再在 ProcessExit 时清理子进程 —— Simple3 是「独立程序」,MiGu.Server 关掉
// 不应该带走 Simple3。仅当用户显式 opt-in FollowParent=true 时才挂软关闭兜底。
if (_opts.FollowParent)
{
AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose();
@@ -53,11 +53,11 @@ public sealed class SimpleLiteLauncher : IDisposable
public string? LastLaunchMode { get { lock (_sync) return _lastLaunchMode; } }
/// <summary>外部已存在的 SimpleLiteMiGu.Server 重启复用上一轮实例)占位 LaunchMode 值,不参与命令行 displayMode 翻译。</summary>
/// <summary>外部已存在的 Simple3MiGu.Server 重启复用上一轮实例)占位 LaunchMode 值,不参与命令行 displayMode 翻译。</summary>
internal const string ExternalReuseLaunchMode = "external";
/// <summary>
/// 按 launchMode 拉起 SimpleLite(已运行则跳过)。
/// 按 launchMode 拉起 Simple3(已运行则跳过)。
/// </summary>
/// <param name="launchMode">"WebOnly" 或 "DesktopAndWeb"(大小写不敏感)。</param>
/// <returns>本次调用产生的状态摘要,可写入登录响应或日志。</returns>
@@ -67,20 +67,20 @@ public sealed class SimpleLiteLauncher : IDisposable
if (!_opts.Enabled)
{
_log.LogInformation("[SimpleLite] auto-start disabled (appsettings: SimpleLite.Enabled=false); launchMode={Mode} ignored", launchMode);
return new LaunchResult(false, "Disabled", "appsettings:SimpleLite:Enabled=false", DisplayMode: null,
Warning: "SimpleLite 自动拉起已被 appsettings:SimpleLite:Enabled=false 关闭;登录已成功但 SimpleLite 未启动,/api/sl/* 反代请求会返回 502。");
_log.LogInformation("[Simple3] auto-start disabled (appsettings: Simple3.Enabled=false); launchMode={Mode} ignored", launchMode);
return new LaunchResult(false, "Disabled", "appsettings:Simple3:Enabled=false", DisplayMode: null,
Warning: "Simple3 自动拉起已被 appsettings:Simple3:Enabled=false 关闭;登录已成功但 Simple3 未启动,/api/sl/* 反代请求会返回 502。");
}
lock (_sync)
{
if (_proc is { HasExited: false })
{
_log.LogInformation("[SimpleLite] already running pid={Pid}, displayMode={DisplayMode}; skip duplicate launch", _proc.Id, _lastLaunchMode);
_log.LogInformation("[Simple3] already running pid={Pid}, displayMode={DisplayMode}; skip duplicate launch", _proc.Id, _lastLaunchMode);
var warning = !string.IsNullOrEmpty(_lastLaunchMode) &&
!string.Equals(_lastLaunchMode, displayMode, StringComparison.OrdinalIgnoreCase)
? $"SimpleLite 已经在运行(pid={_proc.Id}, displayMode={_lastLaunchMode})。" +
$"本次选择的启动模式『{launchMode}』未被应用;如需切换,请手动关闭旧 SimpleLite 后重新登录。"
? $"Simple3 已经在运行(pid={_proc.Id}, displayMode={_lastLaunchMode})。" +
$"本次选择的启动模式『{launchMode}』未被应用;如需切换,请手动关闭旧 Simple3 后重新登录。"
: null;
return new LaunchResult(true, "AlreadyRunning",
$"pid={_proc.Id}, displayMode={_lastLaunchMode}",
@@ -88,44 +88,44 @@ public sealed class SimpleLiteLauncher : IDisposable
Warning: warning);
}
// 会话 N+2MiGu.Server 重启后 _proc 引用丢失,但上一轮拉起的 SimpleLite 可能仍在跑(因为
// 会话 N+2MiGu.Server 重启后 _proc 引用丢失,但上一轮拉起的 Simple3 可能仍在跑(因为
// 默认 FollowParent=false 不带走它)。这里在拉起前先探测 Projection 端口:能连通就视为复用,
// 避免「allowMultiple=false 时新 SimpleLite 检测到多开自杀」+「端口冲突」两类常见崩溃。
// 避免「allowMultiple=false 时新 Simple3 检测到多开自杀」+「端口冲突」两类常见崩溃。
//
// 注意:探测仅判断「有 SimpleLite 在 8222 占着」,无法知道它当时选的 LaunchMode。如果用户本次
// 想换模式,这条路径下不会生效;日志里会明确告知,由用户决定是否手动关掉旧 SimpleLite 再登录。
// 注意:探测仅判断「有 Simple3 在 8222 占着」,无法知道它当时选的 LaunchMode。如果用户本次
// 想换模式,这条路径下不会生效;日志里会明确告知,由用户决定是否手动关掉旧 Simple3 再登录。
//
// 增强(A3):先做 TCP 探活(快),再做 HTTP JSON 探针验证「真的是 SimpleLite」,避免被某个偶然占
// 增强(A3):先做 TCP 探活(快),再做 HTTP JSON 探针验证「真的是 Simple3」,避免被某个偶然占
// 用 8222 的无关进程误判成复用。Probe 失败时不再当作复用成功,否则前端会拿到假的 WebEnabled。
if (TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(500)))
{
var probeOk = ProbeSimpleLiteHttp("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(800));
var probeOk = ProbeSimple3Http("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(800));
if (!probeOk)
{
_log.LogWarning(
"[SimpleLite] projection port :{Port} is occupied, but SimpleLite HTTP probe failed. Skip launch to avoid port conflict.",
"[Simple3] projection port :{Port} is occupied, but Simple3 HTTP probe failed. Skip launch to avoid port conflict.",
_opts.ProjectionPort);
return new LaunchResult(false, "PortOccupied",
$"projection :{_opts.ProjectionPort} tcp reachable but /projection/cars probe failed",
DisplayMode: null,
Warning: $"端口 :{_opts.ProjectionPort} 已被占用,但未识别为 SimpleLite Projection 服务。" +
"请关闭占用该端口的进程,或调整 SimpleLite:ProjectionPort / SimpleLite 配置后重试。");
Warning: $"端口 :{_opts.ProjectionPort} 已被占用,但未识别为 Simple3 Projection 服务。" +
"请关闭占用该端口的进程,或调整 Simple3:ProjectionPort / Simple3 配置后重试。");
}
_log.LogInformation(
"[SimpleLite] projection :{Port} already reachable (httpProbe=ok); assume an existing SimpleLite is running. " +
"Skip launch. If user picked a different LaunchMode this session, please close the existing SimpleLite window and login again.",
"[Simple3] projection :{Port} already reachable (httpProbe=ok); assume an existing Simple3 is running. " +
"Skip launch. If user picked a different LaunchMode this session, please close the existing Simple3 window and login again.",
_opts.ProjectionPort);
// A2 修复:保留一个占位 LaunchMode 让 LastLaunchMode 不再为 null —— 这样 SwitchScope
// 推断 runMode 时不会落到 "web+local" 兜底,前端 RunMode 角标也不会与实际不符。
_lastLaunchMode = ExternalReuseLaunchMode;
var reuseWarning = $"检测到 SimpleLite 已经在 :{_opts.ProjectionPort} 上运行(可能是 MiGu.Server 重启前残留)。" +
$"本次选择的启动模式『{launchMode}』未被应用到既有实例。如需切换,请手动关闭旧 SimpleLite 窗口后重新登录。";
if (SimpleLiteBuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort) == false)
var reuseWarning = $"检测到 Simple3 已经在 :{_opts.ProjectionPort} 上运行(可能是 MiGu.Server 重启前残留)。" +
$"本次选择的启动模式『{launchMode}』未被应用到既有实例。如需切换,请手动关闭旧 Simple3 窗口后重新登录。";
if (Simple3BuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort) == false)
{
reuseWarning += " 当前 SimpleLite 版本过旧,缺少「前往站点」API;请关闭 SimpleLite 窗口后调用 POST /api/health/simplelite/restart-for-update,或运行 scripts/redeploy-simplelite.ps1。";
reuseWarning += " 当前 Simple3 版本过旧,缺少「前往站点」API;请关闭 Simple3 窗口后调用 POST /api/health/simple3/restart-for-update,或运行 scripts/redeploy-simple3.ps1。";
}
return new LaunchResult(true, "ReusingExisting",
@@ -134,18 +134,18 @@ public sealed class SimpleLiteLauncher : IDisposable
Warning: reuseWarning);
}
SimpleLiteBuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
Simple3BuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
var resolved = ResolveExecutable(_opts.ExecutablePath);
if (resolved == null)
{
_log.LogWarning(
"[SimpleLite] auto-start skipped: SimpleLite.exe not found. Configure appsettings:SimpleLite:ExecutablePath or place SimpleLite next to MiGu.Server. ContentRoot={Root}",
"[Simple3] auto-start skipped: Simple3.exe not found. Configure appsettings:Simple3:ExecutablePath or place Simple3 next to MiGu.Server. ContentRoot={Root}",
_env.ContentRootPath);
return new LaunchResult(false, "ExecutableNotFound",
"Set appsettings:SimpleLite:ExecutablePath or place SimpleLite next to MiGu.Server.",
"Set appsettings:Simple3:ExecutablePath or place Simple3 next to MiGu.Server.",
DisplayMode: null,
Warning: "找不到 SimpleLite.exe。请在 appsettings:SimpleLite:ExecutablePath 显式配置,或者把 SimpleLite.exe 放到 MiGu.Server 同目录。");
Warning: "找不到 Simple3.exe。请在 appsettings:Simple3:ExecutablePath 显式配置,或者把 Simple3.exe 放到 MiGu.Server 同目录。");
}
var workdir = string.IsNullOrWhiteSpace(_opts.WorkingDirectory)
@@ -156,12 +156,12 @@ public sealed class SimpleLiteLauncher : IDisposable
try
{
var proc = StartSimpleLiteProcess(resolved, arguments, workdir);
var proc = StartSimple3Process(resolved, arguments, workdir);
if (proc == null)
{
_log.LogError("[SimpleLite] Process.Start returned null; exe={Exe} args={Args}", resolved, arguments);
_log.LogError("[Simple3] Process.Start returned null; exe={Exe} args={Args}", resolved, arguments);
return new LaunchResult(false, "ProcessStartFailed", $"exe={resolved}", DisplayMode: null,
Warning: $"启动 SimpleLite 进程失败;exe={resolved}");
Warning: $"启动 Simple3 进程失败;exe={resolved}");
}
WireProcessExitHandler(proc);
@@ -171,20 +171,20 @@ public sealed class SimpleLiteLauncher : IDisposable
if (_opts.FollowParent) AttachToJobObject(proc);
_log.LogInformation("[SimpleLite] launched pid={Pid} displayMode={Mode} exe={Exe} args=\"{Args}\" workdir={Workdir}; standalone={Standalone}",
_log.LogInformation("[Simple3] launched pid={Pid} displayMode={Mode} exe={Exe} args=\"{Args}\" workdir={Workdir}; standalone={Standalone}",
proc.Id, displayMode, resolved, arguments, workdir, !_opts.FollowParent);
}
catch (Exception ex)
{
_log.LogError(ex, "[SimpleLite] auto-start failed");
_log.LogError(ex, "[Simple3] auto-start failed");
return new LaunchResult(false, "Exception", ex.Message, DisplayMode: null,
Warning: $"启动 SimpleLite 时抛异常:{ex.GetType().Name}: {ex.Message}");
Warning: $"启动 Simple3 时抛异常:{ex.GetType().Name}: {ex.Message}");
}
}
// M1:登录路径传 waitForReady=false —— 进程拉起后立即返回,不再同步阻塞最长
// ReadinessTimeoutMs 等端口就绪(避免冷启动登录干等十几秒)。前端可轮询
// GET /api/health/simplelite 获知就绪状态。
// GET /api/health/simple3 获知就绪状态。
if (!waitForReady)
{
return new LaunchResult(true, "Starting",
@@ -200,12 +200,12 @@ public sealed class SimpleLiteLauncher : IDisposable
DisplayMode: displayMode,
Warning: ready
? null
: $"SimpleLite 进程已起,但 Projection :{_opts.ProjectionPort} 在 {_opts.ReadinessTimeoutMs}ms 内未响应。前端 /api/sl/* 可能短暂 502;可稍后刷新。");
: $"Simple3 进程已起,但 Projection :{_opts.ProjectionPort} 在 {_opts.ReadinessTimeoutMs}ms 内未响应。前端 /api/sl/* 可能短暂 502;可稍后刷新。");
}
/// <summary>
/// 手动关闭 SimpleLite 子进程。
/// 会话 N+2 起默认不调(FollowParent=false)—— SimpleLite 是独立程序,MiGu.Server 关闭不带走它。
/// 手动关闭 Simple3 子进程。
/// 会话 N+2 起默认不调(FollowParent=false)—— Simple3 是独立程序,MiGu.Server 关闭不带走它。
/// 仅当用户显式 opt-in FollowParent=true 时由 ProcessExit / ApplicationStopping 钩子调用。
/// </summary>
public void Dispose()
@@ -227,7 +227,7 @@ public sealed class SimpleLiteLauncher : IDisposable
// 独立模式:不杀子进程,仅释放 MiGu.Server 侧 Process 句柄。
if (proc is { HasExited: false })
{
_log.LogInformation("[SimpleLite] standalone mode: MiGu.Server stopping — SimpleLite pid={Pid} keeps running (FollowParent=false)", proc.Id);
_log.LogInformation("[Simple3] standalone mode: MiGu.Server stopping — Simple3 pid={Pid} keeps running (FollowParent=false)", proc.Id);
}
try { proc?.Dispose(); } catch { /* ignore */ }
return;
@@ -236,7 +236,7 @@ public sealed class SimpleLiteLauncher : IDisposable
if (proc is { HasExited: false })
{
try { proc.Kill(entireProcessTree: true); }
catch (Exception ex) { _log.LogWarning("[SimpleLite] kill failed: {Msg}", ex.Message); }
catch (Exception ex) { _log.LogWarning("[Simple3] kill failed: {Msg}", ex.Message); }
try { proc.WaitForExit(2000); } catch { /* ignore */ }
}
@@ -248,21 +248,21 @@ public sealed class SimpleLiteLauncher : IDisposable
private static string NormalizeDisplayMode(string launchMode)
{
// LaunchMode 是「业务语言」(WebOnly / DesktopAndWeb)displayMode 是 SimpleLite「内部语言」(web / web+local)。
// LaunchMode 是「业务语言」(WebOnly / DesktopAndWeb)displayMode 是 Simple3「内部语言」(web / web+local)。
// 这里做一次显式翻译,前端 / 后端日志均用业务语言,命令行用内部语言。
return launchMode?.Trim().ToLowerInvariant() switch
{
"webonly" or "web" or "web-only" => "web",
// 任何未知值都按"完整本地+web"兜底,保证最小惊讶(既能桌面用,也能浏览器用)。
_ => "web+local",
"desktopandweb" or "web+local" or "weblocal" or "local" => "web+local",
// Simple3 无本地桌面端:默认 / 未知一律 web
_ => "web",
};
}
private string BuildArguments(string displayMode, string extra)
{
var args = $"--display-mode={displayMode}";
// 迷榖嵌入:平台 iframe 场景默认带 --migu,让 SimpleLite WebTerminal 默认纯画布(隐藏 ImGui panel),
// 业务 UI 由 Vue 平台前端接管;declare 时序失败时也安全回退到画布。可用 appsettings:SimpleLite:EmbeddedCanvas=false 关闭。
// 迷榖嵌入:平台 iframe 场景默认带 --migu,让 Simple3 WebTerminal 默认纯画布(隐藏 ImGui panel),
// 业务 UI 由 Vue 平台前端接管;declare 时序失败时也安全回退到画布。可用 appsettings:Simple3:EmbeddedCanvas=false 关闭。
if (_opts.EmbeddedCanvas) args += " --migu";
// 选择性加载:把平台写入的 plugins/active-scenes.json 同步透传为 --scenes(命令行优先级最高,与文件一致,双保险)。
var sceneArg = ReadActiveScenesArg();
@@ -271,7 +271,7 @@ public sealed class SimpleLiteLauncher : IDisposable
return args;
}
/// <summary>解析 SimpleLite 工作目录(与拉起时一致):优先显式 <see cref="SimpleLiteOptions.WorkingDirectory"/>
/// <summary>解析 Simple3 工作目录(与拉起时一致):优先显式 <see cref="Simple3Options.WorkingDirectory"/>
/// 否则取解析到的 exe 所在目录。两者都拿不到返回 null。</summary>
public string? ResolveWorkingDirectory()
{
@@ -281,7 +281,7 @@ public sealed class SimpleLiteLauncher : IDisposable
return resolved == null ? null : (Path.GetDirectoryName(resolved) ?? Environment.CurrentDirectory);
}
/// <summary>SimpleLite 的 plugins 目录(工作目录/plugins);定位不到工作目录时返回 null。</summary>
/// <summary>Simple3 的 plugins 目录(工作目录/plugins);定位不到工作目录时返回 null。</summary>
public string? ResolvePluginsDir()
{
var wd = ResolveWorkingDirectory();
@@ -289,16 +289,16 @@ public sealed class SimpleLiteLauncher : IDisposable
}
/// <summary>
/// 把「配置向导选定的导航场景」写入 SimpleLite 的 <c>plugins/active-scenes.json</c> ——
/// 这是「平台 → 内核」选择性加载的主通道。下次 SimpleLite 启动即据此只加载选定导航场景插件;
/// 已在运行的实例需重启或调 <c>POST /projection/scenes/apply</c> 才生效。字段名与 SimpleLite
/// 把「配置向导选定的导航场景」写入 Simple3 的 <c>plugins/active-scenes.json</c> ——
/// 这是「平台 → 内核」选择性加载的主通道。下次 Simple3 启动即据此只加载选定导航场景插件;
/// 已在运行的实例需重启或调 <c>POST /projection/scenes/apply</c> 才生效。字段名与 Simple3
/// <c>ActiveScenesConfig</c> 对齐(activeScenes / alwaysLoad / source / updatedAt)。
/// </summary>
public ActiveScenesWriteResult WriteActiveScenes(IEnumerable<string> activeScenes, IEnumerable<string>? alwaysLoad, string source)
{
var pluginsDir = ResolvePluginsDir();
if (pluginsDir == null)
return new ActiveScenesWriteResult(false, null, "未找到 SimpleLite 工作目录/可执行文件,无法定位 plugins 目录");
return new ActiveScenesWriteResult(false, null, "未找到 Simple3 工作目录/可执行文件,无法定位 plugins 目录");
try
{
Directory.CreateDirectory(pluginsDir);
@@ -313,13 +313,13 @@ public sealed class SimpleLiteLauncher : IDisposable
updatedAt = DateTime.UtcNow
};
File.WriteAllText(path, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
_log.LogInformation("[SimpleLite] active-scenes.json 写入 {Path}: active=[{Scenes}]",
_log.LogInformation("[Simple3] active-scenes.json 写入 {Path}: active=[{Scenes}]",
path, string.Join(",", payload.activeScenes));
return new ActiveScenesWriteResult(true, path, null);
}
catch (Exception ex)
{
_log.LogWarning(ex, "[SimpleLite] 写 active-scenes.json 失败");
_log.LogWarning(ex, "[Simple3] 写 active-scenes.json 失败");
return new ActiveScenesWriteResult(false, null, ex.Message);
}
}
@@ -349,11 +349,12 @@ public sealed class SimpleLiteLauncher : IDisposable
/// <summary>写 active-scenes.json 的结果(供向导保存接口回显)。</summary>
public readonly record struct ActiveScenesWriteResult(bool Ok, string? Path, string? Error);
/// <summary>终止本机全部 SimpleLite 进程并清理 MiGu.Server 侧托管引用。</summary>
/// <summary>终止本机全部 Simple3(及旧名 SimpleLite进程并清理 MiGu.Server 侧托管引用。</summary>
public int StopAll()
{
var killed = 0;
foreach (var proc in Process.GetProcessesByName("SimpleLite"))
foreach (var name in new[] { "Simple3", "SimpleLite" })
foreach (var proc in Process.GetProcessesByName(name))
{
try
{
@@ -361,12 +362,12 @@ public sealed class SimpleLiteLauncher : IDisposable
{
proc.Kill(entireProcessTree: true);
killed++;
_log.LogInformation("[SimpleLite] stop: killed pid={Pid}", proc.Id);
_log.LogInformation("[Simple3] stop: killed {Name} pid={Pid}", name, proc.Id);
}
}
catch (Exception ex)
{
_log.LogWarning("[SimpleLite] stop: kill pid={Pid} failed: {Msg}", proc.Id, ex.Message);
_log.LogWarning("[Simple3] stop: kill {Name} pid={Pid} failed: {Msg}", name, proc.Id, ex.Message);
}
finally
{
@@ -385,7 +386,7 @@ public sealed class SimpleLiteLauncher : IDisposable
return killed;
}
/// <summary>关闭全部 SimpleLite 后按 launchMode 重新拉起(不同步 DLL)。</summary>
/// <summary>关闭全部 Simple3 后按 launchMode 重新拉起(不同步 DLL)。</summary>
public LaunchResult Restart(string launchMode = "webonly")
{
StopAll();
@@ -393,19 +394,19 @@ public sealed class SimpleLiteLauncher : IDisposable
}
/// <summary>
/// 关闭所有 SimpleLite 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
/// 关闭所有 Simple3 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
/// </summary>
public LaunchResult RestartForUpdate(string launchMode = "webonly")
{
StopAll();
var synced = SimpleLiteBuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
var synced = Simple3BuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
var result = MaybeStart(launchMode);
if (!synced && result.Warning == null)
{
return result with
{
Warning = "未能从 obj/Debug 同步 DLL(可能未编译或 SimpleLite 仍占用文件)。请先运行 scripts/redeploy-simplelite.ps1。"
Warning = "未能从 obj/Debug 同步 DLL(可能未编译或 Simple3 仍占用文件)。请先运行 scripts/redeploy-simple3.ps1。"
};
}
@@ -416,12 +417,12 @@ public sealed class SimpleLiteLauncher : IDisposable
private DateTimeOffset _gotoSiteProbeAt = DateTimeOffset.MinValue;
private static readonly TimeSpan GotoSiteProbeTtl = TimeSpan.FromSeconds(60);
/// <summary>启动前诊断:当前配置、解析到的 exe、端口占用等(供 /api/health/simplelite 与启动日志)。</summary>
public SimpleLiteDiagnostics GetDiagnostics()
/// <summary>启动前诊断:当前配置、解析到的 exe、端口占用等(供 /api/health/simple3 与启动日志)。</summary>
public Simple3Diagnostics GetDiagnostics()
{
var resolved = ResolveExecutable(_opts.ExecutablePath);
var projectionUp = TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(400));
// 探测结果缓存 60s:诊断端点可能被前端轮询,避免每次都对 SimpleLite 发探测请求。
// 探测结果缓存 60s:诊断端点可能被前端轮询,避免每次都对 Simple3 发探测请求。
bool? gotoSite = null;
if (projectionUp)
{
@@ -431,7 +432,7 @@ public sealed class SimpleLiteLauncher : IDisposable
}
else
{
gotoSite = SimpleLiteBuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort);
gotoSite = Simple3BuildSync.ProbeGotoSiteRoute(_opts.ProjectionPort);
if (gotoSite is not null)
{
_gotoSiteProbeCache = gotoSite;
@@ -440,9 +441,9 @@ public sealed class SimpleLiteLauncher : IDisposable
}
}
var deployHint = gotoSite == false
? "关闭 SimpleLite 后:运行 Migu2.0/scripts/redeploy-simplelite.ps1,或 POST /api/health/simplelite/restart-for-update"
? "关闭 Simple3 后:运行 Migu2.0/scripts/redeploy-simple3.ps1,或 POST /api/health/simple3/restart-for-update"
: null;
return new SimpleLiteDiagnostics(
return new Simple3Diagnostics(
Enabled: _opts.Enabled,
FollowParent: _opts.FollowParent,
ConfiguredExecutablePath: _opts.ExecutablePath ?? "",
@@ -456,26 +457,26 @@ public sealed class SimpleLiteLauncher : IDisposable
ProjectionPortReachable: projectionUp,
GotoSiteApiAvailable: gotoSite,
DeployHint: deployHint,
ConfigHint: "编辑 MiGu.Server/appsettings.json 的 SimpleLite 节点(所有环境生效);开发机可叠加 appsettings.Development.json。" +
" FollowParent=false 时关闭 MiGu.Server 不会结束 SimpleLite。");
ConfigHint: "编辑 MiGu.Server/appsettings.json 的 Simple3 节点(所有环境生效);开发机可叠加 appsettings.Development.json。" +
" FollowParent=false 时关闭 MiGu.Server 不会结束 Simple3。");
}
/// <summary>
/// 拉起 SimpleLite。FollowParent=false 时在 Windows 上用 <c>cmd /c start</c> 脱离父进程组,避免平台退出连带结束 SimpleLite
/// 拉起 Simple3。FollowParent=false 时在 Windows 上用 <c>cmd /c start</c> 脱离父进程组,避免平台退出连带结束 Simple3
/// </summary>
private Process? StartSimpleLiteProcess(string resolved, string arguments, string workdir)
private Process? StartSimple3Process(string resolved, string arguments, string workdir)
{
if (_opts.FollowParent)
return StartSimpleLiteDirect(resolved, arguments, workdir, useShellExecute: true);
return StartSimple3Direct(resolved, arguments, workdir, useShellExecute: true);
if (OperatingSystem.IsWindows())
return StartSimpleLiteDetachedWindows(resolved, arguments, workdir)
?? StartSimpleLiteDirect(resolved, arguments, workdir, useShellExecute: true);
return StartSimple3DetachedWindows(resolved, arguments, workdir)
?? StartSimple3Direct(resolved, arguments, workdir, useShellExecute: true);
return StartSimpleLiteDirect(resolved, arguments, workdir, useShellExecute: true);
return StartSimple3Direct(resolved, arguments, workdir, useShellExecute: true);
}
private static Process? StartSimpleLiteDirect(string resolved, string arguments, string workdir, bool useShellExecute)
private static Process? StartSimple3Direct(string resolved, string arguments, string workdir, bool useShellExecute)
{
var psi = new ProcessStartInfo
{
@@ -493,14 +494,15 @@ public sealed class SimpleLiteLauncher : IDisposable
/// <summary>
/// 通过 cmd start 在新进程组/新控制台中启动,与 MiGu.Server 控制台 Ctrl+C、进程树结束解耦。
/// </summary>
private Process? StartSimpleLiteDetachedWindows(string resolved, string arguments, string workdir)
private Process? StartSimple3DetachedWindows(string resolved, string arguments, string workdir)
{
var exeName = Path.GetFileNameWithoutExtension(resolved);
var beforeIds = new HashSet<int>(
Process.GetProcessesByName(exeName).Select(p => { try { return p.Id; } catch { return -1; } })
.Where(id => id > 0));
var cmdArgs = $"/c start \"SimpleLite\" /D \"{workdir}\" \"{resolved}\" {arguments}";
// cmd start 的第一个引号参数是「窗口标题」,不是 exe;写死 Simple3 会让 Simple3 控制台标题错名。
var cmdArgs = $"/c start \"{exeName}\" /D \"{workdir}\" \"{resolved}\" {arguments}";
var shimPsi = new ProcessStartInfo
{
FileName = "cmd.exe",
@@ -521,7 +523,7 @@ public sealed class SimpleLiteLauncher : IDisposable
{
if (p.HasExited) continue;
if (beforeIds.Contains(p.Id)) continue;
_log.LogInformation("[SimpleLite] detached start via cmd.exe; new pid={Pid}", p.Id);
_log.LogInformation("[Simple3] detached start via cmd.exe; new pid={Pid}", p.Id);
return AttachToExistingProcess(p.Id);
}
catch { /* ignore */ }
@@ -529,7 +531,7 @@ public sealed class SimpleLiteLauncher : IDisposable
Thread.Sleep(200);
}
_log.LogWarning("[SimpleLite] detached start: no new {Name} process observed after cmd.exe start", exeName);
_log.LogWarning("[Simple3] detached start: no new {Name} process observed after cmd.exe start", exeName);
return null;
}
@@ -543,7 +545,7 @@ public sealed class SimpleLiteLauncher : IDisposable
}
catch (Exception ex)
{
_log.LogWarning("[SimpleLite] attach to pid={Pid} failed: {Msg}", pid, ex.Message);
_log.LogWarning("[Simple3] attach to pid={Pid} failed: {Msg}", pid, ex.Message);
return null;
}
}
@@ -554,7 +556,7 @@ public sealed class SimpleLiteLauncher : IDisposable
{
int exit;
try { exit = proc.ExitCode; } catch { exit = -1; }
_log.LogInformation("[SimpleLite] process exited code={Code}; next login will relaunch if needed", exit);
_log.LogInformation("[Simple3] process exited code={Code}; next login will relaunch if needed", exit);
lock (_sync)
{
if (ReferenceEquals(_proc, proc))
@@ -577,16 +579,26 @@ public sealed class SimpleLiteLauncher : IDisposable
var cwd = _env.ContentRootPath;
var staticCandidates = new[]
{
Path.Combine(cwd, "Simple3.exe"),
Path.Combine(cwd, "SimpleLite.exe"),
Path.Combine(cwd, "Simple3", "Simple3.exe"),
Path.Combine(cwd, "SimpleLite", "SimpleLite.exe"),
Path.Combine(cwd, "..", "Simple3.exe"),
Path.Combine(cwd, "..", "SimpleLite.exe"),
Path.Combine(cwd, "..", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
Path.Combine(cwd, "..", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
// Migu2.0 与 Simple 并列:Migu2.0/MiGu.Server → ../../Simple/SimpleLite/bin/Debug
// 发布布局(本机常见):D:\MDCS\Release\Simple\Simple3
@"D:\MDCS\Release\Simple\Simple3\Simple3.exe",
// 源码并列:Migu2.0/MiGu.Server → ../../Simple/Simple3OutputPath 常直接写到 Release
Path.Combine(cwd, "..", "..", "Simple", "Simple3", "Simple3.exe"),
Path.Combine(cwd, "..", "..", "Simple", "Simple3", "bin", "Debug", "Simple3.exe"),
Path.Combine(cwd, "..", "..", "Simple", "Simple3", "bin", "Release", "Simple3.exe"),
Path.Combine(cwd, "..", "Simple", "Simple3", "Simple3.exe"),
// 兼容旧 SimpleLite 目录/产物名
Path.Combine(cwd, "..", "..", "Simple", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
Path.Combine(cwd, "..", "..", "Simple", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
Path.Combine(cwd, "..", "Simple", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
Path.Combine(cwd, "..", "Simple", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
Path.Combine(cwd, "..", "SimpleLite", "bin", "Debug", "SimpleLite.exe"),
Path.Combine(cwd, "..", "SimpleLite", "bin", "Release", "SimpleLite.exe"),
};
foreach (var c in staticCandidates)
{
@@ -599,6 +611,13 @@ public sealed class SimpleLiteLauncher : IDisposable
{
foreach (var sub in new[]
{
"Simple3/Simple3.exe",
"Simple3/bin/Debug/Simple3.exe",
"Simple3/bin/Release/Simple3.exe",
"Simple/Simple3/Simple3.exe",
"Simple/Simple3/bin/Debug/Simple3.exe",
"Simple/Simple3/bin/Release/Simple3.exe",
"SimpleLite/SimpleLite.exe",
"SimpleLite/bin/Debug/SimpleLite.exe",
"SimpleLite/bin/Release/SimpleLite.exe",
"Simple/SimpleLite/bin/Debug/SimpleLite.exe",
@@ -668,14 +687,14 @@ public sealed class SimpleLiteLauncher : IDisposable
if (TryConnect("127.0.0.1", _opts.ProjectionPort, TimeSpan.FromMilliseconds(interval)))
{
_log.LogInformation("[SimpleLite] projection :{Port} ready", _opts.ProjectionPort);
_log.LogInformation("[Simple3] projection :{Port} ready", _opts.ProjectionPort);
return true;
}
Thread.Sleep(interval);
}
_log.LogWarning("[SimpleLite] projection :{Port} not ready within {Timeout}ms", _opts.ProjectionPort, _opts.ReadinessTimeoutMs);
_log.LogWarning("[Simple3] projection :{Port} not ready within {Timeout}ms", _opts.ProjectionPort, _opts.ReadinessTimeoutMs);
return false;
}
@@ -694,10 +713,10 @@ public sealed class SimpleLiteLauncher : IDisposable
}
/// <summary>
/// 在 TCP 通的基础上读取 SimpleLite Projection 的强类型 JSON 端点,判别「真的是 SimpleLite」。
/// 在 TCP 通的基础上读取 Simple3 Projection 的强类型 JSON 端点,判别「真的是 Simple3」。
/// 只接受 2xx 且响应体像 JSON 数组/对象;任意普通 HTTP 服务占用 8222 不再被误认为可复用。
/// </summary>
private static bool ProbeSimpleLiteHttp(string host, int port, TimeSpan timeout)
private static bool ProbeSimple3Http(string host, int port, TimeSpan timeout)
{
try
{
@@ -718,20 +737,20 @@ public sealed class SimpleLiteLauncher : IDisposable
}
/// <summary>
/// 拉起 SimpleLite 的结果摘要。
/// 拉起 Simple3 的结果摘要。
/// <list type="bullet">
/// <item><c>Started</c>:本次调用后是否处于"已运行"状态(包含 AlreadyRunning / ReusingExisting / Ready / StartedButNotReady)。</item>
/// <item><c>Status</c>:状态枚举字符串(见上)。</item>
/// <item><c>Detail</c>:技术细节,写入服务端日志。</item>
/// <item><c>DisplayMode</c>:本次实际生效的 displayModeweb / web+local / external / null)。
/// 与 LaunchMode 业务字段区分:external 表示复用了 MiGu.Server 重启前残留的 SimpleLite,对应模式未知。</item>
/// 与 LaunchMode 业务字段区分:external 表示复用了 MiGu.Server 重启前残留的 Simple3,对应模式未知。</item>
/// <item><c>Warning</c>:透传给前端登录响应的告警文本,null 表示无需告警。</item>
/// </list>
/// </summary>
public readonly record struct LaunchResult(bool Started, string Status, string Detail,
string? DisplayMode = null, string? Warning = null);
public sealed record SimpleLiteDiagnostics(
public sealed record Simple3Diagnostics(
bool Enabled,
bool FollowParent,
string ConfiguredExecutablePath,
@@ -807,7 +826,7 @@ public sealed class SimpleLiteLauncher : IDisposable
{
if (!OperatingSystem.IsWindows())
{
_log.LogInformation("[SimpleLite] JobObject skipped on non-Windows; falling back to ProcessExit-only cleanup");
_log.LogInformation("[Simple3] JobObject skipped on non-Windows; falling back to ProcessExit-only cleanup");
return;
}
@@ -818,7 +837,7 @@ public sealed class SimpleLiteLauncher : IDisposable
_job = CreateJobObject(IntPtr.Zero, null);
if (_job == IntPtr.Zero)
{
_log.LogWarning("[SimpleLite] CreateJobObject failed; child may outlive MiGu.Server");
_log.LogWarning("[Simple3] CreateJobObject failed; child may outlive MiGu.Server");
return;
}
@@ -831,17 +850,17 @@ public sealed class SimpleLiteLauncher : IDisposable
{
Marshal.StructureToPtr(info, ptr, false);
if (!SetInformationJobObject(_job, JobObjectExtendedLimitInformation, ptr, (uint)len))
_log.LogWarning("[SimpleLite] SetInformationJobObject failed; child may outlive MiGu.Server");
_log.LogWarning("[Simple3] SetInformationJobObject failed; child may outlive MiGu.Server");
}
finally { Marshal.FreeHGlobal(ptr); }
}
if (!AssignProcessToJobObject(_job, child.Handle))
_log.LogWarning("[SimpleLite] AssignProcessToJobObject failed; child may outlive MiGu.Server");
_log.LogWarning("[Simple3] AssignProcessToJobObject failed; child may outlive MiGu.Server");
}
catch (Exception ex)
{
_log.LogWarning("[SimpleLite] JobObject setup error: {Type}: {Msg}", ex.GetType().Name, ex.Message);
_log.LogWarning("[Simple3] JobObject setup error: {Type}: {Msg}", ex.GetType().Name, ex.Message);
}
}
}
@@ -1,31 +1,30 @@
namespace MiGu.Server.Launcher;
/// <summary>
/// MiGu.Server 启动后由「平台登录」按 LaunchMode 拉起的 SimpleLite 子进程配置。
/// MiGu.Server 启动后由「平台登录」按 LaunchMode 拉起的 Simple3 子进程配置。
///
/// 会话 N+1(启动反转):原架构是 SimpleLite 启动 → 拉 MiGu.Server;现在反过来:
/// MiGu.Server 作为主入口启动 → 登录页选 LaunchMode → 后端拉起 SimpleLite.exe
/// 通过 <c>--display-mode</c> 参数把 web / web+local 透传给 SimpleLite 的 Configuration。
/// 会话 N+1(启动反转):原架构是 Simple3 启动 → 拉 MiGu.Server;现在反过来:
/// MiGu.Server 作为主入口启动 → 登录页选 LaunchMode → 后端拉起 Simple3.exe
/// 通过 <c>--display-mode</c> 参数把 web / web+local 透传给 Simple3 的 Configuration。
///
/// 绑定 <c>appsettings.json:SimpleLite</c>。
/// 绑定 <c>appsettings.json:Simple3</c>。
/// </summary>
public sealed class SimpleLiteOptions
public sealed class Simple3Options
{
/// <summary>主开关。false = 永不拉起;登录无论选什么 LaunchMode 都不会启动子进程,用于「只跑 Platform 调试」场景。</summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// SimpleLite 可执行文件绝对/相对路径(相对 MiGu.Server 工作目录)。留空则按以下顺序自动探测:
/// 1) <c>./SimpleLite.exe</c>(合并发布布局SimpleLite 与 MiGu.Server 同目录
/// 2) <c>./SimpleLite/SimpleLite.exe</c>
/// 3) <c>../SimpleLite.exe</c>MiGu.Server 部署到子目录的常见布局)
/// 4) <c>../SimpleLite/bin/Debug/SimpleLite.exe</c>(开发态:Visual Studio 默认输出
/// 5) <c>../SimpleLite/bin/Release/SimpleLite.exe</c>
/// 6) 沿父目录上行寻找 <c>SimpleLite/bin/{Debug|Release}/SimpleLite.exe</c>
/// Simple3 可执行文件绝对/相对路径(相对 MiGu.Server 工作目录)。留空则按以下顺序自动探测:
/// 1) <c>./Simple3.exe</c>(合并发布布局;仍兼容探测旧名 <c>./SimpleLite.exe</c>
/// 2) <c>./Simple3/Simple3.exe</c>
/// 3) 开发态:兄弟仓库 <c>../Simple/Simple3/</c> 产出目录或 <c>bin/Debug|Release</c>
/// 4) 发布目录常见布局:<c>D:\MDCS\Release\Simple\Simple3\Simple3.exe</c>(仅当该绝对路径存在
/// 5) 沿父目录上行寻找 Simple3 产出
/// </summary>
public string ExecutablePath { get; set; } = "";
/// <summary>留空时取 <see cref="ExecutablePath"/> 所在目录。SimpleLite 在 CWD 读写 simple.json / imgui.iniCWD 选错会出意外。</summary>
/// <summary>留空时取 <see cref="ExecutablePath"/> 所在目录。内核在 CWD 读写 simple.json / imgui.iniCWD 选错会出意外。</summary>
public string WorkingDirectory { get; set; } = "";
/// <summary>附加命令行参数(拼在 <c>--display-mode=xxx</c> 之后)。常用于本地调试时强制 autoload 某场景。</summary>
@@ -33,7 +32,7 @@ public sealed class SimpleLiteOptions
/// <summary>
/// 子进程启动后阻塞等待 Projection (:8222) 端口就绪的最长毫秒数。
/// 0 = 不等待(登录立即返回,前端可能还连不上 SimpleLite WebApi);
/// 0 = 不等待(登录立即返回,前端可能还连不上 Simple3 WebApi);
/// 负值 = 无限等待(直到子进程退出或就绪)。
/// </summary>
public int ReadinessTimeoutMs { get; set; } = 8000;
@@ -41,17 +40,17 @@ public sealed class SimpleLiteOptions
/// <summary>每隔多少毫秒 poll 一次 Projection 端口可达性。</summary>
public int ReadinessPollIntervalMs { get; set; } = 250;
/// <summary>SimpleLite Projection WebApi 监听端口。默认与 SimpleLite Configuration 的 `port` 一致。用于就绪检测。</summary>
/// <summary>Simple3 Projection WebApi 监听端口。默认与 Simple3 Configuration 的 `port` 一致。用于就绪检测。</summary>
public int ProjectionPort { get; set; } = 8222;
/// <summary>
/// 是否把 SimpleLite 绑定到 MiGu.Server 生命周期,默认 <b>false</b>(会话 N+2 用户反馈)。
/// 是否把 Simple3 绑定到 MiGu.Server 生命周期,默认 <b>false</b>(会话 N+2 用户反馈)。
///
/// 设计原则:SimpleLite 与 MiGu.Server 是「两个独立程序」,Platform 只是登录后顺手拉起 SimpleLite
/// MiGu.Server 关闭不应该带走 SimpleLite,反之亦然。所以 FollowParent 默认 false
/// 设计原则:Simple3 与 MiGu.Server 是「两个独立程序」,Platform 只是登录后顺手拉起 Simple3
/// MiGu.Server 关闭不应该带走 Simple3,反之亦然。所以 FollowParent 默认 false
/// - 子进程走 <c>UseShellExecute=true</c> 创建独立进程组 + 独立控制台窗口;
/// - 不挂 JobObject,不在 ApplicationStopping / ProcessExit 时 kill 子进程;
/// - SimpleLite 退出由用户自己负责(关窗口 / 任务管理器 / 调度内核异常退出)。
/// - Simple3 退出由用户自己负责(关窗口 / 任务管理器 / 调度内核异常退出)。
///
/// true 仍可用:会启动 Windows JobObject 父子绑定(仅 Windows 有效)+ 注册 ApplicationStopping 软关闭。
/// 一般只在临时联调期 / CI 流水线想自动清理时打开。
@@ -59,11 +58,11 @@ public sealed class SimpleLiteOptions
public bool FollowParent { get; set; } = false;
/// <summary>
/// 平台拉起 SimpleLite 时是否以「迷榖嵌入画布」模式运行(透传命令行 <c>--migu</c>),默认 <b>true</b>。
/// 平台拉起 Simple3 时是否以「迷榖嵌入画布」模式运行(透传命令行 <c>--migu</c>),默认 <b>true</b>。
///
/// true:平台 iframe 嵌入场景,SimpleLite WebTerminal 默认只显示 3D 画布、隐藏所有 ImGui panel
/// true:平台 iframe 嵌入场景,Simple3 WebTerminal 默认只显示 3D 画布、隐藏所有 ImGui panel
/// 业务 UI 全部由 Vue 平台前端接管;declare 时序失败时也安全回退到纯画布。
/// false:平台拉起的 SimpleLite web 端默认显示完整 panel(便于把平台拉起的实例直连 :8223 调试)。
/// false:平台拉起的 Simple3 web 端默认显示完整 panel(便于把平台拉起的实例直连 :8223 调试)。
///
/// 与 LaunchModeweb / web+local,是否保留本地调试窗口)正交,可任意组合。
/// </summary>