276 lines
9.0 KiB
C#
276 lines
9.0 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Manages PID files and handshake state for the local HTTP server.
|
|
/// Handles persistence of server process information across Unity domain reloads.
|
|
/// </summary>
|
|
public class PidFileManager : IPidFileManager
|
|
{
|
|
/// <inheritdoc/>
|
|
public string GetPidDirectory()
|
|
{
|
|
return Path.Combine(GetProjectRootPath(), "Library", "MCPForUnity", "RunState");
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public string GetPidFilePath(int port)
|
|
{
|
|
string dir = GetPidDirectory();
|
|
Directory.CreateDirectory(dir);
|
|
return Path.Combine(dir, $"mcp_http_{port}.pid");
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void DeletePidFile(string pidFilePath)
|
|
{
|
|
try
|
|
{
|
|
if (!string.IsNullOrEmpty(pidFilePath) && File.Exists(pidFilePath))
|
|
{
|
|
File.Delete(pidFilePath);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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 { }
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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 { }
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public string GetStoredArgsHash()
|
|
{
|
|
try
|
|
{
|
|
return EditorPrefs.GetString(EditorPrefKeys.LastLocalHttpServerPidArgsHash, string.Empty);
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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 { }
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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 ".../<Project>/Assets"
|
|
return Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
|
}
|
|
catch
|
|
{
|
|
return Application.dataPath;
|
|
}
|
|
}
|
|
}
|
|
}
|