multiSSH3 6.35__tar.gz → 6.36__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.
Files changed (110) hide show
  1. {multissh3-6.35/multiSSH3.egg-info → multissh3-6.36}/PKG-INFO +3 -3
  2. {multissh3-6.35 → multissh3-6.36}/README.md +2 -2
  3. {multissh3-6.35 → multissh3-6.36}/mssh_complete.py +23 -4
  4. {multissh3-6.35 → multissh3-6.36/multiSSH3.egg-info}/PKG-INFO +3 -3
  5. {multissh3-6.35 → multissh3-6.36}/multiSSH3.py +27 -5
  6. {multissh3-6.35 → multissh3-6.36}/tests/test_harness_smoke.py +13 -0
  7. {multissh3-6.35 → multissh3-6.36}/tests/test_runtime_dir.py +28 -0
  8. {multissh3-6.35 → multissh3-6.36}/LICENSE +0 -0
  9. {multissh3-6.35 → multissh3-6.36}/MANIFEST.in +0 -0
  10. {multissh3-6.35 → multissh3-6.36}/multiSSH3.egg-info/SOURCES.txt +0 -0
  11. {multissh3-6.35 → multissh3-6.36}/multiSSH3.egg-info/dependency_links.txt +0 -0
  12. {multissh3-6.35 → multissh3-6.36}/multiSSH3.egg-info/entry_points.txt +0 -0
  13. {multissh3-6.35 → multissh3-6.36}/multiSSH3.egg-info/top_level.txt +0 -0
  14. {multissh3-6.35 → multissh3-6.36}/pytest.ini +0 -0
  15. {multissh3-6.35 → multissh3-6.36}/setup.cfg +0 -0
  16. {multissh3-6.35 → multissh3-6.36}/setup.py +0 -0
  17. {multissh3-6.35 → multissh3-6.36}/tests/bench_complete.py +0 -0
  18. {multissh3-6.35 → multissh3-6.36}/tests/complete_shell_harness.py +0 -0
  19. {multissh3-6.35 → multissh3-6.36}/tests/conftest.py +0 -0
  20. {multissh3-6.35 → multissh3-6.36}/tests/data/run_command_golden.json +0 -0
  21. {multissh3-6.35 → multissh3-6.36}/tests/shells/Dockerfile.bash5.1 +0 -0
  22. {multissh3-6.35 → multissh3-6.36}/tests/shells/Dockerfile.bash5.3 +0 -0
  23. {multissh3-6.35 → multissh3-6.36}/tests/shells/Dockerfile.centos7 +0 -0
  24. {multissh3-6.35 → multissh3-6.36}/tests/shells/Dockerfile.current +0 -0
  25. {multissh3-6.35 → multissh3-6.36}/tests/shells/Dockerfile.legacy +0 -0
  26. {multissh3-6.35 → multissh3-6.36}/tests/shells/Dockerfile.modern +0 -0
  27. {multissh3-6.35 → multissh3-6.36}/tests/shells/run_matrix.sh +0 -0
  28. {multissh3-6.35 → multissh3-6.36}/tests/test_cli_branches.py +0 -0
  29. {multissh3-6.35 → multissh3-6.36}/tests/test_cli_option_words.py +0 -0
  30. {multissh3-6.35 → multissh3-6.36}/tests/test_cli_processing.py +0 -0
  31. {multissh3-6.35 → multissh3-6.36}/tests/test_closed_output.py +0 -0
  32. {multissh3-6.35 → multissh3-6.36}/tests/test_color_helpers.py +0 -0
  33. {multissh3-6.35 → multissh3-6.36}/tests/test_command_terminal.py +0 -0
  34. {multissh3-6.35 → multissh3-6.36}/tests/test_compact_hostnames.py +0 -0
  35. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_blesh.py +0 -0
  36. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_cli.py +0 -0
  37. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_context.py +0 -0
  38. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_diagnose.py +0 -0
  39. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_editor.py +0 -0
  40. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_engine.py +0 -0
  41. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_eval.py +0 -0
  42. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_hosts.py +0 -0
  43. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_hoststate.py +0 -0
  44. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_index.py +0 -0
  45. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_keys.py +0 -0
  46. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_lex.py +0 -0
  47. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_mssh_helpers.py +0 -0
  48. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_password_spellings.py +0 -0
  49. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_picker.py +0 -0
  50. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_probe.py +0 -0
  51. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_shells.py +0 -0
  52. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_start.py +0 -0
  53. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_store.py +0 -0
  54. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_suggest.py +0 -0
  55. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_terminal.py +0 -0
  56. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_untrusted.py +0 -0
  57. {multissh3-6.35 → multissh3-6.36}/tests/test_complete_zsh_options.py +0 -0
  58. {multissh3-6.35 → multissh3-6.36}/tests/test_config_cli.py +0 -0
  59. {multissh3-6.35 → multissh3-6.36}/tests/test_config_validate.py +0 -0
  60. {multissh3-6.35 → multissh3-6.36}/tests/test_config_values.py +0 -0
  61. {multissh3-6.35 → multissh3-6.36}/tests/test_control_master_config.py +0 -0
  62. {multissh3-6.35 → multissh3-6.36}/tests/test_copy_id.py +0 -0
  63. {multissh3-6.35 → multissh3-6.36}/tests/test_curses_branches.py +0 -0
  64. {multissh3-6.35 → multissh3-6.36}/tests/test_curses_event_loop.py +0 -0
  65. {multissh3-6.35 → multissh3-6.36}/tests/test_curses_tui.py +0 -0
  66. {multissh3-6.35 → multissh3-6.36}/tests/test_curses_wide_chars.py +0 -0
  67. {multissh3-6.35 → multissh3-6.36}/tests/test_expand_hostnames.py +0 -0
  68. {multissh3-6.35 → multissh3-6.36}/tests/test_file_trust.py +0 -0
  69. {multissh3-6.35 → multissh3-6.36}/tests/test_helpers_extra.py +0 -0
  70. {multissh3-6.35 → multissh3-6.36}/tests/test_history_replay.py +0 -0
  71. {multissh3-6.35 → multissh3-6.36}/tests/test_host.py +0 -0
  72. {multissh3-6.35 → multissh3-6.36}/tests/test_host_validity.py +0 -0
  73. {multissh3-6.35 → multissh3-6.36}/tests/test_hosts_file.py +0 -0
  74. {multissh3-6.35 → multissh3-6.36}/tests/test_identities_only.py +0 -0
  75. {multissh3-6.35 → multissh3-6.36}/tests/test_ipmi_lockout_wait.py +0 -0
  76. {multissh3-6.35 → multissh3-6.36}/tests/test_ipmi_logins.py +0 -0
  77. {multissh3-6.35 → multissh3-6.36}/tests/test_key_path_home.py +0 -0
  78. {multissh3-6.35 → multissh3-6.36}/tests/test_login_drop_retry.py +0 -0
  79. {multissh3-6.35 → multissh3-6.36}/tests/test_main_lifecycle.py +0 -0
  80. {multissh3-6.35 → multissh3-6.36}/tests/test_narrow_terminal.py +0 -0
  81. {multissh3-6.35 → multissh3-6.36}/tests/test_no_hosts.py +0 -0
  82. {multissh3-6.35 → multissh3-6.36}/tests/test_option_no_effect.py +0 -0
  83. {multissh3-6.35 → multissh3-6.36}/tests/test_options_after_command.py +0 -0
  84. {multissh3-6.35 → multissh3-6.36}/tests/test_output_merge.py +0 -0
  85. {multissh3-6.35 → multissh3-6.36}/tests/test_output_robustness.py +0 -0
  86. {multissh3-6.35 → multissh3-6.36}/tests/test_packaging.py +0 -0
  87. {multissh3-6.35 → multissh3-6.36}/tests/test_password_handling.py +0 -0
  88. {multissh3-6.35 → multissh3-6.36}/tests/test_passwords_out_of_ps.py +0 -0
  89. {multissh3-6.35 → multissh3-6.36}/tests/test_run_command.py +0 -0
  90. {multissh3-6.35 → multissh3-6.36}/tests/test_run_command_branches.py +0 -0
  91. {multissh3-6.35 → multissh3-6.36}/tests/test_run_command_characterization.py +0 -0
  92. {multissh3-6.35 → multissh3-6.36}/tests/test_run_command_exit.py +0 -0
  93. {multissh3-6.35 → multissh3-6.36}/tests/test_run_on_hosts.py +0 -0
  94. {multissh3-6.35 → multissh3-6.36}/tests/test_run_on_hosts_modes.py +0 -0
  95. {multissh3-6.35 → multissh3-6.36}/tests/test_runtime_branch_matrix.py +0 -0
  96. {multissh3-6.35 → multissh3-6.36}/tests/test_shared_lines.py +0 -0
  97. {multissh3-6.35 → multissh3-6.36}/tests/test_smoke_optional.py +0 -0
  98. {multissh3-6.35 → multissh3-6.36}/tests/test_ssh_error_text.py +0 -0
  99. {multissh3-6.35 → multissh3-6.36}/tests/test_ssh_timeouts.py +0 -0
  100. {multissh3-6.35 → multissh3-6.36}/tests/test_sshpass_exit.py +0 -0
  101. {multissh3-6.35 → multissh3-6.36}/tests/test_start_run_on_hosts.py +0 -0
  102. {multissh3-6.35 → multissh3-6.36}/tests/test_stopped_commands.py +0 -0
  103. {multissh3-6.35 → multissh3-6.36}/tests/test_stream_reader.py +0 -0
  104. {multissh3-6.35 → multissh3-6.36}/tests/test_streamed_output.py +0 -0
  105. {multissh3-6.35 → multissh3-6.36}/tests/test_tui_ssh_notices.py +0 -0
  106. {multissh3-6.35 → multissh3-6.36}/tests/test_unavailable_hosts.py +0 -0
  107. {multissh3-6.35 → multissh3-6.36}/tests/test_underscore_settings.py +0 -0
  108. {multissh3-6.35 → multissh3-6.36}/tests/test_waiting.py +0 -0
  109. {multissh3-6.35 → multissh3-6.36}/tests/test_wide_wrap.py +0 -0
  110. {multissh3-6.35 → multissh3-6.36}/tests/vt_grid.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.35
