326 lines
14 KiB
C#
326 lines
14 KiB
C#
using System;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Net;
|
||
using System.Text;
|
||
using System.Threading;
|
||
using System.Windows.Forms;
|
||
using Microsoft.Win32;
|
||
|
||
namespace HermesHub
|
||
{
|
||
public static class WebLauncher
|
||
{
|
||
[STAThread]
|
||
public static void Main(string[] args)
|
||
{
|
||
Application.EnableVisualStyles();
|
||
Application.SetCompatibleTextRenderingDefault(false);
|
||
|
||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||
string hermesHome = Environment.GetEnvironmentVariable("HERMES_HOME");
|
||
if (string.IsNullOrEmpty(hermesHome))
|
||
{
|
||
hermesHome = Path.Combine(localAppData, "hermes");
|
||
}
|
||
|
||
// 1. Determine Web API Host and Port from hub_settings.json or default
|
||
string host = "127.0.0.1";
|
||
int port = 5800;
|
||
string settingsFile = Path.Combine(hermesHome, "hub_settings.json");
|
||
if (File.Exists(settingsFile))
|
||
{
|
||
try
|
||
{
|
||
string json = File.ReadAllText(settingsFile, Encoding.UTF8);
|
||
int pIdx = json.IndexOf("\"web_api_port\":", StringComparison.OrdinalIgnoreCase);
|
||
if (pIdx >= 0)
|
||
{
|
||
int colon = json.IndexOf(':', pIdx);
|
||
int comma = json.IndexOfAny(new char[] { ',', '}', '\r', '\n' }, colon + 1);
|
||
if (colon >= 0 && comma > colon)
|
||
{
|
||
string pStr = json.Substring(colon + 1, comma - colon - 1).Trim();
|
||
int parsedPort;
|
||
if (int.TryParse(pStr, out parsedPort) && parsedPort > 0 && parsedPort < 65536)
|
||
{
|
||
port = parsedPort;
|
||
}
|
||
}
|
||
}
|
||
int hIdx = json.IndexOf("\"web_api_host\":", StringComparison.OrdinalIgnoreCase);
|
||
if (hIdx >= 0)
|
||
{
|
||
int q1 = json.IndexOf('"', hIdx + 15);
|
||
int q2 = json.IndexOf('"', q1 + 1);
|
||
if (q1 >= 0 && q2 > q1)
|
||
{
|
||
string hStr = json.Substring(q1 + 1, q2 - q1 - 1).Trim();
|
||
if (!string.IsNullOrEmpty(hStr) && hStr != "0.0.0.0")
|
||
{
|
||
host = hStr;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
string targetUrl = string.Format("http://{0}:{1}/", host, port);
|
||
string healthUrl = string.Format("http://{0}:{1}/api/health", host, port);
|
||
|
||
// 2. Check if server is already running and healthy
|
||
bool serverWasAlreadyRunning = IsServerHealthy(healthUrl);
|
||
Process serverProcess = null;
|
||
|
||
if (!serverWasAlreadyRunning)
|
||
{
|
||
// Find Python
|
||
string hermesPythonW = Path.Combine(hermesHome, @"hermes-agent\venv\Scripts\pythonw.exe");
|
||
string hermesPython = Path.Combine(hermesHome, @"hermes-agent\venv\Scripts\python.exe");
|
||
string exe = File.Exists(hermesPythonW) ? hermesPythonW : hermesPython;
|
||
|
||
if (!File.Exists(exe))
|
||
{
|
||
MessageBox.Show(
|
||
"Hermes Python not found:\n" + exe + "\n\nPlease install Hermes Agent first.",
|
||
"Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
|
||
string pluginSrc = Path.Combine(hermesHome, @"plugins\antigravity-provider\src");
|
||
string agentDir = Path.Combine(hermesHome, "hermes-agent");
|
||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||
string hubSrc = Path.GetFullPath(Path.Combine(baseDir, @"..\src"));
|
||
|
||
StringBuilder script = new StringBuilder();
|
||
script.AppendLine("import sys");
|
||
if (Directory.Exists(pluginSrc))
|
||
script.AppendLine("sys.path.insert(0, r'" + pluginSrc.Replace('\\', '/') + "')");
|
||
if (Directory.Exists(agentDir))
|
||
script.AppendLine("sys.path.insert(0, r'" + agentDir.Replace('\\', '/') + "')");
|
||
if (Directory.Exists(hubSrc))
|
||
script.AppendLine("sys.path.insert(0, r'" + hubSrc.Replace('\\', '/') + "')");
|
||
script.AppendLine("from antigravity_provider.router.web.server import run_server");
|
||
script.AppendLine("run_server()");
|
||
|
||
string entryScript = Path.Combine(hermesHome, "hermes_hub_web_entry.py");
|
||
try
|
||
{
|
||
File.WriteAllText(entryScript, script.ToString(), Encoding.UTF8);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("Cannot write entry script:\n" + ex.Message, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
|
||
ProcessStartInfo serverPsi = new ProcessStartInfo();
|
||
serverPsi.FileName = exe;
|
||
serverPsi.Arguments = "\"" + entryScript + "\"";
|
||
serverPsi.WorkingDirectory = hermesHome;
|
||
serverPsi.UseShellExecute = false;
|
||
serverPsi.CreateNoWindow = true;
|
||
serverPsi.WindowStyle = ProcessWindowStyle.Hidden;
|
||
|
||
try
|
||
{
|
||
serverProcess = Process.Start(serverPsi);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("Failed to start Hermes Hub web server:\n" + ex.Message, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
|
||
// 3. Poll /api/health with timeout (up to 15s)
|
||
bool ready = false;
|
||
for (int i = 0; i < 75; i++)
|
||
{
|
||
if (IsServerHealthy(healthUrl))
|
||
{
|
||
ready = true;
|
||
break;
|
||
}
|
||
if (serverProcess.HasExited)
|
||
{
|
||
MessageBox.Show("Hermes Hub web server process terminated unexpectedly.", "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
Thread.Sleep(200);
|
||
}
|
||
|
||
if (!ready)
|
||
{
|
||
MessageBox.Show("Hermes Hub web server failed to respond within 15 seconds.", "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
if (serverProcess != null && !serverProcess.HasExited)
|
||
{
|
||
try { serverProcess.Kill(); } catch { }
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 4. Locate browser in strict priority: Edge -> Chrome -> Chromium registry -> Fallback
|
||
string browserPath = FindChromiumBrowser();
|
||
Process browserProc = null;
|
||
|
||
if (!string.IsNullOrEmpty(browserPath))
|
||
{
|
||
ProcessStartInfo browserPsi = new ProcessStartInfo();
|
||
browserPsi.FileName = browserPath;
|
||
browserPsi.Arguments = string.Format("--app=\"{0}\" --window-size=1400,900", targetUrl);
|
||
browserPsi.UseShellExecute = false;
|
||
|
||
try
|
||
{
|
||
browserProc = Process.Start(browserPsi);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show(
|
||
"Не удалось запустить браузер в режиме приложения:\n" + ex.Message + "\n\nОткрываю стандартный браузер.",
|
||
"Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||
Process.Start(targetUrl);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// No Chromium browser found
|
||
MessageBox.Show(
|
||
"Браузер с поддержкой режима приложения (Microsoft Edge или Google Chrome) не найден.\n\n" +
|
||
"Интерфейс будет открыт в стандартном браузере с адресной строкой.",
|
||
"Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||
try
|
||
{
|
||
Process.Start(targetUrl);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
MessageBox.Show("Не удалось открыть браузер: " + ex.Message, "Hermes Hub", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
}
|
||
|
||
// 5. Server Lifecycle:
|
||
// If the server was started by this launcher session and browser is tracked,
|
||
// wait for browser window to close, then gracefully terminate server process.
|
||
if (!serverWasAlreadyRunning && serverProcess != null && !serverProcess.HasExited && browserProc != null)
|
||
{
|
||
try
|
||
{
|
||
browserProc.WaitForExit();
|
||
}
|
||
catch { }
|
||
|
||
try
|
||
{
|
||
if (!serverProcess.HasExited)
|
||
{
|
||
serverProcess.Kill();
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
}
|
||
|
||
public static bool IsServerHealthy(string url)
|
||
{
|
||
try
|
||
{
|
||
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
|
||
req.Timeout = 800;
|
||
req.Method = "GET";
|
||
using (HttpWebResponse resp = (HttpWebResponse)req.GetResponse())
|
||
{
|
||
if (resp.StatusCode == HttpStatusCode.OK)
|
||
{
|
||
using (StreamReader r = new StreamReader(resp.GetResponseStream(), Encoding.UTF8))
|
||
{
|
||
string txt = r.ReadToEnd();
|
||
return txt.Contains("\"ok\":true") || txt.Contains("\"ok\": true");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
return false;
|
||
}
|
||
|
||
public static string FindChromiumBrowser()
|
||
{
|
||
// 1. Microsoft Edge
|
||
string[] edgePaths = new string[]
|
||
{
|
||
@"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||
@"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\Edge\Application\msedge.exe")
|
||
};
|
||
foreach (string p in edgePaths)
|
||
{
|
||
if (File.Exists(p)) return p;
|
||
}
|
||
|
||
string edgeReg = GetAppPathFromRegistry("msedge.exe");
|
||
if (!string.IsNullOrEmpty(edgeReg) && File.Exists(edgeReg)) return edgeReg;
|
||
|
||
// 2. Google Chrome
|
||
string[] chromePaths = new string[]
|
||
{
|
||
@"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||
@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Google\Chrome\Application\chrome.exe")
|
||
};
|
||
foreach (string p in chromePaths)
|
||
{
|
||
if (File.Exists(p)) return p;
|
||
}
|
||
|
||
string chromeReg = GetAppPathFromRegistry("chrome.exe");
|
||
if (!string.IsNullOrEmpty(chromeReg) && File.Exists(chromeReg)) return chromeReg;
|
||
|
||
// 3. Brave / Vivaldi / Chromium
|
||
string[] otherPaths = new string[]
|
||
{
|
||
@"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe",
|
||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"BraveSoftware\Brave-Browser\Application\brave.exe"),
|
||
@"C:\Program Files\Vivaldi\Application\vivaldi.exe",
|
||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Vivaldi\Application\vivaldi.exe")
|
||
};
|
||
foreach (string p in otherPaths)
|
||
{
|
||
if (File.Exists(p)) return p;
|
||
}
|
||
|
||
string braveReg = GetAppPathFromRegistry("brave.exe");
|
||
if (!string.IsNullOrEmpty(braveReg) && File.Exists(braveReg)) return braveReg;
|
||
|
||
return null;
|
||
}
|
||
|
||
private static string GetAppPathFromRegistry(string exeName)
|
||
{
|
||
try
|
||
{
|
||
string key = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\" + exeName;
|
||
using (RegistryKey rk = Registry.LocalMachine.OpenSubKey(key))
|
||
{
|
||
if (rk != null)
|
||
{
|
||
object val = rk.GetValue(null);
|
||
if (val != null) return val.ToString();
|
||
}
|
||
}
|
||
using (RegistryKey rk = Registry.CurrentUser.OpenSubKey(key))
|
||
{
|
||
if (rk != null)
|
||
{
|
||
object val = rk.GetValue(null);
|
||
if (val != null) return val.ToString();
|
||
}
|
||
}
|
||
}
|
||
catch { }
|
||
return null;
|
||
}
|
||
}
|
||
}
|