feat(rede): nome, porta, saída e TTL em tempo real
A sala acompanha a interface escolhida, o TCP deixa de travar a janela e a aba de um peer continua fechável quando o IP muda. O guia do colega usa os saltos do traceroute como TTL mínimo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
baaf7582d3
commit
22540eaffb
31 files changed
+1179
-95
No files matched your search
+2
-2
@@ -17,14 +17,14 @@ Convenções editoriais: [documentation/00_convencoes.md](documentation/00_conve
|
||||
| Leitor | Trilha |
|
||||
| --- | --- |
|
||||
| **Primeiro uso** | [00_quickstart](documentation/00_quickstart.md) → [09_sistema_de_plugins](documentation/09_sistema_de_plugins.md) → [10_tutorial_plugin](documentation/10_tutorial_plugin.md) → [11_plugins_referencia](documentation/11_plugins_referencia.md) |
|
||||
| **Redes (conceito + código)** | [16_fundamentacao_redes](documentation/16_fundamentacao_redes.md) → [05](documentation/05_udp_multicast.md)–[08](documentation/08_heartbeat_e_presenca.md) → [appendix_portas_e_multicast](documentation/appendix_portas_e_multicast.md) |
|
||||
| **Redes (conceito + código)** | [16_fundamentacao_redes](documentation/16_fundamentacao_redes.md) → [05](documentation/05_udp_multicast.md)–[08](documentation/08_heartbeat_e_presenca.md) → [appendix_portas_e_multicast](documentation/appendix_portas_e_multicast.md) → [17_teste_com_colega](documentation/17_teste_com_colega.md) |
|
||||
| **Desenvolvedor núcleo** | [13_guia_desenvolvimento](documentation/13_guia_desenvolvimento.md) → [14_arquitetura_e_manutencao](documentation/14_arquitetura_e_manutencao.md) → [04_inicializacao_e_gui](documentation/04_inicializacao_e_gui.md) |
|
||||
|
||||
## Manutenção rápida
|
||||
|
||||
| Mudança | Onde documentar |
|
||||
| --- | --- |
|
||||
| UDP/TCP, multicast, portas | 05, 06, appendix |
|
||||
| UDP/TCP, multicast, portas | 05, 06, appendix, 17 |
|
||||
| SPI / plugins | 09, 10 |
|
||||
| Decisão de arquitetura | decision-register |
|
||||
| Débito técnico | engineering_backlog |
|
||||
@@ -22,5 +22,10 @@
|
||||
| D22 | `PeerIpv4` para IP de peer na UI | No Windows, `InetAddress.getHostAddress()` pode devolver IPv6-mapeado e sufixo de zona (`%n`); chat e roteamento usam IPv4 pontilhado | [appendix_portas_e_multicast.md](documentation/appendix_portas_e_multicast.md) |
|
||||
| D23 | Foco de atividade e badges | **Adiado na v1** — núcleo (`PeerActivityFocus` / `PeerActivityInbox`) mantido; UI sem badges; recebimento passivo em sub-abas (`addTabIfAbsent`) | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||
| D24 | Avisos de sessão (`SessionNoticeMessage`) | Início/fim de atividades (ex. `screen_cast` END ao parar share); extensível por plugin/canal | [09_sistema_de_plugins.md](documentation/09_sistema_de_plugins.md) |
|
||||
| D25 | Nome novo no heartbeat já existente | A lista do colega atualiza o `User` quando o `userName` muda | [08_heartbeat_e_presenca.md](documentation/08_heartbeat_e_presenca.md) |
|
||||
| D26 | Troca de porta sai da sala | Goodbye na porta antiga, lista local zerada, anúncio na porta nova | [appendix_portas_e_multicast.md](documentation/appendix_portas_e_multicast.md) |
|
||||
| D27 | TTL padrão 1, ajustável de 1 a 32 | 1 permanece na sub-rede local; valor maior é opção de configuração | [appendix_portas_e_multicast.md](documentation/appendix_portas_e_multicast.md) |
|
||||
| D28 | Saída Automático, Cabo ou Wi-Fi | Multicast, IP exibido e TCP de saída usam a interface escolhida | [appendix_portas_e_multicast.md](documentation/appendix_portas_e_multicast.md) |
|
||||
| D29 | Connect TCP com prazo de 5 s fora da EDT | Aviso só enquanto não há socket aberto para aquele IP | [06_tcp_conexoes.md](documentation/06_tcp_conexoes.md) |
|
||||
|
||||
Novas decisões: adicionar linha nesta tabela e detalhar no arquivo técnico correspondente.
|
||||
@@ -67,20 +67,20 @@ A interface gráfica é Swing, organizada em shell (`pitiupi.presentation.shell`
|
||||
|
||||
Plugins expõem `getName()` (técnico), `getIdentifier()` (SPI) e `getDisplayName()` (texto na galeria e nas abas).
|
||||
|
||||
A aba **Configurações** mostra em cima **Sua conexão** (nome, porta, IP local, ID da instância, contagem de peers; campos selecionáveis para copiar manualmente) e embaixo a tabela de JARs instalados (altura reduzida).
|
||||
A aba **Configurações** mostra em cima **Sua conexão**: nome e porta editáveis, saída (Automático, Cabo ou Wi-Fi), TTL, IP local, interface em uso, ID da instância e contagem de peers. **Aplicar** grava a preferência e, se porta, saída ou TTL mudaram, reabre a rede na hora. IP e ID continuam somente leitura. Embaixo fica a tabela de JARs instalados.
|
||||
|
||||
Durante a inicialização, cada plugin recebe `ConnectionManager` e `PluginWorkspace`
|
||||
no método `createPlugin()`. Menus multicast na barra superior não são mais usados;
|
||||
ações para todos os peers migram para **peer actions** (contexto na lista lateral).
|
||||
Ao iniciar, só as abas **Plugins** e **Configurações** ficam visíveis. Cada plugin registra um painel com `bindShellWorkspaceContent`; a aba abre ao clicar na galeria ou via peer action. Referência: **Conversa na rede** (`ChatPanel`) e **Compartilhar tela** (`ScreenCastPanel`).
|
||||
|
||||
O **chat** usa campo de mensagem compacto (~2 linhas, quebra de linha; barra de rolagem vertical só se o texto não couber). O histórico ocupa a maior parte da altura da aba (divisor proporcional ao abrir a janela).
|
||||
O **chat** usa campo de mensagem compacto (~2 linhas, quebra de linha; barra de rolagem vertical só se o texto não couber). O histórico ocupa a maior parte da altura da aba (divisor proporcional ao abrir a janela). Cada pessoa tem uma sub-aba; o × fecha essa sub-aba mesmo com outras ao lado. Se a pessoa muda de cabo para Wi-Fi, a sub-aba já aberta passa a usar o IP novo em vez de aparecer uma segunda vez com o mesmo nome.
|
||||
|
||||
Canal TCP privado do chat fica no mesmo `ChatPanel` (histórico separado por conversa: sala geral, peer por IP, privado com toggle). O núcleo mantém `PeerActivityInbox` para atividade por peer; indicadores visuais completos na lista lateral ainda não fazem parte da v1 (**D23**).
|
||||
|
||||
Ao fechar a aba superior de **Compartilhar tela** (×), o núcleo chama `Plugin.onShellTabClosed()` — o plugin interrompe captura e envio (toggle desligado). Reabrir pela galeria deve mostrar compartilhamento inativo.
|
||||
|
||||
**TCP (transporte)** envia uma mensagem serializada pelo núcleo (`sendTcp`), na porta do login. **TCP privado** (toggle) abre conexão dedicada ao plugin entre dois peers; com o toggle ativo, o combo de transporte fica desabilitado e o envio usa esse canal. Na UI só se informa o **IP** do peer: UDP/TCP unicast do Pitiupi usa sempre a **mesma porta** que você escolheu no login (convenção de sala: todos com a mesma porta, ex. 5000 e 5001 em testes no mesmo PC).
|
||||
**TCP (transporte)** envia uma mensagem serializada pelo núcleo, na porta desta instância. A primeira conexão com um IP que ainda não tem socket aberto corre fora da thread da janela: o chat mostra “Conectando em …”, espera no máximo cerca de 5 s e, se falhar, avisa uma vez. Mensagens seguintes no mesmo socket não repetem esse aviso. **TCP privado** (toggle) abre conexão dedicada ao plugin entre dois peers, com o mesmo aviso só na abertura. Na UI só se informa o **IP** do peer: UDP/TCP unicast do Pitiupi usa sempre a **mesma porta** desta instância (convenção de sala: todos com a mesma porta, ex. 5000 e 5001 em testes no mesmo PC).
|
||||
|
||||
Textos da UI (destino, combo de forma de envio e linha **“Como esta mensagem vai”**) usam linguagem simples e o termo técnico entre parênteses (multicast UDP, UDP unicast, TCP). Na sala, o transporte não é só multicast: o combo pode escolher cópias unicast ou TCP para cada pessoa, conforme o plugin.
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@ Durante sua inicialização, o `SocketUDP`:
|
||||
2. cria um `MulticastSocket` na **porta escolhida no login** (mesma porta TCP da sala);
|
||||
3. configura os buffers de envio e recepção;
|
||||
4. habilita o reuso do endereço;
|
||||
5. ingressa no grupo multicast.
|
||||
5. define o TTL (padrão **1**, alterável na aba Configurações) e a interface de saída;
|
||||
6. ingressa no grupo multicast.
|
||||
|
||||
O tamanho dos buffers de comunicação é configurado para `256000` bytes.
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ múltiplas mensagens.
|
||||
|
||||
A arquitetura utiliza uma conexão TCP persistente por endereço de peer.
|
||||
|
||||
**Teoria:** `connect` sem prazo pode ficar parado até o sistema operacional desistir. **No Pitiupi:** `ConnectionFactory` usa `Socket.connect` com prazo de **5 s** (`CONNECT_TIMEOUT_MS`). O chat dispara essa abertura numa thread do núcleo (`sendTcpAsync` / `connectPluginTcpAsync`) e só mostra “Conectando em …” quando ainda não existe socket aberto para aquele IP. **Limitação:** o prazo não é configurável na tela; o timeout de leitura depois do handshake continua longo (minutos).
|
||||
|
||||
Ao mudar porta, saída ou TTL, o `ConnectionManager` envia goodbye, fecha as TCP locais e reabre o `ServerSocket`. Se a porta nova não abre, a sala anterior é restaurada.
|
||||
|
||||
O `TcpConnectionManager` mantém um mapa `InetAddress` → `TcpConnection`, permitindo reutilizar a mesma conexão para vários envios ao mesmo peer.
|
||||
|
||||
### SocketTCP
|
||||
|
||||
@@ -72,7 +72,13 @@ Lista de peers ativos
|
||||
| Timeout de ausência | ~15 s | Peer some após queda ou fechamento sem goodbye |
|
||||
| Goodbye | multicast ao sair | Remove peer mais rápido que só timeout |
|
||||
|
||||
Em **Wi‑Fi de laboratório**, heartbeat usa o mesmo multicast que o chat na sala geral — mesma porta no login, mesma sub-rede, sem isolamento AP→AP. Ver [appendix_portas_e_multicast.md](appendix_portas_e_multicast.md).
|
||||
Em **Wi‑Fi de laboratório**, heartbeat usa o mesmo multicast que o chat na sala geral — mesma porta, mesma sub-rede, sem isolamento AP→AP. Ver [appendix_portas_e_multicast.md](appendix_portas_e_multicast.md) e [17_teste_com_colega.md](17_teste_com_colega.md).
|
||||
|
||||
### Nome e troca de sala
|
||||
|
||||
O nome vai dentro do `HeartbeatMessage` que já existe. Se o nome mudar, o receptor atualiza o peer na lista e avisa a interface; não há outra mensagem só para isso.
|
||||
|
||||
Trocar a porta (ou a saída, ou o TTL) envia `GoodbyeMessage` na sala atual, esvazia a lista local e anuncia de imediato depois do novo socket. Quem ficou na porta antiga deixa de ver esta instância. Quem já está na porta nova aparece no anúncio seguinte. Se o goodbye se perder, o timeout de cerca de 15 s ainda remove o peer.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Teste com um colega
|
||||
|
||||
**Teoria:** dois processos só se descobrem se compartilham a mesma sub-rede, a mesma porta de aplicação e um caminho em que o multicast da LAN não seja filtrado. **No Pitiupi:** a lista **Usuários nesta rede** enche com heartbeats UDP. **Limitação:** o Pitiupi não executa `ping` nem `traceroute` do sistema; esses comandos servem para separar problema de rede de problema do aplicativo.
|
||||
|
||||
Os dois Pitiupi precisam usar a **mesma porta** (na sala, em geral **5000**). Portas diferentes são salas diferentes: quem está em 5001 não aparece para quem ficou em 5000.
|
||||
|
||||
## O que conferir na aplicação
|
||||
|
||||
Abra a aba **Configurações**, seção **Sua conexão**:
|
||||
|
||||
- **IP local** e o nome da **interface** (por exemplo `eth0 · 192.168.0.15`).
|
||||
- **Porta** em que esta instância escuta UDP e TCP.
|
||||
- **Saída:** Automático, Cabo ou Wi-Fi. Os dois colegas precisam estar na mesma rede dessa interface.
|
||||
- **TTL:** o padrão é **1**. O número de saltos do `tracert` (ou `traceroute`) até o IP do colega é o mínimo a colocar aqui. Se a linha 1 já é o IP dele, use **1**. Cada roteador no meio soma 1, até o máximo **32**. O teste na mesma sub-rede usa 1.
|
||||
|
||||
O nome na rede também se altera aí. O colega passa a ver o nome novo no próximo anúncio (até cerca de 5 s), sem você sair e voltar da lista.
|
||||
|
||||
## O que conferir no sistema
|
||||
|
||||
Substitua `5000` e o IP do colega pelos valores da aba Configurações.
|
||||
|
||||
### Windows
|
||||
|
||||
- IP: `ipconfig` e o IPv4 do adaptador Ethernet ou Wi-Fi.
|
||||
- Porta em escuta: `netstat -ano | findstr :5000`
|
||||
- Alcance: `ping` no IP do colega.
|
||||
- Caminho: `tracert` no IP do colega. Conte as linhas numeradas até o IP dele: esse número é o TTL mínimo na aba Configurações.
|
||||
- Porta TCP do aplicativo: no PowerShell, `Test-NetConnection -ComputerName IP -Port 5000`
|
||||
|
||||
### Linux
|
||||
|
||||
- IP: `ip -4 addr`
|
||||
- Porta em escuta: `ss -lntu | grep 5000`
|
||||
- Alcance: `ping -c 3 IP`
|
||||
- Caminho: `traceroute IP` ou `tracepath IP`. Conte os saltos até o IP do colega: esse número é o TTL mínimo.
|
||||
- Porta TCP do aplicativo: `nc -vz -w 3 IP 5000` (ou `ncat`)
|
||||
|
||||
## Como ler o resultado
|
||||
|
||||
- **O ping falha.** O problema está na rede entre os PCs: sub-rede diferente, cabo, Wi-Fi ou isolamento de clientes no roteador. O Pitiupi não contorna isso.
|
||||
- **O ping funciona e a porta TCP não abre.** Firewall, porta errada ou o outro Pitiupi não está escutando. Confira a porta na aba Configurações e libere UDP e TCP nessa porta.
|
||||
- **O ping com TTL 1 responde e a lista só mostra Todos.** A descoberta é multicast, e um TTL baixo não é a causa quando o `tracert` tem um salto só. Confira porta igual, a mesma saída (os dois no cabo ou os dois no Wi-Fi; `169.254` e `192.168` ao mesmo tempo não se veem), firewall de UDP e isolamento de clientes. Um lado no WSL e o outro no Windows costuma impedir o multicast: os dois peers precisam estar no mesmo tipo de ambiente.
|
||||
- **O `tracert` tem mais de um salto e a lista fica vazia.** Suba o TTL em Configurações até o número de saltos (máximo 32) e aplique nos dois. Um roteador no meio só entrega o multicast se estiver disposto a encaminhar o grupo.
|
||||
- **O colega está na lista e o TCP avisa que não conectou.** A tentativa de abrir o socket espera no máximo cerca de **5 s** e a janela continua utilizável. Veja firewall de TCP ou se a porta mudou no meio do caminho.
|
||||
|
||||
A lista espera cerca de **5 s** entre anúncios e remove quem fica cerca de **15 s** sem heartbeat. Ao trocar de porta, esta instância envia goodbye na sala antiga e passa a anunciar na porta nova.
|
||||
|
||||
Detalhe de portas, interface e TTL: [appendix_portas_e_multicast.md](appendix_portas_e_multicast.md).
|
||||
@@ -8,7 +8,7 @@
|
||||
| --- | --- |
|
||||
| **Início** | [00_quickstart](00_quickstart.md), [02_modulos](02_modulos_do_repositorio.md), [03_build](03_build_ant_netbeans.md) |
|
||||
| **Arquitetura e GUI** | [04_inicializacao_e_gui](04_inicializacao_e_gui.md), [13_guia_desenvolvimento](13_guia_desenvolvimento.md), [14_arquitetura_e_manutencao](14_arquitetura_e_manutencao.md) |
|
||||
| **Rede** | [05_udp_multicast](05_udp_multicast.md), [06_tcp_conexoes](06_tcp_conexoes.md), [07_mensagens](07_mensagens_e_serializacao.md), [08_heartbeat](08_heartbeat_e_presenca.md), [16_fundamentacao_redes](16_fundamentacao_redes.md), [appendix_portas_e_multicast](appendix_portas_e_multicast.md) |
|
||||
| **Rede** | [05_udp_multicast](05_udp_multicast.md), [06_tcp_conexoes](06_tcp_conexoes.md), [07_mensagens](07_mensagens_e_serializacao.md), [08_heartbeat](08_heartbeat_e_presenca.md), [16_fundamentacao_redes](16_fundamentacao_redes.md), [17_teste_com_colega](17_teste_com_colega.md), [appendix_portas_e_multicast](appendix_portas_e_multicast.md) |
|
||||
| **Plugins** | [09_sistema_de_plugins](09_sistema_de_plugins.md), [10_tutorial_plugin](10_tutorial_plugin.md), [11_plugins_referencia](11_plugins_referencia.md), [12_transporte_e_peer_actions](12_menus_unicast_multicast.md) |
|
||||
| **Qualidade** | [15_testes_e_gate](15_testes_e_gate.md), [engineering_backlog](engineering_backlog.md) |
|
||||
|
||||
@@ -23,4 +23,5 @@
|
||||
| 9–12 | 09–12 | Plugins, tutorial genérico, transporte na UI |
|
||||
| 13–15 | 13–15 | Dev, manutenção, testes |
|
||||
| 16 | [16_fundamentacao_redes.md](16_fundamentacao_redes.md) | Fundamentação redes ↔ código |
|
||||
| 17 | [17_teste_com_colega.md](17_teste_com_colega.md) | IP, porta, ping e lista entre dois PCs |
|
||||
| — | [appendix_portas_e_multicast.md](appendix_portas_e_multicast.md) | Portas, Wi‑Fi, WSL |
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
## Configuração
|
||||
|
||||
- **Nome** e **porta** definidos no diálogo de login (`Login`).
|
||||
- A mesma porta é usada para UDP (multicast socket) e TCP (`ServerSocket`).
|
||||
- Preferências persistidas localmente (`java.util.prefs`).
|
||||
- **Nome** e **porta** começam no diálogo de login e podem ser alterados na aba **Configurações** sem reiniciar. A alteração vale na hora.
|
||||
- A mesma porta é usada para UDP (multicast socket) e TCP (`ServerSocket`). Trocar a porta sai da sala antiga (goodbye) e passa a procurar colegas na porta nova.
|
||||
- **Saída:** Automático (cabo antes de Wi-Fi), só Cabo ou só Wi-Fi. O multicast e o IP exibido seguem essa escolha. O TCP de saída também sai pelo IPv4 dessa interface.
|
||||
- **TTL** do multicast: padrão **1** (só a rede local). Na Configurações, de 1 a 32.
|
||||
- Preferências persistidas localmente (`java.util.prefs`): nome, porta, TTL e saída. `config.properties` não guarda esses campos.
|
||||
|
||||
## Multicast
|
||||
|
||||
@@ -31,7 +33,9 @@ Executar o Pitiupi no WSL e peers no Windows (ou vice-versa) pode falhar em mult
|
||||
|
||||
## Escolha de interface (multicast)
|
||||
|
||||
O núcleo associa o socket multicast à interface de rede preferida: **cabo (Ethernet) antes de Wi‑Fi**; em cabo com link-local APIPA, prioriza IPv4 **`169.254.*`**. Isso não substitui a exigência de **mesma sub-rede** entre os dois PCs — `169.254.x` e `192.168.x` ao mesmo tempo em hosts diferentes não compartilham heartbeat.
|
||||
No modo **Automático**, o núcleo associa o socket multicast à interface preferida: **cabo (Ethernet) antes de Wi‑Fi**; em cabo com link-local APIPA, prioriza IPv4 **`169.254.*`**. **Cabo** ou **Wi-Fi** na aba Configurações restringe a essa família (nome da interface ou rótulo `Ethernet` / `Wi-Fi`). Isso não substitui a exigência de **mesma sub-rede** entre os dois PCs — `169.254.x` e `192.168.x` ao mesmo tempo em hosts diferentes não compartilham heartbeat.
|
||||
|
||||
Roteiro para achar o próprio IP, a porta e o motivo de a lista ficar vazia: [17_teste_com_colega.md](17_teste_com_colega.md).
|
||||
|
||||
## IP do peer na UI (Windows)
|
||||
|
||||
@@ -51,4 +55,4 @@ Ao abrir conversa pela lista de presença, o campo de destino usa **`PeerIpv4`**
|
||||
| --- | --- |
|
||||
| Lista só com **Todos**, sem peers | Porta igual? Mesma sub-rede? Firewall UDP? AP isolation? WSL num dos PCs? |
|
||||
| Plugin não recebe | Desserialização / SPI / JAR em `PITIUPI_HOME/plugins` |
|
||||
| TCP falha | Peer visível na lista? IP correto? |
|
||||
| TCP falha | Peer visível na lista? IP correto? A tentativa avisa em cerca de 5 s e não trava a janela |
|
||||
@@ -11,10 +11,19 @@ import pitiupi.models.Messages.GoodbyeMessage;
|
||||
import pitiupi.models.Messages.HeartbeatMessage;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.net.tcp.SocketTCP;
|
||||
import pitiupi.net.tcp.TcpAttemptListener;
|
||||
import pitiupi.net.tcp.TcpConnectNotice;
|
||||
import pitiupi.net.tcp.TcpConnection;
|
||||
import pitiupi.net.udp.SocketUDP;
|
||||
import pitiupi.presentation.shell.LocalNetworkInfo;
|
||||
import pitiupi.presentation.shell.NetworkPreferences;
|
||||
import pitiupi.presentation.shell.PeerIpv4;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class ConnectionManager {
|
||||
@@ -29,10 +38,18 @@ public class ConnectionManager {
|
||||
private final PeerActivityInbox peerActivityInbox;
|
||||
private final PeerActivityFocus peerActivityFocus;
|
||||
|
||||
private final Object socketsLock = new Object();
|
||||
private final ExecutorService tcpConnectPool = Executors.newFixedThreadPool(2, runnable -> {
|
||||
Thread thread = new Thread(runnable, "pitiupi-tcp-connect");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
private SocketTCP socketTCP;
|
||||
private SocketUDP socketUDP;
|
||||
private String username;
|
||||
private int port;
|
||||
private int multicastTtl;
|
||||
private volatile boolean shutdownComplete;
|
||||
private Consumer<String> inviteAcceptedUiListener = pluginId -> {
|
||||
};
|
||||
@@ -42,10 +59,15 @@ public class ConnectionManager {
|
||||
|
||||
this.username = username;
|
||||
this.port = port;
|
||||
NetworkPreferences.applyEgressBeforeSockets();
|
||||
this.multicastTtl = NetworkPreferences.loadTtl();
|
||||
|
||||
pitiupiIdentifier = "Pitiupi.Alice.2026";
|
||||
|
||||
reconnect();
|
||||
String bindError = openSockets();
|
||||
if (bindError != null) {
|
||||
System.err.println(bindError);
|
||||
}
|
||||
|
||||
heartbeatManager = new HeartbeatManager(this);
|
||||
peerActionBroker = new PeerActionBroker();
|
||||
@@ -106,25 +128,123 @@ public class ConnectionManager {
|
||||
}
|
||||
shutdownComplete = true;
|
||||
heartbeatManager.sendGoodbyeAndStop();
|
||||
if (socketUDP != null) {
|
||||
socketUDP.close();
|
||||
closeSockets();
|
||||
tcpConnectPool.shutdownNow();
|
||||
}
|
||||
if (socketTCP != null) {
|
||||
socketTCP.close();
|
||||
|
||||
public int getMulticastTtl() {
|
||||
return multicastTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sai da sala atual, reabre UDP e TCP e anuncia de novo.
|
||||
*
|
||||
* @return {@code null} quando a sala nova ficou de pé; mensagem de erro quando a configuração anterior foi mantida.
|
||||
*/
|
||||
public String applyNetwork(int newPort, int newTtl, LocalNetworkInfo.EgressMode mode) {
|
||||
if (newPort < 1 || newPort > 65535) {
|
||||
return "Porta inválida.";
|
||||
}
|
||||
LocalNetworkInfo.EgressMode requested = mode == null ? LocalNetworkInfo.EgressMode.AUTO : mode;
|
||||
if (!LocalNetworkInfo.canUse(requested)) {
|
||||
if (requested == LocalNetworkInfo.EgressMode.WIRED) {
|
||||
return "Não há interface de cabo disponível.";
|
||||
}
|
||||
return "Não há interface de Wi-Fi disponível.";
|
||||
}
|
||||
int ttl = NetworkPreferences.clampTtl(newTtl);
|
||||
synchronized (socketsLock) {
|
||||
int oldPort = this.port;
|
||||
int oldTtl = this.multicastTtl;
|
||||
LocalNetworkInfo.EgressMode oldMode = LocalNetworkInfo.getEgressMode();
|
||||
|
||||
heartbeatManager.sendGoodbye();
|
||||
heartbeatManager.clearPeers();
|
||||
closeSockets();
|
||||
|
||||
this.port = newPort;
|
||||
this.multicastTtl = ttl;
|
||||
LocalNetworkInfo.setEgressMode(requested);
|
||||
String error = openSockets();
|
||||
if (error != null) {
|
||||
this.port = oldPort;
|
||||
this.multicastTtl = oldTtl;
|
||||
LocalNetworkInfo.setEgressMode(oldMode);
|
||||
String rollback = openSockets();
|
||||
heartbeatManager.announceNow();
|
||||
if (rollback != null) {
|
||||
return error + " Também não foi possível restaurar a porta " + oldPort + ".";
|
||||
}
|
||||
return error;
|
||||
}
|
||||
heartbeatManager.announceNow();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public TcpConnection connectPluginTcp(InetAddress destinationAdress, String senderId, String receiverId) {
|
||||
return socketTCP.createPrivateConnection(destinationAdress, senderId, receiverId);
|
||||
SocketTCP tcp = currentTcp();
|
||||
if (tcp == null) {
|
||||
return null;
|
||||
}
|
||||
return tcp.createPrivateConnection(destinationAdress, senderId, receiverId);
|
||||
}
|
||||
|
||||
public void connectPluginTcpAsync(String ipText, String senderId, String receiverId, TcpAttemptListener listener) {
|
||||
tcpConnectPool.execute(() -> {
|
||||
InetAddress address = resolve(ipText);
|
||||
String endpoint = endpointLabel(address, ipText);
|
||||
if (address == null) {
|
||||
deliverFailure(listener, endpoint);
|
||||
return;
|
||||
}
|
||||
deliverConnecting(listener, endpoint);
|
||||
TcpConnection connection = connectPluginTcp(address, senderId, receiverId);
|
||||
if (connection == null) {
|
||||
deliverFailure(listener, endpoint);
|
||||
return;
|
||||
}
|
||||
deliverSuccess(listener, connection);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean sendTcp(Message message, InetAddress destinationAddress) {
|
||||
return socketTCP.send(message, destinationAddress);
|
||||
SocketTCP tcp = currentTcp();
|
||||
if (tcp == null) {
|
||||
return false;
|
||||
}
|
||||
return tcp.send(message, destinationAddress);
|
||||
}
|
||||
|
||||
public void sendTcpAsync(Message message, String ipText, TcpAttemptListener listener) {
|
||||
tcpConnectPool.execute(() -> {
|
||||
InetAddress address = resolve(ipText);
|
||||
String endpoint = endpointLabel(address, ipText);
|
||||
if (address == null) {
|
||||
deliverFailure(listener, endpoint);
|
||||
return;
|
||||
}
|
||||
SocketTCP tcp = currentTcp();
|
||||
boolean live = tcp != null && tcp.hasOpenPublicConnection(address);
|
||||
if (TcpConnectNotice.shouldShowConnecting(live)) {
|
||||
deliverConnecting(listener, endpoint);
|
||||
}
|
||||
boolean sent = tcp != null && tcp.send(message, address);
|
||||
if (sent) {
|
||||
deliverSuccess(listener, null);
|
||||
return;
|
||||
}
|
||||
deliverFailure(listener, endpoint);
|
||||
});
|
||||
}
|
||||
|
||||
public boolean sendUdpUnicast(Message message, InetAddress destinationAddress) {
|
||||
SocketUDP udp = currentUdp();
|
||||
if (udp == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
socketUDP.sendUnicast(message, destinationAddress);
|
||||
udp.sendUnicast(message, destinationAddress);
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
@@ -132,8 +252,12 @@ public class ConnectionManager {
|
||||
}
|
||||
|
||||
public boolean sendUdpMulticast(Message message) {
|
||||
SocketUDP udp = currentUdp();
|
||||
if (udp == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
socketUDP.sendMulticast(message);
|
||||
udp.sendMulticast(message);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error sending UDP Multicast");
|
||||
@@ -157,9 +281,8 @@ public class ConnectionManager {
|
||||
presenceService.onGoodbye(message);
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port = port;
|
||||
reconnect();
|
||||
public boolean setPort(int port) {
|
||||
return applyNetwork(port, multicastTtl, LocalNetworkInfo.getEgressMode()) == null;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
@@ -170,19 +293,105 @@ public class ConnectionManager {
|
||||
return this.pitiupiIdentifier;
|
||||
}
|
||||
|
||||
private void reconnect() {
|
||||
if (this.socketUDP != null) {
|
||||
this.socketUDP.close();
|
||||
private SocketUDP currentUdp() {
|
||||
synchronized (socketsLock) {
|
||||
return socketUDP;
|
||||
}
|
||||
if (this.socketTCP != null) {
|
||||
this.socketTCP.close();
|
||||
}
|
||||
|
||||
this.socketUDP = new SocketUDP(this, pluginManager);
|
||||
this.socketTCP = new SocketTCP(this, pluginManager);
|
||||
private SocketTCP currentTcp() {
|
||||
synchronized (socketsLock) {
|
||||
return socketTCP;
|
||||
}
|
||||
}
|
||||
|
||||
this.socketUDP.start();
|
||||
this.socketTCP.start();
|
||||
private void closeSockets() {
|
||||
SocketUDP udp;
|
||||
SocketTCP tcp;
|
||||
synchronized (socketsLock) {
|
||||
udp = socketUDP;
|
||||
tcp = socketTCP;
|
||||
socketUDP = null;
|
||||
socketTCP = null;
|
||||
}
|
||||
if (udp != null) {
|
||||
udp.close();
|
||||
joinQuiet(udp);
|
||||
}
|
||||
if (tcp != null) {
|
||||
tcp.close();
|
||||
joinQuiet(tcp);
|
||||
}
|
||||
}
|
||||
|
||||
private String openSockets() {
|
||||
SocketUDP udp = new SocketUDP(this, pluginManager);
|
||||
SocketTCP tcp = new SocketTCP(this, pluginManager);
|
||||
if (!udp.isJoined() || !tcp.isAccepting()) {
|
||||
udp.close();
|
||||
tcp.close();
|
||||
return "Não foi possível abrir a porta " + port + ". Ela pode estar em uso.";
|
||||
}
|
||||
synchronized (socketsLock) {
|
||||
socketUDP = udp;
|
||||
socketTCP = tcp;
|
||||
}
|
||||
udp.start();
|
||||
tcp.start();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void joinQuiet(Thread thread) {
|
||||
try {
|
||||
thread.join(1000);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private static InetAddress resolve(String ipText) {
|
||||
if (ipText == null || ipText.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return InetAddress.getByName(ipText.trim());
|
||||
} catch (UnknownHostException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String endpointLabel(InetAddress address, String ipText) {
|
||||
String host = address != null ? PeerIpv4.fromInetAddress(address) : (ipText == null ? "" : ipText.trim());
|
||||
return host + ":" + getPort();
|
||||
}
|
||||
|
||||
private static void deliverConnecting(TcpAttemptListener listener, String endpoint) {
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
onEdt(() -> listener.onConnecting(endpoint));
|
||||
}
|
||||
|
||||
private static void deliverSuccess(TcpAttemptListener listener, TcpConnection connection) {
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
onEdt(() -> listener.onSuccess(connection));
|
||||
}
|
||||
|
||||
private static void deliverFailure(TcpAttemptListener listener, String endpoint) {
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
onEdt(() -> listener.onFailure(TcpConnectNotice.failureMessage(endpoint)));
|
||||
}
|
||||
|
||||
private static void onEdt(Runnable action) {
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
action.run();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(action);
|
||||
}
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
|
||||
@@ -78,14 +78,32 @@ public class HeartbeatManager {
|
||||
connectionManager.sendUdpMulticast(msg);
|
||||
}
|
||||
|
||||
public void sendGoodbyeAndStop() {
|
||||
if (connectionManager != null) {
|
||||
public void announceNow() {
|
||||
announce();
|
||||
}
|
||||
|
||||
public void sendGoodbye() {
|
||||
if (connectionManager == null) {
|
||||
return;
|
||||
}
|
||||
GoodbyeMessage msg = new GoodbyeMessage(connectionManager.getUserName(), instanceId);
|
||||
connectionManager.sendUdpMulticast(msg);
|
||||
}
|
||||
|
||||
public void sendGoodbyeAndStop() {
|
||||
sendGoodbye();
|
||||
stop();
|
||||
}
|
||||
|
||||
public void clearPeers() {
|
||||
if (peers.isEmpty()) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
peers.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public void receiveGoodbye(GoodbyeMessage msg) {
|
||||
if (msg.getInstanceId().equals(instanceId)) {
|
||||
return;
|
||||
@@ -116,12 +134,14 @@ public class HeartbeatManager {
|
||||
}
|
||||
|
||||
String key = msg.getInstanceId();
|
||||
boolean isNew = !peers.containsKey(key);
|
||||
|
||||
User peer = peers.computeIfAbsent(key, k -> new User(msg.getUserName(), from, msg.getTimestamp(), msg.getInstanceId()));
|
||||
User peer = peers.get(key);
|
||||
if (peer == null) {
|
||||
peers.put(key, new User(msg.getUserName(), from, msg.getTimestamp(), msg.getInstanceId()));
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
peer.touch(System.currentTimeMillis());
|
||||
|
||||
if (isNew) {
|
||||
if (peer.updateIdentity(msg.getUserName(), from)) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ public class User {
|
||||
|
||||
public static final String BROADCAST_INSTANCE_ID = "__broadcast__";
|
||||
|
||||
private final String userName;
|
||||
private final InetAddress address;
|
||||
private String userName;
|
||||
private InetAddress address;
|
||||
private volatile long lastSeen;
|
||||
private final String instanceId;
|
||||
|
||||
@@ -65,6 +65,24 @@ public class User {
|
||||
this.lastSeen = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Atualiza nome e endereço quando um heartbeat mais novo traz identidade diferente.
|
||||
*
|
||||
* @return {@code true} se nome ou endereço mudou.
|
||||
*/
|
||||
public boolean updateIdentity(String newName, InetAddress newAddress) {
|
||||
boolean changed = false;
|
||||
if (newName != null && !newName.equals(userName)) {
|
||||
this.userName = newName;
|
||||
changed = true;
|
||||
}
|
||||
if (newAddress != null && !newAddress.equals(address)) {
|
||||
this.address = newAddress;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (isBroadcastTarget() || address == null) {
|
||||
|
||||
@@ -7,7 +7,10 @@ import pitiupi.models.Messages.InfoType;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import pitiupi.presentation.shell.LocalNetworkInfo;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.logging.Level;
|
||||
@@ -28,6 +31,7 @@ import java.util.logging.Logger;
|
||||
public class ConnectionFactory {
|
||||
private static final int HANDSHAKE_TIMEOUT = 1000;
|
||||
private static final int STANDARD_TIMEOUT = 300_000;
|
||||
public static final int CONNECT_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
* Cria uma conexão TCP de saída com o peer de destino e realiza o handshake
|
||||
@@ -55,7 +59,12 @@ public class ConnectionFactory {
|
||||
try {
|
||||
TcpConnection connection;
|
||||
|
||||
Socket socket = new Socket(address, port);
|
||||
Socket socket = new Socket();
|
||||
InetAddress local = LocalNetworkInfo.bestLocalInet4Address();
|
||||
if (local != null) {
|
||||
socket.bind(new InetSocketAddress(local, 0));
|
||||
}
|
||||
socket.connect(new InetSocketAddress(address, port), CONNECT_TIMEOUT_MS);
|
||||
connection = new TcpConnection(socket);
|
||||
connection.setSender(sender);
|
||||
connection.setReceiver(receiver);
|
||||
|
||||
@@ -64,7 +64,10 @@ public class SocketTCP extends Thread {
|
||||
tcpConnectionManager = new TcpConnectionManager();
|
||||
|
||||
try {
|
||||
serverSocket = new ServerSocket(connectionManager.getPort());
|
||||
ServerSocket listening = new ServerSocket();
|
||||
listening.setReuseAddress(true);
|
||||
listening.bind(new InetSocketAddress(connectionManager.getPort()));
|
||||
serverSocket = listening;
|
||||
address = InetAddress.getByName(InetAddress.getLocalHost().getHostAddress());
|
||||
} catch (IOException ex) {
|
||||
System.out.println("There is no socket connection. Sorry.");
|
||||
@@ -83,6 +86,18 @@ public class SocketTCP extends Thread {
|
||||
* @param msg mensagem que será enviada.
|
||||
* @param destinationAddress endereço IP do peer que receberá a mensagem.
|
||||
*/
|
||||
public boolean isAccepting() {
|
||||
return serverSocket != null && serverSocket.isBound() && !serverSocket.isClosed();
|
||||
}
|
||||
|
||||
public boolean hasOpenPublicConnection(InetAddress destinationAddress) {
|
||||
if (destinationAddress == null) {
|
||||
return false;
|
||||
}
|
||||
TcpConnection connection = tcpConnectionManager.getPublicConnection(destinationAddress);
|
||||
return connection != null && !connection.isClosed();
|
||||
}
|
||||
|
||||
public boolean send(Message msg, InetAddress destinationAddress) {
|
||||
try {
|
||||
TcpConnection connection = getPublicConnection(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package pitiupi.net.tcp;
|
||||
|
||||
/**
|
||||
* Resultado de um connect TCP disparado fora da thread da interface.
|
||||
* Os métodos são chamados na EDT.
|
||||
*/
|
||||
public interface TcpAttemptListener {
|
||||
|
||||
void onConnecting(String endpoint);
|
||||
|
||||
void onSuccess(TcpConnection connection);
|
||||
|
||||
void onFailure(String message);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package pitiupi.net.tcp;
|
||||
|
||||
/**
|
||||
* O aviso de conexão na tela só aparece quando ainda não há socket aberto para o IP.
|
||||
*/
|
||||
public final class TcpConnectNotice {
|
||||
|
||||
private TcpConnectNotice() {
|
||||
}
|
||||
|
||||
public static boolean shouldShowConnecting(boolean liveSocketExists) {
|
||||
return !liveSocketExists;
|
||||
}
|
||||
|
||||
public static String failureMessage(String endpoint) {
|
||||
return "Não foi possível conectar em " + endpoint
|
||||
+ ". Verifique se o outro Pitiupi está aberto nessa porta e se o firewall libera TCP.";
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public class SocketUDP extends Thread {
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
private volatile boolean running = true;
|
||||
private volatile boolean joined;
|
||||
public final static String INET_ADDR = "224.0.0.3";
|
||||
|
||||
/**
|
||||
@@ -65,11 +66,12 @@ public class SocketUDP extends Thread {
|
||||
this.address = InetAddress.getByName(SocketUDP.INET_ADDR);
|
||||
|
||||
int port = connectionManager.getPort();
|
||||
multicastSocket = new MulticastSocket(port);
|
||||
multicastSocket = new MulticastSocket(null);
|
||||
multicastSocket.setReuseAddress(true);
|
||||
multicastSocket.bind(new InetSocketAddress(port));
|
||||
multicastSocket.setSendBufferSize(256000);
|
||||
multicastSocket.setReceiveBufferSize(256000);
|
||||
multicastSocket.setReuseAddress(true);
|
||||
multicastSocket.setTimeToLive(1);
|
||||
multicastSocket.setTimeToLive(connectionManager.getMulticastTtl());
|
||||
multicastInterface = LocalNetworkInfo.preferredNetworkInterface();
|
||||
if (multicastInterface != null) {
|
||||
multicastSocket.setNetworkInterface(multicastInterface);
|
||||
@@ -77,6 +79,7 @@ public class SocketUDP extends Thread {
|
||||
} else {
|
||||
multicastSocket.joinGroup(address);
|
||||
}
|
||||
joined = true;
|
||||
}
|
||||
catch (UnknownHostException ex) {
|
||||
System.getLogger(SocketUDP.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
|
||||
@@ -124,8 +127,13 @@ public class SocketUDP extends Thread {
|
||||
/**
|
||||
* Encerra a comunicação multicast e interrompe a recepção de mensagens.
|
||||
*/
|
||||
public boolean isJoined() {
|
||||
return joined && multicastSocket != null && !multicastSocket.isClosed();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
running = false;
|
||||
joined = false;
|
||||
if (multicastSocket != null) {
|
||||
try {
|
||||
int port = connectionManager.getPort();
|
||||
|
||||
@@ -153,18 +153,44 @@ public final class ClosableTabbedPane {
|
||||
closeTab(key, closeFallbackKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Troca a chave de uma aba existente e recria o × com a chave nova.
|
||||
*
|
||||
* @return {@code false} se a aba antiga não existe ou a nova já está ocupada.
|
||||
*/
|
||||
public boolean replaceKey(String oldKey, String newKey, String title, String tooltip, boolean closable) {
|
||||
if (oldKey == null || newKey == null || oldKey.equals(newKey)) {
|
||||
return false;
|
||||
}
|
||||
reconcileKeyListWithTabPane();
|
||||
int index = indexOfKey(oldKey);
|
||||
if (index < 0 || index >= tabbedPane.getTabCount() || hasTab(newKey)) {
|
||||
return false;
|
||||
}
|
||||
orderedKeys.set(index, newKey);
|
||||
reindex();
|
||||
setTabHeader(index, title, tooltip, closable, newKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void closeTab(String key, String fallbackKey) {
|
||||
reconcileKeyListWithTabPane();
|
||||
int index = indexOfKey(key);
|
||||
if (index < 0 || index >= tabbedPane.getTabCount()) {
|
||||
return;
|
||||
}
|
||||
tabbedPane.removeTabAt(index);
|
||||
boolean prev = suppressSelectionEvents;
|
||||
suppressSelectionEvents = true;
|
||||
try {
|
||||
if (index < orderedKeys.size()) {
|
||||
orderedKeys.remove(index);
|
||||
}
|
||||
keyToIndex.remove(key);
|
||||
tabbedPane.removeTabAt(index);
|
||||
reindex();
|
||||
} finally {
|
||||
suppressSelectionEvents = prev;
|
||||
}
|
||||
onTabClosed.accept(key);
|
||||
String next = null;
|
||||
if (fallbackKey != null && !fallbackKey.isBlank() && hasTab(fallbackKey)) {
|
||||
@@ -251,16 +277,22 @@ public final class ClosableTabbedPane {
|
||||
if (indices.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
boolean prev = suppressSelectionEvents;
|
||||
suppressSelectionEvents = true;
|
||||
try {
|
||||
for (int i = indices.size() - 1; i >= 1; i--) {
|
||||
int idx = indices.get(i);
|
||||
if (idx >= 0 && idx < tabbedPane.getTabCount()) {
|
||||
tabbedPane.removeTabAt(idx);
|
||||
}
|
||||
if (idx < orderedKeys.size()) {
|
||||
orderedKeys.remove(idx);
|
||||
}
|
||||
if (idx >= 0 && idx < tabbedPane.getTabCount()) {
|
||||
tabbedPane.removeTabAt(idx);
|
||||
}
|
||||
}
|
||||
reindex();
|
||||
} finally {
|
||||
suppressSelectionEvents = prev;
|
||||
}
|
||||
}
|
||||
|
||||
private void setTabHeader(int tabIndex, String title, String tooltip, boolean closable, String key) {
|
||||
|
||||
@@ -11,15 +11,45 @@ import java.util.Locale;
|
||||
*/
|
||||
public final class LocalNetworkInfo {
|
||||
|
||||
public enum EgressMode {
|
||||
AUTO,
|
||||
WIRED,
|
||||
WIRELESS
|
||||
}
|
||||
|
||||
private static final String APIPA_PREFIX = "169.254.";
|
||||
private static volatile EgressMode egressMode = EgressMode.AUTO;
|
||||
|
||||
private LocalNetworkInfo() {
|
||||
}
|
||||
|
||||
public static void setEgressMode(EgressMode mode) {
|
||||
egressMode = mode == null ? EgressMode.AUTO : mode;
|
||||
}
|
||||
|
||||
public static EgressMode getEgressMode() {
|
||||
return egressMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface preferida para LAN: cabo antes de Wi‑Fi; APIPA (169.254.x) antes de outro IPv4 no cabo.
|
||||
* Modo automático sempre pode ser aplicado. Cabo e Wi-Fi exigem uma interface dessa família.
|
||||
*/
|
||||
public static boolean canUse(EgressMode mode) {
|
||||
if (mode == null || mode == EgressMode.AUTO) {
|
||||
return true;
|
||||
}
|
||||
return selectInterface(mode) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface preferida para LAN, respeitando o modo de saída configurado.
|
||||
*/
|
||||
public static NetworkInterface preferredNetworkInterface() {
|
||||
return selectInterface(egressMode);
|
||||
}
|
||||
|
||||
public static NetworkInterface selectInterface(EgressMode mode) {
|
||||
EgressMode effective = mode == null ? EgressMode.AUTO : mode;
|
||||
try {
|
||||
NetworkInterface wiredApipa = null;
|
||||
NetworkInterface wiredOther = null;
|
||||
@@ -34,8 +64,8 @@ public final class LocalNetworkInfo {
|
||||
if (fallback == null) {
|
||||
fallback = ni;
|
||||
}
|
||||
String name = ni.getName();
|
||||
if (isWiredLanInterfaceName(name)) {
|
||||
Kind kind = classify(ni);
|
||||
if (kind == Kind.WIRED) {
|
||||
if (hasApipaIpv4(ni)) {
|
||||
if (wiredApipa == null) {
|
||||
wiredApipa = ni;
|
||||
@@ -43,10 +73,19 @@ public final class LocalNetworkInfo {
|
||||
} else if (wiredOther == null) {
|
||||
wiredOther = ni;
|
||||
}
|
||||
} else if (isWirelessLanInterfaceName(name) && wireless == null) {
|
||||
} else if (kind == Kind.WIRELESS && wireless == null) {
|
||||
wireless = ni;
|
||||
}
|
||||
}
|
||||
if (effective == EgressMode.WIRED) {
|
||||
if (wiredApipa != null) {
|
||||
return wiredApipa;
|
||||
}
|
||||
return wiredOther;
|
||||
}
|
||||
if (effective == EgressMode.WIRELESS) {
|
||||
return wireless;
|
||||
}
|
||||
if (wiredApipa != null) {
|
||||
return wiredApipa;
|
||||
}
|
||||
@@ -62,6 +101,39 @@ public final class LocalNetworkInfo {
|
||||
}
|
||||
}
|
||||
|
||||
public static String describeActiveInterface() {
|
||||
NetworkInterface ni = preferredNetworkInterface();
|
||||
if (ni == null) {
|
||||
return "nenhuma interface";
|
||||
}
|
||||
String ip = bestIpv4OnInterface(ni);
|
||||
return ni.getName() + " · " + (ip == null ? "—" : ip);
|
||||
}
|
||||
|
||||
public static InetAddress bestLocalInet4Address() {
|
||||
try {
|
||||
NetworkInterface preferred = preferredNetworkInterface();
|
||||
if (preferred == null) {
|
||||
return null;
|
||||
}
|
||||
String ip = bestIpv4OnInterface(preferred);
|
||||
if (ip == null) {
|
||||
return null;
|
||||
}
|
||||
return InetAddress.getByName(ip);
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWiredInterface(String name, String displayName) {
|
||||
return matchesWiredToken(name) || matchesWiredToken(displayName);
|
||||
}
|
||||
|
||||
public static boolean isWirelessInterface(String name, String displayName) {
|
||||
return matchesWirelessToken(name) || matchesWirelessToken(displayName);
|
||||
}
|
||||
|
||||
public static String bestLocalIpv4() {
|
||||
try {
|
||||
NetworkInterface preferred = preferredNetworkInterface();
|
||||
@@ -120,25 +192,52 @@ public final class LocalNetworkInfo {
|
||||
|| n.startsWith("tailscale");
|
||||
}
|
||||
|
||||
private static boolean isWiredLanInterfaceName(String name) {
|
||||
if (name == null) {
|
||||
private enum Kind {
|
||||
WIRED,
|
||||
WIRELESS,
|
||||
OTHER
|
||||
}
|
||||
|
||||
private static Kind classify(NetworkInterface ni) {
|
||||
String name = ni.getName();
|
||||
String display = ni.getDisplayName();
|
||||
boolean wireless = isWirelessInterface(name, display);
|
||||
boolean wired = isWiredInterface(name, display);
|
||||
if (wireless && !wired) {
|
||||
return Kind.WIRELESS;
|
||||
}
|
||||
if (wired && !wireless) {
|
||||
return Kind.WIRED;
|
||||
}
|
||||
if (wireless) {
|
||||
return Kind.WIRELESS;
|
||||
}
|
||||
return Kind.OTHER;
|
||||
}
|
||||
|
||||
private static boolean matchesWiredToken(String value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
String n = name.toLowerCase(Locale.ROOT);
|
||||
String n = value.toLowerCase(Locale.ROOT);
|
||||
return n.startsWith("eth")
|
||||
|| n.startsWith("eno")
|
||||
|| n.startsWith("enp")
|
||||
|| n.startsWith("ens");
|
||||
|| n.startsWith("ens")
|
||||
|| n.contains("ethernet");
|
||||
}
|
||||
|
||||
private static boolean isWirelessLanInterfaceName(String name) {
|
||||
if (name == null) {
|
||||
private static boolean matchesWirelessToken(String value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
String n = name.toLowerCase(Locale.ROOT);
|
||||
String n = value.toLowerCase(Locale.ROOT);
|
||||
return n.startsWith("wlan")
|
||||
|| n.startsWith("wlp")
|
||||
|| n.startsWith("wl");
|
||||
|| n.startsWith("wl")
|
||||
|| n.contains("wi-fi")
|
||||
|| n.contains("wifi")
|
||||
|| n.contains("wireless");
|
||||
}
|
||||
|
||||
private static boolean hasApipaIpv4(NetworkInterface ni) {
|
||||
|
||||
@@ -13,17 +13,11 @@ import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Login modal centralizado (~30% da área útil do monitor).
|
||||
*/
|
||||
public final class LoginDialog extends JDialog {
|
||||
|
||||
private static final Preferences prefs = Preferences.userNodeForPackage(LoginDialog.class);
|
||||
private static final String PREF_NOME = "lastUserName";
|
||||
private static final String PREF_PORTA = "lastPort";
|
||||
|
||||
private final JTextField nameField = new JTextField(24);
|
||||
private final JSpinner portField = new JSpinner();
|
||||
private boolean accepted;
|
||||
@@ -37,8 +31,8 @@ public final class LoginDialog extends JDialog {
|
||||
}
|
||||
|
||||
private void buildUi() {
|
||||
String nomeSalvo = prefs.get(PREF_NOME, System.getProperty("user.name"));
|
||||
int portaSalva = prefs.getInt(PREF_PORTA, 5000);
|
||||
String nomeSalvo = NetworkPreferences.loadName();
|
||||
int portaSalva = NetworkPreferences.loadPort();
|
||||
portField.setModel(new javax.swing.SpinnerNumberModel(portaSalva, 1, 65535, 1));
|
||||
nameField.setText(nomeSalvo);
|
||||
|
||||
@@ -104,8 +98,8 @@ public final class LoginDialog extends JDialog {
|
||||
if (port <= 0) {
|
||||
return;
|
||||
}
|
||||
prefs.put(PREF_NOME, name);
|
||||
prefs.putInt(PREF_PORTA, port);
|
||||
NetworkPreferences.saveName(name);
|
||||
NetworkPreferences.savePort(port);
|
||||
accepted = true;
|
||||
setVisible(false);
|
||||
dispose();
|
||||
|
||||
@@ -38,6 +38,7 @@ public final class MainShellFrame extends JFrame implements ShellHost {
|
||||
|
||||
connectionManager = new ConnectionManager(userName, pluginManager, port);
|
||||
settingsPanel = new SettingsPanel(connectionManager);
|
||||
settingsPanel.setSettingsListener(this::onIdentityApplied);
|
||||
PluginGalleryPanel gallery = new PluginGalleryPanel((pluginId, title) -> { });
|
||||
tabWorkspace = new TabWorkspacePanel(gallery, settingsPanel);
|
||||
galleryPanel = gallery;
|
||||
@@ -131,6 +132,11 @@ public final class MainShellFrame extends JFrame implements ShellHost {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private void onIdentityApplied(String userName, int port) {
|
||||
setUserName(userName);
|
||||
updateStatus(userName, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserName(String userName) {
|
||||
connectionManager.setUserName(userName);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package pitiupi.presentation.shell;
|
||||
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
/**
|
||||
* Nome, porta, TTL e saída de rede no mesmo nó de preferências do login.
|
||||
*/
|
||||
public final class NetworkPreferences {
|
||||
|
||||
public static final String PREF_NOME = "lastUserName";
|
||||
public static final String PREF_PORTA = "lastPort";
|
||||
public static final String PREF_TTL = "lastTtl";
|
||||
public static final String PREF_EGRESS = "lastEgress";
|
||||
|
||||
public static final int DEFAULT_PORT = 5000;
|
||||
public static final int DEFAULT_TTL = 1;
|
||||
public static final int MIN_TTL = 1;
|
||||
public static final int MAX_TTL = 32;
|
||||
|
||||
private static final Preferences PREFS = Preferences.userNodeForPackage(LoginDialog.class);
|
||||
|
||||
private NetworkPreferences() {
|
||||
}
|
||||
|
||||
public static String loadName() {
|
||||
String fallback = System.getProperty("user.name");
|
||||
return PREFS.get(PREF_NOME, fallback == null ? "" : fallback);
|
||||
}
|
||||
|
||||
public static int loadPort() {
|
||||
return PREFS.getInt(PREF_PORTA, DEFAULT_PORT);
|
||||
}
|
||||
|
||||
public static int loadTtl() {
|
||||
return clampTtl(PREFS.getInt(PREF_TTL, DEFAULT_TTL));
|
||||
}
|
||||
|
||||
public static int clampTtl(int ttl) {
|
||||
if (ttl < MIN_TTL) {
|
||||
return MIN_TTL;
|
||||
}
|
||||
if (ttl > MAX_TTL) {
|
||||
return MAX_TTL;
|
||||
}
|
||||
return ttl;
|
||||
}
|
||||
|
||||
public static LocalNetworkInfo.EgressMode loadEgress() {
|
||||
return fromCode(PREFS.get(PREF_EGRESS, "auto"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aplica a saída gravada antes de os sockets abrirem.
|
||||
*/
|
||||
public static void applyEgressBeforeSockets() {
|
||||
LocalNetworkInfo.setEgressMode(loadEgress());
|
||||
}
|
||||
|
||||
public static void saveName(String name) {
|
||||
PREFS.put(PREF_NOME, name);
|
||||
}
|
||||
|
||||
public static void savePort(int port) {
|
||||
PREFS.putInt(PREF_PORTA, port);
|
||||
}
|
||||
|
||||
public static void saveTtl(int ttl) {
|
||||
PREFS.putInt(PREF_TTL, clampTtl(ttl));
|
||||
}
|
||||
|
||||
public static void saveEgress(LocalNetworkInfo.EgressMode mode) {
|
||||
PREFS.put(PREF_EGRESS, toCode(mode));
|
||||
}
|
||||
|
||||
public static String toCode(LocalNetworkInfo.EgressMode mode) {
|
||||
if (mode == LocalNetworkInfo.EgressMode.WIRED) {
|
||||
return "wired";
|
||||
}
|
||||
if (mode == LocalNetworkInfo.EgressMode.WIRELESS) {
|
||||
return "wireless";
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
public static LocalNetworkInfo.EgressMode fromCode(String code) {
|
||||
if ("wired".equals(code)) {
|
||||
return LocalNetworkInfo.EgressMode.WIRED;
|
||||
}
|
||||
if ("wireless".equals(code)) {
|
||||
return LocalNetworkInfo.EgressMode.WIRELESS;
|
||||
}
|
||||
return LocalNetworkInfo.EgressMode.AUTO;
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,10 @@ public final class SessionTabBar {
|
||||
tabs.setTabTitle(key, title, tooltip);
|
||||
}
|
||||
|
||||
public boolean replaceKey(String oldKey, String newKey, String title, String tooltip, boolean closable) {
|
||||
return tabs.replaceKey(oldKey, newKey, title, tooltip, closable);
|
||||
}
|
||||
|
||||
public String selectedKey() {
|
||||
return tabs.selectedKey();
|
||||
}
|
||||
|
||||
@@ -8,23 +8,28 @@ import pitiupi.plugins.store.PluginStore;
|
||||
import pitiupi.presentation.shell.ui.ShellUi;
|
||||
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
import javax.swing.table.DefaultTableModel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Insets;
|
||||
import java.awt.KeyboardFocusManager;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
@@ -36,17 +41,28 @@ import java.util.Map;
|
||||
*/
|
||||
public final class SettingsPanel extends JPanel {
|
||||
|
||||
public interface NetworkSettingsListener {
|
||||
void onIdentityApplied(String userName, int port);
|
||||
}
|
||||
|
||||
private final PluginStore store;
|
||||
private final ConnectionManager connectionManager;
|
||||
private final DefaultTableModel tableModel =
|
||||
new DefaultTableModel(new String[]{"Nome", "ID", "Arquivo", "Versão"}, 0);
|
||||
private final JTable table = new JTable(tableModel);
|
||||
private final JTextField nameField = new JTextField();
|
||||
private final JTextField portField = new JTextField();
|
||||
private final JSpinner portSpinner = new JSpinner(new javax.swing.SpinnerNumberModel(5000, 1, 65535, 1));
|
||||
private final JComboBox<String> egressCombo = new JComboBox<>(new String[]{"Automático", "Cabo", "Wi-Fi"});
|
||||
private final JSpinner ttlSpinner = new JSpinner(
|
||||
new javax.swing.SpinnerNumberModel(NetworkPreferences.DEFAULT_TTL, NetworkPreferences.MIN_TTL, NetworkPreferences.MAX_TTL, 1));
|
||||
private final JLabel interfaceLabel = new JLabel(" ");
|
||||
private final JTextField ipField = new JTextField();
|
||||
private final JTextField instanceField = new JTextField();
|
||||
private final JLabel peersLabel = new JLabel();
|
||||
private final Timer refreshTimer;
|
||||
private boolean applying;
|
||||
private NetworkSettingsListener settingsListener = (name, port) -> {
|
||||
};
|
||||
private Map<String, Plugin> loadedByIdentifier = Map.of();
|
||||
|
||||
public SettingsPanel(ConnectionManager connectionManager) {
|
||||
@@ -55,7 +71,7 @@ public final class SettingsPanel extends JPanel {
|
||||
setLayout(new BorderLayout(10, 10));
|
||||
setBorder(new EmptyBorder(8, 8, 8, 8));
|
||||
|
||||
for (JTextField field : List.of(nameField, portField, ipField, instanceField)) {
|
||||
for (JTextField field : List.of(ipField, instanceField)) {
|
||||
field.setEditable(false);
|
||||
}
|
||||
|
||||
@@ -94,6 +110,11 @@ public final class SettingsPanel extends JPanel {
|
||||
refreshTable();
|
||||
}
|
||||
|
||||
public void setSettingsListener(NetworkSettingsListener listener) {
|
||||
this.settingsListener = listener != null ? listener : (name, port) -> {
|
||||
};
|
||||
}
|
||||
|
||||
public void onTabSelected() {
|
||||
refreshNetworkInfo();
|
||||
}
|
||||
@@ -107,27 +128,47 @@ public final class SettingsPanel extends JPanel {
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
addRow(panel, gbc, 0, "Nome na rede", nameField);
|
||||
addRow(panel, gbc, 1, "Porta", portField);
|
||||
addRow(panel, gbc, 2, "IP local", ipField);
|
||||
addRow(panel, gbc, 3, "ID desta instância", instanceField);
|
||||
addRow(panel, gbc, 1, "Porta", portSpinner);
|
||||
addRow(panel, gbc, 2, "Saída", egressCombo);
|
||||
addRow(panel, gbc, 3, "Interface", interfaceLabel);
|
||||
addRow(panel, gbc, 4, "TTL", ttlSpinner);
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 5;
|
||||
panel.add(ShellUi.mutedLabel("1 alcança só a rede local."), gbc);
|
||||
addRow(panel, gbc, 6, "IP local", ipField);
|
||||
addRow(panel, gbc, 7, "ID desta instância", instanceField);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 4;
|
||||
gbc.gridy = 8;
|
||||
gbc.gridwidth = 1;
|
||||
gbc.weightx = 0;
|
||||
panel.add(new JLabel("Peers visíveis:"), gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1;
|
||||
panel.add(peersLabel, gbc);
|
||||
|
||||
JButton apply = new JButton("Aplicar");
|
||||
apply.setFocusPainted(false);
|
||||
apply.addActionListener(e -> applyNetworkSettings());
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 9;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
panel.add(apply, gbc);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void addRow(JPanel panel, GridBagConstraints gbc, int row, String label, JTextField field) {
|
||||
private void addRow(JPanel panel, GridBagConstraints gbc, int row, String label, Component field) {
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
gbc.gridwidth = 1;
|
||||
gbc.weightx = 0;
|
||||
gbc.fill = GridBagConstraints.NONE;
|
||||
panel.add(new JLabel(label + ":"), gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
panel.add(field, gbc);
|
||||
}
|
||||
|
||||
@@ -158,15 +199,123 @@ public final class SettingsPanel extends JPanel {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private void applyNetworkSettings() {
|
||||
applying = true;
|
||||
try {
|
||||
commitSpinner(portSpinner);
|
||||
commitSpinner(ttlSpinner);
|
||||
String name = nameField.getText().trim();
|
||||
if (name.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "Informe um nome na rede.");
|
||||
return;
|
||||
}
|
||||
int port = (Integer) portSpinner.getValue();
|
||||
int ttl = NetworkPreferences.clampTtl((Integer) ttlSpinner.getValue());
|
||||
LocalNetworkInfo.EgressMode mode = egressFromCombo();
|
||||
boolean nameChanged = !name.equals(connectionManager.getUserName());
|
||||
boolean portChanged = port != connectionManager.getPort();
|
||||
boolean egressChanged = mode != LocalNetworkInfo.getEgressMode();
|
||||
boolean ttlChanged = ttl != connectionManager.getMulticastTtl();
|
||||
if (!nameChanged && !portChanged && !egressChanged && !ttlChanged) {
|
||||
return;
|
||||
}
|
||||
if (portChanged || egressChanged || ttlChanged) {
|
||||
String error = connectionManager.applyNetwork(port, ttl, mode);
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, error, "Configuração de rede", JOptionPane.WARNING_MESSAGE);
|
||||
refreshNetworkInfo();
|
||||
return;
|
||||
}
|
||||
}
|
||||
NetworkPreferences.saveName(name);
|
||||
NetworkPreferences.savePort(connectionManager.getPort());
|
||||
NetworkPreferences.saveTtl(connectionManager.getMulticastTtl());
|
||||
NetworkPreferences.saveEgress(LocalNetworkInfo.getEgressMode());
|
||||
settingsListener.onIdentityApplied(name, connectionManager.getPort());
|
||||
JOptionPane.showMessageDialog(
|
||||
this,
|
||||
confirmation(nameChanged, portChanged, connectionManager.getPort(), egressChanged || ttlChanged)
|
||||
);
|
||||
} finally {
|
||||
applying = false;
|
||||
refreshNetworkInfo();
|
||||
}
|
||||
}
|
||||
|
||||
private static void commitSpinner(JSpinner spinner) {
|
||||
try {
|
||||
spinner.commitEdit();
|
||||
} catch (java.text.ParseException ignored) {
|
||||
// mantém o último valor válido do modelo
|
||||
}
|
||||
}
|
||||
|
||||
private LocalNetworkInfo.EgressMode egressFromCombo() {
|
||||
Object selected = egressCombo.getSelectedItem();
|
||||
if ("Cabo".equals(selected)) {
|
||||
return LocalNetworkInfo.EgressMode.WIRED;
|
||||
}
|
||||
if ("Wi-Fi".equals(selected)) {
|
||||
return LocalNetworkInfo.EgressMode.WIRELESS;
|
||||
}
|
||||
return LocalNetworkInfo.EgressMode.AUTO;
|
||||
}
|
||||
|
||||
private static String confirmation(boolean nameChanged, boolean portChanged, int port, boolean egressOrTtlChanged) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
if (portChanged) {
|
||||
text.append("Porta alterada para ")
|
||||
.append(port)
|
||||
.append(". Você saiu da sala anterior e passou a procurar colegas na porta ")
|
||||
.append(port)
|
||||
.append('.');
|
||||
} else if (egressOrTtlChanged) {
|
||||
text.append("Saída de rede atualizada. A descoberta usa esta interface.");
|
||||
}
|
||||
if (nameChanged) {
|
||||
if (text.length() > 0) {
|
||||
text.append(' ');
|
||||
}
|
||||
text.append("Nome atualizado. Os colegas veem o novo nome no próximo anúncio (até 5 s).");
|
||||
}
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
private void refreshNetworkInfo() {
|
||||
if (!applying && !isEditingNetworkFields()) {
|
||||
nameField.setText(connectionManager.getUserName());
|
||||
portField.setText(String.valueOf(connectionManager.getPort()));
|
||||
portSpinner.setValue(connectionManager.getPort());
|
||||
ttlSpinner.setValue(connectionManager.getMulticastTtl());
|
||||
egressCombo.setSelectedItem(comboLabel(LocalNetworkInfo.getEgressMode()));
|
||||
}
|
||||
interfaceLabel.setText(LocalNetworkInfo.describeActiveInterface());
|
||||
ipField.setText(LocalNetworkInfo.bestLocalIpv4());
|
||||
instanceField.setText(connectionManager.getHeartbeatManager().getInstanceId());
|
||||
int peers = Math.max(0, connectionManager.getPresenceService().listPeerViews().size() - 1);
|
||||
peersLabel.setText(peers + " (além de você)");
|
||||
}
|
||||
|
||||
private static String comboLabel(LocalNetworkInfo.EgressMode mode) {
|
||||
if (mode == LocalNetworkInfo.EgressMode.WIRED) {
|
||||
return "Cabo";
|
||||
}
|
||||
if (mode == LocalNetworkInfo.EgressMode.WIRELESS) {
|
||||
return "Wi-Fi";
|
||||
}
|
||||
return "Automático";
|
||||
}
|
||||
|
||||
private boolean isEditingNetworkFields() {
|
||||
Component focus = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
|
||||
if (focus == null) {
|
||||
return false;
|
||||
}
|
||||
return SwingUtilities.isDescendingFrom(focus, nameField)
|
||||
|| SwingUtilities.isDescendingFrom(focus, portSpinner)
|
||||
|| SwingUtilities.isDescendingFrom(focus, ttlSpinner)
|
||||
|| SwingUtilities.isDescendingFrom(focus, egressCombo);
|
||||
}
|
||||
|
||||
private void refreshTable() {
|
||||
tableModel.setRowCount(0);
|
||||
try {
|
||||
|
||||
@@ -22,4 +22,16 @@ class HeartbeatManagerGoodbyeTest {
|
||||
manager.receiveGoodbye(new GoodbyeMessage("alice", "peer-1"));
|
||||
assertTrue(manager.getPeers().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void receiveHeartbeatUpdatesName() throws Exception {
|
||||
HeartbeatManager manager = HeartbeatManager.createForTesting();
|
||||
InetAddress addr = InetAddress.getByName("127.0.0.1");
|
||||
|
||||
manager.receiveHeartbeat(new HeartbeatMessage("alice", "peer-1"), addr);
|
||||
manager.receiveHeartbeat(new HeartbeatMessage("alice-novo", "peer-1"), addr);
|
||||
|
||||
assertEquals(1, manager.getPeers().size());
|
||||
assertEquals("alice-novo", manager.getPeers().get(0).getUserName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package pitiupi.net.tcp;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TcpConnectNoticeTest {
|
||||
|
||||
@Test
|
||||
void existingSocketDoesNotAnnounceConnecting() {
|
||||
assertFalse(TcpConnectNotice.shouldShowConnecting(true));
|
||||
assertTrue(TcpConnectNotice.shouldShowConnecting(false));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,18 @@ class LocalNetworkInfoTest {
|
||||
assertFalse(ip.isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void classifiesWiredAndWirelessNames() {
|
||||
assertTrue(LocalNetworkInfo.isWiredInterface("eth0", null));
|
||||
assertTrue(LocalNetworkInfo.isWiredInterface("enp3s0", null));
|
||||
assertTrue(LocalNetworkInfo.isWiredInterface("net0", "Ethernet"));
|
||||
assertTrue(LocalNetworkInfo.isWirelessInterface("wlan0", null));
|
||||
assertTrue(LocalNetworkInfo.isWirelessInterface("wlp2s0", null));
|
||||
assertTrue(LocalNetworkInfo.isWirelessInterface("wlan2", "Wi-Fi"));
|
||||
assertFalse(LocalNetworkInfo.isWiredInterface("wlan0", "Wi-Fi"));
|
||||
assertFalse(LocalNetworkInfo.isWirelessInterface("eth0", "Ethernet"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isLocalIpv4DetectsLoopback() {
|
||||
assertTrue(LocalNetworkInfo.isLocalIpv4("127.0.0.1"));
|
||||
|
||||
@@ -35,4 +35,47 @@ class SessionTabBarStripTest {
|
||||
assertEquals(PluginSessionKeys.ROOM, bar.selectedKey());
|
||||
assertEquals(2, bar.getTabCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void closingMiddleTabKeepsTheTabOnItsRightClosable() {
|
||||
SessionTabBar bar = new SessionTabBar();
|
||||
bar.addOrSelectTab(PluginSessionKeys.ROOM, "Sala geral", new JPanel(), false);
|
||||
String firstPeer = PluginSessionKeys.peer("169.254.165.248");
|
||||
String secondPeer = PluginSessionKeys.peer("192.168.3.131");
|
||||
bar.addOrSelectTab(firstPeer, "Afrodite", "169.254.165.248", new JPanel(), true);
|
||||
bar.addOrSelectTab(secondPeer, "Afrodite", "192.168.3.131", new JPanel(), true);
|
||||
bar.selectTab(PluginSessionKeys.ROOM);
|
||||
|
||||
bar.closeTab(firstPeer);
|
||||
|
||||
assertEquals(2, bar.getTabCount());
|
||||
assertEquals(false, bar.hasTab(firstPeer));
|
||||
assertEquals(true, bar.hasTab(secondPeer));
|
||||
assertEquals(PluginSessionKeys.ROOM, bar.selectedKey());
|
||||
|
||||
bar.closeTab(secondPeer);
|
||||
|
||||
assertEquals(1, bar.getTabCount());
|
||||
assertEquals(false, bar.hasTab(secondPeer));
|
||||
assertEquals(PluginSessionKeys.ROOM, bar.selectedKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceKeyKeepsTheSameTabClosableUnderTheNewAddress() {
|
||||
SessionTabBar bar = new SessionTabBar();
|
||||
bar.addOrSelectTab(PluginSessionKeys.ROOM, "Sala geral", new JPanel(), false);
|
||||
String cable = PluginSessionKeys.peer("169.254.165.248");
|
||||
String wifi = PluginSessionKeys.peer("192.168.3.131");
|
||||
bar.addOrSelectTab(cable, "Afrodite", "169.254.165.248", new JPanel(), true);
|
||||
|
||||
assertEquals(true, bar.replaceKey(cable, wifi, "Afrodite", "192.168.3.131", true));
|
||||
assertEquals(false, bar.hasTab(cable));
|
||||
assertEquals(true, bar.hasTab(wifi));
|
||||
assertEquals(2, bar.getTabCount());
|
||||
|
||||
bar.closeTab(wifi);
|
||||
|
||||
assertEquals(1, bar.getTabCount());
|
||||
assertEquals(false, bar.hasTab(wifi));
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import pitiupi.chat.control.PrivateChatUiSink;
|
||||
import pitiupi.chat.net.ChatMessage;
|
||||
import pitiupi.chat.net.LegacyMpChatMessage;
|
||||
import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.net.tcp.TcpAttemptListener;
|
||||
import pitiupi.models.User;
|
||||
import pitiupi.presentation.shell.LocalNetworkInfo;
|
||||
import pitiupi.presentation.shell.PeerIpv4;
|
||||
@@ -25,6 +26,7 @@ import pitiupi.presentation.shell.ui.ShellUi;
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JEditorPane;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSplitPane;
|
||||
@@ -43,8 +45,10 @@ import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.net.InetAddress;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
@@ -55,6 +59,7 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
|
||||
private final JEditorPane transcript = new JEditorPane();
|
||||
private final JTextArea input = new JTextArea();
|
||||
private final JLabel tcpAttemptLabel = ShellUi.mutedLabel(" ");
|
||||
private final ConnectionManager connectionManager;
|
||||
private final PluginSessionWorkspace workspace;
|
||||
private final PluginRoutingPanel routingPanel;
|
||||
@@ -87,6 +92,8 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
buildUi();
|
||||
initSessionTabs();
|
||||
routingPanel.addRoutingListener(s -> applyConversationKeyFromRouting());
|
||||
connectionManager.addPeerListener(peers ->
|
||||
SwingUtilities.invokeLater(() -> adoptCurrentPeerAddresses(peers)));
|
||||
routingPanel.setPrivateTcpListener(selected -> {
|
||||
if (selected) {
|
||||
User user = resolveUserForSingle();
|
||||
@@ -119,7 +126,91 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
draftPanel.clearIpField();
|
||||
}
|
||||
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
|
||||
return;
|
||||
}
|
||||
if (key != null && key.startsWith("peer:")) {
|
||||
peerDisplayNames.remove(canonicalPeerIp(key.substring("peer:".length())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uma pessoa que troca de cabo para Wi-Fi muda de IP e manteria duas abas com o mesmo nome.
|
||||
* A aba aberta no endereço antigo passa a ser a do endereço atual.
|
||||
*/
|
||||
private void adoptCurrentPeerAddresses(List<User> peers) {
|
||||
if (peers == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, User> byIp = new HashMap<>();
|
||||
for (User peer : peers) {
|
||||
if (peer == null || peer.isBroadcastTarget() || peer.getAddress() == null) {
|
||||
continue;
|
||||
}
|
||||
byIp.put(canonicalPeerIp(PeerIpv4.fromInetAddress(peer.getAddress())), peer);
|
||||
}
|
||||
List<String> openIps = new ArrayList<>(peerDisplayNames.keySet());
|
||||
for (User peer : byIp.values()) {
|
||||
String newIp = canonicalPeerIp(PeerIpv4.fromInetAddress(peer.getAddress()));
|
||||
long sameName = byIp.values().stream()
|
||||
.filter(other -> peer.getUserName().equals(other.getUserName()))
|
||||
.count();
|
||||
if (sameName != 1) {
|
||||
continue;
|
||||
}
|
||||
for (String oldIp : openIps) {
|
||||
if (oldIp.equals(newIp) || byIp.containsKey(oldIp)) {
|
||||
continue;
|
||||
}
|
||||
String title = peerDisplayNames.get(oldIp);
|
||||
if (title != null && title.equals(peer.getUserName())
|
||||
&& sessionTabs.hasTab(PluginSessionKeys.peer(oldIp))) {
|
||||
retargetPeerSession(oldIp, newIp, peer.getUserName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void retargetPeerSession(String oldIp, String newIp, String name) {
|
||||
String oldTab = PluginSessionKeys.peer(oldIp);
|
||||
String newTab = PluginSessionKeys.peer(newIp);
|
||||
mergeTranscript(peerKey(oldIp), peerKey(newIp));
|
||||
mergeTranscript(privateKey(oldIp), privateKey(newIp));
|
||||
String selectedBefore = sessionTabs.selectedKey();
|
||||
boolean wasSelected = oldTab.equals(selectedBefore);
|
||||
if (sessionTabs.hasTab(newTab)) {
|
||||
sessionTabs.setTabTitle(newTab, name, newIp);
|
||||
sessionTabs.closeTab(oldTab);
|
||||
String restore = wasSelected ? newTab : selectedBefore;
|
||||
if (restore != null && sessionTabs.hasTab(restore)) {
|
||||
sessionTabs.selectTab(restore);
|
||||
}
|
||||
} else {
|
||||
sessionTabs.replaceKey(oldTab, newTab, name, newIp, true);
|
||||
}
|
||||
peerDisplayNames.remove(oldIp);
|
||||
peerDisplayNames.put(newIp, name);
|
||||
if (oldTab.equals(lastSessionKey)) {
|
||||
lastSessionKey = newTab;
|
||||
}
|
||||
if (peerKey(oldIp).equals(activeConversationKey)) {
|
||||
activeConversationKey = peerKey(newIp);
|
||||
flushTranscriptToPane();
|
||||
} else if (privateKey(oldIp).equals(activeConversationKey)) {
|
||||
activeConversationKey = privateKey(newIp);
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
if (wasSelected) {
|
||||
routingPanel.bindPeerSession(newIp, name);
|
||||
}
|
||||
}
|
||||
|
||||
private void mergeTranscript(String oldBucket, String newBucket) {
|
||||
StringBuilder oldBuf = transcripts.remove(oldBucket);
|
||||
if (oldBuf == null || oldBuf.length() == 0) {
|
||||
return;
|
||||
}
|
||||
StringBuilder current = transcripts.computeIfAbsent(newBucket, key -> new StringBuilder());
|
||||
current.append(oldBuf);
|
||||
}
|
||||
|
||||
private void applyConversationKeyFromRouting() {
|
||||
@@ -364,8 +455,10 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
inputScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
ShellUi.borderlessScroll(inputScroll);
|
||||
|
||||
tcpAttemptLabel.setVisible(false);
|
||||
JPanel compose = new JPanel(new BorderLayout(0, 4));
|
||||
compose.setPreferredSize(new Dimension(0, 72));
|
||||
compose.setPreferredSize(new Dimension(0, 88));
|
||||
compose.add(tcpAttemptLabel, BorderLayout.NORTH);
|
||||
compose.add(inputScroll, BorderLayout.CENTER);
|
||||
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT, 4, 0));
|
||||
actions.add(send);
|
||||
@@ -420,19 +513,26 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
appendSystemLine(activeConversationKey, "Informe o IP do peer para o canal TCP privado.");
|
||||
return;
|
||||
}
|
||||
if (privateChatController != null && privateChatController.sendText(user, text)) {
|
||||
if (privateChatController != null && privateChatController.hasOpenChannel(user)) {
|
||||
if (privateChatController.sendText(user, text)) {
|
||||
input.setText("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (privateChatController != null) {
|
||||
privateChatController.openChat(user);
|
||||
if (privateChatController.sendText(user, text)) {
|
||||
input.setText("");
|
||||
}
|
||||
}
|
||||
appendSystemLine(peerKey(canonicalPeerIp(PeerIpv4.fromInetAddress(user.getAddress()))),
|
||||
"Canal TCP privado conectando…");
|
||||
return;
|
||||
}
|
||||
|
||||
TransportMode mode = routing.transport();
|
||||
if (mode == TransportMode.TCP) {
|
||||
sendTcpOffEdt(text, routing);
|
||||
return;
|
||||
}
|
||||
SendTarget target = resolveSendTarget(routing);
|
||||
if (target == null || mode == null) {
|
||||
appendSystemLine(activeConversationKey, "Informe um IP válido para enviar a um peer.");
|
||||
@@ -469,6 +569,125 @@ public class ChatPanel extends JPanel implements PrivateChatUiSink {
|
||||
}
|
||||
}
|
||||
|
||||
public void showTcpAttempt(String text) {
|
||||
tcpAttemptLabel.setText(text);
|
||||
tcpAttemptLabel.setVisible(true);
|
||||
}
|
||||
|
||||
public void clearTcpAttempt() {
|
||||
tcpAttemptLabel.setText(" ");
|
||||
tcpAttemptLabel.setVisible(false);
|
||||
}
|
||||
|
||||
public void failTcpAttempt(String message) {
|
||||
clearTcpAttempt();
|
||||
appendSystemLine(activeConversationKey, message);
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
|
||||
private void sendTcpOffEdt(String text, PluginRoutingPanel.RoutingSnapshot routing) {
|
||||
String localIp = PeerIpv4.normalize(LocalNetworkInfo.bestLocalIpv4());
|
||||
ChatMessage message = new ChatMessage(
|
||||
connectionManager.getUserName(),
|
||||
text,
|
||||
TransportMode.TCP.getLabel(),
|
||||
localIp
|
||||
);
|
||||
if (routing.everyoneInRoom()) {
|
||||
sendTcpToRoom(message);
|
||||
return;
|
||||
}
|
||||
String peerIp = canonicalPeerIp(routing.peerIp());
|
||||
if (peerIp == null || peerIp.isEmpty()) {
|
||||
appendSystemLine(activeConversationKey, "Informe um IP válido para enviar a um peer.");
|
||||
return;
|
||||
}
|
||||
if (LocalNetworkInfo.isLocalIpv4(peerIp)) {
|
||||
appendSystemLine(activeConversationKey, "TCP para o próprio host não é suportado; use sala geral ou UDP unicast.");
|
||||
return;
|
||||
}
|
||||
connectionManager.sendTcpAsync(message, peerIp, new TcpAttemptListener() {
|
||||
@Override
|
||||
public void onConnecting(String endpoint) {
|
||||
showTcpAttempt("Conectando em " + endpoint + "…");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(pitiupi.net.tcp.TcpConnection connection) {
|
||||
clearTcpAttempt();
|
||||
appendChatLine(activeConversationKey, message);
|
||||
input.setText("");
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
failTcpAttempt(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendTcpToRoom(ChatMessage message) {
|
||||
java.util.List<String> peers = new java.util.ArrayList<>();
|
||||
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
|
||||
if (view.isBroadcastTarget() || view.isSelf()) {
|
||||
continue;
|
||||
}
|
||||
view.getInetAddress().ifPresent(address -> peers.add(PeerIpv4.fromInetAddress(address)));
|
||||
}
|
||||
if (peers.isEmpty()) {
|
||||
appendSystemLine(activeConversationKey, "Nenhum colega na sala para receber por TCP.");
|
||||
return;
|
||||
}
|
||||
int total = peers.size();
|
||||
int[] remaining = {total};
|
||||
int[] succeeded = {0};
|
||||
boolean[] announced = {false};
|
||||
for (String ip : peers) {
|
||||
connectionManager.sendTcpAsync(message, ip, new TcpAttemptListener() {
|
||||
@Override
|
||||
public void onConnecting(String endpoint) {
|
||||
if (!announced[0]) {
|
||||
announced[0] = true;
|
||||
showTcpAttempt("Conectando por TCP…");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(pitiupi.net.tcp.TcpConnection connection) {
|
||||
finishRoomTcp(message, total, remaining, succeeded, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String error) {
|
||||
finishRoomTcp(message, total, remaining, succeeded, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void finishRoomTcp(ChatMessage message, int total, int[] remaining, int[] succeeded, boolean thisOk) {
|
||||
if (thisOk) {
|
||||
succeeded[0]++;
|
||||
}
|
||||
remaining[0]--;
|
||||
if (remaining[0] > 0) {
|
||||
return;
|
||||
}
|
||||
clearTcpAttempt();
|
||||
if (succeeded[0] == 0) {
|
||||
appendSystemLine(activeConversationKey, "Não foi possível conectar nos colegas por TCP.");
|
||||
flushTranscriptToPane();
|
||||
return;
|
||||
}
|
||||
if (succeeded[0] < total) {
|
||||
appendSystemLine(activeConversationKey, "Não foi possível conectar em um ou mais colegas por TCP.");
|
||||
}
|
||||
appendChatLine(activeConversationKey, message);
|
||||
input.setText("");
|
||||
flushTranscriptToPane();
|
||||
}
|
||||
|
||||
private SendTarget resolveSendTarget(PluginRoutingPanel.RoutingSnapshot routing) throws Exception {
|
||||
if (routing.everyoneInRoom()) {
|
||||
return SendTarget.broadcastAll();
|
||||
|
||||
@@ -86,6 +86,15 @@ public class PrivateChatController {
|
||||
}
|
||||
}
|
||||
|
||||
public void abortOutgoing(String address) {
|
||||
if (address == null) {
|
||||
return;
|
||||
}
|
||||
String canonical = PeerIpv4.normalize(address);
|
||||
pendingConnections.remove(canonical);
|
||||
pendingTextByAddress.remove(canonical);
|
||||
}
|
||||
|
||||
public void closeChat(TcpConnection connection) {
|
||||
String address = addressKey(connection);
|
||||
connections.remove(address);
|
||||
|
||||
@@ -15,6 +15,7 @@ import pitiupi.control.ConnectionManager;
|
||||
import pitiupi.models.Messages.Message;
|
||||
import pitiupi.models.Plugin;
|
||||
import pitiupi.models.User;
|
||||
import pitiupi.net.tcp.TcpAttemptListener;
|
||||
import pitiupi.net.tcp.TcpConnection;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
@@ -133,18 +134,32 @@ public class ChatPlugin extends Plugin implements PrivateChatHost {
|
||||
|
||||
@Override
|
||||
public void openConnection(String ip) {
|
||||
try {
|
||||
TcpConnection connection = connectionManager.connectPluginTcp(
|
||||
InetAddress.getByName(ip),
|
||||
getIdentifier(),
|
||||
getIdentifier()
|
||||
);
|
||||
privateChatController.registerOutgoingConnection(ip, connection);
|
||||
} catch (Exception e) {
|
||||
System.out.println("Error opening connection: " + e.getMessage());
|
||||
connectionManager.connectPluginTcpAsync(ip, getIdentifier(), getIdentifier(), new TcpAttemptListener() {
|
||||
@Override
|
||||
public void onConnecting(String endpoint) {
|
||||
if (chatPanel != null) {
|
||||
chatPanel.showTcpAttempt("Conectando em " + endpoint + "…");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(TcpConnection connection) {
|
||||
if (chatPanel != null) {
|
||||
chatPanel.clearTcpAttempt();
|
||||
}
|
||||
privateChatController.registerOutgoingConnection(ip, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(String message) {
|
||||
privateChatController.abortOutgoing(ip);
|
||||
if (chatPanel != null) {
|
||||
chatPanel.failTcpAttempt(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPluginIdentifier() {
|
||||
return getIdentifier();
|
||||
|
||||
Reference in new issue
Block a user