mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-01-31 11:37:35 +00:00
Make stdin pollable on Windows
You can now play Super Mario Bros in CMD.EXE using Cosmopolitan! This is thanks to a new worker thread that's spawned on Windows whenever any one of poll(), select(), or ioctl(FIONREAD) is linked.
This commit is contained in:
parent
ef6387ee5e
commit
9c0821def7
15 changed files with 280 additions and 58 deletions
|
@ -16,10 +16,11 @@
|
||||||
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
│ PERFORMANCE OF THIS SOFTWARE. │
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
#include "libc/calls/calls.h"
|
#include "libc/atomic.h"
|
||||||
#include "libc/nt/process.h"
|
#include "libc/intrin/atomic.h"
|
||||||
|
#include "libc/runtime/internal.h"
|
||||||
|
|
||||||
static textwindows char16_t *UintToChar16Array(char16_t p[21], uint64_t x) {
|
static dontasan textwindows char16_t *itoa16(char16_t p[21], uint64_t x) {
|
||||||
char t;
|
char t;
|
||||||
size_t a, b, i = 0;
|
size_t a, b, i = 0;
|
||||||
do {
|
do {
|
||||||
|
@ -36,14 +37,15 @@ static textwindows char16_t *UintToChar16Array(char16_t p[21], uint64_t x) {
|
||||||
return p + i;
|
return p + i;
|
||||||
}
|
}
|
||||||
|
|
||||||
textwindows char16_t *CreatePipeName(char16_t *a) {
|
// This function is called very early by WinMain().
|
||||||
static long x;
|
dontasan textwindows char16_t *__create_pipe_name(char16_t *a) {
|
||||||
char16_t *p = a;
|
char16_t *p = a;
|
||||||
const char *q = "\\\\?\\pipe\\cosmo\\";
|
const char *q = "\\\\?\\pipe\\cosmo\\";
|
||||||
|
static atomic_uint x;
|
||||||
while (*q) *p++ = *q++;
|
while (*q) *p++ = *q++;
|
||||||
p = UintToChar16Array(p, GetCurrentProcessId());
|
p = itoa16(p, __pid);
|
||||||
*p++ = '-';
|
*p++ = '-';
|
||||||
p = UintToChar16Array(p, (x += 1));
|
p = itoa16(p, atomic_fetch_add(&x, 1));
|
||||||
*p = 0;
|
*p = 0;
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
#include "libc/assert.h"
|
#include "libc/assert.h"
|
||||||
#include "libc/calls/internal.h"
|
#include "libc/calls/internal.h"
|
||||||
|
#include "libc/calls/struct/fd.internal.h"
|
||||||
#include "libc/calls/syscall-sysv.internal.h"
|
#include "libc/calls/syscall-sysv.internal.h"
|
||||||
|
#include "libc/calls/syscall_support-nt.internal.h"
|
||||||
#include "libc/calls/termios.h"
|
#include "libc/calls/termios.h"
|
||||||
#include "libc/dce.h"
|
#include "libc/dce.h"
|
||||||
#include "libc/errno.h"
|
#include "libc/errno.h"
|
||||||
|
@ -29,7 +31,10 @@
|
||||||
#include "libc/macros.internal.h"
|
#include "libc/macros.internal.h"
|
||||||
#include "libc/mem/alloca.h"
|
#include "libc/mem/alloca.h"
|
||||||
#include "libc/mem/mem.h"
|
#include "libc/mem/mem.h"
|
||||||
|
#include "libc/nt/enum/filetype.h"
|
||||||
#include "libc/nt/errors.h"
|
#include "libc/nt/errors.h"
|
||||||
|
#include "libc/nt/files.h"
|
||||||
|
#include "libc/nt/ipc.h"
|
||||||
#include "libc/nt/iphlpapi.h"
|
#include "libc/nt/iphlpapi.h"
|
||||||
#include "libc/nt/runtime.h"
|
#include "libc/nt/runtime.h"
|
||||||
#include "libc/nt/struct/ipadapteraddresses.h"
|
#include "libc/nt/struct/ipadapteraddresses.h"
|
||||||
|
@ -48,6 +53,10 @@
|
||||||
#include "libc/sysv/consts/termios.h"
|
#include "libc/sysv/consts/termios.h"
|
||||||
#include "libc/sysv/errfuns.h"
|
#include "libc/sysv/errfuns.h"
|
||||||
|
|
||||||
|
#ifdef __x86_64__
|
||||||
|
__static_yoink("WinMainStdin");
|
||||||
|
#endif
|
||||||
|
|
||||||
/* Maximum number of unicast addresses handled for each interface */
|
/* Maximum number of unicast addresses handled for each interface */
|
||||||
#define MAX_UNICAST_ADDR 32
|
#define MAX_UNICAST_ADDR 32
|
||||||
#define MAX_NAME_CLASH ((int)('z' - 'a')) /* Allow a..z */
|
#define MAX_NAME_CLASH ((int)('z' - 'a')) /* Allow a..z */
|
||||||
|
@ -61,14 +70,10 @@ static struct HostAdapterInfoNode {
|
||||||
short flags;
|
short flags;
|
||||||
} * __hostInfo;
|
} * __hostInfo;
|
||||||
|
|
||||||
static int ioctl_default(int fd, unsigned long request, ...) {
|
static int ioctl_default(int fd, unsigned long request, void *arg) {
|
||||||
int rc;
|
int rc;
|
||||||
void *arg;
|
|
||||||
va_list va;
|
va_list va;
|
||||||
int64_t handle;
|
int64_t handle;
|
||||||
va_start(va, request);
|
|
||||||
arg = va_arg(va, void *);
|
|
||||||
va_end(va);
|
|
||||||
if (!IsWindows()) {
|
if (!IsWindows()) {
|
||||||
return sys_ioctl(fd, request, arg);
|
return sys_ioctl(fd, request, arg);
|
||||||
} else if (__isfdopen(fd)) {
|
} else if (__isfdopen(fd)) {
|
||||||
|
@ -87,6 +92,36 @@ static int ioctl_default(int fd, unsigned long request, ...) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int ioctl_fionread(int fd, uint32_t *arg) {
|
||||||
|
int rc;
|
||||||
|
va_list va;
|
||||||
|
int64_t handle;
|
||||||
|
uint32_t avail;
|
||||||
|
if (!IsWindows()) {
|
||||||
|
return sys_ioctl(fd, FIONREAD, arg);
|
||||||
|
} else if (__isfdopen(fd)) {
|
||||||
|
handle = __resolve_stdin_handle(g_fds.p[fd].handle);
|
||||||
|
if (g_fds.p[fd].kind == kFdSocket) {
|
||||||
|
if ((rc = _weaken(__sys_ioctlsocket_nt)(handle, FIONREAD, arg)) != -1) {
|
||||||
|
return rc;
|
||||||
|
} else {
|
||||||
|
return _weaken(__winsockerr)();
|
||||||
|
}
|
||||||
|
} else if (GetFileType(handle) == kNtFileTypePipe) {
|
||||||
|
if (PeekNamedPipe(handle, 0, 0, 0, &avail, 0)) {
|
||||||
|
*arg = avail;
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return __winerr();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return eopnotsupp();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return ebadf();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Frees all the nodes of the _hostInfo */
|
/* Frees all the nodes of the _hostInfo */
|
||||||
static textwindows void freeHostInfo(void) {
|
static textwindows void freeHostInfo(void) {
|
||||||
struct HostAdapterInfoNode *next, *node = __hostInfo;
|
struct HostAdapterInfoNode *next, *node = __hostInfo;
|
||||||
|
@ -564,8 +599,7 @@ static int ioctl_siocgifflags(int fd, void *arg) {
|
||||||
* @param request can be any of:
|
* @param request can be any of:
|
||||||
*
|
*
|
||||||
* - `FIONREAD` takes an `int *` and returns how many bytes of input
|
* - `FIONREAD` takes an `int *` and returns how many bytes of input
|
||||||
* are available on a terminal or socket, waiting to be read. On
|
* are available on a terminal or socket, waiting to be read.
|
||||||
* Windows this currently won't work for console file descriptors.
|
|
||||||
*
|
*
|
||||||
* - `TIOCGWINSZ` populates `struct winsize *` with the dimensions
|
* - `TIOCGWINSZ` populates `struct winsize *` with the dimensions
|
||||||
* of your teletypewriter. It's an alias for tcgetwinsize().
|
* of your teletypewriter. It's an alias for tcgetwinsize().
|
||||||
|
@ -635,8 +669,8 @@ int ioctl(int fd, unsigned long request, ...) {
|
||||||
va_start(va, request);
|
va_start(va, request);
|
||||||
arg = va_arg(va, void *);
|
arg = va_arg(va, void *);
|
||||||
va_end(va);
|
va_end(va);
|
||||||
if (request == FIONBIO) {
|
if (request == FIONREAD) {
|
||||||
rc = ioctl_default(fd, request, arg);
|
rc = ioctl_fionread(fd, arg);
|
||||||
} else if (request == TIOCGWINSZ) {
|
} else if (request == TIOCGWINSZ) {
|
||||||
rc = tcgetwinsize(fd, arg);
|
rc = tcgetwinsize(fd, arg);
|
||||||
} else if (request == TIOCSWINSZ) {
|
} else if (request == TIOCSWINSZ) {
|
||||||
|
|
|
@ -34,7 +34,7 @@ textwindows int sys_pipe_nt(int pipefd[2], unsigned flags) {
|
||||||
int64_t hin, hout;
|
int64_t hin, hout;
|
||||||
int reader, writer;
|
int reader, writer;
|
||||||
char16_t pipename[64];
|
char16_t pipename[64];
|
||||||
CreatePipeName(pipename);
|
__create_pipe_name(pipename);
|
||||||
__fds_lock();
|
__fds_lock();
|
||||||
if ((reader = __reservefd_unlocked(-1)) == -1) {
|
if ((reader = __reservefd_unlocked(-1)) == -1) {
|
||||||
__fds_unlock();
|
__fds_unlock();
|
||||||
|
|
|
@ -32,6 +32,8 @@
|
||||||
#include "libc/nt/runtime.h"
|
#include "libc/nt/runtime.h"
|
||||||
#include "libc/nt/struct/pollfd.h"
|
#include "libc/nt/struct/pollfd.h"
|
||||||
#include "libc/nt/synchronization.h"
|
#include "libc/nt/synchronization.h"
|
||||||
|
#include "libc/nt/thread.h"
|
||||||
|
#include "libc/nt/thunk/msabi.h"
|
||||||
#include "libc/nt/winsock.h"
|
#include "libc/nt/winsock.h"
|
||||||
#include "libc/sock/internal.h"
|
#include "libc/sock/internal.h"
|
||||||
#include "libc/sock/struct/pollfd.h"
|
#include "libc/sock/struct/pollfd.h"
|
||||||
|
@ -43,6 +45,8 @@
|
||||||
|
|
||||||
#ifdef __x86_64__
|
#ifdef __x86_64__
|
||||||
|
|
||||||
|
__static_yoink("WinMainStdin");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Polls on the New Technology.
|
* Polls on the New Technology.
|
||||||
*
|
*
|
||||||
|
@ -92,7 +96,7 @@ textwindows int sys_poll_nt(struct pollfd *fds, uint64_t nfds, uint64_t *ms,
|
||||||
}
|
}
|
||||||
} else if (pn < ARRAYLEN(pipefds)) {
|
} else if (pn < ARRAYLEN(pipefds)) {
|
||||||
pipeindices[pn] = i;
|
pipeindices[pn] = i;
|
||||||
pipefds[pn].handle = g_fds.p[fds[i].fd].handle;
|
pipefds[pn].handle = __resolve_stdin_handle(g_fds.p[fds[i].fd].handle);
|
||||||
pipefds[pn].events = 0;
|
pipefds[pn].events = 0;
|
||||||
pipefds[pn].revents = 0;
|
pipefds[pn].revents = 0;
|
||||||
switch (g_fds.p[fds[i].fd].flags & O_ACCMODE) {
|
switch (g_fds.p[fds[i].fd].flags & O_ACCMODE) {
|
||||||
|
|
|
@ -91,9 +91,11 @@ static textwindows ssize_t sys_read_nt_impl(struct Fd *fd, void *data,
|
||||||
bool32 ok;
|
bool32 ok;
|
||||||
uint32_t got;
|
uint32_t got;
|
||||||
int filetype;
|
int filetype;
|
||||||
|
int64_t handle;
|
||||||
int abort_errno = EAGAIN;
|
int abort_errno = EAGAIN;
|
||||||
size = MIN(size, 0x7ffff000);
|
size = MIN(size, 0x7ffff000);
|
||||||
filetype = GetFileType(fd->handle);
|
handle = __resolve_stdin_handle(fd->handle);
|
||||||
|
filetype = GetFileType(handle);
|
||||||
if (filetype == kNtFileTypeChar || filetype == kNtFileTypePipe) {
|
if (filetype == kNtFileTypeChar || filetype == kNtFileTypePipe) {
|
||||||
struct NtOverlapped overlap = {0};
|
struct NtOverlapped overlap = {0};
|
||||||
if (offset != -1) {
|
if (offset != -1) {
|
||||||
|
@ -103,18 +105,18 @@ static textwindows ssize_t sys_read_nt_impl(struct Fd *fd, void *data,
|
||||||
if ((overlap.hEvent = CreateEvent(0, 0, 0, 0))) {
|
if ((overlap.hEvent = CreateEvent(0, 0, 0, 0))) {
|
||||||
// the win32 manual says it's important to *not* put &got here
|
// the win32 manual says it's important to *not* put &got here
|
||||||
// since for overlapped i/o, we always use GetOverlappedResult
|
// since for overlapped i/o, we always use GetOverlappedResult
|
||||||
ok = ReadFile(fd->handle, data, size, 0, &overlap);
|
ok = ReadFile(handle, data, size, 0, &overlap);
|
||||||
if (!ok && GetLastError() == kNtErrorIoPending) {
|
if (!ok && GetLastError() == kNtErrorIoPending) {
|
||||||
// i/o operation is in flight; blocking is unavoidable
|
// i/o operation is in flight; blocking is unavoidable
|
||||||
// if we're in non-blocking mode, then immediately abort
|
// if we're in non-blocking mode, then immediately abort
|
||||||
// if an interrupt is pending, then abort before waiting
|
// if an interrupt is pending, then abort before waiting
|
||||||
// otherwise wait for i/o periodically checking interrupts
|
// otherwise wait for i/o periodically checking interrupts
|
||||||
if (fd->flags & O_NONBLOCK) {
|
if (fd->flags & O_NONBLOCK) {
|
||||||
sys_read_nt_abort(fd->handle, &overlap);
|
sys_read_nt_abort(handle, &overlap);
|
||||||
} else if (_check_interrupts(kSigOpRestartable, g_fds.p)) {
|
} else if (_check_interrupts(kSigOpRestartable, g_fds.p)) {
|
||||||
Interrupted:
|
Interrupted:
|
||||||
abort_errno = errno;
|
abort_errno = errno;
|
||||||
sys_read_nt_abort(fd->handle, &overlap);
|
sys_read_nt_abort(handle, &overlap);
|
||||||
} else {
|
} else {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
uint32_t i;
|
uint32_t i;
|
||||||
|
@ -134,7 +136,7 @@ static textwindows ssize_t sys_read_nt_impl(struct Fd *fd, void *data,
|
||||||
if (ok) {
|
if (ok) {
|
||||||
// overlapped is allocated on stack, so it's important we wait
|
// overlapped is allocated on stack, so it's important we wait
|
||||||
// for windows to acknowledge that it's done using that memory
|
// for windows to acknowledge that it's done using that memory
|
||||||
ok = GetOverlappedResult(fd->handle, &overlap, &got, true);
|
ok = GetOverlappedResult(handle, &overlap, &got, true);
|
||||||
}
|
}
|
||||||
CloseHandle(overlap.hEvent);
|
CloseHandle(overlap.hEvent);
|
||||||
} else {
|
} else {
|
||||||
|
@ -142,21 +144,21 @@ static textwindows ssize_t sys_read_nt_impl(struct Fd *fd, void *data,
|
||||||
}
|
}
|
||||||
} else if (offset == -1) {
|
} else if (offset == -1) {
|
||||||
// perform simple blocking file read
|
// perform simple blocking file read
|
||||||
ok = ReadFile(fd->handle, data, size, &got, 0);
|
ok = ReadFile(handle, data, size, &got, 0);
|
||||||
} else {
|
} else {
|
||||||
// perform pread()-style file read at particular file offset
|
// perform pread()-style file read at particular file offset
|
||||||
int64_t position;
|
int64_t position;
|
||||||
// save file pointer which windows clobbers, even for overlapped i/o
|
// save file pointer which windows clobbers, even for overlapped i/o
|
||||||
if (!SetFilePointerEx(fd->handle, 0, &position, SEEK_CUR)) {
|
if (!SetFilePointerEx(handle, 0, &position, SEEK_CUR)) {
|
||||||
return __winerr(); // fd probably isn't seekable?
|
return __winerr(); // fd probably isn't seekable?
|
||||||
}
|
}
|
||||||
struct NtOverlapped overlap = {0};
|
struct NtOverlapped overlap = {0};
|
||||||
overlap.Pointer = (void *)(uintptr_t)offset;
|
overlap.Pointer = (void *)(uintptr_t)offset;
|
||||||
ok = ReadFile(fd->handle, data, size, 0, &overlap);
|
ok = ReadFile(handle, data, size, 0, &overlap);
|
||||||
if (!ok && GetLastError() == kNtErrorIoPending) ok = true;
|
if (!ok && GetLastError() == kNtErrorIoPending) ok = true;
|
||||||
if (ok) ok = GetOverlappedResult(fd->handle, &overlap, &got, true);
|
if (ok) ok = GetOverlappedResult(handle, &overlap, &got, true);
|
||||||
// restore file pointer which windows clobbers, even on error
|
// restore file pointer which windows clobbers, even on error
|
||||||
unassert(SetFilePointerEx(fd->handle, position, 0, SEEK_SET));
|
unassert(SetFilePointerEx(handle, position, 0, SEEK_SET));
|
||||||
}
|
}
|
||||||
if (ok) {
|
if (ok) {
|
||||||
if (fd->ttymagic & kFdTtyMunging) {
|
if (fd->ttymagic & kFdTtyMunging) {
|
||||||
|
|
|
@ -28,12 +28,24 @@ struct Fd {
|
||||||
int64_t extra;
|
int64_t extra;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct StdinRelay {
|
||||||
|
_Atomic(uint32_t) once;
|
||||||
|
int64_t inisem; /* semaphore to delay 1st read */
|
||||||
|
int64_t handle; /* should == g_fds.p[0].handle */
|
||||||
|
int64_t reader; /* ReadFile() use this instead */
|
||||||
|
int64_t writer; /* only used by WinStdinThread */
|
||||||
|
};
|
||||||
|
|
||||||
struct Fds {
|
struct Fds {
|
||||||
_Atomic(int) f; /* lowest free slot */
|
_Atomic(int) f; /* lowest free slot */
|
||||||
size_t n;
|
size_t n;
|
||||||
struct Fd *p, *e;
|
struct Fd *p, *e;
|
||||||
|
struct StdinRelay stdin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
int64_t __resolve_stdin_handle(int64_t);
|
||||||
|
void WinMainStdin(void);
|
||||||
|
|
||||||
COSMOPOLITAN_C_END_
|
COSMOPOLITAN_C_END_
|
||||||
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */
|
#endif /* !(__ASSEMBLER__ + __LINKER__ + 0) */
|
||||||
#endif /* COSMOPOLITAN_LIBC_CALLS_STRUCT_FD_INTERNAL_H_ */
|
#endif /* COSMOPOLITAN_LIBC_CALLS_STRUCT_FD_INTERNAL_H_ */
|
||||||
|
|
|
@ -7,7 +7,7 @@ bool isdirectory_nt(const char *);
|
||||||
bool isregularfile_nt(const char *);
|
bool isregularfile_nt(const char *);
|
||||||
bool issymlink_nt(const char *);
|
bool issymlink_nt(const char *);
|
||||||
bool32 ntsetprivilege(int64_t, const char16_t *, uint32_t);
|
bool32 ntsetprivilege(int64_t, const char16_t *, uint32_t);
|
||||||
char16_t *CreatePipeName(char16_t *);
|
char16_t *__create_pipe_name(char16_t *);
|
||||||
int __mkntpath(const char *, char16_t[hasatleast PATH_MAX]);
|
int __mkntpath(const char *, char16_t[hasatleast PATH_MAX]);
|
||||||
int __mkntpath2(const char *, char16_t[hasatleast PATH_MAX], int);
|
int __mkntpath2(const char *, char16_t[hasatleast PATH_MAX], int);
|
||||||
int __mkntpathat(int, const char *, int, char16_t[hasatleast PATH_MAX]);
|
int __mkntpathat(int, const char *, int, char16_t[hasatleast PATH_MAX]);
|
||||||
|
|
121
libc/calls/winstdin1.c
Normal file
121
libc/calls/winstdin1.c
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2023 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/calls/calls.h"
|
||||||
|
#include "libc/calls/internal.h"
|
||||||
|
#include "libc/calls/syscall_support-nt.internal.h"
|
||||||
|
#include "libc/intrin/strace.internal.h"
|
||||||
|
#include "libc/nt/createfile.h"
|
||||||
|
#include "libc/nt/enum/accessmask.h"
|
||||||
|
#include "libc/nt/enum/creationdisposition.h"
|
||||||
|
#include "libc/nt/enum/fileflagandattributes.h"
|
||||||
|
#include "libc/nt/ipc.h"
|
||||||
|
#include "libc/nt/runtime.h"
|
||||||
|
#include "libc/nt/synchronization.h"
|
||||||
|
#include "libc/nt/thread.h"
|
||||||
|
#include "libc/nt/thunk/msabi.h"
|
||||||
|
|
||||||
|
__msabi extern typeof(CloseHandle) *const __imp_CloseHandle;
|
||||||
|
__msabi extern typeof(CreateFile) *const __imp_CreateFileW;
|
||||||
|
__msabi extern typeof(CreateNamedPipe) *const __imp_CreateNamedPipeW;
|
||||||
|
__msabi extern typeof(CreateSemaphore) *const __imp_CreateSemaphoreW;
|
||||||
|
__msabi extern typeof(CreateThread) *const __imp_CreateThread;
|
||||||
|
__msabi extern typeof(GetStdHandle) *const __imp_GetStdHandle;
|
||||||
|
__msabi extern typeof(ReadFile) *const __imp_ReadFile;
|
||||||
|
__msabi extern typeof(WaitForSingleObject) *const __imp_WaitForSingleObject;
|
||||||
|
__msabi extern typeof(WriteFile) *const __imp_WriteFile;
|
||||||
|
|
||||||
|
__msabi static dontasan dontubsan dontinstrument textwindows uint32_t
|
||||||
|
WinStdinThread(void *lpParameter) {
|
||||||
|
char buf[4096];
|
||||||
|
uint32_t i, got, wrote;
|
||||||
|
|
||||||
|
// wait forever for semaphore to exceed 0
|
||||||
|
//
|
||||||
|
// this semaphore is unlocked the first time read or poll happens. we
|
||||||
|
// need it so the prog has time to call functions like SetConsoleMode
|
||||||
|
// before we begin performing i/o.
|
||||||
|
__imp_WaitForSingleObject(g_fds.stdin.inisem, -1u);
|
||||||
|
__imp_CloseHandle(g_fds.stdin.inisem);
|
||||||
|
|
||||||
|
// relay stdin to process
|
||||||
|
NTTRACE("<stdin> activated");
|
||||||
|
for (;;) {
|
||||||
|
if (!__imp_ReadFile(g_fds.stdin.handle, buf, sizeof(buf), &got, 0)) {
|
||||||
|
NTTRACE("<stdin> read failed");
|
||||||
|
goto Finish;
|
||||||
|
}
|
||||||
|
if (!got) {
|
||||||
|
NTTRACE("<stdin> end of file");
|
||||||
|
goto Finish;
|
||||||
|
}
|
||||||
|
for (i = 0; i < got; i += wrote) {
|
||||||
|
if (!__imp_WriteFile(g_fds.stdin.writer, buf + i, got - i, &wrote, 0)) {
|
||||||
|
NTTRACE("<stdin> failed to write to pipe");
|
||||||
|
goto Finish;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Finish:
|
||||||
|
__imp_CloseHandle(g_fds.stdin.writer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this makes it possible for our read() implementation to periodically
|
||||||
|
// poll for signals while performing a blocking overlapped io operation
|
||||||
|
dontasan dontubsan dontinstrument textwindows void WinMainStdin(void) {
|
||||||
|
uint32_t mode;
|
||||||
|
char16_t pipename[64];
|
||||||
|
int64_t hStdin, hWriter, hReader, hThread, hSemaphore;
|
||||||
|
hStdin = __imp_GetStdHandle(kNtStdInputHandle);
|
||||||
|
if (hStdin == kNtInvalidHandleValue) {
|
||||||
|
NTTRACE("<stdin> GetStdHandle failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// create non-inherited semaphore with initial value of 0
|
||||||
|
hSemaphore = __imp_CreateSemaphoreW(0, 0, 1, 0);
|
||||||
|
if (!hSemaphore) {
|
||||||
|
NTTRACE("<stdin> CreateSemaphore failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
__create_pipe_name(pipename);
|
||||||
|
hReader = __imp_CreateNamedPipeW(
|
||||||
|
pipename, kNtPipeAccessInbound | kNtFileFlagOverlapped,
|
||||||
|
kNtPipeTypeByte | kNtPipeReadmodeByte, 1, 4096, 4096, 0, 0);
|
||||||
|
if (hReader == kNtInvalidHandleValue) {
|
||||||
|
NTTRACE("<stdin> CreateNamedPipe failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hWriter = __imp_CreateFileW(pipename, kNtGenericWrite, 0, 0, kNtOpenExisting,
|
||||||
|
kNtFileFlagOverlapped, 0);
|
||||||
|
if (hWriter == kNtInvalidHandleValue) {
|
||||||
|
NTTRACE("<stdin> CreateFile failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hThread = __imp_CreateThread(0, 65536, WinStdinThread, 0, 0, 0);
|
||||||
|
if (!hThread) {
|
||||||
|
NTTRACE("<stdin> CreateThread failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
__imp_CloseHandle(hThread);
|
||||||
|
g_fds.stdin.handle = hStdin;
|
||||||
|
g_fds.stdin.reader = hReader;
|
||||||
|
g_fds.stdin.writer = hWriter;
|
||||||
|
g_fds.stdin.inisem = hSemaphore;
|
||||||
|
}
|
32
libc/calls/winstdin2.c
Normal file
32
libc/calls/winstdin2.c
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
|
||||||
|
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│
|
||||||
|
╞══════════════════════════════════════════════════════════════════════════════╡
|
||||||
|
│ Copyright 2023 Justine Alexandra Roberts Tunney │
|
||||||
|
│ │
|
||||||
|
│ Permission to use, copy, modify, and/or distribute this software for │
|
||||||
|
│ any purpose with or without fee is hereby granted, provided that the │
|
||||||
|
│ above copyright notice and this permission notice appear in all copies. │
|
||||||
|
│ │
|
||||||
|
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │
|
||||||
|
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │
|
||||||
|
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │
|
||||||
|
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │
|
||||||
|
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │
|
||||||
|
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │
|
||||||
|
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │
|
||||||
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
|
#include "libc/calls/calls.h"
|
||||||
|
#include "libc/calls/internal.h"
|
||||||
|
#include "libc/intrin/atomic.h"
|
||||||
|
#include "libc/nt/synchronization.h"
|
||||||
|
|
||||||
|
textwindows int64_t __resolve_stdin_handle(int64_t handle) {
|
||||||
|
if (handle == g_fds.stdin.handle) {
|
||||||
|
if (!atomic_exchange(&g_fds.stdin.once, 1)) {
|
||||||
|
ReleaseSemaphore(g_fds.stdin.inisem, 1, 0);
|
||||||
|
}
|
||||||
|
handle = g_fds.stdin.reader;
|
||||||
|
}
|
||||||
|
return handle;
|
||||||
|
}
|
|
@ -31,9 +31,9 @@ __msabi extern typeof(CreateThread) *const __imp_CreateThread;
|
||||||
* @return thread handle, or 0 on failure
|
* @return thread handle, or 0 on failure
|
||||||
* @note this wrapper takes care of ABI, STRACE()
|
* @note this wrapper takes care of ABI, STRACE()
|
||||||
*/
|
*/
|
||||||
textwindows int64_t CreateThread(
|
textwindows int64_t
|
||||||
struct NtSecurityAttributes *lpThreadAttributes, size_t dwStackSize,
|
CreateThread(struct NtSecurityAttributes *lpThreadAttributes,
|
||||||
NtThreadStartRoutine lpStartAddress, void *lpParameter,
|
size_t dwStackSize, void *lpStartAddress, void *lpParameter,
|
||||||
uint32_t dwCreationFlags, uint32_t *opt_lpThreadId) {
|
uint32_t dwCreationFlags, uint32_t *opt_lpThreadId) {
|
||||||
int64_t hHandle;
|
int64_t hHandle;
|
||||||
hHandle = __imp_CreateThread(lpThreadAttributes, dwStackSize, lpStartAddress,
|
hHandle = __imp_CreateThread(lpThreadAttributes, dwStackSize, lpStartAddress,
|
||||||
|
|
|
@ -30,10 +30,8 @@ COSMOPOLITAN_C_START_
|
||||||
│ cosmopolitan § new technology » threads ─╬─│┼
|
│ cosmopolitan § new technology » threads ─╬─│┼
|
||||||
╚────────────────────────────────────────────────────────────────────────────│*/
|
╚────────────────────────────────────────────────────────────────────────────│*/
|
||||||
|
|
||||||
typedef uint32_t (*NtThreadStartRoutine)(void *lpParameter);
|
|
||||||
|
|
||||||
int64_t CreateThread(struct NtSecurityAttributes *lpThreadAttributes,
|
int64_t CreateThread(struct NtSecurityAttributes *lpThreadAttributes,
|
||||||
size_t dwStackSize, NtThreadStartRoutine lpStartAddress,
|
size_t dwStackSize, void *lpStartAddress,
|
||||||
void *lpParameter, uint32_t dwCreationFlags,
|
void *lpParameter, uint32_t dwCreationFlags,
|
||||||
uint32_t *opt_lpThreadId);
|
uint32_t *opt_lpThreadId);
|
||||||
|
|
||||||
|
|
|
@ -183,6 +183,10 @@ textwindows void WinMainForked(void) {
|
||||||
uint32_t i, varlen, oldprot, savepid;
|
uint32_t i, varlen, oldprot, savepid;
|
||||||
long mapcount, mapcapacity, specialz;
|
long mapcount, mapcapacity, specialz;
|
||||||
|
|
||||||
|
struct StdinRelay stdin;
|
||||||
|
struct Fds *fds = __veil("r", &g_fds);
|
||||||
|
stdin = fds->stdin;
|
||||||
|
|
||||||
// check to see if the process was actually forked
|
// check to see if the process was actually forked
|
||||||
// this variable should have the pipe handle numba
|
// this variable should have the pipe handle numba
|
||||||
varlen = GetEnvironmentVariable(u"_FORK", fvar, ARRAYLEN(fvar));
|
varlen = GetEnvironmentVariable(u"_FORK", fvar, ARRAYLEN(fvar));
|
||||||
|
@ -261,7 +265,7 @@ textwindows void WinMainForked(void) {
|
||||||
|
|
||||||
// rewrap the stdin named pipe hack
|
// rewrap the stdin named pipe hack
|
||||||
// since the handles closed on fork
|
// since the handles closed on fork
|
||||||
struct Fds *fds = __veil("r", &g_fds);
|
fds->stdin = stdin;
|
||||||
fds->p[0].handle = GetStdHandle(kNtStdInputHandle);
|
fds->p[0].handle = GetStdHandle(kNtStdInputHandle);
|
||||||
fds->p[1].handle = GetStdHandle(kNtStdOutputHandle);
|
fds->p[1].handle = GetStdHandle(kNtStdOutputHandle);
|
||||||
fds->p[2].handle = GetStdHandle(kNtStdErrorHandle);
|
fds->p[2].handle = GetStdHandle(kNtStdErrorHandle);
|
||||||
|
@ -310,7 +314,7 @@ textwindows int sys_fork_nt(uint32_t dwCreationFlags) {
|
||||||
tib = __tls_enabled ? __get_tls() : 0;
|
tib = __tls_enabled ? __get_tls() : 0;
|
||||||
if (!setjmp(jb)) {
|
if (!setjmp(jb)) {
|
||||||
pid = untrackpid = __reservefd_unlocked(-1);
|
pid = untrackpid = __reservefd_unlocked(-1);
|
||||||
reader = CreateNamedPipe(CreatePipeName(pipename), kNtPipeAccessInbound,
|
reader = CreateNamedPipe(__create_pipe_name(pipename), kNtPipeAccessInbound,
|
||||||
kNtPipeTypeByte | kNtPipeReadmodeByte, 1, PIPE_BUF,
|
kNtPipeTypeByte | kNtPipeReadmodeByte, 1, PIPE_BUF,
|
||||||
PIPE_BUF, 0, &kNtIsInheritable);
|
PIPE_BUF, 0, &kNtIsInheritable);
|
||||||
writer = CreateFile(pipename, kNtGenericWrite, 0, 0, kNtOpenExisting, 0, 0);
|
writer = CreateFile(pipename, kNtGenericWrite, 0, 0, kNtOpenExisting, 0, 0);
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
│ PERFORMANCE OF THIS SOFTWARE. │
|
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||||
#include "libc/assert.h"
|
#include "libc/assert.h"
|
||||||
|
#include "libc/calls/internal.h"
|
||||||
#include "libc/calls/state.internal.h"
|
#include "libc/calls/state.internal.h"
|
||||||
#include "libc/calls/syscall_support-nt.internal.h"
|
#include "libc/calls/syscall_support-nt.internal.h"
|
||||||
#include "libc/dce.h"
|
#include "libc/dce.h"
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
#include "libc/nt/enum/filesharemode.h"
|
#include "libc/nt/enum/filesharemode.h"
|
||||||
#include "libc/nt/enum/pageflags.h"
|
#include "libc/nt/enum/pageflags.h"
|
||||||
#include "libc/nt/files.h"
|
#include "libc/nt/files.h"
|
||||||
|
#include "libc/nt/ipc.h"
|
||||||
#include "libc/nt/memory.h"
|
#include "libc/nt/memory.h"
|
||||||
#include "libc/nt/pedef.internal.h"
|
#include "libc/nt/pedef.internal.h"
|
||||||
#include "libc/nt/process.h"
|
#include "libc/nt/process.h"
|
||||||
|
@ -44,6 +46,8 @@
|
||||||
#include "libc/nt/signals.h"
|
#include "libc/nt/signals.h"
|
||||||
#include "libc/nt/struct/ntexceptionpointers.h"
|
#include "libc/nt/struct/ntexceptionpointers.h"
|
||||||
#include "libc/nt/struct/teb.h"
|
#include "libc/nt/struct/teb.h"
|
||||||
|
#include "libc/nt/synchronization.h"
|
||||||
|
#include "libc/nt/thread.h"
|
||||||
#include "libc/nt/thunk/msabi.h"
|
#include "libc/nt/thunk/msabi.h"
|
||||||
#include "libc/runtime/internal.h"
|
#include "libc/runtime/internal.h"
|
||||||
#include "libc/runtime/memtrack.internal.h"
|
#include "libc/runtime/memtrack.internal.h"
|
||||||
|
@ -54,6 +58,26 @@
|
||||||
|
|
||||||
#ifdef __x86_64__
|
#ifdef __x86_64__
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @fileoverview makes windows stdin handle capable of being poll'd
|
||||||
|
*
|
||||||
|
* 1. On Windows, there's no way to check how many bytes of input are
|
||||||
|
* available from the cmd.exe console. The only thing you can do is a
|
||||||
|
* blocking read that can't be interrupted.
|
||||||
|
*
|
||||||
|
* 2. On Windows, it's up to the parent process whether or not the
|
||||||
|
* handles it passes us are capable of non-blocking overlapped i/o
|
||||||
|
* reads (which we need for busy polling to check for interrupts).
|
||||||
|
*
|
||||||
|
* So we solve this by creating a thread which just does naive reads on
|
||||||
|
* standard input, and then relays the data to the process via a named
|
||||||
|
* pipe, which we explicitly creaete with overlapped i/o enabled.
|
||||||
|
*
|
||||||
|
* This code runs very early during process initialization, at the
|
||||||
|
* beginning of WinMain(). This module is only activated if the app
|
||||||
|
* links any one of: poll(), select(), or ioctl(FIONREAD).
|
||||||
|
*/
|
||||||
|
|
||||||
// clang-format off
|
// clang-format off
|
||||||
__msabi extern typeof(AddVectoredExceptionHandler) *const __imp_AddVectoredExceptionHandler;
|
__msabi extern typeof(AddVectoredExceptionHandler) *const __imp_AddVectoredExceptionHandler;
|
||||||
__msabi extern typeof(CloseHandle) *const __imp_CloseHandle;
|
__msabi extern typeof(CloseHandle) *const __imp_CloseHandle;
|
||||||
|
@ -68,6 +92,7 @@ __msabi extern typeof(GetCurrentProcessId) *const __imp_GetCurrentProcessId;
|
||||||
__msabi extern typeof(GetEnvironmentStrings) *const __imp_GetEnvironmentStringsW;
|
__msabi extern typeof(GetEnvironmentStrings) *const __imp_GetEnvironmentStringsW;
|
||||||
__msabi extern typeof(GetStdHandle) *const __imp_GetStdHandle;
|
__msabi extern typeof(GetStdHandle) *const __imp_GetStdHandle;
|
||||||
__msabi extern typeof(MapViewOfFileEx) *const __imp_MapViewOfFileEx;
|
__msabi extern typeof(MapViewOfFileEx) *const __imp_MapViewOfFileEx;
|
||||||
|
__msabi extern typeof(ReadFile) *const __imp_ReadFile;
|
||||||
__msabi extern typeof(SetConsoleCP) *const __imp_SetConsoleCP;
|
__msabi extern typeof(SetConsoleCP) *const __imp_SetConsoleCP;
|
||||||
__msabi extern typeof(SetConsoleMode) *const __imp_SetConsoleMode;
|
__msabi extern typeof(SetConsoleMode) *const __imp_SetConsoleMode;
|
||||||
__msabi extern typeof(SetConsoleOutputCP) *const __imp_SetConsoleOutputCP;
|
__msabi extern typeof(SetConsoleOutputCP) *const __imp_SetConsoleOutputCP;
|
||||||
|
@ -147,22 +172,8 @@ __msabi static textwindows int OnEarlyWinCrash(struct NtExceptionPointers *ep) {
|
||||||
__builtin_unreachable();
|
__builtin_unreachable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// this makes it possible for our read() implementation to periodically
|
__msabi static textwindows bool ProxyStdin(void) {
|
||||||
// poll for signals while performing a blocking overlapped io operation
|
return true;
|
||||||
__msabi static textwindows void ReopenConsoleForOverlappedIo(void) {
|
|
||||||
uint32_t mode;
|
|
||||||
int64_t hOld, hNew;
|
|
||||||
hOld = __imp_GetStdHandle(kNtStdInputHandle);
|
|
||||||
if (__imp_GetConsoleMode(hOld, &mode)) {
|
|
||||||
hNew = __imp_CreateFileW(u"CONIN$", kNtGenericRead | kNtGenericWrite,
|
|
||||||
kNtFileShareRead | kNtFileShareWrite,
|
|
||||||
&kNtIsInheritable, kNtOpenExisting,
|
|
||||||
kNtFileFlagOverlapped, 0);
|
|
||||||
if (hNew != kNtInvalidHandleValue) {
|
|
||||||
__imp_SetStdHandle(kNtStdInputHandle, hNew);
|
|
||||||
__imp_CloseHandle(hOld);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this ensures close(1) won't accidentally close(2) for example
|
// this ensures close(1) won't accidentally close(2) for example
|
||||||
|
@ -196,8 +207,6 @@ __msabi static textwindows wontreturn void WinMainNew(const char16_t *cmdline) {
|
||||||
intptr_t stackaddr, allocaddr;
|
intptr_t stackaddr, allocaddr;
|
||||||
version = NtGetPeb()->OSMajorVersion;
|
version = NtGetPeb()->OSMajorVersion;
|
||||||
__oldstack = (intptr_t)__builtin_frame_address(0);
|
__oldstack = (intptr_t)__builtin_frame_address(0);
|
||||||
ReopenConsoleForOverlappedIo();
|
|
||||||
DeduplicateStdioHandles();
|
|
||||||
if ((intptr_t)v_ntsubsystem == kNtImageSubsystemWindowsCui && version >= 10) {
|
if ((intptr_t)v_ntsubsystem == kNtImageSubsystemWindowsCui && version >= 10) {
|
||||||
rc = __imp_SetConsoleCP(kNtCpUtf8);
|
rc = __imp_SetConsoleCP(kNtCpUtf8);
|
||||||
NTTRACE("SetConsoleCP(kNtCpUtf8) → %hhhd", rc);
|
NTTRACE("SetConsoleCP(kNtCpUtf8) → %hhhd", rc);
|
||||||
|
@ -268,6 +277,10 @@ __msabi textwindows int64_t WinMain(int64_t hInstance, int64_t hPrevInstance,
|
||||||
os = _HOSTWINDOWS; /* madness https://news.ycombinator.com/item?id=21019722 */
|
os = _HOSTWINDOWS; /* madness https://news.ycombinator.com/item?id=21019722 */
|
||||||
kStartTsc = rdtsc();
|
kStartTsc = rdtsc();
|
||||||
__pid = __imp_GetCurrentProcessId();
|
__pid = __imp_GetCurrentProcessId();
|
||||||
|
DeduplicateStdioHandles();
|
||||||
|
if (_weaken(WinMainStdin)) {
|
||||||
|
_weaken(WinMainStdin)();
|
||||||
|
}
|
||||||
#if !IsTiny()
|
#if !IsTiny()
|
||||||
__wincrashearly =
|
__wincrashearly =
|
||||||
__imp_AddVectoredExceptionHandler(1, (void *)OnEarlyWinCrash);
|
__imp_AddVectoredExceptionHandler(1, (void *)OnEarlyWinCrash);
|
||||||
|
|
|
@ -54,7 +54,7 @@ textwindows int sys_socketpair_nt(int family, int type, int proto, int sv[2]) {
|
||||||
return eopnotsupp();
|
return eopnotsupp();
|
||||||
}
|
}
|
||||||
|
|
||||||
CreatePipeName(pipename);
|
__create_pipe_name(pipename);
|
||||||
__fds_lock();
|
__fds_lock();
|
||||||
reader = __reservefd_unlocked(-1);
|
reader = __reservefd_unlocked(-1);
|
||||||
writer = __reservefd_unlocked(-1);
|
writer = __reservefd_unlocked(-1);
|
||||||
|
|
|
@ -1625,7 +1625,7 @@ static char *FinishGeneratingDosHeader(char *p) {
|
||||||
p = WRITE16LE(p, 0); // 38
|
p = WRITE16LE(p, 0); // 38
|
||||||
|
|
||||||
// terminate the shell quote started earlier in the ape magic. the big
|
// terminate the shell quote started earlier in the ape magic. the big
|
||||||
// concern with shell script quoting is that binary content mimght get
|
// concern with shell script quoting, is that binary content might get
|
||||||
// generated in the dos stub which has an ascii value that is the same
|
// generated in the dos stub which has an ascii value that is the same
|
||||||
// as the end of quote. using a longer terminator reduces it to a very
|
// as the end of quote. using a longer terminator reduces it to a very
|
||||||
// low order of probability. tacking on an unpredictable deterministic
|
// low order of probability. tacking on an unpredictable deterministic
|
||||||
|
|
Loading…
Reference in a new issue