using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
namespace SynapticPro
{
///
/// Launches Node.js HTTP server detached from Unity's Win32 JobObject.
///
/// Unity Editor on Windows assigns a Job Object with
/// JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE to itself; any child process started
/// via Process.Start inherits the Job and is killed when Unity manipulates
/// the Job (assembly reload, PlayMode transitions, certain GC paths).
///
/// This launcher uses CreateProcessW via P/Invoke with
/// CREATE_BREAKAWAY_FROM_JOB so the spawned node.exe is independent of
/// Unity's Job. Combined with a parent-PID watchdog in http-server.js the
/// child is reliably tied to Unity's lifecycle without being subject to
/// Job-Object cascade kills.
///
/// PID is persisted in SessionState so the same process can be re-attached
/// after domain reload (recovers ESC-0095 "Connect Unity Only" case).
///
public static class SynapticDetachedProcess
{
public const string PID_KEY = "Synaptic.NodeServer.PID";
public const string PORT_KEY = "Synaptic.NodeServer.PORT";
// ----- Win32 P/Invoke -----
private const uint CREATE_BREAKAWAY_FROM_JOB = 0x01000000;
private const uint CREATE_NEW_PROCESS_GROUP = 0x00000200;
private const uint DETACHED_PROCESS = 0x00000008;
private const uint CREATE_NO_WINDOW = 0x08000000;
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute;
public int dwFlags;
public short wShowWindow, cbReserved2;
public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess, hThread;
public int dwProcessId, dwThreadId;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CreateProcessW(
string lpApplicationName, string lpCommandLine,
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
bool bInheritHandles, uint dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr h);
///
/// Start node.exe http-server.js detached from Unity's Job.
/// Returns spawned PID on success, 0 on failure.
/// Windows only — caller falls back to Process.Start on other platforms.
///
public static int StartWindows(string nodeExe, string scriptPath, int port, string mcpServerDir, string logFile)
{
int unityPid = Process.GetCurrentProcess().Id;
string cmd = $"\"{nodeExe}\" \"{scriptPath}\" {port} --parent-pid={unityPid} --log=\"{logFile}\"";
var si = new STARTUPINFO { cb = Marshal.SizeOf() };
uint flags = CREATE_BREAKAWAY_FROM_JOB
| DETACHED_PROCESS
| CREATE_NEW_PROCESS_GROUP
| CREATE_NO_WINDOW
| CREATE_UNICODE_ENVIRONMENT;
bool ok = CreateProcessW(
null, cmd,
IntPtr.Zero, IntPtr.Zero,
false, flags,
IntPtr.Zero, mcpServerDir,
ref si, out PROCESS_INFORMATION pi);
// Fallback: if BREAKAWAY denied (ACCESS_DENIED on JobObject without
// JOB_OBJECT_LIMIT_BREAKAWAY_OK), retry with DETACHED only and rely
// on node-side parent-PID watchdog for orphan cleanup.
if (!ok)
{
int err = Marshal.GetLastWin32Error();
UnityEngine.Debug.LogWarning($"[Synaptic] CreateProcess with BREAKAWAY failed (err={err}). Retrying without BREAKAWAY.");
flags &= ~CREATE_BREAKAWAY_FROM_JOB;
ok = CreateProcessW(
null, cmd,
IntPtr.Zero, IntPtr.Zero,
false, flags,
IntPtr.Zero, mcpServerDir,
ref si, out pi);
if (!ok)
{
int err2 = Marshal.GetLastWin32Error();
UnityEngine.Debug.LogError($"[Synaptic] CreateProcess fallback also failed (err={err2}).");
return 0;
}
}
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
SessionState.SetInt(PID_KEY, pi.dwProcessId);
SessionState.SetInt(PORT_KEY, port);
EditorPrefs.SetInt(PID_KEY, pi.dwProcessId);
EditorPrefs.SetInt(PORT_KEY, port);
return pi.dwProcessId;
}
///
/// Check whether the previously-spawned PID is still alive.
/// Used after domain reload to recover the connection rather than
/// re-spawning.
///
public static bool IsStoredProcessAlive(out int pid, out int port)
{
pid = SessionState.GetInt(PID_KEY, 0);
port = SessionState.GetInt(PORT_KEY, 0);
if (pid == 0) { pid = EditorPrefs.GetInt(PID_KEY, 0); port = EditorPrefs.GetInt(PORT_KEY, 0); }
if (pid == 0) return false;
try
{
var p = Process.GetProcessById(pid);
if (p == null || p.HasExited) return false;
// Sanity: must be a node process
string name = p.ProcessName.ToLowerInvariant();
return name.Contains("node");
}
catch
{
return false;
}
}
public static void ClearStoredPid()
{
SessionState.SetInt(PID_KEY, 0);
SessionState.SetInt(PORT_KEY, 0);
EditorPrefs.DeleteKey(PID_KEY);
EditorPrefs.DeleteKey(PORT_KEY);
}
public static bool KillStored()
{
int pid = SessionState.GetInt(PID_KEY, 0);
if (pid == 0) pid = EditorPrefs.GetInt(PID_KEY, 0);
if (pid == 0) return false;
try
{
var p = Process.GetProcessById(pid);
if (!p.HasExited)
{
p.Kill();
p.WaitForExit(3000);
}
return true;
}
catch
{
return false;
}
finally
{
ClearStoredPid();
}
}
}
}