-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathPortManager.cs
More file actions
353 lines (317 loc) · 12.6 KB
/
PortManager.cs
File metadata and controls
353 lines (317 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using MCPForUnity.Editor.Constants;
using Newtonsoft.Json;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Manages dynamic port allocation and persistent storage for MCP for Unity
/// </summary>
public static class PortManager
{
private static bool IsDebugEnabled()
{
try { return EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); }
catch { return false; }
}
private const int DefaultPort = 6400;
private const int MaxPortAttempts = 100;
private const string RegistryFileName = "unity-mcp-port.json";
[Serializable]
public class PortConfig
{
public int unity_port;
public string created_date;
public string project_path;
public int pid;
}
/// <summary>
/// Get the port to use from storage, or return the default if none has been saved yet.
/// When the stored port is in use by another process (multi-instance scenario),
/// falls back to the default port instead of returning an occupied port.
/// </summary>
/// <returns>Port number to use</returns>
public static int GetPortWithFallback()
{
var storedConfig = GetStoredPortConfig();
if (storedConfig != null &&
storedConfig.unity_port > 0 &&
string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
// Only return the stored port if it's actually available.
// When another instance of the same project is running, the stored
// port will be occupied; falling back to DefaultPort lets Start()'s
// SocketException handler pick a truly free port via DiscoverNewPort().
if (IsPortAvailable(storedConfig.unity_port))
{
return storedConfig.unity_port;
}
if (IsDebugEnabled()) McpLog.Info($"Stored port {storedConfig.unity_port} is occupied, falling back to default");
}
return DefaultPort;
}
/// <summary>
/// Discover and save a new available port (used by Auto-Connect button)
/// </summary>
/// <returns>New available port</returns>
public static int DiscoverNewPort()
{
int newPort = FindAvailablePort();
SavePort(newPort);
if (IsDebugEnabled()) McpLog.Info($"Discovered and saved new port: {newPort}");
return newPort;
}
/// <summary>
/// Persist a user-selected port and return the value actually stored.
/// If <paramref name="port"/> is unavailable, the next available port is chosen instead.
/// </summary>
public static int SetPreferredPort(int port)
{
if (port <= 0)
{
throw new ArgumentOutOfRangeException(nameof(port), "Port must be positive.");
}
if (!IsPortAvailable(port))
{
throw new InvalidOperationException($"Port {port} is already in use.");
}
SavePort(port);
return port;
}
/// <summary>
/// Find an available port starting from the default port
/// </summary>
/// <returns>Available port number</returns>
private static int FindAvailablePort()
{
// Always try default port first
if (IsPortAvailable(DefaultPort))
{
if (IsDebugEnabled()) McpLog.Info($"Using default port {DefaultPort}");
return DefaultPort;
}
if (IsDebugEnabled()) McpLog.Info($"Default port {DefaultPort} is in use, searching for alternative...");
// Search for alternatives
for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++)
{
if (IsPortAvailable(port))
{
if (IsDebugEnabled()) McpLog.Info($"Found available port {port}");
return port;
}
}
throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}");
}
/// <summary>
/// Check if a specific port is available for binding
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port is available</returns>
public static bool IsPortAvailable(int port)
{
try
{
var testListener = new TcpListener(IPAddress.Loopback, port);
// ExclusiveAddressUse prevents SO_REUSEADDR from allowing multiple
// processes (including AssetImportWorkers) to bind the same port.
// The test bind fails when another process already holds the port.
try { testListener.Server.ExclusiveAddressUse = true; } catch { }
testListener.Start();
testListener.Stop();
}
catch (SocketException)
{
return false;
}
return true;
}
/// <summary>
/// Check if a port is currently being used by MCP for Unity
/// This helps avoid unnecessary port changes when Unity itself is using the port
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port appears to be used by MCP for Unity</returns>
public static bool IsPortUsedByMCPForUnity(int port)
{
try
{
// Try to make a quick connection to see if it's an MCP for Unity server
using var client = new TcpClient();
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
if (connectTask.Wait(100)) // 100ms timeout
{
// If connection succeeded, it's likely the MCP for Unity server
return client.Connected;
}
return false;
}
catch
{
return false;
}
}
/// <summary>
/// Wait for a port to become available for a limited amount of time.
/// Used to bridge the gap during domain reload when the old listener
/// hasn't released the socket yet.
/// </summary>
private static bool WaitForPortRelease(int port, int timeoutMs)
{
int waited = 0;
const int step = 100;
while (waited < timeoutMs)
{
if (IsPortAvailable(port))
{
return true;
}
// If the port is in use by an MCP instance, continue waiting briefly
if (!IsPortUsedByMCPForUnity(port))
{
// In use by something else; don't keep waiting
return false;
}
Thread.Sleep(step);
waited += step;
}
return IsPortAvailable(port);
}
/// <summary>
/// Save port to persistent storage
/// </summary>
/// <param name="port">Port to save</param>
private static void SavePort(int port)
{
try
{
int pid = 0;
try { pid = System.Diagnostics.Process.GetCurrentProcess().Id; } catch { }
var portConfig = new PortConfig
{
unity_port = port,
created_date = DateTime.UtcNow.ToString("O"),
project_path = Application.dataPath,
pid = pid
};
string registryDir = GetRegistryDirectory();
Directory.CreateDirectory(registryDir);
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
byte[] utf8Bytes = new System.Text.UTF8Encoding(false).GetBytes(json);
// Write to hashed, project-scoped file
string registryFile = GetRegistryFilePath();
File.WriteAllBytes(registryFile, utf8Bytes);
// Also write to legacy stable filename to avoid hash/case drift across reloads
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
File.WriteAllBytes(legacy, utf8Bytes);
// Write instance-scoped file so multiple instances of the same project
// can be tracked independently.
if (pid > 0)
{
string hash = ComputeProjectHash(Application.dataPath);
string instanceFile = Path.Combine(registryDir, $"unity-mcp-port-{hash}-{pid}.json");
File.WriteAllBytes(instanceFile, utf8Bytes);
}
if (IsDebugEnabled()) McpLog.Info($"Saved port {port} to storage");
}
catch (Exception ex)
{
McpLog.Warn($"Could not save port to storage: {ex.Message}");
}
}
/// <summary>
/// Load port from persistent storage
/// </summary>
/// <returns>Stored port number, or 0 if not found</returns>
private static int LoadStoredPort()
{
try
{
string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
// Backwards compatibility: try the legacy file name
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
if (!File.Exists(legacy))
{
return 0;
}
registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
var portConfig = JsonConvert.DeserializeObject<PortConfig>(json);
return portConfig?.unity_port ?? 0;
}
catch (Exception ex)
{
McpLog.Warn($"Could not load port from storage: {ex.Message}");
return 0;
}
}
/// <summary>
/// Get the current stored port configuration
/// </summary>
/// <returns>Port configuration if exists, null otherwise</returns>
public static PortConfig GetStoredPortConfig()
{
try
{
string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
// Backwards compatibility: try the legacy file
string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
if (!File.Exists(legacy))
{
return null;
}
registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
return JsonConvert.DeserializeObject<PortConfig>(json);
}
catch (Exception ex)
{
McpLog.Warn($"Could not load port config: {ex.Message}");
return null;
}
}
private static string GetRegistryDirectory()
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
}
private static string GetRegistryFilePath()
{
string dir = GetRegistryDirectory();
string hash = ComputeProjectHash(Application.dataPath);
string fileName = $"unity-mcp-port-{hash}.json";
return Path.Combine(dir, fileName);
}
private static string ComputeProjectHash(string input)
{
try
{
using SHA1 sha1 = SHA1.Create();
byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
byte[] hashBytes = sha1.ComputeHash(bytes);
var sb = new StringBuilder();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
return sb.ToString()[..8]; // short, sufficient for filenames
}
catch
{
return "default";
}
}
}
}