Skip to content

Commit 516a61d

Browse files
authored
Enhance log sharing capability (#4526)
1 parent e48ca45 commit 516a61d

6 files changed

Lines changed: 157 additions & 90 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ repos:
4141
additional_dependencies:
4242
- pydantic
4343
- pytest
44+
- hypothesis
4445
- cryptography
4546
- textual
4647
- repo: local

archinstall/lib/args.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import urllib.parse
77
from argparse import ArgumentParser, Namespace
88
from dataclasses import dataclass, field
9-
from enum import StrEnum
9+
from enum import Enum, StrEnum
1010
from pathlib import Path
1111
from typing import Any, Self
1212
from urllib.request import Request, urlopen
@@ -35,6 +35,10 @@
3535
from archinstall.tui.components import tui
3636

3737

38+
class SubCommand(Enum):
39+
SHARE_LOG = 'share-log'
40+
41+
3842
@p_dataclass
3943
class Arguments:
4044
config: Path | None = None
@@ -58,6 +62,8 @@ class Arguments:
5862
advanced: bool = False
5963
verbose: bool = False
6064

65+
command: SubCommand | None = None
66+
6167

6268
class ArchConfigType(StrEnum):
6369
VERSION = 'version'
@@ -365,13 +371,13 @@ def from_config(cls, args_config: dict[str, Any], args: Arguments) -> Self:
365371
class ArchConfigHandler:
366372
def __init__(self) -> None:
367373
self._parser: ArgumentParser = self._define_arguments()
368-
args: Arguments = self._parse_args()
369-
self._args = args
374+
self._add_sub_parsers()
370375

376+
self._args: Arguments = self._parse_args()
371377
config = self._parse_config()
372378

373379
try:
374-
self._config = ArchConfig.from_config(config, args)
380+
self._config = ArchConfig.from_config(config, self._args)
375381
self._config.version = get_version()
376382
except ValueError as err:
377383
warn(str(err))
@@ -397,8 +403,13 @@ def get_script(self) -> str:
397403
def print_help(self) -> None:
398404
self._parser.print_help()
399405

406+
def _add_sub_parsers(self) -> None:
407+
subparsers = self._parser.add_subparsers(dest='command', help='Available subcommands')
408+
_ = subparsers.add_parser(SubCommand.SHARE_LOG.value, help='Upload log file to public server')
409+
400410
def _define_arguments(self) -> ArgumentParser:
401411
parser = ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
412+
402413
parser.add_argument(
403414
'-v',
404415
'--version',

archinstall/lib/output.py

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ def __init__(self, path: Path = Path('/var/log/archinstall')) -> None:
185185
def path(self) -> Path:
186186
return self._path / 'install.log'
187187

188+
@path.setter
189+
def path(self, value: Path) -> None:
190+
self._path = value
191+
188192
@property
189193
def directory(self) -> Path:
190194
return self._path
@@ -212,6 +216,17 @@ def log(self, level: int, content: str) -> None:
212216
level_name = logging.getLevelName(level)
213217
f.write(f'[{ts}] - {level_name} - {content}\n')
214218

219+
def get_content(self, max_bytes: int | None = None) -> bytes:
220+
content = self.path.read_bytes()
221+
222+
if max_bytes is not None:
223+
size = self.path.stat().st_size
224+
225+
if size > max_bytes:
226+
content = content[-max_bytes:]
227+
228+
return content
229+
215230

216231
logger = Logger()
217232

@@ -295,6 +310,11 @@ def _stylize_output(
295310
return f'\033[{ansi}m{text}\033[0m'
296311

297312

313+
def _timestamp() -> str:
314+
now = datetime.now(tz=UTC)
315+
return now.strftime('%Y-%m-%d %H:%M:%S')
316+
317+
298318
def info(
299319
*msgs: str,
300320
level: int = logging.INFO,
@@ -306,11 +326,6 @@ def info(
306326
log(*msgs, level=level, fg=fg, bg=bg, reset=reset, font=font)
307327

308328

309-
def _timestamp() -> str:
310-
now = datetime.now(tz=UTC)
311-
return now.strftime('%Y-%m-%d %H:%M:%S')
312-
313-
314329
def debug(
315330
*msgs: str,
316331
level: int = logging.DEBUG,
@@ -368,48 +383,31 @@ def log(
368383

369384

370385
def share_install_log(
371-
paste_url: str = 'https://paste.rs',
372-
max_size: int = 10 * 1024 * 1024,
373-
confirm: Callable[[str], bool] = lambda _: True,
374-
) -> int:
386+
paste_url: str,
387+
max_bytes: int | None = None,
388+
) -> str | None:
375389
log_path = logger.path
376390

377391
if not log_path.exists():
378392
info(f'Log file not found: {log_path}')
379-
return 1
393+
return None
380394

381-
size = log_path.stat().st_size
382-
if size == 0:
383-
info(f'Log file is empty: {log_path}')
384-
return 1
385-
386-
if size > max_size:
387-
info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes')
388-
content = log_path.read_bytes()[-max_size:]
389-
else:
390-
content = log_path.read_bytes()
395+
content = logger.get_content(max_bytes=max_bytes)
391396

392-
header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n'
393-
header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n'
394-
header += 'The uploaded paste is public.\n\n'
395-
header += 'Continue?'
396-
397-
if not confirm(header):
398-
info('Cancelled.')
399-
return 1
397+
if len(content) == 0:
398+
info(f'Log file is empty: {log_path}')
399+
return None
400400

401401
try:
402402
req = urllib.request.Request(paste_url, data=content)
403403
with urllib.request.urlopen(req) as response:
404404
url = response.read().decode().strip()
405405
except urllib.error.URLError as e:
406406
info(f'Upload failed: {e}')
407-
return 1
407+
return None
408408

409409
if not url.startswith('http'):
410410
info(f'Unexpected response from {paste_url}: {url[:200]!r}')
411-
return 1
411+
return None
412412

413-
# raw print so the URL is pipe-friendly (no ANSI colors, no log prefix)
414-
print(url)
415-
return 0
413+
return url

archinstall/main.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
import traceback
99
from pathlib import Path
1010

11-
from archinstall.lib.args import ArchConfigHandler
11+
from archinstall.lib.args import ArchConfigHandler, SubCommand
1212
from archinstall.lib.disk.utils import disk_layouts
1313
from archinstall.lib.hardware import SysInfo
1414
from archinstall.lib.menu.helpers import Confirmation
1515
from archinstall.lib.network.wifi_handler import WifiHandler
1616
from archinstall.lib.networking import ping
17-
from archinstall.lib.output import debug, error, info, share_install_log, warn
17+
from archinstall.lib.output import debug, error, info, logger, share_install_log, warn
1818
from archinstall.lib.packages.util import check_version_upgrade
1919
from archinstall.lib.pacman.pacman import Pacman
2020
from archinstall.lib.translationhandler import tr, translation_handler
@@ -75,17 +75,36 @@ def _list_scripts() -> str:
7575
return '\n'.join(lines)
7676

7777

78-
def _tui_confirm(header: str) -> bool:
79-
async def _ask() -> bool:
78+
def _share_log_command() -> None:
79+
paste_url: str = 'https://paste.rs'
80+
log_path = logger.path
81+
max_size = 10 * 1024 * 1024 # max supported size by paste.rs
82+
content = logger.get_content(max_bytes=max_size).decode()
83+
84+
header = tr('About to upload "{}" to the publicly accessible {}').format(log_path, paste_url) + '\n\n'
85+
header += tr('Do you want to continue?')
86+
87+
group = MenuItemGroup.yes_no()
88+
group.set_preview_for_all(lambda _: content)
89+
90+
async def _confirm() -> bool:
8091
result = await Confirmation(
81-
group=MenuItemGroup.yes_no(),
8292
header=header,
8393
allow_skip=False,
84-
preset=False,
94+
group=group,
95+
preview_header='Log content',
96+
preview_location='bottom',
8597
).show()
8698
return result.get_value()
8799

88-
return tui.run(_ask)
100+
result = tui.run(_confirm)
101+
102+
if result is True:
103+
res = share_install_log(paste_url=paste_url, max_bytes=max_size)
104+
if res is not None:
105+
info(tr('Log uploaded successfully. URL: {}').format(res))
106+
else:
107+
error(tr('Failed to upload log.'))
89108

90109

91110
def run() -> int:
@@ -94,15 +113,19 @@ def run() -> int:
94113
OR straight as a module: python -m archinstall
95114
In any case we will be attempting to load the provided script to be run from the scripts/ folder
96115
"""
97-
if 'share-log' in sys.argv:
98-
return share_install_log(confirm=_tui_confirm)
99-
100116
arch_config_handler = ArchConfigHandler()
101117

102118
if '--help' in sys.argv or '-h' in sys.argv:
103119
arch_config_handler.print_help()
104120
return 0
105121

122+
match arch_config_handler.args.command:
123+
case SubCommand.SHARE_LOG:
124+
_share_log_command()
125+
exit(0)
126+
case None:
127+
pass
128+
106129
script = arch_config_handler.get_script()
107130

108131
if script == 'list':

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dev = [
4040
"ruff==0.15.13",
4141
"pylint==4.0.5",
4242
"pytest==9.0.3",
43+
"hypothesis>=6.152.4",
4344
]
4445
doc = ["sphinx"]
4546

0 commit comments

Comments
 (0)