diff --git a/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java b/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java index 7837bef855..700d1836f2 100644 --- a/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java +++ b/core/src/main/java/lucee/runtime/config/ConfigWebFactory.java @@ -140,6 +140,7 @@ import lucee.runtime.listener.MixedAppListener; import lucee.runtime.listener.ModernAppListener; import lucee.runtime.listener.SerializationSettings; +import lucee.runtime.lsp.LSPEndpointFactory; import lucee.runtime.monitor.ActionMonitor; import lucee.runtime.monitor.ActionMonitorCollector; import lucee.runtime.monitor.ActionMonitorFatory; @@ -689,6 +690,8 @@ synchronized static void load(ConfigServerImpl cs, ConfigImpl config, ConfigWebI _loadStartupHook(cs, config, root, log); if (LOG) LogUtil.logGlobal(ThreadLocalPageContext.getConfig(cs == null ? config : cs), Log.LEVEL_DEBUG, ConfigWebFactory.class.getName(), "loaded startup hook"); + + if (config instanceof ConfigServer) LSPEndpointFactory.init(config, false); } config.setLoadTime(System.currentTimeMillis()); diff --git a/core/src/main/java/lucee/runtime/lsp/LSPEndpointFactory.java b/core/src/main/java/lucee/runtime/lsp/LSPEndpointFactory.java new file mode 100644 index 0000000000..0faec00179 --- /dev/null +++ b/core/src/main/java/lucee/runtime/lsp/LSPEndpointFactory.java @@ -0,0 +1,244 @@ +package lucee.runtime.lsp; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.servlet.http.Cookie; + +import lucee.print; +import lucee.commons.io.SystemUtil; +import lucee.commons.io.log.Log; +import lucee.loader.engine.CFMLEngine; +import lucee.loader.engine.CFMLEngineFactory; +import lucee.loader.util.Util; +import lucee.runtime.Component; +import lucee.runtime.PageContext; +import lucee.runtime.config.Config; +import lucee.runtime.config.ConfigServer; +import lucee.runtime.config.ConfigWeb; +import lucee.runtime.op.Caster; +import lucee.runtime.thread.ThreadUtil; + +public class LSPEndpointFactory { + private static final int DEFAULT_LSP_PORT = 2089; // Common LSP port + private static final String DEFAULT_COMPONENT = "org.lucee.cfml.lsp.LSPEndpoint"; + private static final long TIMEOUT = 3000; + private static final String DEFAULT_LOG = "debug"; + private ServerSocket serverSocket; + private ExecutorService executor; + private volatile boolean running = true; + private CFMLEngine engine; + private int port; + private String cfcPath; + private Log log; + private static LSPEndpointFactory instance; + + private LSPEndpointFactory(Config config) { + // setup config and utils + engine = CFMLEngineFactory.getInstance(); + log = getLog(config); + port = engine.getCastUtil().toIntValue(SystemUtil.getSystemPropOrEnvVar("lucee.lsp.port", null), DEFAULT_LSP_PORT); + cfcPath = engine.getCastUtil().toString(SystemUtil.getSystemPropOrEnvVar("lucee.lsp.component", null), DEFAULT_COMPONENT); + if (Util.isEmpty(cfcPath, true)) cfcPath = DEFAULT_COMPONENT; + + log.debug("lsp", "LSP server port: " + port); + log.debug("lsp", "LSP server component endpoint: " + cfcPath); + } + + public static LSPEndpointFactory init(Config config, boolean forceRestart) throws IOException { + if (Caster.toBooleanValue(SystemUtil.getSystemPropOrEnvVar("lucee.lsp.enabled", null), false)) { + print.e("---- LSPEndpointFactory ----"); + synchronized (SystemUtil.createToken("LSPEndpointFactory", "init")) { + if (forceRestart) { + print.e("- restart ----"); + if (instance != null) { + instance.stop(); + } + instance = new LSPEndpointFactory(config).start(); + } + else { + if (instance == null) { + print.e("- start ----"); + instance = new LSPEndpointFactory(config).start(); + } + } + } + print.e("- init"); + } + return instance; + } + + private LSPEndpointFactory start() throws IOException { + log.debug("lsp", "starting LSP server"); + try { + serverSocket = new ServerSocket(port); + } + catch (IOException e) { + error("lsp", e); + throw e; + } + executor = Executors.newCachedThreadPool(); + + // Start listening thread + Thread listenerThread = new Thread(() -> { + while (running) { + try { + Socket clientSocket = serverSocket.accept(); + executor.submit(() -> handleClient(clientSocket)); + } + catch (IOException e) { + if (running) { + error("lsp", e); + } + } + } + }, "LSP-Listener"); + + listenerThread.setDaemon(true); + listenerThread.start(); + + log.debug("lsp", "LSP server started"); + return this; + } + + private void stop() throws IOException { + running = false; + if (executor != null) { + executor.shutdown(); + } + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + } + + public static LSPEndpointFactory getInstance(Config config) { + return instance; + } + + private void handleClient(Socket clientSocket) { + + log.debug("lsp", "LSP server handle client"); + try { + // Get input/output streams + BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + OutputStream out = clientSocket.getOutputStream(); + StringBuilder buffer = new StringBuilder(); + + while (!clientSocket.isClosed()) { + // Read incoming data into buffer + char[] cbuf = new char[1024]; + int len; + while ((len = reader.read(cbuf)) != -1) { + buffer.append(cbuf, 0, len); + + // Process complete messages in the buffer + while (true) { + // Look for Content-Length header + String content = buffer.toString(); + int headerIndex = content.indexOf("Content-Length: "); + if (headerIndex == -1) break; + + // Parse content length + int lengthStart = headerIndex + 16; + int lengthEnd = content.indexOf("\r\n", lengthStart); + if (lengthEnd == -1) break; + + int contentLength = Integer.parseInt(content.substring(lengthStart, lengthEnd)); + + // Find start of JSON content + int contentStart = content.indexOf("\r\n\r\n", lengthEnd); + if (contentStart == -1) break; + contentStart += 4; + + // Check if we have the complete message + if (buffer.length() < contentStart + contentLength) break; + + // Extract the JSON message + String jsonMessage = content.substring(contentStart, contentStart + contentLength); + + // Here you would call your component to handle the message + String response = processMessage(jsonMessage); + + // Send response using LSP format + if (response != null) { + String header = "Content-Length: " + response.length() + "\r\n\r\n"; + out.write(header.getBytes()); + out.write(response.getBytes()); + out.flush(); + } + + // Remove processed message from buffer + buffer.delete(0, contentStart + contentLength); + } + } + } + } + catch (Exception e) { + error("lsp", e); + } + finally { + engine.getIOUtil().closeSilent(clientSocket); + } + } + + private String processMessage(String jsonMessage) { + try { + log.info("lsp", "Received message: " + jsonMessage); + PageContext pc = createPageContext((ConfigWeb) engine.getThreadConfig()); + Component cfc = engine.getCreationUtil().createComponentFromName(pc, cfcPath); + + String response = engine.getCastUtil().toString(cfc.call(pc, "execute", new Object[] { jsonMessage })); + log.info("lsp", "response from component [" + cfcPath + "]: " + response); + + return response; + } + catch (Exception e) { + error("lsp", e); + return null; + } + } + + private void error(String type, Exception e) { + // TODO remove the print out + System.err.println(type); + e.printStackTrace(); + log.error(type, e); + } + + public static Log getLog(Config config) { + if (config == null) config = CFMLEngineFactory.getInstance().getThreadConfig(); + if (config instanceof ConfigServer) { + // we only log to config Server if there is no web context + Config cw = CFMLEngineFactory.getInstance().getThreadConfig(); + if (cw != null) config = cw; + } + if (config == null) return null; + try { + Log log = config.getLog("lsp"); + if (log == null) log = config.getLog(DEFAULT_LOG); + if (log != null) return log; + } + catch (Exception e) { + Log log = config.getLog(DEFAULT_LOG); + log.error("lsp", e); + return log; + } + return null; + } + + public static PageContext createPageContext(final ConfigWeb cw) { + return ThreadUtil.createPageContext(cw, new ByteArrayOutputStream(), "", "/", "", new Cookie[0], null, null, null, null, true, TIMEOUT); + } + + public static void releasePageContext(PageContext pc) { + CFMLEngineFactory.getInstance().releasePageContext(pc, true); + } + +} \ No newline at end of file diff --git a/loader/build.xml b/loader/build.xml index 3ab7e3ec89..e8fd88d97e 100644 --- a/loader/build.xml +++ b/loader/build.xml @@ -2,7 +2,7 @@ - + diff --git a/loader/pom.xml b/loader/pom.xml index d62b48dcdb..956dd6165e 100644 --- a/loader/pom.xml +++ b/loader/pom.xml @@ -3,7 +3,7 @@ org.lucee lucee - 6.1.2.12-SNAPSHOT + 6.1.2.13-SNAPSHOT jar Lucee Loader Build