using System; using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using MCPForUnity.Editor.Constants; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Services.Server { /// /// Manages PID files and handshake state for the local HTTP server. /// Handles persistence of server process information across Unity domain reloads. /// public class PidFileManager : IPidFileManager { /// public string GetPidDirectory() { return Path.Combine(GetProjectRootPath(), "Library", "MCPForUnity", "RunState"); } /// public string GetPidFilePath(int port) { string dir = GetPidDirectory(); Directory.CreateDirectory(dir); return Path.Combine(dir, $"mcp_http_{port}.pid"); } /// public bool TryReadPid(string pidFilePath, out int pid) { pid = 0; try { if (string.IsNullOrEmpty(pidFilePath) || !File.Exists(pidFilePath)) { return false; } string text = File.ReadAllText(pidFilePath).Trim(); if (int.TryParse(text, out pid)) { return pid > 0; } // Best-effort: tolerate accidental extra whitespace/newlines. var firstLine = text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); if (int.TryParse(firstLine, out pid)) { return pid > 0; } pid = 0; return false; } catch { pid = 0; return false; } } /// public bool TryGetPortFromPidFilePath(string pidFilePath, out int port) { port = 0; if (string.IsNullOrEmpty(pidFilePath)) { return false; } try { string fileName = Path.GetFileNameWithoutExtension(pidFilePath); if (string.IsNullOrEmpty(fileName)) { return false; } const string prefix = "mcp_http_"; if (!fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return false; } string portText = fileName.Substring(prefix.Length); return int.TryParse(portText, out port) && port > 0; } catch { port = 0; return false; } } /// public void DeletePidFile(string pidFilePath) { try { if (!string.IsNullOrEmpty(pidFilePath) && File.Exists(pidFilePath)) { File.Delete(pidFilePath); } } catch { } } /// public void StoreHandshake(string pidFilePath, string instanceToken) { try { if (!string.IsNullOrEmpty(pidFilePath)) { EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerPidFilePath, pidFilePath); } } catch { } try { if (!string.IsNullOrEmpty(instanceToken)) { EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerInstanceToken, instanceToken); } } catch { } } /// public bool TryGetHandshake(out string pidFilePath, out string instanceToken) { pidFilePath = null; instanceToken = null; try { pidFilePath = EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerPidFilePath, string.Empty); instanceToken = EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerInstanceToken, string.Empty); if (string.IsNullOrEmpty(pidFilePath) || string.IsNullOrEmpty(instanceToken)) { pidFilePath = null; instanceToken = null; return false; } return true; } catch { pidFilePath = null; instanceToken = null; return false; } } /// public void StoreTracking(int pid, int port, string argsHash = null) { try { EditorPrefs.SetInt(EditorPrefKeys.LastLocalHttpServerPid, pid); } catch { } try { EditorPrefs.SetInt(EditorPrefKeys.LastLocalHttpServerPort, port); } catch { } try { EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerStartedUtc, DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture)); } catch { } try { if (!string.IsNullOrEmpty(argsHash)) { EditorPrefs.SetString(EditorPrefKeys.LastLocalHttpServerPidArgsHash, argsHash); } else { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidArgsHash); } } catch { } } /// public bool TryGetStoredPid(int expectedPort, out int pid) { pid = 0; try { int storedPid = EditorPrefs.GetInt(EditorPrefKeys.LastLocalHttpServerPid, 0); int storedPort = EditorPrefs.GetInt(EditorPrefKeys.LastLocalHttpServerPort, 0); string storedUtc = EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerStartedUtc, string.Empty); if (storedPid <= 0 || storedPort != expectedPort) { return false; } // Only trust the stored PID for a short window to avoid PID reuse issues. // (We still verify the PID is listening on the expected port before killing.) if (!string.IsNullOrEmpty(storedUtc) && DateTime.TryParse(storedUtc, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var startedAt)) { if ((DateTime.UtcNow - startedAt) > TimeSpan.FromHours(6)) { return false; } } pid = storedPid; return true; } catch { return false; } } /// public string GetStoredArgsHash() { try { return EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerPidArgsHash, string.Empty); } catch { return string.Empty; } } /// public void ClearTracking() { try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPid); } catch { } try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPort); } catch { } try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerStartedUtc); } catch { } try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidArgsHash); } catch { } try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerPidFilePath); } catch { } try { EditorPrefs.DeleteKey(EditorPrefKeys.LastLocalHttpServerInstanceToken); } catch { } } /// public string ComputeShortHash(string input) { if (string.IsNullOrEmpty(input)) return string.Empty; try { using var sha = SHA256.Create(); byte[] bytes = Encoding.UTF8.GetBytes(input); byte[] hash = sha.ComputeHash(bytes); // 8 bytes => 16 hex chars is plenty as a stable fingerprint for our purposes. var sb = new StringBuilder(16); for (int i = 0; i < 8 && i < hash.Length; i++) { sb.Append(hash[i].ToString("x2")); } return sb.ToString(); } catch { return string.Empty; } } private static string GetProjectRootPath() { try { // Application.dataPath is "...//Assets" return Path.GetFullPath(Path.Combine(Application.dataPath, "..")); } catch { return Application.dataPath; } } } }