package hep.io.root.daemon.xrootd;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.ExampleMode;
import org.kohsuke.args4j.Option;
import jline.ConsoleReader;
import jline.History;
import jline.Completor;
import jline.ConsoleOperations;
/**
* A simple command line interface to xrootd
* @author tonyj
*/
public class SimpleConsole {
@Option(metaVar = "host", name = "-h", usage = "Host to connect to")
private String host;
@Option(metaVar = "port", name = "-p", usage = "Port to connect to")
private int port = 1094;
@Option(metaVar = "level", name = "-l", usage = "Logging level")
private String level;
@Argument
private List<String> arguments = new ArrayList<String>();
private Session session;
private static Map<String, Command> commandMap = new TreeMap<String, Command>();
private static final ByteFormat format = new ByteFormat();
static {
commandMap.put("open", new OpenCommand());
commandMap.put("close", new CloseCommand());
commandMap.put("ping", new PingCommand());
commandMap.put("locate", new LocateCommand());
commandMap.put("level", new LevelCommand());
commandMap.put("stat", new StatCommand());
commandMap.put("exit", new ExitCommand());
commandMap.put("quit", new ExitCommand());
commandMap.put("dirList", new DirListCommand());
commandMap.put("checksum", new ChecksumCommand());
commandMap.put("stats", new StatsCommand());
commandMap.put("get", new GetCommand());
commandMap.put("put", new PutCommand());
commandMap.put("connect", new ConnectCommand());
commandMap.put("disconnect", new DisconnectCommand());
commandMap.put("protocol", new ProtocolCommand());
commandMap.put("help", new HelpCommand());
commandMap.put("remove", new RemoveCommand());
}
public static void main(String[] args) throws IOException {
new SimpleConsole().doMain(args);
}
private void doMain(String[] args) throws IOException {
CmdLineParser parser = new CmdLineParser(this);
try {
// parse the arguments.
parser.parseArgument(args);
if (host != null) {
session = new Session(host, port, System.getProperty("user.name"));
}
if (level != null) {
setLoggingLevel(level);
}
if (!arguments.isEmpty()) {
handleCommand(arguments, this, new PrintWriter(System.out, true));
} else {
ReadAheadConsole console = new ReadAheadConsole();
for (;;) {
try {
String line = console.readLine(String.format("scalla%s>", session == null ? "" : "(" + session + ")"));
if (line == null) {
break;
}
if (line.trim().length() == 0) {
continue;
}
String[] tokens = line.trim().split("\\s+");
handleCommand(Arrays.asList(tokens), this, new PrintWriter(System.out, true));
} catch (Exception x) {
x.printStackTrace();
}
}
}
} catch (CmdLineException e) {
// if there's a problem in the command line,
// you'll get this exception. this will report
// an error message.
System.err.println(e.getMessage());
System.err.printf("java %s [options...] arguments...\n", SimpleConsole.class.getName());
// print the list of available options
parser.printUsage(System.err);
System.err.println();
// print option sample. This is useful some time
System.err.printf(" Example: java %s %s\n", SimpleConsole.class.getName(), parser.printExample(ExampleMode.ALL));
return;
}
}
private Session getSession() {
return session;
}
private void setSession(Session newSession) throws IOException {
if (session != null) {
session.close();
}
session = newSession;
}
private void handleCommand(List<String> tokens, SimpleConsole session, PrintWriter console) throws SecurityException, IOException, IllegalArgumentException {
String commandName = tokens.get(0);
Command command = commandMap.get(commandName);
if (command == null) {
console.printf("Unknown command: %s\n", commandName);
} else {
command.doCommand(commandName, tokens.subList(1, tokens.size()), session, console);
}
}
private void setLoggingLevel(String token) throws IllegalArgumentException, SecurityException {
Level logLevel = Level.parse(token);
Logger.getLogger("").setLevel(logLevel);
Logger.getLogger("").getHandlers()[0].setLevel(logLevel);
}
private String getLoggingLevel() {
return Logger.getLogger("").getLevel().getName();
}
static class CommandCompletor implements Completor {
public int complete(String buffer, int position, List candidates) {
if (buffer.contains(" ")) {
return 0;
} else {
for (String command : commandMap.keySet()) {
if (command.startsWith(buffer)) {
candidates.add(command + " ");
}
}
return 0;
}
}
}
static class ControlCListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.err.println("ctrlc");
}
}
static abstract class Command {
@Option(metaVar = "level", name = "-l", usage = "Set logging level for this command")
private String level;
private SimpleConsole simpleConsole;
Session getSession() {
if (simpleConsole.getSession() == null) {
throw new RuntimeException("No session");
}
return simpleConsole.getSession();
}
void setLoggingLevel(String level) {
simpleConsole.setLoggingLevel(level);
}
void setSession(Session session) throws IOException {
simpleConsole.setSession(session);
}
String printExample() {
return new CmdLineParser(this).printExample(ExampleMode.ALL);
}
abstract void doCommand(PrintWriter console) throws IOException;
void doCommand(String command, List<String> args, SimpleConsole session, PrintWriter console) throws IOException {
reset();
CmdLineParser parser = new CmdLineParser(this);
try {
// parse the arguments.
parser.parseArgument(args.toArray(new String[args.size()]));
this.simpleConsole = session;
if (level != null) {
String oldLevel = simpleConsole.getLoggingLevel();
try {
setLoggingLevel(level);
doCommand(console);
} finally {
setLoggingLevel(oldLevel);
}
} else {
doCommand(console);
}
} catch (CmdLineException e) {
// if there's a problem in the command line,
// you'll get this exception. this will report
// an error message.
System.err.println(e.getMessage());
System.err.printf("%s [options...] arguments...\n", command);
// print the list of available options
parser.printUsage(System.err);
System.err.println();
// print option sample. This is useful some time
System.err.printf(" Example: %s %s\n", command, parser.printExample(ExampleMode.ALL));
}
}
private void printHelp(Writer writer) {
new CmdLineParser(this).printUsage(writer, null);
}
void reset() {
level = null;
}
}
static class HelpCommand extends Command {
@Argument(metaVar = "command", index = 0, usage = "Command for which help is requested")
private String commandName;
@Override
void reset() {
super.reset();
commandName = null;
}
@Override
void doCommand(PrintWriter console) throws IOException {
if (commandName == null) {
for (Map.Entry<String, Command> entry : commandMap.entrySet()) {
console.printf("%s %s\n", entry.getKey(), entry.getValue().printExample());
}
} else {
Command command = commandMap.get(commandName);
if (command == null) {
console.printf("Unknown command: %s\n", commandName);
} else {
command.printHelp(console);
}
}
}
}
static class OpenCommand extends Command {
@Argument(metaVar = "path", index = 0, required = true, usage = "Path to open")
private String path;
@Option(name = "-y", usage = "Open the file for asynchronous i/o")
private boolean async;
@Option(name = "-c", usage = "Open a file even when compressed")
private boolean compress;
@Option(name = "-d", usage = "Open a new file, deleting any existing file")
private boolean delete;
@Option(name = "-f", usage = "Ignore file usage rules")
private boolean force;
@Option(name = "-m", usage = "Create directory path if it does not already exist")
private boolean mkpath;
@Option(name = "-n", usage = "Open a new file only if it does not already exist")
private boolean newFile;
@Option(name = "-w", usage = "Open the file only if it does not cause a wait")
private boolean nowait;
@Option(name = "-a", usage = "Open only for appending")
private boolean open_apnd;
@Option(name = "-r", usage = "Open only for reading")
private boolean open_read;
@Option(name = "-u", usage = "Open for reading and writing")
private boolean open_updt;
@Option(name = "-e", usage = "Update cached information on the file�s location ")
private boolean refresh;
@Option(name = "-p", usage = "The file is being opened for replica creation")
private boolean replica;
@Option(name = "-s", usage = "Return file status information in the response")
private boolean retstat;
@Option(name = "-h", usage = "Hide the file until successfully closed")
private boolean ulterior;
@Override
void reset() {
super.reset();
async = compress = delete = force = mkpath = newFile = nowait = open_apnd = open_read = open_updt = refresh = replica = retstat = ulterior = false;
}
void doCommand(PrintWriter console) throws IOException {
int options = 0;
if (async) {
options |= XrootdProtocol.kXR_async;
}
if (compress) {
options |= XrootdProtocol.kXR_compress;
}
if (delete) {
options |= XrootdProtocol.kXR_delete;
}
if (force) {
options |= XrootdProtocol.kXR_force;
}
if (mkpath) {
options |= XrootdProtocol.kXR_mkpath;
}
if (newFile) {
options |= XrootdProtocol.kXR_new;
}
if (nowait) {
options |= XrootdProtocol.kXR_nowait;
}
if (open_apnd) {
options |= XrootdProtocol.kXR_open_apnd;
}
if (open_read) {
options |= XrootdProtocol.kXR_open_read;
}
if (open_updt) {
options |= XrootdProtocol.kXR_open_updt;
}
if (refresh) {
options |= XrootdProtocol.kXR_refresh;
}
if (replica) {
options |= XrootdProtocol.kXR_replica;
}
if (retstat) {
options |= XrootdProtocol.kXR_retstat;
}
if (ulterior) {
options |= XrootdProtocol.kXR_ulterior;
}
// int handle = getSession().open(path, 0, options);
// console.printf("file handle=%d\n", handle);
}
}
static class CloseCommand extends Command {
@Argument(metaVar = "handle", index = 0, required = true, usage = "Handle to close")
private int handle;
void doCommand(PrintWriter console) throws IOException {
// getSession().close(handle);
}
}
static class PingCommand extends Command {
void doCommand(PrintWriter console) throws IOException {
getSession().ping();
}
}
static class ExitCommand extends Command {
void doCommand(PrintWriter console) throws IOException {
System.exit(0);
}
}
static class StatCommand extends Command {
@Argument(metaVar = "path", index = 0, required = true, usage = "Path to file")
private String path;
void doCommand(PrintWriter console) throws IOException {
FileStatus status = getSession().stat(path);
console.printf("%s\n", status);
}
}
static class RemoveCommand extends Command {
@Argument(metaVar = "path", index = 0, required = true, usage = "Path to file")
private String path;
void doCommand(PrintWriter console) throws IOException {
getSession().remove(path);
}
}
static class DirListCommand extends Command {
@Argument(metaVar = "path", index = 0, required = true, usage = "Path to directory")
private String path;
@Override
void doCommand(PrintWriter console) throws IOException {
List<String> list = getSession().dirList(path);
for (String file : list) {
console.printf("%s\n", file);
}
}
}
static class LevelCommand extends Command {
@Argument(metaVar = "level", index = 0, required = true, usage = "Logging level")
private String level;
@Override
void doCommand(PrintWriter console) throws IOException {
setLoggingLevel(level);
}
}
static class LocateCommand extends Command {
@Argument(metaVar = "path", index = 0, required = true, usage = "Path to locate")
private String path;
@Override
void doCommand(PrintWriter console) throws IOException {
String[] result = getSession().locate(path, false, false);
for (String file : result) {
console.printf("%s\n", file);
}
}
}
static class ChecksumCommand extends Command {
@Argument(metaVar = "path", index = 0, required = true, usage = "Path to file")
private String path;
@Override
void doCommand(PrintWriter console) throws IOException {
String checksum = getSession().query(XrootdProtocol.kXR_Qcksum, path);
console.printf("%s\n", checksum);
}
}
static class StatsCommand extends Command {
@Argument(metaVar = "arg", index = 0, required = false, usage = "Optional list of letters, each indicating the statistical components to be returned (default all)")
private String arg = "a";
@Override
void doCommand(PrintWriter console) throws IOException {
String result = getSession().query(XrootdProtocol.kXR_QStats,arg);
console.printf("%s\n", result);
}
@Override
void reset() {
super.reset();
arg = "a";
}
}
static class ConnectCommand extends Command {
@Argument(metaVar = "host", index = 0, required = true, usage = "Host to connect to")
private String host;
@Option(name = "-p", usage = "Port to connect to")
private int port = 1094;
@Override
void reset() {
super.reset();
port = 1094;
}
@Override
void doCommand(PrintWriter console) throws IOException {
Session session = new Session(host, port, System.getProperty("user.name"));
setSession(session);
}
}
static class DisconnectCommand extends Command {
@Override
void doCommand(PrintWriter console) throws IOException {
setSession(null);
}
}
static class ProtocolCommand extends Command {
@Override
void doCommand(PrintWriter console) throws IOException {
String protocol = getSession().protocol();
console.println(protocol);
}
}
static class GetCommand extends Command {
@Argument(metaVar = "source", index = 0, required = true, usage = "Source file")
private String path;
@Argument(metaVar = "target", index = 1, required = false, usage = "Destination file")
private File dest;
@Option(name = "-q", usage = "Quiet mode")
private boolean quiet;
@Override
void reset() {
super.reset();
quiet = false;
dest = null;
}
@Override
void doCommand(PrintWriter console) throws IOException {
File file = new File(path);
File local = dest == null ? new File(file.getName()) : dest.isDirectory() ? new File(dest, file.getName()) : dest;
OpenFile openFile = getSession().open(path, 0, XrootdProtocol.kXR_open_read + XrootdProtocol.kXR_retstat);
FileOutputStream out = new FileOutputStream(local);
FileChannel fileChannel = out.getChannel();
try {
long lTotal = 0;
long tStart = System.currentTimeMillis();
long tNext = tStart;
int bufferSize = 1000000;
// Note, this may be null for older servers
FileStatus status = openFile.getStatus();
if (status == null) {
status = getSession().stat(path);
}
long fileSize = status.getSize();
for (long offset = 0;; offset += bufferSize) {
ReadOperation ro = new ReadOperation(openFile, fileChannel, offset, bufferSize);
int l = getSession().send(ro).getResponse();
if (l <= 0) {
if (!quiet) {
tNext = updateProgress(tNext, tStart, console, lTotal, fileSize, true);
}
break;
}
lTotal += l;
if (!quiet) {
tNext = updateProgress(tNext, tStart, console, lTotal, fileSize, false);
}
}
} finally {
getSession().close(openFile);
fileChannel.close();
if (!quiet) {
console.println();
}
}
}
private long updateProgress(long tNext, long tStart, PrintWriter console, long current, long size, boolean finalUpdate) {
long tNow = System.currentTimeMillis();
if (finalUpdate || tNow > tNext) {
int progress = (int) (40 * current / size);
tNext = tNow + 100; // Update at 10Hz max
long elapsed = tNow - tStart;
console.print('[');
for (int i = 0; i < progress; i++) {
console.print('*');
}
for (int i = progress; i < 40; i++) {
console.print(' ');
}
console.print("] ");
console.print(format.format(current));
console.print('/');
console.print(format.format(size));
console.print(" ");
if (finalUpdate || elapsed > 500) {
console.print(format.format(1000 * current / elapsed));
console.print("/sec");
}
console.print(" \r");
console.flush();
}
return tNext;
}
}
static class PutCommand extends Command {
@Argument(metaVar = "file", index = 0, required = true, usage = "Local file path")
private File local;
@Argument(metaVar = "path", index = 1, required = true, usage = "Scalla file path")
private String path;
@Option(name = "-d", usage = "Open a new file, deleting any existing file")
private boolean delete;
@Option(name = "-f", usage = "Ignore file usage rules")
private boolean force;
@Option(name = "-m", usage = "Create directory path if it does not already exist")
private boolean mkpath;
@Option(name = "-n", usage = "Open a new file only if it does not already exist")
private boolean newFile;
@Override
void reset() {
super.reset();
delete = force = mkpath = newFile = false;
}
@Override
void doCommand(PrintWriter console) throws IOException {
int options = 0;
if (delete) {
options |= XrootdProtocol.kXR_delete;
}
if (force) {
options |= XrootdProtocol.kXR_force;
}
if (mkpath) {
options |= XrootdProtocol.kXR_mkpath;
}
if (newFile) {
options |= XrootdProtocol.kXR_new;
}
OpenFile file = getSession().open(path, 0, options);
InputStream in = new FileInputStream(local);
try {
byte[] buffer = new byte[65536];
int lTotal = 0;
for (;;) {
int l = in.read(buffer);
if (l <= 0) {
break;
}
getSession().write(file, lTotal, buffer, 0, l);
lTotal += l;
}
} finally {
getSession().close(file);
in.close();
}
}
}
private class ReadAheadConsole implements Runnable {
private ConsoleReader console;
private Thread readAheadThread = new Thread(this,"ReadAheadConsole");
private BlockingQueue<String> readAhead = new LinkedBlockingQueue<String>();
private final String END_OF_DATA = new String("EOD");
ReadAheadConsole() throws IOException {
console = new ConsoleReader(System.in, new PrintWriter(System.out), ReadAheadConsole.class.getResourceAsStream("keybindings.properties"));
File historyDir = new File(new File(System.getProperty("user.home")), ".scalla");
historyDir.mkdir();
File historyFile = new File(historyDir, "command.history");
History history = historyDir.canWrite() ? new History(historyFile) : new History();
console.setHistory(history);
console.addCompletor(new CommandCompletor());
console.addTriggeredAction(ConsoleOperations.CTRL_C, new ControlCListener());
readAheadThread.setDaemon(true);
readAheadThread.start();
}
private String readLine(String prompt) throws IOException {
try
{
if (readAhead.isEmpty())
{
console.setDefaultPrompt(prompt);
console.redrawLine();
console.flushConsole();
}
String result = readAhead.take();
// Don't use .equals
if (result == END_OF_DATA) {
console.printNewline();
console.flushConsole();
return null;
}
return result;
}
catch (InterruptedException x)
{
throw new InterruptedIOException("IO Error reading console");
}
}
public void run() {
for (;;)
{
try
{
console.setDefaultPrompt("");
String result = console.readLine();
readAhead.offer(result == null ? END_OF_DATA : result);
}
catch (IOException x) {
}
}
}
}
}