PythonProcessController.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using LightGlue.Unity.Config;
  5. using UnityEngine;
  6. using Debug = UnityEngine.Debug;
  7. namespace LightGlue.Unity.Python
  8. {
  9. /// <summary>
  10. /// Unity-controlled Python child process launcher (start/stop + exit detection).
  11. /// This class only wraps process control; it does not change LightGlue algorithm logic.
  12. /// </summary>
  13. public sealed class PythonProcessController : MonoBehaviour
  14. {
  15. [Header("Python")]
  16. [Tooltip("python.exe path or just 'python' if it's on PATH.")]
  17. public string pythonExe = "python";
  18. [Tooltip("Working directory for the Python process (e.g. LightGlue_Deployment).")]
  19. public string workingDirectory = "";
  20. [Tooltip("Script path relative to workingDirectory or absolute path.")]
  21. public string scriptPath = "demo_lightglue_camera_position_async.py";
  22. [Tooltip("参考NetworkConfig.pythonScriptArgs")]
  23. [TextArea(3, 8)]
  24. public string scriptArgs = "";
  25. [Header("配置管理")]
  26. [Tooltip("是否从NetworkConfigManager自动加载Python启动参数(启动时)")]
  27. public bool autoLoadNetworkConfig = true;
  28. [Tooltip("若为 true,则从 NetworkConfig.romaPythonScriptArgs 读取并替换占位符;否则使用 NetworkConfig.pythonScriptArgs(默认旧 LightGlue_Deployment)。")]
  29. public bool useRomaArgsFromNetworkConfig = false;
  30. [Tooltip("若为 true,则使用 NetworkConfig.romaWifiPythonScriptArgs(适配 demo_roma_camera_position_async_wifi.py);优先级高于 useRomaArgsFromNetworkConfig。")]
  31. public bool useRomaWifiArgsFromNetworkConfig = false;
  32. [Header("Lifecycle")]
  33. [Tooltip("是否在OnEnable时自动启动Python进程。若启用下面的「首帧后再启动」,则 OnEnable 时不启动,由 Bridge 在收到首帧图像后调用 StartPython()。")]
  34. public bool autoStartOnEnable = true;
  35. [Tooltip("为 true 时:不在一启动就拉 Python,等收到首帧图像后再启动(首次启动或应用配置重启后均生效),避免无图时白跑 TensorRT 等。")]
  36. public bool startOnFirstImageReceived = true;
  37. public bool killProcessTreeOnStop = true;
  38. [Header("无窗口运行(避免后台卡顿)")]
  39. [Tooltip("勾选后自动在启动参数中加上 --no_display,Python 不创建 OpenCV 窗口;结果仍通过 UDP 回 Unity。可避免「窗口在游戏背后时被系统节流导致卡顿」。取消勾选可保留 Python 绘制窗口便于调试。")]
  40. public bool addNoDisplayWhenLaunching = true;
  41. [Header("打包模式")]
  42. [Tooltip("勾选后在打包客户端中自动查找同级目录下的 LightGlue_Deployment/lightglue_runtime.exe," +
  43. "无需在 Inspector 中手动配置 pythonExe / workingDirectory / scriptPath。开发阶段建议不勾选,使用本机 Python + .py 脚本。")]
  44. public bool usePackagedExe = false;
  45. private Process _proc;
  46. private StreamWriter _stdinWriter;
  47. private volatile bool _readyToReceiveFrames;
  48. private float _processStartTime = -1f;
  49. [Tooltip("若在此秒数内未收到 Python 就绪输出,则视为就绪并开始发图(防止 stdout 缓冲导致窗口一直不弹出)")]
  50. public float readyTimeoutSeconds = 120f;
  51. public bool IsRunning => _proc != null && !_proc.HasExited;
  52. /// <summary>
  53. /// Python 是否已进入接收循环(收到脚本 stdout 中的就绪标记后才为 true,避免 TensorRT 编译期间发图导致缓冲堆积、后续解码损坏)。
  54. /// </summary>
  55. public bool IsReadyToReceiveFrames => _readyToReceiveFrames;
  56. /// <summary>
  57. /// 获取Python进程的stdin写入流(用于直接发送图片数据)
  58. /// </summary>
  59. public StreamWriter StdinWriter => _stdinWriter;
  60. private void Start()
  61. {
  62. // 自动加载网络配置中的Python启动参数
  63. if (autoLoadNetworkConfig)
  64. {
  65. LoadPythonArgsFromConfig();
  66. }
  67. }
  68. /// <summary>
  69. /// 从NetworkConfigManager加载Python启动参数
  70. /// </summary>
  71. private void LoadPythonArgsFromConfig()
  72. {
  73. try
  74. {
  75. NetworkConfig config = NetworkConfigManager.LoadConfig();
  76. if (config != null && config.Validate())
  77. {
  78. // 获取处理后的参数(替换占位符)
  79. string processedArgs;
  80. if (useRomaWifiArgsFromNetworkConfig)
  81. processedArgs = config.GetRomaWifiPythonScriptArgs();
  82. else if (useRomaArgsFromNetworkConfig)
  83. processedArgs = config.GetRomaPythonScriptArgs();
  84. else
  85. processedArgs = config.GetPythonScriptArgs();
  86. if (!string.IsNullOrWhiteSpace(processedArgs))
  87. {
  88. scriptArgs = processedArgs;
  89. Debug.Log($"[Python] 已从配置文件加载启动参数: {scriptArgs}");
  90. }
  91. }
  92. }
  93. catch (System.Exception ex)
  94. {
  95. Debug.LogWarning($"[Python] 加载网络配置失败,使用Inspector中的默认值: {ex.Message}");
  96. }
  97. }
  98. private void OnEnable()
  99. {
  100. if (autoStartOnEnable && !startOnFirstImageReceived)
  101. StartPython();
  102. }
  103. private void OnDisable()
  104. {
  105. StopPython();
  106. }
  107. private void Update()
  108. {
  109. if (IsRunning && !_readyToReceiveFrames && _processStartTime >= 0f && readyTimeoutSeconds > 0f
  110. && (UnityEngine.Time.time - _processStartTime) >= readyTimeoutSeconds)
  111. {
  112. _readyToReceiveFrames = true;
  113. Debug.Log("[Python] 未检测到就绪输出,超时后视为就绪并开始发送。");
  114. }
  115. }
  116. public void StartPython()
  117. {
  118. if (IsRunning) return;
  119. // 确保在真正启动进程前,先加载最新的配置参数
  120. if (autoLoadNetworkConfig)
  121. {
  122. LoadPythonArgsFromConfig();
  123. }
  124. string wd = workingDirectory;
  125. string packagedExePath = null;
  126. if (!usePackagedExe)
  127. {
  128. // 开发模式:使用本机 Python + 脚本.py
  129. if (string.IsNullOrWhiteSpace(wd))
  130. wd = Directory.GetCurrentDirectory();
  131. wd = Path.GetFullPath(wd);
  132. }
  133. else
  134. {
  135. // 打包模式:自动定位 LightGlue_Deployment 目录和 lightglue_runtime.exe
  136. string baseDir;
  137. #if UNITY_STANDALONE
  138. // 打包后的客户端:Application.dataPath 指向 xxx_Data,父目录为 exe 所在目录
  139. baseDir = Path.GetDirectoryName(Application.dataPath);
  140. #else
  141. // 编辑器或其他平台:使用当前工作目录,方便在本机模拟客户端目录结构
  142. baseDir = Directory.GetCurrentDirectory();
  143. #endif
  144. string deploymentDir = Path.Combine(baseDir, "LightGlue_Deployment");
  145. packagedExePath = Path.Combine(deploymentDir, "lightglue_runtime.exe");
  146. wd = deploymentDir;
  147. if (!File.Exists(packagedExePath))
  148. {
  149. Debug.LogError($"[Python] 打包模式启动失败:未找到打包好的运行文件: {packagedExePath}");
  150. return;
  151. }
  152. }
  153. string script = scriptPath;
  154. if (!usePackagedExe)
  155. {
  156. // 开发模式:仍然使用 python + 脚本.py 方式启动
  157. if (!Path.IsPathRooted(script))
  158. script = Path.GetFullPath(Path.Combine(wd, script));
  159. if (!File.Exists(script))
  160. {
  161. Debug.LogError($"[Python] Script not found: {script}");
  162. return;
  163. }
  164. }
  165. var psi = new ProcessStartInfo
  166. {
  167. // 打包模式:直接执行打包好的 exe;开发模式:执行 python.exe
  168. FileName = usePackagedExe ? packagedExePath : pythonExe,
  169. WorkingDirectory = wd,
  170. UseShellExecute = false,
  171. CreateNoWindow = true,
  172. RedirectStandardOutput = true,
  173. RedirectStandardError = true,
  174. RedirectStandardInput = true, // 启用stdin重定向
  175. };
  176. string args = (scriptArgs ?? string.Empty).Trim();
  177. if (addNoDisplayWhenLaunching && !args.Contains("--no_display"))
  178. args = args + " --no_display";
  179. psi.Arguments = usePackagedExe ? args : $"{Quote(script)} {args}";
  180. _proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
  181. _readyToReceiveFrames = false;
  182. _proc.OutputDataReceived += (_, e) =>
  183. {
  184. if (string.IsNullOrEmpty(e.Data)) return;
  185. if (!_readyToReceiveFrames && (e.Data.Contains("UDP JPEG mode: receiver started") || e.Data.Contains("First frame received and processed")))
  186. {
  187. _readyToReceiveFrames = true;
  188. Debug.Log("[Python] 已就绪接收图像,开始向 Python 发送帧。");
  189. }
  190. Debug.Log($"[Python] {e.Data}");
  191. };
  192. _proc.ErrorDataReceived += (_, e) =>
  193. {
  194. if (string.IsNullOrEmpty(e.Data)) return;
  195. // Some Python libraries output warnings to stderr that are not fatal errors
  196. string msg = e.Data.ToLower();
  197. if (msg.Contains("unable to import") || msg.Contains("please install") ||
  198. msg.Contains("warning") || msg.Contains("deprecated"))
  199. {
  200. Debug.LogWarning($"[Python] {e.Data}");
  201. }
  202. else
  203. {
  204. Debug.LogError($"[Python] {e.Data}");
  205. }
  206. };
  207. _proc.Exited += (_, __) =>
  208. {
  209. int code = _proc?.ExitCode ?? -1;
  210. Debug.LogWarning($"[Python] Process exited. code={code}");
  211. };
  212. try
  213. {
  214. if (!_proc.Start())
  215. {
  216. Debug.LogError("[Python] Failed to start process.");
  217. _proc = null;
  218. return;
  219. }
  220. // 获取stdin写入流(用于发送图片数据)
  221. _stdinWriter = _proc.StandardInput;
  222. _stdinWriter.AutoFlush = true; // 自动刷新,减少延迟
  223. _processStartTime = UnityEngine.Time.time;
  224. _proc.BeginOutputReadLine();
  225. _proc.BeginErrorReadLine();
  226. Debug.Log($"[Python] Started successfully! pid={_proc.Id}\n" +
  227. $" WorkingDir: {wd}\n" +
  228. $" Command: {psi.FileName} {psi.Arguments}\n" +
  229. $" Mode: {(usePackagedExe ? "PackagedExe" : "DevPython")}\n" +
  230. $" Stdin: Enabled (for direct image transfer)");
  231. }
  232. catch (Exception ex)
  233. {
  234. Debug.LogError($"[Python] Start failed: {ex}");
  235. _proc = null;
  236. _stdinWriter = null;
  237. }
  238. }
  239. public void StopPython()
  240. {
  241. if (_proc == null) return;
  242. try
  243. {
  244. if (!_proc.HasExited)
  245. {
  246. if (killProcessTreeOnStop)
  247. KillProcessTree(_proc);
  248. else
  249. _proc.Kill();
  250. }
  251. }
  252. catch (Exception ex)
  253. {
  254. Debug.LogWarning($"[Python] Stop error: {ex.Message}");
  255. }
  256. finally
  257. {
  258. _readyToReceiveFrames = false;
  259. _processStartTime = -1f;
  260. try
  261. {
  262. _stdinWriter?.Close();
  263. _stdinWriter = null;
  264. }
  265. catch { /* ignore */ }
  266. try { _proc.Dispose(); } catch { /* ignore */ }
  267. _proc = null;
  268. }
  269. }
  270. /// <summary>
  271. /// Attempts to kill the entire process tree for the given process.
  272. /// On Windows this uses 'taskkill /T /F', on other platforms it falls back to Kill().
  273. /// </summary>
  274. private static void KillProcessTree(Process proc)
  275. {
  276. if (proc == null) return;
  277. try
  278. {
  279. #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
  280. // Use Windows taskkill to terminate the whole tree
  281. try
  282. {
  283. var startInfo = new ProcessStartInfo
  284. {
  285. FileName = "taskkill",
  286. Arguments = $"/PID {proc.Id} /T /F",
  287. CreateNoWindow = true,
  288. UseShellExecute = false,
  289. RedirectStandardOutput = false,
  290. RedirectStandardError = false
  291. };
  292. using (var killer = Process.Start(startInfo))
  293. {
  294. // Best-effort: wait a short time, but don't block indefinitely
  295. killer?.WaitForExit(3000);
  296. }
  297. }
  298. catch
  299. {
  300. // If taskkill fails, fall back to normal Kill
  301. if (!proc.HasExited)
  302. proc.Kill();
  303. }
  304. #else
  305. // Non-Windows: no simple built-in tree-kill; just kill the main process
  306. if (!proc.HasExited)
  307. proc.Kill();
  308. #endif
  309. }
  310. catch
  311. {
  312. // Swallow any errors; caller already wraps StopPython() in a try/catch.
  313. if (!proc.HasExited)
  314. try { proc.Kill(); } catch { /* ignore */ }
  315. }
  316. }
  317. private static string Quote(string s)
  318. {
  319. if (string.IsNullOrEmpty(s)) return "\"\"";
  320. if (s.Contains("\"")) s = s.Replace("\"", "\\\"");
  321. if (s.Contains(" ") || s.Contains("\t")) return $"\"{s}\"";
  322. return s;
  323. }
  324. }
  325. }