forked from clan/clan-core
Merge pull request 'clan-cli: cmd.py uses pseudo terminal now. Remove tty.py. Refactor password_store.py to use cmd.py' (#1536) from Qubasa/clan-core:Qubasa-main into main
This commit is contained in:
commit
0993fe45f6
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
.direnv
|
.direnv
|
||||||
|
***/.vscode
|
||||||
***/.hypothesis
|
***/.hypothesis
|
||||||
out.log
|
out.log
|
||||||
.coverage.*
|
.coverage.*
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pty
|
||||||
import select
|
import select
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -8,7 +9,6 @@ import weakref
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO, Any
|
|
||||||
|
|
||||||
from .custom_logger import get_caller
|
from .custom_logger import get_caller
|
||||||
from .errors import ClanCmdError, CmdOut
|
from .errors import ClanCmdError, CmdOut
|
||||||
@ -23,42 +23,6 @@ class Log(Enum):
|
|||||||
NONE = 4
|
NONE = 4
|
||||||
|
|
||||||
|
|
||||||
def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
|
|
||||||
rlist = [process.stdout, process.stderr]
|
|
||||||
stdout_buf = b""
|
|
||||||
stderr_buf = b""
|
|
||||||
|
|
||||||
while len(rlist) != 0:
|
|
||||||
r, _, _ = select.select(rlist, [], [], 0.1)
|
|
||||||
if len(r) == 0: # timeout in select
|
|
||||||
if process.poll() is None:
|
|
||||||
continue
|
|
||||||
# Process has exited
|
|
||||||
break
|
|
||||||
|
|
||||||
def handle_fd(fd: IO[Any] | None) -> bytes:
|
|
||||||
if fd and fd in r:
|
|
||||||
read = os.read(fd.fileno(), 4096)
|
|
||||||
if len(read) != 0:
|
|
||||||
return read
|
|
||||||
rlist.remove(fd)
|
|
||||||
return b""
|
|
||||||
|
|
||||||
ret = handle_fd(process.stdout)
|
|
||||||
if ret and log in [Log.STDOUT, Log.BOTH]:
|
|
||||||
sys.stdout.buffer.write(ret)
|
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
stdout_buf += ret
|
|
||||||
ret = handle_fd(process.stderr)
|
|
||||||
|
|
||||||
if ret and log in [Log.STDERR, Log.BOTH]:
|
|
||||||
sys.stderr.buffer.write(ret)
|
|
||||||
sys.stderr.flush()
|
|
||||||
stderr_buf += ret
|
|
||||||
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace")
|
|
||||||
|
|
||||||
|
|
||||||
class TimeTable:
|
class TimeTable:
|
||||||
"""
|
"""
|
||||||
This class is used to store the time taken by each command
|
This class is used to store the time taken by each command
|
||||||
@ -114,38 +78,91 @@ def run(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
|
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
|
||||||
|
|
||||||
|
# Create pseudo-terminals for stdout/stderr and stdin
|
||||||
|
stdout_master_fd, stdout_slave_fd = pty.openpty()
|
||||||
|
stderr_master_fd, stderr_slave_fd = pty.openpty()
|
||||||
|
|
||||||
tstart = datetime.now()
|
tstart = datetime.now()
|
||||||
|
|
||||||
# Start the subprocess
|
proc = subprocess.Popen(
|
||||||
process = subprocess.Popen(
|
|
||||||
cmd,
|
cmd,
|
||||||
cwd=str(cwd),
|
preexec_fn=os.setsid,
|
||||||
|
stdin=stdout_slave_fd,
|
||||||
|
stdout=stdout_slave_fd,
|
||||||
|
stderr=stderr_slave_fd,
|
||||||
|
close_fds=True,
|
||||||
env=env,
|
env=env,
|
||||||
stdout=subprocess.PIPE,
|
cwd=str(cwd),
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
)
|
||||||
stdout_buf, stderr_buf = handle_output(process, log)
|
|
||||||
|
os.close(stdout_slave_fd) # Close slave FD in parent
|
||||||
|
os.close(stderr_slave_fd) # Close slave FD in parent
|
||||||
|
|
||||||
|
stdout_file = sys.stdout
|
||||||
|
stderr_file = sys.stderr
|
||||||
|
stdout_buf = b""
|
||||||
|
stderr_buf = b""
|
||||||
|
|
||||||
if input:
|
if input:
|
||||||
process.communicate(input)
|
written_b = os.write(stdout_master_fd, input)
|
||||||
else:
|
|
||||||
process.wait()
|
|
||||||
tend = datetime.now()
|
|
||||||
|
|
||||||
|
if written_b != len(input):
|
||||||
|
raise ValueError("Could not write all input to subprocess")
|
||||||
|
|
||||||
|
rlist = [stdout_master_fd, stderr_master_fd]
|
||||||
|
|
||||||
|
def handle_fd(fd: int | None) -> bytes:
|
||||||
|
if fd and fd in r:
|
||||||
|
try:
|
||||||
|
read = os.read(fd, 4096)
|
||||||
|
if len(read) != 0:
|
||||||
|
return read
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
rlist.remove(fd)
|
||||||
|
return b""
|
||||||
|
|
||||||
|
while len(rlist) != 0:
|
||||||
|
r, w, e = select.select(rlist, [], [], 0.1)
|
||||||
|
if len(r) == 0: # timeout in select
|
||||||
|
if proc.poll() is None:
|
||||||
|
continue
|
||||||
|
# Process has exited
|
||||||
|
break
|
||||||
|
|
||||||
|
ret = handle_fd(stdout_master_fd)
|
||||||
|
stdout_buf += ret
|
||||||
|
if ret and log in [Log.STDOUT, Log.BOTH]:
|
||||||
|
stdout_file.buffer.write(ret)
|
||||||
|
stdout_file.flush()
|
||||||
|
|
||||||
|
ret = handle_fd(stderr_master_fd)
|
||||||
|
stderr_buf += ret
|
||||||
|
if ret and log in [Log.STDERR, Log.BOTH]:
|
||||||
|
stderr_file.buffer.write(ret)
|
||||||
|
stderr_file.flush()
|
||||||
|
|
||||||
|
os.close(stdout_master_fd)
|
||||||
|
os.close(stderr_master_fd)
|
||||||
|
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
tend = datetime.now()
|
||||||
global TIME_TABLE
|
global TIME_TABLE
|
||||||
TIME_TABLE.add(shlex.join(cmd), tend - tstart)
|
TIME_TABLE.add(shlex.join(cmd), tend - tstart)
|
||||||
|
|
||||||
# Wait for the subprocess to finish
|
# Wait for the subprocess to finish
|
||||||
cmd_out = CmdOut(
|
cmd_out = CmdOut(
|
||||||
stdout=stdout_buf,
|
stdout=stdout_buf.decode("utf-8", "replace"),
|
||||||
stderr=stderr_buf,
|
stderr=stderr_buf.decode("utf-8", "replace"),
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
command=shlex.join(cmd),
|
command=shlex.join(cmd),
|
||||||
returncode=process.returncode,
|
returncode=proc.returncode,
|
||||||
msg=error_msg,
|
msg=error_msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
if check and process.returncode != 0:
|
if check and proc.returncode != 0:
|
||||||
raise ClanCmdError(cmd_out)
|
raise ClanCmdError(cmd_out)
|
||||||
|
|
||||||
return cmd_out
|
return cmd_out
|
||||||
|
@ -2,7 +2,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from clan_cli.cmd import Log, run
|
from clan_cli.cmd import run
|
||||||
from clan_cli.machines.machines import Machine
|
from clan_cli.machines.machines import Machine
|
||||||
from clan_cli.nix import nix_shell
|
from clan_cli.nix import nix_shell
|
||||||
|
|
||||||
@ -16,14 +16,13 @@ class SecretStore(SecretStoreBase):
|
|||||||
def set(
|
def set(
|
||||||
self, service: str, name: str, value: bytes, groups: list[str]
|
self, service: str, name: str, value: bytes, groups: list[str]
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
run(
|
subprocess.run(
|
||||||
nix_shell(
|
nix_shell(
|
||||||
["nixpkgs#pass"],
|
["nixpkgs#pass"],
|
||||||
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
|
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
|
||||||
),
|
),
|
||||||
input=value,
|
input=value,
|
||||||
log=Log.BOTH,
|
check=True,
|
||||||
error_msg=f"Failed to insert secret {name}",
|
|
||||||
)
|
)
|
||||||
return None # we manage the files outside of the git repo
|
return None # we manage the files outside of the git repo
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import getpass
|
import getpass
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@ -8,7 +9,6 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
||||||
from .. import tty
|
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..git import commit_files
|
from ..git import commit_files
|
||||||
from .folders import (
|
from .folders import (
|
||||||
@ -21,6 +21,13 @@ from .folders import (
|
|||||||
from .sops import decrypt_file, encrypt_file, ensure_sops_key, read_key, update_keys
|
from .sops import decrypt_file, encrypt_file, ensure_sops_key, read_key, update_keys
|
||||||
from .types import VALID_SECRET_NAME, secret_name_type
|
from .types import VALID_SECRET_NAME, secret_name_type
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def tty_is_interactive() -> bool:
|
||||||
|
"""Returns true if the current process is interactive"""
|
||||||
|
return sys.stdin.isatty() and sys.stdout.isatty()
|
||||||
|
|
||||||
|
|
||||||
def update_secrets(
|
def update_secrets(
|
||||||
flake_dir: Path, filter_secrets: Callable[[Path], bool] = lambda _: True
|
flake_dir: Path, filter_secrets: Callable[[Path], bool] = lambda _: True
|
||||||
@ -49,11 +56,11 @@ def collect_keys_for_type(folder: Path) -> set[str]:
|
|||||||
try:
|
try:
|
||||||
target = p.resolve()
|
target = p.resolve()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
tty.warn(f"Ignoring broken symlink {p}")
|
log.warn(f"Ignoring broken symlink {p}")
|
||||||
continue
|
continue
|
||||||
kind = target.parent.name
|
kind = target.parent.name
|
||||||
if folder.name != kind:
|
if folder.name != kind:
|
||||||
tty.warn(f"Expected {p} to point to {folder} but points to {target.parent}")
|
log.warn(f"Expected {p} to point to {folder} but points to {target.parent}")
|
||||||
continue
|
continue
|
||||||
keys.add(read_key(target))
|
keys.add(read_key(target))
|
||||||
return keys
|
return keys
|
||||||
@ -285,7 +292,7 @@ def set_command(args: argparse.Namespace) -> None:
|
|||||||
secret_value = None
|
secret_value = None
|
||||||
elif env_value:
|
elif env_value:
|
||||||
secret_value = env_value
|
secret_value = env_value
|
||||||
elif tty.is_interactive():
|
elif tty_is_interactive():
|
||||||
secret_value = getpass.getpass(prompt="Paste your secret: ")
|
secret_value = getpass.getpass(prompt="Paste your secret: ")
|
||||||
encrypt_secret(
|
encrypt_secret(
|
||||||
Path(args.flake),
|
Path(args.flake),
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
import sys
|
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import IO, Any
|
|
||||||
|
|
||||||
|
|
||||||
def is_interactive() -> bool:
|
|
||||||
"""Returns true if the current process is interactive"""
|
|
||||||
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
||||||
|
|
||||||
|
|
||||||
def color_text(code: int, file: IO[Any] = sys.stdout) -> Callable[[str], None]:
|
|
||||||
"""
|
|
||||||
Print with color if stderr is a tty
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(text: str) -> None:
|
|
||||||
if file.isatty():
|
|
||||||
print(f"\x1b[{code}m{text}\x1b[0m", file=file)
|
|
||||||
else:
|
|
||||||
print(text, file=file)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
warn = color_text(91, file=sys.stderr)
|
|
||||||
info = color_text(92, file=sys.stderr)
|
|
Loading…
Reference in New Issue
Block a user