using System;
using System.Diagnostics;
using System.IO;
using LightGlue.Unity.Config;
using UnityEngine;
using Debug = UnityEngine.Debug;
namespace LightGlue.Unity.Python
{
///
/// Unity-controlled Python child process launcher (start/stop + exit detection).
/// This class only wraps process control; it does not change LightGlue algorithm logic.
///
public sealed class PythonProcessController : MonoBehaviour
{
[Header("Python")]
[Tooltip("python.exe path or just 'python' if it's on PATH.")]
public string pythonExe = "python";
[Tooltip("Working directory for the Python process (e.g. LightGlue_Deployment).")]
public string workingDirectory = "";
[Tooltip("Script path relative to workingDirectory or absolute path.")]
public string scriptPath = "demo_lightglue_camera_position_async.py";
[Tooltip("参考NetworkConfig.pythonScriptArgs")]
[TextArea(3, 8)]
public string scriptArgs = "";
[Header("配置管理")]
[Tooltip("是否从NetworkConfigManager自动加载Python启动参数(启动时)")]
public bool autoLoadNetworkConfig = true;
[Tooltip("若为 true,则从 NetworkConfig.romaPythonScriptArgs 读取并替换占位符;否则使用 NetworkConfig.pythonScriptArgs(默认旧 LightGlue_Deployment)。")]
public bool useRomaArgsFromNetworkConfig = false;
[Tooltip("若为 true,则使用 NetworkConfig.romaWifiPythonScriptArgs(适配 demo_roma_camera_position_async_wifi.py);优先级高于 useRomaArgsFromNetworkConfig。")]
public bool useRomaWifiArgsFromNetworkConfig = false;
[Header("Lifecycle")]
[Tooltip("是否在OnEnable时自动启动Python进程。若启用下面的「首帧后再启动」,则 OnEnable 时不启动,由 Bridge 在收到首帧图像后调用 StartPython()。")]
public bool autoStartOnEnable = true;
[Tooltip("为 true 时:不在一启动就拉 Python,等收到首帧图像后再启动(首次启动或应用配置重启后均生效),避免无图时白跑 TensorRT 等。")]
public bool startOnFirstImageReceived = true;
public bool killProcessTreeOnStop = true;
[Header("无窗口运行(避免后台卡顿)")]
[Tooltip("勾选后自动在启动参数中加上 --no_display,Python 不创建 OpenCV 窗口;结果仍通过 UDP 回 Unity。可避免「窗口在游戏背后时被系统节流导致卡顿」。取消勾选可保留 Python 绘制窗口便于调试。")]
public bool addNoDisplayWhenLaunching = true;
[Header("打包模式")]
[Tooltip("勾选后在打包客户端中自动查找同级目录下的 LightGlue_Deployment/lightglue_runtime.exe," +
"无需在 Inspector 中手动配置 pythonExe / workingDirectory / scriptPath。开发阶段建议不勾选,使用本机 Python + .py 脚本。")]
public bool usePackagedExe = false;
private Process _proc;
private StreamWriter _stdinWriter;
private volatile bool _readyToReceiveFrames;
private float _processStartTime = -1f;
[Tooltip("若在此秒数内未收到 Python 就绪输出,则视为就绪并开始发图(防止 stdout 缓冲导致窗口一直不弹出)")]
public float readyTimeoutSeconds = 120f;
public bool IsRunning => _proc != null && !_proc.HasExited;
///
/// Python 是否已进入接收循环(收到脚本 stdout 中的就绪标记后才为 true,避免 TensorRT 编译期间发图导致缓冲堆积、后续解码损坏)。
///
public bool IsReadyToReceiveFrames => _readyToReceiveFrames;
///
/// 获取Python进程的stdin写入流(用于直接发送图片数据)
///
public StreamWriter StdinWriter => _stdinWriter;
private void Start()
{
// 自动加载网络配置中的Python启动参数
if (autoLoadNetworkConfig)
{
LoadPythonArgsFromConfig();
}
}
///
/// 从NetworkConfigManager加载Python启动参数
///
private void LoadPythonArgsFromConfig()
{
try
{
NetworkConfig config = NetworkConfigManager.LoadConfig();
if (config != null && config.Validate())
{
// 获取处理后的参数(替换占位符)
string processedArgs;
if (useRomaWifiArgsFromNetworkConfig)
processedArgs = config.GetRomaWifiPythonScriptArgs();
else if (useRomaArgsFromNetworkConfig)
processedArgs = config.GetRomaPythonScriptArgs();
else
processedArgs = config.GetPythonScriptArgs();
if (!string.IsNullOrWhiteSpace(processedArgs))
{
scriptArgs = processedArgs;
Debug.Log($"[Python] 已从配置文件加载启动参数: {scriptArgs}");
}
}
}
catch (System.Exception ex)
{
Debug.LogWarning($"[Python] 加载网络配置失败,使用Inspector中的默认值: {ex.Message}");
}
}
private void OnEnable()
{
if (autoStartOnEnable && !startOnFirstImageReceived)
StartPython();
}
private void OnDisable()
{
StopPython();
}
private void Update()
{
if (IsRunning && !_readyToReceiveFrames && _processStartTime >= 0f && readyTimeoutSeconds > 0f
&& (UnityEngine.Time.time - _processStartTime) >= readyTimeoutSeconds)
{
_readyToReceiveFrames = true;
Debug.Log("[Python] 未检测到就绪输出,超时后视为就绪并开始发送。");
}
}
public void StartPython()
{
if (IsRunning) return;
// 确保在真正启动进程前,先加载最新的配置参数
if (autoLoadNetworkConfig)
{
LoadPythonArgsFromConfig();
}
string wd = workingDirectory;
string packagedExePath = null;
if (!usePackagedExe)
{
// 开发模式:使用本机 Python + 脚本.py
if (string.IsNullOrWhiteSpace(wd))
wd = Directory.GetCurrentDirectory();
wd = Path.GetFullPath(wd);
}
else
{
// 打包模式:自动定位 LightGlue_Deployment 目录和 lightglue_runtime.exe
string baseDir;
#if UNITY_STANDALONE
// 打包后的客户端:Application.dataPath 指向 xxx_Data,父目录为 exe 所在目录
baseDir = Path.GetDirectoryName(Application.dataPath);
#else
// 编辑器或其他平台:使用当前工作目录,方便在本机模拟客户端目录结构
baseDir = Directory.GetCurrentDirectory();
#endif
string deploymentDir = Path.Combine(baseDir, "LightGlue_Deployment");
packagedExePath = Path.Combine(deploymentDir, "lightglue_runtime.exe");
wd = deploymentDir;
if (!File.Exists(packagedExePath))
{
Debug.LogError($"[Python] 打包模式启动失败:未找到打包好的运行文件: {packagedExePath}");
return;
}
}
string script = scriptPath;
if (!usePackagedExe)
{
// 开发模式:仍然使用 python + 脚本.py 方式启动
if (!Path.IsPathRooted(script))
script = Path.GetFullPath(Path.Combine(wd, script));
if (!File.Exists(script))
{
Debug.LogError($"[Python] Script not found: {script}");
return;
}
}
var psi = new ProcessStartInfo
{
// 打包模式:直接执行打包好的 exe;开发模式:执行 python.exe
FileName = usePackagedExe ? packagedExePath : pythonExe,
WorkingDirectory = wd,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true, // 启用stdin重定向
};
string args = (scriptArgs ?? string.Empty).Trim();
if (addNoDisplayWhenLaunching && !args.Contains("--no_display"))
args = args + " --no_display";
psi.Arguments = usePackagedExe ? args : $"{Quote(script)} {args}";
_proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
_readyToReceiveFrames = false;
_proc.OutputDataReceived += (_, e) =>
{
if (string.IsNullOrEmpty(e.Data)) return;
if (!_readyToReceiveFrames && (e.Data.Contains("UDP JPEG mode: receiver started") || e.Data.Contains("First frame received and processed")))
{
_readyToReceiveFrames = true;
Debug.Log("[Python] 已就绪接收图像,开始向 Python 发送帧。");
}
Debug.Log($"[Python] {e.Data}");
};
_proc.ErrorDataReceived += (_, e) =>
{
if (string.IsNullOrEmpty(e.Data)) return;
// Some Python libraries output warnings to stderr that are not fatal errors
string msg = e.Data.ToLower();
if (msg.Contains("unable to import") || msg.Contains("please install") ||
msg.Contains("warning") || msg.Contains("deprecated"))
{
Debug.LogWarning($"[Python] {e.Data}");
}
else
{
Debug.LogError($"[Python] {e.Data}");
}
};
_proc.Exited += (_, __) =>
{
int code = _proc?.ExitCode ?? -1;
Debug.LogWarning($"[Python] Process exited. code={code}");
};
try
{
if (!_proc.Start())
{
Debug.LogError("[Python] Failed to start process.");
_proc = null;
return;
}
// 获取stdin写入流(用于发送图片数据)
_stdinWriter = _proc.StandardInput;
_stdinWriter.AutoFlush = true; // 自动刷新,减少延迟
_processStartTime = UnityEngine.Time.time;
_proc.BeginOutputReadLine();
_proc.BeginErrorReadLine();
Debug.Log($"[Python] Started successfully! pid={_proc.Id}\n" +
$" WorkingDir: {wd}\n" +
$" Command: {psi.FileName} {psi.Arguments}\n" +
$" Mode: {(usePackagedExe ? "PackagedExe" : "DevPython")}\n" +
$" Stdin: Enabled (for direct image transfer)");
}
catch (Exception ex)
{
Debug.LogError($"[Python] Start failed: {ex}");
_proc = null;
_stdinWriter = null;
}
}
public void StopPython()
{
if (_proc == null) return;
try
{
if (!_proc.HasExited)
{
if (killProcessTreeOnStop)
KillProcessTree(_proc);
else
_proc.Kill();
}
}
catch (Exception ex)
{
Debug.LogWarning($"[Python] Stop error: {ex.Message}");
}
finally
{
_readyToReceiveFrames = false;
_processStartTime = -1f;
try
{
_stdinWriter?.Close();
_stdinWriter = null;
}
catch { /* ignore */ }
try { _proc.Dispose(); } catch { /* ignore */ }
_proc = null;
}
}
///
/// Attempts to kill the entire process tree for the given process.
/// On Windows this uses 'taskkill /T /F', on other platforms it falls back to Kill().
///
private static void KillProcessTree(Process proc)
{
if (proc == null) return;
try
{
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
// Use Windows taskkill to terminate the whole tree
try
{
var startInfo = new ProcessStartInfo
{
FileName = "taskkill",
Arguments = $"/PID {proc.Id} /T /F",
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = false,
RedirectStandardError = false
};
using (var killer = Process.Start(startInfo))
{
// Best-effort: wait a short time, but don't block indefinitely
killer?.WaitForExit(3000);
}
}
catch
{
// If taskkill fails, fall back to normal Kill
if (!proc.HasExited)
proc.Kill();
}
#else
// Non-Windows: no simple built-in tree-kill; just kill the main process
if (!proc.HasExited)
proc.Kill();
#endif
}
catch
{
// Swallow any errors; caller already wraps StopPython() in a try/catch.
if (!proc.HasExited)
try { proc.Kill(); } catch { /* ignore */ }
}
}
private static string Quote(string s)
{
if (string.IsNullOrEmpty(s)) return "\"\"";
if (s.Contains("\"")) s = s.Replace("\"", "\\\"");
if (s.Contains(" ") || s.Contains("\t")) return $"\"{s}\"";
return s;
}
}
}