Files
Khaled Ahmed b715bea03d chore: bump to 0.3.7 with native tray exe
- version 0.3.6 → 0.3.7
- native tray: dotnet WinForms tray (native/tray) built to bin/omp-web-tray.exe (171KB, single-file, no hidden wscript heap)
- install-tray: shortcuts now prefer native exe (bin/omp-web-tray.exe -OpenBrowser/-Startup) with wscript fallback; task creation best-effort
- windows-service: start prefers native exe, isRunning also probes http://127.0.0.1:port/api/app-update fallback
2026-09-02 15:01:21 +03:00

606 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading;
using System.Windows.Forms;
namespace OmpWebTray;
static class Program
{
[STAThread]
static void Main(string[] args)
{
var portArg = 0;
var hostnameArg = "";
var modeArg = "";
var configPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".omp", "agent", "web-service.json");
var startup = false;
var openBrowser = false;
for (int i = 0; i < args.Length; i++)
{
var a = args[i];
if (a == "-Port" || a == "--port" || a == "-p") { if (i + 1 < args.Length && int.TryParse(args[++i], out var p)) portArg = p; }
else if (a == "-Hostname" || a == "--hostname" || a == "-H") { if (i + 1 < args.Length) hostnameArg = args[++i]; }
else if (a == "-Mode" || a == "--mode") { if (i + 1 < args.Length) modeArg = args[++i]; }
else if (a == "-ConfigPath") { if (i + 1 < args.Length) configPath = args[++i]; }
else if (a == "-Startup" || a == "--startup") startup = true;
else if (a == "-OpenBrowser" || a == "--open-browser") openBrowser = true;
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var tray = new TrayApplication(portArg, hostnameArg, modeArg, configPath, startup, openBrowser);
tray.Run();
}
}
class TrayApplication : IDisposable
{
private readonly int _portArg;
private readonly string _hostnameArg;
private readonly string _modeArg;
private readonly string _configPath;
private readonly bool _startup;
private bool _openBrowser;
private readonly string _repoRoot;
private readonly string _pkgVersion;
private int _effectivePort;
private string _effectiveHostname = "127.0.0.1";
private string _effectiveMode = "start";
private bool _effectiveAutoRestart = true;
private string _serverUrl = "http://127.0.0.1:30177";
private NotifyIcon _notifyIcon = null!;
private ContextMenuStrip _contextMenu = null!;
private ToolStripMenuItem _menuHeader = null!;
private ToolStripMenuItem _menuStatus = null!;
private ToolStripMenuItem _menuOpen = null!;
private ToolStripMenuItem _menuCopy = null!;
private ToolStripMenuItem _menuRestart = null!;
private ToolStripMenuItem _menuToggle = null!;
private ToolStripMenuItem _menuViewLogs = null!;
private ToolStripMenuItem _menuConfig = null!;
private ToolStripMenuItem _menuOpenFolder = null!;
private ToolStripMenuItem _menuAutostart = null!;
private ToolStripMenuItem _menuExit = null!;
private System.Windows.Forms.Timer _timer = null!;
private Mutex? _appMutex;
private bool _createdNew;
private Process? _childProcess;
private string _state = "Starting";
private readonly List<DateTime> _crashTimestamps = new();
private bool _isExiting;
private readonly string _logFile;
private readonly object _logLock = new();
private string? _nodeExe;
private readonly string _icoPath;
private readonly string _pngPath;
public TrayApplication(int portArg, string hostnameArg, string modeArg, string configPath, bool startup, bool openBrowser)
{
_portArg = portArg;
_hostnameArg = hostnameArg;
_modeArg = modeArg;
_configPath = configPath;
_startup = startup;
_openBrowser = openBrowser;
_repoRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", ".."));
// When published single-file, BaseDirectory is different; fallback to resolving from assembly location
if (!Directory.Exists(Path.Combine(_repoRoot, "bin")) || !File.Exists(Path.Combine(_repoRoot, "package.json")))
{
// Try 4 levels up (common for win-x64 publish)
var alt = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
if (File.Exists(Path.Combine(alt, "package.json"))) _repoRoot = alt;
else _repoRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", ".."));
if (!Directory.Exists(Path.Combine(_repoRoot, "bin"))) _repoRoot = Directory.GetCurrentDirectory();
// Final fallback: assume exe is in bin/
var binDir = Path.GetDirectoryName(Environment.ProcessPath) ?? "";
if (File.Exists(Path.Combine(binDir, "..", "package.json"))) _repoRoot = Path.GetFullPath(Path.Combine(binDir, ".."));
}
_pkgVersion = "0.0.0";
try
{
var pkgJson = Path.Combine(_repoRoot, "package.json");
if (File.Exists(pkgJson))
{
var json = File.ReadAllText(pkgJson);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("version", out var v)) _pkgVersion = v.GetString() ?? "0.0.0";
}
}
catch { }
_icoPath = Path.Combine(_repoRoot, "public", "omp-web.ico");
_pngPath = Path.Combine(_repoRoot, "public", "icon.png");
var logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".omp", "agent", "logs");
Directory.CreateDirectory(logDir);
_logFile = Path.Combine(logDir, "omp-web-service.log");
try
{
if (File.Exists(_logFile) && new FileInfo(_logFile).Length > 5 * 1024 * 1024)
File.Move(_logFile, Path.Combine(logDir, "omp-web-service.old.log"), true);
}
catch { }
ResolveConfig();
}
private void ResolveConfig()
{
const int DefaultPort = 30177;
const string DefaultHostname = "127.0.0.1";
const string DefaultMode = "start";
JsonElement? config = null;
try
{
if (File.Exists(_configPath))
{
var txt = File.ReadAllText(_configPath);
using var doc = JsonDocument.Parse(txt);
config = doc.RootElement.Clone();
}
}
catch { }
int? cfgPort = null;
string? cfgHost = null;
string? cfgMode = null;
bool? cfgAutoRestart = null;
bool? cfgOpenBrowser = null;
if (config.HasValue)
{
var el = config.Value;
if (el.TryGetProperty("port", out var p) && p.TryGetInt32(out var pi)) cfgPort = pi;
if (el.TryGetProperty("hostname", out var h)) cfgHost = h.GetString();
if (el.TryGetProperty("mode", out var m)) cfgMode = m.GetString();
if (el.TryGetProperty("autoRestart", out var ar) && (ar.ValueKind == JsonValueKind.True || ar.ValueKind == JsonValueKind.False)) cfgAutoRestart = ar.GetBoolean();
if (el.TryGetProperty("openBrowserOnLaunch", out var ob) && (ob.ValueKind == JsonValueKind.True || ob.ValueKind == JsonValueKind.False)) cfgOpenBrowser = ob.GetBoolean();
}
_effectivePort = _portArg > 0 ? _portArg : (cfgPort ?? DefaultPort);
_effectiveHostname = !string.IsNullOrWhiteSpace(_hostnameArg) ? _hostnameArg : (!string.IsNullOrWhiteSpace(cfgHost) ? cfgHost! : DefaultHostname);
_effectiveMode = !string.IsNullOrWhiteSpace(_modeArg) ? _modeArg : (!string.IsNullOrWhiteSpace(cfgMode) ? cfgMode! : DefaultMode);
_effectiveAutoRestart = cfgAutoRestart ?? true;
if (_openBrowser) { /* explicit */ }
else if (!_startup && cfgOpenBrowser == true) _openBrowser = true;
var nextDir = Path.Combine(_repoRoot, ".next");
if (_effectiveMode == "start" && !Directory.Exists(nextDir))
{
_effectiveMode = "dev";
if (_effectivePort == 30177) _effectivePort = 30178;
}
_serverUrl = (_effectiveHostname == "0.0.0.0" || _effectiveHostname == "::" || string.IsNullOrWhiteSpace(_effectiveHostname))
? $"http://localhost:{_effectivePort}"
: $"http://{_effectiveHostname}:{_effectivePort}";
}
private void WriteLog(string message)
{
var ts = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
var line = $"[{ts}] {message}";
lock (_logLock)
{
try { File.AppendAllText(_logFile, line + "\r\n"); } catch { }
}
}
private string FindNodeExecutable()
{
try
{
var where = Environment.GetEnvironmentVariable("PATH") ?? "";
// Try where node
var psi = new ProcessStartInfo("where", "node") { RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true };
using var p = Process.Start(psi);
if (p != null)
{
var outp = p.StandardOutput.ReadToEnd();
p.WaitForExit(2000);
var first = outp.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.Trim();
if (!string.IsNullOrEmpty(first) && File.Exists(first)) return first;
}
}
catch { }
var candidates = new[]
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "nodejs", "node.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nodejs", "node.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "node", "node.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".bun", "bin", "node.exe"),
};
foreach (var c in candidates) if (File.Exists(c)) return c;
return "node.exe";
}
private void StartWebServer()
{
if (_childProcess != null && !_childProcess.HasExited) return;
_state = "Starting";
UpdateTrayUI();
WriteLog($"Starting web server in {_effectiveMode} mode on {_effectiveHostname}:{_effectivePort}...");
if (_nodeExe == null) _nodeExe = FindNodeExecutable();
var psi = new ProcessStartInfo
{
FileName = _nodeExe,
WorkingDirectory = _repoRoot,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
if (_effectiveMode == "start")
{
var launcher = Path.Combine(_repoRoot, "bin", "omp-web.js");
psi.Arguments = $"\"{launcher}\" -p {_effectivePort} -H {_effectiveHostname} --no-open";
}
else
{
var nextBin = Path.Combine(_repoRoot, "node_modules", "next", "dist", "bin", "next");
psi.Arguments = $"\"{nextBin}\" dev -H {_effectiveHostname} -p {_effectivePort}";
}
psi.EnvironmentVariables["OMP_WEB_PORT"] = _effectivePort.ToString();
psi.EnvironmentVariables["OMP_WEB_HOSTNAME"] = _effectiveHostname;
psi.EnvironmentVariables["OMP_WEB_SERVICE"] = "1";
psi.EnvironmentVariables["PORT"] = _effectivePort.ToString();
var proc = new Process { StartInfo = psi, EnableRaisingEvents = true };
proc.OutputDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) WriteLog($"[STDOUT] {e.Data}"); };
proc.ErrorDataReceived += (s, e) => { if (!string.IsNullOrEmpty(e.Data)) WriteLog($"[STDERR] {e.Data}"); };
try
{
if (proc.Start())
{
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
_childProcess = proc;
WriteLog($"Child server process started with PID {proc.Id}");
}
else
{
_state = "Error";
WriteLog("Failed to start child server process.");
UpdateTrayUI();
}
}
catch (Exception ex)
{
_state = "Error";
WriteLog($"Exception starting child server: {ex.Message}");
UpdateTrayUI();
}
}
private void StopWebServer()
{
if (_childProcess != null && !_childProcess.HasExited)
{
var pid = _childProcess.Id;
WriteLog($"Stopping child server process tree (PID {pid})...");
try
{
var taskkill = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "taskkill.exe");
if (!File.Exists(taskkill)) taskkill = "taskkill.exe";
var psi = new ProcessStartInfo(taskkill, $"/PID {pid} /T /F") { CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden };
using var p = Process.Start(psi);
p?.WaitForExit(5000);
}
catch { try { _childProcess.Kill(); } catch { } }
_childProcess = null;
}
_state = "Stopped";
WriteLog("Server stopped.");
UpdateTrayUI();
}
private bool TestServerHealth()
{
try
{
var req = WebRequest.Create(_serverUrl);
req.Timeout = 2000;
using var resp = req.GetResponse();
return true;
}
catch { return false; }
}
private bool CheckAutostart()
{
try
{
var startup = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
var lnk = Path.Combine(startup, "omp-web-tray.lnk");
return File.Exists(lnk);
}
catch { return false; }
}
private void SetAutostart(bool enable)
{
try
{
var startup = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
var lnk = Path.Combine(startup, "omp-web-tray.lnk");
if (enable)
{
// Recreate via install script logic (simplified): copy from install
var ps = Path.Combine(_repoRoot, "scripts", "windows", "install-tray.ps1");
if (File.Exists(ps))
{
var psi = new ProcessStartInfo("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -File \"{ps}\"") { CreateNoWindow = true, UseShellExecute = false };
using var p = Process.Start(psi);
p?.WaitForExit(10000);
}
}
else
{
if (File.Exists(lnk)) File.Delete(lnk);
}
}
catch { }
}
private void UpdateTrayUI()
{
if (_menuStatus == null) return;
var statusText = _state switch
{
"Running" => $"Running ({_effectivePort})",
"Starting" => $"Starting... ({_effectivePort})",
"Stopped" => "Stopped",
"Error" => "Error",
_ => _state
};
_menuStatus.Text = $" Status: {statusText}";
var tip = $"omp-web ({statusText})";
if (tip.Length > 63) tip = tip.Substring(0, 63);
if (_notifyIcon != null) _notifyIcon.Text = tip;
if (_menuToggle != null)
{
if (_state == "Running" || _state == "Starting")
{
_menuToggle.Text = "Stop Server";
if (_menuRestart != null) _menuRestart.Enabled = true;
}
else
{
_menuToggle.Text = "Start Server";
if (_menuRestart != null) _menuRestart.Enabled = false;
}
}
if (_menuAutostart != null) _menuAutostart.Checked = CheckAutostart();
}
public void Run()
{
// Mutex
try
{
_appMutex = new Mutex(true, @"Local\OmpWebTray_Instance_Mutex", out _createdNew);
}
catch { _createdNew = true; }
if (!_createdNew)
{
if (_openBrowser)
{
try { Process.Start(new ProcessStartInfo(_serverUrl) { UseShellExecute = true }); } catch { }
}
return;
}
WriteLog("==========================================");
WriteLog($"omp-web System Tray Manager v{_pkgVersion} starting (native)");
WriteLog($"Repository Root: {_repoRoot}");
WriteLog($"Target: {_serverUrl} (Mode: {_effectiveMode}, Port: {_effectivePort})");
WriteLog("==========================================");
// Icon
Icon? icon = null;
try
{
if (File.Exists(_icoPath)) icon = new Icon(_icoPath);
else if (File.Exists(_pngPath))
{
using var bmp = new Bitmap(_pngPath);
var h = bmp.GetHicon();
icon = Icon.FromHandle(h);
}
}
catch { }
icon ??= SystemIcons.Application;
_contextMenu = new ContextMenuStrip();
_menuHeader = new ToolStripMenuItem($"omp-web (v{_pkgVersion})") { Enabled = false, Font = new Font(SystemFonts.DefaultFont, FontStyle.Bold) };
_contextMenu.Items.Add(_menuHeader);
_menuStatus = new ToolStripMenuItem($" Status: Starting ({_effectivePort})") { Enabled = false };
_contextMenu.Items.Add(_menuStatus);
_contextMenu.Items.Add(new ToolStripSeparator());
_menuOpen = new ToolStripMenuItem("Open in Browser", null, (s, e) => { try { Process.Start(new ProcessStartInfo(_serverUrl) { UseShellExecute = true }); } catch { } }) { Font = new Font(SystemFonts.DefaultFont, FontStyle.Bold) };
_contextMenu.Items.Add(_menuOpen);
_menuCopy = new ToolStripMenuItem("Copy Web URL", null, (s, e) =>
{
try
{
Clipboard.SetText(_serverUrl);
_notifyIcon.ShowBalloonTip(1500, "omp-web", $"URL copied: {_serverUrl}", ToolTipIcon.Info);
}
catch { }
});
_contextMenu.Items.Add(_menuCopy);
_contextMenu.Items.Add(new ToolStripSeparator());
_menuRestart = new ToolStripMenuItem("Restart Server", null, (s, e) =>
{
WriteLog("Restart requested from tray.");
StopWebServer();
Thread.Sleep(500);
StartWebServer();
});
_contextMenu.Items.Add(_menuRestart);
_menuToggle = new ToolStripMenuItem("Stop Server", null, (s, e) =>
{
if (_state == "Running" || _state == "Starting") StopWebServer();
else StartWebServer();
});
_contextMenu.Items.Add(_menuToggle);
_contextMenu.Items.Add(new ToolStripSeparator());
_menuViewLogs = new ToolStripMenuItem("View Logs", null, (s, e) => { try { Process.Start("notepad.exe", $"\"{Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".omp", "agent", "logs", "omp-web-service.log")}\""); } catch { } });
_contextMenu.Items.Add(_menuViewLogs);
_menuConfig = new ToolStripMenuItem("Edit Configuration", null, (s, e) =>
{
var cfg = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".omp", "agent", "web-service.json");
if (!File.Exists(cfg))
{
// create via autostart check
SetAutostart(CheckAutostart());
}
try { Process.Start("notepad.exe", $"\"{cfg}\""); } catch { }
});
_contextMenu.Items.Add(_menuConfig);
_menuOpenFolder = new ToolStripMenuItem("Open Project Folder", null, (s, e) => { try { Process.Start("explorer.exe", $"\"{_repoRoot}\""); } catch { } });
_contextMenu.Items.Add(_menuOpenFolder);
_contextMenu.Items.Add(new ToolStripSeparator());
_menuAutostart = new ToolStripMenuItem("Start with Windows") { CheckOnClick = true, Checked = CheckAutostart() };
_menuAutostart.Click += (s, e) => SetAutostart(_menuAutostart.Checked);
_contextMenu.Items.Add(_menuAutostart);
_contextMenu.Items.Add(new ToolStripSeparator());
_menuExit = new ToolStripMenuItem("Exit Tray & Server", null, (s, e) =>
{
_isExiting = true;
WriteLog("Exit requested from system tray menu.");
StopWebServer();
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
_timer?.Stop();
_timer?.Dispose();
try { _appMutex?.ReleaseMutex(); } catch { }
_appMutex?.Dispose();
Application.Exit();
});
_contextMenu.Items.Add(_menuExit);
_notifyIcon = new NotifyIcon
{
Icon = icon,
ContextMenuStrip = _contextMenu,
Text = $"omp-web ({_state})",
Visible = true
};
_notifyIcon.DoubleClick += (s, e) => { try { Process.Start(new ProcessStartInfo(_serverUrl) { UseShellExecute = true }); } catch { } };
_timer = new System.Windows.Forms.Timer { Interval = 3000 };
_timer.Tick += (s, e) =>
{
if (_isExiting) return;
if (_childProcess != null && _childProcess.HasExited)
{
var code = _childProcess.ExitCode;
WriteLog($"Child server process exited with code {code}.");
_childProcess = null;
if (_effectiveAutoRestart)
{
var now = DateTime.Now;
_crashTimestamps.Add(now);
var cutoff = now.AddSeconds(-60);
_crashTimestamps.RemoveAll(t => t < cutoff);
if (_crashTimestamps.Count >= 3)
{
_state = "Error";
WriteLog("Server crashed repeatedly (3 times in 60s). Auto-restart suspended.");
_notifyIcon.ShowBalloonTip(3000, "omp-web Service Error", "Server crashed repeatedly. Check logs for details.", ToolTipIcon.Error);
}
else
{
WriteLog($"Auto-restarting server (crash {_crashTimestamps.Count} of 3)...");
StartWebServer();
}
}
else _state = "Stopped";
UpdateTrayUI();
return;
}
if (_childProcess != null && !_childProcess.HasExited)
{
var healthy = TestServerHealth();
if (healthy)
{
if (_state != "Running")
{
_state = "Running";
WriteLog($"Server is healthy and responsive at {_serverUrl}");
UpdateTrayUI();
if (_openBrowser)
{
_openBrowser = false;
try { Process.Start(new ProcessStartInfo(_serverUrl) { UseShellExecute = true }); } catch { }
}
}
}
else if (_state == "Running")
{
_state = "Starting";
UpdateTrayUI();
}
}
};
StartWebServer();
_timer.Start();
Application.Run();
// Cleanup on exit
if (!_isExiting)
{
StopWebServer();
_notifyIcon.Visible = false;
_notifyIcon.Dispose();
_timer.Stop();
_timer.Dispose();
try { _appMutex?.ReleaseMutex(); } catch { }
_appMutex?.Dispose();
}
}
public void Dispose()
{
_notifyIcon?.Dispose();
_timer?.Dispose();
_appMutex?.Dispose();
_childProcess?.Dispose();
}
}