using System;
using System.IO;
using System.Text;
using UnityEngine;
using Newtonsoft.Json;
namespace UnityEditorToolkit.Server
{
///
/// Manages server status file for Unity WebSocket Server
///
/// Stores current server state in .unity-websocket/server-status.json
/// allowing CLI tools to discover the correct port and server state.
///
[Serializable]
public class ServerStatus
{
// Constants
private const int HeartbeatStaleSeconds = 30;
public string version = "1.0";
public int port;
public bool isRunning;
public int pid;
public string editorVersion;
public string startedAt;
public string lastHeartbeat;
///
/// Get server status file path
///
public static string GetStatusFilePath(string projectRoot)
{
string statusDir = Path.Combine(projectRoot, ".unity-websocket");
return Path.Combine(statusDir, "server-status.json");
}
///
/// Create new server status
///
public static ServerStatus Create(int port)
{
return new ServerStatus
{
version = "1.0",
port = port,
isRunning = true,
pid = System.Diagnostics.Process.GetCurrentProcess().Id,
editorVersion = Application.unityVersion,
startedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"),
lastHeartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
};
}
///
/// Save server status to file (atomic write)
///
public static bool Save(ServerStatus status, string projectRoot)
{
string tempPath = null;
try
{
string statusDir = Path.Combine(projectRoot, ".unity-websocket");
if (!Directory.Exists(statusDir))
{
Directory.CreateDirectory(statusDir);
}
string statusPath = GetStatusFilePath(projectRoot);
tempPath = statusPath + ".tmp";
// Serialize to JSON
string json = JsonConvert.SerializeObject(status, Formatting.Indented);
// Write to temp file first
File.WriteAllText(tempPath, json, Encoding.UTF8);
// Atomic replace using File.Replace (crash-safe)
if (File.Exists(statusPath))
{
// File.Replace is atomic - no data loss even if crash occurs
File.Replace(tempPath, statusPath, null);
}
else
{
// First time, just move
File.Move(tempPath, statusPath);
}
tempPath = null; // Successfully handled, no cleanup needed
return true;
}
catch (Exception e)
{
Debug.LogError($"Unity Editor Toolkit: Failed to save server status: {e.Message}");
return false;
}
finally
{
// Cleanup temp file if it still exists (error case)
if (tempPath != null && File.Exists(tempPath))
{
try
{
File.Delete(tempPath);
}
catch (Exception cleanupEx)
{
Debug.LogWarning($"Unity Editor Toolkit: Failed to cleanup temp file: {cleanupEx.Message}");
}
}
}
}
///
/// Load server status from file
///
public static ServerStatus Load(string projectRoot)
{
try
{
string statusPath = GetStatusFilePath(projectRoot);
if (!File.Exists(statusPath))
{
return null;
}
string json = File.ReadAllText(statusPath, Encoding.UTF8);
return JsonConvert.DeserializeObject(json);
}
catch (Exception e)
{
Debug.LogWarning($"Unity Editor Toolkit: Failed to load server status: {e.Message}");
return null;
}
}
///
/// Update heartbeat timestamp
///
public void UpdateHeartbeat()
{
lastHeartbeat = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
}
///
/// Mark server as stopped
///
public static bool MarkStopped(string projectRoot)
{
try
{
ServerStatus status = Load(projectRoot);
if (status == null)
{
return true; // Already no status file
}
status.isRunning = false;
status.UpdateHeartbeat();
return Save(status, projectRoot);
}
catch (Exception e)
{
Debug.LogError($"Unity Editor Toolkit: Failed to mark server as stopped: {e.Message}");
return false;
}
}
///
/// Check if status is stale (heartbeat > configured seconds old)
///
public bool IsStale()
{
try
{
if (string.IsNullOrEmpty(lastHeartbeat))
{
return true;
}
DateTime lastBeat = DateTime.Parse(lastHeartbeat);
double secondsSinceLastBeat = (DateTime.UtcNow - lastBeat).TotalSeconds;
return secondsSinceLastBeat > HeartbeatStaleSeconds;
}
catch
{
return true; // If we can't parse, assume stale
}
}
}
}