2026-06-06 20:12:40 +07:00
parent de84b2bf48
commit 97ac0f71f5
13682 changed files with 1125938 additions and 0 deletions
@@ -0,0 +1,420 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using SynapticAIPro;
using UnityEditor;
using Newtonsoft.Json;
namespace SynapticPro
{
/// <summary>
/// Port management system per project
/// Assigns and manages unique ports for each Unity project
/// </summary>
[InitializeOnLoad]
public static class NexusProjectPortManager
{
private static string projectId;
private static int assignedPort = -1;
private static readonly string MAPPING_FILE_PATH;
// Project-port mapping information
[Serializable]
public class ProjectPortMapping
{
public Dictionary<string, ProjectInfo> projects = new Dictionary<string, ProjectInfo>();
}
[Serializable]
public class ProjectInfo
{
public string projectName;
public string projectPath;
public int port;
public DateTime lastUpdated;
public bool isActive;
}
static NexusProjectPortManager()
{
// Mapping file path (saved in user home directory)
string homeDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
MAPPING_FILE_PATH = Path.Combine(homeDir, ".config", "nexus", "project_port_mapping.json");
// Initialize
Initialize();
}
private static void Initialize()
{
// Generate project ID (hash value of project path)
string projectPath = Application.dataPath;
projectId = GetProjectId(projectPath);
SynLog.Info($"[Nexus Port Manager] Project ID: {projectId}");
SynLog.Info($"[Nexus Port Manager] Project Path: {projectPath}");
// Assign port
AssignPort();
// Update status periodically
EditorApplication.update += UpdateProjectStatus;
EditorApplication.quitting += OnEditorQuitting;
}
/// <summary>
/// Generate project ID
/// </summary>
private static string GetProjectId(string projectPath)
{
// Generate unique ID from project path
using (var md5 = System.Security.Cryptography.MD5.Create())
{
byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(projectPath);
byte[] hashBytes = md5.ComputeHash(inputBytes);
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant().Substring(0, 8);
}
}
/// <summary>
/// Assign port
/// </summary>
private static void AssignPort()
{
var mapping = LoadMapping();
// Check existing port assignment
if (mapping.projects.ContainsKey(projectId))
{
var info = mapping.projects[projectId];
assignedPort = info.port;
info.lastUpdated = DateTime.Now;
info.isActive = true;
info.projectName = PlayerSettings.productName;
SynLog.Info($"[Nexus Port Manager] Using existing port: {assignedPort}");
}
else
{
// Assign new port
assignedPort = FindAvailablePort(mapping);
mapping.projects[projectId] = new ProjectInfo
{
projectName = PlayerSettings.productName,
projectPath = Application.dataPath,
port = assignedPort,
lastUpdated = DateTime.Now,
isActive = true
};
SynLog.Info($"[Nexus Port Manager] Assigned new port: {assignedPort}");
}
SaveMapping(mapping);
// Notify port to NexusEditorMCPService
UpdateMCPServicePort();
}
/// <summary>
/// Find available port
/// </summary>
private static int FindAvailablePort(ProjectPortMapping mapping)
{
int[] candidatePorts = { 8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099 };
foreach (int port in candidatePorts)
{
bool isUsed = false;
foreach (var project in mapping.projects.Values)
{
if (project.port == port && project.isActive)
{
isUsed = true;
break;
}
}
if (!isUsed)
{
return port;
}
}
// If all in use, search sequentially from 8090
return 8090 + mapping.projects.Count;
}
/// <summary>
/// Set port for MCP service
/// </summary>
private static void UpdateMCPServicePort()
{
if (assignedPort > 0)
{
// Check actual port of current MCP server
string currentUrl = NexusEditorMCPService.GetServerUrl();
if (!string.IsNullOrEmpty(currentUrl) && currentUrl.Contains("localhost:"))
{
// Get actually running port
int startIndex = currentUrl.IndexOf("localhost:") + "localhost:".Length;
int endIndex = currentUrl.IndexOf("/", startIndex);
if (endIndex == -1) endIndex = currentUrl.Length;
if (int.TryParse(currentUrl.Substring(startIndex, endIndex - startIndex), out int actualPort))
{
// If actual port differs from assigned port, prioritize actual port
if (actualPort != assignedPort && IsPortInUse(actualPort))
{
SynLog.Info($"[Nexus Port Manager] MCP Server is actually running on port {actualPort}, not {assignedPort}. Keeping actual port.");
assignedPort = actualPort;
// Update mapping
var mapping = LoadMapping();
if (mapping.projects.ContainsKey(projectId))
{
mapping.projects[projectId].port = actualPort;
SaveMapping(mapping);
}
return;
}
}
}
// Only use saved port if actual server not found
string serverUrl = $"ws://localhost:{assignedPort}";
NexusEditorMCPService.SetServerUrl(serverUrl);
SynLog.Info($"[Nexus Port Manager] Updated MCP Service URL: {serverUrl}");
}
}
/// <summary>
/// Check if specified port is in use
/// </summary>
private static bool IsPortInUse(int port)
{
try
{
using (var tcpClient = new System.Net.Sockets.TcpClient())
{
var result = tcpClient.BeginConnect("localhost", port, null, null);
bool success = result.AsyncWaitHandle.WaitOne(100);
if (success)
{
tcpClient.EndConnect(result);
tcpClient.Close();
return true;
}
return false;
}
}
catch
{
return false;
}
}
/// <summary>
/// Update project status
/// </summary>
private static void UpdateProjectStatus()
{
// Update status every 5 minutes
if (EditorApplication.timeSinceStartup % 300 < 1)
{
var mapping = LoadMapping();
if (mapping.projects.ContainsKey(projectId))
{
mapping.projects[projectId].lastUpdated = DateTime.Now;
mapping.projects[projectId].isActive = true;
SaveMapping(mapping);
}
}
}
/// <summary>
/// Processing on editor exit
/// </summary>
private static void OnEditorQuitting()
{
var mapping = LoadMapping();
if (mapping.projects.ContainsKey(projectId))
{
mapping.projects[projectId].isActive = false;
SaveMapping(mapping);
}
EditorApplication.update -= UpdateProjectStatus;
}
/// <summary>
/// Load mapping information
/// </summary>
private static ProjectPortMapping LoadMapping()
{
try
{
if (File.Exists(MAPPING_FILE_PATH))
{
string json = File.ReadAllText(MAPPING_FILE_PATH);
// Validate and clean JSON string
json = json.Trim();
if (string.IsNullOrEmpty(json))
{
SynLog.Warn("[Nexus Port Manager] Mapping file is empty, creating new mapping.");
return new ProjectPortMapping();
}
// Check JSON syntax
if (!json.StartsWith("{") || !json.EndsWith("}"))
{
Debug.LogError($"[Nexus Port Manager] Invalid JSON format in mapping file. Content: {json.Substring(0, Math.Min(100, json.Length))}...");
return new ProjectPortMapping();
}
var result = JsonConvert.DeserializeObject<ProjectPortMapping>(json);
return result ?? new ProjectPortMapping();
}
}
catch (JsonException jsonEx)
{
Debug.LogError($"[Nexus Port Manager] JSON parsing error: {jsonEx.Message}");
Debug.LogError($"[Nexus Port Manager] Recreating mapping file due to corruption.");
// Move-aside corrupted file, then write a fresh empty mapping so
// the next LoadMapping doesn't hit the same corrupt content and
// spam the console at frame rate.
// Bugs the previous implementation had:
// 1. `File.Move` to an existing `.backup` throws on Windows,
// the silent catch left the corrupt file in place → loop.
// 2. Even on success, no fresh file was written, so any later
// reader before a SaveMapping run kept hitting the error
// (relevant when MAPPING_FILE_PATH still existed under
// filesystem latency on slow disks).
try
{
string backupPath = MAPPING_FILE_PATH + ".backup";
if (File.Exists(backupPath))
{
try { File.Delete(backupPath); } catch { }
}
if (File.Exists(MAPPING_FILE_PATH))
{
try { File.Move(MAPPING_FILE_PATH, backupPath); }
catch
{
// Move failed (permissions, fs lock) — delete the
// corrupt source outright so we don't loop.
try { File.Delete(MAPPING_FILE_PATH); } catch { }
}
}
// Immediately materialize a clean mapping file so concurrent
// readers (and the next Update tick) see valid JSON.
var fresh = new ProjectPortMapping();
var freshJson = JsonConvert.SerializeObject(fresh, Formatting.Indented);
File.WriteAllText(MAPPING_FILE_PATH, freshJson);
SynLog.Info($"[Nexus Port Manager] Corrupted file backed up to: {backupPath}");
return fresh;
}
catch (Exception writeErr)
{
Debug.LogError($"[Nexus Port Manager] Failed to reset mapping file: {writeErr.Message}");
return new ProjectPortMapping();
}
}
catch (Exception e)
{
Debug.LogError($"[Nexus Port Manager] Failed to load mapping: {e.Message}");
}
return new ProjectPortMapping();
}
/// <summary>
/// Save mapping information
/// </summary>
private static void SaveMapping(ProjectPortMapping mapping)
{
try
{
// Clean up old entries (not updated for more than 30 days)
var keysToRemove = new List<string>();
foreach (var kvp in mapping.projects)
{
if ((DateTime.Now - kvp.Value.lastUpdated).TotalDays > 30)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (var key in keysToRemove)
{
mapping.projects.Remove(key);
}
// Create directory
string directory = Path.GetDirectoryName(MAPPING_FILE_PATH);
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Save
string json = JsonConvert.SerializeObject(mapping, Formatting.Indented);
File.WriteAllText(MAPPING_FILE_PATH, json);
}
catch (Exception e)
{
Debug.LogError($"[Nexus Port Manager] Failed to save mapping: {e.Message}");
}
}
/// <summary>
/// Get port for current project
/// </summary>
public static int GetAssignedPort()
{
return assignedPort;
}
/// <summary>
/// Get current project ID
/// </summary>
public static string GetProjectId()
{
return projectId;
}
/// <summary>
/// Display mapping information (for debugging)
/// </summary>
[MenuItem("Tools/Synaptic Pro/Show Port Mapping")]
public static void ShowPortMapping()
{
var mapping = LoadMapping();
System.Text.StringBuilder info = new System.Text.StringBuilder();
info.AppendLine("🔌 Nexus Project Port Mapping");
info.AppendLine("================================");
info.AppendLine($"Current Project ID: {projectId}");
info.AppendLine($"Assigned Port: {assignedPort}");
info.AppendLine("\nAll Projects:");
foreach (var kvp in mapping.projects)
{
var project = kvp.Value;
info.AppendLine($"\n📁 {project.projectName}");
info.AppendLine($" ID: {kvp.Key}");
info.AppendLine($" Port: {project.port}");
info.AppendLine($" Path: {project.projectPath}");
info.AppendLine($" Active: {project.isActive}");
info.AppendLine($" Last Updated: {project.lastUpdated}");
}
SynLog.Info(info.ToString());
EditorUtility.DisplayDialog("Port Mapping", info.ToString(), "OK");
}
}
}