3
+ Version: 6.36
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -826,7 +826,7 @@ mssh -f ./data/ allServers /tmp/data/
826
826
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
827
827
  - Unreachable hosts will be tried again when their timeout expires.
828
828
  - A skipped host is not run and counts as a failed host: it shows return code 255 ( what ssh returns when it cannot connect ) with a line saying it was skipped, and the summary lists it after `failed_hosts:` as `Skipped N hosts unreachable in a recent run, without trying them: ...`. So mssh does not exit 0 or print `Success` when it skipped a host.
829
- - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to). If none can be used, they are only kept for the current run.
829
+ - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to, or could make). mssh reads them back from that one only, not from a later place an older session left them in. If none can be used, they are only kept for the current run.
830
830
  - To force mssh to not use unreachable hosts from previous runs, you can use `-a, -nsu, --no_skip_unreachable` to set `skip_unreachable` to false.
831
831
 
832
832
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -974,7 +974,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
974
974
 
975
975
  Putting the command after `--` is no substitute for quotes: every word after the hosts is a command of its own, so `mssh web1 -- bash -c 'echo hi'` runs `bash`, `-c` and `echo hi` one after another (when each word is a single word, as in `mssh web1 -- ls -la`, mssh asks whether to join them).
976
976
 
977
- Below is a sample help message output from multiSSH3 6.35 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
977
+ Below is a sample help message output from multiSSH3 6.36 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
978
978
 
