Skip to content

Commit 8ba5d82

Browse files
authored
ssh: handle Window size changes on Windows. (#421)
1 parent 6d3784f commit 8ba5d82

File tree

4 files changed

+181
-47
lines changed

4 files changed

+181
-47
lines changed

src/ssh/Program.cs

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ static async Task<int> ExecuteAsync(string destination, string[] command, bool f
131131
command.Length == 0 ? await client.ExecuteShellAsync(executeOptions)
132132
: await client.ExecuteAsync(string.Join(" ", command), executeOptions);
133133

134-
using IDisposable? updateWindowSize = allocateTerminal && !Console.IsOutputRedirected ? UpdateTerminalSize(process) : null;
135134
Task[] tasks = new[]
136135
{
137136
PrintToConsole(process),
@@ -166,6 +165,19 @@ static async Task ReadInputFromConsole(RemoteProcess process)
166165
{
167166
using IStandardInputReader reader = CreateConsoleInReader(process.HasTerminal);
168167

168+
if (process.HasTerminal)
169+
{
170+
reader.WindowSizeChanged += (width, height) =>
171+
{
172+
try
173+
{
174+
process.SetTerminalSize(width, height);
175+
}
176+
catch
177+
{ }
178+
};
179+
}
180+
169181
char[] buffer = new char[100 * 1024];
170182
try
171183
{
@@ -217,26 +229,6 @@ static void PrintExceptions(Task[] tasks)
217229
return null;
218230
}
219231

220-
static IDisposable? UpdateTerminalSize(RemoteProcess process)
221-
{
222-
if (OperatingSystem.IsWindows())
223-
{
224-
return null;
225-
}
226-
else
227-
{
228-
return PosixSignalRegistration.Create(PosixSignal.SIGWINCH, ctx =>
229-
{
230-
try
231-
{
232-
process.SetTerminalSize(Console.WindowWidth, Console.WindowHeight);
233-
}
234-
catch
235-
{ }
236-
});
237-
}
238-
}
239-
240232
static IStandardInputReader CreateConsoleInReader(bool forTerminal)
241233
{
242234
if (OperatingSystem.IsWindows())
@@ -370,4 +362,5 @@ static SshConfigSettings CreateSshConfigSettings(string[] options)
370362
interface IStandardInputReader : IDisposable
371363
{
372364
ValueTask<int> ReadAsync(Memory<char> buffer, CancellationToken cancellationToken = default);
365+
event Action<int, int> WindowSizeChanged;
373366
}

src/ssh/UnixStandardInputReader.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Diagnostics;
33
using System.Net.Sockets;
4+
using System.Runtime.InteropServices;
45
using System.Text;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -11,6 +12,30 @@ sealed partial class UnixStandardInputReader : IStandardInputReader
1112
private readonly Encoding _encoding;
1213
private readonly Decoder _decoder;
1314
private readonly Socket _socket;
15+
private readonly IDisposable? _sigWinchHandler;
16+
private Action<int, int>? _windowSizeChanged;
17+
18+
public event Action<int, int>? WindowSizeChanged
19+
{
20+
add
21+
{
22+
_windowSizeChanged += value;
23+
24+
if (_sigWinchHandler is not null)
25+
{
26+
value?.Invoke(Console.WindowWidth, Console.WindowHeight);
27+
}
28+
}
29+
remove
30+
{
31+
_windowSizeChanged -= value;
32+
}
33+
}
34+
35+
private void EmitWindowSizeChanged(int width, int height)
36+
{
37+
_windowSizeChanged?.Invoke(width, height);
38+
}
1439

1540
public UnixStandardInputReader(bool forTerminal)
1641
{
@@ -23,6 +48,9 @@ public UnixStandardInputReader(bool forTerminal)
2348
// This makes a cursor position read to disable CANON mode.
2449
_ = Console.CursorTop;
2550
Console.TreatControlCAsInput = true;
51+
52+
// Register for window size changed.
53+
_sigWinchHandler = PosixSignalRegistration.Create(PosixSignal.SIGWINCH, ctx => EmitWindowSizeChanged(Console.WindowWidth, Console.WindowHeight));
2654
}
2755
}
2856

@@ -44,5 +72,7 @@ public async ValueTask<int> ReadAsync(Memory<char> buffer, CancellationToken can
4472
}
4573

4674
public void Dispose()
47-
{ }
75+
{
76+
_sigWinchHandler?.Dispose();
77+
}
4878
}

src/ssh/WindowsInterop.cs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,68 @@ internal static partial class WindowsInterop
2222
public const int STD_OUTPUT_HANDLE = -11;
2323
public const int STD_INPUT_HANDLE = -10;
2424

25-
[LibraryImport("kernel32.dll", SetLastError = true)]
25+
[LibraryImport(Kernel32, SetLastError = true)]
2626
[return: MarshalAs(UnmanagedType.Bool)]
2727
public static partial bool GetConsoleMode(IntPtr handle, out uint mode);
2828

29-
[LibraryImport("kernel32.dll", SetLastError = true)]
29+
[LibraryImport(Kernel32, SetLastError = true)]
3030
[return: MarshalAs(UnmanagedType.Bool)]
3131
public static partial bool SetConsoleMode(IntPtr handle, uint mode);
3232

33-
[LibraryImport("kernel32.dll", SetLastError = true)]
33+
[LibraryImport(Kernel32, SetLastError = true)]
3434
public static partial IntPtr GetStdHandle(int handle);
3535

3636
[LibraryImport(Kernel32, SetLastError = true)]
3737
[return: MarshalAs(UnmanagedType.Bool)]
3838
public static partial bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);
3939

4040
[LibraryImport(Kernel32, SetLastError = true)]
41-
internal static unsafe partial int ReadFile(
41+
public static unsafe partial int ReadFile(
4242
IntPtr handle,
4343
byte* bytes,
4444
int numBytesToRead,
4545
out int numBytesRead,
4646
IntPtr mustBeZero);
47+
48+
public const int KEY_EVENT = 0x1;
49+
public const int WINDOW_BUFFER_SIZE_EVENT = 0x4;
50+
51+
[LibraryImport(Kernel32, EntryPoint = "ReadConsoleInputW", SetLastError = true)]
52+
[return: MarshalAs(UnmanagedType.Bool)]
53+
public unsafe static partial bool ReadConsoleInput(IntPtr hConsoleInput, INPUT_RECORD* buffer, int length, out int numberOfEventsRead);
54+
55+
[StructLayout(LayoutKind.Explicit)]
56+
public struct INPUT_RECORD
57+
{
58+
[FieldOffset(0)]
59+
public ushort EventType;
60+
[FieldOffset(4)]
61+
public KEY_EVENT_RECORD KeyEvent;
62+
[FieldOffset(4)]
63+
public WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
64+
};
65+
66+
[StructLayout(LayoutKind.Sequential)]
67+
public struct KEY_EVENT_RECORD
68+
{
69+
public int KeyDown;
70+
public short RepeatCount;
71+
public short VirtualKeyCode;
72+
public short VirtualScanCode;
73+
public char uChar;
74+
public int ControlKeyState;
75+
}
76+
77+
[StructLayout(LayoutKind.Sequential)]
78+
public struct COORD
79+
{
80+
public short X;
81+
public short Y;
82+
}
83+
84+
[StructLayout(LayoutKind.Sequential)]
85+
public struct WINDOW_BUFFER_SIZE_RECORD
86+
{
87+
public COORD Size;
88+
}
4789
}

src/ssh/WindowsStandardInputReader.cs

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ sealed class WindowsStandardInputReader : IStandardInputReader
2020
private readonly WindowsConsoleModeConfig? _stdInConfig;
2121
private bool _reading;
2222
private bool _convertLineEndings;
23+
private bool _readConsole;
24+
private Action<int, int>? _windowSizeChanged;
25+
26+
public event Action<int, int>? WindowSizeChanged
27+
{
28+
add
29+
{
30+
_windowSizeChanged += value;
31+
if (_readConsole)
32+
{
33+
value?.Invoke(Console.WindowWidth, Console.WindowHeight);
34+
}
35+
}
36+
remove
37+
{
38+
_windowSizeChanged -= value;
39+
}
40+
}
41+
42+
private void EmitWindowSizeChanged(int width, int height)
43+
{
44+
_windowSizeChanged?.Invoke(width, height);
45+
}
2346

2447
public WindowsStandardInputReader(bool forTerminal)
2548
{
@@ -28,6 +51,7 @@ public WindowsStandardInputReader(bool forTerminal)
2851
_stdInConfig = forTerminal ? WindowsConsoleModeConfig.Configure(STD_INPUT_HANDLE, TerminalEnableStdInFlags, TerminalDisableStdInFlags)
2952
: WindowsConsoleModeConfig.Configure(STD_INPUT_HANDLE, NoTerminalEnableStdInFlags, NoTerminalDisableStdInFlags);
3053
_convertLineEndings = !forTerminal;
54+
_readConsole = forTerminal;
3155
}
3256

3357
_handle = GetStdHandle(STD_INPUT_HANDLE);
@@ -48,45 +72,90 @@ private unsafe int Read(Memory<char> buffer, CancellationToken cancellationToken
4872
{
4973
cancellationToken.ThrowIfCancellationRequested();
5074

51-
bool readSuccess;
52-
5375
_reading = true;
5476
// Note: this poor man's cancellation may cause characters to get lost.
5577
// we don't care about that because the application exists.
5678
using var ctr = cancellationToken.UnsafeRegister(o => ((WindowsStandardInputReader)o!).CancelIO(), this);
79+
int charsWritten = _readConsole ? ReadConsole(_handle, buffer.Span) : Readfile(_handle, buffer);
80+
_reading = false;
81+
82+
if (charsWritten == -1)
83+
{
84+
cancellationToken.ThrowIfCancellationRequested();
85+
86+
int errorCode = Marshal.GetLastPInvokeError();
87+
throw new Win32Exception(errorCode);
88+
}
89+
90+
return charsWritten;
91+
}
92+
93+
private unsafe int Readfile(IntPtr hConsoleInput, Memory<char> buffer)
94+
{
5795
byte[] bytes = new byte[_encoding.GetMaxByteCount(buffer.Length)];
5896
int bytesRead;
5997
fixed (byte* ptr = bytes)
6098
{
61-
readSuccess = (0 != ReadFile(_handle, ptr, buffer.Length, out bytesRead, IntPtr.Zero));
99+
bool readSuccess = 0 != ReadFile(_handle, ptr, buffer.Length, out bytesRead, IntPtr.Zero);
100+
if (!readSuccess)
101+
{
102+
return -1;
103+
}
62104
}
63-
_reading = false;
105+
Span<byte> bytesReadSpan = bytes.AsSpan(0, bytesRead);
106+
if (_convertLineEndings)
107+
{
108+
if (bytesReadSpan.EndsWith("\r\n"u8))
109+
{
110+
bytesReadSpan = bytesReadSpan[..^1];
111+
bytesReadSpan[^1] = (byte)'\n';
112+
}
113+
else if (bytesReadSpan.EndsWith("\r"u8))
114+
{
115+
bytesReadSpan[^1] = (byte)'\n';
116+
}
117+
}
118+
_decoder.Convert(bytesReadSpan, buffer.Span, flush: false, out int bytesUsed, out int charsRead, out bool completed);
119+
Debug.Assert(bytesReadSpan.Length == bytesUsed);
120+
return charsRead;
121+
}
64122

65-
if (readSuccess)
123+
private unsafe int ReadConsole(IntPtr hConsoleInput, Span<char> buffer)
124+
{
125+
Span<INPUT_RECORD> inputRecords = stackalloc INPUT_RECORD[Math.Min(buffer.Length, 1024)];
126+
while (true)
66127
{
67-
Span<byte> bytesReadSpan = bytes.AsSpan(0, bytesRead);
68-
if (_convertLineEndings)
128+
int numEventsRead;
129+
fixed (INPUT_RECORD* ptr = inputRecords)
69130
{
70-
if (bytesReadSpan.EndsWith("\r\n"u8))
131+
bool readSuccess = ReadConsoleInput(_handle, ptr, inputRecords.Length, out numEventsRead);
132+
if (!readSuccess)
71133
{
72-
bytesReadSpan = bytesReadSpan[..^1];
73-
bytesReadSpan[^1] = (byte)'\n';
134+
return -1;
74135
}
75-
else if (bytesReadSpan.EndsWith("\r"u8))
136+
}
137+
138+
int charsWritten = 0;
139+
foreach (INPUT_RECORD record in inputRecords.Slice(0, numEventsRead))
140+
{
141+
switch (record.EventType)
76142
{
77-
bytesReadSpan[^1] = (byte)'\n';
143+
case KEY_EVENT:
144+
if (record.KeyEvent.KeyDown != 0 && record.KeyEvent.uChar != 0)
145+
{
146+
buffer[charsWritten++] = record.KeyEvent.uChar;
147+
}
148+
break;
149+
case WINDOW_BUFFER_SIZE_EVENT:
150+
EmitWindowSizeChanged(record.WindowBufferSizeEvent.Size.X, record.WindowBufferSizeEvent.Size.Y);
151+
break;
78152
}
79153
}
80-
_decoder.Convert(bytesReadSpan, buffer.Span, flush: false, out int bytesUsed, out int charsUsed, out bool completed);
81-
Debug.Assert(bytesReadSpan.Length == bytesUsed);
82-
return charsUsed;
83-
}
84-
else
85-
{
86-
cancellationToken.ThrowIfCancellationRequested();
87154

88-
int errorCode = Marshal.GetLastPInvokeError();
89-
throw new Win32Exception(errorCode);
155+
if (charsWritten > 0)
156+
{
157+
return charsWritten;
158+
}
90159
}
91160
}
92161

0 commit comments

Comments
 (0)