Skip to content

Commit 00fbabe

Browse files
committed
Implement terminal I/O cancellation on Windows.
Closes #63.
1 parent bc512ec commit 00fbabe

12 files changed

Lines changed: 117 additions & 105 deletions

src/core/Native/TerminalInterop.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public enum TerminalException
1111
{
1212
None,
1313
ArgumentOutOfRange,
14+
OperationCanceled,
1415
PlatformNotSupported,
1516
TerminalNotAttached,
1617
TerminalConfiguration,
@@ -28,11 +29,11 @@ public struct TerminalResult
2829

2930
public readonly void ThrowIfError()
3031
{
31-
// For when ArgumentOutOfRangeException is not expected.
32+
// For when ArgumentOutOfRangeException and/or OperationCanceledException are not expected.
3233
ThrowIfError(value: (object?)null);
3334
}
3435

35-
public readonly void ThrowIfError<T>(in T value, [CallerArgumentExpression(nameof(value))] string? name = null)
36+
public readonly void ThrowIfError<T>(T value, [CallerArgumentExpression(nameof(value))] string? name = null)
3637
{
3738
_ = value;
3839

@@ -43,6 +44,8 @@ public readonly void ThrowIfError<T>(in T value, [CallerArgumentExpression(nameo
4344
{
4445
case TerminalException.ArgumentOutOfRange:
4546
throw new ArgumentOutOfRangeException(name);
47+
case TerminalException.OperationCanceled:
48+
throw new OperationCanceledException(Unsafe.As<T, CancellationToken>(ref value));
4649
case TerminalException.PlatformNotSupported:
4750
throw new PlatformNotSupportedException();
4851
case TerminalException.TerminalNotAttached:
@@ -152,4 +155,8 @@ public static partial TerminalResult SetMode(
152155
[LibraryImport(Library, EntryPoint = "cathode_poll")]
153156
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
154157
public static partial void Poll([MarshalAs(UnmanagedType.U1)] bool write, int* fds, bool* results, int count);
158+
159+
[LibraryImport(Library, EntryPoint = "cathode_cancel")]
160+
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
161+
public static partial void Cancel(TerminalDescriptor* descriptor);
155162
}

src/core/Terminals/NativeTerminalReader.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,12 @@ internal sealed unsafe class NativeTerminalReader : TerminalReader
2222

2323
private readonly SemaphoreSlim _semaphore;
2424

25-
private readonly Action<nuint, CancellationToken>? _cancellationHook;
26-
2725
public NativeTerminalReader(
28-
NativeVirtualTerminal terminal,
29-
TerminalInterop.TerminalDescriptor* descriptor,
30-
SemaphoreSlim semaphore,
31-
Action<nuint, CancellationToken>? cancellationHook)
26+
NativeVirtualTerminal terminal, TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
3227
{
3328
Terminal = terminal;
3429
Descriptor = descriptor;
3530
_semaphore = semaphore;
36-
_cancellationHook = cancellationHook;
3731
Stream = new SynchronizedStream(new TerminalInputStream(this));
3832
TextReader =
3933
new SynchronizedTextReader(
@@ -58,14 +52,15 @@ private int ReadPartialNative(scoped Span<byte> buffer, CancellationToken cancel
5852

5953
using (_semaphore.Enter(cancellationToken))
6054
{
61-
_cancellationHook?.Invoke((nuint)Descriptor, cancellationToken);
62-
63-
int progress;
55+
using (Terminal.ArrangeCancellation(Descriptor, write: false, cancellationToken))
56+
{
57+
int progress;
6458

65-
fixed (byte* p = buffer)
66-
TerminalInterop.Read(Descriptor, p, buffer.Length, &progress).ThrowIfError();
59+
fixed (byte* p = buffer)
60+
TerminalInterop.Read(Descriptor, p, buffer.Length, &progress).ThrowIfError(cancellationToken);
6761

68-
return progress;
62+
return progress;
63+
}
6964
}
7065
}
7166
}

src/core/Terminals/NativeTerminalWriter.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,12 @@ internal sealed unsafe class NativeTerminalWriter : TerminalWriter
2121

2222
private readonly SemaphoreSlim _semaphore;
2323

24-
private readonly Action<nuint, CancellationToken>? _cancellationHook;
25-
2624
public NativeTerminalWriter(
27-
NativeVirtualTerminal terminal,
28-
TerminalInterop.TerminalDescriptor* descriptor,
29-
SemaphoreSlim semaphore,
30-
Action<nuint, CancellationToken>? cancellationHook)
25+
NativeVirtualTerminal terminal, TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
3126
{
3227
Terminal = terminal;
3328
Descriptor = descriptor;
3429
_semaphore = semaphore;
35-
_cancellationHook = cancellationHook;
3630
Stream = new SynchronizedStream(new TerminalOutputStream(this));
3731
TextWriter =
3832
new SynchronizedTextWriter(
@@ -55,14 +49,15 @@ private int WritePartialNative(scoped ReadOnlySpan<byte> buffer, CancellationTok
5549

5650
using (_semaphore.Enter(cancellationToken))
5751
{
58-
_cancellationHook?.Invoke((nuint)Descriptor, cancellationToken);
59-
60-
int progress;
52+
using (Terminal.ArrangeCancellation(Descriptor, write: true, cancellationToken))
53+
{
54+
int progress;
6155

62-
fixed (byte* p = buffer)
63-
TerminalInterop.Write(Descriptor, p, buffer.Length, &progress).ThrowIfError();
56+
fixed (byte* p = buffer)
57+
TerminalInterop.Write(Descriptor, p, buffer.Length, &progress).ThrowIfError(cancellationToken);
6458

65-
return progress;
59+
return progress;
60+
}
6661
}
6762
}
6863
}

src/core/Terminals/NativeVirtualTerminal.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ private protected unsafe NativeVirtualTerminal()
3232

3333
NativeTerminalReader CreateReader(TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
3434
{
35-
return new(this, descriptor, semaphore, CreateCancellationHook(write: false));
35+
return new(this, descriptor, semaphore);
3636
}
3737

3838
NativeTerminalWriter CreateWriter(TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
3939
{
40-
return new(this, descriptor, semaphore, CreateCancellationHook(write: true));
40+
return new(this, descriptor, semaphore);
4141
}
4242

4343
StandardIn = CreateReader(stdIn, inLock);
@@ -47,7 +47,8 @@ NativeTerminalWriter CreateWriter(TerminalInterop.TerminalDescriptor* descriptor
4747
TerminalOut = CreateWriter(ttyOut, outLock);
4848
}
4949

50-
protected abstract Action<nuint, CancellationToken>? CreateCancellationHook(bool write);
50+
internal abstract unsafe IDisposable? ArrangeCancellation(
51+
TerminalInterop.TerminalDescriptor* descriptor, bool write, CancellationToken cancellationToken);
5152

5253
private protected override sealed unsafe Size? QuerySize()
5354
{

src/core/Terminals/UnixVirtualTerminal.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Vezel.Cathode.Native;
2+
13
namespace Vezel.Cathode.Terminals;
24

35
internal sealed class UnixVirtualTerminal : NativeVirtualTerminal
@@ -6,6 +8,10 @@ internal sealed class UnixVirtualTerminal : NativeVirtualTerminal
68

79
public static UnixVirtualTerminal Instance { get; } = new();
810

11+
private readonly UnixCancellationPipe _readPipe = new(write: false);
12+
13+
private readonly UnixCancellationPipe _writePipe = new(write: true);
14+
915
private readonly PosixSignalRegistration _sigWinch;
1016

1117
private readonly PosixSignalRegistration _sigCont;
@@ -54,14 +60,12 @@ void HandleSignal(PosixSignalContext context)
5460
_sigChld = PosixSignalRegistration.Create(PosixSignal.SIGCHLD, HandleSignal);
5561
}
5662

57-
protected override unsafe Action<nuint, CancellationToken> CreateCancellationHook(bool write)
63+
internal override unsafe IDisposable? ArrangeCancellation(
64+
TerminalInterop.TerminalDescriptor* descriptor, bool write, CancellationToken cancellationToken)
5865
{
59-
var pipe = new UnixCancellationPipe(write);
66+
if (cancellationToken.CanBeCanceled)
67+
(write ? _writePipe : _readPipe).PollWithCancellation(*(int*)descriptor, cancellationToken);
6068

61-
return (descriptor, cancellationToken) =>
62-
{
63-
if (cancellationToken.CanBeCanceled)
64-
pipe.PollWithCancellation(*(int*)descriptor, cancellationToken);
65-
};
69+
return null;
6670
}
6771
}

src/core/Terminals/WindowsVirtualTerminal.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Vezel.Cathode.Native;
2+
13
namespace Vezel.Cathode.Terminals;
24

35
internal sealed class WindowsVirtualTerminal : NativeVirtualTerminal
@@ -20,8 +22,14 @@ private WindowsVirtualTerminal()
2022
{
2123
}
2224

23-
protected override Action<nuint, CancellationToken>? CreateCancellationHook(bool write)
25+
internal override unsafe IDisposable? ArrangeCancellation(
26+
TerminalInterop.TerminalDescriptor* descriptor, bool write, CancellationToken cancellationToken)
2427
{
25-
return null;
28+
return cancellationToken.CanBeCanceled
29+
? cancellationToken.UnsafeRegister(
30+
static descriptor =>
31+
TerminalInterop.Cancel((TerminalInterop.TerminalDescriptor*)Unsafe.Unbox<nuint>(descriptor!)),
32+
(nuint)descriptor)
33+
: null;
2634
}
2735
}

src/native/driver-unix.c

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ TerminalResult cathode_generate_signal(TerminalSignal signal)
321321
}
322322

323323
TerminalResult cathode_read(
324-
const TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
324+
TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
325325
{
326326
assert(descriptor);
327327
assert(buffer);
@@ -372,10 +372,7 @@ TerminalResult cathode_read(
372372
}
373373

374374
TerminalResult cathode_write(
375-
const TerminalDescriptor *nonnull descriptor,
376-
const uint8_t *nullable buffer,
377-
int32_t length,
378-
int32_t *nonnull progress)
375+
TerminalDescriptor *nonnull descriptor, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
379376
{
380377
assert(descriptor);
381378
assert(buffer);
@@ -385,9 +382,9 @@ TerminalResult cathode_write(
385382
{
386383
ssize_t ret;
387384

388-
// Note that this call may get us suspended by way of a SIGTTOU signal if we are a background process, the handle
389-
// refers to a terminal, and the TOSTOP bit is set (we disable TOSTOP but there are ways that it could get set
390-
// anyway).
385+
// Note that this call may get us suspended by way of a SIGTTOU signal if we are a background process, the
386+
// handle refers to a terminal, and the TOSTOP bit is set (we disable TOSTOP but there are ways that it could
387+
// get set anyway).
391388
while ((ret = write(descriptor->fd, buffer, (size_t)length)) == -1 && errno == EINTR)
392389
{
393390
// Retry in case we get interrupted by a signal.

src/native/driver-windows.c

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ struct TerminalDescriptor
99
HANDLE handle;
1010
};
1111

12-
typedef struct {
12+
typedef struct
13+
{
1314
TerminalDescriptor descriptor;
1415
DWORD original_mode;
1516
UINT original_code_page;
@@ -38,7 +39,6 @@ static HANDLE open_console_handle(const wchar_t *nonnull name)
3839
SECURITY_ATTRIBUTES attrs =
3940
{
4041
.nLength = sizeof(SECURITY_ATTRIBUTES),
41-
.lpSecurityDescriptor = nullptr,
4242
.bInheritHandle = true,
4343
};
4444

@@ -310,14 +310,10 @@ TerminalResult cathode_generate_signal(TerminalSignal signal)
310310
};
311311
}
312312

313-
TerminalResult cathode_read(
314-
const TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
313+
static TerminalResult create_io_result(BOOL result, const int32_t *nonnull progress)
315314
{
316-
assert(descriptor);
317-
assert(buffer);
318315
assert(progress);
319316

320-
BOOL result = ReadFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr);
321317
DWORD error = GetLastError();
322318

323319
// See driver-unix.c for the error handling rationale.
@@ -326,39 +322,43 @@ TerminalResult cathode_read(
326322
{
327323
.exception = TerminalException_None,
328324
}
329-
: (TerminalResult)
330-
{
331-
.exception = TerminalException_Terminal,
332-
.message = u"Could not read from input handle.",
333-
.error = (int32_t)error,
334-
};
325+
: error == ERROR_OPERATION_ABORTED
326+
? (TerminalResult)
327+
{
328+
.exception = TerminalException_OperationCanceled,
329+
}
330+
: (TerminalResult)
331+
{
332+
.exception = TerminalException_Terminal,
333+
.message = u"Could not read from input handle.",
334+
.error = (int32_t)error,
335+
};
336+
}
337+
338+
TerminalResult cathode_read(
339+
TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
340+
{
341+
assert(descriptor);
342+
assert(buffer);
343+
assert(progress);
344+
345+
return create_io_result(ReadFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr), progress);
335346
}
336347

337348
TerminalResult cathode_write(
338-
const TerminalDescriptor *nonnull descriptor,
339-
const uint8_t *nullable buffer,
340-
int32_t length,
341-
int32_t *nonnull progress)
349+
TerminalDescriptor *nonnull descriptor, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
342350
{
343351
assert(descriptor);
344352
assert(buffer);
345353
assert(progress);
346354

347-
BOOL result = WriteFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr);
348-
DWORD error = GetLastError();
355+
return create_io_result(WriteFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr), progress);
356+
}
349357

350-
// See driver-unix.c for the error handling rationale.
351-
return result || *progress || error == ERROR_HANDLE_EOF || error == ERROR_BROKEN_PIPE || error == ERROR_NO_DATA
352-
? (TerminalResult)
353-
{
354-
.exception = TerminalException_None,
355-
}
356-
: (TerminalResult)
357-
{
358-
.exception = TerminalException_Terminal,
359-
.message = u"Could not write to output handle.",
360-
.error = (int32_t)error,
361-
};
358+
void cathode_cancel(TerminalDescriptor *nonnull descriptor)
359+
{
360+
// This is a best-effort situation; nothing we can do if this fails.
361+
CancelIoEx(descriptor->handle, nullptr);
362362
}
363363

364364
#endif

src/native/driver-windows.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
#include "driver.h"
44

5-
// Currently no OS-specific APIs.
5+
CATHODE_API void cathode_cancel(TerminalDescriptor *nonnull descriptor);

src/native/driver.h

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,27 @@
22

33
typedef struct TerminalDescriptor TerminalDescriptor;
44

5-
typedef enum {
5+
typedef enum
6+
{
67
TerminalException_None,
78
TerminalException_ArgumentOutOfRange,
9+
TerminalException_OperationCanceled,
810
TerminalException_PlatformNotSupported,
911
TerminalException_TerminalNotAttached,
1012
TerminalException_TerminalConfiguration,
1113
TerminalException_Terminal,
1214
} TerminalException;
1315

14-
typedef struct {
16+
typedef struct
17+
{
1518
TerminalException exception;
1619
const uint16_t *nullable message; // TODO: This should be char16_t.
1720
int32_t error;
1821
} TerminalResult;
1922

2023
// Keep in sync with src/core/TerminalSignal.cs (public API).
21-
typedef enum {
24+
typedef enum
25+
{
2226
TerminalSignal_Close,
2327
TerminalSignal_Interrupt,
2428
TerminalSignal_Quit,
@@ -48,10 +52,7 @@ CATHODE_API TerminalResult cathode_set_mode(bool raw, bool flush);
4852
CATHODE_API TerminalResult cathode_generate_signal(TerminalSignal signal);
4953

5054
CATHODE_API TerminalResult cathode_read(
51-
const TerminalDescriptor *nonnull handle, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress);
55+
TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress);
5256

5357
CATHODE_API TerminalResult cathode_write(
54-
const TerminalDescriptor *nonnull handle,
55-
const uint8_t *nullable buffer,
56-
int32_t length,
57-
int32_t *nonnull progress);
58+
TerminalDescriptor *nonnull descriptor, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress);

0 commit comments

Comments
 (0)