979
979
  ```bash
980
980
  $ mssh -h
@@ -800,7 +800,7 @@ mssh -f ./data/ allServers /tmp/data/
800
800
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
801
801
  - Unreachable hosts will be tried again when their timeout expires.
802
802
  - A skipped host is not run and counts as a failed host: it shows return code 255 ( what ssh returns when it cannot connect ) with a line saying it was skipped, and the summary lists it after `failed_hosts:` as `Skipped N hosts unreachable in a recent run, without trying them: ...`. So mssh does not exit 0 or print `Success` when it skipped a host.
803
- - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to). If none can be used, they are only kept for the current run.
803
+ - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to, or could make). mssh reads them back from that one only, not from a later place an older session left them in. If none can be used, they are only kept for the current run.
804
804
  - To force mssh to not use unreachable hosts from previous runs, you can use `-a, -nsu, --no_skip_unreachable` to set `skip_unreachable` to false.
805
805
 
806
806
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -948,7 +948,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
948
948
 
949
949
  Putting the command after `--` is no substitute for quotes: every word after the hosts is a command of its own, so `mssh web1 -- bash -c 'echo hi'` runs `bash`, `-c` and `echo hi` one after another (when each word is a single word, as in `mssh web1 -- ls -la`, mssh asks whether to join them).
950
950
 
951
- Below is a sample help message output from multiSSH3 6.35 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
951
+ Below is a sample help message output from multiSSH3 6.36 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
952
952
 
953
953
  ```bash
