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.
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/PKG-INFO +1 -1
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/kkpyutil.py +153 -50
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/pyproject.toml +1 -1
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/LICENSE +0 -0
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/README.md +0 -0
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/kkpyutil_helper/windows/kkttssave.ps1 +0 -0
- {kkpyutil-1.44.0 → kkpyutil-1.46.0}/kkpyutil_helper/windows/kkttsspeak.ps1 +0 -0
|
@@ -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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
""
|
|
1302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|