diff --git a/launcher.sh b/launcher.sh new file mode 100755 index 00000000..0e907421 --- /dev/null +++ b/launcher.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Inicia o servidor do miniTruco e o reinicia quando o JAR é modificado. +# +# Esse reinício é feito com um "soft shutdown" do servidor (enviando um SIGUSR1 +# para que ele libere a porta) e subindo uma nova instância imediatamente*; dessa +# forma os jogadores podem finalizar partidas em andamento, mas novas conexões +# vão para o servidor novo. +# +# * na real uns 2-3 segundos pra garantir que o .jar novo está finalizado + +if [ -z "$1" ]; then + echo "Erro: é necessário fornecer o caminho do arquivo JAR como parâmetro." + echo "Exemplo de uso: $0 /caminho/do/arquivo.jar" + exit 1 +fi + +jar="$1" +servidor_pid="" + +inicia_servidor() { + java -jar "$jar" & + servidor_pid=$! # $! contém o PID do último processo em background +} + +shutdown_suave_do_servidor() { + if [ -n "$servidor_pid" ]; then + echo "Enviando sinal SIGUSR1 para o servidor no PID: $servidor_pid" + kill -SIGUSR1 "$servidor_pid" + fi +} + +servidor_em_execucao() { + if ps -p "$servidor_pid" >/dev/null; then + return 0 + else + return 1 + fi +} + +aguarda_mudanca_no_jar() { + (inotifywait -e modify "$jar") & # em background para não bloquer o SIGTERM + inotify_pid=$! + wait $inotify_pid +} + +aguarda_o_novo_jar_estar_pronto() { + while [ ! -e "$jar" ]; do + sleep 1 + done + sleep 2 # Para ter certeza que o jar foi completamente salvo +} + +# Se o launcher for finalizado, finaliza o servidor também +trap 'shutdown_suave_do_servidor; exit 0' SIGTERM + +###### O script efetivamente começa aqui ###### + +while true; do + inicia_servidor + aguarda_mudanca_no_jar + shutdown_suave_do_servidor + aguarda_o_novo_jar_estar_pronto +done diff --git a/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java b/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java index 63a85ae6..c41bf8ee 100644 --- a/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java +++ b/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java @@ -6,13 +6,16 @@ import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; +import java.net.SocketTimeoutException; + +import sun.misc.Signal; public class MiniTrucoServer { + /** + * Porta onde o servidor escuta por conexões. Acho que ninguém nunca + * entendeu porque eu escolhi este número. 🤡 + */ public static final int PORTA_SERVIDOR = 6912; /** @@ -20,44 +23,74 @@ public class MiniTrucoServer { */ public static final String VERSAO_SERVER = "3.0"; - public static DateFormat dfStartup; - - public static Date dataStartup; - - public static String strDataStartup; - + /** + * Ponto de entrada do servidor. Apenas dispara a thread que aceita + * conexões e encerra ela quando o launcher.sh solicitar. + */ public static void main(String[] args) { + // Enquanto esta thread estiver rodando, o servidor vai aceitar conexões + // e colocar o socket de cada cliente numa thread separada + Thread threadAceitaConexoes = new Thread(() -> aceitaConexoes()); + threadAceitaConexoes.start(); + + // Se recebermos um USR1, o .jar foi atualizado. Nesse caso, vamos parar + // de aceitar conexões (liberando a porta para a nova versão) mas as + // threads dos jogadores conectados e partidas em andamento continuam + // rodando. + Signal.handle(new Signal("USR1"), signal -> { + ServerLogger.evento("Recebido sinal USR1 - interrompendo threadAceitaConxoes"); + threadAceitaConexoes.interrupt(); + }); + + // Quando *todas* as threads encerrarem, loga o evento final + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + ServerLogger.evento("Servidor finalizado; JVM desligando. Tchau."); + })); + + // A thread inicial termina por aqui, mas o servidor continua rodandno + // até que todas as threads se encerrem. + } + /** + * Loop que aceita conexões de clientes e coloca o socket de cada um num + * objeto JogadorConectado que roda em uma thread separada. + *
+ * Permanece em execução até que a thread onde ele está rodando receba + * um interrupt. + */ + public static void aceitaConexoes() { + ServerLogger.evento("Servidor inicializado e escutando na porta " + PORTA_SERVIDOR); + ServerSocket s = null; try { - - // Guarda a data de início do servidor num formato apropriado para HTTP - // vide JogadorContectado.serveArquivosApplet - - dfStartup = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", - Locale.US); - dataStartup = new Date(); - strDataStartup = dfStartup.format(dataStartup); - - ServerLogger - .evento("Servidor Inicializado, pronto para escutar na porta " - + PORTA_SERVIDOR); - - try { - ServerSocket s = new ServerSocket(PORTA_SERVIDOR); - while (true) { - Socket sCliente = s.accept(); - JogadorConectado j = new JogadorConectado(sCliente); - Thread t = new Thread(j); - t.start(); + s = new ServerSocket(PORTA_SERVIDOR); + // Vamos checar a cada 1s se recebemos um interrupt + s.setSoTimeout(1000); + while (true) { + Socket sCliente; + try { + sCliente = s.accept(); + } catch (SocketTimeoutException e) { + // Era um interrupt, vamos sair do loop + if (Thread.interrupted()) { + break; + } + // Era só o timeout, vamos continuar + continue; } - } catch (IOException e) { - ServerLogger.evento(e, "Erro de I/O no ServerSocket, saindo do programa"); + JogadorConectado j = new JogadorConectado(sCliente); + (new Thread(j)).start(); } - + } catch (IOException e) { + ServerLogger.evento(e, "Erro de I/O no ServerSocket"); } finally { - ServerLogger.evento("Servidor Finalizado"); + if (s != null) { + try { + s.close(); + } catch (IOException e) { + ServerLogger.evento(e, "Erro de I/O ao fechar ServerSocket"); + } + } + ServerLogger.evento("Servidor não está mais escutando; aguardando finalização dos jogadores conectados."); } - } - }