954
954
  $ mssh -h
@@ -16,7 +16,7 @@ import sys
16
16
  import time
17
17
  from bisect import bisect_left
18
18
 
19
- version = '0.8'
19
+ version = '0.9'
20
20
  __version__ = version
21
21
  PROTOCOL = 'MSSHC1'
22
22
  MSSH_NAMES = ('mssh', 'mssh3', 'multissh', 'multissh3', 'multiSSH3')
@@ -1450,6 +1450,21 @@ def _private_dir(path, create=True):
1450
1450
  return False
1451
1451
  return stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid() and not st.st_mode & 0o077
1452
1452
 
1453
+ def _could_be_private_dir(path):
1454
+ '''Whether _private_dir(path, create=True) would take path, without changing anything (as multiSSH3's).'''
1455
+ import stat
1456
+ try:
1457
+ st = os.lstat(path)
1458
+ except FileNotFoundError:
1459
+ return os.access(os.path.dirname(path), os.W_OK | os.X_OK)
1460
+ except OSError:
1461
+ return False
1462
+ if not stat.S_ISDIR(st.st_mode):
1463
+ return False
1464
+ if not hasattr(os, 'getuid'):
1465
+ return True
1466
+ return st.st_uid == os.getuid() and not st.st_mode & 0o022
1467
+
1453
1468
  def _owned_and_not_writable_by_others(path):
1454
1469
  if not hasattr(os, 'getuid'):
1455
1470
  return True
@@ -1468,7 +1483,8 @@ def cache_dir(create=True):
1468
1483
  mssh's private runtime directory, where unavailable_hosts.csv and the index live, or None.
1469
1484
 
1470
1485
  Exactly as multiSSH3.runtime_dir picks it: $XDG_RUNTIME_DIR/multiSSH3 when that runtime dir
