mirror of
https://github.com/facebook/sapling.git
synced 2024-10-05 06:18:07 +03:00
eden cli: Refactor handler.exe code a bit
Summary: This diff moves the code that manages handler.exe to its own module and abstract the functionality into a base class, even though for now only Windows is implemented. It also calls this new handle in the branch that deletes a non-Eden directory, because it's very convenient to test. Reviewed By: genevievehelsel Differential Revision: D53594227 fbshipit-source-id: 309efab3f89893f745a82fe5acd128ecd702d42e
This commit is contained in:
parent
b1b6d9b430
commit
a23600b759
@ -141,6 +141,7 @@ python_library(
|
|||||||
"doctor/facebook/lib/fake_vscode_extensions_checker.py",
|
"doctor/facebook/lib/fake_vscode_extensions_checker.py",
|
||||||
"doctor/problem.py",
|
"doctor/problem.py",
|
||||||
"doctor/util.py",
|
"doctor/util.py",
|
||||||
|
"file_handler_tools.py",
|
||||||
"filesystem.py",
|
"filesystem.py",
|
||||||
"hg_util.py",
|
"hg_util.py",
|
||||||
"logfile.py",
|
"logfile.py",
|
||||||
|
@ -44,6 +44,7 @@ from facebook.eden.ttypes import MountInfo as ThriftMountInfo, MountState
|
|||||||
from filelock import BaseFileLock, FileLock
|
from filelock import BaseFileLock, FileLock
|
||||||
|
|
||||||
from . import configinterpolator, configutil, telemetry, util, version
|
from . import configinterpolator, configutil, telemetry, util, version
|
||||||
|
from .file_handler_tools import WinFileHandlerReleaser
|
||||||
from .util import (
|
from .util import (
|
||||||
FUSE_MOUNT_PROTOCOL_STRING,
|
FUSE_MOUNT_PROTOCOL_STRING,
|
||||||
HealthStatus,
|
HealthStatus,
|
||||||
@ -862,51 +863,6 @@ Do you want to run `eden mount %s` instead?"""
|
|||||||
with self.get_thrift_client_legacy(timeout=60) as client:
|
with self.get_thrift_client_legacy(timeout=60) as client:
|
||||||
client.unmount(os.fsencode(path))
|
client.unmount(os.fsencode(path))
|
||||||
|
|
||||||
def get_handle_path(self) -> Optional[Path]:
|
|
||||||
handle = shutil.which("handle.exe")
|
|
||||||
if handle:
|
|
||||||
return Path(handle)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def check_handle(self, mount: Path) -> None:
|
|
||||||
handle = self.get_handle_path()
|
|
||||||
|
|
||||||
if not handle:
|
|
||||||
return
|
|
||||||
|
|
||||||
print(f"Checking handle.exe for processes using '{mount}'...")
|
|
||||||
print("Press ctrl+c to skip.")
|
|
||||||
try:
|
|
||||||
output = subprocess.check_output(
|
|
||||||
[handle, "-nobanner", "-accepteula", mount]
|
|
||||||
)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
print("Handle check interrupted.\n")
|
|
||||||
print("If you want to find out which process is still using the repo, run:")
|
|
||||||
print(f" handle.exe {mount}\n")
|
|
||||||
return
|
|
||||||
parsed = [
|
|
||||||
line.split() for line in output.decode(errors="ignore").splitlines() if line
|
|
||||||
]
|
|
||||||
non_edenfs_process = any(filter(lambda x: x[0].lower() != "edenfs.exe", parsed))
|
|
||||||
|
|
||||||
# When no handle is found in the repo, handle.exe will report `"No
|
|
||||||
# matching handles found."`, which will be 4 words.
|
|
||||||
if not non_edenfs_process or not parsed or len(parsed[0]) == 4:
|
|
||||||
# Nothing other than edenfs.exe is holding handles to files from
|
|
||||||
# the repo, we can proceed with the removal
|
|
||||||
return
|
|
||||||
|
|
||||||
print(
|
|
||||||
"The following processes are still using the repo, please terminate them.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
for executable, _, pid, _, _type, _, path in parsed:
|
|
||||||
print(f"{executable}({pid}): {path}")
|
|
||||||
|
|
||||||
print()
|
|
||||||
return
|
|
||||||
|
|
||||||
def destroy_mount(
|
def destroy_mount(
|
||||||
self, path: Union[Path, str], preserve_mount_point: bool = False
|
self, path: Union[Path, str], preserve_mount_point: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -1007,22 +963,8 @@ trouble cleaning up leftovers. You will need to manually remove {path}.
|
|||||||
)
|
)
|
||||||
|
|
||||||
if used_by_other:
|
if used_by_other:
|
||||||
if self.get_handle_path():
|
winhr = WinFileHandlerReleaser()
|
||||||
self.check_handle(path)
|
winhr.try_release(path)
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"""\
|
|
||||||
It looks like {path} is still in use by another process. If you need help to
|
|
||||||
figure out which process, please try `handle.exe` from sysinternals:
|
|
||||||
|
|
||||||
handle.exe {path}
|
|
||||||
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
f"After terminating the processes, please manually delete {path}."
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
|
|
||||||
raise errors[0][1]
|
raise errors[0][1]
|
||||||
|
|
||||||
|
106
eden/fs/cli/file_handler_tools.py
Normal file
106
eden/fs/cli/file_handler_tools.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
#
|
||||||
|
# This software may be used and distributed according to the terms of the
|
||||||
|
# GNU General Public License version 2.
|
||||||
|
|
||||||
|
# pyre-strict
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
WINDOWS_HANDLE_BIN = "handle.exe"
|
||||||
|
|
||||||
|
|
||||||
|
class FileHandlerReleaser(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def get_handle_path(self) -> Optional[Path]:
|
||||||
|
"""Returns the path to the handle tool if it exists on the system, such as handle.exe on Windows or lsof on Linux."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def check_handle(self, mount: Path) -> None:
|
||||||
|
"""Displays processes keeping an open handle to files and if possible, offers to terminate them."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def try_release(self, mount: Path) -> None:
|
||||||
|
"""If a handle tool exist, use it to display info to the user with check handle."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WinFileHandlerReleaser(FileHandlerReleaser):
|
||||||
|
def get_handle_path(self) -> Optional[Path]:
|
||||||
|
if sys.platform != "win32":
|
||||||
|
return None
|
||||||
|
|
||||||
|
handle = shutil.which(WINDOWS_HANDLE_BIN)
|
||||||
|
if handle:
|
||||||
|
return Path(handle)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_handle(self, mount: Path) -> None:
|
||||||
|
handle = self.get_handle_path()
|
||||||
|
|
||||||
|
if not handle:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Checking handle.exe for processes using '{mount}'. This can take a while..."
|
||||||
|
)
|
||||||
|
print("Press ctrl+c to skip.")
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
[
|
||||||
|
handle,
|
||||||
|
"-nobanner",
|
||||||
|
"/accepteula",
|
||||||
|
mount,
|
||||||
|
] # / vs - is importart for accepteula, otherwise it won't find handles (??)
|
||||||
|
)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Handle check interrupted.\n")
|
||||||
|
print("If you want to find out which process is still using the repo, run:")
|
||||||
|
print(f" handle.exe {mount}\n")
|
||||||
|
return
|
||||||
|
parsed = [
|
||||||
|
line.split() for line in output.decode(errors="ignore").splitlines() if line
|
||||||
|
]
|
||||||
|
non_edenfs_process = any(filter(lambda x: x[0].lower() != "edenfs.exe", parsed))
|
||||||
|
|
||||||
|
# When no handle is found in the repo, handle.exe will report `"No
|
||||||
|
# matching handles found."`, which will be 4 words.
|
||||||
|
if not non_edenfs_process or not parsed or len(parsed[0]) == 4:
|
||||||
|
# Nothing other than edenfs.exe is holding handles to files from
|
||||||
|
# the repo, we can proceed with the removal
|
||||||
|
return
|
||||||
|
|
||||||
|
print(
|
||||||
|
"The following processes are still using the repo, please terminate them.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
for executable, _, pid, _, _type, _, path in parsed:
|
||||||
|
print(f"{executable}({pid}): {path}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
def try_release(self, mount: Path) -> None:
|
||||||
|
if self.get_handle_path():
|
||||||
|
self.check_handle(mount)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"""\
|
||||||
|
It looks like {mount} is still in use by another process. If you need help to
|
||||||
|
figure out which process, please try `handle.exe` from sysinternals:
|
||||||
|
|
||||||
|
handle.exe {mount}
|
||||||
|
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
print(f"After terminating the processes, please manually delete {mount}.")
|
||||||
|
print()
|
@ -78,6 +78,7 @@ from .constants import (
|
|||||||
SHUTDOWN_EXIT_CODE_REQUESTED_SHUTDOWN,
|
SHUTDOWN_EXIT_CODE_REQUESTED_SHUTDOWN,
|
||||||
SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL,
|
SHUTDOWN_EXIT_CODE_TERMINATED_VIA_SIGKILL,
|
||||||
)
|
)
|
||||||
|
from .file_handler_tools import WinFileHandlerReleaser
|
||||||
from .stats_print import format_size
|
from .stats_print import format_size
|
||||||
from .subcmd import Subcmd
|
from .subcmd import Subcmd
|
||||||
from .util import get_environment_suitable_for_subprocess, print_stderr, ShutdownError
|
from .util import get_environment_suitable_for_subprocess, print_stderr, ShutdownError
|
||||||
@ -1599,8 +1600,15 @@ Do you still want to delete {path}?"""
|
|||||||
path.rmdir()
|
path.rmdir()
|
||||||
return 0
|
return 0
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"error: cannot remove contents of {path}: {ex}")
|
if sys.platform != "win32":
|
||||||
return 1
|
print(
|
||||||
|
f"Error: cannot remove contents of {path}: {ex}"
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
winhr = WinFileHandlerReleaser()
|
||||||
|
winhr.try_release(path)
|
||||||
|
return 0
|
||||||
else:
|
else:
|
||||||
# We can't ask the user what their true intentions are,
|
# We can't ask the user what their true intentions are,
|
||||||
# so let's fail by default.
|
# so let's fail by default.
|
||||||
|
Loading…
Reference in New Issue
Block a user