sapling/eden/fs/cli/main.py

447 lines
15 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
#
# Copyright (c) 2016-present, Facebook, Inc.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree. An additional grant
# of patent rights can be found in the PATENTS file in the same directory.
import argparse
from eden.thrift import EdenNotRunningError
import errno
import json
import os
import sys
from . import config as config_mod
from . import util
from facebook.eden import EdenService
# Relative to the user's $HOME/%USERPROFILE% directory.
# TODO: This value should be .eden outside of Facebook devservers.
DEFAULT_CONFIG_DIR = 'local/.eden'
# Environment variable that can be used instead of specifying --config-dir.
CONFIG_DIR_ENVIRONMENT_VARIABLE = 'EDEN_CONFIG_DIR'
def infer_client_from_cwd(config, clientname):
if clientname:
return clientname
all_clients = config.get_all_client_config_info()
path = normalize_path_arg(os.getcwd())
# Keep going while we're not in the root, as dirname(/) is /
# and we can keep iterating forever.
while len(path) > 1:
for client, info in all_clients.items():
if info['mount'] == path:
return info['mount']
path = os.path.dirname(path)
print_stderr(
'cwd is not an eden mount point, and no client name was specified.')
sys.exit(2)
def do_help(args, parser, subparsers):
help_args = getattr(args, 'args', [])
num_help_args = len(help_args)
if num_help_args == 1:
name = args.args[0]
subparser = subparsers.choices.get(name, None)
if subparser:
subparser.parse_args(['--help'])
else:
print_stderr('No manual entry for %s' % name)
sys.exit(2)
elif num_help_args == 0:
parser.parse_args(['--help'])
else:
print_stderr('Too many args passed to help: %s' % help_args)
sys.exit(2)
def do_info(args):
config = create_config(args)
info = config.get_client_info(infer_client_from_cwd(config, args.client))
json.dump(info, sys.stdout, indent=2)
sys.stdout.write('\n')
def do_health(args):
config = create_config(args)
health_info = config.check_health()
if health_info.is_healthy():
print('eden running normally (pid {})'.format(health_info.pid))
return 0
print('edenfs not healthy: {}'.format(health_info.detail))
return 1
def do_repository(args):
config = create_config(args)
if (args.name and args.path):
repo_source, repo_type = util.get_repo_source_and_type(args.path)
if repo_type is None:
print_stderr(
'%s does not look like a git or hg repository' % args.path)
return 1
try:
config.add_repository(args.name,
repo_type=repo_type,
source=repo_source,
with_buck=args.with_buck)
except config_mod.UsageError as ex:
print_stderr('error: {}', ex)
return 1
elif (args.name or args.path):
print_stderr('repository command called with incorrect arguments')
return 1
else:
repo_list = config.get_repository_list()
for repo in sorted(repo_list):
print(repo)
def do_list(args):
config = create_config(args)
for path in config.get_mount_paths():
print(path)
def do_clone(args):
args.path = normalize_path_arg(args.path)
config = create_config(args)
snapshot_id = args.snapshot
if not snapshot_id:
try:
source = config.get_repo_data(args.repo)
except Exception as ex:
print_stderr('error: {}', ex)
return 1
if source['type'] == 'git':
snapshot_id = util.get_git_commit(source['path'])
elif source['type'] == 'hg':
snapshot_id = util.get_hg_commit(source['path'])
else:
print_stderr(
'%s does not look like a git or hg repository' % args.path)
return 1
try:
return config.clone(args.repo, args.path, snapshot_id)
except Exception as ex:
print_stderr('error: {}', ex)
return 1
def do_mount(args):
config = create_config(args)
try:
return config.mount(args.path)
except EdenNotRunningError as ex:
print_stderr('error: {}', ex)
return 1
def do_unmount(args):
args.path = normalize_path_arg(args.path)
config = create_config(args)
try:
return config.unmount(args.path, delete_config=not args.no_forget)
except EdenService.EdenError as ex:
print_stderr('error: {}', ex)
return 1
def do_checkout(args):
config = create_config(args)
try:
config.checkout(infer_client_from_cwd(config, args.client),
args.snapshot)
except Exception as ex:
print_stderr('checkout of %s failed for client %s: %s' % (
args.snapshot,
args.client,
str(ex)))
sys.exit(1)
def do_daemon(args):
config = create_config(args)
daemon_binary = args.daemon_binary or _find_default_daemon_binary()
# If this is the first time running the daemon, the ~/.eden directory
# structure needs to be set up.
# TODO(mbolin): Check whether the user is running as sudo/root. In general,
# we want to avoid creating ~/.eden as root.
_ensure_dot_eden_folder_exists(config)
# If the user put an "--" argument before the edenfs args, argparse passes
# that through to us. Strip it out.
edenfs_args = args.edenfs_args
if edenfs_args and edenfs_args[0] == '--':
edenfs_args = edenfs_args[1:]
try:
health_info = config.spawn(daemon_binary, edenfs_args,
gdb=args.gdb, gdb_args=args.gdb_arg,
foreground=args.foreground)
except config_mod.EdenStartError as ex:
print_stderr('error: {}', ex)
return 1
print('Started edenfs (pid {}). Logs available at {}'.format(
health_info.pid, config.get_log_path()))
return 0
def _find_default_daemon_binary():
# By default, we look for the daemon executable alongside this file.
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
candidate = os.path.join(script_dir, 'edenfs')
permissions = os.R_OK | os.X_OK
if os.access(candidate, permissions):
return candidate
# This is where the binary will be found relative to this file when it is
# run out of buck-out in debug mode.
candidate = os.path.normpath(os.path.join(script_dir, '../service/edenfs'))
if os.access(candidate, permissions):
return candidate
else:
return None
def _ensure_dot_eden_folder_exists(config):
'''Creates the ~/.eden folder as specified by --config-dir/$EDEN_CONFIG_DIR.
If the ~/.eden folder already exists, it will be left alone.
Returns the path to the RocksDB.
'''
db = config.get_or_create_path_to_rocks_db()
return db
def do_shutdown(args):
config = create_config(args)
client = None
try:
with config.get_thrift_client() as client:
pid = client.getPid()
# Ask the client to shutdown
client.shutdown()
except EdenNotRunningError:
print_stderr('error: edenfs is not running')
return 1
if args.timeout == 0:
print('Sent shutdown request to edenfs.')
return 0
# Wait until the process exits.
def eden_exited():
try:
os.kill(pid, 0)
except OSError as ex:
if ex.errno == errno.ESRCH:
# The process has exited
return True
# EPERM is okay (and means the process is still running),
# anything else is unexpected
if ex.errno != errno.EPERM:
raise
# Still running
return None
try:
util.poll_until(eden_exited, timeout=args.timeout)
print('edenfs exited')
return 0
except util.TimeoutError:
print_stderr('error: sent shutdown request, but edenfs did not exit '
'within {} seconds', args.timeout)
return 1
def create_parser():
'''Returns a parser and its immediate subparsers.'''
parser = argparse.ArgumentParser(description='Manage Eden clients.')
parser.add_argument(
'--config-dir',
help='Path to directory where client data is stored.')
parser.add_argument(
'--system-config-dir',
help='Path to directory that holds the system configuration files.')
parser.add_argument(
'--home-dir',
help='Path to directory where .edenrc config file is stored.')
subparsers = parser.add_subparsers(dest='subparser_name')
# Please add the subparsers in alphabetical order because that is the order
# in which they are displayed when the user runs --help.
checkout_parser = subparsers.add_parser(
'checkout', help='Check out an alternative snapshot hash.')
checkout_parser.add_argument('--client', '-c',
default=None,
help='Name of the mounted client')
checkout_parser.add_argument('snapshot', help='Snapshot hash to check out')
checkout_parser.set_defaults(func=do_checkout)
clone_parser = subparsers.add_parser(
'clone', help='Create a clone of a specific repo')
clone_parser.add_argument(
'repo', help='Name of repository to clone')
clone_parser.add_argument(
'path', help='Path where the client should be mounted')
clone_parser.add_argument(
'--snapshot', '-s', type=str, help='Snapshot id of revision')
clone_parser.set_defaults(func=do_clone)
daemon_parser = subparsers.add_parser(
'daemon', help='Run the edenfs daemon')
daemon_parser.add_argument(
'--daemon-binary',
help='Path to the binary for the Eden daemon.')
daemon_parser.add_argument(
'--foreground', '-F', action='store_true',
help='Run eden in the foreground, rather than daemonizing')
daemon_parser.add_argument(
'--gdb', '-g', action='store_true', help='Run under gdb')
daemon_parser.add_argument(
'--gdb-arg', action='append', default=[],
help='Extra arguments to pass to gdb')
daemon_parser.add_argument(
'edenfs_args', nargs=argparse.REMAINDER,
help='Any extra arguments after an "--" argument will be passed to the '
'edenfs daemon.')
daemon_parser.set_defaults(func=do_daemon)
health_parser = subparsers.add_parser(
'health', help='Check the health of the Eden service')
health_parser.set_defaults(func=do_health)
help_parser = subparsers.add_parser(
'help', help='Display help information about Eden.')
help_parser.set_defaults(func=do_help)
help_parser.add_argument('args', nargs='*')
info_parser = subparsers.add_parser(
'info', help='Get details about a client.')
info_parser.add_argument(
'client',
default=None,
nargs='?',
help='Name of the client')
info_parser.set_defaults(func=do_info)
list_parser = subparsers.add_parser(
'list', help='List available clients')
list_parser.set_defaults(func=do_list)
repository_parser = subparsers.add_parser(
'repository', help='List all repositories')
repository_parser.add_argument(
'name', nargs='?', default=None, help='Name of the client to mount')
repository_parser.add_argument(
'path',
nargs='?',
default=None,
help='Path to the repository to import')
repository_parser.add_argument(
'--with-buck', '-b', action='store_true',
help='Client should create a bind mount for buck-out/.')
repository_parser.set_defaults(func=do_repository)
shutdown_parser = subparsers.add_parser(
'shutdown', help='Shutdown the daemon')
shutdown_parser.add_argument(
'-t', '--timeout', type=float, default=15.0,
help='Wait up to TIMEOUT seconds for the daemon to exit. '
'(default=%(default)s). If timeout is 0, then do not wait at all.')
shutdown_parser.set_defaults(func=do_shutdown)
unmount_parser = subparsers.add_parser(
'mount', help='Remount an existing client (for instance, after it was '
'unmounted with "unmount -n")')
unmount_parser.add_argument(
'path', help='The client mount path')
unmount_parser.set_defaults(func=do_mount)
unmount_parser = subparsers.add_parser(
'unmount', help='Unmount a specific client')
unmount_parser.add_argument(
'-n', '--no-forget',
action='store_true',
help='Only unmount the client, without forgetting about its '
'configuration. The client can be re-mounted later using the mount '
'command.')
unmount_parser.add_argument(
'path', help='Path where client should be unmounted from')
unmount_parser.set_defaults(func=do_unmount)
return parser, subparsers
def find_default_config_dir():
'''Returns the path to default Eden config directory.
If the environment variable $EDEN_CONFIG_DIR is set, it takes precedence
over the default, which is "$HOME/.eden".
Note that the path is not guaranteed to correspond to an existing directory.
'''
config_dir = os.getenv(CONFIG_DIR_ENVIRONMENT_VARIABLE)
if config_dir:
return config_dir
home_dir = util.get_home_dir()
return os.path.join(home_dir, DEFAULT_CONFIG_DIR)
def create_config(args):
config = args.config_dir or find_default_config_dir()
home_dir = args.home_dir or util.get_home_dir()
return config_mod.Config(config, args.system_config_dir, home_dir)
def main():
parser, subparsers = create_parser()
args = parser.parse_args()
if args.subparser_name == 'help' or getattr(args, 'func', None) is None:
retcode = do_help(args, parser, subparsers)
else:
retcode = args.func(args)
return retcode
def print_stderr(message, *args, **kwargs):
'''Prints the message to stderr.'''
if args or kwargs:
message = message.format(*args, **kwargs)
print(message, file=sys.stderr)
def normalize_path_arg(path_arg, may_need_tilde_expansion=False):
'''Normalizes a path by using os.path.realpath().
Note that this function is expected to be used with command-line arguments.
If the argument comes from a config file or GUI where tilde expansion is not
done by the shell, then may_need_tilde_expansion=True should be specified.
'''
if path_arg:
if may_need_tilde_expansion:
path_arg = os.path.expanduser(path_arg)
# Use the canonical version of the path.
path_arg = os.path.realpath(path_arg)
return path_arg
if __name__ == '__main__':
retcode = main()
sys.exit(retcode)