/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.crash; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import org.crsh.cli.impl.Delimiter; import org.crsh.cli.impl.completion.CompletionMatch; import org.crsh.cli.spi.Completion; import org.crsh.plugin.PluginContext; import org.crsh.shell.Shell; import org.crsh.shell.ShellFactory; import org.crsh.shell.ShellProcess; import org.crsh.util.Utils; import org.jooby.Request; import org.jooby.WebSocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.ImmutableMap; import javaslang.control.Try; class WebShellHandler implements WebSocket.OnOpen { /** The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); @SuppressWarnings("rawtypes") @Override public void onOpen(final Request req, final WebSocket ws) throws Exception { PluginContext ctx = req.require(PluginContext.class); ShellFactory factory = ctx.getPlugin(ShellFactory.class); Shell shell = factory.create(null); AtomicReference<ShellProcess> process = new AtomicReference<ShellProcess>(); ws.onMessage(msg -> { Map event = msg.to(Map.class); String type = (String) event.get("type"); if (type.equals("welcome")) { log.debug("sending welcome + prompt"); ws.send(event("print", shell.getWelcome())); ws.send(event("prompt", shell.getPrompt())); } else if (type.equals("execute")) { String command = (String) event.get("command"); Integer width = (Integer) event.get("width"); Integer height = (Integer) event.get("height"); process.set(shell.createProcess(command)); SimpleProcessContext context = new SimpleProcessContext(r -> { Try.run(() -> { // reset process process.set(null); ws.send(event("print", r.get())); ws.send(event("prompt", shell.getPrompt())); ws.send(event("end")); }).onFailure(x -> log.error("error found while sending output", x)); if ("bye".equals(command)) { ws.close(WebSocket.NORMAL); } }, width, height); log.debug("executing {}", command); process.get().execute(context); } else if (type.equals("cancel")) { ShellProcess p = process.get(); if (p != null) { log.info("cancelling {}", p); p.cancel(); } } else if (type.equals("complete")) { String prefix = (String) event.get("prefix"); CompletionMatch completion = shell.complete(prefix); Completion completions = completion.getValue(); Delimiter delimiter = completion.getDelimiter(); StringBuilder sb = new StringBuilder(); List<String> values = new ArrayList<String>(); if (completions.getSize() == 1) { String value = completions.getValues().iterator().next(); delimiter.escape(value, sb); if (completions.get(value)) { sb.append(delimiter.getValue()); } values.add(sb.toString()); } else { String commonCompletion = Utils.findLongestCommonPrefix(completions.getValues()); if (commonCompletion.length() > 0) { delimiter.escape(commonCompletion, sb); values.add(sb.toString()); } else { for (Map.Entry<String, Boolean> entry : completions) { delimiter.escape(entry.getKey(), sb); values.add(sb.toString()); sb.setLength(0); } } } log.debug("completing {} with {}", prefix, values); ws.send(event("complete", values)); } }); // clean up on close ws.onClose(status -> { log.info("closing web-socket"); ShellProcess sp = process.get(); if (sp != null) { sp.cancel(); } shell.close(); }); } private Object event(final String type, final Object data) { return ImmutableMap.of("type", type, "data", data); } private Object event(final String type) { return ImmutableMap.of("type", type); } }