001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.cli; 019 020import dev.enola.ai.adk.web.AdkHttpServer; 021import dev.enola.chat.sshd.EnolaSshServer; 022import dev.enola.common.FreedesktopDirectories; 023import dev.enola.common.context.TLC; 024import dev.enola.core.grpc.EnolaGrpcServer; 025import dev.enola.core.proto.EnolaServiceGrpc; 026import dev.enola.web.*; 027import dev.enola.web.netty.NettyHttpServer; 028 029import org.jspecify.annotations.Nullable; 030 031import picocli.CommandLine; 032 033@CommandLine.Command(name = "server", description = "Start HTTP, SSH and/or gRPC Server/s") 034public class ServerCommand extends CommandWithModel { 035 036 @CommandLine.ArgGroup(exclusive = false, multiplicity = "1") 037 HttpAndOrGrpcPorts ports; 038 039 @CommandLine.ArgGroup(exclusive = false) 040 @Nullable AiOptions aiOptions; 041 042 @CommandLine.Option( 043 names = {"--immediateExitOnlyForTest"}, 044 defaultValue = "false", 045 hidden = true) 046 boolean immediateExitOnlyForTest; 047 048 private @Nullable EnolaGrpcServer grpcServer; 049 private @Nullable WebServer httpServer; 050 private @Nullable AutoCloseable chatServer; 051 private @Nullable EnolaSshServer sshServer; 052 053 @Override 054 protected void run(EnolaServiceGrpc.EnolaServiceBlockingStub service) throws Exception { 055 try (var ctx = TLC.open()) { 056 setup(ctx); 057 runInContext(service); 058 } 059 } 060 061 private void runInContext(EnolaServiceGrpc.EnolaServiceBlockingStub service) throws Exception { 062 var out = spec.commandLine().getOut(); 063 064 // gRPC API 065 if (ports.grpcPort != null) { 066 grpcServer = new EnolaGrpcServer(esp, esp.getEnolaService()); 067 grpcServer.start(ports.grpcPort); 068 out.println("gRPC API server now available on port " + grpcServer.getPort()); 069 } 070 071 // HTML UI + JSON REST API 072 if (ports.httpPort != null) { 073 var handlers = new WebHandlers(); 074 new UI(service, getMetadataProvider(new EnolaThingProvider(service))) 075 .register(handlers); 076 handlers.register("/api", new RestAPI(service)); 077 httpServer = new NettyHttpServer(ports.httpPort, handlers); 078 httpServer.start(); 079 out.println( 080 "HTTP JSON REST API + HTML UI server started; open http://" 081 + httpServer.getInetAddress() 082 + "/ui ..."); 083 } 084 085 // Chat (ADK) UI 086 if (ports.chatPort != null) { 087 var agents = AI.load(rp, aiOptions); 088 chatServer = AdkHttpServer.start(agents, ports.chatPort); 089 out.println( 090 "HTTP Chat UI server started; open http://localhost:" 091 + ports.chatPort 092 + " ..."); 093 } 094 095 // SSH Server 096 if (ports.sshPort != null) { 097 var hostKeyPath = FreedesktopDirectories.HOSTKEY_PATH; 098 sshServer = new EnolaSshServer(ports.sshPort, hostKeyPath); 099 out.println("SSH server (" + hostKeyPath + ") running on port " + sshServer.port()); 100 } 101 102 if (!immediateExitOnlyForTest) { 103 Thread.currentThread().join(); 104 } else { 105 close(); 106 } 107 } 108 109 public void close() throws Exception { 110 if (grpcServer != null) { 111 grpcServer.close(); 112 } 113 if (httpServer != null) { 114 httpServer.close(); 115 } 116 if (chatServer != null) { 117 chatServer.close(); 118 } 119 if (sshServer != null) { 120 sshServer.close(); 121 } 122 // TODO Thread.currentThread().interrupt(); 123 } 124 125 static class HttpAndOrGrpcPorts { 126 @CommandLine.Option( 127 names = {"--httpPort"}, 128 description = "HTTP Port of Enola UI") 129 @Nullable Integer httpPort; 130 131 @CommandLine.Option( 132 names = {"--chatPort"}, 133 description = "HTTP Port of Chat UI") 134 @Nullable Integer chatPort; 135 136 @CommandLine.Option( 137 names = {"--sshPort"}, 138 description = "SSH (Chat) Port") 139 @Nullable Integer sshPort; 140 141 @CommandLine.Option( 142 names = {"--grpcPort"}, 143 description = "gRPC API Port") 144 @Nullable Integer grpcPort; 145 } 146}