package diskCacheV111.admin;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.collect.Range;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import jline.console.completer.Completer;
import jline.console.completer.StringsCompleter;
import org.fusesource.jansi.Ansi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.CharArrayWriter;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import diskCacheV111.util.CacheException;
import diskCacheV111.util.FsPath;
import diskCacheV111.util.PnfsId;
import diskCacheV111.util.TimeoutCacheException;
import diskCacheV111.vehicles.PoolManagerGetPoolsByPoolGroupMessage;
import diskCacheV111.vehicles.PoolManagerPoolInformation;
import dmg.cells.network.PingMessage;
import dmg.cells.nucleus.CellAddressCore;
import dmg.cells.nucleus.CellEndpoint;
import dmg.cells.nucleus.CellMessage;
import dmg.cells.nucleus.CellMessageAnswerable;
import dmg.cells.nucleus.CellPath;
import dmg.cells.nucleus.NoRouteToCellException;
import dmg.cells.services.GetAllDomainsReply;
import dmg.cells.services.GetAllDomainsRequest;
import dmg.util.AclException;
import dmg.util.AuthorizedString;
import dmg.util.CommandAclException;
import dmg.util.CommandException;
import dmg.util.CommandExitException;
import dmg.util.CommandInterpreter;
import dmg.util.CommandThrowableException;
import dmg.util.command.Argument;
import dmg.util.command.Command;
import dmg.util.command.CommandLine;
import dmg.util.command.HelpFormat;
import org.dcache.auth.Subjects;
import org.dcache.auth.attributes.Restrictions;
import org.dcache.cells.CellStub;
import org.dcache.namespace.FileAttribute;
import org.dcache.namespace.FileType;
import org.dcache.util.Args;
import org.dcache.util.Glob;
import org.dcache.util.Version;
import org.dcache.util.list.DirectoryEntry;
import org.dcache.util.list.DirectoryStream;
import org.dcache.util.list.ListDirectoryHandler;
import org.dcache.vehicles.FileAttributes;
import org.dcache.vehicles.PnfsGetFileAttributes;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Maps.immutableEntry;
import static com.google.common.util.concurrent.Futures.*;
import static java.lang.String.CASE_INSENSITIVE_ORDER;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.partitioningBy;
import static java.util.stream.Collectors.toList;
import static org.dcache.util.Glob.parseGlobToPattern;
import static org.fusesource.jansi.Ansi.Color.GREEN;
import static org.fusesource.jansi.Ansi.Color.RED;
public class UserAdminShell
extends CommandInterpreter
implements Completer
{
private static final Logger _log =
LoggerFactory.getLogger(UserAdminShell.class);
/**
* Timeout is milliseconds for the {@literal xyzzy} message sent to cells when connecting
* to them.
*/
private static final int CONNECT_PROBE_MESSAGE_TIMEOUT_MS = 1000;
/**
* jline completer for shell backslash commands.
*/
private static final StringsCompleter SHELL_COMMAND_COMPLETER =
new StringsCompleter("\\c", "\\exception", "\\l", "\\s", "\\sl", "\\sn",
"\\sp", "\\timeout", "\\q", "\\h", "\\?");
/**
* jline completer for pool manager cell commands.
*/
private final Completer POOL_MANAGER_COMPLETER = createRemoteCompleter("PoolManager");
/**
* jline completer for pnfs manager cell commands.
*/
private final Completer PNFS_MANAGER_COMPLETER = createRemoteCompleter("PnfsManager");
/**
* Communication endpoint of the admin cell.
*/
private CellEndpoint _cellEndpoint;
/**
* Communication stub for the ACL cell that controls admin command permissions.
*/
private CellStub _acmStub;
/**
* Communication stub for pool manager.
*/
private CellStub _poolManager;
/**
* Communication stub for pnfs manager.
*/
private CellStub _pnfsManager;
/**
* Generic communication stub not bound to a particular cell.
*/
private CellStub _cellStub;
/**
* Client handler for listing directories in the dCache name space.
*/
private ListDirectoryHandler _list;
/**
* Current effective user identity. May be different from _authUser as
* a user may request a different identity when connecting to a cell.
*/
private String _user;
/**
* User identity as reported by the transport (typically SSH).
*/
private String _authUser;
/**
* Timeout of cell commands. Carbon units may interrupt the current command
* by pressing Ctrl-C, but currently in cells we don't have any means of
* actually cancel the callback and hence a timeout is needed (Ctrl-C merely
* causes the shell to stop waiting).
*/
private long _timeout = TimeUnit.MINUTES.toMillis(5);
/**
* Whether to provide a full stack trace when cell commands result in an
* exception. This is a debugging feature and can be enabled using the
* {@literal \exception} command.
*/
private boolean _fullException;
/**
* Identifier of this admin door. This will be shown in the command prompt.
*/
private final String _instance;
/**
* The Position of the cell the shell is currently connected to.
*/
private Position _currentPosition = null;
/**
* jline completer for the cell the shell is currently connected to.
*/
private Completer _completer;
public UserAdminShell(String prompt)
{
_instance = prompt;
}
public void setUser(String user)
{
_user = _authUser = user;
}
protected String getUser()
{
return _user;
}
public void setCellEndpoint(CellEndpoint endpoint)
{
_cellEndpoint = endpoint;
_cellStub = new CellStub(_cellEndpoint);
}
public void setAcm(CellStub stub)
{
_acmStub = stub;
}
public void setPoolManager(CellStub stub)
{
_poolManager = stub;
}
public void setPnfsManager(CellStub stub)
{
_pnfsManager = stub;
}
public void setListHandler(ListDirectoryHandler list)
{
_list = list;
}
@Override
protected Serializable doExecute(CommandEntry entry, Args args, String[] acls)
throws CommandException
{
try {
checkPermission(acls);
return super.doExecute(entry, args, acls);
} catch (AclException e) {
throw new CommandAclException(e.getPrincipal(), e.getAcl());
}
}
/**
* Checks that the current effective user has any of the given ACLs.
* @throws AclException if the current user does not have any of the {@code acls}
*/
protected void checkPermission(String[] acls) throws AclException
{
if (acls.length > 0) {
AclException e = null;
for (String acl : acls) {
try {
checkPermission(acl);
return;
} catch (AclException ce) {
e = ce;
}
}
throw e;
}
}
/**
* Checks that the current effective user has the given acl.
* @throws AclException if the current user does not have the given {@code aclName}
*/
public void checkPermission(String aclName)
throws AclException
{
Object[] request = new Object[5];
request[0] = "request";
request[1] = "<nobody>";
request[2] = "check-permission";
request[3] = getUser();
request[4] = aclName;
Object[] r;
try {
r = _acmStub.sendAndWait(request, Object[].class);
} catch (TimeoutCacheException | NoRouteToCellException e) {
throw new AclException(e.getMessage());
} catch (CacheException | InterruptedException e) {
throw new AclException("Problem: " + e.getMessage());
}
if (r.length < 6 || !(r[5] instanceof Boolean)) {
throw new AclException("Protocol violation 4456");
}
if (!((Boolean) r[5])) {
throw new AclException(getUser(), aclName);
}
}
/**
* Asynchronously fetch the list of cells of {@code domain} matching {@code cellPredicate}.
* <p>
* The resulting list is sorted and fully qualified. Errors are logged and otherwise ignored.
*/
private ListenableFuture<List<String>> getCells(String domain, Predicate<String> cellPredicate)
{
/* Query System cell and split, filter, sort and expand the answer. */
ListenableFuture<List<String>> future = transform(
_cellStub.send(new CellPath("System", domain), "ps", String.class),
(String s) ->
Arrays.stream(s.split("\n"))
.filter(cellPredicate)
.sorted(CASE_INSENSITIVE_ORDER)
.map(cell -> cell + "@" + domain)
.collect(toList()));
/* Log and ignore any errors. */
return catchingAsync(future, Throwable.class,
t -> {
_log.debug("Failed to query the System cell of domain {}: {}", domain, t);
return immediateFuture(emptyList());
});
}
/**
* Asynchronously fetch the list of pools matching the given predicate.
*/
private ListenableFuture<List<String>> getPools(Predicate<String> predicate)
{
return transform(
_poolManager.send("psu ls pool", String.class),
(String s) -> Stream.of(s.split("\n"))
.filter(predicate)
.collect(toList()));
}
/**
* Asynchronously fetch the list of pools of a pool group.
*/
private ListenableFuture<Stream<String>> getPools(String poolGroup)
{
return CellStub.transform(
_poolManager.send(new PoolManagerGetPoolsByPoolGroupMessage(singletonList(poolGroup))),
(PoolManagerGetPoolsByPoolGroupMessage m) ->
m.getPools().stream().map(PoolManagerPoolInformation::getName));
}
/**
* Asynchronously fetch the list of pool groups.
*/
private ListenableFuture<List<String>> getPoolGroups()
{
return transform(
_poolManager.send("psu ls pgroup", String.class),
(String s) -> asList(s.split("\n")));
}
/**
* Asynchronously fetch the list of pools in pool groups matching the given predicate.
*/
private ListenableFuture<List<String>> getPoolsInGroups(Predicate<String> predicate)
{
ListenableFuture<List<String>> poolGroups = getPoolGroups();
/* Query the pools of each pool group so we have a list of list of pools. */
ListenableFuture<List<Stream<String>>> pools = transformAsync(
poolGroups,
(List<String> groups) ->
allAsList(groups.stream().filter(predicate).map(this::getPools).collect(toList())));
/* Flatten these to form a list of pools. */
return transform(pools,
(List<Stream<String>> l) -> l.stream().flatMap(s -> s).distinct().collect(toList()));
}
/**
* Expands a list of cell address globs into a list of cell addresses.
*
* Processes globs on both the left and right side of the '@' separator of a cell address. Also
* processes the special '/' pool group separator, interpreting the left side as a pool name
* pattern and the right side as a pool group pattern.
*
* The result is sorted lexicographically and case insensitive, but the order of the input patterns
* is preserved (ie. the output contains matching cells in the same order).
*/
private List<String> expandCellPatterns(List<String> patterns)
throws CacheException, InterruptedException, ExecutionException, NoRouteToCellException
{
/* Query domains and well-known cells on demand. */
Supplier<Future<Map<String, Collection<String>>>> domains =
Suppliers.memoize(() -> transform(_cellStub.send(new CellPath("RoutingMgr"),
new GetAllDomainsRequest(), GetAllDomainsReply.class),
GetAllDomainsReply::getDomains));
List<ListenableFuture<List<String>>> futures = new ArrayList<>();
for (String pattern : patterns) {
int i = pattern.indexOf('@');
if (i >= 0) {
/* Find the cells of each matching domain.
*/
Predicate<String> matchesCellName = toGlobPredicate(pattern.substring(0, i));
Predicate<String> matchesDomainName = toGlobPredicate(pattern.substring(i + 1));
CellStub.get(domains.get()).keySet().stream()
.filter(matchesDomainName)
.sorted(CASE_INSENSITIVE_ORDER)
.map(domain -> getCells(domain, matchesCellName))
.forEach(futures::add);
continue;
}
i = pattern.indexOf('/');
if (i >= 0) {
Predicate<String> matchesPool = toGlobPredicate(pattern.substring(0, i));
if (i + 1 == pattern.length()) {
/* Special case when no pool group is specified - matches over all pools, even those
* not in a pool group.
*/
futures.add(transform(getPools(matchesPool),
(List<String> pools) ->
pools.stream()
.sorted(CASE_INSENSITIVE_ORDER)
.collect(toList())));
} else {
/* Find the pools of each matching pool group.
*/
Predicate<String> matchesPoolGroup = toGlobPredicate(pattern.substring(i + 1));
futures.add(
transform(getPoolsInGroups(matchesPoolGroup),
(List<String> pools) ->
pools.stream()
.filter(matchesPool)
.sorted(CASE_INSENSITIVE_ORDER)
.collect(toList())));
}
continue;
}
Predicate<String> matchesCellName = toGlobPredicate(pattern);
/* Add matching well-known cells. */
CellStub.get(domains.get()).values().stream()
.flatMap(Collection::stream)
.filter(matchesCellName)
.sorted(CASE_INSENSITIVE_ORDER)
.map(Collections::singletonList)
.map(Futures::immediateFuture)
.forEach(futures::add);
}
/* Collect and flatten the result. */
return allAsList(futures).get().stream().flatMap(Collection::stream).collect(toList());
}
/**
* Returns true iff the given string is a cell address pattern that should be expanded using
* expandCellPatterns.
*/
private static boolean isExpandable(String s)
{
return !s.contains(":") && (s.startsWith("@") || s.endsWith("@") || Glob.isGlob(s) || s.indexOf('/') > -1);
}
/**
* Returns a Predicate that evaluates to true when the input matches the given glob pattern.
*
* As a special case, an empty glob matches all strings.
*/
private static Predicate<String> toGlobPredicate(String glob)
{
return glob.isEmpty() ? (String) -> true : parseGlobToPattern(glob).asPredicate();
}
/**
* Returns the welcome string printed to the user's console when connecting.
*/
public String getHello()
{
return "dCache (" + Version.of(UserAdminShell.class).getVersion() + ")\n" + "Type \"\\?\" for help.\n";
}
/**
* Returns the command prompt that should be displayed on the user's console.
*/
public String getPrompt()
{
return (_instance == null ? "" : ("[" + _instance + "] ")) +
(_currentPosition == null ? "(local) " : ("(" + _currentPosition.remoteName + ") ")) +
getUser() + " > ";
}
/**
* Returns the preferred help format.
*
* For carbon units, this will be typically ANSI, while silicon units will get PLAIN.
*/
private HelpFormat getPreferredHelpFormat()
{
return Ansi.isEnabled() ? HelpFormat.ANSI : HelpFormat.PLAIN;
}
@Command(name = "\\exception", hint = "controls display of stack traces",
description = "When enabled, full Java stack traces are displayed on errors.")
class SetExceptionCommand implements Callable<String>
{
@Argument(required = false)
Boolean trace;
@Override
public String call() throws Exception
{
if (trace != null) {
_fullException = trace;
}
return "Stack traces on errors are " + (_fullException ? "enabled" : "disabled") + ".";
}
}
@Command(name = "\\timeout", hint = "sets the command timeout",
description = "Sets the timeout after which command execution is cancelled. " +
"Commands can always be cancelled interactively by pressing Ctrl-C.")
class TimeoutCommand implements Callable<String>
{
@Argument(required = false)
Integer seconds;
@Override
public String call() throws Exception
{
if (seconds != null) {
checkArgument(seconds >= 1, "Timeout must be positive.");
_timeout = TimeUnit.SECONDS.toMillis(seconds);
}
return "Timeout is " + (_timeout / 1000) + " seconds.";
}
}
@Command(name = "\\l", hint = "list cells",
description = "Lists all matching cells. The argument is interpreted as a glob. If no " +
"domain suffix is provided, only well known cells are listed. Otherwise " +
"all matching cells in all matching domains are listed.")
class ListCommand implements Callable<String>
{
@Argument(required = false, valueSpec = "CELL[@DOMAIN]|POOL/POOLGROUP",
usage = "A glob pattern. An empty CELL, DOMAIN, POOL or POOLGROUP string matches any name.")
String[] pattern = {"*"};
@Override
public String call() throws Exception
{
return String.join("\n", expandCellPatterns(asList(pattern)));
}
}
@Command(name = "\\c", hint = "connect to cell",
description = "Connect to new cell. May optionally switch to another user.")
class ConnectCommand implements Callable<String>
{
@Argument(index = 0, valueSpec = "CELL[@DOMAIN]",
usage = "Well known or fully qualified cell name.")
String name;
@Argument(required = false, index = 1,
usage = "Account to connect with.")
String user;
@Override
public String call() throws Exception
{
String oldUser = _user;
try {
if (user != null) {
if (!user.equals(_authUser) && !user.equals(_user)) {
try {
checkPermission("system.*.newuser");
} catch (AclException acle) {
checkPermission("system." + user + ".newuser");
}
}
_user = user;
}
checkCdPermission(name);
_currentPosition = resolve(name);
_completer = null;
} catch (Throwable e) {
_user = oldUser;
throw e;
}
return "";
}
private Position resolve(String cell) throws InterruptedException
{
CellPath path = new CellPath(cell);
try {
SettableFuture<CellAddressCore> future = SettableFuture.create();
_cellEndpoint.sendMessage(new CellMessage(path, new PingMessage()),
new CellMessageAnswerable()
{
@Override
public void answerArrived(CellMessage request, CellMessage answer)
{
future.set(answer.getSourceAddress());
}
@Override
public void exceptionArrived(CellMessage request, Exception exception)
{
future.setException(exception);
}
@Override
public void answerTimedOut(CellMessage request)
{
future.setException(new NoRouteToCellException(request, "No reply"));
}
}, MoreExecutors.directExecutor(), CONNECT_PROBE_MESSAGE_TIMEOUT_MS);
CellAddressCore remote = future.get();
if (path.hops() == 1 && path.getDestinationAddress().isLocalAddress()) {
return new Position(remote.toString(), new CellPath(remote));
} else {
return new Position(cell, path);
}
} catch (ExecutionException e) {
if (e.getCause() instanceof NoRouteToCellException) {
throw new IllegalArgumentException("Cell does not exist.");
}
// Some other failure, but apparently the cell exists
_log.info("Cell probe failed: {}", e.getCause().toString());
return new Position(cell, path);
}
}
}
@Command(name = "\\q", hint = "quit")
class QuitCommand implements Callable<Serializable>
{
@Override
public Serializable call() throws Exception
{
throw new CommandExitException("Done", 0);
}
}
@Command(name = "\\?", hint = "display help for shell commands",
description = "Shows help for shell commands. Commands that begin with a backslash are always " +
"accessible, while other commands are only available when not connected to a cell." +
"\n\n" +
"When invoked with a specific command, detailed help for that " +
"command is displayed. When invoked with a partial command or without " +
"an argument, a summary of all matching commands is shown.")
class ShellHelpCommand implements Callable<String>
{
@Argument(valueSpec = "COMMAND", required = false,
usage = "Partial or full command for which to show help.")
String[] command = {};
@Override
public String call()
{
return getHelp(getPreferredHelpFormat(), command);
}
}
@Command(name = "\\h", hint = "display help for cell commands",
description = "Shows help for cell commands." +
"\n\n" +
"When invoked with a specific command, detailed help for that " +
"command is displayed. When invoked with a partial command or without " +
"an argument, a summary of all matching commands is shown.")
class HelpCommand implements Callable<Serializable>
{
@Argument(valueSpec = "COMMAND", required = false,
usage = "Partial or full command for which to show help.")
String[] command = {};
@Override
public Serializable call() throws InterruptedException, CommandException, NoRouteToCellException
{
if (_currentPosition == null) {
return "You are not connected to any cell. Use \\? to display shell commands.";
} else {
String cmd = "help -format=" + getPreferredHelpFormat() + " " + String.join(" ", command);
Serializable reply = sendObject(_currentPosition.remote, new AuthorizedString(_user, cmd));
return filterHelp(Objects.toString(reply, ""));
}
}
private String filterHelp(String help)
{
return Joiner.on('\n').join(filter(Splitter.on('\n').split(help), input -> !input.startsWith("help ")));
}
}
@Command(name = "\\sn", hint = "send pnfsmanager command",
acl = {"cell.*.execute", "cell.PnfsManager.execute"},
description = "Sends COMMAND to the pnfsmanager service. Use \\sn help for a list of supported commands.")
class NameSpaceCommand implements Callable<Serializable>
{
@Argument(usage = "A pnfsmanager command.")
String[] command;
@CommandLine(allowAnyOption = true, valueSpec = "[OPTIONS]")
Args args;
@Override
public Serializable call() throws InterruptedException, CommandException, NoRouteToCellException
{
return sendObject(_pnfsManager.getDestinationPath(), args.toString());
}
}
@Command(name = "\\sp", hint = "send poolmanager command",
acl = {"cell.*.execute", "cell.PoolManager.execute"},
description = "Sends COMMAND to the poolmanager service. Use \\sp help for a list of supported commands.")
class PoolManagerCommand implements Callable<Serializable>
{
@Argument(usage = "A poolmanager command.")
String[] command;
@CommandLine(allowAnyOption = true, valueSpec = "[OPTIONS]")
Args args;
@Override
public Serializable call() throws InterruptedException, NoRouteToCellException, CommandException
{
return sendObject(_poolManager.getDestinationPath(), args.toString());
}
}
@Command(name = "\\s", hint = "send command",
description = "Sends COMMAND to one or more cells.")
class SendCommand implements Callable<Serializable>
{
@Argument(index = 0, valueSpec = "(CELL[@DOMAIN]|POOL/POOLGROUP)[,(CELL[@DOMAIN]|POOL/POOLGROUP)]...",
usage = "List of cell addresses. Wildcards are expanded. An empty CELL, DOMAIN, " +
"POOL or POOLGROUP string matches any name.")
String destination;
@Argument(index = 1, usage = "A cell command.")
String[] command;
@CommandLine(allowAnyOption = true, valueSpec = "[OPTIONS]")
Args args;
@Override
public Serializable call()
throws InterruptedException, ExecutionException, CacheException, AclException,
CommandException, NoRouteToCellException
{
args.shift();
AuthorizedString command = new AuthorizedString(_user, args.toString());
/* Special case non-wildcard single cell destinations to avoid the indentation and
* addition of a cell name header. Makes the command nicer to use in scripts.
*/
if (!destination.contains(",") && !isExpandable(destination)) {
return sendObject(destination, command);
}
/* Expand wildcards.
*/
Map<Boolean, List<String>> expandable =
StreamSupport.stream(Glob.expandList(destination).spliterator(), false)
.collect(partitioningBy(UserAdminShell::isExpandable));
Iterable<String> destinations = concat(expandable.get(false), expandCellPatterns(expandable.get(true)));
return sendToMany(destinations, command);
}
}
@Command(name = "\\sl", hint = "send to locations",
description = "Sends COMMAND to all pools hosting a copy of the given file. If the " +
"string $1 occurs in the command, the string is replaced by the PNFS ID " +
"of the given file.")
class SendLocationsCommand implements Callable<String>
{
@Argument(index = 0, valueSpec = "PNFSID|PATH",
usage = "The command is submitted to all pools hosting a copy of this file.")
String file;
@Argument(index = 1, usage = "A pool command. $1 is substituted for the PNFS ID.")
String[] command;
@CommandLine(allowAnyOption = true, valueSpec = "[OPTIONS]")
Args args;
@Override
public String call() throws InterruptedException, CacheException, NoRouteToCellException, AclException
{
FileAttributes attributes = getFileAttributes(file);
args.shift();
AuthorizedString command =
new AuthorizedString(_user, args.toString().replace("$1", attributes.getPnfsId().toString()));
return sendToMany(attributes.getLocations(), command);
}
}
private void checkCdPermission(String remoteName) throws AclException
{
int pos = remoteName.indexOf('-');
String prefix = null;
if (pos > 0) {
prefix = remoteName.substring(0, pos);
}
try {
checkPermission("cell.*.execute");
} catch (AclException acle) {
try {
checkPermission("cell." + remoteName + ".execute");
} catch (AclException acle2) {
if (prefix == null) {
throw acle2;
}
try {
checkPermission("cell." + prefix + "-pools.execute");
} catch (AclException acle3) {
throw new AclException(getUser(), remoteName);
}
}
}
}
@Override
public int complete(String buffer, int cursor, List<CharSequence> candidates)
{
if (buffer.startsWith("\\") || _currentPosition == null) {
return completeShell(buffer, cursor, candidates);
}
return completeRemote(buffer, cursor, candidates);
}
/**
* Completion function using the currently connected remote cell as a source
* for completion candidates.
*/
private int completeRemote(String buffer, int cursor, List<CharSequence> candidates)
{
try {
if (_completer == null) {
Object help = executeCommand("help");
if (help == null) {
return -1;
}
_completer = new HelpCompleter(String.valueOf(help));
}
return _completer.complete(buffer, cursor, candidates);
} catch (CommandException | NoRouteToCellException e) {
_log.info("Completion failed: {}", e.toString());
return -1;
} catch (InterruptedException e) {
return -1;
}
}
/**
* Completes the \c command with well-known cells and local cells of the connected domain
* as a source for completion candidates.
*/
private int completeConnectCommand(String buffer, int cursor, List<CharSequence> candidates)
{
try {
if (CharMatcher.whitespace().or(CharMatcher.is('/')).matchesAnyOf(buffer)) {
return -1;
}
candidates.addAll(expandCellPatterns(singletonList(buffer + "*")));
if (!buffer.contains("@") && _currentPosition != null) {
/* Add local cells in the connected domain too. */
candidates.addAll(
getCells(_currentPosition.remote.getDestinationAddress().getCellDomainName(),
toGlobPredicate(buffer + "*")).get());
}
return 0;
} catch (CacheException | NoRouteToCellException | ExecutionException e) {
_log.info("Completion failed: {}", e.toString());
return -1;
} catch (InterruptedException e) {
return -1;
}
}
/**
* Completes a cell address wildcard. Does not provide completion for cell paths.
*/
private int completeCellWildcard(String buffer, int cursor, List<CharSequence> candidates)
{
if (buffer.contains(":")) {
return -1;
}
try {
int i = buffer.indexOf('@');
if (i > -1) {
expandCellPatterns(singletonList(buffer + "*")).stream()
.map(s -> s.substring(s.indexOf('@') + 1))
.forEach(candidates::add);
return i + 1;
}
i = buffer.indexOf('/');
if (i > -1) {
Predicate<String> predicate = toGlobPredicate(buffer.substring(i + 1) + "*");
getPoolGroups().get().stream().filter(predicate).forEach(candidates::add);
return i + 1;
}
candidates.addAll(expandCellPatterns(singletonList(buffer + "*")));
if (_currentPosition != null) {
candidates.addAll(
getCells(_currentPosition.remote.getDestinationAddress().getCellDomainName(),
toGlobPredicate(buffer + "*")).get());
}
return 0;
} catch (CacheException | NoRouteToCellException | ExecutionException e) {
_log.info("Completion failed: {}", e.toString());
return -1;
} catch (InterruptedException e) {
return -1;
}
}
/**
* Completes the \l command.
*/
private int completeListCommand(String buffer, int cursor, List<CharSequence> candidates)
{
int lastDestinationStart = buffer.lastIndexOf(' ') + 1;
String lastDestination = buffer.substring(lastDestinationStart);
int i = completeCellWildcard(lastDestination, lastDestination.length(), candidates);
return (i == -1) ? -1 : lastDestinationStart + i;
}
/**
* Completes the \s command. Is able to complete the last address of the destination
* argument. If only a single cell is provided as a destination, the command argument
* itself is completed too (using that cell as a source for completion candidates).
*/
private int completeSendCommand(String buffer, int cursor, List<CharSequence> candidates)
{
Completable arguments = new Completable(buffer, cursor, candidates);
if (!arguments.hasTail()) {
int lastDestinationStart = arguments.head.lastIndexOf(',') + 1;
String lastDestination = arguments.head.substring(lastDestinationStart);
int i = completeCellWildcard(lastDestination, lastDestination.length(), candidates);
return (i == -1) ? -1 : lastDestinationStart + i;
} else if (!arguments.head.contains(",") && !isExpandable(arguments.head)) {
return arguments.completeTail(createRemoteCompleter(arguments.head));
}
return -1;
}
/**
* Completes a name space path. This will query pnfs manager to obtain a directory
* listing with possible candidates.
*/
private int completePath(String buffer, int cursor, List<CharSequence> candidates)
{
if (buffer.isEmpty()) {
candidates.add("/");
return 0;
} else if (buffer.startsWith("/")) {
int endIndex = buffer.lastIndexOf('/');
String dir = buffer.length() == 1 ? "/" : buffer.substring(0, endIndex);
String file = buffer.substring(endIndex + 1);
try (DirectoryStream stream = list(dir, file + "*")) {
for (DirectoryEntry entry : stream) {
if (entry.getFileAttributes().getFileType() == FileType.DIR) {
candidates.add(entry.getName() + "/");
} else {
candidates.add(entry.getName());
}
}
} catch (InterruptedException e) {
return -1;
} catch (CacheException e) {
_log.info("Completion failed: {}", e.toString());
return -1;
}
return endIndex + 1;
}
return -1;
}
/**
* Completes the {@literal \sl} command.
*/
private int completeSendLocationsCommand(String buffer, int cursor, List<CharSequence> candidates)
{
Completable arguments = new Completable(buffer, cursor, candidates);
if (!arguments.hasTail()) {
return arguments.complete(this::completePath);
} else {
try {
Collection<String> locations = getFileAttributes(arguments.head).getLocations();
if (!locations.isEmpty()) {
/* Assume all pools have the same commands. */
return arguments.completeTail(createRemoteCompleter(Iterables.get(locations, 0)));
}
} catch (CacheException | NoRouteToCellException e) {
_log.info("Completion failed: {}", e.toString());
return -1;
} catch (InterruptedException e) {
return -1;
}
}
return -1;
}
/**
* Utility method to initiate a directory listing returning entries matching the given
* glob string.
*/
private DirectoryStream list(String dir, String pattern) throws InterruptedException, CacheException
{
return _list.list(Subjects.ROOT, Restrictions.none(), FsPath.create(dir),
new Glob(pattern), Range.all(), EnumSet.of(FileAttribute.TYPE));
}
/**
* Queries the pnfs id and file locations of a file. Used by the {@literal \sl} command.
*/
private FileAttributes getFileAttributes(String file) throws CacheException, InterruptedException, NoRouteToCellException
{
/* Lookup file in name space */
PnfsGetFileAttributes request;
EnumSet<FileAttribute> attributeSet = EnumSet.of(FileAttribute.LOCATIONS, FileAttribute.PNFSID);
if (PnfsId.isValid(file)) {
request = new PnfsGetFileAttributes(new PnfsId(file), attributeSet);
} else {
request = new PnfsGetFileAttributes(file, attributeSet);
}
return _pnfsManager.sendAndWait(request).getFileAttributes();
}
/**
* Completes local shell commands.
*/
private int completeShell(String buffer, int cursor, List<CharSequence> candidates)
{
Completable command = new Completable(buffer, cursor, candidates);
if (!command.hasTail()) {
return command.complete(SHELL_COMMAND_COMPLETER);
}
switch (command.head) {
case "\\?":
return command.completeTail(SHELL_COMMAND_COMPLETER);
case "\\h":
if (_currentPosition != null) {
return command.completeTail(this::completeRemote);
}
break;
case "\\c":
return command.completeTail(this::completeConnectCommand);
case "\\l":
return command.completeTail(this::completeListCommand);
case "\\s":
return command.completeTail(this::completeSendCommand);
case "\\sl":
return command.completeTail(this::completeSendLocationsCommand);
case "\\sp":
return command.completeTail(POOL_MANAGER_COMPLETER);
case "\\sn":
return command.completeTail(PNFS_MANAGER_COMPLETER);
}
return -1;
}
/**
* Factory method to constructor a jline completer for commands of the given cell.
*/
private Completer createRemoteCompleter(String cell)
{
return (buffer, cursor, candidates) -> completeRemote(cell, buffer, cursor, candidates);
}
/**
* Completes remote commands using a particular cell as a source for completion candidates.
*/
private int completeRemote(String cell, String buffer, int cursor, List<CharSequence> candidates)
{
try {
Serializable help = sendObject(cell, "help");
if (help == null) {
return -1;
}
HelpCompleter completer = new HelpCompleter(String.valueOf(help));
return completer.complete(buffer, cursor, candidates);
} catch (NoRouteToCellException | CommandException e) {
_log.info("Completion failed: {}", e.toString());
return -1;
} catch (InterruptedException e) {
return -1;
}
}
public Object executeCommand(String str) throws CommandException, InterruptedException, NoRouteToCellException
{
_log.info("String command (super) " + str);
if (str.trim().isEmpty()) {
return "";
}
Args args = new Args(str);
if (_currentPosition == null || str.startsWith("\\")) {
return localCommand(args);
} else {
return sendObject(_currentPosition.remote, new AuthorizedString(_user, str));
}
}
private Serializable localCommand(Args args) throws CommandException
{
_log.info("Local command {}", args);
Object or = command(args);
if (or == null) {
return "";
}
String r = or.toString();
if (r.length() < 1) {
return "";
}
if (r.substring(r.length() - 1).equals("\n")) {
return r;
} else {
return r + "\n";
}
}
private Serializable sendObject(String cellPath, Serializable object)
throws NoRouteToCellException, InterruptedException, CommandException
{
return sendObject(new CellPath(cellPath), object);
}
private Serializable sendObject(CellPath cellPath, Serializable object)
throws NoRouteToCellException, InterruptedException, CommandException
{
try {
return _cellStub.send(cellPath, object, Serializable.class, _timeout).get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (_fullException) {
return getStackTrace(cause);
}
Throwables.throwIfInstanceOf(cause, Error.class);
Throwables.throwIfInstanceOf(cause, NoRouteToCellException.class);
Throwables.throwIfInstanceOf(cause, CommandException.class);
throw new CommandThrowableException(cause.toString(), cause);
}
}
/**
* Concurrently sends a command to several cells and collects the result from each.
*/
private String sendToMany(Iterable<String> destinations, Serializable object) throws AclException
{
/* Check permissions */
try {
checkPermission("cell.*.execute");
} catch (AclException e) {
for (String cell : destinations) {
checkPermission("cell." + cell + ".execute");
}
}
/* Submit */
List<Map.Entry<String, ListenableFuture<Serializable>>> futures = new ArrayList<>();
for (String cell : destinations) {
futures.add(immutableEntry(cell, _cellStub.send(new CellPath(cell), object, Serializable.class, _timeout)));
}
/* Collect results */
StringBuilder result = new StringBuilder();
for (Map.Entry<String, ListenableFuture<Serializable>> entry : futures) {
result.append(Ansi.ansi().bold().a(entry.getKey()).boldOff()).append(":");
try {
String reply = Objects.toString(entry.getValue().get(), "");
if (reply.isEmpty()) {
result.append(Ansi.ansi().fg(GREEN).a(" OK").reset()).append("\n");
} else {
result.append("\n");
for (String s : reply.split("\n")) {
result.append(" ").append(s).append("\n");
}
}
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof NoRouteToCellException) {
result.append(Ansi.ansi().fg(RED).a(" Cell is unreachable.").reset()).append("\n");
} else {
result.append(" ").append(Ansi.ansi().fg(RED).a(cause.getMessage()).reset()).append("\n");
}
} catch (InterruptedException e) {
result.append(" ^C\n");
/* Cancel all uncompleted tasks. Doesn't actually cancel any requests, but will cause
* the remaining uncompleted futures to throw a CancellationException.
*/
for (Map.Entry<String, ListenableFuture<Serializable>> entry2 : futures) {
entry2.getValue().cancel(true);
}
} catch (CancellationException e) {
result.append(" ^C\n");
}
}
return result.toString();
}
private String getStackTrace(Throwable obj)
{
CharArrayWriter ca = new CharArrayWriter();
obj.printStackTrace(new PrintWriter(ca));
return ca.toString();
}
/**
* Utility class for completing an input buffer.
*
* An instance wraps the editing buffer (a string and a cursor position) and splits the input
* into a head (anything up to the first whitespace) and a tail (anything after the first
* whitespace, excluding the leading whitespace).
*/
private static class Completable
{
final String buffer;
final String head;
final String tail;
final int position;
final int cursor;
final List<CharSequence> candidates;
Completable(String buffer, int cursor, List<CharSequence> candidates)
{
int offset = CharMatcher.whitespace().indexIn(buffer);
if (offset > -1) {
head = buffer.substring(0, offset);
int i = CharMatcher.whitespace().negate().indexIn(buffer, offset);
offset = (i > -1) ? i : buffer.length();
tail = buffer.substring(offset);
} else {
head = buffer;
tail = null;
}
this.buffer = buffer;
this.position = offset;
this.cursor = cursor;
this.candidates = candidates;
}
boolean hasTail()
{
return tail != null;
}
int complete(Completer completer)
{
return completer.complete(buffer, cursor, candidates);
}
int completeTail(Completer completer)
{
int i = completer.complete(tail, cursor - position, candidates);
return (i == -1) ? -1 : i + position;
}
}
/**
* A Position tracks the address of a cell and a human readable version of that address.
*/
private static class Position
{
/**
* User readable form of the position. Is typically displayed in the prompt.
*/
final String remoteName;
/**
* CellPath address of the cell identified by the position.
*/
final CellPath remote;
Position(String name, CellPath path)
{
remoteName = name;
remote = path;
}
}
}