Skip to content

Commit

Permalink
Add stdin reader to Tty class (#472)
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton committed Sep 12, 2024
1 parent 6b5f3e3 commit d254d94
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 49 deletions.
7 changes: 7 additions & 0 deletions mosaic-terminal/api/mosaic-terminal.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
public final class com/jakewharton/mosaic/terminal/StdinReader : java/lang/AutoCloseable {
public fun close ()V
public final fun interrupt ()V
public final fun read ([BII)I
}

public final class com/jakewharton/mosaic/terminal/Tty {
public static final field INSTANCE Lcom/jakewharton/mosaic/terminal/Tty;
public final fun enableRawMode ()Ljava/lang/AutoCloseable;
public final fun stdinReader ()Lcom/jakewharton/mosaic/terminal/StdinReader;
}

7 changes: 7 additions & 0 deletions mosaic-terminal/api/mosaic-terminal.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
// - Show declarations: true

// Library unique name: <com.jakewharton.mosaic:mosaic-terminal>
final class com.jakewharton.mosaic.terminal/StdinReader : kotlin/AutoCloseable { // com.jakewharton.mosaic.terminal/StdinReader|null[0]
final fun close() // com.jakewharton.mosaic.terminal/StdinReader.close|close(){}[0]
final fun interrupt() // com.jakewharton.mosaic.terminal/StdinReader.interrupt|interrupt(){}[0]
final fun read(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.terminal/StdinReader.read|read(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
}

final object com.jakewharton.mosaic.terminal/Tty { // com.jakewharton.mosaic.terminal/Tty|null[0]
final fun enableRawMode(): kotlin/AutoCloseable // com.jakewharton.mosaic.terminal/Tty.enableRawMode|enableRawMode(){}[0]
final fun stdinReader(): com.jakewharton.mosaic.terminal/StdinReader // com.jakewharton.mosaic.terminal/Tty.stdinReader|stdinReader(){}[0]
}
2 changes: 2 additions & 0 deletions mosaic-terminal/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ fn setupMosaicTarget(b: *std.Build, step: *std.Build.Step, tag: std.Target.Os.Ta
.files = &.{
"src/c/mosaic-rawMode-posix.c",
"src/c/mosaic-rawMode-windows.c",
"src/c/mosaic-stdin-posix.c",
"src/c/mosaic-stdin-windows.c",
"src/jvmMain/jni/mosaic-jni.c",
},
.flags = &.{
Expand Down
97 changes: 97 additions & 0 deletions mosaic-terminal/src/c/mosaic-stdin-posix.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include "mosaic.h"

#if defined(__APPLE__) || defined(__linux__)

#include "cutils.h"
#include <errno.h>
#include <stdlib.h>
#include <sys/select.h>
#include <unistd.h>

typedef struct stdinReaderImpl {
int pipe[2];
fd_set fds;
} stdinReaderImpl;

stdinReaderResult stdinReader_init() {
stdinReaderResult result = {};

stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl));
if (unlikely(reader == NULL)) {
// result.reader is set to 0 which will trigger OOM.
goto ret;
}

if (unlikely(pipe(reader->pipe)) != 0) {
result.error = errno;
goto err;
}

result.reader = reader;

ret:
return result;

err:
free(reader);
goto ret;
}

stdinRead stdinReader_read(stdinReader *reader, void *buffer, int count) {
int pipeIn = reader->pipe[0];

FD_SET(STDIN_FILENO, &reader->fds);
FD_SET(pipeIn, &reader->fds);

// Our pipe's FD is always going to be higher than stdin, so use it as the max value.
int nfds = pipeIn + 1;

stdinRead result = {};

if (likely(select(nfds, &reader->fds, NULL, NULL, NULL) >= 0)) {
if (likely(FD_ISSET(STDIN_FILENO, &reader->fds) != 0)) {
int c = read(STDIN_FILENO, buffer, count);
if (likely(c > 0)) {
result.count = c;
} else if (c == 0) {
result.count = -1; // EOF
} else {
goto err;
}
}
// Otherwise if the interrupt pipe was selected we return a count of 0.
} else {
goto err;
}

ret:
return result;

err:
result.error = errno;
goto ret;
}

platformError stdinReader_interrupt(stdinReader *reader) {
int pipeOut = reader->pipe[1];
int result = write(pipeOut, " ", 1);
return unlikely(result == -1)
? errno
: 0;
}

platformError stdinReader_free(stdinReader *reader) {
int *pipe = reader->pipe;

int result = 0;
if (unlikely(close(pipe[0]) != 0)) {
result = errno;
}
if (unlikely(close(pipe[1]) != 0 && result != 0)) {
result = errno;
}
free(reader);
return result;
}

#endif
82 changes: 82 additions & 0 deletions mosaic-terminal/src/c/mosaic-stdin-windows.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include "mosaic.h"

#if defined(WIN32)

#include "cutils.h"
#include <Windows.h>

typedef struct stdinReaderImpl {
HANDLE handles[2];
} stdinReaderImpl;

stdinReaderResult stdinReader_init() {
stdinReaderResult result = {};

stdinReaderImpl *reader = calloc(1, sizeof(stdinReaderImpl));
if (unlikely(reader == NULL)) {
// result.reader is set to 0 which will trigger OOM.
goto ret;
}

HANDLE stdin = GetStdHandle(STD_INPUT_HANDLE);
if (unlikely(stdin == INVALID_HANDLE_VALUE)) {
result.error = GetLastError();
goto err;
}
reader->handles[0] = stdin;

HANDLE interruptEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("TODO UUID"));
if (unlikely(interruptEvent == NULL)) {
result.error = GetLastError();
goto err;
}
reader->handles[1] = interruptEvent;

ret:
return result;

err:
free(reader);
goto ret;
}

stdinRead stdinReader_read(stdinReader *reader, void *buffer, int count) {
stdinRead result = {};
DWORD waitResult = WaitForMultipleObjects(2, reader->handles, FALSE, INFINITE);
if (likely(waitResult == WAIT_OBJECT_0)) {
LPDWORD read = 0;
if (likely(ReadConsole(reader->handles[0], buffer, count, read, NULL) != 0)) {
// TODO EOF?
result.count = (*read);
} else {
goto err;
}
} else if (unlikely(waitResult == WAIT_FAILED)) {
goto err;
}
// Else if the interrupt event was selected we return a count of 0.

ret:
return result;

err:
result.error = GetLastError();
goto ret;
}

platformError stdinReader_interrupt(stdinReader *reader) {
return likely(SetEvent(reader->handles[1]) != 0)
? 0
: GetLastError();
}

platformError stdinReader_free(stdinReader *reader) {
DWORD result = 0;
if (unlikely(CloseHandle(reader->handles[1]) != 0)) {
result = GetLastError();
}
free(reader);
return result;
}

#endif
18 changes: 18 additions & 0 deletions mosaic-terminal/src/c/mosaic.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,22 @@ typedef struct rawModeResult {
rawModeResult enterRawMode();
platformError exitRawMode(rawModeConfig *saved);


typedef struct stdinReaderImpl stdinReader;

typedef struct stdinReaderResult {
stdinReader* reader;
platformError error;
} stdinReaderResult;

typedef struct stdinRead {
int count;
platformError error;
} stdinRead;

stdinReaderResult stdinReader_init();
stdinRead stdinReader_read(stdinReader *reader, void *buffer, int count);
platformError stdinReader_interrupt(stdinReader* reader);
platformError stdinReader_free(stdinReader *reader);

#endif // MOSAIC_H
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,38 @@ public expect object Tty {
* the returned instance.
*
* See [`termios(3)`](https://linux.die.net/man/3/termios) for more information.
*
* In addition to the flags required for entering "raw" mode, on POSIX-compliant platforms,
* this function will change the standard input stream to block indefinitely until a minimum
* of 1 byte is available to read. This allows the reader thread to fully be suspended rather
* than consuming CPU. Use [stdinReader] to read in a manner that can still be interrupted.
*/
public fun enableRawMode(): AutoCloseable

/**
* Create a [StdinReader] which will read from this process' stdin stream while also
* supporting interruption.
*
* Use with [enableRawMode] to read input byte-by-byte.
*/
public fun stdinReader(): StdinReader
}

public expect class StdinReader : AutoCloseable {
/**
* Read up to [length] bytes into [buffer] at [offset]. The number of bytes read will be returned.
* 0 will be returned if [interrupt] is called while waiting for input. -1 will be returned if
* the input stream is closed.
*/
public fun read(buffer: ByteArray, offset: Int, length: Int): Int

/** Signal blocking calls to [read] to wake up and return 0. */
public fun interrupt()

/**
* Free the resources associated with this reader.
*
* This call can be omitted if your process is exiting.
*/
override fun close()
}
68 changes: 61 additions & 7 deletions mosaic-terminal/src/jvmMain/jni/mosaic-jni.c
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
#include <jni.h>
#include <mosaic.h>
#include <stdlib.h>
#include "cutils.h"
#include "jni.h"
#include "mosaic.h"
#include <stdlib.h>

JNIEXPORT jlong JNICALL
Java_com_jakewharton_mosaic_terminal_Tty_enterRawMode(JNIEnv *env, jclass type) {
rawModeResult result = enterRawMode();
if (unlikely(result.error)) {
jclass ise = (*env)->FindClass(env, "java/lang/IllegalStateException");
char *message = malloc(40 * sizeof(char));
char *message = malloc(50 * sizeof(char));
if (message) {
sprintf(message, "Unable to enable raw mode: %i", result.error);
sprintf(message, "Unable to enable raw mode: %lu", result.error);
// This throw can fail, but the only condition that should cause that is OOM which
// will occur from returning 0 (which is otherwise ignored if the throw succeeds).
(*env)->ThrowNew(env, ise, message);
Expand All @@ -20,7 +20,61 @@ Java_com_jakewharton_mosaic_terminal_Tty_enterRawMode(JNIEnv *env, jclass type)
return (jlong) result.saved;
}

JNIEXPORT int JNICALL
JNIEXPORT jint JNICALL
Java_com_jakewharton_mosaic_terminal_Tty_exitRawMode(JNIEnv *env, jclass type, jlong ptr) {
return exitRawMode((rawModeConfig*)ptr);
return exitRawMode((rawModeConfig *) ptr);
}

JNIEXPORT jlong JNICALL
Java_com_jakewharton_mosaic_terminal_Tty_stdinReaderInit(JNIEnv *env, jclass type) {
stdinReaderResult result = stdinReader_init();
if (unlikely(result.error)) {
jclass ise = (*env)->FindClass(env, "java/lang/IllegalStateException");
char *message = malloc(54 * sizeof(char));
if (message) {
sprintf(message, "Unable to create stdin reader: %lu", result.error);
// This throw can fail, but the only condition that should cause that is OOM which
// will occur from returning 0 (which is otherwise ignored if the throw succeeds).
(*env)->ThrowNew(env, ise, message);
}
return 0;
}
return (jlong) result.reader;
}

JNIEXPORT jint JNICALL
Java_com_jakewharton_mosaic_terminal_Tty_stdinReaderRead(
JNIEnv *env,
jclass type,
jlong ptr,
jbyteArray buffer,
jint offset,
jint length
) {
jbyte *nativeBuffer = (*env)->GetByteArrayElements(env, buffer, NULL);
jbyte *nativeBufferAtOffset = nativeBuffer + offset;
stdinRead read = stdinReader_read((stdinReader *) ptr, nativeBufferAtOffset, length);
(*env)->ReleaseByteArrayElements(env, buffer, nativeBuffer, 0);
if (unlikely(read.error)) {
jclass ise = (*env)->FindClass(env, "java/lang/IllegalStateException");
char *message = malloc(44 * sizeof(char));
if (message) {
sprintf(message, "Unable to read stdin: %lu", read.error);
// This throw can fail, but the only condition that should cause that is OOM. Return -1 (EOF)
// and should cause the program to try and exit cleanly. 0 is a valid return value.
(*env)->ThrowNew(env, ise, message);
}
return -1;
}
return read.count;
}

JNIEXPORT jint JNICALL
Java_com_jakewharton_mosaic_terminal_Tty_stdinReaderInterrupt(JNIEnv *env, jclass type, jlong ptr) {
return stdinReader_interrupt((stdinReader *) ptr);
}

JNIEXPORT jint JNICALL
Java_com_jakewharton_mosaic_terminal_Tty_stdinReaderFree(JNIEnv *env, jclass type, jlong ptr) {
return stdinReader_free((stdinReader *) ptr);
}
Loading

0 comments on commit d254d94

Please sign in to comment.