using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reflection; using HIKKY.VketCloudSDK.Builder.Components; using HIKKY.VketCloudSDK.Core; using UnityEditor; using UnityEditor.ShortcutManagement; using UnityEngine; using UnityEngine.Events; namespace HIKKY.VketCloudSDK.UI { public class ServerInfo { public Process Process { get; set; } public int Port { get; set; } public string Url { get; set; } } public class LocalServerLauncher { const string MENU_NAME = "VketCloudSDK/Launch Local Server"; //private static readonly string BAT_NAME = "LaunchLocalServer.bat"; private static readonly string EXE_NAME = "LocalServerLauncher.exe"; private static readonly string APP_NAME = "LocalServerLauncher.sh"; private static readonly string TERMINATOR_APP_NAME = "LocalServerTarminator.sh"; private const string VKETCLOUD_SERVER_PORT = "VKETCLOUD_SERVER_PORT"; [SerializeField] private static int _portNumber; public static int portNumber { get { return _portNumber; } set { _portNumber = value; } } private static Process process; private static List _servers = new List(); [LocalizedMenuItem("VketCloudSDK/", "LaunchLocalServer_Title", "", false, 4)] [Shortcut("Vket Cloud SDK/Launch Local Server", KeyCode.L, ShortcutModifiers.Alt)] public static void LaunchLocalServer() { var root = PathManager.GetReleaseFolderPath; var worldSetting = SceneAsset.FindObjectOfType(); if (worldSetting == null) { UnityEngine.Debug.LogWarning("HEOWorldSetting is missing! It is needed at least one in the scene."); } portNumber = PlayerPrefs.GetInt(VKETCLOUD_SERVER_PORT); int availablePort = GetAvailablePort(portNumber, 49513, 65534); if (availablePort == -1) { UnityEngine.Debug.LogWarning("No available port found in the specified range."); return; } portNumber = availablePort; LocalServerLauncher.LaunchLocalServer($"\"http://localhost:{portNumber}/main.html?worldid=" + worldSetting.worldname + $"&avatar=0&private=0&Vary=abc\"", root, portNumber); } public static int GetAvailablePort(int currentPort, int startRange, int endRange) { if (currentPort == 0) { currentPort = 8000; } if (!IsPortInUse(currentPort)) { return currentPort; } for (int port = startRange; port <= endRange; port++) { if (!IsPortInUse(port)) { return port; } } return -1; } // Add this helper function to check if a port is in use public static bool IsPortInUse(int port) { bool isUsed = false; try { if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows) { IPGlobalProperties ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties(); IPEndPoint[] tcpConnInfoArray = ipGlobalProperties.GetActiveTcpListeners(); foreach (IPEndPoint endpoint in tcpConnInfoArray) { if (endpoint.Port == port) { isUsed = true; break; } } } else { using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1); socket.Bind(new IPEndPoint(IPAddress.Any, port)); socket.Listen(1); socket.Close(); } } } catch (SocketException ex) { if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { return true; } else { throw; // Handle other socket exceptions if needed } } return isUsed; } public static void LaunchLocalServer(string url, string datapath, int port) { //CurrentLocalServerProcessShutdown(); //UnityEngine.Debug.Log(url); //UnityEngine.Debug.Log(datapath); process = new Process(); if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows) { if (PlayerPrefs.GetInt("VKETCLOUD_USE_PYTHON_SERVER", 0) == 1) { string pythonPath = "python3"; // Ensure 'python' is in your PATH environment variable string arguments = $"-m http.server {port}"; process.StartInfo.FileName = pythonPath; process.StartInfo.Arguments = arguments; process.StartInfo.CreateNoWindow = false; process.StartInfo.WorkingDirectory = datapath; } else { process.StartInfo.FileName = System.IO.Path.GetFullPath(PathManager.GetServerLauncherFolderPath + "exe/" + EXE_NAME); process.StartInfo.Arguments = "\"" + datapath + "\" \"" + url + "\" \"" + port.ToString() + "\""; process.StartInfo.WindowStyle = ProcessWindowStyle.Normal; process.StartInfo.UseShellExecute = true; } } else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { process.StartInfo.FileName = "sh"; process.StartInfo.Arguments = System.IO.Path.GetFullPath(PathManager.GetServerLauncherFolderPath + "sh/" + APP_NAME) + " " + datapath + " " + url + " " + port.ToString(); process.StartInfo.WindowStyle = ProcessWindowStyle.Normal; process.StartInfo.UseShellExecute = true; process.StartInfo.CreateNoWindow = false; } else { UnityEngine.Debug.LogWarning("VketCloud SDK is not supported for Linux."); } if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows) { if (PlayerPrefs.GetInt("VKETCLOUD_USE_PYTHON_SERVER", 0) == 1) { process.Start(); Application.OpenURL(url); } else { process.Start(); } } else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { process.Start(); } else { UnityEngine.Debug.LogWarning("VketCloud SDK is not supported for Linux."); } _servers.Add(new ServerInfo { Process = process, Port = port, Url = url }); } public static void OpenBrowserByUrl(string url) { try { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } catch (System.Exception ex) { UnityEngine.Debug.LogError($"Failed to open URL: {url}. Exception: {ex.Message}"); } } public static void CurrentLocalServerProcessShutdown() { if (process != null && !process.HasExited) { process.Kill(); } if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { var tempProcesses = Process.GetProcessesByName("Python"); if (tempProcesses.Length > 0) { foreach (var v in tempProcesses) { v.Kill(); } } } } public static IReadOnlyList GetRunningServers() { if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows) { _servers.RemoveAll(s => s.Process.HasExited); return _servers.AsReadOnly(); } else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { _servers.RemoveAll(s => !IsPortInUse(s.Port)); return _servers.AsReadOnly(); } else { UnityEngine.Debug.LogWarning("VketCloud SDK is not supported for Linux."); return _servers.AsReadOnly(); } } public static void ShutdownServerByPort(int port) { if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows) { var server = _servers.Find(s => s.Port == port); if (server != null) { if (!server.Process.HasExited) { server.Process.Kill(); } _servers.Remove(server); } } else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { var server = _servers.Find(s => s.Port == port); if (server != null) { ShutdownServerByShell(port); _servers.Remove(server); } } } /// /// 指定されたポートをshellスクリプトで終了させる(Mac専用) /// /// static void ShutdownServerByShell(int port) { if (SystemInfo.operatingSystemFamily != OperatingSystemFamily.MacOSX) { return; } var startInfo = new ProcessStartInfo { FileName = "sh", Arguments = System.IO.Path.GetFullPath(PathManager.GetServerLauncherFolderPath + "sh/" + TERMINATOR_APP_NAME) + " " + port.ToString(), WindowStyle = ProcessWindowStyle.Normal, UseShellExecute = true, CreateNoWindow = false, }; var stopProcess = Process.Start(startInfo); stopProcess.WaitForExit(); } public static void ShutdownAllServers() { foreach (var server in _servers) { if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows) { if (!server.Process.HasExited) { server.Process.Kill(); } } else if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.MacOSX) { if (server != null) { ShutdownServerByShell(server.Port); } } } _servers.Clear(); } [InitializeOnLoad] public static class EditorApplicationUtility { private const BindingFlags BINDING_ATTR = BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic; private static readonly FieldInfo m_info = typeof(EditorApplication).GetField("editorApplicationQuit", BINDING_ATTR); public static UnityAction editorApplicationQuit { get { return m_info.GetValue(null) as UnityAction; } set { var functions = m_info.GetValue(null) as UnityAction; functions += value; m_info.SetValue(null, functions); } } } [InitializeOnLoadMethod] private static void CallProcessShutdownOnEditorQuit() { EditorApplicationUtility.editorApplicationQuit += () => { CurrentLocalServerProcessShutdown(); }; } } }