kkpyutil 1.44.0__tar.gz → 1.46.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: kkpyutil
3
- Version: 1.44.0
3
+ Version: 1.46.0
4
4
  Summary: Building blocks for sysadmin and DevOps
5
5
  Home-page: https://github.com/kakyoism/kkpyutil/
6
6
  License: MIT
@@ -667,6 +667,24 @@ def format_xml(elem, indent=' ', encoding='utf-8'):
667
667
  return '\n'.join(non_empty_lines)
668
668
 
669
669
 
670
+ def format_callstack():
671
+ """
672
+ - traceback in worker thread is hard to propagate to main thread
673
+ - so we wrap around inspect.stack() before passing it
674
+ """
675
+ import inspect
676
+ stack = inspect.stack()
677
+ formatted_stack = []
678
+ for frame_info in reversed(stack): # Reverse to match traceback's order
679
+ filename = frame_info.filename
680
+ lineno = frame_info.lineno
681
+ function = frame_info.function
682
+ code_context = frame_info.code_context[0].strip() if frame_info.code_context else ''
683
+ # Format each frame like traceback
684
+ formatted_stack.append(f' File "{filename}", line {lineno}, in {function}\n {code_context}\n')
685
+ return ''.join(formatted_stack)
686
+
687
+
670
688
  def throw(err_cls, detail, advice):
