Skip to content

Commit 5546e3c

Browse files
feat: adicionar suporte para modo de serviço Windows e atualizações correspondentes na interface
1 parent d56f9eb commit 5546e3c

10 files changed

Lines changed: 149 additions & 54 deletions

File tree

build/scripts/build-bootstrap-installer.ps1

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ param(
1010
[ValidateSet("0", "1")]
1111
[Alias("DiscoveryEnabled")]
1212
[string]$AutoProvisioning = "1",
13+
[ValidateSet("0", "1")]
14+
[string]$EnableWindowsService = "0",
1315
[string]$ExpectedTag = "",
1416
[switch]$GenericInstall
1517
)
@@ -167,7 +169,8 @@ $nsisArgs = @(
167169
"/DARG_PAYLOAD_URL=$PayloadUrl",
168170
"/DARG_PAYLOAD_FILENAME=$PayloadFileName",
169171
"/DARG_OUTFILE_NAME=$OutputName",
170-
"/DARG_DEFAULT_DISCOVERY=$AutoProvisioning"
172+
"/DARG_DEFAULT_DISCOVERY=$AutoProvisioning",
173+
"/DARG_ENABLE_WINDOWS_SERVICE=$EnableWindowsService"
171174
)
172175

173176
if ($PayloadSha256 -ne "") {
@@ -193,4 +196,9 @@ if (-not (Test-Path $installerPath)) {
193196

194197
Write-Output "[3/3] Concluido."
195198
Write-Output "Bootstrap gerado em: $installerPath"
196-
Write-Output "Esse bootstrap baixa a segunda etapa e executa o instalador completo com Discovery habilitado por padrao."
199+
if ($EnableWindowsService -eq "1") {
200+
Write-Output "Esse bootstrap baixa a segunda etapa e executa o instalador completo com servico Windows habilitado."
201+
}
202+
else {
203+
Write-Output "Esse bootstrap baixa a segunda etapa e executa o instalador completo em modo tray no logon (sem servico Windows)."
204+
}

build/scripts/build-install-installer.ps1

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ param(
88
[ValidateSet("0", "1")]
99
[Alias("DiscoveryEnabled")]
1010
[string]$AutoProvisioning = "1",
11+
[ValidateSet("0", "1")]
12+
[string]$EnableWindowsService = "0",
1113
[switch]$GenericInstall
1214
)
1315

@@ -149,7 +151,8 @@ $nsisArgs = @(
149151
"UTF8",
150152
"/DARG_WAILS_AMD64_BINARY=$agentExe",
151153
"/DARG_OUTFILE_NAME=$OutputName",
152-
"/DARG_DEFAULT_DISCOVERY=$AutoProvisioning"
154+
"/DARG_DEFAULT_DISCOVERY=$AutoProvisioning",
155+
"/DARG_ENABLE_WINDOWS_SERVICE=$EnableWindowsService"
153156
)
154157

155158
if ($DefaultUrl -ne "") {
@@ -187,4 +190,9 @@ if (-not (Test-Path $installerPath)) {
187190

188191
Write-Output "[3/3] Concluido."
189192
Write-Output "Instalador gerado em: $installerPath"
190-
Write-Output "Observacao: o instalador registra o servico Windows, inicia o Discovery e cria a regra de Windows Firewall para permitir a comunicacao do discovery-agent.exe na rede."
193+
if ($EnableWindowsService -eq "1") {
194+
Write-Output "Observacao: o instalador registra o servico Windows, configura autostart da UI no logon e cria a regra de Windows Firewall para o discovery-agent.exe."
195+
}
196+
else {
197+
Write-Output "Observacao: modo padrao sem servico Windows. O agente inicia via Task Scheduler no logon (tray icon) e a regra de Windows Firewall e mantida para rede/P2P."
198+
}

src/app/app.go

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ const (
5656
// Temporarily disable efficiency mode until we revisit this behavior.
5757
efficiencyModeEnabled = false
5858

59+
// windowsServiceModeEnabled controla o modo service-first.
60+
// Enquanto false, o runtime opera sempre em modo local (tray icon no logon).
61+
windowsServiceModeEnabled = false
62+
5963
WindowWidth = 1280
6064
WindowHeight = 860
6165
WindowMinWidth = 980
@@ -136,9 +140,9 @@ type App struct {
136140
zeroTouchAttemptInFlight atomic.Bool
137141
zeroTouchApprovalPending atomic.Bool
138142

139-
// serviceConnectedMode é true quando o Windows Service foi detectado no startup.
140-
// Quando ativo, workers locais de automação e inventário são omitidos para
141-
// evitar duplicação com os workers do service (arquitetura service-first).
143+
// serviceConnectedMode é true quando o Windows Service foi detectado no startup
144+
// E o modo service-first está habilitado. Enquanto o modo estiver desativado,
145+
// essa flag permanece false e o runtime fica sempre local.
142146
serviceConnectedMode atomic.Bool
143147

144148
// startupTime registra quando a aplicação iniciou, usado para calcular
@@ -162,8 +166,11 @@ func NewApp(opts AppStartupOptions) *App {
162166
reg := mcp.NewRegistry()
163167
chatSvc := ai.NewService(reg)
164168

165-
// Initialize service client for communicating with Windows Service
166-
serviceClient := service.NewServiceClient()
169+
var serviceClient *service.ServiceClient
170+
if windowsServiceModeEnabled {
171+
// Initialize service client for communicating with Windows Service.
172+
serviceClient = service.NewServiceClient()
173+
}
167174

168175
a := &App{
169176
ctx: context.Background(), // inicializado para evitar nil; sobrescrito por SetContext()
@@ -526,12 +533,22 @@ func (a *App) shouldRunLocalP2P() bool {
526533
if a == nil {
527534
return false
528535
}
529-
if a.serviceConnectedMode.Load() && !a.runtimeFlags.DebugMode {
536+
if a.shouldUseServiceRuntime() && !a.runtimeFlags.DebugMode {
530537
return false
531538
}
532539
return true
533540
}
534541

542+
func (a *App) shouldUseServiceRuntime() bool {
543+
if a == nil || !windowsServiceModeEnabled {
544+
return false
545+
}
546+
if a.serviceClient == nil {
547+
return false
548+
}
549+
return a.serviceConnectedMode.Load()
550+
}
551+
535552
func (a *App) startup(ctx context.Context) {
536553
ctx, cancel := context.WithCancel(ctx)
537554
a.ctx = ctx
@@ -566,10 +583,11 @@ func (a *App) startup(ctx context.Context) {
566583
a.consolEngine = newConsolidationEngine(db, agentIDForEngine)
567584
}
568585

569-
// Verificar conectividade com o Windows Service antes de iniciar workers locais.
570-
// Quando o service está disponível, workers de automação e inventário são omitidos
571-
// para evitar duplicação (arquitetura service-first: service executa, UI consome).
572-
if a.serviceClient != nil {
586+
// Modo padrão: runtime local (tray icon no logon), sem service-first.
587+
if !windowsServiceModeEnabled {
588+
a.serviceConnectedMode.Store(false)
589+
log.Println("[startup] modo Windows Service desativado — runtime local (tray) ativo")
590+
} else if a.serviceClient != nil {
573591
probeCtx, probeCancel := context.WithTimeout(ctx, 2*time.Second)
574592
if a.serviceClient.Ping(probeCtx) {
575593
a.serviceConnectedMode.Store(true)
@@ -585,7 +603,7 @@ func (a *App) startup(ctx context.Context) {
585603
defer a.startupWg.Done()
586604

587605
// Quando o service está disponível, ele já gerencia inventário; pular coleta local.
588-
if a.serviceConnectedMode.Load() {
606+
if a.shouldUseServiceRuntime() {
589607
log.Println("[startup] inventory-startup: ignorado (service disponível)")
590608
return
591609
}
@@ -622,7 +640,7 @@ func (a *App) startup(ctx context.Context) {
622640
if a.debugSvc != nil {
623641
a.debugSvc.BootstrapAgentCredentialsFromInstallerConfig(ctx)
624642
}
625-
if a.serviceConnectedMode.Load() {
643+
if a.shouldUseServiceRuntime() {
626644
a.requestServiceConfigReload(ctx, "startup-bootstrap")
627645
}
628646

@@ -631,7 +649,7 @@ func (a *App) startup(ctx context.Context) {
631649
a.ensureMeshCentralInstalled(ctx, "startup-auth", false)
632650
})
633651

634-
if a.serviceConnectedMode.Load() {
652+
if a.shouldUseServiceRuntime() {
635653
log.Println("[startup] agent-runtime local: ignorado (service disponível)")
636654
return
637655
}
@@ -663,7 +681,7 @@ func (a *App) startup(ctx context.Context) {
663681
}
664682

665683
// Quando o service está disponível, ele gerencia automação; pular worker local.
666-
if a.serviceConnectedMode.Load() {
684+
if a.shouldUseServiceRuntime() {
667685
log.Println("[startup] automation-service: ignorado (service disponível)")
668686
return
669687
}
@@ -690,7 +708,7 @@ func (a *App) startup(ctx context.Context) {
690708
log.Println("[startup] p2p local: ignorado (service disponível)")
691709
return
692710
}
693-
if a.serviceConnectedMode.Load() && a.runtimeFlags.DebugMode {
711+
if a.shouldUseServiceRuntime() && a.runtimeFlags.DebugMode {
694712
log.Println("[startup] p2p local: iniciado em modo debug mesmo com service disponível")
695713
}
696714
if !isAgentConfigured() && a.zeroTouchConfigRegistrationAllowed() {
@@ -742,7 +760,7 @@ func (a *App) SendTestHeartbeat() string {
742760
defer a.queuedForceHeartbeat.Store(false)
743761

744762
a.logs.append("[heartbeat][manual] enviando heartbeat manual...")
745-
if a.serviceConnectedMode.Load() && a.serviceClient != nil {
763+
if a.shouldUseServiceRuntime() {
746764
message, err := a.requestServiceForceHeartbeat(a.ctx, "debug-manual-heartbeat")
747765
if err != nil {
748766
a.logs.append("[heartbeat][manual] falha ao enviar heartbeat manual via service: " + err.Error())
@@ -997,11 +1015,24 @@ func serviceOnlyUnavailablePayload(detail string) dto.ServiceHealthPayload {
9971015
}
9981016
}
9991017

1018+
func localRuntimeHealthPayload() dto.ServiceHealthPayload {
1019+
return dto.ServiceHealthPayload{
1020+
Running: true,
1021+
ServiceOnly: false,
1022+
UserMessage: "Runtime local ativo (tray icon no logon).",
1023+
}
1024+
}
1025+
10001026
// GetServiceHealth returns the health of the headless Windows Service.
10011027
// Retained as map[string]interface{} for Wails frontend compatibility.
10021028
func (a *App) GetServiceHealth() map[string]interface{} {
10031029
var payload dto.ServiceHealthPayload
10041030

1031+
if !windowsServiceModeEnabled {
1032+
payload = localRuntimeHealthPayload()
1033+
return toMap(payload)
1034+
}
1035+
10051036
if a.serviceClient == nil {
10061037
payload = serviceOnlyUnavailablePayload("service client not initialized")
10071038
return toMap(payload)

src/app/app_test.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,18 @@ func TestServiceOnlyUnavailablePayload_HasUserGuidance(t *testing.T) {
8585
}
8686
}
8787

88-
func TestGetServiceHealth_ServiceClientNil_ReturnsServiceOnlyGuidance(t *testing.T) {
88+
func TestGetServiceHealth_ServiceModeDisabled_ReturnsLocalRuntimeStatus(t *testing.T) {
8989
a := &App{}
9090
health := a.GetServiceHealth()
91-
if health["running"] != false {
92-
t.Fatalf("expected running=false, got %v", health["running"])
91+
if health["running"] != true {
92+
t.Fatalf("expected running=true, got %v", health["running"])
9393
}
94-
if health["service_only"] != true {
95-
t.Fatalf("expected service_only=true, got %v", health["service_only"])
94+
if health["service_only"] != false {
95+
t.Fatalf("expected service_only=false, got %v", health["service_only"])
9696
}
9797
msg, _ := health["user_message"].(string)
98-
if !strings.Contains(strings.ToLower(msg), "contate o suporte") {
99-
t.Fatalf("expected support guidance in user_message, got %q", msg)
98+
if !strings.Contains(strings.ToLower(msg), "tray") {
99+
t.Fatalf("expected local runtime guidance in user_message, got %q", msg)
100100
}
101101
}
102102

@@ -170,11 +170,11 @@ func TestShouldRunLocalP2P_NoServiceConnected(t *testing.T) {
170170
}
171171
}
172172

173-
func TestShouldRunLocalP2P_ServiceConnectedNormalModeSkips(t *testing.T) {
173+
func TestShouldRunLocalP2P_ServiceConnectedNormalModeRunsWhenServiceModeDisabled(t *testing.T) {
174174
a := &App{}
175175
a.serviceConnectedMode.Store(true)
176-
if a.shouldRunLocalP2P() {
177-
t.Fatal("expected local P2P to be skipped when service is connected in normal mode")
176+
if !a.shouldRunLocalP2P() {
177+
t.Fatal("expected local P2P to run when service mode is globally disabled")
178178
}
179179
}
180180

@@ -186,6 +186,14 @@ func TestShouldRunLocalP2P_ServiceConnectedDebugModeRuns(t *testing.T) {
186186
}
187187
}
188188

189+
func TestShouldUseServiceRuntime_DisabledByGlobalFlag(t *testing.T) {
190+
a := &App{}
191+
a.serviceConnectedMode.Store(true)
192+
if a.shouldUseServiceRuntime() {
193+
t.Fatal("expected shouldUseServiceRuntime=false when windowsServiceModeEnabled=false")
194+
}
195+
}
196+
189197
func TestHeartbeatIntervalFromAgentConfig_DebugForcedInterval(t *testing.T) {
190198
configured := 45
191199
got := heartbeatIntervalFromAgentConfig(AgentConfiguration{

src/app/remote_debug.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,10 @@ func (a *App) requestAgentUpdateCheck(ctx context.Context, source string) error
567567
if source == "" {
568568
source = "manual"
569569
}
570-
if a.serviceConnectedMode.Load() && a.serviceClient != nil {
570+
if !windowsServiceModeEnabled {
571+
return fmt.Errorf("self-update remoto indisponivel: modo Windows Service desativado")
572+
}
573+
if a.shouldUseServiceRuntime() {
571574
if !a.serviceClient.IsConnected() {
572575
if err := a.serviceClient.Connect(ctx); err != nil {
573576
return err

src/app/remote_debug_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func TestHandleAgentRuntimeCommand_UpdateRequiresService(t *testing.T) {
112112
if strings.TrimSpace(output) != "" {
113113
t.Fatalf("expected no output on failure, got %q", output)
114114
}
115-
if !strings.Contains(errText, "Windows Service") {
116-
t.Fatalf("expected service-first error, got %q", errText)
115+
if !strings.Contains(strings.ToLower(errText), "desativado") {
116+
t.Fatalf("expected service-disabled error, got %q", errText)
117117
}
118118
}

src/build/windows/installer/project.nsi

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ Unicode true
124124
!endif
125125
!endif
126126

127+
!ifdef ARG_ENABLE_WINDOWS_SERVICE
128+
!define BUILD_ENABLE_WINDOWS_SERVICE "${ARG_ENABLE_WINDOWS_SERVICE}"
129+
!else
130+
!ifdef ENABLE_WINDOWS_SERVICE
131+
!define BUILD_ENABLE_WINDOWS_SERVICE "1"
132+
!else
133+
!define BUILD_ENABLE_WINDOWS_SERVICE "0"
134+
!endif
135+
!endif
136+
127137
!ifdef ARG_PAYLOAD_URL
128138
!define BUILD_PAYLOAD_URL "${ARG_PAYLOAD_URL}"
129139
!else
@@ -459,8 +469,16 @@ Section
459469
# Salvar configurações do agente
460470
Call SaveAgentConfig
461471

462-
# Registrar e iniciar serviço Windows (modo headless 24/7)
463-
Call RegisterWindowsService
472+
# Registrar regra de firewall para runtime local/P2P.
473+
Call RegisterWindowsFirewallRule
474+
475+
# Opcional: registrar serviço Windows somente quando habilitado em build.
476+
${If} "${BUILD_ENABLE_WINDOWS_SERVICE}" == "1"
477+
Call RegisterWindowsService
478+
${Else}
479+
# Garantir migração limpa removendo serviço legado, se existir.
480+
Call UnregisterWindowsService
481+
${EndIf}
464482

465483
# Registrar autostart da UI via Task Scheduler (At log on of any user)
466484
Call RegisterUIStartupTask
@@ -778,9 +796,6 @@ Function RegisterWindowsService
778796
ExecWait '"$SYSDIR\sc.exe" failure "${DISCOVERY_SERVICE_NAME}" reset= 86400 actions= restart/5000/restart/5000/restart/5000' $R1
779797
ExecWait '"$SYSDIR\sc.exe" description "${DISCOVERY_SERVICE_NAME}" "Discovery background service (multi-user)"' $R1
780798

781-
# Garantir acesso de rede ao executável antes do primeiro start do serviço.
782-
Call RegisterWindowsFirewallRule
783-
784799
# Iniciar serviço após instalação
785800
ExecWait '"$SYSDIR\sc.exe" start "${DISCOVERY_SERVICE_NAME}"' $R1
786801
${If} $R1 != 0
@@ -841,7 +856,13 @@ FunctionEnd
841856
Function UnregisterWindowsService
842857
DetailPrint "Removendo Windows Service ${DISCOVERY_SERVICE_NAME}"
843858
ExecWait '"$SYSDIR\sc.exe" stop "${DISCOVERY_SERVICE_NAME}"' $R0
859+
${If} $R0 != 0
860+
DetailPrint "Aviso: falha (ou service inexistente) ao parar ${DISCOVERY_SERVICE_NAME}. Codigo: $R0"
861+
${EndIf}
844862
ExecWait '"$SYSDIR\sc.exe" delete "${DISCOVERY_SERVICE_NAME}"' $R0
863+
${If} $R0 != 0
864+
DetailPrint "Aviso: falha (ou service inexistente) ao remover ${DISCOVERY_SERVICE_NAME}. Codigo: $R0"
865+
${EndIf}
845866
FunctionEnd
846867

847868
Function un.UnregisterWindowsFirewallRule
@@ -870,7 +891,13 @@ FunctionEnd
870891
Function un.UnregisterWindowsService
871892
DetailPrint "Removendo Windows Service ${DISCOVERY_SERVICE_NAME}"
872893
ExecWait '"$SYSDIR\sc.exe" stop "${DISCOVERY_SERVICE_NAME}"' $R0
894+
${If} $R0 != 0
895+
DetailPrint "Aviso: falha (ou service inexistente) ao parar ${DISCOVERY_SERVICE_NAME}. Codigo: $R0"
896+
${EndIf}
873897
ExecWait '"$SYSDIR\sc.exe" delete "${DISCOVERY_SERVICE_NAME}"' $R0
898+
${If} $R0 != 0
899+
DetailPrint "Aviso: falha (ou service inexistente) ao remover ${DISCOVERY_SERVICE_NAME}. Codigo: $R0"
900+
${EndIf}
874901
FunctionEnd
875902

876903
Function un.UnregisterUIStartupTask

src/frontend/js/app-status.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ function renderStatusError(message) {
186186
}
187187

188188
function renderServiceHealth(health) {
189+
if (health && health.service_only === false) {
190+
var localRuntimeLabel = translate('status.localRuntimeMode');
191+
if (serviceHealthDotEl) serviceHealthDotEl.className = 'agent-status-indicator online';
192+
if (serviceHealthLabelEl) serviceHealthLabelEl.textContent = localRuntimeLabel;
193+
updateTopbarServiceIndicator('online', localRuntimeLabel);
194+
return;
195+
}
196+
189197
if (!health) {
190198
if (serviceHealthDotEl) serviceHealthDotEl.className = 'agent-status-indicator offline';
191199
if (serviceHealthLabelEl) serviceHealthLabelEl.textContent = translate('status.serviceUnavailable');

0 commit comments

Comments
 (0)