ES
apps/caramail-server / src / Command / ServeCommand.php · commit 070afda
<?phpnamespace App\Command;use App\Chat\ChatServer;use App\Chat\RoomStore;use App\Net\TcpServer;use Symfony\Component\Console\Attribute\AsCommand;use Symfony\Component\Console\Command\Command;use Symfony\Component\Console\Input\InputInterface;use Symfony\Component\Console\Input\InputOption;use Symfony\Component\Console\Output\OutputInterface;/** * The server: listens for the chat clients until SIGTERM / SIGINT (docker stop, Ctrl+C), then saves the permanent * rooms. Settings: options, else CARAMAIL_HOST, CARAMAIL_PORT, CARAMAIL_ADMIN_KEY, CARAMAIL_SUPER_KEY, CARAMAIL_OWNER_IPS. */#[AsCommand('serve', 'Runs the chat server (TCP, the protocol of the 2002 applet)')]final class ServeCommand extends Command{    public function __construct(private readonly string $data)    {        parent::__construct();    }    protected function configure(): void    {        $this            ->addOption('host', null, InputOption::VALUE_REQUIRED, 'address to listen on', getenv('CARAMAIL_HOST') ?: '0.0.0.0')            ->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'TCP port for the clients', getenv('CARAMAIL_PORT') ?: '4500')            ->addOption('ws-port', null, InputOption::VALUE_REQUIRED, 'WebSocket port (behind the web server on 443; 0 = none)', getenv('CARAMAIL_WS_PORT') ?: '8080')            ->addOption('admin-key', null, InputOption::VALUE_REQUIRED, 'key that makes a client a moderator (in its tempid: k=...)', getenv('CARAMAIL_ADMIN_KEY') ?: '')            ->addOption('super-key', null, InputOption::VALUE_REQUIRED, 'key that makes a client an administrator (admin 2)', getenv('CARAMAIL_SUPER_KEY') ?: '')            ->addOption('owner-ips', null, InputOption::VALUE_REQUIRED, 'the owner\'s public addresses, comma separated: the login form offers them normal / moderator / admin', getenv('CARAMAIL_OWNER_IPS') ?: '');    }    protected function execute(InputInterface $input, OutputInterface $output): int    {        if (!function_exists('stream_socket_server') || getenv('JUMPHP_WEB_TERMINAL')) {            $output->writeln('<error>The server needs a real machine (Docker, a service): a browser cannot listen on a port.</error>');            return Command::FAILURE;        }        $log = static function (string $line) use ($output): void {            $output->writeln(date('[Y-m-d H:i:s] ') . $line);        };        $store = new RoomStore($this->data . '/rooms.json');        $chat = new ChatServer($store, (string)$input->getOption('admin-key'), $output->isVerbose() ? $log : static function (string $l) use ($log): void {            if (preg_match('/^(login|leave|drop|unknown|.* ejects )/', $l)) {                $log($l);            }        });        $chat->setRoles((string)$input->getOption('super-key'), explode(',', (string)$input->getOption('owner-ips')));        $server = new TcpServer($chat, $log);        try {            $address = $server->listen((string)$input->getOption('host'), (int)$input->getOption('port'));            if ((int)$input->getOption('ws-port') > 0) {                $address .= ', WebSocket ' . $server->listenWebSocket((string)$input->getOption('host'), (int)$input->getOption('ws-port'));            }        } catch (\RuntimeException $e) {            $output->writeln('<error>' . $e->getMessage() . '</error>');            return Command::FAILURE;        }        $log(ChatServer::BANNER . " listening on $address, data in {$this->data}");        if (function_exists('pcntl_async_signals')) {            pcntl_async_signals(true);            foreach ([SIGTERM, SIGINT, SIGHUP] as $signal) {                pcntl_signal($signal, static fn () => $server->stop());            }        }        $status = new StatusFile($this->data . '/status.json');        $saveEvery = 60;        $server->run(function () use ($chat, $status, $store, &$saveEvery): void {            $status->write($chat->stats());            if (--$saveEvery <= 0) {                $saveEvery = 60;                $store->save($chat->rooms());            }        });        $chat->shutdown();        $status->remove();        $log('stopped, rooms saved');        return Command::SUCCESS;    }}