671
689
  raise err_cls(f"""
672
690
  {format_brief('Detail', detail if isinstance(detail, list) else [detail])}
@@ -1237,23 +1255,85 @@ def save_winreg_record(full_key, var, value, value_type=winreg.REG_EXPAND_SZ if
1237
1255
  winreg.SetValueEx(key, var, 0, value_type, value)
1238
1256
 
1239
1257
 
1258
+ def _log_subprocess_command(cmd, cwd, logger, func_name="subprocess"):
1259
+ """Helper function to log subprocess command consistently"""
1260
+ cmd_log = f"""\
1261
+ {func_name}:
1262
+ {' '.join(cmd)}
1263
+ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1264
+ """
1265
+ logger.info(cmd_log)
1266
+
1267
+
1268
+ def _log_subprocess_startup_error(e, cmd, logger, useexception=True):
1269
+ """Helper function to log subprocess startup errors with structured format"""
1270
+ error_type = type(e).__name__
1271
+ cmd_str = ' '.join(cmd)
1272
+
1273
+ # Create structured error message based on error type
1274
+ if isinstance(e, FileNotFoundError):
1275
+ situation = "Command not found"
1276
+ detail = [f"Command: {cmd_str}", f"Error: {error_type}"]
1277
+ advice = [
1278
+ "Check if the command is installed and available in PATH",
1279
+ "Verify the command name spelling",
1280
+ "Use absolute path if the command is in a specific location"
1281
+ ]
1282
+ elif isinstance(e, PermissionError):
1283
+ situation = "Permission denied"
1284
+ detail = [f"Command: {cmd_str}", f"Error: {error_type}"]
1285
+ advice = [
1286
+ "Check if you have permission to execute the command",
1287
+ "Try running with elevated privileges if necessary",
1288
+ "Verify file permissions on the executable"
1289
+ ]
1290
+ else:
1291
+ situation = "Subprocess failed to start"
1292
+ detail = [f"Command: {cmd_str}", f"Error: {error_type}: {str(e)}"]
1293
+ advice = [
1294
+ "Check if the command exists and is executable",
1295
+ "Verify all command arguments are valid",
1296
+ "Check system resources and environment"
1297
+ ]
1298
+
1299
+ error_msg = format_log(situation, detail=detail, advice=advice)
1300
+ logger.error(error_msg)
1301
+
1302
+ if useexception:
1303
+ raise e
1304
+ return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1305
+
1306
+
1240
1307
  def run_cmd(cmd, cwd=None, logger=None, check=True, shell=False, verbose=False, useexception=True, env=None, hidedoswin=True):
1241
1308
  """
1242
- - Use shell==True with autotools where new shell is needed to treat the entire command option sequence as a command,
1243
- e.g., shell=True means running sh -c ./configure CFLAGS="..."
1244
- - we do not use check=False to supress exception because that'd leave app no way to tell if child-proc succeeded or not
1245
- - instead, we catch CallProcessError but avoid rethrow, and then return error code and other key diagnostics to app
1246
- - allow user to input non-str options, e.g., int, bool, etc., and auto-convert to str for subprocess
1309
+ Run a subprocess command and wait for completion.
1310
+
1311
+ Args:
1312
+ cmd: Command list (non-str items auto-converted to str)
1313
+ cwd: Working directory (default: current directory)
1314
+ logger: Logger instance (default: glogger)
1315
+ check: Whether to raise exception on non-zero exit (default: True)
1316
+ shell: Whether to use shell (default: False)
1317
+ verbose: Whether to log stdout at INFO level vs DEBUG (default: False)
1318
+ useexception: Whether to raise exceptions vs return error info (default: True)
1319
+ env: Environment variables (default: None)
1320
+ hidedoswin: Whether to hide DOS window on Windows (default: True)
1321
+
1322
+ Returns:
1323
+ subprocess.CompletedProcess on success, or SimpleNamespace with error info
1324
+
1325
+ Best practices:
1326
+ - Use useexception=False for optional commands that may fail
1327
+ - Use verbose=True to see subprocess output in logs
1328
+ - Use shell=True only when needed (e.g., shell built-ins, complex commands)
1329
+ - Use check=False with useexception=False for commands where failure is expected
1247
1330
  """
1248
1331
  cmd = [comp if isinstance(comp, str) else str(comp) for comp in cmd]
1249
1332
  logger = logger or glogger
1250
1333
  console_info = logger.info if logger and verbose else logger.debug
1251
- # show cmdline with or without exceptions
1252
- cmd_log = f"""\
1253
- {' '.join(cmd)}
1254
- cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1255
- """
1256
- logger.info(cmd_log)
1334
+
1335
+ # Log command execution
1336
+ _log_subprocess_command(cmd, cwd, logger, "run_cmd")
1257
1337
  try:
1258
1338
  if hidedoswin and PLATFORM == 'Windows':
1259
1339
  startupinfo = subprocess.STARTUPINFO()
@@ -1268,39 +1348,71 @@ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1268
1348
  if stderr_log:
1269
1349
  logger.error(f'stderr:\n{stderr_log}')
1270
1350
  # subprocess started but failed halfway: check=True, proc returns non-zero
1271
- # won't trigger this exception when useexception=True
1272
1351
  except subprocess.CalledProcessError as e:
1273
- # generic error, grandchild_cmd error with noexception enabled
1274
- stdout_log = f'stdout:\n{safe_decode_bytes(e.stdout)}'
1275
- stderr_log = f'stderr:\n{safe_decode_bytes(e.stderr)}'
1276
- logger.info(stdout_log)
1277
- logger.error(stderr_log)
1352
+ stdout_log = safe_decode_bytes(e.stdout)
1353
+ stderr_log = safe_decode_bytes(e.stderr)
1354
+
1355
+ # Log subprocess output with clear separation
1356
+ if stdout_log:
1357
+ logger.info(f'Process stdout:\n{stdout_log}')
1358
+ if stderr_log:
1359
+ logger.error(f'Process stderr:\n{stderr_log}')
1360
+
1361
+ # Log structured error message
1362
+ situation = "Subprocess completed with non-zero exit code"
1363
+ detail = [
1364
+ f"Command: {' '.join(cmd)}",
1365
+ f"Exit code: {e.returncode}",
1366
+ f"Has stdout: {'Yes' if stdout_log else 'No'}",
1367
+ f"Has stderr: {'Yes' if stderr_log else 'No'}"
1368
+ ]
1369
+ error_msg = format_log(situation, detail=detail)
1370
+ logger.error(error_msg)
1371
+
1278
1372
  if useexception:
1279
1373
  raise e
1280
1374
  return types.SimpleNamespace(returncode=1, stdout=e.stdout, stderr=e.stderr)
1281
1375
  # subprocess fails to start
1282
1376
  except Exception as e:
1283
- # cmd missing ...FileNotFound
1284
- # PermissionError, OSError, TimeoutExpired
1285
- logger.error(e)
1286
- if useexception:
1287
- raise e
1288
- return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1377
+ return _log_subprocess_startup_error(e, cmd, logger, useexception)
1289
1378
  return proc
1290
1379
 
1291
1380
 
1292
- def run_daemon(cmd, cwd=None, logger=None, shell=False, useexception=True, env=None, hidedoswin=True):
1381
+ def run_daemon(cmd, cwd=None, logger=None, shell=False, verbose=False, useexception=True, env=None, hidedoswin=True):
1293
1382
  """
1294
- - if returned proc is None, means
1383
+ Start a subprocess in the background (non-blocking).
1384
+
1385
+ Args:
1386
+ cmd: Command list (non-str items auto-converted to str)
1387
+ cwd: Working directory (default: current directory)
1388
+ logger: Logger instance (default: glogger)
1389
+ shell: Whether to use shell (default: False)
1390
+ verbose: Whether to log at INFO level vs DEBUG (default: False)
1391
+ useexception: Whether to raise exceptions vs return error info (default: True)
1392
+ env: Environment variables (default: None)
1393
+ hidedoswin: Whether to hide DOS window on Windows (default: True)
1394
+
1395
+ Returns:
1396
+ subprocess.Popen object on success, or SimpleNamespace with error info
1397
+
1398
+ Best practices:
1399
+ - Use for long-running processes or fire-and-forget commands
1400
+ - Call proc.communicate() or proc.wait() to get final results
1401
+ - Use verbose=True to see command execution in logs
1402
+ - Background processes won't show stdout/stderr in logs automatically
1403
+ - Use useexception=False for optional background processes
1404
+
1405
+ Note:
1406
+ Background processes capture stdout/stderr but don't log them automatically.
1407
+ Call proc.communicate() to retrieve output when the process completes.
1295
1408
  """
1296
1409
  cmd = [comp if isinstance(comp, str) else str(comp) for comp in cmd]
1297
1410
  logger = logger or glogger
1298
- logger.debug(f"""run in background:
1299
- {' '.join(cmd)}
1300
- cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1301
- """)
1302
- # fake the same proc interface
1303
- proc = None
1411
+
1412
+ # Log command execution with appropriate level
1413
+ log_func = logger.info if verbose else logger.debug
1414
+ _log_subprocess_command(cmd, cwd, logger, "run_daemon")
1415
+
1304
1416
  try:
1305
1417
  if hidedoswin and PLATFORM == 'Windows':
1306
1418
  startupinfo = subprocess.STARTUPINFO()
@@ -1308,16 +1420,14 @@ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1308
1420
  proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env, startupinfo=startupinfo)
1309
1421
  else:
1310
1422
  proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env)
1311
- # won't be able to retrieve log from background
1423
+
1424
+ # Log successful startup
1425
+ log_func(f"Background process started successfully (PID: {proc.pid})")
1426
+ return proc
1427
+
1312
1428
  # subprocess fails to start
1313
1429
  except Exception as e:
1314
- # cmd missing ...FileNotFound
1315
- # PermissionError, OSError, TimeoutExpired
1316
- logger.error(e)
1317
- if useexception:
1318
- raise e
1319
- return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1320
- return proc
1430
+ return _log_subprocess_startup_error(e, cmd, logger, useexception)
1321
1431
 
1322
1432
 
1323
1433
  def watch_cmd(cmd, cwd=None, logger=None, shell=False, verbose=False, useexception=True, prompt=None, timeout=None, env=None, hidedoswin=True):
@@ -1330,12 +1440,9 @@ def watch_cmd(cmd, cwd=None, logger=None, shell=False, verbose=False, useexcepti
1330
1440
  output_queue.put(line)
1331
1441
  cmd = [comp if isinstance(comp, str) else str(comp) for comp in cmd]
1332
1442
  logger = logger or glogger
1333
- # show cmdline with or without exceptions
1334
- cmd_log = f"""\
1335
- {' '.join(cmd)}
1336
- cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1337
- """
1338
- logger.info(cmd_log)
1443
+
1444
+ # Log command execution
1445
+ _log_subprocess_command(cmd, cwd, logger, "watch_cmd")
1339
1446
  try:
1340
1447
  if hidedoswin and PLATFORM == 'Windows':
1341
1448
  startupinfo = subprocess.STARTUPINFO()
@@ -1379,11 +1486,7 @@ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1379
1486
  return proc
1380
1487
  # subprocess fails to start
1381
1488
  except Exception as e:
1382
- # no need to have header, exception has it all
1383
- logger.error(e)
1384
- if useexception:
1385
- raise e
1386
- return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1489
+ return _log_subprocess_startup_error(e, cmd, logger, useexception)
1387
1490
 
1388
1491
 
1389
1492
  def extract_call_args(file, caller, callee):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "kkpyutil"
3
- version = "1.44.0"
3
+ version = "1.46.0"
4
4
  description = "Building blocks for sysadmin and DevOps"
5
5
  authors = ["Beinan Li <li.beinan@gmail.com>"]
6
6
  maintainers = ["Beinan Li <li.beinan@gmail.com>"]
File without changes
File without changes