Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
234 lines
8.5 KiB
C#
234 lines
8.5 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Threading;
|
|
|
|
// Launcher da Maquina Comercial Pessoal:
|
|
// 1. localiza a pasta do projeto (procura package.json + scripts/servidor.mjs);
|
|
// 2. se o servidor ja responde em http://127.0.0.1:3910, so abre o navegador;
|
|
// 3. senao, inicia "node scripts/servidor.mjs" escondido (log em data/server.log),
|
|
// espera o /api/health responder (ate 30s) e abre o navegador.
|
|
//
|
|
// Compilavel tanto pelo dotnet SDK (net8.0) quanto pelo csc.exe do
|
|
// .NET Framework 4 (C# 5): sem interpolacao de string, sem APIs novas.
|
|
|
|
namespace MaquinaComercialPessoalLauncher
|
|
{
|
|
internal static class Program
|
|
{
|
|
private static void LoadDotEnv(string root)
|
|
{
|
|
string envPath = Path.Combine(root, ".env");
|
|
if (!File.Exists(envPath)) return;
|
|
foreach (string rawLine in File.ReadAllLines(envPath))
|
|
{
|
|
string line = rawLine.Trim();
|
|
if (line.Length == 0 || line.StartsWith("#")) continue;
|
|
int separator = line.IndexOf('=');
|
|
if (separator <= 0) continue;
|
|
string key = line.Substring(0, separator).Trim();
|
|
string value = line.Substring(separator + 1).Trim();
|
|
if (key.Length > 0 && Environment.GetEnvironmentVariable(key) == null)
|
|
{
|
|
Environment.SetEnvironmentVariable(key, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string BaseUrl(string root)
|
|
{
|
|
LoadDotEnv(root);
|
|
string port = Environment.GetEnvironmentVariable("APP_PORT");
|
|
int parsedPort;
|
|
if (!int.TryParse(port, out parsedPort) || parsedPort < 1 || parsedPort > 65535)
|
|
{
|
|
port = "3910";
|
|
}
|
|
else
|
|
{
|
|
port = parsedPort.ToString();
|
|
}
|
|
return "http://127.0.0.1:" + port;
|
|
}
|
|
|
|
private static int Main()
|
|
{
|
|
Console.Title = "Maquina Comercial Pessoal";
|
|
Console.WriteLine("=== Maquina Comercial Pessoal ===");
|
|
|
|
string root = LocateProjectDir();
|
|
if (root == null)
|
|
{
|
|
return Fail("Pasta do projeto nao encontrada (procurei package.json + scripts/servidor.mjs).\n" +
|
|
"Deixe o Maquina-Comercial-Pessoal.exe dentro da pasta do projeto.");
|
|
}
|
|
|
|
string url = BaseUrl(root);
|
|
|
|
if (IsHealthy(url))
|
|
{
|
|
Console.WriteLine("Servidor ja esta rodando. Abrindo o navegador...");
|
|
OpenBrowser(url);
|
|
return 0;
|
|
}
|
|
|
|
string node = FindNode();
|
|
if (node == null)
|
|
{
|
|
return Fail("Node.js nao encontrado no PATH. Instale em https://nodejs.org (versao 24 ou superior).");
|
|
}
|
|
|
|
Console.WriteLine("Iniciando o servidor (aguarde alguns segundos)...");
|
|
if (!StartServer(node, root))
|
|
{
|
|
return Fail("Nao foi possivel iniciar o servidor. Veja o log em data\\server.log");
|
|
}
|
|
|
|
for (int i = 0; i < 60; i++)
|
|
{
|
|
Thread.Sleep(500);
|
|
if (IsHealthy(url))
|
|
{
|
|
Console.WriteLine("Pronto! Abrindo " + url + " no navegador...");
|
|
OpenBrowser(url);
|
|
Thread.Sleep(1500);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return Fail("O servidor nao respondeu em 30 segundos.\n" +
|
|
"Veja o log em data\\server.log ou rode manualmente: node scripts\\servidor.mjs");
|
|
}
|
|
|
|
private static string LocateProjectDir()
|
|
{
|
|
string[] candidates = new string[]
|
|
{
|
|
AppDomain.CurrentDomain.BaseDirectory,
|
|
Directory.GetCurrentDirectory()
|
|
};
|
|
|
|
foreach (string start in candidates)
|
|
{
|
|
DirectoryInfo dir = new DirectoryInfo(start);
|
|
for (int depth = 0; dir != null && depth < 5; depth++)
|
|
{
|
|
if (File.Exists(Path.Combine(dir.FullName, "package.json")) &&
|
|
File.Exists(Path.Combine(Path.Combine(dir.FullName, "scripts"), "servidor.mjs")))
|
|
{
|
|
return dir.FullName;
|
|
}
|
|
dir = dir.Parent;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static string FindNode()
|
|
{
|
|
try
|
|
{
|
|
ProcessStartInfo psi = new ProcessStartInfo("where.exe", "node");
|
|
psi.UseShellExecute = false;
|
|
psi.RedirectStandardOutput = true;
|
|
psi.CreateNoWindow = true;
|
|
using (Process p = Process.Start(psi))
|
|
{
|
|
string output = p.StandardOutput.ReadToEnd();
|
|
p.WaitForExit(5000);
|
|
if (!string.IsNullOrEmpty(output))
|
|
{
|
|
string first = output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)[0].Trim();
|
|
if (File.Exists(first)) return first;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// continua para o retorno nulo
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static bool StartServer(string node, string root)
|
|
{
|
|
try
|
|
{
|
|
string dataDir = Path.Combine(root, "data");
|
|
Directory.CreateDirectory(dataDir);
|
|
string logPath = Path.Combine(dataDir, "server.log");
|
|
|
|
File.AppendAllText(logPath,
|
|
"---- " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " launcher iniciou o servidor ----\r\n");
|
|
|
|
// O redirecionamento para o log e feito pelo proprio cmd.exe: assim o
|
|
// launcher pode encerrar sem deixar o servidor escrevendo num pipe
|
|
// sem leitor (o que derrubaria o node com EPIPE mais tarde).
|
|
string comando = "/c \"\"" + node + "\" scripts\\servidor.mjs >> \"" + logPath + "\" 2>&1\"";
|
|
ProcessStartInfo psi = new ProcessStartInfo("cmd.exe", comando);
|
|
psi.WorkingDirectory = root;
|
|
psi.UseShellExecute = false;
|
|
psi.CreateNoWindow = true;
|
|
|
|
Process.Start(psi);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Falha ao iniciar: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static bool IsHealthy(string baseUrl)
|
|
{
|
|
try
|
|
{
|
|
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(baseUrl + "/api/health");
|
|
request.Timeout = 1500;
|
|
request.Method = "GET";
|
|
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
|
|
{
|
|
if ((int)response.StatusCode != 200) return false;
|
|
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
|
|
{
|
|
string body = reader.ReadToEnd();
|
|
string compact = body.Replace(" ", "").Replace("\r", "").Replace("\n", "").Replace("\t", "");
|
|
return (int)response.StatusCode == 200 &&
|
|
compact.IndexOf("\"app\":\"maquina-comercial-pessoal\"", StringComparison.Ordinal) >= 0;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void OpenBrowser(string url)
|
|
{
|
|
try
|
|
{
|
|
ProcessStartInfo psi = new ProcessStartInfo(url);
|
|
psi.UseShellExecute = true;
|
|
Process.Start(psi);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
Console.WriteLine("Abra manualmente no navegador: " + url);
|
|
}
|
|
}
|
|
|
|
private static int Fail(string message)
|
|
{
|
|
Console.WriteLine();
|
|
Console.WriteLine("ERRO: " + message);
|
|
Console.WriteLine();
|
|
Console.WriteLine("Pressione qualquer tecla para fechar...");
|
|
try { Console.ReadKey(true); } catch (Exception) { Thread.Sleep(8000); }
|
|
return 1;
|
|
}
|
|
}
|
|
}
|