package act.cli;
/*-
* #%L
* ACT Framework
* %%
* Copyright (C) 2014 - 2017 ActFramework
* %%
* Licensed 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.
* #L%
*/
import act.Destroyable;
import act.app.ActionContext;
import act.app.App;
import act.app.CliServer;
import act.cli.builtin.IterateCursor;
import act.cli.event.CliSessionStart;
import act.cli.event.CliSessionTerminate;
import act.cli.util.CliCursor;
import act.handler.CliHandler;
import act.util.Banner;
import act.util.DestroyableBase;
import jline.console.ConsoleReader;
import org.osgl.$;
import org.osgl.util.C;
import org.osgl.util.IO;
import org.osgl.util.S;
import javax.enterprise.context.ApplicationScoped;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.Map;
import static act.app.App.logger;
public class CliSession extends DestroyableBase implements Runnable {
private String id;
private CliServer server;
protected App app;
private Socket socket;
private long ts;
private boolean exit;
private Thread runningThread;
private ConsoleReader console;
private CliCursor cursor;
private CommandNameCompleter commandNameCompleter;
// the current handler
private CliHandler handler;
private boolean daemon;
private CliContext cliContext;
/**
* Allow user command to attach data to the context and fetched for later use.
* <p>
* A typical usage scenario is user command wants to set up a "context" for the
* following commands. However it shall provide a command to exit the "context"
* </p>
*/
private Map<String, Object> attributes = C.newMap();
/**
* Construct a CliOverHttp session
* @param context the ActionContext
*/
protected CliSession(ActionContext context) {
this.app = context.app();
this.id = context.session().id();
this.ts = $.ms();
}
public CliSession(Socket socket, CliServer server) {
this.socket = $.NPE(socket);
this.server = $.NPE(server);
this.app = server.app();
id = app.cuid();
ts = $.ms();
commandNameCompleter = new CommandNameCompleter(app);
}
public String id() {
return id;
}
public CliSession attribute(String key, Object val) {
attributes.put(key, val);
return this;
}
public CliSession removeAttribute(String key) {
attributes.remove(key);
return this;
}
public CliSession dameon(boolean daemon) {
this.daemon = daemon;
return this;
}
public CliCursor cursor() {
return cursor;
}
public CliSession cursor(CliCursor cursor) {
this.cursor = cursor;
return this;
}
public void removeCursor() {
cursor = null;
}
public <T> T attribute(String key) {
return $.cast(attributes.get(key));
}
/**
* Check if this session is expired.
* @param expiration the expiration in seconds
* @return {@code true} if this session is expired
*/
public boolean expired(int expiration) {
if (daemon && null != cliContext && !cliContext.disconnected()) {
return false;
}
long l = expiration * 1000;
return l < ($.ms() - ts);
}
@Override
protected void releaseResources() {
stop();
server = null;
Destroyable.Util.tryDestroyAll(attributes.values(), ApplicationScoped.class);
}
@Override
public void run() {
runningThread = Thread.currentThread();
try {
app.eventBus().emitSync(new CliSessionStart(this));
OutputStream os = socket.getOutputStream();
console = new ConsoleReader(socket.getInputStream(), os);
String banner = Banner.cachedBanner();
printBanner(banner, console);
String appName = App.instance().name();
if (S.blank(appName)) {
appName = "act";
}
console.setPrompt(S.fmt("%s[%s]>", appName, id));
console.addCompleter(commandNameCompleter);
console.getTerminal().setEchoEnabled(false);
while (!exit) {
final String line = console.readLine();
if (exit) {
console.println("session terminated");
console.flush();
return;
}
ts = $.ms();
app.checkUpdates(true);
if (S.blank(line)) {
continue;
}
try {
CliContext context = new CliContext(line, app, console, this);
cliContext = context;
context.handle();
} catch ($.Break b) {
Object payload = b.get();
if (null == payload) {
continue;
}
if (payload instanceof Boolean) {
exit = b.get();
} else if (payload instanceof String) {
console.println((String) payload);
} else {
console.println(S.fmt("INTERNAL ERROR: unknown payload type: %s", payload.getClass()));
}
}
}
} catch (InterruptedIOException e) {
logger.info("session thread interrupted");
} catch (SocketException e) {
logger.error(e.getMessage());
} catch (Exception e) {
logger.error(e, "Error processing cli session");
} finally {
if (null != server) {
server.remove(this);
}
IO.close(socket);
app.eventBus().emitSync(new CliSessionTerminate(this));
}
}
public void stop() {
exit = true;
if (null != runningThread) {
runningThread.interrupt();
}
console = null;
IO.close(socket);
}
public void stop(String message) {
if (null != console) {
PrintWriter pw = new PrintWriter(console.getOutput());
pw.println(message);
pw.flush();
}
stop();
}
void handler(CliHandler handler) {
if (handler == IterateCursor.INSTANCE) {
return;
}
if (null == this.handler || S.string(this.handler).equals(S.string(handler))) {
this.handler = handler;
return;
}
this.handler = handler;
removeCursor();
}
private static void printBanner(String banner, ConsoleReader console) throws IOException {
String[] lines = banner.split("[\n\r]");
for (String line : lines) {
console.println(line);
}
}
}