1471
- is ours, /run/user/<uid>/multiSSH3, /dev/shm/multiSSH3-<uid>, then <tempdir>/multiSSH3-<uid>.
1486
+ is ours, /run/user/<uid>/multiSSH3, /dev/shm/multiSSH3-<uid>, then <tempdir>/multiSSH3-<uid>;
1487
+ without create, the one a write would use if it is there already, else None.
1472
1488
  '''
1473
1489
  import tempfile
1474
1490
  if hasattr(os, 'getuid'):
@@ -1488,8 +1504,11 @@ def cache_dir(create=True):
1488
1504
  candidates.append(os.path.join(_SHM_DIR, 'multiSSH3-{}'.format(user_id)))
1489
1505
  candidates.append(os.path.join(tempfile.gettempdir(), 'multiSSH3-{}'.format(user_id)))
1490
1506
  for directory in candidates:
1491
- if _private_dir(directory, create):
1492
- return directory
1507
+ if create:
1508
+ if _private_dir(directory):
1509
+ return directory
1510
+ elif _could_be_private_dir(directory):
1511
+ return directory if _private_dir(directory, create=False) else None
1493
1512
  return None
1494
1513
 
1495
1514
  def _debug(message):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: multiSSH3
3
- Version: 6.35
3
+ Version: 6.36
4
4
  Summary: Run commands on multiple hosts via SSH
5
5
  Home-page: https://github.com/yufei-pan/multiSSH3
6
6
  Author: Yufei Pan
@@ -826,7 +826,7 @@ mssh -f ./data/ allServers /tmp/data/
826
826
  - By default mssh set this to true to speed up operations on large host lists with some unreachable hosts.
827
827
  - Unreachable hosts will be tried again when their timeout expires.
828
828
  - A skipped host is not run and counts as a failed host: it shows return code 255 ( what ssh returns when it cannot connect ) with a line saying it was skipped, and the summary lists it after `failed_hosts:` as `Skipped N hosts unreachable in a recent run, without trying them: ...`. So mssh does not exit 0 or print `Success` when it skipped a host.
829
- - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to). If none can be used, they are only kept for the current run.
829
+ - mssh stores the current run unreachable hosts in memory and if `skip_unreachable` is true, it will store them in `unavailable_hosts.csv` in mssh's private runtime directory: `$XDG_RUNTIME_DIR/multiSSH3`, else `/run/user/<uid>/multiSSH3`, `/dev/shm/multiSSH3-<uid>`, or a `multiSSH3-<uid>` folder in the system temp folder (the first that only you can write to, or could make). mssh reads them back from that one only, not from a later place an older session left them in. If none can be used, they are only kept for the current run.
830
830
  - To force mssh to not use unreachable hosts from previous runs, you can use `-a, -nsu, --no_skip_unreachable` to set `skip_unreachable` to false.
831
831
 
832
832
  ### `-a, -nsu, --no_skip_unreachable` | not _`DEFAULT_SKIP_UNREACHABLE`_
@@ -974,7 +974,7 @@ Option names count in full only: `--user` is not `--username` and `-c` is not `-
974
974
 
975
975
  Putting the command after `--` is no substitute for quotes: every word after the hosts is a command of its own, so `mssh web1 -- bash -c 'echo hi'` runs `bash`, `-c` and `echo hi` one after another (when each word is a single word, as in `mssh web1 -- ls -la`, mssh asks whether to join them).
976
976
 
977
- Below is a sample help message output from multiSSH3 6.35 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
977
+ Below is a sample help message output from multiSSH3 6.36 @ 2026-10-16 (no config file, 120 columns; the lines after the options list the programs found on that machine)
978
978
 
979
979
  ```bash
980
980
  $ mssh -h
@@ -41,7 +41,7 @@ from itertools import count, product
41
41
  from pprint import pformat
42
42
 
43
43
 
44
- version = '6.35'
44
+ version = '6.36'
45
45
  VERSION = version
46
46
  __version__ = version
47
47
  COMMIT_DATE = '2026-10-16'
@@ -6588,6 +6588,24 @@ def _private_dir(path, create=True):
6588
6588
  return False
6589
6589
  return stat.S_ISDIR(st.st_mode) and st.st_uid == os.getuid() and not st.st_mode & 0o077
6590
6590
 
6591
+ def _could_be_private_dir(path):
6592
+ '''
6593
+ Whether _private_dir(path, create=True) would take path, without changing anything: a directory
6594
+ ours and only ours to write to (it makes one readable by others 0700), or none yet where we can
6595
+ make it.
6596
+ '''
6597
+ try:
6598
+ st = os.lstat(path)
6599
+ except FileNotFoundError:
6600
+ return os.access(os.path.dirname(path), os.W_OK | os.X_OK)
6601
+ except OSError:
6602
+ return False
6603
+ if not stat.S_ISDIR(st.st_mode):
6604
+ return False
6605
+ if not hasattr(os, 'getuid'):
6606
+ return True
6607
+ return st.st_uid == os.getuid() and not st.st_mode & 0o022
6608
+
6591
6609
  def _owned_and_not_writable_by_others(path):
6592
6610
  '''Whether path exists, is owned by us and only we can write to it.'''
6593
6611
  if not hasattr(os, 'getuid'):
@@ -6606,8 +6624,9 @@ def runtime_dir(create=True):
6606
6624
  '''
6607
6625
  mssh's private directory for state that must not outlive a reboot, or None to run without it.
6608
6626
 
6609
- create: set up the directory (for writing). Without it only an existing one is used, so
6610
- reading changes nothing on disk.
6627
+ create: set up the directory (for writing). Without it, the directory a write would use if it
6628
+ is there already, else None: reading changes nothing on disk, and never reads a later place (an
6629
+ older session's state, or this machine's own when $XDG_RUNTIME_DIR is set to a new directory).
6611
6630
 
6612
6631
  The first of these that is a directory only we control (_private_dir):
6613
6632
  1. $XDG_RUNTIME_DIR/multiSSH3, when $XDG_RUNTIME_DIR is ours and only we can write to it
