cdx-manager 0.20.8 → 0.20.10

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.
@@ -226,7 +226,15 @@ def install(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, targe
226
226
  # Best-effort, and after the companion is known to be on disk: the shortcut
227
227
  # points at it. A failure here does not fail the install — the tray runs
228
228
  # either way, only its toasts would go nowhere, and doctor reports that.
229
- shortcut = create_shortcut(executable, env=env)
229
+ #
230
+ # Only the install that is about to be live writes it. A staged executable
231
+ # is going to move, and a shortcut recorded against the staged path is a
232
+ # broken Start Menu entry the moment it does; the promotion writes it
233
+ # against the final path instead.
234
+ shortcut = (
235
+ create_shortcut(executable, env=env) if record
236
+ else {"created": False, "path": None, "reason": "staged install; the promotion writes the shortcut"}
237
+ )
230
238
 
231
239
  state = {
232
240
  "version": STATE_VERSION,
@@ -292,9 +300,9 @@ def align_companion(
292
300
  try:
293
301
  result = update(
294
302
  base_dir, version,
295
- download=download, ledger_path=ledger_path, target=target, probe=probe,
303
+ download=download, ledger_path=ledger_path, target=target, probe=probe, env=env,
296
304
  )
297
- except TrayInstallError as error:
305
+ except (TrayInstallError, OSError) as error:
298
306
  outcome = {
299
307
  "aligned": False,
300
308
  "reason": str(error),
@@ -407,7 +415,8 @@ def staged_dir(base_dir):
407
415
  return os.path.join(state_dir(base_dir), "companion.staged")
408
416
 
409
417
 
410
- def update(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, target=None, probe=None):
418
+ def update(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, target=None, probe=None,
419
+ env=None):
411
420
  """Replace an installed companion without ever being left with none.
412
421
 
413
422
  The order is the whole point. The replacement is downloaded, verified and
@@ -444,18 +453,43 @@ def update(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, target
444
453
  live = os.path.join(state_dir(base_dir), "companion")
445
454
  retired = previous_dir(base_dir)
446
455
  shutil.rmtree(retired, ignore_errors=True)
447
- if os.path.isdir(live):
448
- os.rename(live, retired)
449
- os.rename(staged, live)
456
+ retired_live = False
457
+ try:
458
+ if os.path.isdir(live):
459
+ os.rename(live, retired)
460
+ retired_live = True
461
+ os.rename(staged, live)
462
+ except OSError as error:
463
+ # A failure between the two renames used to leave the recorded
464
+ # executable pointing at nothing: the install claimed a live companion
465
+ # and the path was empty. Put the proven one back, and report as a
466
+ # TrayInstallError so the caller's restart path runs.
467
+ restored = True
468
+ if retired_live and not os.path.isdir(live):
469
+ try:
470
+ os.rename(retired, live)
471
+ except OSError:
472
+ restored = False
473
+ shutil.rmtree(staged, ignore_errors=True)
474
+ raise TrayInstallError(
475
+ f"Could not promote the staged tray companion for CDX {version}: {error}. "
476
+ + ("The installed one was restored." if restored
477
+ else f"The previous companion is retained at {retired}.")
478
+ ) from error
450
479
  # The retired copy stays. It used to be deleted here, which left nothing to
451
480
  # go back to the moment the replacement turned out not to start — and that
452
481
  # is precisely when it is needed. The next update clears it, so at most one
453
482
  # extra copy is ever on disk.
454
483
 
484
+ live_executable = fresh["executable"].replace(staged, live, 1)
485
+ # Written now, against the path that will still exist afterwards, and
486
+ # recorded so uninstall keeps owning it.
487
+ shortcut = create_shortcut(live_executable, env=env)
455
488
  state = {
456
489
  **fresh,
457
- "executable": fresh["executable"].replace(staged, live, 1),
458
- "paths": [live],
490
+ "executable": live_executable,
491
+ "paths": [live] + ([shortcut["path"]] if shortcut["created"] else []),
492
+ "shortcut": shortcut["path"] if shortcut["created"] else None,
459
493
  }
460
494
  _record_state(base_dir, state)
461
495
  return {"state": state, "replaced": previous.get("cdx_version"), "previous_state": previous}
@@ -496,17 +530,55 @@ def restore_previous(base_dir, previous_state):
496
530
  return False
497
531
 
498
532
 
533
+ # `--print` exits non-zero when CDX itself is unavailable, and that is a
534
+ # diagnostic that ran, not a companion that failed to launch. These are the
535
+ # codes that mean the program never got to run: the exec conventions for "found
536
+ # but not executable" and "not found". Death by signal is reported as a
537
+ # negative returncode and counts the same way.
538
+ _PROBE_LAUNCH_FAILURE_CODES = (126, 127)
539
+
540
+
541
+ def probe_command(executable):
542
+ """How to ask the companion itself for its diagnostic output.
543
+
544
+ Not `launch_command`: on macOS that hands the bundle to `open`, which takes
545
+ `--print` for itself and reports nothing whatsoever about the companion. The
546
+ binary inside the bundle is what has to run, and an app bundle with no
547
+ runnable binary in it is exactly the case this gate exists to refuse.
548
+ """
549
+ if platform.system() == "Darwin" and executable.endswith(".app"):
550
+ macos_dir = os.path.join(executable, "Contents", "MacOS")
551
+ try:
552
+ entries = sorted(os.listdir(macos_dir))
553
+ except OSError:
554
+ return None
555
+ for entry in entries:
556
+ candidate = os.path.join(macos_dir, entry)
557
+ if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
558
+ return [candidate]
559
+ return None
560
+ return [executable]
561
+
562
+
499
563
  def _probe(executable):
500
- """Does this companion start at all? `--print` exits non-zero when CDX is
501
- unavailable, which is not the question here, so any completed run counts."""
564
+ """Does this companion actually run?
565
+
566
+ `--print` makes it emit its snapshot and exit, so a bundle that cannot
567
+ launch — wrong architecture, missing library, no binary inside it — is
568
+ refused here rather than after the working companion is gone.
569
+ """
570
+ command = probe_command(executable)
571
+ if not command:
572
+ return False
502
573
  try:
503
- subprocess.run( # noqa: S603 (path comes from our own install state)
504
- launch_command(executable) + ["--print"],
574
+ completed = subprocess.run( # noqa: S603 (path comes from our own install state)
575
+ command + ["--print"],
505
576
  capture_output=True, timeout=20, check=False,
506
577
  )
507
578
  except (OSError, subprocess.SubprocessError):
508
579
  return False
509
- return True
580
+ returncode = completed.returncode
581
+ return not (returncode < 0 or returncode in _PROBE_LAUNCH_FAILURE_CODES)
510
582
 
511
583
 
512
584
  def interrupted_update(base_dir):