PythonProcessController.cs 14 KB

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