package com.github.ompc.greys.core;
import com.github.ompc.greys.core.command.Commands;
import jline.console.ConsoleReader;
import jline.console.completer.Completer;
import jline.console.history.FileHistory;
import jline.console.history.History;
import jline.console.history.MemoryHistory;
import org.apache.commons.lang3.StringUtils;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import static com.github.ompc.greys.core.util.GaStringUtils.DEFAULT_PROMPT;
import static java.io.File.separatorChar;
import static java.lang.System.getProperty;
import static jline.console.KeyMap.CTRL_D;
import static jline.internal.Preconditions.checkNotNull;
import static org.apache.commons.io.IOUtils.closeQuietly;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* Greys控制台
* Created by oldmanpushcart@gmail.com on 15/5/30.
*/
public class GreysConsole {
private static final byte EOT = 0x04;
private static final byte EOF = -1;
// 5分钟
private static final int _1MIN = 60 * 1000;
// 工作目录
private static final String WORKING_DIR = getProperty("user.home");
// 历史命令存储文件
private static final String HISTORY_FILENAME = ".greys_history";
private final ConsoleReader console;
private final History history;
private final Writer out;
private final Socket socket;
private BufferedWriter socketWriter;
private BufferedReader socketReader;
private volatile boolean isRunning;
public GreysConsole(InetSocketAddress address) throws IOException {
this.console = initConsoleReader();
this.history = initHistory();
this.out = console.getOutput();
this.history.moveToEnd();
this.console.setHistoryEnabled(true);
this.console.setHistory(history);
this.console.setExpandEvents(false);
this.socket = connect(address);
// 关闭会话静默
disableSilentOfSession();
// 初始化自动补全
initCompleter();
this.isRunning = true;
activeConsoleReader();
socketWriter.write("version\n");
socketWriter.flush();
loopForWriter();
}
// jLine的自动补全
private void initCompleter() {
final SortedSet<String> commands = new TreeSet<String>();
commands.addAll(Commands.getInstance().listCommands().keySet());
console.addCompleter(new Completer() {
@Override
public int complete(String buffer, int cursor, List<CharSequence> candidates) {
// buffer could be null
checkNotNull(candidates);
if (buffer == null) {
candidates.addAll(commands);
} else {
String prefix = buffer;
if (buffer.length() > cursor) {
prefix = buffer.substring(0, cursor);
}
for (String match : commands.tailSet(prefix)) {
if (!match.startsWith(prefix)) {
break;
}
candidates.add(match);
}
}
if (candidates.size() == 1) {
candidates.set(0, candidates.get(0) + " ");
}
return candidates.isEmpty() ? -1 : 0;
}
});
}
private History initHistory() throws IOException {
final File WORK_DIR = new File(WORKING_DIR);
final File historyFile = new File(WORKING_DIR + separatorChar + HISTORY_FILENAME);
if (WORK_DIR.canWrite()
&& WORK_DIR.canRead()
&& ((!historyFile.exists() && historyFile.createNewFile()) || historyFile.exists())) {
return new FileHistory(historyFile);
}
return new MemoryHistory();
}
private void disableSilentOfSession() throws IOException {
socketWriter.write("session -s false\n");
socketWriter.flush();
waitingForEOT();
}
private void waitingForEOT() throws IOException {
int ch;
do {
ch = socketReader.read();
} while (ch != EOT && ch != EOF);
}
private ConsoleReader initConsoleReader() throws IOException {
final ConsoleReader console = new ConsoleReader(System.in, System.out);
console.getKeys().bind("" + CTRL_D, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
try {
socketWriter.write(CTRL_D);
socketWriter.flush();
} catch (Exception e1) {
// 这里是控制台,可能么?
GreysConsole.this.err("write fail : %s", e1.getMessage());
shutdown();
}
}
});
return console;
}
/**
* 激活网络
*/
private Socket connect(InetSocketAddress address) throws IOException {
final Socket socket = new Socket();
socket.setSoTimeout(0);
socket.connect(address, _1MIN);
socket.setKeepAlive(true);
socketWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
socketReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
return socket;
}
/**
* 激活读线程
*/
private void activeConsoleReader() {
final Thread socketThread = new Thread("ga-console-reader-daemon") {
private StringBuilder lineBuffer = new StringBuilder();
@Override
public void run() {
try {
while (isRunning) {
final String line = console.readLine();
// 如果是\结尾,则说明还有下文,需要对换行做特殊处理
if (StringUtils.endsWith(line, "\\")) {
// 去掉结尾的\
lineBuffer.append(line.substring(0, line.length() - 1));
continue;
} else {
lineBuffer.append(line);
}
final String lineForWrite = lineBuffer.toString();
lineBuffer = new StringBuilder();
// replace ! to \!
// history.add(StringUtils.replace(lineForWrite, "!", "\\!"));
// flush if need
if (history instanceof Flushable) {
((Flushable) history).flush();
}
console.setPrompt(EMPTY);
if (isNotBlank(lineForWrite)) {
socketWriter.write(lineForWrite + "\n");
} else {
socketWriter.write("\n");
}
socketWriter.flush();
}
} catch (IOException e) {
err("read fail : %s", e.getMessage());
shutdown();
}
}
};
socketThread.setDaemon(true);
socketThread.start();
}
private volatile boolean hackingForReDrawPrompt = true;
/*
* Console在启动的时候会出现第一个提示符占位不准的BUG
* 这个我无法很优雅的消除,所以这里做了一个小hacking
*/
private void hackingForReDrawPrompt() {
if (hackingForReDrawPrompt) {
hackingForReDrawPrompt = false;
System.out.println(EMPTY);
}
}
private void loopForWriter() {
try {
while (isRunning) {
final int c = socketReader.read();
if (c == EOF) {
break;
}
if (c == EOT) {
hackingForReDrawPrompt();
console.setPrompt(DEFAULT_PROMPT);
console.redrawLine();
} else {
out.write(c);
}
out.flush();
}
} catch (IOException e) {
err("write fail : %s", e.getMessage());
shutdown();
}
}
private void err(String format, Object... args) {
System.err.println(String.format(format, args));
}
/**
* 关闭Console
*/
private void shutdown() {
isRunning = false;
closeQuietly(socketWriter);
closeQuietly(socketReader);
closeQuietly(socket);
console.shutdown();
}
public static void main(String... args) throws IOException {
new GreysConsole(new InetSocketAddress(args[0], Integer.valueOf(args[1])));
}
}