2016-06-27 21:58:55 +03:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
#
|
2017-01-21 09:02:33 +03:00
|
|
|
# Copyright (c) 2016-present, Facebook, Inc.
|
2016-05-12 23:43:17 +03:00
|
|
|
# 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
|
2016-05-25 23:14:22 +03:00
|
|
|
import errno
|
2018-05-04 21:29:10 +03:00
|
|
|
import glob
|
2016-05-12 23:43:17 +03:00
|
|
|
import json
|
|
|
|
import os
|
2017-10-18 21:18:38 +03:00
|
|
|
import signal
|
2017-10-16 21:47:36 +03:00
|
|
|
import subprocess
|
2016-05-12 23:43:17 +03:00
|
|
|
import sys
|
2018-03-29 08:10:41 +03:00
|
|
|
import typing
|
2018-09-01 04:54:29 +03:00
|
|
|
from pathlib import Path
|
2018-04-24 23:17:19 +03:00
|
|
|
from typing import Any, List, Optional, Set, Tuple
|
2016-05-12 23:43:17 +03:00
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
import eden.thrift
|
2018-10-10 01:09:37 +03:00
|
|
|
import thrift.transport
|
|
|
|
from eden.cli.util import check_health_using_lockfile
|
2018-05-10 07:33:49 +03:00
|
|
|
from eden.thrift import EdenNotRunningError
|
|
|
|
from facebook.eden import EdenService
|
2018-05-25 23:47:46 +03:00
|
|
|
from facebook.eden.ttypes import GlobParams
|
2018-07-12 05:01:03 +03:00
|
|
|
from fb303.ttypes import fb_status
|
2018-05-10 07:33:49 +03:00
|
|
|
|
|
|
|
from . import (
|
2018-05-10 23:35:17 +03:00
|
|
|
buck,
|
2018-05-10 07:33:49 +03:00
|
|
|
config as config_mod,
|
2018-05-10 23:35:17 +03:00
|
|
|
daemon,
|
2018-05-10 07:33:49 +03:00
|
|
|
debug as debug_mod,
|
|
|
|
doctor as doctor_mod,
|
2018-06-21 04:05:24 +03:00
|
|
|
filesystem,
|
2018-07-17 04:39:49 +03:00
|
|
|
fsck as fsck_mod,
|
2018-05-10 07:33:49 +03:00
|
|
|
mtab,
|
2018-07-17 04:39:49 +03:00
|
|
|
overlay as overlay_mod,
|
2018-10-12 04:53:49 +03:00
|
|
|
process_finder,
|
2018-05-10 07:33:49 +03:00
|
|
|
rage as rage_mod,
|
|
|
|
stats as stats_mod,
|
|
|
|
subcmd as subcmd_mod,
|
2018-09-10 23:42:15 +03:00
|
|
|
top as top_mod,
|
2018-11-01 18:06:07 +03:00
|
|
|
trace as trace_mod,
|
2018-05-10 07:33:49 +03:00
|
|
|
util,
|
|
|
|
version as version_mod,
|
|
|
|
)
|
2018-11-10 03:02:51 +03:00
|
|
|
from .cmd_util import get_eden_instance, require_checkout
|
2018-08-16 07:34:10 +03:00
|
|
|
from .config import EdenInstance
|
2018-04-20 03:36:17 +03:00
|
|
|
from .subcmd import Subcmd
|
2018-05-10 23:35:17 +03:00
|
|
|
from .util import ShutdownError, print_stderr
|
2018-05-10 07:33:49 +03:00
|
|
|
|
2016-05-12 23:43:17 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
subcmd = subcmd_mod.Decorator()
|
|
|
|
|
2016-06-07 07:00:41 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
def infer_client_from_cwd(instance: EdenInstance, clientname: str) -> str:
|
2016-05-12 23:43:17 +03:00
|
|
|
if clientname:
|
|
|
|
return clientname
|
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
all_clients = instance.get_all_client_config_info()
|
2016-05-12 23:43:17 +03:00
|
|
|
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:
|
2017-04-04 01:47:53 +03:00
|
|
|
for _, info in all_clients.items():
|
2018-05-10 07:33:49 +03:00
|
|
|
if info["mount"] == path:
|
|
|
|
return typing.cast(str, info["mount"])
|
2016-05-12 23:43:17 +03:00
|
|
|
path = os.path.dirname(path)
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("cwd is not an eden mount point, and no checkout name was specified.")
|
2016-05-12 23:43:17 +03:00
|
|
|
sys.exit(2)
|
|
|
|
|
|
|
|
|
2018-03-29 08:10:41 +03:00
|
|
|
def do_version(args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-05-10 07:33:49 +03:00
|
|
|
print("Installed: %s" % version_mod.get_installed_eden_rpm_version())
|
2018-01-19 02:31:31 +03:00
|
|
|
import eden
|
2018-05-10 07:33:49 +03:00
|
|
|
|
2018-01-19 02:31:31 +03:00
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
rv = version_mod.get_running_eden_version(instance)
|
2018-05-10 07:33:49 +03:00
|
|
|
print("Running: %s" % rv)
|
|
|
|
if rv.startswith("-") or rv.endswith("-"):
|
|
|
|
print("(Dev version of eden seems to be running)")
|
2018-05-23 05:45:21 +03:00
|
|
|
except EdenNotRunningError:
|
2018-05-10 07:33:49 +03:00
|
|
|
print("Running: Unknown (edenfs does not appear to be running)")
|
2018-01-19 02:31:31 +03:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("version", "Print Eden's version information.")
|
2018-04-20 03:36:17 +03:00
|
|
|
class VersionCmd(Subcmd):
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
|
|
|
return do_version(args)
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("info", "Get details about a checkout")
|
2018-04-20 03:36:17 +03:00
|
|
|
class InfoCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"client", default=None, nargs="?", help="Name of the checkout"
|
|
|
|
)
|
2016-05-12 23:43:17 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
|
|
|
info = instance.get_client_info(infer_client_from_cwd(instance, args.client))
|
2018-04-20 03:36:17 +03:00
|
|
|
json.dump(info, sys.stdout, indent=2)
|
2018-05-10 07:33:49 +03:00
|
|
|
sys.stdout.write("\n")
|
2016-06-11 00:15:26 +03:00
|
|
|
return 0
|
2016-06-18 01:11:04 +03:00
|
|
|
|
2016-06-07 23:01:59 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("status", "Check the health of the Eden service", aliases=["health"])
|
2018-04-26 00:02:51 +03:00
|
|
|
class StatusCmd(Subcmd):
|
2018-10-09 23:55:16 +03:00
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
|
|
|
"--timeout",
|
|
|
|
type=float,
|
|
|
|
default=15.0,
|
|
|
|
help="Wait up to TIMEOUT seconds for the daemon to respond "
|
|
|
|
"(default=%(default)s).",
|
|
|
|
)
|
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-10-09 23:55:16 +03:00
|
|
|
health_info = instance.check_health(timeout=args.timeout)
|
2018-04-20 03:36:17 +03:00
|
|
|
if health_info.is_healthy():
|
2018-05-10 07:33:49 +03:00
|
|
|
print("eden running normally (pid {})".format(health_info.pid))
|
2018-04-20 03:36:17 +03:00
|
|
|
return 0
|
2016-06-07 23:01:59 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
print("edenfs not healthy: {}".format(health_info.detail))
|
2016-07-07 02:14:18 +03:00
|
|
|
return 1
|
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("repository", "List all repositories")
|
2018-04-20 03:36:17 +03:00
|
|
|
class RepositoryCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"name", nargs="?", default=None, help="Name of the checkout to mount"
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"path", nargs="?", default=None, help="Path to the repository to import"
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--with-buck",
|
|
|
|
"-b",
|
|
|
|
action="store_true",
|
|
|
|
help="Checkout should create a bind mount for buck-out/.",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-05-10 07:33:49 +03:00
|
|
|
if args.name and args.path:
|
2018-04-24 23:17:19 +03:00
|
|
|
repo = util.get_repo(args.path)
|
|
|
|
if repo is None:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("%s does not look like a git or hg repository" % args.path)
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.add_repository(
|
2018-05-10 07:33:49 +03:00
|
|
|
args.name,
|
|
|
|
repo_type=repo.type,
|
|
|
|
source=repo.source,
|
|
|
|
with_buck=args.with_buck,
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
except config_mod.UsageError as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("error: {}", ex)
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
2018-05-19 09:05:39 +03:00
|
|
|
elif args.name or args.path:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("repository command called with incorrect arguments")
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
2018-01-04 23:10:19 +03:00
|
|
|
else:
|
2018-08-16 07:34:10 +03:00
|
|
|
repo_list = instance.get_repository_list()
|
2018-04-24 23:17:19 +03:00
|
|
|
for repo_name in sorted(repo_list):
|
|
|
|
print(repo_name)
|
2018-04-20 03:36:17 +03:00
|
|
|
return 0
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("list", "List available checkouts")
|
2018-04-20 03:36:17 +03:00
|
|
|
class ListCmd(Subcmd):
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2017-11-17 00:19:59 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
with instance.get_thrift_client() as client:
|
2018-04-20 03:36:17 +03:00
|
|
|
active_mount_points: Set[Optional[str]] = {
|
2018-08-11 11:34:44 +03:00
|
|
|
os.fsdecode(mount.mountPoint) for mount in client.listMounts()
|
2018-04-20 03:36:17 +03:00
|
|
|
}
|
|
|
|
except EdenNotRunningError:
|
|
|
|
active_mount_points = set()
|
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
config_mount_points = set(instance.get_mount_paths())
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
for path in sorted(active_mount_points | config_mount_points):
|
|
|
|
assert path is not None
|
|
|
|
if path not in config_mount_points:
|
2018-06-21 21:14:31 +03:00
|
|
|
print(f"{path} (unconfigured)")
|
2018-04-20 03:36:17 +03:00
|
|
|
elif path in active_mount_points:
|
|
|
|
print(path)
|
2018-06-21 21:14:31 +03:00
|
|
|
else:
|
|
|
|
print(f"{path} (not mounted)")
|
2018-04-20 03:36:17 +03:00
|
|
|
return 0
|
2017-11-17 00:19:59 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
|
2018-04-24 23:17:19 +03:00
|
|
|
class RepoError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("clone", "Create a clone of a specific repo and check it out")
|
2018-04-20 03:36:17 +03:00
|
|
|
class CloneCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"repo",
|
|
|
|
help="The path to an existing repo to clone, or the name of a "
|
|
|
|
"known repository configuration",
|
|
|
|
)
|
|
|
|
parser.add_argument("path", help="Path where the checkout should be mounted")
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--rev", "-r", type=str, help="The initial revision to check out"
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--allow-empty-repo",
|
|
|
|
"-e",
|
|
|
|
action="store_true",
|
|
|
|
help="Allow repo with null revision (no revisions)",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
# Optional arguments to control how to start the daemon if clone needs
|
|
|
|
# to start edenfs. We do not show these in --help by default These
|
|
|
|
# behave identically to the daemon arguments with the same name.
|
2018-05-10 07:33:49 +03:00
|
|
|
parser.add_argument("--daemon-binary", help=argparse.SUPPRESS)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--daemon-args",
|
|
|
|
dest="edenfs_args",
|
2018-04-20 03:36:17 +03:00
|
|
|
nargs=argparse.REMAINDER,
|
2018-05-10 07:33:49 +03:00
|
|
|
help=argparse.SUPPRESS,
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-04-24 23:17:19 +03:00
|
|
|
|
|
|
|
# Make sure the destination directory does not exist or is an empty
|
|
|
|
# directory. (We'll check this again later when actually creating the
|
|
|
|
# mount, but check this here just to fail early if things look wrong.)
|
|
|
|
try:
|
|
|
|
for _ in os.listdir(args.path):
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr(f"error: destination path {args.path} " "is not empty")
|
2018-04-24 23:17:19 +03:00
|
|
|
return 1
|
|
|
|
except OSError as ex:
|
|
|
|
if ex.errno == errno.ENOTDIR:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr(
|
|
|
|
f"error: destination path {args.path} " "is not a directory"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
return 1
|
|
|
|
elif ex.errno != errno.ENOENT:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr(
|
|
|
|
f"error: unable to access destination path " f"{args.path}: {ex}"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
return 1
|
|
|
|
|
2018-04-26 03:03:47 +03:00
|
|
|
args.path = os.path.realpath(args.path)
|
|
|
|
|
2018-04-24 23:17:19 +03:00
|
|
|
# Find the repository information
|
2018-04-20 03:36:17 +03:00
|
|
|
try:
|
2018-04-24 23:17:19 +03:00
|
|
|
repo, repo_type, repo_config = self._get_repo_info(
|
2018-08-16 07:34:10 +03:00
|
|
|
instance, args.repo, args.rev
|
2018-04-24 23:17:19 +03:00
|
|
|
)
|
|
|
|
except RepoError as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("error: {}", ex)
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
|
|
|
|
2018-04-24 23:17:19 +03:00
|
|
|
# Find the commit to check out
|
|
|
|
if args.rev is not None:
|
|
|
|
try:
|
|
|
|
commit = repo.get_commit_hash(args.rev)
|
|
|
|
except Exception as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr(
|
|
|
|
f"error: unable to find hash for commit " f"{args.rev!r}: {ex}"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
commit = repo.get_commit_hash(repo_config.default_revision)
|
|
|
|
except Exception as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr(
|
|
|
|
f"error: unable to find hash for commit "
|
|
|
|
f"{repo_config.default_revision!r}: {ex}"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
return 1
|
2018-04-20 03:36:17 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
NULL_REVISION = "0" * 40
|
2018-04-24 23:17:19 +03:00
|
|
|
if commit == NULL_REVISION and not args.allow_empty_repo:
|
2018-04-20 03:36:17 +03:00
|
|
|
print_stderr(
|
2018-05-10 07:33:49 +03:00
|
|
|
f"""\
|
2018-04-24 23:17:19 +03:00
|
|
|
error: the initial revision that would be checked out is the empty commit
|
|
|
|
|
|
|
|
The repository at {repo.source} may still be cloning.
|
|
|
|
Please make sure cloning completes before running `eden clone`
|
|
|
|
If you do want to check out the empty commit,
|
2018-05-10 07:33:49 +03:00
|
|
|
re-run `eden clone` with --allow-empty-repo"""
|
2018-04-20 03:36:17 +03:00
|
|
|
)
|
|
|
|
return 1
|
2017-11-17 00:19:59 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
# Attempt to start the daemon if it is not already running.
|
2018-08-16 07:34:10 +03:00
|
|
|
health_info = instance.check_health()
|
2018-04-20 03:36:17 +03:00
|
|
|
if not health_info.is_healthy():
|
2018-05-10 07:33:49 +03:00
|
|
|
print("edenfs daemon is not currently running. Starting edenfs...")
|
2018-04-20 03:36:17 +03:00
|
|
|
# Sometimes this returns a non-zero exit code if it does not finish
|
|
|
|
# startup within the default timeout.
|
2018-05-10 23:35:17 +03:00
|
|
|
exit_code = daemon.start_daemon(
|
2018-08-16 07:34:10 +03:00
|
|
|
instance, args.daemon_binary, args.edenfs_args
|
2018-05-10 23:35:17 +03:00
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
if exit_code != 0:
|
|
|
|
return exit_code
|
2017-11-28 21:25:16 +03:00
|
|
|
|
2018-04-24 23:17:19 +03:00
|
|
|
if repo_type is not None:
|
2018-05-10 07:33:49 +03:00
|
|
|
print(f"Cloning new {repo_type} repository at {args.path}...")
|
2018-04-24 23:17:19 +03:00
|
|
|
else:
|
2018-05-10 07:33:49 +03:00
|
|
|
print(f"Cloning new repository at {args.path}...")
|
2018-04-24 23:17:19 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.clone(repo_config, args.path, commit)
|
2018-05-10 07:33:49 +03:00
|
|
|
print(f"Success. Checked out commit {commit:.8}")
|
2018-04-24 23:17:19 +03:00
|
|
|
# In the future it would probably be nice to fork a background
|
|
|
|
# process here to prefetch files that we think the user is likely
|
|
|
|
# to want to access soon.
|
2018-04-20 03:36:17 +03:00
|
|
|
return 0
|
|
|
|
except Exception as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("error: {}", ex)
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
2016-07-07 02:14:24 +03:00
|
|
|
|
2018-04-24 23:17:19 +03:00
|
|
|
def _get_repo_info(
|
2018-08-16 07:34:10 +03:00
|
|
|
self, instance: EdenInstance, repo_arg: str, rev: Optional[str]
|
2018-04-24 23:17:19 +03:00
|
|
|
) -> Tuple[util.Repo, Optional[str], config_mod.ClientConfig]:
|
|
|
|
# Check to see if repo_arg points to an existing Eden mount
|
2018-08-16 07:34:10 +03:00
|
|
|
eden_config = instance.get_client_config_for_path(repo_arg)
|
2018-04-24 23:17:19 +03:00
|
|
|
if eden_config is not None:
|
|
|
|
repo = util.get_repo(eden_config.path)
|
|
|
|
if repo is None:
|
2018-05-10 07:33:49 +03:00
|
|
|
raise RepoError(
|
|
|
|
"eden mount is configured to use repository "
|
|
|
|
f"{eden_config.path} but unable to find a "
|
|
|
|
"repository at that location"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
return repo, None, eden_config
|
|
|
|
|
|
|
|
# Check to see if repo_arg looks like an existing repository path.
|
|
|
|
repo = util.get_repo(repo_arg)
|
|
|
|
if repo is None:
|
|
|
|
# This is not a valid repository path.
|
|
|
|
# Check to see if this is a repository config name instead.
|
2018-08-16 07:34:10 +03:00
|
|
|
repo_config = instance.find_config_for_alias(repo_arg)
|
2018-04-24 23:17:19 +03:00
|
|
|
if repo_config is None:
|
2018-05-10 07:33:49 +03:00
|
|
|
raise RepoError(
|
|
|
|
f"{repo_arg!r} does not look like a valid "
|
|
|
|
"hg or git repository or a well-known "
|
|
|
|
"repository name"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
|
|
|
|
repo = util.get_repo(repo_config.path)
|
|
|
|
if repo is None:
|
2018-05-10 07:33:49 +03:00
|
|
|
raise RepoError(
|
|
|
|
f"cloning {repo_arg} requires an existing "
|
|
|
|
f"repository to be present at "
|
|
|
|
f"{repo_config.path}"
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
|
|
|
|
return repo, repo_arg, repo_config
|
|
|
|
|
|
|
|
# This is a valid repository path.
|
|
|
|
# Try to identify what type of repository this is, so we can find
|
|
|
|
# the proper configuration to use.
|
2018-04-29 02:53:21 +03:00
|
|
|
project_id = util.get_project_id(repo, rev)
|
2018-04-24 23:17:19 +03:00
|
|
|
|
|
|
|
project_config = None
|
|
|
|
if project_id is not None:
|
2018-08-16 07:34:10 +03:00
|
|
|
project_config = instance.find_config_for_alias(project_id)
|
2018-04-24 23:17:19 +03:00
|
|
|
repo_type = project_id
|
|
|
|
if project_config is None:
|
|
|
|
repo_config = config_mod.ClientConfig(
|
|
|
|
path=repo.source,
|
|
|
|
scm_type=repo.type,
|
2018-08-16 07:34:10 +03:00
|
|
|
hooks_path=instance.get_default_hooks_path(),
|
2018-04-24 23:17:19 +03:00
|
|
|
bind_mounts={},
|
2018-05-10 07:33:49 +03:00
|
|
|
default_revision=config_mod.DEFAULT_REVISION[repo.type],
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
else:
|
|
|
|
# Build our own ClientConfig object, using our source repository
|
|
|
|
# path and type, but the hooks, bind-mount, and revision
|
|
|
|
# configuration from the project configuration.
|
|
|
|
repo_config = config_mod.ClientConfig(
|
|
|
|
path=repo.source,
|
|
|
|
scm_type=repo.type,
|
|
|
|
hooks_path=project_config.hooks_path,
|
|
|
|
bind_mounts=project_config.bind_mounts,
|
2018-05-10 07:33:49 +03:00
|
|
|
default_revision=project_config.default_revision,
|
|
|
|
)
|
2018-04-24 23:17:19 +03:00
|
|
|
|
|
|
|
return repo, repo_type, repo_config
|
2017-11-17 00:19:59 +03:00
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("config", "Query Eden configuration")
|
2018-04-20 03:36:17 +03:00
|
|
|
class ConfigCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
2018-05-10 07:33:49 +03:00
|
|
|
parser.add_argument("--get", help="Name of value to get")
|
2017-01-24 10:52:35 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-04-20 03:36:17 +03:00
|
|
|
if args.get:
|
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
print(instance.get_config_value(args.get))
|
2018-04-20 03:36:17 +03:00
|
|
|
except (KeyError, ValueError):
|
|
|
|
# mirrors `git config --get invalid`; just exit with code 1
|
|
|
|
return 1
|
|
|
|
else:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.print_full_config()
|
2018-04-20 03:36:17 +03:00
|
|
|
return 0
|
2017-01-24 10:52:35 +03:00
|
|
|
|
2017-11-01 03:04:30 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("doctor", "Debug and fix issues with Eden")
|
2018-04-20 03:36:17 +03:00
|
|
|
class DoctorCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--dry-run",
|
|
|
|
"-n",
|
|
|
|
action="store_true",
|
|
|
|
help="Do not try to fix any issues: only report them.",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-04-20 03:36:17 +03:00
|
|
|
return doctor_mod.cure_what_ails_you(
|
2018-08-16 07:34:10 +03:00
|
|
|
instance,
|
2018-06-21 04:05:24 +03:00
|
|
|
args.dry_run,
|
|
|
|
mount_table=mtab.LinuxMountTable(),
|
|
|
|
fs_util=filesystem.LinuxFsUtil(),
|
2018-10-12 04:53:49 +03:00
|
|
|
process_finder=process_finder.LinuxProcessFinder(),
|
2018-04-20 03:36:17 +03:00
|
|
|
)
|
2017-12-01 22:20:05 +03:00
|
|
|
|
|
|
|
|
2018-09-10 23:42:15 +03:00
|
|
|
@subcmd("top", "Monitor Eden accesses by process.")
|
|
|
|
class TopCmd(Subcmd):
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
|
|
|
return top_mod.show(args)
|
|
|
|
|
|
|
|
|
2018-07-17 04:39:49 +03:00
|
|
|
@subcmd("fsck", "Perform a filesystem check for Eden")
|
|
|
|
class FsckCmd(Subcmd):
|
2018-11-10 03:02:51 +03:00
|
|
|
EXIT_OK = 0
|
|
|
|
EXIT_SKIPPED = 1
|
|
|
|
EXIT_WARNINGS = 2
|
|
|
|
EXIT_ERRORS = 3
|
|
|
|
|
2018-07-17 04:39:49 +03:00
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
2018-11-10 03:02:51 +03:00
|
|
|
parser.add_argument(
|
|
|
|
"--force",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="Force fsck to scan for errors even on checkouts that appear to "
|
|
|
|
"currently be mounted. It will not attempt to fix any problems, but will "
|
|
|
|
"only scan and report possible issues.",
|
|
|
|
)
|
2018-11-26 23:26:39 +03:00
|
|
|
parser.add_argument(
|
|
|
|
"-n",
|
|
|
|
"--check-only",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="Only report errors, and do not attempt to fix any problems found.",
|
|
|
|
)
|
2018-07-17 04:39:49 +03:00
|
|
|
parser.add_argument(
|
|
|
|
"-v",
|
|
|
|
"--verbose",
|
|
|
|
action="store_true",
|
|
|
|
default=False,
|
|
|
|
help="Print more verbose information about issues found.",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"path",
|
2018-08-22 21:05:42 +03:00
|
|
|
metavar="CHECKOUT_PATH",
|
2018-11-10 03:02:51 +03:00
|
|
|
nargs="*",
|
2018-08-22 21:05:42 +03:00
|
|
|
help="The path to an Eden checkout to verify.",
|
2018-07-17 04:39:49 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-11-10 03:02:51 +03:00
|
|
|
if not args.path:
|
|
|
|
return_codes = self.check_all(args)
|
2018-09-01 04:54:29 +03:00
|
|
|
else:
|
2018-11-10 03:02:51 +03:00
|
|
|
return_codes = self.check_explicit_paths(args)
|
|
|
|
|
|
|
|
return max(return_codes)
|
|
|
|
|
|
|
|
def check_explicit_paths(self, args: argparse.Namespace) -> List[int]:
|
|
|
|
return_codes: List[int] = []
|
|
|
|
for path in args.path:
|
|
|
|
# Check to see if this looks like an Eden checkout state directory.
|
|
|
|
# If this looks like an Eden checkout state directory,
|
|
|
|
if (Path(path) / "local" / "info").exists() and (
|
|
|
|
Path(path) / "config.toml"
|
|
|
|
).exists():
|
|
|
|
result = self.check_one(args, Path(path), Path(path))
|
|
|
|
else:
|
|
|
|
instance, checkout, rel_path = require_checkout(args, path)
|
|
|
|
result = self.check_one(args, checkout.path, checkout.state_dir)
|
|
|
|
return_codes.append(result)
|
2018-07-17 04:39:49 +03:00
|
|
|
|
2018-11-10 03:02:51 +03:00
|
|
|
return return_codes
|
|
|
|
|
|
|
|
def check_all(self, args: argparse.Namespace) -> List[int]:
|
|
|
|
# Check all configured checkouts that are not currently mounted.
|
|
|
|
instance = get_eden_instance(args)
|
|
|
|
return_codes: List[int] = []
|
|
|
|
for checkout_path, rel_state_dir in instance._get_directory_map().items():
|
|
|
|
abs_state_dir = instance.state_dir / config_mod.CLIENTS_DIR / rel_state_dir
|
|
|
|
result = self.check_one(args, Path(checkout_path), abs_state_dir)
|
|
|
|
return_codes.append(result)
|
|
|
|
|
|
|
|
return return_codes
|
|
|
|
|
|
|
|
def check_one(
|
|
|
|
self, args: argparse.Namespace, checkout_path: Path, state_dir: Path
|
|
|
|
) -> int:
|
2018-11-26 23:26:39 +03:00
|
|
|
with fsck_mod.FilesystemChecker(state_dir) as checker:
|
2018-11-10 03:02:51 +03:00
|
|
|
if not checker._overlay_locked:
|
|
|
|
if args.force:
|
|
|
|
print(
|
|
|
|
f"warning: could not obtain lock on {checkout_path}, but "
|
|
|
|
f"scanning anyway due to --force "
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
print(f"Not checking {checkout_path}: mount is currently in use")
|
|
|
|
return self.EXIT_SKIPPED
|
|
|
|
|
|
|
|
print(f"Checking {checkout_path}...")
|
2018-07-17 04:39:49 +03:00
|
|
|
checker.scan_for_errors()
|
|
|
|
if not checker.errors:
|
|
|
|
print(" No issues found")
|
2018-11-10 03:02:51 +03:00
|
|
|
return self.EXIT_OK
|
2018-07-17 04:39:49 +03:00
|
|
|
|
|
|
|
num_warnings = 0
|
|
|
|
num_errors = 0
|
|
|
|
for error in checker.errors:
|
2018-11-10 03:02:52 +03:00
|
|
|
self._report_error(args, error)
|
2018-07-17 04:39:49 +03:00
|
|
|
if error.level == fsck_mod.ErrorLevel.WARNING:
|
|
|
|
num_warnings += 1
|
|
|
|
else:
|
|
|
|
num_errors += 1
|
|
|
|
|
|
|
|
if num_warnings > 0:
|
|
|
|
print(f" {num_warnings} warnings")
|
|
|
|
print(f" {num_errors} errors")
|
2018-11-26 23:26:39 +03:00
|
|
|
|
|
|
|
if args.check_only:
|
|
|
|
print("Not fixing errors: --check-only was specified")
|
|
|
|
elif not checker._overlay_locked:
|
|
|
|
print("Not fixing errors: checkout is currently in use")
|
|
|
|
else:
|
|
|
|
checker.fix_errors()
|
|
|
|
|
2018-07-17 04:39:49 +03:00
|
|
|
if num_errors == 0:
|
2018-11-10 03:02:51 +03:00
|
|
|
return self.EXIT_WARNINGS
|
|
|
|
return self.EXIT_ERRORS
|
2018-07-17 04:39:49 +03:00
|
|
|
|
2018-11-10 03:02:52 +03:00
|
|
|
def _report_error(self, args: argparse.Namespace, error: fsck_mod.Error) -> None:
|
|
|
|
print(f"{fsck_mod.ErrorLevel.get_label(error.level)}: {error}")
|
|
|
|
if args.verbose:
|
|
|
|
details = error.detailed_description()
|
|
|
|
if details:
|
|
|
|
print(" " + "\n ".join(details.splitlines()))
|
|
|
|
|
2018-07-17 04:39:49 +03:00
|
|
|
|
2018-08-10 21:09:48 +03:00
|
|
|
@subcmd("gc", "Minimize disk and memory usage by freeing caches")
|
|
|
|
class GcCmd(Subcmd):
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-08-10 21:09:48 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
with instance.get_thrift_client() as client:
|
2018-08-10 21:09:48 +03:00
|
|
|
# TODO: unload
|
|
|
|
print("Clearing and compacting local caches...", end="", flush=True)
|
|
|
|
client.clearAndCompactLocalStore()
|
|
|
|
print()
|
|
|
|
# TODO: clear kernel caches
|
|
|
|
|
2018-08-16 07:34:12 +03:00
|
|
|
return 0
|
|
|
|
|
2018-08-10 21:09:48 +03:00
|
|
|
|
2018-11-07 19:56:49 +03:00
|
|
|
@subcmd("chown", "Chown an entire eden repository")
|
|
|
|
class ChownCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument("path", metavar="path", help="The Eden checkout to chown")
|
|
|
|
parser.add_argument("uid", metavar="uid", help="The uid to chown to", type=int)
|
|
|
|
parser.add_argument("gid", metavar="gid", help="The gid to chown to", type=int)
|
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
|
|
|
instance = get_eden_instance(args)
|
|
|
|
bindmounts: List[bytes] = []
|
|
|
|
with instance.get_thrift_client() as client:
|
|
|
|
print("Chowning Eden repository...", end="", flush=True)
|
|
|
|
client.chown(args.path, args.uid, args.gid)
|
|
|
|
print("done")
|
|
|
|
bindmounts = client.getBindMounts(args.path)
|
|
|
|
for bindmount in bindmounts:
|
|
|
|
mount = bindmount.decode("utf-8")
|
|
|
|
print(f"Chowning bindmount: {mount}...", end="", flush=True)
|
|
|
|
full_path = os.path.join(args.path, mount)
|
|
|
|
subprocess.run(["sudo", "chown", "-R", f"{args.uid}:{args.gid}", full_path])
|
|
|
|
print("done")
|
|
|
|
|
2018-11-13 07:23:19 +03:00
|
|
|
return 0
|
|
|
|
|
2018-11-07 19:56:49 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
@subcmd(
|
2018-05-10 07:33:49 +03:00
|
|
|
"mount",
|
2018-06-21 21:14:26 +03:00
|
|
|
"Remount an existing checkout (for instance, after it was manually unmounted)",
|
2018-04-20 03:36:17 +03:00
|
|
|
)
|
|
|
|
class MountCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"paths", nargs="+", metavar="path", help="The checkout mount path"
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-04-20 03:36:17 +03:00
|
|
|
for path in args.paths:
|
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
exitcode = instance.mount(path)
|
2018-04-20 03:36:17 +03:00
|
|
|
if exitcode:
|
|
|
|
return exitcode
|
|
|
|
except EdenNotRunningError as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("error: {}", ex)
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
|
|
|
return 0
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
@subcmd("remove", "Remove an eden checkout", aliases=["rm"])
|
|
|
|
class RemoveCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
|
|
|
"-y",
|
|
|
|
"--yes",
|
|
|
|
"--no-prompt",
|
|
|
|
dest="prompt",
|
|
|
|
default=True,
|
|
|
|
action="store_false",
|
|
|
|
help="Do not prompt for confirmation before removing the checkouts.",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"paths", nargs="+", metavar="path", help="The Eden checkout(s) to remove"
|
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-11-13 07:23:19 +03:00
|
|
|
configured_mounts = list(instance.get_mount_paths())
|
2018-05-23 05:45:21 +03:00
|
|
|
|
2018-06-23 22:15:55 +03:00
|
|
|
# First translate the list of paths into canonical checkout paths
|
|
|
|
# We also track a bool indicating if this checkout is currently mounted
|
|
|
|
mounts: List[Tuple[str, bool]] = []
|
2018-05-23 05:45:21 +03:00
|
|
|
for path in args.paths:
|
|
|
|
try:
|
|
|
|
mount_path = util.get_eden_mount_name(path)
|
2018-06-23 22:15:55 +03:00
|
|
|
active = True
|
2018-05-23 05:45:21 +03:00
|
|
|
except util.NotAnEdenMountError as ex:
|
2018-06-23 22:15:55 +03:00
|
|
|
# This is not an active mount point.
|
|
|
|
# Check for it by name in the config file anyway, in case it is
|
|
|
|
# listed in the config file but not currently mounted.
|
|
|
|
mount_path = os.path.realpath(path)
|
|
|
|
if mount_path in configured_mounts:
|
|
|
|
active = False
|
|
|
|
else:
|
|
|
|
print(f"error: {ex}")
|
|
|
|
return 1
|
|
|
|
active = False
|
2018-05-23 05:45:21 +03:00
|
|
|
except Exception as ex:
|
2018-06-23 22:15:55 +03:00
|
|
|
print(f"error: cannot determine mount point for {path}: {ex}")
|
2018-05-23 05:45:21 +03:00
|
|
|
return 1
|
2018-06-23 22:15:55 +03:00
|
|
|
mounts.append((mount_path, active))
|
2018-05-23 05:45:21 +03:00
|
|
|
|
|
|
|
# Warn the user since this operation permanently destroys data
|
|
|
|
if args.prompt and sys.stdin.isatty():
|
2018-06-23 22:15:55 +03:00
|
|
|
mounts_list = "\n ".join(path for path, active in mounts)
|
2018-05-23 05:45:21 +03:00
|
|
|
print(
|
|
|
|
f"""\
|
|
|
|
Warning: this operation will permanently delete the following checkouts:
|
|
|
|
{mounts_list}
|
|
|
|
|
|
|
|
Any uncommitted changes and shelves in this checkout will be lost forever."""
|
|
|
|
)
|
|
|
|
if not prompt_confirmation("Proceed?"):
|
|
|
|
print("Not confirmed")
|
|
|
|
return 2
|
|
|
|
|
|
|
|
# Unmount + destroy everything
|
2018-06-23 22:15:55 +03:00
|
|
|
exit_code = 0
|
|
|
|
for mount, active in mounts:
|
2018-05-23 05:45:21 +03:00
|
|
|
print(f"Removing {mount}...")
|
2018-06-23 22:15:55 +03:00
|
|
|
if active:
|
|
|
|
try:
|
|
|
|
stop_aux_processes_for_path(mount)
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.unmount(mount)
|
2018-06-23 22:15:55 +03:00
|
|
|
except Exception as ex:
|
|
|
|
print_stderr(f"error unmounting {mount}: {ex}")
|
|
|
|
exit_code = 1
|
|
|
|
# We intentionally fall through here and remove the mount point
|
|
|
|
# from the config file. The most likely cause of failure is if
|
|
|
|
# edenfs times out performing the unmount. We still want to go
|
|
|
|
# ahead delete the mount from the config in this case.
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.destroy_mount(mount)
|
2018-06-27 06:53:22 +03:00
|
|
|
except Exception as ex:
|
2018-06-23 22:15:55 +03:00
|
|
|
print_stderr(f"error deleting configuration for {mount}: {ex}")
|
|
|
|
exit_code = 1
|
|
|
|
# Continue around the loop removing any other mount points
|
2018-05-23 05:45:21 +03:00
|
|
|
|
2018-06-23 22:15:55 +03:00
|
|
|
if exit_code == 0:
|
|
|
|
print(f"Success")
|
|
|
|
return exit_code
|
2018-05-23 05:45:21 +03:00
|
|
|
|
|
|
|
|
2018-05-25 23:47:46 +03:00
|
|
|
@subcmd("prefetch", "Prefetch content for matching file patterns")
|
|
|
|
class PrefetchCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
|
|
|
"--repo", help="Specify path to repo root (default: root of cwd)"
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--pattern-file",
|
|
|
|
help=(
|
|
|
|
"Specify path to a file that lists patterns/files "
|
|
|
|
"to match, one per line"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--silent",
|
|
|
|
help="Do not print the names of the matching files",
|
2018-05-25 23:47:49 +03:00
|
|
|
default=False,
|
|
|
|
action="store_true",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--no-prefetch",
|
|
|
|
help="Do not prefetch; only match names",
|
|
|
|
default=False,
|
2018-05-25 23:47:46 +03:00
|
|
|
action="store_true",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"PATTERN", nargs="+", help="Filename patterns to match via fnmatch"
|
|
|
|
)
|
|
|
|
|
|
|
|
def _repo_root(self, path: str) -> Optional[str]:
|
|
|
|
try:
|
|
|
|
return util.get_eden_mount_name(path)
|
|
|
|
except Exception:
|
|
|
|
# Likely no .eden dir there, so probably not an eden repo
|
|
|
|
return None
|
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-05-25 23:47:46 +03:00
|
|
|
|
|
|
|
if args.repo:
|
|
|
|
repo_root = self._repo_root(args.repo)
|
|
|
|
if not repo_root:
|
|
|
|
print(f"{args.repo} does not appear to be an eden repo")
|
|
|
|
return 1
|
|
|
|
if repo_root != os.path.realpath(args.repo):
|
|
|
|
print(f"{args.repo} is not the root of an eden repo")
|
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
repo_root = self._repo_root(os.getcwd())
|
|
|
|
if not repo_root:
|
|
|
|
print("current directory does not appear to be an eden repo")
|
|
|
|
return 1
|
|
|
|
|
|
|
|
if args.pattern_file is not None:
|
|
|
|
with open(args.pattern_file) as f:
|
|
|
|
args.PATTERN += [pat.strip() for pat in f.readlines()]
|
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
with instance.get_thrift_client() as client:
|
2018-05-25 23:47:46 +03:00
|
|
|
result = client.globFiles(
|
|
|
|
GlobParams(
|
2018-08-11 11:34:44 +03:00
|
|
|
mountPoint=os.fsencode(repo_root),
|
2018-05-25 23:47:46 +03:00
|
|
|
globs=args.PATTERN,
|
|
|
|
includeDotfiles=False,
|
2018-05-25 23:47:49 +03:00
|
|
|
prefetchFiles=not args.no_prefetch,
|
|
|
|
suppressFileList=args.silent,
|
2018-05-25 23:47:46 +03:00
|
|
|
)
|
|
|
|
)
|
|
|
|
if not args.silent:
|
|
|
|
for name in result.matchingFiles:
|
|
|
|
print(name)
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
2018-06-21 21:14:26 +03:00
|
|
|
#
|
|
|
|
# Most users should not need the "unmount" command in most circumstances.
|
|
|
|
# Maybe we should deprecate or remove it in the future.
|
|
|
|
#
|
|
|
|
# - "eden unmount --destroy" used to be the way to remove a checkout, but this has been
|
|
|
|
# replaced by "eden rm".
|
|
|
|
# - I can't think of many situations where users would need to temporarily unmount a
|
|
|
|
# checkout. However, "/bin/umount" can be used to accomplish this. The only
|
|
|
|
# potential advantage of "eden umount" over "/bin/umount" is that "eden unmount" does
|
|
|
|
# not require root privileges.
|
|
|
|
#
|
|
|
|
@subcmd("unmount", "Temporarily unmount a specific checkout")
|
2018-04-20 03:36:17 +03:00
|
|
|
class UnmountCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
2018-06-21 21:14:26 +03:00
|
|
|
parser.add_argument("--destroy", action="store_true", help=argparse.SUPPRESS)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"paths",
|
|
|
|
nargs="+",
|
|
|
|
metavar="path",
|
|
|
|
help="Path where checkout should be unmounted from",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-06-21 21:14:26 +03:00
|
|
|
if args.destroy:
|
|
|
|
print_stderr(
|
|
|
|
'note: "eden unmount --destroy" is deprecated; '
|
|
|
|
'prefer using "eden rm" instead'
|
|
|
|
)
|
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-04-20 03:36:17 +03:00
|
|
|
for path in args.paths:
|
|
|
|
path = normalize_path_arg(path)
|
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.unmount(path)
|
2018-06-23 22:15:55 +03:00
|
|
|
if args.destroy:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance.destroy_mount(path)
|
2018-06-21 21:14:26 +03:00
|
|
|
except (EdenService.EdenError, EdenNotRunningError) as ex:
|
|
|
|
print_stderr(f"error: {ex}")
|
2018-04-20 03:36:17 +03:00
|
|
|
return 1
|
|
|
|
return 0
|
2016-06-07 07:00:41 +03:00
|
|
|
|
2018-03-29 08:10:41 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("start", "Start the edenfs daemon", aliases=["daemon"])
|
2018-04-26 00:02:51 +03:00
|
|
|
class StartCmd(Subcmd):
|
2018-04-20 03:36:17 +03:00
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--daemon-binary", help="Path to the binary for the Eden daemon."
|
|
|
|
)
|
2018-05-31 21:41:02 +03:00
|
|
|
parser.add_argument(
|
|
|
|
"--if-necessary",
|
|
|
|
action="store_true",
|
|
|
|
help="Only start edenfs if there are Eden checkouts configured.",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--foreground",
|
|
|
|
"-F",
|
|
|
|
action="store_true",
|
|
|
|
help="Run eden in the foreground, rather than daemonizing",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--takeover",
|
|
|
|
"-t",
|
|
|
|
action="store_true",
|
|
|
|
help="If an existing edenfs daemon is running, gracefully take "
|
|
|
|
"over its mount points.",
|
|
|
|
)
|
|
|
|
parser.add_argument("--gdb", "-g", action="store_true", help="Run under gdb")
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--gdb-arg",
|
|
|
|
action="append",
|
|
|
|
default=[],
|
|
|
|
help="Extra arguments to pass to gdb",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--strace",
|
|
|
|
"-s",
|
|
|
|
metavar="FILE",
|
|
|
|
help="Run eden under strace, and write strace output to FILE",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"edenfs_args",
|
|
|
|
nargs=argparse.REMAINDER,
|
2018-04-20 03:36:17 +03:00
|
|
|
help='Any extra arguments after an "--" argument will be passed '
|
2018-05-10 07:33:49 +03:00
|
|
|
"to the edenfs daemon.",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-12-08 03:55:49 +03:00
|
|
|
# If the user put an "--" argument before the edenfs args, argparse passes
|
|
|
|
# that through to us. Strip it out.
|
|
|
|
try:
|
|
|
|
args.edenfs_args.remove("--")
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
|
2018-12-08 05:48:35 +03:00
|
|
|
instance = get_eden_instance(args)
|
|
|
|
|
|
|
|
if instance.should_use_experimental_systemd_mode():
|
2018-12-07 02:58:09 +03:00
|
|
|
if args.foreground:
|
2018-12-08 05:48:35 +03:00
|
|
|
return self.start(args, instance)
|
2018-12-07 02:58:09 +03:00
|
|
|
else:
|
2018-12-08 05:48:35 +03:00
|
|
|
return self.start_using_systemd(args, instance)
|
2018-12-07 02:58:09 +03:00
|
|
|
else:
|
2018-12-08 05:48:35 +03:00
|
|
|
return self.start(args, instance)
|
2018-05-31 21:41:02 +03:00
|
|
|
|
2018-12-08 05:48:35 +03:00
|
|
|
def start(self, args: argparse.Namespace, instance: EdenInstance) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
if args.if_necessary and not instance.get_mount_paths():
|
2018-05-31 21:41:02 +03:00
|
|
|
print("No Eden mount points configured.")
|
|
|
|
return 0
|
|
|
|
|
2018-06-26 22:05:35 +03:00
|
|
|
daemon.exec_daemon(
|
2018-08-16 07:34:10 +03:00
|
|
|
instance,
|
2018-05-10 07:33:49 +03:00
|
|
|
args.daemon_binary,
|
|
|
|
args.edenfs_args,
|
|
|
|
takeover=args.takeover,
|
|
|
|
gdb=args.gdb,
|
|
|
|
gdb_args=args.gdb_arg,
|
|
|
|
strace_file=args.strace,
|
|
|
|
foreground=args.foreground,
|
|
|
|
)
|
2017-11-28 21:25:16 +03:00
|
|
|
|
2018-12-08 05:48:35 +03:00
|
|
|
def start_using_systemd(
|
|
|
|
self, args: argparse.Namespace, instance: EdenInstance
|
|
|
|
) -> int:
|
2018-12-07 02:58:09 +03:00
|
|
|
if args.gdb:
|
|
|
|
raise NotImplementedError("TODO(T33122320): Implement 'eden start --gdb'")
|
|
|
|
if args.strace:
|
|
|
|
raise NotImplementedError(
|
|
|
|
"TODO(T33122320): Implement 'eden start --strace'"
|
|
|
|
)
|
|
|
|
if args.takeover:
|
|
|
|
raise NotImplementedError(
|
|
|
|
"TODO(T33122320): Implement 'eden start --takeover'"
|
|
|
|
)
|
|
|
|
|
|
|
|
return daemon.start_systemd_service(
|
2018-12-08 03:55:49 +03:00
|
|
|
instance=instance,
|
|
|
|
daemon_binary=args.daemon_binary,
|
|
|
|
edenfs_args=args.edenfs_args,
|
2018-12-07 02:58:09 +03:00
|
|
|
)
|
|
|
|
|
2017-11-28 21:25:16 +03:00
|
|
|
|
2018-06-19 02:21:27 +03:00
|
|
|
def stop_aux_processes_for_path(repo_path: str) -> None:
|
|
|
|
"""Tear down processes that will hold onto file handles and prevent shutdown
|
|
|
|
for a given mount point/repo"""
|
|
|
|
buck.stop_buckd_for_repo(repo_path)
|
|
|
|
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
def stop_aux_processes(client: eden.thrift.EdenClient) -> None:
|
2018-06-19 02:21:27 +03:00
|
|
|
"""Tear down processes that will hold onto file handles and prevent shutdown
|
|
|
|
for all mounts"""
|
2018-04-27 08:52:24 +03:00
|
|
|
|
|
|
|
active_mount_points: Set[Optional[str]] = {
|
2018-08-11 11:34:44 +03:00
|
|
|
os.fsdecode(mount.mountPoint) for mount in client.listMounts()
|
2018-04-27 08:52:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
for repo in active_mount_points:
|
2018-05-04 21:29:10 +03:00
|
|
|
if repo is not None:
|
2018-06-19 02:21:27 +03:00
|
|
|
stop_aux_processes_for_path(repo)
|
2018-04-27 08:52:24 +03:00
|
|
|
|
|
|
|
# TODO: intelligently stop nuclide-server associated with eden
|
|
|
|
# print('Stopping nuclide-server...')
|
|
|
|
# subprocess.run(['pkill', '-f', 'nuclide-main'])
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
|
|
|
|
RESTART_MODE_FULL = "full"
|
|
|
|
RESTART_MODE_GRACEFUL = "graceful"
|
2018-07-12 05:01:03 +03:00
|
|
|
RESTART_MODE_FORCE = "force"
|
2018-04-20 03:59:32 +03:00
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("restart", "Restart the edenfs daemon")
|
2018-04-20 03:59:32 +03:00
|
|
|
class RestartCmd(Subcmd):
|
2018-11-13 07:23:19 +03:00
|
|
|
args: argparse.Namespace
|
|
|
|
|
2018-04-20 03:59:32 +03:00
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
mode_group = parser.add_mutually_exclusive_group()
|
|
|
|
mode_group.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--full",
|
|
|
|
action="store_const",
|
2018-04-20 03:59:32 +03:00
|
|
|
const=RESTART_MODE_FULL,
|
2018-05-10 07:33:49 +03:00
|
|
|
dest="restart_type",
|
|
|
|
help="Completely shut down edenfs before restarting it. This "
|
|
|
|
"will unmount and remount the edenfs mounts, requiring processes "
|
|
|
|
"using them to re-open any files and directories they are using.",
|
2018-04-20 03:59:32 +03:00
|
|
|
)
|
|
|
|
mode_group.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--graceful",
|
|
|
|
action="store_const",
|
2018-04-20 03:59:32 +03:00
|
|
|
const=RESTART_MODE_GRACEFUL,
|
2018-05-10 07:33:49 +03:00
|
|
|
dest="restart_type",
|
|
|
|
help="Perform a graceful restart. The new edenfs daemon will "
|
|
|
|
"take over the existing edenfs mount points with minimal "
|
|
|
|
"disruption to clients. Open file handles will continue to work "
|
|
|
|
"across the restart.",
|
2018-04-20 03:59:32 +03:00
|
|
|
)
|
2018-07-12 05:01:03 +03:00
|
|
|
mode_group.add_argument(
|
|
|
|
"--force",
|
|
|
|
action="store_const",
|
|
|
|
const=RESTART_MODE_FORCE,
|
|
|
|
dest="restart_type",
|
|
|
|
help="Force a full restart, even if the existing edenfs daemon is "
|
|
|
|
"still in the middle of starting or stopping.",
|
|
|
|
)
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-07-12 05:01:03 +03:00
|
|
|
parser.add_argument(
|
|
|
|
"--daemon-binary", help="Path to the binary for the Eden daemon."
|
|
|
|
)
|
2018-04-20 03:59:32 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--shutdown-timeout",
|
2018-04-20 03:59:32 +03:00
|
|
|
type=float,
|
2018-07-12 05:01:03 +03:00
|
|
|
default=None,
|
2018-05-10 07:33:49 +03:00
|
|
|
help="How long to wait for the old edenfs process to exit when "
|
|
|
|
"performing a full restart.",
|
2018-04-20 03:59:32 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
|
|
|
self.args = args
|
2018-07-12 05:01:03 +03:00
|
|
|
if args.restart_type is None:
|
2018-04-20 03:59:32 +03:00
|
|
|
# Default to a full restart for now
|
2018-07-12 05:01:03 +03:00
|
|
|
args.restart_type = RESTART_MODE_FULL
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(self.args)
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
health = instance.check_health()
|
2018-07-12 05:01:03 +03:00
|
|
|
if health.is_healthy():
|
|
|
|
assert health.pid is not None
|
|
|
|
if self.args.restart_type == RESTART_MODE_GRACEFUL:
|
2018-08-16 07:34:10 +03:00
|
|
|
return self._graceful_restart(instance)
|
2018-07-12 05:01:03 +03:00
|
|
|
else:
|
2018-08-16 07:34:10 +03:00
|
|
|
return self._full_restart(instance, health.pid)
|
2018-07-12 05:01:03 +03:00
|
|
|
elif health.pid is None:
|
|
|
|
# The daemon is not running
|
2018-08-16 07:34:10 +03:00
|
|
|
return self._start(instance)
|
2018-04-20 03:59:32 +03:00
|
|
|
else:
|
2018-07-12 05:01:03 +03:00
|
|
|
if health.status == fb_status.STARTING:
|
|
|
|
print(
|
|
|
|
f"The current edenfs daemon (pid {health.pid}) is still starting."
|
|
|
|
)
|
|
|
|
# Give the daemon a little extra time to hopefully finish starting
|
|
|
|
# before we time out and kill it.
|
|
|
|
stop_timeout = 30
|
|
|
|
elif health.status == fb_status.STOPPING:
|
|
|
|
print(
|
|
|
|
f"The current edenfs daemon (pid {health.pid}) is in the middle "
|
|
|
|
"of stopping."
|
|
|
|
)
|
|
|
|
# Use a reduced stopping timeout. If the user is using --force
|
|
|
|
# then the daemon is probably stuck or something, and we'll likely need
|
|
|
|
# to kill it anyway.
|
|
|
|
stop_timeout = 5
|
|
|
|
else:
|
|
|
|
# The only other status value we generally expect to receive here is
|
|
|
|
# fb_status.STOPPED. This is returned if we found an existing edenfs
|
|
|
|
# process but it is not responding to thrift calls.
|
|
|
|
print(
|
|
|
|
f"Found an existing edenfs daemon (pid {health.pid} that does not "
|
|
|
|
"seem to be responding to thrift calls."
|
|
|
|
)
|
|
|
|
# Don't attempt to ask the daemon to stop at all in this case;
|
|
|
|
# just kill it.
|
|
|
|
stop_timeout = 0
|
|
|
|
|
|
|
|
if self.args.restart_type != RESTART_MODE_FORCE:
|
|
|
|
print(f"Use --force if you want to forcibly restart the current daemon")
|
|
|
|
return 1
|
2018-08-16 07:34:10 +03:00
|
|
|
return self._force_restart(instance, health.pid, stop_timeout)
|
2018-07-12 05:01:03 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
def _graceful_restart(self, instance: EdenInstance) -> int:
|
2018-07-12 05:01:03 +03:00
|
|
|
print("Performing a graceful restart...")
|
2018-12-08 05:48:35 +03:00
|
|
|
if instance.should_use_experimental_systemd_mode():
|
2018-12-08 05:48:35 +03:00
|
|
|
raise NotImplementedError(
|
|
|
|
"TODO(T33122320): Implement 'eden restart --graceful'"
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
daemon.exec_daemon(
|
|
|
|
instance, daemon_binary=self.args.daemon_binary, takeover=True
|
|
|
|
)
|
|
|
|
return 1 # never reached
|
2018-07-12 05:01:03 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
def _start(self, instance: EdenInstance) -> int:
|
2018-07-12 05:01:03 +03:00
|
|
|
print("Eden is not currently running. Starting it...")
|
2018-12-08 05:48:35 +03:00
|
|
|
if instance.should_use_experimental_systemd_mode():
|
2018-12-08 05:48:35 +03:00
|
|
|
return daemon.start_systemd_service(
|
|
|
|
instance=instance, daemon_binary=self.args.daemon_binary
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
daemon.exec_daemon(instance, daemon_binary=self.args.daemon_binary)
|
2018-07-12 05:01:03 +03:00
|
|
|
return 1 # never reached
|
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
def _full_restart(self, instance: EdenInstance, old_pid: int) -> int:
|
2018-07-12 05:01:03 +03:00
|
|
|
print(
|
|
|
|
"""\
|
|
|
|
About to perform a full restart of Eden.
|
|
|
|
Note: this will temporarily disrupt access to your Eden-managed repositories.
|
|
|
|
Any programs using files or directories inside the Eden mounts will need to
|
|
|
|
re-open these files after Eden is restarted.
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
if self.args.restart_type != RESTART_MODE_FORCE and sys.stdin.isatty():
|
|
|
|
if not prompt_confirmation("Proceed?"):
|
|
|
|
print("Not confirmed.")
|
|
|
|
return 1
|
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
self._do_stop(instance, old_pid, timeout=15)
|
|
|
|
return self._finish_restart(instance)
|
2018-07-12 05:01:03 +03:00
|
|
|
|
|
|
|
def _force_restart(
|
2018-08-16 07:34:10 +03:00
|
|
|
self, instance: EdenInstance, old_pid: int, stop_timeout: int
|
2018-07-12 05:01:03 +03:00
|
|
|
) -> int:
|
|
|
|
print("Forcing a full restart...")
|
|
|
|
if stop_timeout <= 0:
|
|
|
|
print("Sending SIGTERM...")
|
|
|
|
os.kill(old_pid, signal.SIGTERM)
|
2018-08-16 07:34:10 +03:00
|
|
|
self._wait_for_stop(instance, old_pid, timeout=5)
|
2018-07-12 05:01:03 +03:00
|
|
|
else:
|
2018-08-16 07:34:10 +03:00
|
|
|
self._do_stop(instance, old_pid, stop_timeout)
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
return self._finish_restart(instance)
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-11-13 07:23:19 +03:00
|
|
|
def _wait_for_stop(self, instance: EdenInstance, pid: int, timeout: float) -> None:
|
2018-07-12 05:01:03 +03:00
|
|
|
# If --shutdown-timeout was specified on the command line that always takes
|
|
|
|
# precedence over the default timeout passed in by our caller.
|
|
|
|
if self.args.shutdown_timeout is not None:
|
2018-11-13 07:23:19 +03:00
|
|
|
timeout = typing.cast(float, self.args.shutdown_timeout)
|
2018-09-21 21:44:27 +03:00
|
|
|
daemon.wait_for_shutdown(pid, timeout=timeout)
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
def _do_stop(self, instance: EdenInstance, pid: int, timeout: int) -> None:
|
|
|
|
with instance.get_thrift_client() as client:
|
2018-07-12 05:01:03 +03:00
|
|
|
try:
|
|
|
|
stop_aux_processes(client)
|
|
|
|
except Exception as ex:
|
|
|
|
pass
|
|
|
|
try:
|
|
|
|
client.initiateShutdown(
|
|
|
|
f"`eden restart --force` requested by pid={os.getpid()} "
|
|
|
|
f"uid={os.getuid()}"
|
|
|
|
)
|
|
|
|
except Exception as ex:
|
|
|
|
print("Sending SIGTERM...")
|
|
|
|
os.kill(pid, signal.SIGTERM)
|
2018-08-16 07:34:10 +03:00
|
|
|
self._wait_for_stop(instance, pid, timeout)
|
2018-07-12 05:01:03 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
def _finish_restart(self, instance: EdenInstance) -> int:
|
2018-12-08 05:48:35 +03:00
|
|
|
if instance.should_use_experimental_systemd_mode():
|
2018-12-08 05:48:35 +03:00
|
|
|
exit_code = daemon.start_systemd_service(
|
|
|
|
instance=instance, daemon_binary=self.args.daemon_binary
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
exit_code = daemon.start_daemon(
|
|
|
|
instance, daemon_binary=self.args.daemon_binary
|
|
|
|
)
|
2018-07-12 05:01:03 +03:00
|
|
|
if exit_code != 0:
|
|
|
|
print("Failed to start edenfs!", file=sys.stderr)
|
|
|
|
return exit_code
|
|
|
|
|
|
|
|
print(
|
|
|
|
"""\
|
|
|
|
|
|
|
|
Successfully restarted edenfs.
|
|
|
|
Note: any programs running inside of an Eden-managed directory will need to cd
|
|
|
|
out of and back into the repository to pick up the new working directory state.
|
|
|
|
If you see "Transport endpoint not connected" errors from any program this
|
|
|
|
means it is still attempting to use the old mount point from the previous Eden
|
|
|
|
process."""
|
|
|
|
)
|
|
|
|
return 0
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-06-21 21:14:21 +03:00
|
|
|
@subcmd("rage", "Gather diagnostic information about eden")
|
2018-04-20 03:36:17 +03:00
|
|
|
class RageCmd(Subcmd):
|
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--stdout",
|
|
|
|
action="store_true",
|
|
|
|
help="Print the rage report to stdout: ignore reporter.",
|
|
|
|
)
|
2017-08-25 22:41:41 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
|
|
|
rage_processor = None
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-04-20 03:36:17 +03:00
|
|
|
try:
|
2018-08-16 07:34:10 +03:00
|
|
|
rage_processor = instance.get_config_value("rage.reporter")
|
2018-04-20 03:36:17 +03:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
proc: Optional[subprocess.Popen] = None
|
|
|
|
if rage_processor and not args.stdout:
|
2018-05-10 07:33:49 +03:00
|
|
|
proc = subprocess.Popen(["sh", "-c", rage_processor], stdin=subprocess.PIPE)
|
2018-04-20 03:36:17 +03:00
|
|
|
sink = proc.stdin
|
|
|
|
else:
|
|
|
|
proc = None
|
|
|
|
sink = sys.stdout.buffer
|
2017-08-25 22:41:41 +03:00
|
|
|
|
2018-08-16 07:34:10 +03:00
|
|
|
rage_mod.print_diagnostic_info(instance, sink)
|
2018-04-20 03:36:17 +03:00
|
|
|
if proc:
|
|
|
|
sink.close()
|
|
|
|
proc.wait()
|
|
|
|
return 0
|
2017-06-15 20:53:16 +03:00
|
|
|
|
|
|
|
|
2017-10-18 21:18:38 +03:00
|
|
|
SHUTDOWN_EXIT_CODE_NORMAL = 0
|
|
|
|
SHUTDOWN_EXIT_CODE_REQUESTED_SHUTDOWN = 0
|
|
|
|
SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR = 2
|
|
|
|
SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL = 3
|
2018-04-20 03:59:32 +03:00
|
|
|
SHUTDOWN_EXIT_CODE_ERROR = 4
|
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
@subcmd("stop", "Shutdown the daemon", aliases=["shutdown"])
|
2018-04-26 00:02:51 +03:00
|
|
|
class StopCmd(Subcmd):
|
2018-04-20 03:36:17 +03:00
|
|
|
def setup_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"-t",
|
|
|
|
"--timeout",
|
|
|
|
type=float,
|
|
|
|
default=15.0,
|
|
|
|
help="Wait up to TIMEOUT seconds for the daemon to exit "
|
|
|
|
"(default=%(default)s). If it does not exit within the timeout, "
|
|
|
|
"then SIGKILL will be sent. If timeout is 0, then do not wait at "
|
|
|
|
"all and do not send SIGKILL.",
|
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
def run(self, args: argparse.Namespace) -> int:
|
2018-08-16 07:34:10 +03:00
|
|
|
instance = get_eden_instance(args)
|
2018-10-10 01:09:37 +03:00
|
|
|
pid = None
|
2016-06-18 01:11:04 +03:00
|
|
|
try:
|
2018-10-10 01:09:37 +03:00
|
|
|
try:
|
|
|
|
with instance.get_thrift_client() as client:
|
|
|
|
client.set_timeout(self.__thrift_timeout(args))
|
|
|
|
pid = client.getPid()
|
|
|
|
stop_aux_processes(client)
|
|
|
|
# Ask the client to shutdown
|
|
|
|
print(f"Stopping edenfs daemon (pid {pid})...")
|
|
|
|
client.initiateShutdown(
|
|
|
|
f"`eden stop` requested by pid={os.getpid()} uid={os.getuid()}"
|
|
|
|
)
|
|
|
|
except thrift.transport.TTransport.TTransportException as e:
|
|
|
|
print_stderr(f"warning: edenfs is not responding: {e}")
|
|
|
|
if pid is None:
|
|
|
|
pid = check_health_using_lockfile(args.config_dir).pid
|
|
|
|
if pid is None:
|
|
|
|
raise EdenNotRunningError(args.config_dir) from e
|
2018-04-20 03:36:17 +03:00
|
|
|
except EdenNotRunningError:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("error: edenfs is not running")
|
2018-04-20 03:36:17 +03:00
|
|
|
return SHUTDOWN_EXIT_CODE_NOT_RUNNING_ERROR
|
|
|
|
|
|
|
|
if args.timeout == 0:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("Sent async shutdown request to edenfs.")
|
2018-04-20 03:36:17 +03:00
|
|
|
return SHUTDOWN_EXIT_CODE_REQUESTED_SHUTDOWN
|
|
|
|
|
|
|
|
try:
|
2018-09-21 21:44:27 +03:00
|
|
|
if daemon.wait_for_shutdown(pid, timeout=args.timeout):
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("edenfs exited cleanly.")
|
2018-04-20 03:59:32 +03:00
|
|
|
return SHUTDOWN_EXIT_CODE_NORMAL
|
|
|
|
else:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("Terminated edenfs with SIGKILL.")
|
2018-04-20 03:59:32 +03:00
|
|
|
return SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL
|
|
|
|
except ShutdownError as ex:
|
2018-05-10 07:33:49 +03:00
|
|
|
print_stderr("Error: " + str(ex))
|
2018-04-20 03:59:32 +03:00
|
|
|
return SHUTDOWN_EXIT_CODE_ERROR
|
2017-10-18 21:18:38 +03:00
|
|
|
|
2018-10-10 01:09:37 +03:00
|
|
|
def __thrift_timeout(self, args: argparse.Namespace) -> Optional[float]:
|
|
|
|
if args.timeout == 0:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return args.timeout
|
|
|
|
|
2018-04-20 03:59:32 +03:00
|
|
|
|
2018-04-20 03:36:17 +03:00
|
|
|
def create_parser() -> argparse.ArgumentParser:
|
2018-05-10 07:33:49 +03:00
|
|
|
"""Returns a parser"""
|
|
|
|
parser = argparse.ArgumentParser(description="Manage Eden checkouts.")
|
2018-08-16 07:34:10 +03:00
|
|
|
# TODO: We should probably rename this argument to --state-dir.
|
|
|
|
# This directory contains materialized file state and the list of managed checkouts,
|
|
|
|
# but doesn't really contain configuration.
|
2018-05-10 07:33:49 +03:00
|
|
|
parser.add_argument(
|
2018-08-16 07:34:10 +03:00
|
|
|
"--config-dir",
|
|
|
|
help="The path to the directory where edenfs stores its internal state.",
|
2018-05-10 07:33:49 +03:00
|
|
|
)
|
2016-05-12 23:43:17 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--etc-eden-dir",
|
|
|
|
help="Path to directory that holds the system configuration files.",
|
|
|
|
)
|
2016-08-05 22:49:21 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--home-dir", help="Path to directory where .edenrc config file is stored."
|
|
|
|
)
|
2016-07-07 02:14:29 +03:00
|
|
|
parser.add_argument(
|
2018-05-10 07:33:49 +03:00
|
|
|
"--version", "-v", action="store_true", help="Print eden version."
|
|
|
|
)
|
2017-08-25 22:41:41 +03:00
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
subcmd_mod.add_subcommands(
|
|
|
|
parser,
|
2018-11-01 18:06:07 +03:00
|
|
|
subcmd.commands
|
|
|
|
+ [
|
|
|
|
debug_mod.DebugCmd,
|
|
|
|
subcmd_mod.HelpCmd,
|
|
|
|
stats_mod.StatsCmd,
|
|
|
|
trace_mod.TraceCmd,
|
|
|
|
],
|
2018-05-10 07:33:49 +03:00
|
|
|
)
|
2018-04-20 03:36:17 +03:00
|
|
|
|
|
|
|
return parser
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
def prompt_confirmation(prompt: str) -> bool:
|
2018-09-10 23:42:15 +03:00
|
|
|
# Import readline lazily here because it conflicts with ncurses's resize support.
|
|
|
|
# https://bugs.python.org/issue2675
|
|
|
|
import readline # noqa: F401 Importing readline improves the behavior of input()
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
prompt_str = f"{prompt} [y/N] "
|
|
|
|
while True:
|
|
|
|
response = input(prompt_str)
|
|
|
|
value = response.lower()
|
|
|
|
if value in ("y", "yes"):
|
|
|
|
return True
|
|
|
|
if value in ("", "n", "no"):
|
|
|
|
return False
|
|
|
|
print('Please enter "yes" or "no"')
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
def normalize_path_arg(path_arg: str, may_need_tilde_expansion: bool = False) -> str:
|
|
|
|
"""Normalizes a path by using os.path.realpath().
|
2016-08-05 21:47:02 +03:00
|
|
|
|
|
|
|
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.
|
2018-05-10 07:33:49 +03:00
|
|
|
"""
|
2016-05-12 23:43:17 +03:00
|
|
|
if path_arg:
|
2016-08-05 21:47:02 +03:00
|
|
|
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
|
2016-05-12 23:43:17 +03:00
|
|
|
|
|
|
|
|
2018-07-18 06:30:40 +03:00
|
|
|
def is_working_directory_stale() -> bool:
|
|
|
|
try:
|
|
|
|
os.getcwd()
|
|
|
|
return False
|
|
|
|
except OSError as ex:
|
|
|
|
if ex.errno == errno.ENOTCONN:
|
|
|
|
return True
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
def check_for_stale_working_directory() -> Optional[int]:
|
|
|
|
try:
|
|
|
|
if not is_working_directory_stale():
|
|
|
|
return None
|
|
|
|
except OSError as ex:
|
|
|
|
print(
|
|
|
|
f"error: unable to determine current working directory: {ex}",
|
|
|
|
file=sys.stderr,
|
|
|
|
)
|
|
|
|
return os.EX_OSFILE
|
|
|
|
|
|
|
|
# See if we can figure out what the current working directory should be
|
|
|
|
# based on the $PWD environment variable that is normally set by most shells.
|
|
|
|
#
|
|
|
|
# If we have a valid $PWD, cd to it and try to continue using it.
|
|
|
|
# This lets commands like "eden doctor" work and report useful data even if
|
|
|
|
# the user is running it from a stale directory.
|
|
|
|
can_continue = False
|
|
|
|
cwd = os.environ.get("PWD")
|
|
|
|
if cwd is not None:
|
|
|
|
try:
|
|
|
|
os.chdir(cwd)
|
|
|
|
can_continue = True
|
|
|
|
except OSError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
msg = """\
|
|
|
|
Your current working directory appears to be a stale Eden
|
|
|
|
mount point from a previous Eden daemon instance.
|
|
|
|
Please run "cd / && cd -" to update your shell's working directory."""
|
|
|
|
if not can_continue:
|
|
|
|
print(f"Error: {msg}", file=sys.stderr)
|
|
|
|
return os.EX_OSFILE
|
|
|
|
|
|
|
|
print(f"Warning: {msg}", file=sys.stderr)
|
|
|
|
doctor_mod.working_directory_was_stale = True
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
def main() -> int:
|
2018-07-18 06:30:40 +03:00
|
|
|
# Before doing anything else check that the current working directory is valid.
|
|
|
|
# This helps catch the case where a user is trying to run the Eden CLI inside
|
|
|
|
# a stale eden mount point.
|
|
|
|
stale_return_code = check_for_stale_working_directory()
|
|
|
|
if stale_return_code is not None:
|
|
|
|
return stale_return_code
|
|
|
|
|
2018-05-23 05:45:21 +03:00
|
|
|
parser = create_parser()
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.version:
|
|
|
|
return do_version(args)
|
|
|
|
if getattr(args, "func", None) is None:
|
|
|
|
parser.print_help()
|
2018-07-18 06:30:40 +03:00
|
|
|
return os.EX_OK
|
2018-08-22 21:05:44 +03:00
|
|
|
try:
|
|
|
|
return_code: int = args.func(args)
|
|
|
|
except subcmd_mod.CmdError as ex:
|
|
|
|
print(f"error: {ex}", file=sys.stderr)
|
|
|
|
return os.EX_SOFTWARE
|
2018-05-23 05:45:21 +03:00
|
|
|
return return_code
|
|
|
|
|
|
|
|
|
2018-05-10 07:33:49 +03:00
|
|
|
if __name__ == "__main__":
|
2016-05-12 23:43:17 +03:00
|
|
|
retcode = main()
|
|
|
|
sys.exit(retcode)
|