Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ to the StandTerm server process across page reloads.
[Download and install StandTerm Desktop](#desktop-downloads-evaluation), or use
the [browser-based Core quick start](#quick-start).

**Core 2.12.0** is a [source release](https://github.com/askac/standterm/releases/tag/v2.12.0).
It adds SSH Agent Tunnel with shared skills and helpers, Agent Panel permission
**Core 2.12.1** is a [source release](https://github.com/askac/standterm/releases/tag/v2.12.1).
It fixes Windows local shell startup when an executable path contains spaces.
Core 2.12 adds SSH Agent Tunnel with shared skills and helpers, Agent Panel permission
sync, Agent Info for the current tab, and SSH host fingerprint management.
IME input-line anchoring remains an [experimental PoC](docs/ime_anchor_poc.md).
This release does not publish new Desktop installers: the downloads below remain
Expand Down
2 changes: 1 addition & 1 deletion core_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Core release identity, shared by source and packaged launchers."""

CORE_VERSION = '2.12.0'
CORE_VERSION = '2.12.1'
10 changes: 6 additions & 4 deletions terminal_backends/local_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,11 +722,13 @@ def _connect_windows(self, cols, rows):
return False, {'message': str(exc), 'error_code': 'local_shell_start_failed'}

def _spawn_windows_process(self, cols, rows, cwd, env):
# A legacy string config is one executable path, not a command line.
command = [self.shell_command] if isinstance(self.shell_command, str) else self.shell_command
spawn_attempts = (
lambda: WinPtyProcess.spawn(self.shell, cwd=cwd, env=env, dimensions=(rows, cols)),
lambda: WinPtyProcess.spawn(self.shell, cwd=cwd, env=env),
lambda: WinPtyProcess.spawn(self.shell, dimensions=(rows, cols)),
lambda: WinPtyProcess.spawn(self.shell),
lambda: WinPtyProcess.spawn(command, cwd=cwd, env=env, dimensions=(rows, cols)),
lambda: WinPtyProcess.spawn(command, cwd=cwd, env=env),
lambda: WinPtyProcess.spawn(command, dimensions=(rows, cols)),
lambda: WinPtyProcess.spawn(command),
)
last_error = None
for spawn in spawn_attempts:
Expand Down
40 changes: 39 additions & 1 deletion tests/terminal_read_smoke.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Exercise bounded terminal reads without network connections or user shells."""
"""Exercise terminal reads and spawn arguments without network or user shells."""
import codecs
from collections import deque
from pathlib import Path
Expand Down Expand Up @@ -174,5 +174,43 @@ def test_windows_empty_live_read_waits(self):
self.assertEqual(runtime.sleeps, [LOCAL_SHELL_IDLE_WAIT_SECONDS])


class WindowsShellSpawnTests(unittest.TestCase):
def test_windows_preserves_executable_paths_and_arguments(self):
executable = r'C:\Fixture Folder\PowerShell\pwsh.exe'
for command in ['cmd.exe', executable, [executable, '-NoLogo', 'argument with spaces'],
(executable, '-NoLogo')]:
with self.subTest(command=command):
bridge, _runtime = local_bridge(None)
bridge.shell = 'PowerShell display label'
bridge.shell_command = command
expected = [command] if isinstance(command, str) else command
with patch('terminal_backends.local_shell.WinPtyProcess') as winpty:
process = bridge._spawn_windows_process(80, 24, 'fixture-cwd', {'TERM': 'fixture'})
self.assertIs(process, winpty.spawn.return_value)
winpty.spawn.assert_called_once_with(expected, cwd='fixture-cwd',
env={'TERM': 'fixture'}, dimensions=(24, 80))

def test_windows_compatibility_fallbacks_keep_the_same_command(self):
command = [r'C:\Fixture Folder\shell.exe', 'argument with spaces']
for failures in range(4):
with self.subTest(failures=failures):
bridge, _runtime = local_bridge(None)
bridge.shell_command = command
process = object()
with patch('terminal_backends.local_shell.WinPtyProcess') as winpty:
winpty.spawn.side_effect = [TypeError('Legacy keyword signature')] * failures + [process]
self.assertIs(bridge._spawn_windows_process(80, 24, 'fixture-cwd', {}), process)
self.assertEqual(winpty.spawn.call_count, failures + 1)
self.assertTrue(all(call.args == (command,) for call in winpty.spawn.call_args_list))

def test_windows_spawn_errors_are_not_retried_as_signature_failures(self):
bridge, _runtime = local_bridge(None)
with patch('terminal_backends.local_shell.WinPtyProcess') as winpty:
winpty.spawn.side_effect = FileNotFoundError('Missing executable')
with self.assertRaises(FileNotFoundError):
bridge._spawn_windows_process(80, 24, 'fixture-cwd', {})
self.assertEqual(winpty.spawn.call_count, 1)


if __name__ == '__main__':
unittest.main()
Loading