2025-11-18 12:07:02 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
2026-03-06 11:45:15 +01:00
|
|
|
from __future__ import annotations
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2026-03-06 11:45:15 +01:00
|
|
|
from typing import TYPE_CHECKING, Iterable
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from typing import Sequence
|
|
|
|
|
from ExecContext import ExecContext
|
|
|
|
|
|
|
|
|
|
import os, sys, json
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2025-11-18 12:07:02 +01:00
|
|
|
from argparse import Namespace
|
|
|
|
|
from urllib.parse import urlparse
|
2025-11-20 11:29:50 +01:00
|
|
|
from enum import Enum, auto
|
|
|
|
|
|
2026-01-27 16:20:57 +01:00
|
|
|
from .log import *
|
|
|
|
|
|
2025-11-20 11:29:50 +01:00
|
|
|
class AskpassKey(Enum):
|
|
|
|
|
Username = auto()
|
|
|
|
|
Password = auto()
|
2025-11-18 12:07:02 +01:00
|
|
|
|
2025-11-20 10:44:14 +01:00
|
|
|
def pretty_cmd(cmd: list[str], wd=None):
|
2025-11-18 12:07:02 +01:00
|
|
|
tokens = [cmd[0]]
|
|
|
|
|
for token in cmd[1:]:
|
|
|
|
|
if token.find(' ') != -1:
|
|
|
|
|
token = '"' + token + '"'
|
|
|
|
|
tokens.append(token)
|
2026-02-23 10:26:59 +01:00
|
|
|
ret = ' '.join(tokens)
|
2025-11-18 12:07:02 +01:00
|
|
|
if wd is not None:
|
2025-11-20 10:44:14 +01:00
|
|
|
ret += f' in {wd}'
|
|
|
|
|
return ret
|
|
|
|
|
|
2026-03-06 16:50:27 +01:00
|
|
|
# See ExecContext.run() for what this function does
|
2026-03-23 13:13:55 +01:00
|
|
|
async def run_cmd(*args, ec: ExecContext|None=None, verbose: bool|None=None, **kwargs) -> Result:
|
2026-03-06 16:50:27 +01:00
|
|
|
if verbose is None:
|
|
|
|
|
verbose = False if ec is None else ec.verbose_default
|
2026-03-06 11:54:27 +01:00
|
|
|
if ec is None:
|
|
|
|
|
from .ec.Local import Local
|
2026-03-06 16:50:27 +01:00
|
|
|
ec = Local(verbose_default=verbose)
|
|
|
|
|
return await ec.run(verbose=verbose, *args, **kwargs)
|
2026-01-27 16:20:57 +01:00
|
|
|
|
2026-04-07 13:04:11 +02:00
|
|
|
async def run_curl(args: list[str], parse_json: bool=False, wd=None, throw=None, verbose=None, cmd_input=None, ec: ExecContext|None=None, decode=False) -> dict|str: # export
|
2026-03-06 16:50:27 +01:00
|
|
|
if verbose is None:
|
|
|
|
|
verbose = False if ec is None else ec.verbose_default
|
2025-11-18 12:07:02 +01:00
|
|
|
cmd = ['curl']
|
|
|
|
|
if not verbose:
|
|
|
|
|
cmd.append('-s')
|
|
|
|
|
cmd.extend(args)
|
|
|
|
|
if parse_json:
|
2026-04-07 13:04:11 +02:00
|
|
|
decode = True
|
|
|
|
|
output = await run_cmd(cmd, wd=wd, throw=throw, verbose=verbose, cmd_input=cmd_input, ec=ec)
|
|
|
|
|
stdout, stderr, status = output.decode() if decode else output
|
|
|
|
|
if not parse_json:
|
|
|
|
|
ret = stdout
|
|
|
|
|
else:
|
2025-11-20 10:44:14 +01:00
|
|
|
try:
|
2026-04-07 13:04:11 +02:00
|
|
|
ret = json.loads(stdout)
|
2025-11-20 10:44:14 +01:00
|
|
|
except Exception as e:
|
2026-02-27 15:45:08 +01:00
|
|
|
size = 'unknown number of'
|
2026-01-27 16:20:57 +01:00
|
|
|
try:
|
2026-04-07 13:04:11 +02:00
|
|
|
size = len(stdout)
|
2026-01-27 16:20:57 +01:00
|
|
|
except:
|
|
|
|
|
pass
|
2026-03-25 11:38:29 +01:00
|
|
|
log(ERR, f'Failed to parse {size} bytes output of command '
|
2026-04-07 13:04:11 +02:00
|
|
|
+ f'>{pretty_cmd(cmd, wd)}< ({str(e)}): "{stdout}"', file=sys.stderr)
|
2025-11-20 10:44:14 +01:00
|
|
|
raise
|
2026-03-03 08:34:27 +01:00
|
|
|
return ret, stderr, status
|
2025-11-18 12:07:02 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def run_askpass(askpass_env: list[str], key: AskpassKey, host: str|None=None, ec: ExecContext|None=None):
|
2025-11-20 11:29:50 +01:00
|
|
|
assert host is None # Currently unsupported
|
|
|
|
|
for var in askpass_env:
|
|
|
|
|
exe = os.getenv(var)
|
|
|
|
|
if exe is None:
|
|
|
|
|
continue
|
|
|
|
|
exe_arg = ''
|
|
|
|
|
match var:
|
|
|
|
|
case 'GIT_ASKPASS':
|
|
|
|
|
match key:
|
|
|
|
|
case AskpassKey.Username:
|
|
|
|
|
exe_arg += 'Username'
|
|
|
|
|
case AskpassKey.Password:
|
|
|
|
|
exe_arg += 'Password'
|
|
|
|
|
case 'SSH_ASKPASS':
|
|
|
|
|
match key:
|
|
|
|
|
case AskpassKey.Username:
|
|
|
|
|
continue # Can't get user name from SSH_ASKPASS
|
|
|
|
|
case AskpassKey.Password:
|
|
|
|
|
exe_arg += 'Password'
|
2026-03-23 13:13:55 +01:00
|
|
|
ret, stderr, status = await run_cmd([exe, exe_arg], throw=False, ec=ec).decode()
|
2025-11-20 11:29:50 +01:00
|
|
|
if ret is not None:
|
|
|
|
|
return ret
|
|
|
|
|
return None
|
|
|
|
|
|
2026-03-25 08:28:10 +01:00
|
|
|
async def run_sudo(cmd: list[str], *args, interactive: bool=True, ec: ExecContext|None=None, **kwargs):
|
2026-03-06 11:54:27 +01:00
|
|
|
if ec is None:
|
|
|
|
|
from .ec.Local import Local
|
|
|
|
|
ec = Local(interactive=interactive)
|
2026-03-25 08:28:10 +01:00
|
|
|
return await ec.sudo(cmd, *args, **kwargs)
|
2026-02-17 10:19:57 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def get_username(args: Namespace|None=None, url: str|None=None, askpass_env: list[str]=[], ec: ExecContext|None=None) -> str: # export
|
2025-11-18 12:07:02 +01:00
|
|
|
url_user = None if url is None else urlparse(url).username
|
|
|
|
|
if args is not None:
|
|
|
|
|
if args.username is not None:
|
|
|
|
|
if url_user is not None and url_user != args.username:
|
|
|
|
|
raise Exception(f'Username mismatch: called with --username="{args.username}", URL has user name "{url_user}"')
|
|
|
|
|
return args.username
|
2025-11-20 11:29:50 +01:00
|
|
|
if url_user is not None:
|
2025-11-18 12:07:02 +01:00
|
|
|
return url_user
|
2026-03-06 11:54:27 +01:00
|
|
|
return await run_askpass(askpass_env, AskpassKey.Username, ec=ec)
|
2025-11-18 12:07:02 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def get_password(args: Namespace|None=None, url: str|None=None, askpass_env: list[str]=[], ec: ExecContext|None=None) -> str: # export
|
2025-11-18 12:07:02 +01:00
|
|
|
if args is None and url is None and not askpass_env:
|
|
|
|
|
raise Exception(f'Neither URL nor command-line arguments nor askpass environment variable available, can\'t get password')
|
|
|
|
|
if args is not None and hasattr(args, 'password'): # use getattr(), because we don't necessarily want to have insecure --password among options
|
|
|
|
|
ret = getattr(args, 'password')
|
|
|
|
|
if ret is not None:
|
|
|
|
|
return ret
|
|
|
|
|
if url is not None:
|
|
|
|
|
parsed = urlparse(url)
|
|
|
|
|
if parsed.password is not None:
|
|
|
|
|
return parsed.password
|
2026-03-06 11:54:27 +01:00
|
|
|
return await run_askpass(askpass_env, AskpassKey.Password, ec=ec)
|
2026-02-18 14:18:26 +01:00
|
|
|
|
2026-03-06 11:54:27 +01:00
|
|
|
async def get_profile_env(throw: bool=True, keep: Iterable[str]|bool=False, ec: ExecContext|None=None) -> dict[str, str]: # export
|
2026-02-19 07:33:59 +01:00
|
|
|
"""
|
|
|
|
|
Get a fresh environment from /etc/profile
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
keep:
|
|
|
|
|
- False -> Don't keep anything
|
|
|
|
|
- True -> Keep what's in the current environment
|
|
|
|
|
- List of strings -> Keep those variables
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dictionary with fresh environment
|
|
|
|
|
"""
|
|
|
|
|
env: dict[str,str]|None = None
|
|
|
|
|
if keep == False or isinstance(keep, Iterable):
|
|
|
|
|
env = {
|
|
|
|
|
'HOME': os.environ.get('HOME', '/'),
|
|
|
|
|
'USER': os.environ.get('USER', ''),
|
|
|
|
|
'PATH': '/usr/bin:/bin',
|
|
|
|
|
}
|
|
|
|
|
# Run bash as a login shell, which sources /etc/profile, then print environment as NUL-separated key=value pairs
|
|
|
|
|
cmd = ['/usr/bin/env', '-i', '/bin/bash', '-lc', 'env -0']
|
lib.ExecContext: Align .sudo() prototype to .run()
ExecContext's .sudo() omits many of run()'s parameters, and this
commit adds them. To avoid redundancy around repeating and massaging
the long parameter list of both functions and their return values, it
also adds some deeper changes:
- Make run(), _run(), sudo() and _sudo() always return instances of
Result. Before it was allowed to return a triplet of stdout,
stderr, and exit status.
- Have ExecContext stay out of the business of decoding the result
entirely. Result provides a convenience method .decode()
operating on stdout and stderr and leaves the decision to the
caller.
This entails miniscule adaptations in calling code, namely in
App.os_release, util.get_profile_env() and CmdListRepos._run().
- Wrap the _run() and _sudo() callbacks in a context manager object
of type CallContext to avoid code duplication.
- Consistently name the first argument to run(), _run(), sudo() and
_sudo() "cmd", not "args". The latter suggests that the caller is
omitting the executable, which is not the case.
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-03-19 11:38:16 +01:00
|
|
|
result = await run_cmd(cmd, throw=throw, verbose=True, env=env, ec=ec)
|
2026-02-18 14:18:26 +01:00
|
|
|
ret: dict[str, str] = {}
|
2026-03-23 13:13:55 +01:00
|
|
|
for entry in result.stdout.rstrip(b"\0").split(b"\0"):
|
2026-02-18 14:18:26 +01:00
|
|
|
if not entry:
|
|
|
|
|
continue
|
|
|
|
|
key, val = entry.split(b"=", 1)
|
|
|
|
|
ret[key.decode()] = val.decode()
|
2026-02-19 07:33:59 +01:00
|
|
|
if isinstance(keep, Iterable):
|
|
|
|
|
for key in keep:
|
|
|
|
|
val = os.getenv(key)
|
|
|
|
|
if val is not None:
|
|
|
|
|
ret[key] = val
|
2026-02-18 14:18:26 +01:00
|
|
|
return ret
|