// INSTALADOR DE UN PROGRAMA DE GUSTITO XPRESS. // // Escrito en C# y compilado con el compilador que ya trae Windows: pesa // kilobytes, no los 90 MB de un programa empaquetado con su propio motor. Un // instalador que pesa lo mismo que lo que instala no sirve de nada. // // ⚠ CADA INSTALADOR TRAE UN SOLO PROGRAMA (se fija al compilar). La Estacion // Fiscal es el sistema de facturacion, sujeto a homologacion ante el SENIAT; // el Cerebrito es gestion del negocio y no emite documentos fiscales. // Entregarlos juntos difuminaria esa frontera y ademas encerraria a la // Estacion dentro de Gustito, cuando le sirve a cualquier comercio. // // Que hace: baja del canal publicado la ultima version, comprueba que llego // completa, la deja en su carpeta y la enciende. De los accesos del escritorio // y del arranque con Windows se encarga el propio programa: aqui no se toca el // registro. using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; using System.Net; using System.Text.RegularExpressions; using System.Windows.Forms; static class Marca { public static readonly Color Fondo = ColorTranslator.FromHtml("#0B0D14"); public static readonly Color Panel = ColorTranslator.FromHtml("#131624"); public static readonly Color Panel2 = ColorTranslator.FromHtml("#1A1E30"); public static readonly Color Borde = ColorTranslator.FromHtml("#2A2F4A"); public static readonly Color Texto = Color.White; public static readonly Color Texto2 = ColorTranslator.FromHtml("#D6DAEA"); public static readonly Color Oro = ColorTranslator.FromHtml("#FBBF24"); public static readonly Color Verde = ColorTranslator.FromHtml("#34D399"); public static readonly Color Rojo = ColorTranslator.FromHtml("#F87171"); } class Producto { public string Nombre, Lema, Sigla, Resumen, Despues, Carpeta, Exe, Canal; public string[] Incluye; } static class Catalogo { public static Producto Actual() { #if PRODUCTO_CEREBRITO return new Producto { Nombre = "Cerebrito", Lema = "La cocina y el salón, en orden.", Sigla = "CG", Resumen = "El programa que corre en la laptop del local: imprime la comanda de cada puesto y levanta el punto de venta de las tablets del salón.", Incluye = new string[] { "Comandas impresas en la impresora de cada puesto", "Punto de venta para las tablets del salón, por la red del local", "Sigue trabajando sin internet y se pone al día solo" }, Despues = "Al abrirlo entrarás con la cuenta del negocio y elegirás sus impresoras.", Carpeta = "Cerebrito", Exe = "Cerebrito-Gustito.exe", Canal = "cerebrito" }; #else return new Producto { Nombre = "Estación Fiscal", Lema = "Cada venta, sellada y en regla.", Sigla = "EF", Resumen = "El sistema de facturación del negocio: arma el documento con su desglose de impuestos, lo asienta en un libro inalterable con numeración consecutiva y lo transmite a la imprenta digital autorizada.", Incluye = new string[] { "Libro fiscal inalterable, con numeración única y consecutiva", "Transmisión inmediata y entrega de la factura al cliente", "Consulta de solo lectura para el SENIAT, con su propia clave", "Respaldo cifrado fuera del equipo y contingencia con talonario" }, Despues = "Al abrirla por primera vez le pondrás la clave del dueño y cargarás los datos del negocio.", Carpeta = "EstacionFiscal", Exe = "Estacion-Fiscal.exe", Canal = "estacion" }; #endif } } /// La mecanica, aparte de la ventana para poder probarla desde la consola. static class Trabajo { public static string BaseCanal = Environment.GetEnvironmentVariable("GX_CANAL") ?? "https://img.gustitoxpress.app"; public static string Destino = Environment.GetEnvironmentVariable("GX_DESTINO") ?? "C:\\Gustito"; public static string Carpeta(Producto p) { return Path.Combine(Destino, p.Carpeta); } public static string RutaExe(Producto p) { return Path.Combine(Carpeta(p), p.Exe); } public static bool YaInstalado(Producto p) { return File.Exists(RutaExe(p)); } /// Lo que publica hoy el canal. El archivo trae dos campos: se leen sin /// arrastrar una libreria de JSON para no engordar el instalador. public static bool Publicado(Producto p, out string version, out string url) { version = null; url = null; try { ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // TLS 1.2 using (var cliente = new WebClient()) { cliente.Headers.Add("cache-control", "no-cache"); string crudo = cliente.DownloadString(BaseCanal + "/" + p.Canal + "/version.json"); var mv = Regex.Match(crudo, "\"version\"\\s*:\\s*\"([^\"]+)\""); var mu = Regex.Match(crudo, "\"url\"\\s*:\\s*\"([^\"]+)\""); if (!mv.Success || !mu.Success) return false; version = mv.Groups[1].Value; url = mu.Groups[1].Value; return true; } } catch { return false; } } /// Baja el programa. Se escribe en un archivo aparte y se renombra al final: /// si el internet del local se corta, no queda un programa a medias /// haciendose pasar por el bueno. public static void Descargar(Producto p, string url, Action avance) { Directory.CreateDirectory(Carpeta(p)); string destino = RutaExe(p), temporal = destino + ".bajando"; try { ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; var pedido = (HttpWebRequest)WebRequest.Create(url); pedido.Timeout = 60000; using (var respuesta = (HttpWebResponse)pedido.GetResponse()) using (var entrada = respuesta.GetResponseStream()) using (var salida = new FileStream(temporal, FileMode.Create, FileAccess.Write)) { long esperado = respuesta.ContentLength, leidos = 0; byte[] buf = new byte[131072]; int n; while ((n = entrada.Read(buf, 0, buf.Length)) > 0) { salida.Write(buf, 0, n); leidos += n; if (esperado > 0 && avance != null) avance((int)Math.Min(99, leidos * 100 / esperado)); } salida.Flush(); if (esperado > 0 && leidos != esperado) throw new Exception("La descarga quedó incompleta (" + leidos + " de " + esperado + " bytes)."); } long real = new FileInfo(temporal).Length; if (real < 10000000) throw new Exception("Lo descargado no parece un programa completo."); if (File.Exists(destino)) File.Delete(destino); File.Move(temporal, destino); if (avance != null) avance(100); } catch (Exception e) { // Nunca se deja un archivo a medias en la carpeta del negocio. try { if (File.Exists(temporal)) File.Delete(temporal); } catch { } string dicho = e.Message; if (!dicho.StartsWith("La descarga") && !dicho.StartsWith("Lo descargado")) dicho = "La descarga quedó incompleta."; throw new Exception(dicho + " Revisa la conexión a internet e intenta de nuevo."); } } /// Lo enciende. De sus accesos y su arranque con Windows se encarga el. public static void Arrancar(Producto p) { var inicio = new ProcessStartInfo(RutaExe(p)); inicio.WorkingDirectory = Carpeta(p); inicio.UseShellExecute = true; Process.Start(inicio); } } class Ventana : Form { readonly Producto p; Label titulo, estado, versionDicha; Button accion, cerrar; Panel barra, relleno; public Ventana(Producto producto) { p = producto; Text = "Instalar " + p.Nombre; ClientSize = new Size(980, 620); StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; BackColor = Marca.Fondo; Font = new Font("Segoe UI", 9.75f); try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch { } Armar(); } static Label Texto(string t, Font f, Color c, int x, int y, int ancho, int alto) { var l = new Label(); l.Text = t; l.Font = f; l.ForeColor = c; l.Location = new Point(x, y); l.Size = new Size(ancho, alto); l.BackColor = Color.Transparent; return l; } void Armar() { // --- Lado de la marca (izquierda) --- var marca = new Panel(); marca.Bounds = new Rectangle(0, 0, 470, 620); marca.BackColor = Marca.Fondo; marca.Paint += delegate(object s, PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; using (var brocha = new LinearGradientBrush(new Rectangle(176, 170, 118, 118), Marca.Oro, ColorTranslator.FromHtml("#B45309"), 55f)) using (var camino = new GraphicsPath()) { int x = 176, y = 170, lado = 118, r = 26; camino.AddArc(x, y, r * 2, r * 2, 180, 90); camino.AddArc(x + lado - r * 2, y, r * 2, r * 2, 270, 90); camino.AddArc(x + lado - r * 2, y + lado - r * 2, r * 2, r * 2, 0, 90); camino.AddArc(x, y + lado - r * 2, r * 2, r * 2, 90, 90); camino.CloseFigure(); g.FillPath(brocha, camino); } using (var f = new Font("Segoe UI", 30f, FontStyle.Bold)) using (var t = new SolidBrush(Marca.Fondo)) { var tam = g.MeasureString(p.Sigla, f); g.DrawString(p.Sigla, f, t, 176 + (118 - tam.Width) / 2, 170 + (118 - tam.Height) / 2); } using (var f = new Font("Segoe UI", 19f, FontStyle.Bold)) using (var t = new SolidBrush(Marca.Texto)) { var tam = g.MeasureString(p.Nombre, f); g.DrawString(p.Nombre, f, t, (470 - tam.Width) / 2, 316); } using (var f = new Font("Consolas", 9.5f)) using (var t = new SolidBrush(Marca.Texto2)) { var tam = g.MeasureString(p.Lema, f); g.DrawString(p.Lema, f, t, (470 - tam.Width) / 2, 356); } using (var t = new SolidBrush(Marca.Oro)) g.FillRectangle(t, 212, 392, 46, 3); using (var lapiz = new Pen(Marca.Borde)) g.DrawLine(lapiz, 469, 0, 469, 620); }; Controls.Add(marca); // --- Tarjeta de accion (derecha) --- var tarjeta = new Panel(); tarjeta.Bounds = new Rectangle(506, 74, 438, 472); tarjeta.BackColor = Marca.Panel; tarjeta.Paint += delegate(object s, PaintEventArgs e) { using (var lapiz = new Pen(Marca.Borde)) e.Graphics.DrawRectangle(lapiz, 0, 0, tarjeta.Width - 1, tarjeta.Height - 1); }; Controls.Add(tarjeta); bool instalado = Trabajo.YaInstalado(p); titulo = Texto(instalado ? "Ya está instalado" : "Instalar en este equipo", new Font("Segoe UI", 15f, FontStyle.Bold), Marca.Texto, 28, 26, 380, 30); tarjeta.Controls.Add(titulo); tarjeta.Controls.Add(Texto(p.Resumen, new Font("Segoe UI", 9f), Marca.Texto2, 28, 62, 382, 76)); int fila = 146; foreach (string linea in p.Incluye) { tarjeta.Controls.Add(Texto("\u2022 " + linea, new Font("Segoe UI", 9f), Marca.Texto2, 30, fila, 380, 30)); fila += 26; } fila += 12; tarjeta.Controls.Add(Texto("Se instala en", new Font("Segoe UI", 8.5f), Marca.Texto2, 28, fila, 140, 18)); tarjeta.Controls.Add(Texto(Trabajo.Carpeta(p), new Font("Consolas", 8.5f), Marca.Texto, 168, fila, 242, 18)); fila += 22; tarjeta.Controls.Add(Texto("Versión a instalar", new Font("Segoe UI", 8.5f), Marca.Texto2, 28, fila, 140, 18)); versionDicha = Texto("consultando...", new Font("Consolas", 8.5f), Marca.Texto, 168, fila, 242, 18); tarjeta.Controls.Add(versionDicha); fila += 30; barra = new Panel(); barra.Bounds = new Rectangle(28, fila, 382, 9); barra.BackColor = Marca.Panel2; barra.Visible = false; relleno = new Panel(); relleno.Bounds = new Rectangle(0, 0, 0, 9); relleno.BackColor = Marca.Oro; barra.Controls.Add(relleno); tarjeta.Controls.Add(barra); fila += 18; estado = Texto("", new Font("Segoe UI", 8.75f), Marca.Texto2, 28, fila, 382, 54); tarjeta.Controls.Add(estado); accion = new Button(); accion.Text = instalado ? "Volver a instalar" : "Instalar"; accion.Bounds = new Rectangle(28, 400, 382, 42); accion.FlatStyle = FlatStyle.Flat; accion.FlatAppearance.BorderSize = 0; accion.BackColor = Marca.Oro; accion.ForeColor = Marca.Fondo; accion.Font = new Font("Segoe UI", 10.5f, FontStyle.Bold); accion.Cursor = Cursors.Hand; accion.Enabled = false; accion.Click += delegate { Instalar(); }; tarjeta.Controls.Add(accion); cerrar = new Button(); cerrar.Text = "Cerrar el instalador"; cerrar.Bounds = new Rectangle(28, 400, 382, 42); cerrar.FlatStyle = FlatStyle.Flat; cerrar.FlatAppearance.BorderColor = Marca.Borde; cerrar.BackColor = Marca.Panel; cerrar.ForeColor = Marca.Texto2; cerrar.Font = new Font("Segoe UI", 10f); cerrar.Cursor = Cursors.Hand; cerrar.Visible = false; cerrar.Click += delegate { Close(); }; tarjeta.Controls.Add(cerrar); Shown += delegate { ConsultarCanal(); }; } string urlDescarga; void ConsultarCanal() { var hilo = new System.Threading.Thread(delegate() { string v, u; bool ok = Trabajo.Publicado(p, out v, out u); Invoke((MethodInvoker)delegate { if (ok) { urlDescarga = u; versionDicha.Text = v; accion.Enabled = true; } else { versionDicha.Text = "sin conexión"; versionDicha.ForeColor = Marca.Rojo; estado.ForeColor = Marca.Rojo; estado.Text = "No se pudo consultar el canal de descargas. Revisa la conexión a internet y vuelve a abrir el instalador."; } }); }); hilo.IsBackground = true; hilo.Start(); } void Instalar() { accion.Enabled = false; accion.Text = "Instalando..."; barra.Visible = true; estado.ForeColor = Marca.Texto2; estado.Text = "Descargando el programa..."; var hilo = new System.Threading.Thread(delegate() { try { Trabajo.Descargar(p, urlDescarga, delegate(int pc) { Invoke((MethodInvoker)delegate { relleno.Width = barra.Width * pc / 100; estado.Text = "Descargando el programa... " + pc + "%"; }); }); Invoke((MethodInvoker)delegate { estado.Text = "Encendiendo el programa por primera vez..."; }); Trabajo.Arrancar(p); System.Threading.Thread.Sleep(2500); Invoke((MethodInvoker)delegate { titulo.Text = "Listo"; estado.ForeColor = Marca.Verde; estado.Text = "Quedó instalado y encendido. Su acceso está en el escritorio y arranca solo con Windows. " + p.Despues; accion.Visible = false; cerrar.Visible = true; }); } catch (Exception e) { string dicho = e.Message; Invoke((MethodInvoker)delegate { estado.ForeColor = Marca.Rojo; estado.Text = dicho; accion.Enabled = true; accion.Text = "Intentar de nuevo"; }); } }); hilo.IsBackground = true; hilo.Start(); } } static class Programa { [STAThread] static int Main(string[] args) { var p = Catalogo.Actual(); // Modo consola: lo usan las pruebas para ejercitar el instalador de verdad // (descarga cortada, descarga completa) sin abrir ventana. if (Array.IndexOf(args, "--consola") >= 0) { // La consola de Windows arranca en una codificacion vieja: sin esto, un // mensaje con tildes sale ilegible en el archivo de registro del negocio. try { Console.OutputEncoding = System.Text.Encoding.UTF8; } catch { } try { string v, u; if (!Trabajo.Publicado(p, out v, out u)) { Console.WriteLine("SIN CANAL"); return 2; } Console.WriteLine("VERSION " + v); Trabajo.Descargar(p, u, null); Console.WriteLine("INSTALADO " + Trabajo.RutaExe(p)); return 0; } catch (Exception e) { Console.WriteLine("PROBLEMA " + e.Message); return 1; } } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Ventana(p)); return 0; } }