feat: add exit confirmation dialog, system tray enhancements, and diagnostic options
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
# Diagnóstico: Janela Principal no Windows 11 (Bisseção e Monitoramento)
|
||||||
|
|
||||||
|
Este documento descreve o procedimento de diagnóstico para isolar a causa de ocultamento nativo da janela principal no Windows 11 ao passar o mouse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Monitor Nativo Win32/DWM (`scripts/diag-window.ps1`)
|
||||||
|
|
||||||
|
O script [scripts/diag-window.ps1](file:///var/www/cybercore/cybercore-signer/scripts/diag-window.ps1) inspeciona a cada 300ms a janela nativa via P/Invoke da Win32 API e DWM:
|
||||||
|
- `IsWindowVisible`: se a janela está visível perante o subsistema Win32;
|
||||||
|
- `DWMWA_CLOAKED`: se o gerenciador gráfico do Windows 11 ocultou a composição;
|
||||||
|
- `IsIconic`: se foi minimizada;
|
||||||
|
- `GetWindowRect`: coordenadas reais (detecta se foi deslocada para fora da tela).
|
||||||
|
|
||||||
|
### Como executar o monitor:
|
||||||
|
Em uma janela separada do PowerShell:
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Roteiro de Bisseção (Fase 2)
|
||||||
|
|
||||||
|
Execute cada um dos comandos abaixo individualmente (garantindo antes que instâncias anteriores foram encerradas com `Stop-Process -Name java -Force` se necessário):
|
||||||
|
|
||||||
|
### Teste A: Desativar Middleware PKCS#11 / SafeSign (`aetpkss1.dll`)
|
||||||
|
Isola hooks nativos do SafeSign e middlewares A3:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-pkcs11=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
*Se a janela NÃO sumir ao passar o mouse, a causa é o hook nativo do middleware PKCS#11.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste B: Plain Stage (Janela livre sem `setResizable(false)` e sem limites min/max)
|
||||||
|
Isola o bug de composição do DWM / Glass no Windows 11 com janelas de tamanho fixo:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.plain-stage=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
*Se a janela NÃO sumir ao passar o mouse, a causa é a restrição fixa de tamanho no Windows 11.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste C: Desativar Efeitos de Sombra no Mouse (`-fx-effect`)
|
||||||
|
Isola recálculo de dropshadow e troca de cursor:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-effects=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste D: Desativar Monitor Dinâmico de Smart Cards (PC/SC)
|
||||||
|
Isola threads em segundo plano de `javax.smartcardio`:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-smartcard-watcher=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Teste E: Desativar Windows Certificate Store (Windows-MY)
|
||||||
|
Isola inicialização de CryptoAPI / SunMSCAPI:
|
||||||
|
```powershell
|
||||||
|
java -Dcybercore.diag.no-wincert=true -jar .\cybercore-signer.jar --dev
|
||||||
|
```
|
||||||
+14
@@ -36,6 +36,17 @@ public class CyberCoreSignerApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// Previne conflitos nativos de UI Automation/Acessibilidade do Glass no Windows 11
|
||||||
|
if (System.getProperty("javafx.accessibility") == null) {
|
||||||
|
System.setProperty("javafx.accessibility", "false");
|
||||||
|
}
|
||||||
|
if (System.getProperty("glass.accessible.force") == null) {
|
||||||
|
System.setProperty("glass.accessible.force", "false");
|
||||||
|
}
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||||
|
logger.error("Exceção não tratada na thread '{}': {}", thread.getName(), throwable.getMessage(), throwable);
|
||||||
|
});
|
||||||
|
|
||||||
parseArguments(args);
|
parseArguments(args);
|
||||||
printBanner();
|
printBanner();
|
||||||
|
|
||||||
@@ -67,6 +78,9 @@ public class CyberCoreSignerApplication {
|
|||||||
try {
|
try {
|
||||||
logger.info("Iniciando interface gráfica desktop...");
|
logger.info("Iniciando interface gráfica desktop...");
|
||||||
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
|
CyberCoreDesktopApplication.setDevelopmentMode(developmentMode);
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() ->
|
||||||
|
logger.warn("JVM em encerramento (sinal externo, System.exit ou fim do aplicativo)."),
|
||||||
|
"cybercore-shutdown-diagnostic"));
|
||||||
Application.launch(CyberCoreDesktopApplication.class, args);
|
Application.launch(CyberCoreDesktopApplication.class, args);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
|
logger.warn("Não foi possível inicializar ambiente gráfico ({}). Continuando em modo console.", t.getMessage());
|
||||||
|
|||||||
+17
-2
@@ -33,12 +33,27 @@ public class CertificateManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
* Cria e inicializa uma instância padrão do CertificateManager com os provedores nativos da plataforma.
|
||||||
|
* Suporta chaves de diagnóstico para isolamento de problemas de middleware:
|
||||||
|
* -Dcybercore.diag.no-wincert=true (não inicializa Windows Certificate Store)
|
||||||
|
* -Dcybercore.diag.no-pkcs11=true (não inicializa PKCS#11 / DLLs nativas)
|
||||||
*/
|
*/
|
||||||
public static CertificateManager createDefault() {
|
public static CertificateManager createDefault() {
|
||||||
CertificateManager manager = new CertificateManager();
|
CertificateManager manager = new CertificateManager();
|
||||||
manager.registerProvider(new WindowsCertificateStoreProvider());
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-wincert")) {
|
||||||
|
logger.warn("Diagnóstico: Provedor Windows Certificate Store DESATIVADO via -Dcybercore.diag.no-wincert=true");
|
||||||
|
} else {
|
||||||
|
manager.registerProvider(new WindowsCertificateStoreProvider());
|
||||||
|
}
|
||||||
|
|
||||||
manager.registerProvider(new Pkcs12CertificateProvider());
|
manager.registerProvider(new Pkcs12CertificateProvider());
|
||||||
manager.registerProvider(new Pkcs11CertificateProvider());
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-pkcs11")) {
|
||||||
|
logger.warn("Diagnóstico: Provedor PKCS#11 DESATIVADO via -Dcybercore.diag.no-pkcs11=true");
|
||||||
|
} else {
|
||||||
|
manager.registerProvider(new Pkcs11CertificateProvider());
|
||||||
|
}
|
||||||
|
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
@@ -63,4 +63,34 @@ class CertificateManagerTest {
|
|||||||
assertThat(byCnpj).isPresent();
|
assertThat(byCnpj).isPresent();
|
||||||
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
|
assertThat(byCnpj.get().id()).isEqualTo("cert-1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve desativar provider PKCS#11 quando chave de diagnóstico estiver ativa")
|
||||||
|
void shouldRespectDiagnosticsNoPkcs11() {
|
||||||
|
System.setProperty("cybercore.diag.no-pkcs11", "true");
|
||||||
|
try {
|
||||||
|
CertificateManager manager = CertificateManager.createDefault();
|
||||||
|
assertThat(manager.getProviders())
|
||||||
|
.extracting(CertificateProvider::getProviderId)
|
||||||
|
.doesNotContain("provider-pkcs11")
|
||||||
|
.contains("provider-a1");
|
||||||
|
} finally {
|
||||||
|
System.clearProperty("cybercore.diag.no-pkcs11");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve desativar provider Windows Certificate Store quando chave de diagnóstico estiver ativa")
|
||||||
|
void shouldRespectDiagnosticsNoWinCert() {
|
||||||
|
System.setProperty("cybercore.diag.no-wincert", "true");
|
||||||
|
try {
|
||||||
|
CertificateManager manager = CertificateManager.createDefault();
|
||||||
|
assertThat(manager.getProviders())
|
||||||
|
.extracting(CertificateProvider::getProviderId)
|
||||||
|
.doesNotContain("provider-win-store")
|
||||||
|
.contains("provider-a1");
|
||||||
|
} finally {
|
||||||
|
System.clearProperty("cybercore.diag.no-wincert");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-12
@@ -117,10 +117,14 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
|
mainAppView.setOnValidateTimestamp(() -> new SignatureValidationDialog(primaryStage, true).show());
|
||||||
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
|
mainAppView.setOnManageSeals(() -> new SealManagementDialog(primaryStage).show());
|
||||||
mainAppView.setOnExit(() -> {
|
mainAppView.setOnExit(() -> {
|
||||||
logger.info("Encerrando CyberCore Signer pela interface gráfica.");
|
logger.info("Ação 'Sair' acionada na interface gráfica.");
|
||||||
primaryStage.close();
|
if (systemTrayManager != null) {
|
||||||
Platform.exit();
|
systemTrayManager.handleWindowCloseRequest();
|
||||||
System.exit(0);
|
} else {
|
||||||
|
primaryStage.close();
|
||||||
|
Platform.exit();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ações do Painel Lateral
|
// Ações do Painel Lateral
|
||||||
@@ -130,9 +134,21 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
|
mainAppView.setOnCertificates(() -> new CertificatesManagerDialog(primaryStage, certificateManager).show());
|
||||||
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
|
mainAppView.setOnAbout(() -> openAboutDialog(primaryStage));
|
||||||
|
|
||||||
// Exibe imediatamente a janela principal em primeiro plano para o usuário
|
// Observabilidade do ciclo de vida da janela
|
||||||
mainAppView.show();
|
primaryStage.showingProperty().addListener((obs, oldVal, newVal) ->
|
||||||
logger.info("Janela principal JavaFX exibida.");
|
logger.info("Visibilidade da janela principal JavaFX alterada: visível={}", newVal));
|
||||||
|
primaryStage.iconifiedProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Estado de minimização da janela JavaFX alterado: minimizada={}", newVal));
|
||||||
|
primaryStage.focusedProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.info("Foco da janela principal alterado: focada={}", newVal));
|
||||||
|
primaryStage.xProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal movida: x={}, y={}", newVal, primaryStage.getY()));
|
||||||
|
primaryStage.yProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal movida: x={}, y={}", primaryStage.getX(), newVal));
|
||||||
|
primaryStage.widthProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal redimensionada: largura={}, altura={}", newVal, primaryStage.getHeight()));
|
||||||
|
primaryStage.heightProperty().addListener((obs, oldVal, newVal) ->
|
||||||
|
logger.debug("Janela principal redimensionada: largura={}, altura={}", primaryStage.getWidth(), newVal));
|
||||||
|
|
||||||
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
|
// Inicializa o servidor local do protocolo CCSP/1 para atender requisições do navegador
|
||||||
initCcspServer(primaryStage);
|
initCcspServer(primaryStage);
|
||||||
@@ -140,12 +156,22 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
|
// Monitoramento em tempo real de conexão/remoção de Smart Cards e Tokens USB
|
||||||
initSmartCardWatcher();
|
initSmartCardWatcher();
|
||||||
|
|
||||||
// Configura bandeja do sistema (System Tray) para operação contínua em segundo plano
|
|
||||||
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
|
this.systemTrayManager = new SystemTrayManager(primaryStage, this::refreshCertificates);
|
||||||
this.systemTrayManager.setup();
|
|
||||||
|
|
||||||
// Carrega os certificados instalados na máquina em segundo plano para não congelar a interface
|
// Carrega os certificados instalados na máquina do usuário de forma estável
|
||||||
CompletableFuture.runAsync(this::refreshCertificates);
|
refreshCertificates();
|
||||||
|
|
||||||
|
// Exibe a janela principal em primeiro plano após os serviços estarem prontos
|
||||||
|
mainAppView.show();
|
||||||
|
logger.info("Janela principal JavaFX exibida.");
|
||||||
|
|
||||||
|
// Configura a bandeja (System Tray) somente depois que a janela JavaFX está visível,
|
||||||
|
// para não inicializar o toolkit AWT concorrentemente com a criação da janela.
|
||||||
|
if (Boolean.getBoolean("cybercore.no.tray")) {
|
||||||
|
logger.warn("System Tray desativado via -Dcybercore.no.tray=true (diagnóstico).");
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> this.systemTrayManager.setup());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshCertificates() {
|
private void refreshCertificates() {
|
||||||
@@ -473,12 +499,23 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
|
this.ccspServer = new CcspServer(this.certificateManager, this.signingService, promptListener);
|
||||||
this.ccspServer.start();
|
this.ccspServer.start();
|
||||||
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
logger.info("Servidor local CCSP/1 inicializado na porta {}", this.ccspServer.getPort());
|
||||||
|
} catch (java.net.BindException be) {
|
||||||
|
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
if (e.getCause() instanceof java.net.BindException || (e.getMessage() != null && e.getMessage().contains("Address already in use"))) {
|
||||||
|
logger.warn("Porta 21325 já está ocupada por outra instância do CyberCore Signer em execução. O agente continuará ativo na interface gráfica.");
|
||||||
|
} else {
|
||||||
|
logger.error("Falha ao inicializar servidor local CCSP/1: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSmartCardWatcher() {
|
private void initSmartCardWatcher() {
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-smartcard-watcher")) {
|
||||||
|
logger.warn("Diagnóstico: Monitor dinâmico de smart cards DESATIVADO via -Dcybercore.diag.no-smartcard-watcher=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.smartCardWatcher = new SmartCardWatcher();
|
this.smartCardWatcher = new SmartCardWatcher();
|
||||||
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
|
this.smartCardWatcher.addListener(new SmartCardWatcher.CardEventListener() {
|
||||||
|
|||||||
+55
-40
@@ -1,5 +1,6 @@
|
|||||||
package br.com.cybercore.signer.ui.tray;
|
package br.com.cybercore.signer.ui.tray;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.view.ExitConfirmationDialog;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -15,8 +16,6 @@ import java.awt.PopupMenu;
|
|||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
import java.awt.SystemTray;
|
import java.awt.SystemTray;
|
||||||
import java.awt.TrayIcon;
|
import java.awt.TrayIcon;
|
||||||
import java.awt.event.MouseAdapter;
|
|
||||||
import java.awt.event.MouseEvent;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -42,12 +41,19 @@ public class SystemTrayManager {
|
|||||||
/**
|
/**
|
||||||
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
|
* Inicializa o ícone e menu de contexto na bandeja do sistema, caso haja suporte.
|
||||||
*/
|
*/
|
||||||
public synchronized boolean setup() {
|
public boolean setup() {
|
||||||
if (!SystemTray.isSupported()) {
|
if (!SystemTray.isSupported()) {
|
||||||
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
|
logger.info("SystemTray não é suportado pelo ambiente atual ou sistema operacional.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// O TrayIcon AWT é criado na thread de eventos do AWT, nunca na thread do JavaFX,
|
||||||
|
// para evitar conflito entre os dois loops de mensagens nativos no Windows.
|
||||||
|
java.awt.EventQueue.invokeLater(this::setupOnAwtThread);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void setupOnAwtThread() {
|
||||||
try {
|
try {
|
||||||
SystemTray tray = SystemTray.getSystemTray();
|
SystemTray tray = SystemTray.getSystemTray();
|
||||||
Image iconImage = createTrayIconImage();
|
Image iconImage = createTrayIconImage();
|
||||||
@@ -78,69 +84,78 @@ public class SystemTrayManager {
|
|||||||
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
|
this.trayIcon = new TrayIcon(iconImage, "CyberCore Signer (CCSP/1 Ativo)", popup);
|
||||||
this.trayIcon.setImageAutoSize(true);
|
this.trayIcon.setImageAutoSize(true);
|
||||||
|
|
||||||
// Clique com o botão esquerdo (single-click) restaura a janela imediatamente
|
// Duplo clique (ActionListener) e clique simples (MouseListener) restauram a janela
|
||||||
this.trayIcon.addMouseListener(new MouseAdapter() {
|
this.trayIcon.addActionListener(e -> {
|
||||||
|
logger.info("Ícone da bandeja acionado (duplo clique).");
|
||||||
|
Platform.runLater(this::showStage);
|
||||||
|
});
|
||||||
|
this.trayIcon.addMouseListener(new java.awt.event.MouseAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void mouseClicked(MouseEvent e) {
|
public void mouseClicked(java.awt.event.MouseEvent e) {
|
||||||
if (e.getButton() == MouseEvent.BUTTON1) {
|
if (e.getButton() == java.awt.event.MouseEvent.BUTTON1) {
|
||||||
|
logger.info("Ícone da bandeja acionado (clique simples).");
|
||||||
Platform.runLater(SystemTrayManager.this::showStage);
|
Platform.runLater(SystemTrayManager.this::showStage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Duplo clique ou acionamento nativo padrão também restaura a janela
|
|
||||||
this.trayIcon.addActionListener(e -> Platform.runLater(this::showStage));
|
|
||||||
|
|
||||||
tray.add(this.trayIcon);
|
tray.add(this.trayIcon);
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
// Intercepta fechamento da janela para ocultar em vez de encerrar
|
// Intercepta fechamento da janela (botão X) para confirmar ação com o usuário
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
stage.setOnCloseRequest(event -> {
|
stage.setOnCloseRequest(event -> {
|
||||||
event.consume();
|
event.consume();
|
||||||
stage.hide();
|
handleWindowCloseRequest();
|
||||||
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info("System Tray configurado com sucesso.");
|
logger.info("System Tray configurado com sucesso.");
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (AWTException e) {
|
} catch (AWTException e) {
|
||||||
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
|
logger.warn("Falha ao registrar ícone no System Tray: {}", e.getMessage());
|
||||||
return false;
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage());
|
logger.warn("Erro ao inicializar System Tray: {}", t.getMessage(), t);
|
||||||
return false;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exibe o diálogo de confirmação ao clicar no 'X' da janela principal,
|
||||||
|
* permitindo minimizar para a bandeja, encerrar definitivamente ou cancelar.
|
||||||
|
*/
|
||||||
|
public void handleWindowCloseRequest() {
|
||||||
|
try {
|
||||||
|
ExitConfirmationDialog dialog = new ExitConfirmationDialog(stage);
|
||||||
|
ExitConfirmationDialog.ExitChoice choice = dialog.showAndGetChoice();
|
||||||
|
|
||||||
|
switch (choice) {
|
||||||
|
case MINIMIZE_TO_TRAY -> {
|
||||||
|
logger.info("Usuário optou por minimizar o CyberCore Signer para a bandeja.");
|
||||||
|
stage.hide();
|
||||||
|
showMessage("CyberCore Signer", "O agente continua ativo na bandeja atendendo aplicações web.", TrayIcon.MessageType.INFO);
|
||||||
|
}
|
||||||
|
case EXIT_COMPLETELY -> {
|
||||||
|
logger.info("Usuário optou por encerrar definitivamente o CyberCore Signer.");
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
case CANCEL -> {
|
||||||
|
logger.debug("Fechamento da janela cancelado pelo usuário.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.warn("Erro ao exibir diálogo de fechamento, minimizando para bandeja por segurança: {}", t.getMessage());
|
||||||
|
stage.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showStage() {
|
public void showStage() {
|
||||||
if (stage == null) {
|
if (stage != null) {
|
||||||
return;
|
logger.info("Restaurando janela principal (visível={}, minimizada={}).",
|
||||||
}
|
stage.isShowing(), stage.isIconified());
|
||||||
|
stage.show();
|
||||||
try {
|
stage.setIconified(false);
|
||||||
if (!stage.isShowing()) {
|
|
||||||
stage.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stage.isIconified()) {
|
|
||||||
stage.setIconified(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
stage.requestFocus();
|
stage.requestFocus();
|
||||||
|
|
||||||
// No Windows (Win32 / DWM), chamadas de toFront() partindo da bandeja do sistema
|
|
||||||
// podem ser suprimidas por políticas de foco do SO (Foreground Lock Timeout).
|
|
||||||
// A elevação momentânea via setAlwaysOnTop força o gerenciador de janelas a trazer
|
|
||||||
// o assinador diretamente para o primeiro plano.
|
|
||||||
stage.setAlwaysOnTop(true);
|
|
||||||
stage.setAlwaysOnTop(false);
|
|
||||||
|
|
||||||
} catch (Throwable t) {
|
|
||||||
logger.debug("Falha secundária ao restaurar stage da bandeja: {}", t.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package br.com.cybercore.signer.ui.view;
|
||||||
|
|
||||||
|
import br.com.cybercore.signer.ui.components.SvgIcons;
|
||||||
|
import br.com.cybercore.signer.ui.theme.ThemeManager;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Diálogo modal de confirmação ao fechar a janela principal do CyberCore Signer.
|
||||||
|
* Permite ao usuário escolher entre manter o agente ativo na bandeja do sistema
|
||||||
|
* ou encerrar completamente a aplicação.
|
||||||
|
*/
|
||||||
|
public final class ExitConfirmationDialog {
|
||||||
|
|
||||||
|
public enum ExitChoice {
|
||||||
|
MINIMIZE_TO_TRAY,
|
||||||
|
EXIT_COMPLETELY,
|
||||||
|
CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Stage stage;
|
||||||
|
private ExitChoice chosenAction = ExitChoice.CANCEL;
|
||||||
|
|
||||||
|
public ExitConfirmationDialog(Stage owner) {
|
||||||
|
this.stage = new Stage();
|
||||||
|
this.stage.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
if (owner != null) {
|
||||||
|
this.stage.initOwner(owner);
|
||||||
|
}
|
||||||
|
this.stage.setTitle("CyberCore Signer — Encerrar ou Manter na Bandeja");
|
||||||
|
this.stage.setResizable(false);
|
||||||
|
this.stage.setMinWidth(520);
|
||||||
|
this.stage.setMinHeight(190);
|
||||||
|
|
||||||
|
initLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initLayout() {
|
||||||
|
VBox root = new VBox(22);
|
||||||
|
root.setPadding(new Insets(24));
|
||||||
|
root.getStyleClass().add("dialog-container");
|
||||||
|
|
||||||
|
// Cabeçalho com ícone e texto explicativo
|
||||||
|
HBox header = new HBox(16);
|
||||||
|
header.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
VBox titleBox = new VBox(4);
|
||||||
|
Label title = new Label("Como você deseja proceder?");
|
||||||
|
title.setStyle("-fx-font-size: 15px; -fx-font-weight: bold;");
|
||||||
|
|
||||||
|
Label subtitle = new Label(
|
||||||
|
"Você pode manter o CyberCore Signer ativo na bandeja do sistema\n" +
|
||||||
|
"para atender assinaturas de aplicações web, ou encerrá-lo por completo."
|
||||||
|
);
|
||||||
|
subtitle.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
||||||
|
|
||||||
|
titleBox.getChildren().addAll(title, subtitle);
|
||||||
|
header.getChildren().addAll(SvgIcons.cyberShieldLogo(38), titleBox);
|
||||||
|
|
||||||
|
// Barra de Ações com os botões
|
||||||
|
HBox actionsRow = new HBox(10);
|
||||||
|
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
Button btnCancel = new Button("Cancelar");
|
||||||
|
btnCancel.getStyleClass().addAll("btn-cancel", "btn-dialog");
|
||||||
|
btnCancel.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.CANCEL;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox spacer = new HBox();
|
||||||
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button btnExit = new Button("Encerrar Definitivamente");
|
||||||
|
btnExit.getStyleClass().addAll("btn-danger", "btn-dialog");
|
||||||
|
btnExit.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.EXIT_COMPLETELY;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
Button btnMinimize = new Button("Minimizar para a Bandeja");
|
||||||
|
btnMinimize.getStyleClass().addAll("btn-primary", "btn-dialog");
|
||||||
|
btnMinimize.setDefaultButton(true);
|
||||||
|
btnMinimize.setOnAction(e -> {
|
||||||
|
chosenAction = ExitChoice.MINIMIZE_TO_TRAY;
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
actionsRow.getChildren().addAll(btnCancel, spacer, btnExit, btnMinimize);
|
||||||
|
|
||||||
|
root.getChildren().addAll(header, actionsRow);
|
||||||
|
|
||||||
|
Scene scene = new Scene(root);
|
||||||
|
ThemeManager.applyTheme(scene);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setOnCloseRequest(e -> chosenAction = ExitChoice.CANCEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExitChoice showAndGetChoice() {
|
||||||
|
stage.showAndWait();
|
||||||
|
return chosenAction;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
-14
@@ -16,6 +16,8 @@ import javafx.scene.layout.Region;
|
|||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ import java.util.Objects;
|
|||||||
@SuppressWarnings("this-escape")
|
@SuppressWarnings("this-escape")
|
||||||
public final class MainAppView {
|
public final class MainAppView {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MainAppView.class);
|
||||||
|
|
||||||
private final Stage stage;
|
private final Stage stage;
|
||||||
private final VBox root = new VBox(16);
|
private final VBox root = new VBox(16);
|
||||||
private final Button themeToggleBtn = new Button();
|
private final Button themeToggleBtn = new Button();
|
||||||
@@ -136,6 +140,14 @@ public final class MainAppView {
|
|||||||
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
getClass().getResource(CyberCoreUiConstants.MAIN_CSS_PATH)
|
||||||
).toExternalForm());
|
).toExternalForm());
|
||||||
|
|
||||||
|
if (Boolean.getBoolean("cybercore.diag.no-effects")) {
|
||||||
|
logger.warn("Diagnóstico: Efeitos/sombras visuais DESATIVADOS via -Dcybercore.diag.no-effects=true");
|
||||||
|
var noEffectsUrl = getClass().getResource("/styles/no-effects.css");
|
||||||
|
if (noEffectsUrl != null) {
|
||||||
|
scene.getStylesheets().add(noEffectsUrl.toExternalForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Configuração e inicialização do Tema
|
// Configuração e inicialização do Tema
|
||||||
ThemeManager.applyTheme(scene);
|
ThemeManager.applyTheme(scene);
|
||||||
updateThemeToggleButton();
|
updateThemeToggleButton();
|
||||||
@@ -144,11 +156,17 @@ public final class MainAppView {
|
|||||||
|
|
||||||
stage.setTitle(CyberCoreUiConstants.APP_NAME);
|
stage.setTitle(CyberCoreUiConstants.APP_NAME);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.setResizable(false);
|
|
||||||
stage.setMinWidth(780);
|
if (Boolean.getBoolean("cybercore.diag.plain-stage")) {
|
||||||
stage.setMinHeight(480);
|
logger.warn("Diagnóstico: Plain Stage ATIVADO via -Dcybercore.diag.plain-stage=true (janela redimensionável, sem restrições min/max)");
|
||||||
stage.setMaxWidth(820);
|
stage.setResizable(true);
|
||||||
stage.setMaxHeight(520);
|
} else {
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.setMinWidth(780);
|
||||||
|
stage.setMinHeight(480);
|
||||||
|
stage.setMaxWidth(820);
|
||||||
|
stage.setMaxHeight(520);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HBox createHeaderBar() {
|
private HBox createHeaderBar() {
|
||||||
@@ -297,17 +315,8 @@ public final class MainAppView {
|
|||||||
|
|
||||||
public void show() {
|
public void show() {
|
||||||
stage.centerOnScreen();
|
stage.centerOnScreen();
|
||||||
if (stage.isIconified()) {
|
|
||||||
stage.setIconified(false);
|
|
||||||
}
|
|
||||||
stage.show();
|
stage.show();
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
stage.requestFocus();
|
|
||||||
try {
|
|
||||||
stage.setAlwaysOnTop(true);
|
|
||||||
stage.setAlwaysOnTop(false);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void hide() {
|
public void hide() {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* CyberCore Signer - Modo Diagnóstico: Remoção de sombras/efeitos visuais dinâmicos */
|
||||||
|
|
||||||
|
.cyber-action-card,
|
||||||
|
.serpro-action-btn,
|
||||||
|
.cyber-side-card,
|
||||||
|
.serpro-side-btn,
|
||||||
|
.cyber-header-bar,
|
||||||
|
.theme-toggle-btn,
|
||||||
|
.cyber-status-badge {
|
||||||
|
-fx-effect: null !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light .cyber-action-card:hover,
|
||||||
|
.theme-light .serpro-action-btn:hover,
|
||||||
|
.theme-dark .cyber-action-card:hover,
|
||||||
|
.theme-dark .serpro-action-btn:hover,
|
||||||
|
.cyber-side-card:hover,
|
||||||
|
.serpro-side-btn:hover,
|
||||||
|
.theme-toggle-btn:hover {
|
||||||
|
-fx-effect: null !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# ==============================================================================
|
||||||
|
# CyberCore Signer - Diagnóstico e Monitor de Janela Win32 / DWM
|
||||||
|
# Suporta matar instâncias anteriores órfãs e monitorar a janela real.
|
||||||
|
# Uso simples:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1
|
||||||
|
# Ou iniciando o app automaticamente junto com o monitor:
|
||||||
|
# powershell -ExecutionPolicy Bypass -File .\scripts\diag-window.ps1 -AutoLaunch
|
||||||
|
# ==============================================================================
|
||||||
|
param (
|
||||||
|
[switch]$AutoLaunch,
|
||||||
|
[string]$ExtraArgs = "--dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " CyberCore Signer - Monitor de Janela Win32 / DWM" -ForegroundColor Cyan
|
||||||
|
Write-Host "============================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 1. Encerra processos java zumbis anteriores que possam estar travando a porta 21325 ou a bandeja
|
||||||
|
$oldProcs = Get-Process -Name java -ErrorAction SilentlyContinue
|
||||||
|
if ($oldProcs) {
|
||||||
|
Write-Host "[INFO] Encerrando $($oldProcs.Count) processo(s) Java em segundo plano para limpar o ambiente..." -ForegroundColor Yellow
|
||||||
|
$oldProcs | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||||
|
Start-Sleep -Milliseconds 800
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature = @"
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public class Win32WindowWatcher {
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT {
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||||
|
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool IsIconic(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
|
[DllImport("dwmapi.dll")]
|
||||||
|
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out int pvAttribute, int cbAttribute);
|
||||||
|
|
||||||
|
public const int DWMWA_CLOAKED = 14;
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
Add-Type -TypeDefinition $signature -PassThru | Out-Null
|
||||||
|
|
||||||
|
if ($AutoLaunch) {
|
||||||
|
Write-Host "[LAUNCH] Iniciando cybercore-signer.jar com argumentos: $ExtraArgs" -ForegroundColor Green
|
||||||
|
$jarPath = ".\cybercore-signer.jar"
|
||||||
|
if (-not (Test-Path $jarPath)) {
|
||||||
|
$jarPath = ".\modules\cybercore-app\target\cybercore-signer.jar"
|
||||||
|
}
|
||||||
|
Start-Process -FilePath "java" -ArgumentList "-jar", $jarPath, $ExtraArgs -PassThru | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Aguardando janela principal abrir (verificando processos e Win32)..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
$lastState = ""
|
||||||
|
$found = $false
|
||||||
|
$monitoredHwnd = [IntPtr]::Zero
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
# Procura via FindWindow pelo título ou pelo MainWindowHandle do processo Java
|
||||||
|
$hWnd = [Win32WindowWatcher]::FindWindow($null, "CyberCore Signer")
|
||||||
|
|
||||||
|
if ($hWnd -eq [IntPtr]::Zero) {
|
||||||
|
$javaProcs = Get-Process -Name java -ErrorAction SilentlyContinue
|
||||||
|
foreach ($p in $javaProcs) {
|
||||||
|
if ($p.MainWindowHandle -ne [IntPtr]::Zero) {
|
||||||
|
$hWnd = $p.MainWindowHandle
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($hWnd -ne [IntPtr]::Zero -and [Win32WindowWatcher]::IsWindow($hWnd)) {
|
||||||
|
if (-not $found -or $hWnd -ne $monitoredHwnd) {
|
||||||
|
$found = $true
|
||||||
|
$monitoredHwnd = $hWnd
|
||||||
|
Write-Host "[OK] JANELA DETECTADA! HWND: $hWnd. Monitorando em tempo real a cada 200ms..." -ForegroundColor Green
|
||||||
|
Write-Host ">>> Passe o mouse sobre a janela agora para observar o que o Windows faz. <<<" -ForegroundColor Magenta
|
||||||
|
}
|
||||||
|
|
||||||
|
$visible = [Win32WindowWatcher]::IsWindowVisible($hWnd)
|
||||||
|
$iconic = [Win32WindowWatcher]::IsIconic($hWnd)
|
||||||
|
$foreground = ([Win32WindowWatcher]::GetForegroundWindow() -eq $hWnd)
|
||||||
|
|
||||||
|
$rect = New-Object Win32WindowWatcher+RECT
|
||||||
|
[Win32WindowWatcher]::GetWindowRect($hWnd, [ref]$rect) | Out-Null
|
||||||
|
$w = $rect.Right - $rect.Left
|
||||||
|
$h = $rect.Bottom - $rect.Top
|
||||||
|
|
||||||
|
$cloakedVal = 0
|
||||||
|
[Win32WindowWatcher]::DwmGetWindowAttribute($hWnd, [Win32WindowWatcher]::DWMWA_CLOAKED, [ref]$cloakedVal, 4) | Out-Null
|
||||||
|
$cloaked = ($cloakedVal -ne 0)
|
||||||
|
|
||||||
|
$currentState = "Visible=$visible | Cloaked(DWM)=$cloaked | Minimized=$iconic | Focused=$foreground | Rect=($($rect.Left),$($rect.Top)) ${w}x${h}"
|
||||||
|
|
||||||
|
if ($currentState -ne $lastState) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
|
||||||
|
if (-not $visible -or $cloaked -or $w -le 0 -or $h -le 0) {
|
||||||
|
Write-Host "[$timestamp] [ALERTA-JANELA-SUMIU] $currentState" -ForegroundColor Red
|
||||||
|
} else {
|
||||||
|
Write-Host "[$timestamp] [STATUS] $currentState" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
$lastState = $currentState
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ($found) {
|
||||||
|
$timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
|
||||||
|
Write-Host "[$timestamp] [DESTRUÍDA/FECHADA] O handle da janela não existe mais no Windows!" -ForegroundColor Red
|
||||||
|
$found = $false
|
||||||
|
$lastState = ""
|
||||||
|
$monitoredHwnd = [IntPtr]::Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user