@@ -6631,8 +6650,11 @@ def runtime_dir(create=True):
6631
6650
  candidates.append(os.path.join(_SHM_DIR, 'multiSSH3-{}'.format(user_id)))
6632
6651
  candidates.append(os.path.join(tempfile.gettempdir(), 'multiSSH3-{}'.format(user_id)))
6633
6652
  for directory in candidates:
6634
- if _private_dir(directory, create):
6635
- return directory
6653
+ if create:
6654
+ if _private_dir(directory):
6655
+ return directory
6656
+ elif _could_be_private_dir(directory):
6657
+ return directory if _private_dir(directory, create=False) else None
6636
6658
  return None
6637
6659
 
6638
6660
  def _unavailable_hosts_file_path(create=True):
@@ -123,6 +123,19 @@ def test_the_tests_never_use_the_real_runtime_dir(monkeypatch, tmp_path_factory,
123
123
  assert multiSSH3._unavailable_hosts_file_path().startswith(base + os.sep)
124
124
 
125
125
 
126
+ def test_mssh_started_by_a_test_never_reads_the_real_runtime_dir(tmp_path):
127
+ # a test running mssh in a process of its own gives it a new $XDG_RUNTIME_DIR, and reading the
128
+ # unreachable hosts fell back to /run/user/<uid>/multiSSH3: hosts this machine's own runs found
129
+ # unreachable were skipped there
130
+ runtime = tmp_path / "run"
131
+ runtime.mkdir(mode=0o700)
132
+ child = subprocess.run([sys.executable, "-c", "import multiSSH3; print(multiSSH3._unavailable_hosts_file_path(create=False))"],
133
+ stdout=subprocess.PIPE, universal_newlines=True, check=True, cwd=os.path.dirname(os.path.abspath(multiSSH3.__file__)),
134
+ env=dict(os.environ, XDG_RUNTIME_DIR=str(runtime)))
135
+
136
+ assert child.stdout.strip() == "None"
137
+
138
+
126
139
  def test_the_tests_never_see_this_machine_s_host_names():
127
140
  # its env files (/etc/profile.d/hosts.sh) and hosts file
128
141
  assert multiSSH3.readEnvFromFile() == {}
@@ -76,6 +76,34 @@ def test_a_place_someone_else_controls_is_skipped(places, monkeypatch, planted):
76
76
  places['shm'].mkdir(mode=0o700)
77
77
  places['shm'].chmod(0o777)
78
78
  assert multiSSH3.runtime_dir() == mc.cache_dir() == str(places['temp'])
79
+ assert multiSSH3.runtime_dir(create=False) == mc.cache_dir(create=False) == str(places['temp'])
80
+
81
+
82
+ @posix_only
83
+ @pytest.mark.parametrize('holding', ['run_user', 'shm', 'temp'])
84
+ def test_reading_looks_only_where_a_write_goes(places, holding):
85
+ # a later place's state (an older session's, or this machine's own while a test or a script sets
86
+ # $XDG_RUNTIME_DIR to a new directory) is not read while the place a write goes to is not made yet
87
+ places[holding].mkdir(mode=0o700)
88
+ (places[holding] / 'unavailable_hosts.csv').write_text('#boot_id,x\n')
89
+
90
+ assert multiSSH3.runtime_dir(create=False) is None and mc.cache_dir(create=False) is None
91
+ assert multiSSH3._unavailable_hosts_file_path(create=False) is None
92
+ assert not places['xdg'].exists()
93
+ # once made, it is read
94
+ assert multiSSH3.runtime_dir() == str(places['xdg'])
95
+ assert multiSSH3.runtime_dir(create=False) == mc.cache_dir(create=False) == str(places['xdg'])
96
+
97
+
98
+ @posix_only
99
+ def test_reading_does_not_pass_over_a_place_a_write_would_tighten(places):
100
+ # ours, but readable by others: a write makes it 0700 and uses it
101
+ places['xdg'].mkdir(mode=0o700)
102
+ places['xdg'].chmod(0o755)
103
+ places['run_user'].mkdir(mode=0o700)
104
+
105
+ assert multiSSH3.runtime_dir(create=False) is None and mc.cache_dir(create=False) is None
106
+ assert multiSSH3.runtime_dir() == mc.cache_dir() == str(places['xdg'])
79
107
 
80
108
 
81
109
  @posix_only
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes