kkpyutil 1.45.0__tar.gz → 1.46.1__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.45.0
3
+ Version: 1.46.1
4
4
  Summary: Building blocks for sysadmin and DevOps
5
5
  Home-page: https://github.com/kakyoism/kkpyutil/
6
6
  License: MIT
@@ -581,7 +581,7 @@ glogger.setLevel(logging.DEBUG)
581
581
  def catch_unknown_exception(exc_type, exc_value, exc_traceback):
582
582
  """Global exception to handle uncaught exceptions"""
583
583
  exc_info = exc_type, exc_value, exc_traceback
584
- glogger.error('Unhandled exception: ', exc_info=exc_info)
584
+ glogger.error('Unhandled exception:', exc_info=exc_info)
585
585
  # _logger.exception('Unhandled exception: ') # try-except block only.
586
586
  # sys.__excepthook__(*exc_info) # Keep commented out to avoid msg dup.
587
587
 
@@ -1255,23 +1255,84 @@ def save_winreg_record(full_key, var, value, value_type=winreg.REG_EXPAND_SZ if
1255
1255
  winreg.SetValueEx(key, var, 0, value_type, value)
1256
1256
 
1257
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
+ logger.info(cmd_log)
1265
+
1266
+
1267
+ def _log_subprocess_startup_error(e, cmd, logger, useexception=True):
1268
+ """Helper function to log subprocess startup errors with structured format"""
1269
+ error_type = type(e).__name__
1270
+ cmd_str = ' '.join(cmd)
1271
+
1272
+ # Create structured error message based on error type
1273
+ if isinstance(e, FileNotFoundError):
1274
+ situation = "Command not found"
1275
+ detail = [f"Command: {cmd_str}", f"Error: {error_type}"]
1276
+ advice = [
1277
+ "Check if the command is installed and available in PATH",
1278
+ "Verify the command name spelling",
1279
+ "Use absolute path if the command is in a specific location"
1280
+ ]
1281
+ elif isinstance(e, PermissionError):
1282
+ situation = "Permission denied"
1283
+ detail = [f"Command: {cmd_str}", f"Error: {error_type}"]
1284
+ advice = [
1285
+ "Check if you have permission to execute the command",
1286
+ "Try running with elevated privileges if necessary",
1287
+ "Verify file permissions on the executable"
1288
+ ]
1289
+ else:
1290
+ situation = "Subprocess failed to start"
1291
+ detail = [f"Command: {cmd_str}", f"Error: {error_type}: {str(e)}"]
1292
+ advice = [
1293
+ "Check if the command exists and is executable",
1294
+ "Verify all command arguments are valid",
1295
+ "Check system resources and environment"
1296
+ ]
1297
+
1298
+ error_msg = format_log(situation, detail=detail, advice=advice)
1299
+ logger.error(error_msg)
1300
+
1301
+ if useexception:
1302
+ raise e
1303
+ return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1304
+
1305
+
1258
1306
  def run_cmd(cmd, cwd=None, logger=None, check=True, shell=False, verbose=False, useexception=True, env=None, hidedoswin=True):
1259
1307
  """
1260
- - Use shell==True with autotools where new shell is needed to treat the entire command option sequence as a command,
1261
- e.g., shell=True means running sh -c ./configure CFLAGS="..."
1262
- - we do not use check=False to supress exception because that'd leave app no way to tell if child-proc succeeded or not
1263
- - instead, we catch CallProcessError but avoid rethrow, and then return error code and other key diagnostics to app
1264
- - allow user to input non-str options, e.g., int, bool, etc., and auto-convert to str for subprocess
1308
+ Run a subprocess command and wait for completion.
1309
+
1310
+ Args:
1311
+ cmd: Command list (non-str items auto-converted to str)
1312
+ cwd: Working directory (default: current directory)
1313
+ logger: Logger instance (default: glogger)
1314
+ check: Whether to raise exception on non-zero exit (default: True)
1315
+ shell: Whether to use shell (default: False)
1316
+ verbose: Whether to log stdout at INFO level vs DEBUG (default: False)
1317
+ useexception: Whether to raise exceptions vs return error info (default: True)
1318
+ env: Environment variables (default: None)
1319
+ hidedoswin: Whether to hide DOS window on Windows (default: True)
1320
+
1321
+ Returns:
1322
+ subprocess.CompletedProcess on success, or SimpleNamespace with error info
1323
+
1324
+ Best practices:
1325
+ - Use useexception=False for optional commands that may fail
1326
+ - Use verbose=True to see subprocess output in logs
1327
+ - Use shell=True only when needed (e.g., shell built-ins, complex commands)
1328
+ - Use check=False with useexception=False for commands where failure is expected
1265
1329
  """
1266
1330
  cmd = [comp if isinstance(comp, str) else str(comp) for comp in cmd]
1267
1331
  logger = logger or glogger
1268
1332
  console_info = logger.info if logger and verbose else logger.debug
1269
- # show cmdline with or without exceptions
1270
- cmd_log = f"""\
1271
- {' '.join(cmd)}
1272
- cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1273
- """
1274
- logger.info(cmd_log)
1333
+
1334
+ # Log command execution
1335
+ _log_subprocess_command(cmd, cwd, logger, "run_cmd")
1275
1336
  try:
1276
1337
  if hidedoswin and PLATFORM == 'Windows':
1277
1338
  startupinfo = subprocess.STARTUPINFO()
@@ -1282,43 +1343,75 @@ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1282
1343
  stdout_log = safe_decode_bytes(proc.stdout)
1283
1344
  stderr_log = safe_decode_bytes(proc.stderr)
1284
1345
  if stdout_log:
1285
- console_info(f'stdout:\n{stdout_log}')
1346
+ console_info(f'stdout:\n{stdout_log.rstrip()}')
1286
1347
  if stderr_log:
1287
- logger.error(f'stderr:\n{stderr_log}')
1348
+ logger.error(f'stderr:\n{stderr_log.rstrip()}')
1288
1349
  # subprocess started but failed halfway: check=True, proc returns non-zero
1289
- # won't trigger this exception when useexception=True
1290
1350
  except subprocess.CalledProcessError as e:
1291
- # generic error, grandchild_cmd error with noexception enabled
1292
- stdout_log = f'stdout:\n{safe_decode_bytes(e.stdout)}'
1293
- stderr_log = f'stderr:\n{safe_decode_bytes(e.stderr)}'
1294
- logger.info(stdout_log)
1295
- logger.error(stderr_log)
1351
+ stdout_log = safe_decode_bytes(e.stdout)
1352
+ stderr_log = safe_decode_bytes(e.stderr)
1353
+
1354
+ # Log subprocess output with clear separation
1355
+ if stdout_log:
1356
+ logger.info(f'stdout:\n{stdout_log.rstrip()}')
1357
+ if stderr_log:
1358
+ logger.error(f'stderr:\n{stderr_log.rstrip()}')
1359
+
1360
+ # Log structured error message
1361
+ situation = "Subprocess completed with non-zero exit code"
1362
+ detail = [
1363
+ f"Command: {' '.join(cmd)}",
1364
+ f"Exit code: {e.returncode}",
1365
+ f"Has stdout: {'Yes' if stdout_log else 'No'}",
1366
+ f"Has stderr: {'Yes' if stderr_log else 'No'}"
1367
+ ]
1368
+ error_msg = format_log(situation, detail=detail)
1369
+ logger.error(error_msg)
1370
+
1296
1371
  if useexception:
1297
1372
  raise e
1298
1373
  return types.SimpleNamespace(returncode=1, stdout=e.stdout, stderr=e.stderr)
1299
1374
  # subprocess fails to start
1300
1375
  except Exception as e:
1301
- # cmd missing ...FileNotFound
1302
- # PermissionError, OSError, TimeoutExpired
1303
- logger.error(e)
1304
- if useexception:
1305
- raise e
1306
- return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1376
+ return _log_subprocess_startup_error(e, cmd, logger, useexception)
1307
1377
  return proc
1308
1378
 
1309
1379
 
1310
- def run_daemon(cmd, cwd=None, logger=None, shell=False, useexception=True, env=None, hidedoswin=True):
1380
+ def run_daemon(cmd, cwd=None, logger=None, shell=False, verbose=False, useexception=True, env=None, hidedoswin=True):
1311
1381
  """
1312
- - if returned proc is None, means
1382
+ Start a subprocess in the background (non-blocking).
1383
+
1384
+ Args:
1385
+ cmd: Command list (non-str items auto-converted to str)
1386
+ cwd: Working directory (default: current directory)
1387
+ logger: Logger instance (default: glogger)
1388
+ shell: Whether to use shell (default: False)
1389
+ verbose: Whether to log at INFO level vs DEBUG (default: False)
1390
+ useexception: Whether to raise exceptions vs return error info (default: True)
1391
+ env: Environment variables (default: None)
1392
+ hidedoswin: Whether to hide DOS window on Windows (default: True)
1393
+
1394
+ Returns:
1395
+ subprocess.Popen object on success, or SimpleNamespace with error info
1396
+
1397
+ Best practices:
1398
+ - Use for long-running processes or fire-and-forget commands
1399
+ - Call proc.communicate() or proc.wait() to get final results
1400
+ - Use verbose=True to see command execution in logs
1401
+ - Background processes won't show stdout/stderr in logs automatically
1402
+ - Use useexception=False for optional background processes
1403
+
1404
+ Note:
1405
+ Background processes capture stdout/stderr but don't log them automatically.
1406
+ Call proc.communicate() to retrieve output when the process completes.
1313
1407
  """
1314
1408
  cmd = [comp if isinstance(comp, str) else str(comp) for comp in cmd]
1315
1409
  logger = logger or glogger
1316
- logger.debug(f"""run in background:
1317
- {' '.join(cmd)}
1318
- cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1319
- """)
1320
- # fake the same proc interface
1321
- proc = None
1410
+
1411
+ # Log command execution with appropriate level
1412
+ log_func = logger.info if verbose else logger.debug
1413
+ _log_subprocess_command(cmd, cwd, logger, "run_daemon")
1414
+
1322
1415
  try:
1323
1416
  if hidedoswin and PLATFORM == 'Windows':
1324
1417
  startupinfo = subprocess.STARTUPINFO()
@@ -1326,16 +1419,14 @@ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1326
1419
  proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env, startupinfo=startupinfo)
1327
1420
  else:
1328
1421
  proc = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env)
1329
- # won't be able to retrieve log from background
1422
+
1423
+ # Log successful startup
1424
+ log_func(f"Background process started successfully (PID: {proc.pid})")
1425
+ return proc
1426
+
1330
1427
  # subprocess fails to start
1331
1428
  except Exception as e:
1332
- # cmd missing ...FileNotFound
1333
- # PermissionError, OSError, TimeoutExpired
1334
- logger.error(e)
1335
- if useexception:
1336
- raise e
1337
- return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1338
- return proc
1429
+ return _log_subprocess_startup_error(e, cmd, logger, useexception)
1339
1430
 
1340
1431
 
1341
1432
  def watch_cmd(cmd, cwd=None, logger=None, shell=False, verbose=False, useexception=True, prompt=None, timeout=None, env=None, hidedoswin=True):
@@ -1348,12 +1439,9 @@ def watch_cmd(cmd, cwd=None, logger=None, shell=False, verbose=False, useexcepti
1348
1439
  output_queue.put(line)
1349
1440
  cmd = [comp if isinstance(comp, str) else str(comp) for comp in cmd]
1350
1441
  logger = logger or glogger
1351
- # show cmdline with or without exceptions
1352
- cmd_log = f"""\
1353
- {' '.join(cmd)}
1354
- cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1355
- """
1356
- logger.info(cmd_log)
1442
+
1443
+ # Log command execution
1444
+ _log_subprocess_command(cmd, cwd, logger, "watch_cmd")
1357
1445
  try:
1358
1446
  if hidedoswin and PLATFORM == 'Windows':
1359
1447
  startupinfo = subprocess.STARTUPINFO()
@@ -1397,11 +1485,7 @@ cwd: {osp.abspath(cwd) if cwd else os.getcwd()}
1397
1485
  return proc
1398
1486
  # subprocess fails to start
1399
1487
  except Exception as e:
1400
- # no need to have header, exception has it all
1401
- logger.error(e)
1402
- if useexception:
1403
- raise e
1404
- return types.SimpleNamespace(returncode=2, stdout='', stderr=safe_encode_text(str(e), encoding=LOCALE_CODEC))
1488
+ return _log_subprocess_startup_error(e, cmd, logger, useexception)
1405
1489
 
1406
1490
 
1407
1491
  def extract_call_args(file, caller, callee):
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "kkpyutil"
3
- version = "1.45.0"
3
+ version = "1.46.1"
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