Skip to content

Commit

Permalink
Implement terminal I/O cancellation on Windows.
Browse files Browse the repository at this point in the history
Closes #63.
  • Loading branch information
alexrp committed Jan 17, 2024
1 parent bc512ec commit 00fbabe
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 105 deletions.
11 changes: 9 additions & 2 deletions src/core/Native/TerminalInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum TerminalException
{
None,
ArgumentOutOfRange,
OperationCanceled,
PlatformNotSupported,
TerminalNotAttached,
TerminalConfiguration,
Expand All @@ -28,11 +29,11 @@ public struct TerminalResult

public readonly void ThrowIfError()
{
// For when ArgumentOutOfRangeException is not expected.
// For when ArgumentOutOfRangeException and/or OperationCanceledException are not expected.
ThrowIfError(value: (object?)null);
}

public readonly void ThrowIfError<T>(in T value, [CallerArgumentExpression(nameof(value))] string? name = null)
public readonly void ThrowIfError<T>(T value, [CallerArgumentExpression(nameof(value))] string? name = null)
{
_ = value;

Expand All @@ -43,6 +44,8 @@ public readonly void ThrowIfError<T>(in T value, [CallerArgumentExpression(nameo
{
case TerminalException.ArgumentOutOfRange:
throw new ArgumentOutOfRangeException(name);
case TerminalException.OperationCanceled:
throw new OperationCanceledException(Unsafe.As<T, CancellationToken>(ref value));
case TerminalException.PlatformNotSupported:
throw new PlatformNotSupportedException();
case TerminalException.TerminalNotAttached:
Expand Down Expand Up @@ -152,4 +155,8 @@ public static partial TerminalResult SetMode(
[LibraryImport(Library, EntryPoint = "cathode_poll")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial void Poll([MarshalAs(UnmanagedType.U1)] bool write, int* fds, bool* results, int count);

[LibraryImport(Library, EntryPoint = "cathode_cancel")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
public static partial void Cancel(TerminalDescriptor* descriptor);
}
21 changes: 8 additions & 13 deletions src/core/Terminals/NativeTerminalReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,12 @@ internal sealed unsafe class NativeTerminalReader : TerminalReader

private readonly SemaphoreSlim _semaphore;

private readonly Action<nuint, CancellationToken>? _cancellationHook;

public NativeTerminalReader(
NativeVirtualTerminal terminal,
TerminalInterop.TerminalDescriptor* descriptor,
SemaphoreSlim semaphore,
Action<nuint, CancellationToken>? cancellationHook)
NativeVirtualTerminal terminal, TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
{
Terminal = terminal;
Descriptor = descriptor;
_semaphore = semaphore;
_cancellationHook = cancellationHook;
Stream = new SynchronizedStream(new TerminalInputStream(this));
TextReader =
new SynchronizedTextReader(
Expand All @@ -58,14 +52,15 @@ private int ReadPartialNative(scoped Span<byte> buffer, CancellationToken cancel

using (_semaphore.Enter(cancellationToken))
{
_cancellationHook?.Invoke((nuint)Descriptor, cancellationToken);

int progress;
using (Terminal.ArrangeCancellation(Descriptor, write: false, cancellationToken))
{
int progress;

fixed (byte* p = buffer)
TerminalInterop.Read(Descriptor, p, buffer.Length, &progress).ThrowIfError();
fixed (byte* p = buffer)
TerminalInterop.Read(Descriptor, p, buffer.Length, &progress).ThrowIfError(cancellationToken);

return progress;
return progress;
}
}
}
}
Expand Down
21 changes: 8 additions & 13 deletions src/core/Terminals/NativeTerminalWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,12 @@ internal sealed unsafe class NativeTerminalWriter : TerminalWriter

private readonly SemaphoreSlim _semaphore;

private readonly Action<nuint, CancellationToken>? _cancellationHook;

public NativeTerminalWriter(
NativeVirtualTerminal terminal,
TerminalInterop.TerminalDescriptor* descriptor,
SemaphoreSlim semaphore,
Action<nuint, CancellationToken>? cancellationHook)
NativeVirtualTerminal terminal, TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
{
Terminal = terminal;
Descriptor = descriptor;
_semaphore = semaphore;
_cancellationHook = cancellationHook;
Stream = new SynchronizedStream(new TerminalOutputStream(this));
TextWriter =
new SynchronizedTextWriter(
Expand All @@ -55,14 +49,15 @@ private int WritePartialNative(scoped ReadOnlySpan<byte> buffer, CancellationTok

using (_semaphore.Enter(cancellationToken))
{
_cancellationHook?.Invoke((nuint)Descriptor, cancellationToken);

int progress;
using (Terminal.ArrangeCancellation(Descriptor, write: true, cancellationToken))
{
int progress;

fixed (byte* p = buffer)
TerminalInterop.Write(Descriptor, p, buffer.Length, &progress).ThrowIfError();
fixed (byte* p = buffer)
TerminalInterop.Write(Descriptor, p, buffer.Length, &progress).ThrowIfError(cancellationToken);

return progress;
return progress;
}
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/core/Terminals/NativeVirtualTerminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ private protected unsafe NativeVirtualTerminal()

NativeTerminalReader CreateReader(TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
{
return new(this, descriptor, semaphore, CreateCancellationHook(write: false));
return new(this, descriptor, semaphore);
}

NativeTerminalWriter CreateWriter(TerminalInterop.TerminalDescriptor* descriptor, SemaphoreSlim semaphore)
{
return new(this, descriptor, semaphore, CreateCancellationHook(write: true));
return new(this, descriptor, semaphore);
}

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

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

private protected override sealed unsafe Size? QuerySize()
{
Expand Down
18 changes: 11 additions & 7 deletions src/core/Terminals/UnixVirtualTerminal.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Vezel.Cathode.Native;

namespace Vezel.Cathode.Terminals;

internal sealed class UnixVirtualTerminal : NativeVirtualTerminal
Expand All @@ -6,6 +8,10 @@ internal sealed class UnixVirtualTerminal : NativeVirtualTerminal

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

private readonly UnixCancellationPipe _readPipe = new(write: false);

private readonly UnixCancellationPipe _writePipe = new(write: true);

private readonly PosixSignalRegistration _sigWinch;

private readonly PosixSignalRegistration _sigCont;
Expand Down Expand Up @@ -54,14 +60,12 @@ void HandleSignal(PosixSignalContext context)
_sigChld = PosixSignalRegistration.Create(PosixSignal.SIGCHLD, HandleSignal);
}

protected override unsafe Action<nuint, CancellationToken> CreateCancellationHook(bool write)
internal override unsafe IDisposable? ArrangeCancellation(
TerminalInterop.TerminalDescriptor* descriptor, bool write, CancellationToken cancellationToken)
{
var pipe = new UnixCancellationPipe(write);
if (cancellationToken.CanBeCanceled)
(write ? _writePipe : _readPipe).PollWithCancellation(*(int*)descriptor, cancellationToken);

return (descriptor, cancellationToken) =>
{
if (cancellationToken.CanBeCanceled)
pipe.PollWithCancellation(*(int*)descriptor, cancellationToken);
};
return null;
}
}
12 changes: 10 additions & 2 deletions src/core/Terminals/WindowsVirtualTerminal.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Vezel.Cathode.Native;

namespace Vezel.Cathode.Terminals;

internal sealed class WindowsVirtualTerminal : NativeVirtualTerminal
Expand All @@ -20,8 +22,14 @@ private WindowsVirtualTerminal()
{
}

protected override Action<nuint, CancellationToken>? CreateCancellationHook(bool write)
internal override unsafe IDisposable? ArrangeCancellation(
TerminalInterop.TerminalDescriptor* descriptor, bool write, CancellationToken cancellationToken)
{
return null;
return cancellationToken.CanBeCanceled
? cancellationToken.UnsafeRegister(
static descriptor =>
TerminalInterop.Cancel((TerminalInterop.TerminalDescriptor*)Unsafe.Unbox<nuint>(descriptor!)),
(nuint)descriptor)
: null;
}
}
13 changes: 5 additions & 8 deletions src/native/driver-unix.c
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ TerminalResult cathode_generate_signal(TerminalSignal signal)
}

TerminalResult cathode_read(
const TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
{
assert(descriptor);
assert(buffer);
Expand Down Expand Up @@ -372,10 +372,7 @@ TerminalResult cathode_read(
}

TerminalResult cathode_write(
const TerminalDescriptor *nonnull descriptor,
const uint8_t *nullable buffer,
int32_t length,
int32_t *nonnull progress)
TerminalDescriptor *nonnull descriptor, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
{
assert(descriptor);
assert(buffer);
Expand All @@ -385,9 +382,9 @@ TerminalResult cathode_write(
{
ssize_t ret;

// Note that this call may get us suspended by way of a SIGTTOU signal if we are a background process, the handle
// refers to a terminal, and the TOSTOP bit is set (we disable TOSTOP but there are ways that it could get set
// anyway).
// Note that this call may get us suspended by way of a SIGTTOU signal if we are a background process, the
// handle refers to a terminal, and the TOSTOP bit is set (we disable TOSTOP but there are ways that it could
// get set anyway).
while ((ret = write(descriptor->fd, buffer, (size_t)length)) == -1 && errno == EINTR)
{
// Retry in case we get interrupted by a signal.
Expand Down
62 changes: 31 additions & 31 deletions src/native/driver-windows.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ struct TerminalDescriptor
HANDLE handle;
};

typedef struct {
typedef struct
{
TerminalDescriptor descriptor;
DWORD original_mode;
UINT original_code_page;
Expand Down Expand Up @@ -38,7 +39,6 @@ static HANDLE open_console_handle(const wchar_t *nonnull name)
SECURITY_ATTRIBUTES attrs =
{
.nLength = sizeof(SECURITY_ATTRIBUTES),
.lpSecurityDescriptor = nullptr,
.bInheritHandle = true,
};

Expand Down Expand Up @@ -310,14 +310,10 @@ TerminalResult cathode_generate_signal(TerminalSignal signal)
};
}

TerminalResult cathode_read(
const TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
static TerminalResult create_io_result(BOOL result, const int32_t *nonnull progress)
{
assert(descriptor);
assert(buffer);
assert(progress);

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

// See driver-unix.c for the error handling rationale.
Expand All @@ -326,39 +322,43 @@ TerminalResult cathode_read(
{
.exception = TerminalException_None,
}
: (TerminalResult)
{
.exception = TerminalException_Terminal,
.message = u"Could not read from input handle.",
.error = (int32_t)error,
};
: error == ERROR_OPERATION_ABORTED
? (TerminalResult)
{
.exception = TerminalException_OperationCanceled,
}
: (TerminalResult)
{
.exception = TerminalException_Terminal,
.message = u"Could not read from input handle.",
.error = (int32_t)error,
};
}

TerminalResult cathode_read(
TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
{
assert(descriptor);
assert(buffer);
assert(progress);

return create_io_result(ReadFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr), progress);
}

TerminalResult cathode_write(
const TerminalDescriptor *nonnull descriptor,
const uint8_t *nullable buffer,
int32_t length,
int32_t *nonnull progress)
TerminalDescriptor *nonnull descriptor, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress)
{
assert(descriptor);
assert(buffer);
assert(progress);

BOOL result = WriteFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr);
DWORD error = GetLastError();
return create_io_result(WriteFile(descriptor->handle, buffer, (DWORD)length, (LPDWORD)progress, nullptr), progress);
}

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

#endif
2 changes: 1 addition & 1 deletion src/native/driver-windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

#include "driver.h"

// Currently no OS-specific APIs.
CATHODE_API void cathode_cancel(TerminalDescriptor *nonnull descriptor);
17 changes: 9 additions & 8 deletions src/native/driver.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@

typedef struct TerminalDescriptor TerminalDescriptor;

typedef enum {
typedef enum
{
TerminalException_None,
TerminalException_ArgumentOutOfRange,
TerminalException_OperationCanceled,
TerminalException_PlatformNotSupported,
TerminalException_TerminalNotAttached,
TerminalException_TerminalConfiguration,
TerminalException_Terminal,
} TerminalException;

typedef struct {
typedef struct
{
TerminalException exception;
const uint16_t *nullable message; // TODO: This should be char16_t.
int32_t error;
} TerminalResult;

// Keep in sync with src/core/TerminalSignal.cs (public API).
typedef enum {
typedef enum
{
TerminalSignal_Close,
TerminalSignal_Interrupt,
TerminalSignal_Quit,
Expand Down Expand Up @@ -48,10 +52,7 @@ CATHODE_API TerminalResult cathode_set_mode(bool raw, bool flush);
CATHODE_API TerminalResult cathode_generate_signal(TerminalSignal signal);

CATHODE_API TerminalResult cathode_read(
const TerminalDescriptor *nonnull handle, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress);
TerminalDescriptor *nonnull descriptor, uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress);

CATHODE_API TerminalResult cathode_write(
const TerminalDescriptor *nonnull handle,
const uint8_t *nullable buffer,
int32_t length,
int32_t *nonnull progress);
TerminalDescriptor *nonnull descriptor, const uint8_t *nullable buffer, int32_t length, int32_t *nonnull progress);
Loading

0 comments on commit 00fbabe

Please sign in to comment.