Standard stream reading and writing (#964)

This commit is contained in:
Jake Wharton
2025-09-15 21:17:41 -04:00
committed by GitHub
parent 07147ad368
commit 331c0eabea
12 changed files with 302 additions and 3 deletions

View File

@ -2,9 +2,14 @@ public final class com/jakewharton/mosaic/tty/StandardStreams : java/lang/AutoCl
public static final field Companion Lcom/jakewharton/mosaic/tty/StandardStreams$Companion; public static final field Companion Lcom/jakewharton/mosaic/tty/StandardStreams$Companion;
public static final fun bind ()Lcom/jakewharton/mosaic/tty/StandardStreams; public static final fun bind ()Lcom/jakewharton/mosaic/tty/StandardStreams;
public fun close ()V public fun close ()V
public final fun interruptInputRead ()V
public final fun isErrorTty ()Z public final fun isErrorTty ()Z
public final fun isInputTty ()Z public final fun isInputTty ()Z
public final fun isOutputTty ()Z public final fun isOutputTty ()Z
public final fun readInput ([BII)I
public final fun readInputWithTimeout ([BIII)I
public final fun writeError ([BII)I
public final fun writeOutput ([BII)I
} }
public final class com/jakewharton/mosaic/tty/StandardStreams$Companion { public final class com/jakewharton/mosaic/tty/StandardStreams$Companion {

View File

@ -12,9 +12,14 @@ final class com.jakewharton.mosaic.tty/IOException : kotlin/Exception { // com.j
final class com.jakewharton.mosaic.tty/StandardStreams : kotlin/AutoCloseable { // com.jakewharton.mosaic.tty/StandardStreams|null[0] final class com.jakewharton.mosaic.tty/StandardStreams : kotlin/AutoCloseable { // com.jakewharton.mosaic.tty/StandardStreams|null[0]
final fun close() // com.jakewharton.mosaic.tty/StandardStreams.close|close(){}[0] final fun close() // com.jakewharton.mosaic.tty/StandardStreams.close|close(){}[0]
final fun interruptInputRead() // com.jakewharton.mosaic.tty/StandardStreams.interruptInputRead|interruptInputRead(){}[0]
final fun isErrorTty(): kotlin/Boolean // com.jakewharton.mosaic.tty/StandardStreams.isErrorTty|isErrorTty(){}[0] final fun isErrorTty(): kotlin/Boolean // com.jakewharton.mosaic.tty/StandardStreams.isErrorTty|isErrorTty(){}[0]
final fun isInputTty(): kotlin/Boolean // com.jakewharton.mosaic.tty/StandardStreams.isInputTty|isInputTty(){}[0] final fun isInputTty(): kotlin/Boolean // com.jakewharton.mosaic.tty/StandardStreams.isInputTty|isInputTty(){}[0]
final fun isOutputTty(): kotlin/Boolean // com.jakewharton.mosaic.tty/StandardStreams.isOutputTty|isOutputTty(){}[0] final fun isOutputTty(): kotlin/Boolean // com.jakewharton.mosaic.tty/StandardStreams.isOutputTty|isOutputTty(){}[0]
final fun readInput(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/StandardStreams.readInput|readInput(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
final fun readInputWithTimeout(kotlin/ByteArray, kotlin/Int, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/StandardStreams.readInputWithTimeout|readInputWithTimeout(kotlin.ByteArray;kotlin.Int;kotlin.Int;kotlin.Int){}[0]
final fun writeError(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/StandardStreams.writeError|writeError(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
final fun writeOutput(kotlin/ByteArray, kotlin/Int, kotlin/Int): kotlin/Int // com.jakewharton.mosaic.tty/StandardStreams.writeOutput|writeOutput(kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0]
final object Companion { // com.jakewharton.mosaic.tty/StandardStreams.Companion|null[0] final object Companion { // com.jakewharton.mosaic.tty/StandardStreams.Companion|null[0]
final fun bind(): com.jakewharton.mosaic.tty/StandardStreams // com.jakewharton.mosaic.tty/StandardStreams.Companion.bind|bind(){}[0] final fun bind(): com.jakewharton.mosaic.tty/StandardStreams // com.jakewharton.mosaic.tty/StandardStreams.Companion.bind|bind(){}[0]

View File

@ -1,6 +1,7 @@
#if defined(__APPLE__) || defined(__linux__) #if defined(__APPLE__) || defined(__linux__)
#include "mosaic-streams-posix.h" #include "mosaic-streams-posix.h"
#include "mosaic-tty-posix.h"
#include <errno.h> #include <errno.h>
#include <stdlib.h> #include <stdlib.h>
@ -8,6 +9,8 @@
typedef struct MosaicStreamsImpl { typedef struct MosaicStreamsImpl {
int stdin; int stdin;
int interrupt_stdin_reader;
int interrupt_stdin_writer;
int stdout; int stdout;
int stderr; int stderr;
} MosaicStreamsImpl; } MosaicStreamsImpl;
@ -21,7 +24,15 @@ MosaicStreamsInitResult mosaic_streams_init_internal(int stdin, int stdout, int
goto ret; goto ret;
} }
int interruptPipe[2];
if (unlikely(pipe(interruptPipe)) != 0) {
result.error = errno;
goto err_free;
}
streams->stdin = stdin; streams->stdin = stdin;
streams->interrupt_stdin_reader = interruptPipe[0];
streams->interrupt_stdin_writer = interruptPipe[1];
streams->stdout = stdout; streams->stdout = stdout;
streams->stderr = stderr; streams->stderr = stderr;
@ -29,6 +40,10 @@ MosaicStreamsInitResult mosaic_streams_init_internal(int stdin, int stdout, int
ret: ret:
return result; return result;
err_free:
free(streams);
goto ret;
} }
MosaicStreamsInitResult mosaic_streams_init() { MosaicStreamsInitResult mosaic_streams_init() {
@ -60,6 +75,32 @@ MosaicStreamsTtyResult mosaic_streams_is_stderr_tty(MosaicStreams *streams) {
return mosaic_streams_is_tty(streams->stderr); return mosaic_streams_is_tty(streams->stderr);
} }
MosaicIoResult mosaic_streams_read_input(MosaicStreams *streams, uint8_t *buffer, int count) {
return tty_readInternal(streams->stdin, streams->interrupt_stdin_reader, buffer, count, NULL);
}
MosaicIoResult mosaic_streams_read_input_with_timeout(MosaicStreams *streams, uint8_t *buffer, int count, int timeoutMillis) {
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = timeoutMillis * 1000;
return tty_readInternal(streams->stdin, streams->interrupt_stdin_reader, buffer, count, &timeout);
}
uint32_t mosaic_streams_interrupt_input_read(MosaicStreams *streams) {
uint8_t space = ' ';
MosaicIoResult result = tty_writeInternal(streams->interrupt_stdin_writer, &space, 1);
return result.error;
}
MosaicIoResult mosaic_streams_write_output(MosaicStreams *streams, uint8_t *buffer, int count) {
return tty_writeInternal(streams->stdout, buffer, count);
}
MosaicIoResult mosaic_streams_write_error(MosaicStreams *streams, uint8_t *buffer, int count) {
return tty_writeInternal(streams->stderr, buffer, count);
}
uint32_t mosaic_streams_free(MosaicStreams *streams) { uint32_t mosaic_streams_free(MosaicStreams *streams) {
free(streams); free(streams);
return 0; return 0;

View File

@ -1,9 +1,11 @@
#if defined(_WIN32) #if defined(_WIN32)
#include "mosaic-streams-windows.h" #include "mosaic-streams-windows.h"
#include "mosaic-tty-windows.h"
typedef struct MosaicStreamsImpl { typedef struct MosaicStreamsImpl {
HANDLE stdin; HANDLE stdin;
HANDLE stdin_interrupt_event;
HANDLE stdout; HANDLE stdout;
HANDLE stderr; HANDLE stderr;
} MosaicStreamsImpl; } MosaicStreamsImpl;
@ -17,7 +19,14 @@ MosaicStreamsInitResult mosaic_streams_init_internal(HANDLE stdin, HANDLE stdout
goto ret; goto ret;
} }
HANDLE stdinInterruptEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (unlikely(stdinInterruptEvent == NULL)) {
result.error = GetLastError();
goto err_free;
}
streams->stdin = stdin; streams->stdin = stdin;
streams->stdin_interrupt_event = stdinInterruptEvent;
streams->stdout = stdout; streams->stdout = stdout;
streams->stderr = stderr; streams->stderr = stderr;
@ -25,6 +34,10 @@ MosaicStreamsInitResult mosaic_streams_init_internal(HANDLE stdin, HANDLE stdout
ret: ret:
return result; return result;
err_free:
free(streams);
goto ret;
} }
MOSAIC_EXPORT MosaicStreamsInitResult mosaic_streams_init() { MOSAIC_EXPORT MosaicStreamsInitResult mosaic_streams_init() {
@ -76,9 +89,61 @@ MOSAIC_EXPORT MosaicStreamsTtyResult mosaic_streams_is_stderr_tty(MosaicStreams
return mosaic_streams_is_tty(streams->stderr); return mosaic_streams_is_tty(streams->stderr);
} }
MOSAIC_EXPORT MosaicIoResult mosaic_streams_read_input(MosaicStreams *streams, uint8_t *buffer, int count) {
return mosaic_streams_read_input_with_timeout(streams, buffer, count, INFINITE);
}
MOSAIC_EXPORT MosaicIoResult mosaic_streams_read_input_with_timeout(MosaicStreams *streams, uint8_t *buffer, int count, int timeoutMillis) {
MosaicIoResult result = {};
HANDLE waitHandles[2] = { streams->stdin, streams->stdin_interrupt_event };
DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, timeoutMillis);
if (likely(waitResult == WAIT_OBJECT_0)) {
DWORD c;
if (!ReadFile(streams->stdin, buffer, count, &c, NULL)) {
goto err;
}
result.count = c;
} else if (unlikely(waitResult == WAIT_FAILED)) {
goto err;
}
// Else return a count of 0 because either:
// - The interrupt event was selected (which auto resets its state).
// - The user-supplied, non-infinite timeout ran out.
ret:
return result;
err:
result.error = GetLastError();
goto ret;
}
MOSAIC_EXPORT uint32_t mosaic_streams_interrupt_input_read(MosaicStreams *streams) {
return likely(SetEvent(streams->stdin_interrupt_event) != 0)
? 0
: GetLastError();
}
MOSAIC_EXPORT MosaicIoResult mosaic_streams_write_output(MosaicStreams *streams, uint8_t *buffer, int count) {
return tty_writeInternal(streams->stdout, buffer, count);
}
MOSAIC_EXPORT MosaicIoResult mosaic_streams_write_error(MosaicStreams *streams, uint8_t *buffer, int count) {
return tty_writeInternal(streams->stderr, buffer, count);
}
uint32_t mosaic_streams_free(MosaicStreams *streams) { uint32_t mosaic_streams_free(MosaicStreams *streams) {
DWORD result = 0;
if (unlikely(CloseHandle(streams->stdin_interrupt_event) == 0)) {
result = GetLastError();
}
free(streams); free(streams);
return 0;
return result;
} }
#endif #endif

View File

@ -24,6 +24,13 @@ MOSAIC_EXPORT MosaicStreamsTtyResult mosaic_streams_is_stdin_tty(MosaicStreams *
MOSAIC_EXPORT MosaicStreamsTtyResult mosaic_streams_is_stdout_tty(MosaicStreams *streams); MOSAIC_EXPORT MosaicStreamsTtyResult mosaic_streams_is_stdout_tty(MosaicStreams *streams);
MOSAIC_EXPORT MosaicStreamsTtyResult mosaic_streams_is_stderr_tty(MosaicStreams *streams); MOSAIC_EXPORT MosaicStreamsTtyResult mosaic_streams_is_stderr_tty(MosaicStreams *streams);
MOSAIC_EXPORT MosaicIoResult mosaic_streams_read_input(MosaicStreams *streams, uint8_t *buffer, int count);
MOSAIC_EXPORT MosaicIoResult mosaic_streams_read_input_with_timeout(MosaicStreams *streams, uint8_t *buffer, int count, int timeoutMillis);
MOSAIC_EXPORT uint32_t mosaic_streams_interrupt_input_read(MosaicStreams *streams);
MOSAIC_EXPORT MosaicIoResult mosaic_streams_write_output(MosaicStreams *streams, uint8_t *buffer, int count);
MOSAIC_EXPORT MosaicIoResult mosaic_streams_write_error(MosaicStreams *streams, uint8_t *buffer, int count);
MOSAIC_EXPORT uint32_t mosaic_streams_free(MosaicStreams *streams); MOSAIC_EXPORT uint32_t mosaic_streams_free(MosaicStreams *streams);
#endif // MOSAIC_STREAMS_H #endif // MOSAIC_STREAMS_H

View File

@ -176,11 +176,11 @@ MOSAIC_EXPORT uint32_t tty_interruptRead(MosaicTty *tty) {
: GetLastError(); : GetLastError();
} }
MOSAIC_EXPORT MosaicIoResult tty_write(MosaicTty *tty, uint8_t *buffer, int count) { MosaicIoResult tty_writeInternal(HANDLE h, uint8_t *buffer, int count) {
MosaicIoResult result = {}; MosaicIoResult result = {};
DWORD written; DWORD written;
if (WriteFile(tty->conout_for_write, buffer, count, &written, NULL)) { if (WriteFile(h, buffer, count, &written, NULL)) {
result.count = written; result.count = written;
} else { } else {
result.error = GetLastError(); result.error = GetLastError();
@ -189,6 +189,10 @@ MOSAIC_EXPORT MosaicIoResult tty_write(MosaicTty *tty, uint8_t *buffer, int coun
return result; return result;
} }
MOSAIC_EXPORT MosaicIoResult tty_write(MosaicTty *tty, uint8_t *buffer, int count) {
return tty_writeInternal(tty->conout_for_write, buffer, count);
}
MOSAIC_EXPORT uint32_t tty_enableRawMode(MosaicTty *tty) { MOSAIC_EXPORT uint32_t tty_enableRawMode(MosaicTty *tty) {
uint32_t result = 0; uint32_t result = 0;

View File

@ -28,4 +28,6 @@ MosaicTtyInitResult tty_initWithHandles(
bool conoutForWriteFake bool conoutForWriteFake
); );
MosaicIoResult tty_writeInternal(HANDLE h, uint8_t *buffer, int count);
#endif // MOSAIC_TTY_WINDOWS_H #endif // MOSAIC_TTY_WINDOWS_H

View File

@ -9,5 +9,12 @@ public expect class StandardStreams : AutoCloseable {
public fun isOutputTty(): Boolean public fun isOutputTty(): Boolean
public fun isErrorTty(): Boolean public fun isErrorTty(): Boolean
public fun readInput(buffer: ByteArray, offset: Int, count: Int): Int
public fun readInputWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int
public fun interruptInputRead()
public fun writeOutput(buffer: ByteArray, offset: Int, count: Int): Int
public fun writeError(buffer: ByteArray, offset: Int, count: Int): Int
override fun close() override fun close()
} }

View File

@ -2,11 +2,17 @@ package com.jakewharton.mosaic.tty
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_free import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_free
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_init import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_init
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_interrupt_input_read
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_is_stderr_tty import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_is_stderr_tty
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_is_stdin_tty import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_is_stdin_tty
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_is_stdout_tty import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_is_stdout_tty
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_read_input
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_read_input_with_timeout
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_write_error
import com.jakewharton.mosaic.tty.Libmosaic.mosaic_streams_write_output
import java.lang.foreign.Arena import java.lang.foreign.Arena
import java.lang.foreign.MemorySegment import java.lang.foreign.MemorySegment
import java.lang.foreign.ValueLayout
public class StandardStreams internal constructor( public class StandardStreams internal constructor(
private var ptr: MemorySegment, private var ptr: MemorySegment,
@ -57,6 +63,63 @@ public class StandardStreams internal constructor(
} }
} }
@Throws(IOException::class)
public fun readInput(buffer: ByteArray, offset: Int, count: Int): Int {
val segment = Libmosaic.LIBRARY_ARENA.allocate(count.toLong())
val result = mosaic_streams_read_input(Arena.global(), ptr, segment, count)
val error = MosaicIoResult.error(result)
if (error == 0) {
val read = MosaicIoResult.count(result)
MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, 0L, buffer, offset, read)
return read
}
throwIoe(error)
}
@Throws(IOException::class)
public fun readInputWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int {
val segment = Libmosaic.LIBRARY_ARENA.allocate(count.toLong())
val result = mosaic_streams_read_input_with_timeout(Arena.global(), ptr, segment, count, timeoutMillis)
val error = MosaicIoResult.error(result)
if (error == 0) {
val read = MosaicIoResult.count(result)
MemorySegment.copy(segment, ValueLayout.JAVA_BYTE, 0L, buffer, offset, read)
return read
}
throwIoe(error)
}
@Throws(IOException::class)
public fun interruptInputRead() {
val error = mosaic_streams_interrupt_input_read(ptr)
if (error == 0) return
throwIoe(error)
}
@Throws(IOException::class)
public fun writeOutput(buffer: ByteArray, offset: Int, count: Int): Int {
val segment = Libmosaic.LIBRARY_ARENA.allocate(count.toLong())
MemorySegment.copy(buffer, offset, segment, ValueLayout.JAVA_BYTE, 0, count)
val result = mosaic_streams_write_output(Arena.global(), ptr, segment, count)
val error = MosaicIoResult.error(result)
if (error == 0) {
return MosaicIoResult.count(result)
}
throwIoe(error)
}
@Throws(IOException::class)
public fun writeError(buffer: ByteArray, offset: Int, count: Int): Int {
val segment = Libmosaic.LIBRARY_ARENA.allocate(count.toLong())
MemorySegment.copy(buffer, offset, segment, ValueLayout.JAVA_BYTE, 0, count)
val result = mosaic_streams_write_error(Arena.global(), ptr, segment, count)
val error = MosaicIoResult.error(result)
if (error == 0) {
return MosaicIoResult.count(result)
}
throwIoe(error)
}
override fun close() { override fun close() {
val ptr = ptr val ptr = ptr
if (ptr != MemorySegment.NULL) { if (ptr != MemorySegment.NULL) {

View File

@ -13,6 +13,37 @@ final class Jni {
static native boolean streamsErrorIsTty(long streamsPtr); static native boolean streamsErrorIsTty(long streamsPtr);
static native int streamsReadInput(
long streamsPtr,
byte[] buffer,
int offset,
int count
);
static native int streamsReadInputWithTimeout(
long streamsPtr,
byte[] buffer,
int offset,
int count,
int timeout
);
static native void streamsInterruptInputRead(long streamsPtr);
static native int streamsWriteOutput(
long streamsPtr,
byte[] buffer,
int offset,
int count
);
static native int streamsWriteError(
long streamsPtr,
byte[] buffer,
int offset,
int count
);
static native void streamsFree(long streamsPtr); static native void streamsFree(long streamsPtr);
static native long ttyCallbackInit(Tty.Callback callback); static native long ttyCallbackInit(Tty.Callback callback);

View File

@ -25,6 +25,31 @@ public actual class StandardStreams internal constructor(
return Jni.streamsErrorIsTty(ptr) return Jni.streamsErrorIsTty(ptr)
} }
@Throws(IOException::class)
public actual fun readInput(buffer: ByteArray, offset: Int, count: Int): Int {
return Jni.streamsReadInput(ptr, buffer, offset, count)
}
@Throws(IOException::class)
public actual fun readInputWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int {
return Jni.streamsReadInputWithTimeout(ptr, buffer, offset, count, timeoutMillis)
}
@Throws(IOException::class)
public actual fun interruptInputRead() {
return Jni.streamsInterruptInputRead(ptr)
}
@Throws(IOException::class)
public actual fun writeOutput(buffer: ByteArray, offset: Int, count: Int): Int {
return Jni.streamsWriteOutput(ptr, buffer, offset, count)
}
@Throws(IOException::class)
public actual fun writeError(buffer: ByteArray, offset: Int, count: Int): Int {
return Jni.streamsWriteError(ptr, buffer, offset, count)
}
@Throws(IOException::class) @Throws(IOException::class)
actual override fun close() { actual override fun close() {
val ptr = ptr val ptr = ptr

View File

@ -2,7 +2,9 @@ package com.jakewharton.mosaic.tty
import kotlinx.cinterop.CValue import kotlinx.cinterop.CValue
import kotlinx.cinterop.CValuesRef import kotlinx.cinterop.CValuesRef
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.useContents import kotlinx.cinterop.useContents
import kotlinx.cinterop.usePinned
public actual class StandardStreams internal constructor( public actual class StandardStreams internal constructor(
ptr: CValuesRef<MosaicStreams>, ptr: CValuesRef<MosaicStreams>,
@ -32,6 +34,48 @@ public actual class StandardStreams internal constructor(
public actual fun isOutputTty(): Boolean = mosaic_streams_is_stdout_tty(ptr).isTty public actual fun isOutputTty(): Boolean = mosaic_streams_is_stdout_tty(ptr).isTty
public actual fun isErrorTty(): Boolean = mosaic_streams_is_stderr_tty(ptr).isTty public actual fun isErrorTty(): Boolean = mosaic_streams_is_stderr_tty(ptr).isTty
public actual fun readInput(buffer: ByteArray, offset: Int, count: Int): Int {
buffer.asUByteArray().usePinned {
mosaic_streams_read_input(ptr, it.addressOf(offset), count).useContents {
if (error == 0U) return this.count
throwIoe(error)
}
}
}
public actual fun readInputWithTimeout(buffer: ByteArray, offset: Int, count: Int, timeoutMillis: Int): Int {
buffer.asUByteArray().usePinned {
mosaic_streams_read_input_with_timeout(ptr, it.addressOf(offset), count, timeoutMillis).useContents {
if (error == 0U) return this.count
throwIoe(error)
}
}
}
public actual fun interruptInputRead() {
val error = mosaic_streams_interrupt_input_read(ptr)
if (error == 0U) return
throwIoe(error)
}
public actual fun writeOutput(buffer: ByteArray, offset: Int, count: Int): Int {
buffer.asUByteArray().usePinned {
mosaic_streams_write_output(ptr, it.addressOf(offset), count).useContents {
if (error == 0U) return this.count
throwIoe(error)
}
}
}
public actual fun writeError(buffer: ByteArray, offset: Int, count: Int): Int {
buffer.asUByteArray().usePinned {
mosaic_streams_write_error(ptr, it.addressOf(offset), count).useContents {
if (error == 0U) return this.count
throwIoe(error)
}
}
}
actual override fun close() { actual override fun close() {
ptr?.let { ptr?.let {
mosaic_streams_free(it) mosaic_streams_free(it)