diff --git a/.gitignore b/.gitignore index 9491a2f..69797de 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,13 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Installer projects output +*.msi +*.exe +AssetManager.installer/Output + +# Env +.Env +AssetManager.tray/appsettings.json \ No newline at end of file diff --git a/AssetManager.installer/setup.iss b/AssetManager.installer/setup.iss new file mode 100644 index 0000000..63cce3f --- /dev/null +++ b/AssetManager.installer/setup.iss @@ -0,0 +1,172 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! +; Non-commercial use only + +#define MyAppName "AssetManager" +#define MyAppVersion "1.0.2" +#define MyAppPublisher "Borgno" +#define MyAppExeName "AssetManager.tray.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{8E2374B6-2EE6-4C3A-B17A-AF32C9565CB1} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +DefaultDirName={autopf}\{#MyAppPublisher}\{#MyAppName} +DisableDirPage=yes +UninstallDisplayIcon={app}\{#MyAppExeName} +; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run +; on anything but x64 and Windows 11 on Arm. +ArchitecturesAllowed=x64compatible +; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the +; install be done in "64-bit mode" on x64 or Windows 11 on Arm, +; meaning it should use the native 64-bit Program Files directory and +; the 64-bit view of the registry. +ArchitecturesInstallIn64BitMode=x64compatible +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +; Uncomment the following line to run in non administrative install mode (install for current user only). +;PrivilegesRequired=lowest +OutputDir=Output +OutputBaseFilename=assetmanager_installer +Compression=lzma +SolidCompression=yes +WizardStyle=modern dynamic windows11 + +[Languages] +Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" + +[Files] +Source: "C:\Users\GYNTI-N03.GYNTI-N03\source\repos\AssetManager\AssetManager.tray\bin\Release\net10.0-windows\win-x64\*"; \ +DestDir: "{app}\tray"; Flags: ignoreversion; \ +Excludes: "appsettings.json, appsettings_example.json" + +Source: "C:\Users\GYNTI-N03.GYNTI-N03\source\repos\AssetManager\AssetManager.service\bin\Release\net10.0-windows\win-x64\*"; \ +DestDir: "{app}\service"; Flags: ignoreversion + +Source: "C:\Users\GYNTI-N03.GYNTI-N03\source\repos\AssetManager\AssetManager.tray\bin\Release\net10.0-windows\win-x64\appsettings_example.json"; \ +DestDir: "{app}\tray"; \ +DestName: "appsettings.json"; Flags: ignoreversion +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +; [Icons] +; Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +; Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" + +[Run] +Filename: "sc.exe"; \ +Parameters: "create AssetManager binPath= ""{app}\service\AssetManager.service.exe"" start= auto"; \ +Flags: runhidden + +Filename: "sc.exe"; \ +Parameters: "start AssetManager"; \ +Flags: runhidden + +Filename: "schtasks.exe"; \ +Parameters: "/create /f /sc onlogon /rl highest /ru SYSTEM /tn ""AssetManagerTray"" /tr ""\""{app}\tray\AssetManager.tray.exe\"" """; \ +Flags: runhidden + +Filename: "powershell.exe"; \ +Parameters: "-ExecutionPolicy Bypass -Command ""$t = Get-ScheduledTask -TaskName 'AssetManagerTray'; $s = $t.Settings; $s.ExecutionTimeLimit = [TimeSpan]::Zero; $s.AllowStartIfOnBatteries = $true; $s.DontStopIfGoingOnBatteries = $true; Set-ScheduledTask -TaskName 'AssetManagerTray' -Settings $s"""; \ +Flags: runhidden + +; Iniciar o tray após a instalação +;Filename: "{app}\tray\{#MyAppExeName}"; \ +;Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; \ +;Flags: postinstall skipifsilent + +[UninstallRun] +; Parar e remover serviço +Filename: "sc.exe"; Parameters: "stop AssetManager"; Flags: runhidden +Filename: "sc.exe"; Parameters: "delete AssetManager"; Flags: runhidden + +; Remover tarefa agendada +Filename: "schtasks.exe"; Parameters: "/end /tn ""AssetManagerTray"""; Flags: runhidden +Filename: "schtasks.exe"; Parameters: "/delete /f /tn ""AssetManagerTray""" ; Flags: runhidden + +[Code] +var + TokenPage: TInputQueryWizardPage; + +procedure InitializeWizard; +begin + TokenPage := CreateInputQueryPage(wpWelcome, + 'Configuração da API', 'Autenticação do Agente', + 'Insira a URL e o Token de acesso da API para que o AssetManager possa enviar os dados.'); + TokenPage.Add('URL da API:', False); + TokenPage.Add('API Token:', False); +end; + +function ShouldSkipPage(PageID: Integer): Boolean; +begin + if (PageID = TokenPage.ID) and FileExists(ExpandConstant('{src}\appsettings.json')) then + Result := True + else + Result := False; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +var + JsonDestino: String; + JsonOrigem: String; + FileContentAnsi: AnsiString; + FileContentString: String; + UserUrl: String; + UserToken: String; + HouveAlteracao: Boolean; +begin + if CurStep = ssPostInstall then + begin + JsonDestino := ExpandConstant('{app}\tray\appsettings.json'); + JsonOrigem := ExpandConstant('{src}\appsettings.json'); + HouveAlteracao := False; + + if FileExists(JsonOrigem) then + begin + Log('Arquivo appsettings.json encontrado na origem. Copiando...'); + if FileCopy(JsonOrigem, JsonDestino, False) then + Log('Configuração importada automaticamente.') + else + Log('Falha ao copiar configuração automática.'); + end + + else + begin + if (TokenPage <> nil) then + begin + UserUrl := TokenPage.Values[0]; + UserToken := TokenPage.Values[1]; + end + else + begin + UserUrl := ''; + UserToken := ''; + end; + if FileExists(JsonDestino) then + begin + if LoadStringFromFile(JsonDestino, FileContentAnsi) then + begin + FileContentString := String(FileContentAnsi); + if (UserUrl <> '') and (StringChange(FileContentString, '{{URL}}', UserUrl) > 0) then + begin + HouveAlteracao := True; + Log('URL atualizada.'); + end; + if (UserToken <> '') and (StringChange(FileContentString, '{{TOKEN}}', UserToken) > 0) then + begin + HouveAlteracao := True; + Log('Token atualizado.'); + end; + if HouveAlteracao then + begin + SaveStringToFile(JsonDestino, AnsiString(FileContentString), False); + Log('Arquivo JSON salvo com sucesso.'); + end; + end; + end; + end; + end; +end; \ No newline at end of file diff --git a/AssetManager.service/AssetManager.service.csproj b/AssetManager.service/AssetManager.service.csproj index f9d0f08..e54cff1 100644 --- a/AssetManager.service/AssetManager.service.csproj +++ b/AssetManager.service/AssetManager.service.csproj @@ -1,10 +1,13 @@ - + - net10.0 + net10.0-windows enable enable + pt-BR dotnet-AssetManager-7dd1a0f1-3fce-4c35-bc7b-8f9a71a59f68 + true + false @@ -12,4 +15,4 @@ - + \ No newline at end of file diff --git a/AssetManager.service/README.md b/AssetManager.service/README.md deleted file mode 100644 index 12fa51b..0000000 --- a/AssetManager.service/README.md +++ /dev/null @@ -1 +0,0 @@ -# AssetManager \ No newline at end of file diff --git a/AssetManager.service/debug.ps1 b/AssetManager.service/debug.ps1 new file mode 100644 index 0000000..68a63d5 --- /dev/null +++ b/AssetManager.service/debug.ps1 @@ -0,0 +1,29 @@ +param( + [string]$PipeName = "asset-monitor-pipe", + [int]$IntervalSeconds = 2 +) + +while ($true) { + Clear-Host + Write-Host "AssetManager Debug ($(Get-Date))" + Write-Host "-------------------------------" + + try { + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream(".", $PipeName, [System.IO.Pipes.PipeDirection]::In) + $pipe.Connect(3000) + + $reader = New-Object System.IO.StreamReader($pipe) + $json = $reader.ReadLine() + + $reader.Close() + $pipe.Close() + + $obj = $json | ConvertFrom-Json + $obj | Format-List + } + catch { + Write-Host "Erro ao ler pipe: $_" -ForegroundColor Red + } + + Start-Sleep -Seconds $IntervalSeconds +} diff --git a/AssetManager.service/package-lock.json b/AssetManager.service/package-lock.json deleted file mode 100644 index 80bcb7f..0000000 --- a/AssetManager.service/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "AssetManager", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/AssetManager.service/src/Program.cs b/AssetManager.service/src/Program.cs index 1c0d141..6306cf3 100644 --- a/AssetManager.service/src/Program.cs +++ b/AssetManager.service/src/Program.cs @@ -7,7 +7,7 @@ builder.Services.AddWindowsService(options => { - options.ServiceName = "AssetManager"; +options.ServiceName = "AssetManager"; }); builder.Services.AddSingleton(); diff --git a/AssetManager.service/src/Worker.cs b/AssetManager.service/src/Worker.cs index aeb043f..f5ed02a 100644 --- a/AssetManager.service/src/Worker.cs +++ b/AssetManager.service/src/Worker.cs @@ -4,13 +4,17 @@ // O tray faz (vai fazer) POST para o backend. // TODO: Melhorar coleta, incluir CPU model, RAM total, tráfego de rede. // TODO: Implementar reconexão do pipe e manejo de falhas de envio. +// TODO: Mudar a lógica de cálculo e outras estatiticas para o tray (ex: disk usage / cpu model). +using System; +using System.IO; +using System.IO.Pipes; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Text.Json; -using System.Text; -using System.Runtime.InteropServices; -using System.IO.Pipes; public class Worker : BackgroundService { @@ -21,17 +25,18 @@ public class Worker : BackgroundService private readonly INetworkMonitor _net; private const string PIPE_NAME = "asset-monitor-pipe"; - private const int PIPE_CONNECT_TIMEOUT_MS = 5_000; + private const int COLLECT_INTERVAL_SECONDS = 5; - private const int TIMER = 2_000; - private readonly int timerSeconds = TIMER / 1000; + // Snapshot atual (thread-safe) + private readonly object _lock = new(); + private string _latestJson = "{}"; public Worker( - ILogger logger, - ICpuMonitor cpu, - IMemoryMonitor mem, - IDiskMonitor disk, - INetworkMonitor net) + ILogger logger, + ICpuMonitor cpu, + IMemoryMonitor mem, + IDiskMonitor disk, + INetworkMonitor net) { _logger = logger; _cpu = cpu; @@ -39,99 +44,110 @@ public Worker( _disk = disk; _net = net; } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation($"Monitor iniciado. Intervalo: {timerSeconds} segundos"); + _logger.LogInformation("AssetManager Worker iniciado."); - while (!stoppingToken.IsCancellationRequested) - { - await SendMetricsAsync(stoppingToken); // TODO: Melhorar - await Task.Delay(TIMER, stoppingToken); - } - } + // Loop de coleta + _ = Task.Run(() => CollectLoop(stoppingToken), stoppingToken); - private async Task SendMetricsAsync(CancellationToken stoppingToken) + // Loop do servidor de pipe + await PipeServerLoop(stoppingToken); + } + /// Coleta e mantém o último snapshot + private async Task CollectLoop(CancellationToken token) { var (diskUsedGb, diskTotalGb) = _disk.GetDiskUsage(); + var diskUsagePercent = diskTotalGb > 0 + ? Math.Round((diskUsedGb / diskTotalGb) * 100, 2) + : 0; var (ip, mac) = _net.GetNetworkInfo(); // TODO: mudar para ID real var assetId = Environment.MachineName; var hostname = Environment.MachineName; - var diskUsagePercent = diskTotalGb > 0 - ? Math.Round((diskUsedGb / diskTotalGb) * 100, 2) - : 0; - - var payload = new + while (!token.IsCancellationRequested) { - asset_id = assetId, // TODO: mudar para ID real - hostname = hostname, - cpu_usage = _cpu.GetCpuUsage(), - memory_usage = _mem.GetMemoryUsage(), - disk_usage = diskUsagePercent, - network_in = 0, // TODO: implementar - network_out = 0, - uptime = (long)Environment.TickCount64 / 1000, - - cpu_info = new + try { - model = "unknown", // TODO: mostrar modelo - cores = Environment.ProcessorCount - }, + var payload = new + { + asset_id = assetId, // TODO: mudar para ID real + hostname = Environment.MachineName, + // TODO: mostrar model CPU + cpu_usage = _cpu.GetCpuUsage(), + // TODO: mostrar RAM total + memory_usage = _mem.GetMemoryUsage(), + disk = diskUsagePercent, + ip_addr = ip, + mac_addr = mac, + os = Environment.OSVersion.ToString(), + timestamp = DateTime.UtcNow, + // TODO: adicionar versionamento do payload + version = "v1" + }; + + string json = JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + WriteIndented = false + }); - ram_info = new + lock (_lock) + { + _latestJson = json; + } + } + catch (Exception ex) { - usage_percent = _mem.GetMemoryUsage() // TODO: mostrar total em GB - }, + _logger.LogError(ex, "Erro ao coletar métricas."); + } - storage_info = new + await Task.Delay(TimeSpan.FromSeconds(COLLECT_INTERVAL_SECONDS), token); + } + } + private async Task PipeServerLoop(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + try { - disks = new[] - { - new { name = "C:", used_gb = diskUsedGb, total_gb = diskTotalGb } - } - }, + using var pipeServer = new NamedPipeServerStream( + PIPE_NAME, + PipeDirection.Out, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous); - ip_address = ip, - mac_address = mac, - os_name = RuntimeInformation.OSDescription, - os_version = Environment.OSVersion.Version.ToString() - }; + _logger.LogDebug("Aguardando conexão no named pipe..."); - var json = JsonSerializer.Serialize(payload); + await pipeServer.WaitForConnectionAsync(token); - try - { - using var pipeClient = new NamedPipeClientStream(".", PIPE_NAME, PipeDirection.Out); - using var cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); - cts.CancelAfter(PIPE_CONNECT_TIMEOUT_MS); + string snapshot; + lock (_lock) + { + snapshot = _latestJson; + } + + using var writer = new StreamWriter(pipeServer, Encoding.UTF8) + { + AutoFlush = true + }; - await pipeClient.ConnectAsync(cts.Token); + // Envia uma única linha (snapshot atual) + await writer.WriteLineAsync(snapshot); - if (!pipeClient.IsConnected) + _logger.LogDebug("Snapshot enviado para cliente do pipe."); + } + catch (OperationCanceledException) { - _logger.LogWarning("Nao foi possivel conectar ao named pipe {PipeName}.", PIPE_NAME); - return; + // shutdown normal + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro no servidor do named pipe."); + await Task.Delay(1000, token); // backoff leve } - - using var writer = new StreamWriter(pipeClient, Encoding.UTF8, leaveOpen: true) { AutoFlush = true }; - await writer.WriteLineAsync(json); - - _logger.LogInformation("Metricas enviadas via named pipe."); - } - catch (OperationCanceledException) - { - _logger.LogWarning("Timeout ao conectar ao named pipe {PipeName}.", PIPE_NAME); - } - catch (IOException ex) - { - _logger.LogError(ex, "Erro de E/S ao enviar metricas via named pipe."); - } - catch (Exception ex) - { - _logger.LogError(ex, "Erro ao enviar metricas via named pipe."); } } -} +} \ No newline at end of file diff --git a/AssetManager.slnx b/AssetManager.slnx index 5e08da1..e6652e9 100644 --- a/AssetManager.slnx +++ b/AssetManager.slnx @@ -1,4 +1,15 @@ + + + + + + + + + + + diff --git a/AssetManager.tray/AssetManager.tray.csproj b/AssetManager.tray/AssetManager.tray.csproj index e7df544..51f391e 100644 --- a/AssetManager.tray/AssetManager.tray.csproj +++ b/AssetManager.tray/AssetManager.tray.csproj @@ -2,12 +2,27 @@ WinExe + win-x64 net10.0-windows enable true enable - SystemAware - true + app.manifest + SystemAware + true + pt-BR + true + false + + + + + + + + PreserveNewest + + \ No newline at end of file diff --git a/AssetManager.tray/AssetManager.tray.sln b/AssetManager.tray/AssetManager.tray.sln new file mode 100644 index 0000000..e8d41ec --- /dev/null +++ b/AssetManager.tray/AssetManager.tray.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssetManager.tray", "AssetManager.tray.csproj", "{6CB97AFA-98C8-08E3-26D7-7B13F7EF296A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6CB97AFA-98C8-08E3-26D7-7B13F7EF296A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CB97AFA-98C8-08E3-26D7-7B13F7EF296A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CB97AFA-98C8-08E3-26D7-7B13F7EF296A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CB97AFA-98C8-08E3-26D7-7B13F7EF296A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D9664C6F-572A-4672-9D20-14731F5C2FB9} + EndGlobalSection +EndGlobal diff --git a/AssetManager.tray/Program.cs b/AssetManager.tray/Program.cs deleted file mode 100644 index 3ad457b..0000000 --- a/AssetManager.tray/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Windows.Forms; - -namespace AssetManager.Tray -{ - internal static class Program - { - [STAThread] - static void Main() - { - ApplicationConfiguration.Initialize(); - Application.Run(new TrayForm()); - } - } - -} \ No newline at end of file diff --git a/AssetManager.tray/TrayForm.cs b/AssetManager.tray/TrayForm.cs deleted file mode 100644 index 790fd44..0000000 --- a/AssetManager.tray/TrayForm.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Windows.Forms; -using System.ServiceProcess; - -namespace AssetManager.Tray -{ - public class TrayForm : Form - { - private readonly NotifyIcon trayIcon; - private readonly ContextMenuStrip trayMenu; - - private void EnsureServiceRunning() - { - const string serviceName = "AssetManager"; - - try - { - using var sc = new ServiceController(serviceName); - - if (sc.Status == ServiceControllerStatus.Stopped || - sc.Status == ServiceControllerStatus.StopPending) - { - sc.Start(); - sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(30)); - } - } - catch (Exception ex) - { - MessageBox.Show($"Erro ao iniciar serviço: {ex.Message}"); - } - } - - public TrayForm() - { - // Ocultar a janela - this.WindowState = FormWindowState.Minimized; - this.ShowInTaskbar = false; - this.Visible = false; - - // Menu - trayMenu = new ContextMenuStrip(); - trayMenu.Items.Add("Enviar agora", null, OnSendNow); - trayMenu.Items.Add("Sair", null, OnExit); - - // Icon - trayIcon = new NotifyIcon - { - Icon = SystemIcons.Information, // TODO: trocar icone - Visible = true, - Text = "Asset Manager Agent", - ContextMenuStrip = trayMenu - }; - EnsureServiceRunning(); - } - - private void OnSendNow(object? sender, EventArgs e) - { - MessageBox.Show("Envio manual ainda não implementado."); - } - - private void OnExit(object? sender, EventArgs e) - { - trayIcon.Visible = false; - Application.Exit(); - } - } -} diff --git a/AssetManager.tray/app.manifest b/AssetManager.tray/app.manifest new file mode 100644 index 0000000..467e905 --- /dev/null +++ b/AssetManager.tray/app.manifest @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AssetManager.tray/appsettings_example.json b/AssetManager.tray/appsettings_example.json new file mode 100644 index 0000000..448b57d --- /dev/null +++ b/AssetManager.tray/appsettings_example.json @@ -0,0 +1,7 @@ +{ + "API": { + "Url": "{{URL}}", + "Token": "{{TOKEN}}", + "ReconnectSeconds": 5 + } +} \ No newline at end of file diff --git a/AssetManager.tray/src/DebugConsole.resx b/AssetManager.tray/src/DebugConsole.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/AssetManager.tray/src/DebugConsole.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/AssetManager.tray/src/HttpDebugService.cs b/AssetManager.tray/src/HttpDebugService.cs new file mode 100644 index 0000000..a336974 --- /dev/null +++ b/AssetManager.tray/src/HttpDebugService.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace AssetManager.Tray +{ + public class HttpDebugService + { + // Snapshot atual (por enquanto fixo / mock) + // Depois isso vai ser atualizado pelo pipe + private static string _latestJson = "{ \"status\": \"aguardando dados\" }"; + public static void UpdateSnapshot(string json) + { + _latestJson = json; + } + public async Task StartAsync() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseUrls("http://localhost:8765"); + + var app = builder.Build(); + + app.MapGet("/metrics", () => + { + return Results.Text(_latestJson, "application/json"); + }); + + await app.RunAsync(); + } + } +} \ No newline at end of file diff --git a/AssetManager.tray/src/Program.cs b/AssetManager.tray/src/Program.cs new file mode 100644 index 0000000..1d5f362 --- /dev/null +++ b/AssetManager.tray/src/Program.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Windows.Forms; + +namespace AssetManager.Tray +{ + internal static class Program + { + public static IConfiguration Configuration { get; private set; } = null!; + + [STAThread] + static void Main() + { + Configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false) + .Build(); + + ApplicationConfiguration.Initialize(); + Application.Run(new TrayForm()); + } + } +} diff --git a/AssetManager.tray/src/TrayForm.cs b/AssetManager.tray/src/TrayForm.cs new file mode 100644 index 0000000..89a87d5 --- /dev/null +++ b/AssetManager.tray/src/TrayForm.cs @@ -0,0 +1,167 @@ +using System; +using System.Windows.Forms; +using System.ServiceProcess; +using System.IO.Pipes; +using System.Net.Http; +using System.Text; +using System.Text.Json; + +namespace AssetManager.Tray +{ + public class TrayForm : Form + { + private readonly NotifyIcon trayIcon; + private readonly ContextMenuStrip trayMenu; + private readonly HttpDebugService _httpDebugService = new(); + private const string ServiceName = "AssetManager"; + private static readonly HttpClient _http = new(); + private readonly string _apiUrl; + private readonly string _apiToken; + private readonly bool _apiEnabled; + public TrayForm() + { + // Ocultar a janela + this.WindowState = FormWindowState.Minimized; + this.ShowInTaskbar = false; + this.Visible = false; + + // Menu + trayMenu = new ContextMenuStrip(); + trayMenu.Items.Add("Enviar agora", null, OnSendNow); + trayMenu.Items.Add("Sair", null, OnExit); + + // Icon + trayIcon = new NotifyIcon + { + Icon = SystemIcons.Information, // TODO: trocar icone + Visible = true, + Text = "Asset Manager Agent", + ContextMenuStrip = trayMenu + }; + var config = Program.Configuration; + + _apiUrl = config["Api:Url"]; + _apiToken = config["Api:Token"]; + + _apiEnabled = !string.IsNullOrWhiteSpace(_apiUrl); + + if (_apiEnabled && !string.IsNullOrWhiteSpace(_apiToken)) + { + _http.DefaultRequestHeaders.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken); + } + + _ = Task.Run(_httpDebugService.StartAsync); + _ = Task.Run(PipeReadLoop); + EnsureServiceRunning(); + } + private void EnsureServiceRunning() + { + try + { + using var sc = new ServiceController(ServiceName); + + if (sc.Status == ServiceControllerStatus.Stopped || + sc.Status == ServiceControllerStatus.StopPending) + { + sc.Start(); + sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(15)); + } + } + catch (Exception ex) + { + MessageBox.Show( + $"Erro ao iniciar o serviço {ServiceName}.\n\n{ex.Message}", + "AssetManager", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + private void StopService() + { + try + { + using var sc = new ServiceController(ServiceName); + + if (sc.Status == ServiceControllerStatus.Running) + { + sc.Stop(); + sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(15)); + } + } + catch (Exception ex) + { + MessageBox.Show( + $"Erro ao parar o serviço {ServiceName}.\n\n{ex.Message}", + "AssetManager", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); + } + } + private void OnSendNow(object? sender, EventArgs e) + { + MessageBox.Show("Envio manual ainda não implementado."); + } + + private void OnExit(object? sender, EventArgs e) + { + trayIcon.Visible = false; + StopService(); + Application.Exit(); + } + private async Task PipeReadLoop() + { + while (true) + { + try + { + using var pipe = new NamedPipeClientStream( + ".", + "asset-monitor-pipe", + PipeDirection.In + ); + + await pipe.ConnectAsync(3000); + + using var reader = new StreamReader(pipe); + var json = await reader.ReadLineAsync(); + + if (!string.IsNullOrWhiteSpace(json)) + { + HttpDebugService.UpdateSnapshot(json); + _ = SendToApiAsync(json); + } + } + catch + { + // Serviço pode estar indisponível ainda + // ignorar + } + await Task.Delay(2000); + } + } + private async Task SendToApiAsync(string json) + { + if (!_apiEnabled) + return; + + try + { + using var content = new StringContent( + json, + Encoding.UTF8, + "application/json" + ); + + await _http.PostAsync(_apiUrl!, content); + } + catch + { + // falha silenciosa + // tray nunca deve quebrar por rede + } + } + } +} diff --git a/AssetManager.tray/src/TrayForm.resx b/AssetManager.tray/src/TrayForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/AssetManager.tray/src/TrayForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/README.md b/README.md index 2c5646a..2a07298 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ # AssetManager -Sistema para controle de ativos + +Sistema para Controle de Ativos + +O **AssetManager** é uma aplicação desenvolvida para gerenciar e monitorar ativos de uma organização. Ele permite o cadastro, acompanhamento e manutenção de ativos, garantindo maior controle e eficiência na gestão. + +## Funcionalidades + +- Cadastro de ativos com informações detalhadas. +- Monitoramento do status e localização dos ativos. +- Agendamento e controle de manutenções preventivas e corretivas. +- Geração de relatórios para análise de desempenho e custos. +- Integração com outros sistemas de gestão. + +## Tecnologias Utilizadas + +- **Linguagem:** C# +- **Framework:** .NET + +## Licença + +Este projeto está licenciado sob a [MIT License](LICENSE).