diff --git a/Makefile b/Makefile index 3b1c74594..82160201f 100644 --- a/Makefile +++ b/Makefile @@ -247,6 +247,8 @@ HOST_LIBS=-lm -ldl -lpthread LIBS=-lm -lpthread ifndef CONFIG_WIN32 LIBS+=-ldl +else +LIBS+=-lws2_32 endif LIBS+=$(EXTRA_LIBS) diff --git a/doc/globals.d.ts b/doc/globals.d.ts new file mode 100644 index 000000000..173768db4 --- /dev/null +++ b/doc/globals.d.ts @@ -0,0 +1,14 @@ +/** Command line arguments where argument[0] is the JS script name. */ +declare const scriptArgs: string[]; +/** Print args separated by spaces and a trailing newline. */ +declare function print(...args: any[]): void; + +declare const console: { + /** Print args separated by spaces and a trailing newline. */ + log: typeof print +}; + +interface ImportMeta { + url: string; + main: boolean; +} diff --git a/doc/os.d.ts b/doc/os.d.ts new file mode 100644 index 000000000..4a444300f --- /dev/null +++ b/doc/os.d.ts @@ -0,0 +1,290 @@ +/** + * Provides Operating System specific functions: + * file access, sockets, signals, timers, async I/O, workers (threads) + */ +declare module "os" { + type Success = 0; + type Errno = number; + type NegativeErrno = number; + type Result = T | NegativeErrno; + type ExitStatus = number; + type WaitStatus = number; + type Branded = T & { __brand: B }; // Prevent interchangeable primitives (e.g. number) + type OpenOption = Branded; + type SocketFamily = Branded; + type SocketType = Branded; + type SocketOpt = Branded; + type SocketShutOpt = Branded; + type PollOpt = Branded; + type PollRet = Branded; + type WaitpidOpt = Branded; + type FileDescriptor = Branded; + type Signal = Branded; + type StatMode = Branded; + type Pid = Branded; + type TimerHandle = Branded; + type Callback = () => void; + type Platform = "linux" | "darwin" | "win32" | "js"; + type WorkerMessage = any; + + interface ExecOptions { + /** + * Boolean (default = `true`). If `true`, wait until the process is + * terminated. In this case, exec return the exit code if positive or the + * negated signal number if the process was interrupted by a signal. If + * false, do not block and return the process id of the child. + */ + block?: boolean; + /** Is the file searched in the PATH @default true. */ + usePath?: boolean; + /** Set the file to be executed @default args[0] */ + file?: string, + /** The working directory of the new process */ + cwd?: string, + /** Handler for stdin */ + stdin?: FileDescriptor, + /** Handler for stdout */ + stdout?: FileDescriptor, + /** Handler for stderr */ + stderr?: FileDescriptor, + /** set the process environment from the object key-value pairs. + * If unset, it will inherit them from current process. */ + env?: { [key: string]: string }, + /** Process uid with `setuid` */ + uid?: number, + /** Process gid with `setgid` */ + gid?: number, + } + type ExecNonBlockingOptions = ExecOptions & { block?: false }; + type ExecBlockingOptions = ExecOptions & { block: true }; + + interface Stat { + dev: number; + ino: number; + mode: StatMode; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blocks: number; + /** milliseconds since 1970 */ + atime: number; + /** milliseconds since 1970 */ + mtime: number; + /** milliseconds since 1970 */ + ctime: number; + } + interface SocketAddr { + /** @example 80 */ + port: number; + /** @example "8.8.8.8" or "::1" */ + addr: string; + /** @default AF_INET */ + family: SocketFamily; + } + interface SocketAddrInfo extends SocketAddr { + socktype: SocketType; + } + interface HintAddr { + /** @example 80 or "http" */ + service?: number | string; + /** @default any */ + family?: SocketFamily; + /** @default any */ + socktype?: SocketType; + } + /* POSIX constants (Windows-specific) */ + export const O_TEXT: OpenOption; + /* POSIX constants */ + export const O_APPEND: OpenOption; + export const O_CREAT: OpenOption; + export const O_EXCL: OpenOption; + export const O_RDONLY: OpenOption; + export const O_RDWR: OpenOption; + export const O_TRUNC: OpenOption; + export const O_WRONLY: OpenOption; + export const S_IFBLK: StatMode; + export const S_IFCHR: StatMode; + export const S_IFDIR: StatMode; + export const S_IFIFO: StatMode; + export const S_IFLNK: StatMode; + export const S_IFMT: StatMode; + export const S_IFREG: StatMode; + export const S_IFSOCK: StatMode; + export const S_ISGID: StatMode; + export const S_ISUID: StatMode; + export const SIGABRT: Signal; + export const SIGALRM: Signal; + export const SIGCHLD: Signal; + export const SIGCONT: Signal; + export const SIGFPE: Signal; + export const SIGILL: Signal; + export const SIGINT: Signal; + export const SIGPIPE: Signal; + export const SIGQUIT: Signal; + export const SIGSEGV: Signal; + export const SIGSTOP: Signal; + export const SIGTERM: Signal; + export const SIGTSTP: Signal; + export const SIGTTIN: Signal; + export const SIGTTOU: Signal; + export const SIGUSR1: Signal; + export const SIGUSR2: Signal; + export const WNOHANG: WaitpidOpt; + export const AF_INET: SocketFamily; + export const AF_INET6: SocketFamily; + export const SOCK_STREAM: SocketType; + export const SOCK_DGRAM: SocketType; + export const SOCK_RAW: SocketType; + //export const SOCK_BLOCK: SocketType; // SOCK_NONBLOCK + export const SO_REUSEADDR: SocketOpt; + export const SO_ERROR: SocketOpt; + export const SO_RCVBUF: SocketOpt; + export const SHUT_RD: SocketShutOpt; + export const SHUT_WR: SocketShutOpt; + export const SHUT_RDWR: SocketShutOpt; + /** string representing the platform. */ + export const platform: Platform; + /** Open a file. Return a handle or `< 0` if error. */ + export function open(filename: string, flags?: OpenOption, mode?: number): Result; + /** Close the file handle `fd`. */ + export function close(fd: FileDescriptor): Result; + /** Seek in the file. Use `std.SEEK_*` for whence */ + export function seek(fd: FileDescriptor, offset: number, whence: number): Result; + export function seek(fd: FileDescriptor, offset: bigint, whence: number): Result; + /** Read `length` bytes from the file handle `fd` to the `ArrayBuffer` buffer at byte position `offset` */ + export function read(fd: FileDescriptor, buffer: ArrayBuffer, offset: number, length: number): Result; + /** Write `length` bytes to the file handle `fd` from the ArrayBuffer `buffer` at byte position `offset` */ + export function write(fd: FileDescriptor, buffer: ArrayBuffer, offset: number, length: number): Result; + /** Return `true` is fd is a TTY (terminal) handle. */ + export function isatty(fd: FileDescriptor): boolean; + /** Return the TTY size as `[width, height]` or `null` if not available. */ + export function ttyGetWinSize(fd: FileDescriptor): [width: number, height: number] | null; + /** Set the TTY in raw mode. */ + export function ttySetRaw(fd: FileDescriptor): void; + /** Remove a file. */ + export function remove(filename: string): Result; + /** Rename a file. */ + export function rename(filename: string): Result; + /** Get the canonicalized absolute pathname of `path` */ + export function realpath(path: string): [absPath: string, code: Success | Errno]; + /** Return the current working directory */ + export function getcwd(): [cwd: string, code: Success | Errno]; + /** Change the current directory. Return 0 if OK or `-errno`. */ + export function chdir(): Result; + /** Create a directory at `path`. Return 0 if OK or `-errno`. */ + export function mkdir(path: string, mode?: number): Result; + /** Get a file status */ + export function stat(path: string): [fileStatus: Stat, code: Success | Errno] + /** Get a link status */ + export function lstat(path: string): [linkStatus: Stat, code: Success | Errno] + /** Change the access and modification times of the file `path` @returns ms since 1970. */ + export function utimes(path: string, atime: number, mtime: number): Result; + /** Create a link at `linkpath` containing the string `target`. */ + export function symlink(target: string, linkpath: string): Result; + /** Get link target */ + export function readlink(path: string): [linkTarget: string, code: Success | Errno]; + /** List directory entries */ + export function readdir(dirPath: string): [dirFilenames: string[], code: Success | Errno]; + /** Set the single `func` read handler to be called each time data can be written to `fd`. */ + export function setReadHandler(fd: FileDescriptor, func: Callback): void; + /** Remove the read handler for `fd`. */ + export function setReadHandler(fd: FileDescriptor, func: null): void; + /** Set the single `func` read handler to be called each time data can be written to `fd`. */ + export function setWriteHandler(fd: FileDescriptor, func: Callback): void; + /** Remove the write handler for `fd`. */ + export function setWriteHandler(fd: FileDescriptor, func: null): void; + /** Set the single `func` to be called when `signal` happens. Work in main thread only */ + export function signal(signal: Signal, func: Callback): void + /** Call the default handler when `signal` happens. */ + export function signal(signal: Signal, func: null): void + /** Ignore when `signal` happens. */ + export function signal(signal: Signal, func: undefined): void + /** Send the signal `sig` to the process `pid`. */ + export function kill(pid: Pid, signal: Signal): Result; + /** Execute a process with the arguments args. */ + export function exec(args: string[], options?: ExecBlockingOptions): Result; + /** Execute a process with the arguments args. */ + export function exec(args: string[], options: ExecNonBlockingOptions): Result; + /** `waitpid` Unix system call. */ + export function waitpid(pid: Pid, options: WaitpidOpt): [ret: Result, status: WaitStatus]; + /** `getpid` Unix system call. */ + export function getpid(): [Pid]; + /** `dup` Unix system call. */ + export function dup(fd: FileDescriptor): Result; + /** `dup2` Unix system call. */ + export function dup2(oldFd: FileDescriptor, newFd: FileDescriptor): Result; + /** `pipe` Unix system call. */ + export function pipe(): [readFd: FileDescriptor, writeFd: FileDescriptor] | null; + /** Sleep during `delay_ms` milliseconds. */ + export function sleep(delay_ms: number): Result; + /** Sleep during `delay_ms` milliseconds. */ + export function sleepAsync(delay_ms: number): Promise>; + /** Call the function func after `delay` ms. */ + export function setTimeout(func: Callback, delay: number): TimerHandle; + /** Cancel a timer. */ + export function clearTimeout(handle: TimerHandle): void; + /** Create a POSIX socket */ + export function socket(family: SocketFamily, type: SocketType): Result; + /** Get a socket option @example os.getsockopt(sock_srv, os.SO_RCVBUF, uintArr1.buffer); */ + export function getsockopt(sockfd: FileDescriptor, name: SocketOpt, data: ArrayBuffer): Result; + /** Set a socket option @example os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer); */ + export function setsockopt(sockfd: FileDescriptor, name: SocketOpt, data: ArrayBuffer): Result; + /** Get address information for a given node and/or service @example os.getaddrinfo("localhost", {family:os.AF_INET6}) */ + export function getaddrinfo(node?: string, hint?: HintAddr): Result>; + /** Get current address to which the socket sockfd is bound */ + export function getsockname(sockfd: FileDescriptor): Result; + /** Bind socket to a specific address */ + export function bind(sockfd: FileDescriptor, addr: SocketAddr): Result; + /** Mark `sockfd` as passive socket that will `accept()` a `backlog` number of incoming connection (SOMAXCONN by default). */ + export function listen(sockfd: FileDescriptor, backlog?: number): Result; + /** Shut down part of a full-duplex connection */ + export function shutdown(sockfd: FileDescriptor, type: SocketShutOpt): Result; + /** Accept incoming connections */ + export function accept(sockfd: FileDescriptor): Promise<[remotefd: FileDescriptor, remoteaddr: SocketAddr]>; + /** Connect `sockfd` to `addr` */ + export function connect(sockfd: FileDescriptor, addr: SocketAddr): Promise>; + /** Send `length` byte from `buffer` on `sockfd` @returns bytes sent or <0 if error */ + export function send(sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number): Promise>; + /** Receive `length` byte in `buffer` from `sockfd` @returns bytes received or <0 if error */ + export function recv(sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number): Promise>; + /** Send `length` byte from `buffer` on `sockfd` to `addr` @returns bytes sent or <0 if error */ + export function sendto(sockfd: FileDescriptor, addr: SocketAddr, buffer: ArrayBuffer, length?: number): Promise>; + /** Receive `length` byte in `buffer` from `sockfd` @returns bytes received or <0 if error, and remote address used */ + export function recvfrom(sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number): Promise<[total: Result, from: SocketAddr]>; + + export class Worker { + /** + * In the created worker, `Worker.parent` represents the parent worker + * and is used to send or receive messages. + */ + static parent?: Worker; + /** + * Constructor to create a new thread (worker) with an API close to + * the `WebWorkers`. `module_filename` is a string specifying the + * module filename which is executed in the newly created thread. As + * for dynamically imported module, it is relative to the current + * script or module path. Threads normally don’t share any data and + * communicate between each other with messages. Nested workers are + * not supported. An example is available in `tests/test_worker.js`. + */ + constructor(module_filename: string); + /** + * Send a message to the corresponding worker. msg is cloned in the + * destination worker using an algorithm similar to the HTML structured + * clone algorithm. SharedArrayBuffer are shared between workers. + * + * Current limitations: `Map` and `Set` are not supported yet. + */ + postMessage(msg: WorkerMessage): void; + /** + * Getter and setter. Set a function which is called each time a + * message is received. The function is called with a single argument. + * It is an object with a `data` property containing the received + * message. The thread is not terminated if there is at least one non + * `null` onmessage handler. + */ + onmessage: (msg: WorkerMessage) => void; + } +} diff --git a/doc/std.d.ts b/doc/std.d.ts new file mode 100644 index 000000000..c4716a32b --- /dev/null +++ b/doc/std.d.ts @@ -0,0 +1,279 @@ +/** + * The std module provides wrappers to the libc stdlib.h and stdio.h and a + * few other utilities. + */ +declare module "std" { + import { FileDescriptor, ExitStatus, Errno } from "os"; + + /** + * FILE prototype + */ + export interface FILE { + /** + * Close the file. Return 0 if OK or `-errno` in case of I/O error. + */ + close(): number + /** + * Outputs the string with the UTF-8 encoding. + */ + puts(str: string): void; + /** + * Formatted printf. + * + * The same formats as the standard C library printf are supported. + * Integer format types (e.g. `%d`) truncate the Numbers or BigInts to 32 + * bits. Use the `l` modifier (e.g. `%ld`) to truncate to 64 bits. + */ + printf(format: string, ...args: any[]): number; + /** + * Flush the buffered file. + */ + flush(): void; + /** + * Seek to a give file position (whence is `std.SEEK_*`). `offset` can + * be a number or a bigint. Return 0 if OK or `-errno` in case of I/O + * error. + */ + seek(offset: number, whence: number): number; + /** + * Return the current file position. + */ + tell(): number; + /** + * Return the current file position as a bigint. + */ + tello(): bigint; + /** + * Return true if end of file. + */ + eof(): boolean; + /** + * Return the associated OS handle. + */ + fileno(): FileDescriptor; + /** + * Return true if there was an error. + */ + error(): boolean; + /** + * Clear the error indication. + */ + clearerr(): void; + /** + * Read `length` bytes from the file to the ArrayBuffer `buffer` at + * byte position `position` (wrapper to the libc `fread`). + */ + read(buffer: ArrayBuffer, position: number, length: number): number; + /** + * Write `length` bytes to the file from the ArrayBuffer `buffer` at + * byte position position (wrapper to the libc `fwrite`). + */ + write(buffer: ArrayBuffer, postion: number, length: number): number; + /** + * Return the next line from the file, assuming UTF-8 encoding, excluding + * the trailing line feed. + */ + getline(): string; + /** + * Read `max_size` bytes from the file and return them as a string + * assuming UTF-8 encoding. If `max_size` is not present, the file is + * read up its end. + */ + readAsString(max_size?: number): string; + /** + * Return the next byte from the file. Return -1 if the end of file is + * reached. + */ + getByte(): number + /** + * Write one byte to the file. + */ + putByte(c: number): number; + } + + export interface EvalOptions { + /** + * Boolean (default = `false`). If `true`, error backtraces do not list + * the stack frames below the evalScript. + */ + backtrace_barrier?: boolean; + } + + export interface ErrorObj { + errno?: number; + } + + export interface UrlGetOptions { + /** + * Boolean (default = `false`). If `true`, the response is an + * ArrayBuffer instead of a string. When a string is returned, the + * data is assumed to be UTF-8 encoded. + */ + binary?: boolean; + /** + * Boolean (default = `false`). If `true`, return the an object contains + * the properties response (response content), responseHeaders (headers + * separated by CRLF), status (status code). response is null is case of + * protocol or network error. If full is false, only the response is + * returned if the status is between 200 and 299. Otherwise null is + * returned. + */ + full?: boolean; + } + + export interface UrlGetResponse { + response: T | null; + status: number; + responseHeaders: string; + } + + /** + * Result that either represents a FILE or null on error. + */ + export type FILEResult = FILE | null; + + /** + * Exit the process. + */ + export function exit(n: ExitStatus): never; + /** + * Evaluate the string `str` as a script (global eval). + */ + export function evalScript(str: string, options?: EvalOptions): any; + /** + * Evaluate the file filename as a script (global eval). + */ + export function loadScript(filename: string): any; + /** + * Load the file filename and return it as a string assuming UTF-8 + * encoding. Return `null` in case of I/O error. + */ + export function loadFile(filename: string): string | null; + /** + * Open a file (wrapper to the libc fopen()). Return the FILE object or + * `null` in case of I/O error. If errorObj is not undefined, set its + * `errno` property to the error code or to 0 if no error occured. + */ + export function open(filename: string, flags: string, errorObj?: ErrorObj): FILEResult; + /** + * Open a process by creating a pipe (wrapper to the libc `popen()`). + * Return the `FILE` object or `null` in case of I/O error. If `errorObj` + * is not `undefined`, set its `errno` property to the error code or to 0 + * if no error occured. + */ + export function popen(command: string, flags: string, errorObj?: ErrorObj): FILEResult; + /** + * Open a file from a file handle (wrapper to the libc `fdopen()`). Return + * the `FILE` object or `null` in case of I/O error. If `errorObj` is not + * `undefined`, set its errno property to the error code or to 0 if no + * error occured. + */ + export function fdopen(fd: FileDescriptor, flags: string, errorObj?: ErrorObj): FILEResult; + /** + * Open a temporary file. Return the `FILE` object or `null` in case of I/O + * error. If `errorObj` is not undefined, set its `errno` property to the + * error code or to 0 if no error occured. + */ + export function tmpfile(errorObj?: ErrorObj): FILE; + /** + * Equivalent to `std.out.puts(str)`. + */ + export const puts: typeof out.puts; + /** + * Equivalent to `std.out.printf(fmt, ...args)`. + */ + export const printf: typeof out.printf; + /** + * Equivalent to the libc `sprintf()`. + */ + export function sprintf(format: string, ...args: any[]): string; + const $in: FILE; + /** + * Wrappers to the libc file `stdin`, `stdout`, `stderr`. + */ + export { $in as in }; + /** + * Wrappers to the libc file `stdin`, `stdout`, `stderr`. + */ + export const out: FILE; + /** + * Wrappers to the libc file `stdin`, `stdout`, `stderr`. + */ + export const err: FILE; + /** + * Constants for seek(). + */ + export const SEEK_CUR: number; + /** + * Constants for seek(). + */ + export const SEEK_END: number; + /** + * Constants for seek(). + */ + export const SEEK_SET: number; + /** + * Enumeration object containing the integer value of common errors + * (additional error codes may be defined): + */ + export const Error: { + readonly EACCES: number, + readonly ENOENT: number, + readonly EBADF: number, + readonly ENOSPC: number, + readonly EBUSY: number, + readonly ENOSYS: number, + readonly EEXIST: number, + readonly EPERM: number, + readonly EINVAL: number, + readonly EPIPE: number, + readonly EIO: number, + readonly EAGAIN: number, + readonly EINPROGRESS: number, + readonly EWOULDBLOCK: number, + }; + /** + * Return a string that describes the error `errno`. + */ + export function strerror(errno: Errno): string; + /** + * Manually invoke the cycle removal algorithm. The cycle removal + * algorithm is automatically started when needed, so this function is + * useful in case of specific memory constraints or for testing. + */ + export function gc(): void; + /** + * Return the value of the environment variable `name` or `undefined` if it + * is not defined. + */ + export function getenv(name: string): string | undefined; + /** + * Set the value of the environment variable `name` to the string `value`. + */ + export function setenv(name: string, value: string): void; + /** + * Delete the environment variable `name`. + */ + export function unsetenv(name: string): void; + /** + * Return an object containing the environment variables as key-value pairs. + */ + export function getenviron(): { [key: string]: string }; + /** Download url using the curl command line utility. */ + export function urlGet(url: string, options?: UrlGetOptions): string | null; + export function urlGet(url: string, options: UrlGetOptions & { full?: false, binary: true }): ArrayBuffer | null; + export function urlGet(url: string, options: UrlGetOptions & { full: true, binary?: false }): UrlGetResponse; + export function urlGet(url: string, options: UrlGetOptions & { full: true, binary: true }): UrlGetResponse; + /** + * Parse `str` using a superset of `JSON.parse`. The following extensions are accepted: + * + * - Single line and multiline comments + * - unquoted properties (ASCII-only Javascript identifiers) + * - trailing comma in array and object definitions + * - single quoted strings + * - \\f and \\v are accepted as space characters + * - leading plus in numbers + * - octal (0o prefix) and hexadecimal (0x prefix) numbers + */ + export function parseExtJSON(str: string): any; +} diff --git a/examples/http_client.js b/examples/http_client.js new file mode 100644 index 000000000..33a348fcf --- /dev/null +++ b/examples/http_client.js @@ -0,0 +1,25 @@ +#!/usr/bin/env qjs +///@ts-check +/// +/// +/// +import * as os from "os"; +import * as std from "std"; + +/** @template T @param {os.Result} result */ +function must(result) { + if (typeof result === "number" && result < 0) throw new Error(std.strerror(-result)); + return /** @type {T} */ (result) +} +//USAGE: client.js wttr.in/paris +const uriRegexp = /^(?[A-Za-z0-9\-\.]+)(?:[0-9]+)?(?.*)$/; +const { host = "bellard.org", port = ":80", query = "/" } = scriptArgs[1]?.match(uriRegexp)?.groups || {}; +console.log("sending GET on",{ host, port, query }) +const [addr] = must(os.getaddrinfo(host, { service: port.slice(1) })); +const sockfd = must(os.socket(addr.family, addr.socktype)); +await os.connect(sockfd, addr); +const httpReq = Uint8Array.from(`GET ${query||'/'} HTTP/1.0\r\nHost: ${host}\r\nUser-Agent: curl\r\n\r\n`, c => c.charCodeAt(0)) +must(await os.send(sockfd, httpReq.buffer) > 0); +const chunk = new Uint8Array(512); +const recvd = await os.recv(sockfd, chunk.buffer); +console.log([...chunk.slice(0, recvd)].map(c => String.fromCharCode(c)).join('')); diff --git a/examples/http_server.js b/examples/http_server.js new file mode 100755 index 000000000..b38265f14 --- /dev/null +++ b/examples/http_server.js @@ -0,0 +1,96 @@ +#!/usr/bin/env qjs +///@ts-check +/// +/// +/// +import * as os from "os"; +import * as std from "std";// for std.strerror + +const MIMES = new Map([ + ['html', 'text/html'], + ['txt', 'text/plain'], + ['css', 'text/css'], + ['c', 'text/plain'], + ['h', 'text/plain'], + ['json', 'application/json'], + ['mjs', 'application/javascript'], + ['js', 'application/javascript'], + ['', 'application/octet-stream'], +]); +/** @template T @param {os.Result} result */ +function must(result) { + if (typeof result === "number" && result < 0) throw new Error(std.strerror(-result)); + return /** @type {T} */ (result) +} +/**@param {os.FileDescriptor} fd */ +async function* recvLines(fd) { + const chunk = new Uint8Array(1); + let line = ''; + while (await os.recv(fd, chunk.buffer) > 0) { + const char = String.fromCharCode(...chunk); + if (char == '\n') { + yield line; + line = ''; + } else line += char; + } + if (line) yield line; +} +/** @param {os.FileDescriptor} fd @param {string[]} lines */ +function sendLines(fd, lines) { + const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0)); + return os.send(fd, buf.buffer); +} +//USAGE: qjs http_server.js [PORT=8080 [HOST=localhost]] +const [port = "8080", host = "localhost"] = scriptArgs.slice(1); +const [ai] = must(os.getaddrinfo(host, { service: port })); +//if (!ai.length) throw `Unable to getaddrinfo(${host}, ${port})`; +const sock_srv = must(os.socket(os.AF_INET, os.SOCK_STREAM)); +must(os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer)); +must(os.bind(sock_srv, ai)); +must(os.listen(sock_srv)); +//os.signal(os.SIGINT, ()=>os.close(sock_srv)); // don't work +console.log(`Listening on http://${host}:${port} (${ai.addr}:${ai.port}) ...`); +const openCmd = { linux: "xdg-open", darwin: "open", win32: "start" }[os.platform]; +if (openCmd && os.exec) os.exec([openCmd, `http://${host}:${port}`]); +while (true) { // TODO: break on SIG* + const [sock_cli] = await os.accept(sock_srv); + + const lines = recvLines(sock_cli); + const [method, path, http_ver] = ((await lines.next()).value || '').split(' '); + let safe_path = '.' + path.replaceAll(/\.+/g, '.'); // may += index.html later + console.log(method, safe_path, http_ver); + + const headers = new Map() + for await (const line of lines) { + const header = line.trimEnd(); + if (!header) break; + const sepIdx = header.indexOf(': '); + headers.set(header.slice(0, sepIdx), header.slice(sepIdx + 2)); + } + + let [obj, err] = os.stat(safe_path); + if (obj?.mode & os.S_IFDIR && safe_path.endsWith('/') && os.stat(safe_path + 'index.html')[0]) { + safe_path += 'index.html'; + [obj, err] = os.stat(safe_path); + } + if (err) { + await sendLines(sock_cli, ['HTTP/1.1 404', '', safe_path, 'errno:' + err]) + } else if (obj?.mode & os.S_IFDIR) { + if (!safe_path.endsWith('/')) + await sendLines(sock_cli, ['HTTP/1.1 301', `Location: ${safe_path}/`, '']); + else + await sendLines(sock_cli, ['HTTP/1.1 200', 'Content-Type: text/html', '', + os.readdir(safe_path)[0]?.filter(e => e[0] != '.').map(e => `
  • ${e}
  • `).join('') + ]); + } else { + const mime = MIMES.get(safe_path.split('.').at(-1) || '') || MIMES.get(''); + await sendLines(sock_cli, ['HTTP/1.1 200', `Content-Type: ${mime}`, '', '']); + const fd = must(os.open(safe_path)); + const fbuf = new Uint8Array(4096); + for (let got = 0; (got = os.read(fd, fbuf.buffer, 0, fbuf.byteLength)) > 0;) { + await os.send(sock_cli, fbuf.buffer, got); + } + } + + os.close(sock_cli); +} diff --git a/qjs.c b/qjs.c index a88e39ad4..016feb289 100644 --- a/qjs.c +++ b/qjs.c @@ -39,6 +39,11 @@ #elif defined(__FreeBSD__) #include #endif +#if defined(_WIN32) +#include +#include +#include +#endif #include "cutils.h" #include "quickjs-libc.h" diff --git a/quickjs-libc.c b/quickjs-libc.c index ca8e359cc..fb0f3e196 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -38,6 +38,8 @@ #include #include #if defined(_WIN32) +#include +#include #include #include #include @@ -46,6 +48,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #if defined(__FreeBSD__) extern char **environ; @@ -80,16 +88,37 @@ typedef sig_t sighandler_t; #define PATH_MAX 4096 #endif -/* TODO: - - add socket calls -*/ +typedef enum { + MAGIC_SOCKET_RECV, + MAGIC_SOCKET_SEND, + MAGIC_SOCKET_RECVFROM, + MAGIC_SOCKET_SENDTO, + MAGIC_SOCKET_CONNECT, + MAGIC_SOCKET_ACCEPT, +} MagicSocket; typedef struct { struct list_head link; int fd; - JSValue rw_func[2]; + JSValue r_func; + JSValue w_func; } JSOSRWHandler; +typedef struct { + struct list_head link; + int sockfd; + MagicSocket magic; + uint64_t length; + uint8_t* buffer; + JSValue bufval; + struct sockaddr_storage sockaddr; // for sendto() + JSValue resolve; + JSValue reject; +#ifdef _WIN32 + WSAEVENT event; // so os_pool can wait on it +#endif +} JSOSSockHandler; + typedef struct { struct list_head link; int sig_num; @@ -144,6 +173,7 @@ typedef struct { typedef struct JSThreadState { struct list_head os_rw_handlers; /* list of JSOSRWHandler.link */ + struct list_head os_sock_handlers; /* list of JSOSSockHandler.link */ struct list_head os_signal_handlers; /* list JSOSSignalHandler.link */ struct list_head os_timers; /* list of JSOSTimer.link */ struct list_head port_list; /* list of JSWorkerMessageHandler.link */ @@ -167,6 +197,93 @@ static BOOL my_isdigit(int c) return (c >= '0' && c <= '9'); } + +static int JS_ToSockaddrStruct(JSContext *ctx, JSValue addr, + struct sockaddr_storage *sockaddr, uint32_t sockfd) +{ + JSValue val; + const char *addr_str; + uint32_t port, family; + int ret; + + val = JS_GetPropertyStr(ctx, addr, "family"); + if (JS_IsException(val)) + return -1; + if (JS_IsUndefined(val)) { // get from sockfd when no .family given + struct sockaddr_storage saddr; + socklen_t len = sizeof(saddr); + if (getsockname(sockfd, (struct sockaddr *)&saddr, &len) != -1) { + family = saddr.ss_family; + } else { + family = AF_INET; + } + } else if (JS_ToUint32(ctx, &family, val)) + return -1; + sockaddr->ss_family = family; + JS_FreeValue(ctx, val); + + val = JS_GetPropertyStr(ctx, addr, "addr"); + if (JS_IsException(val)) + return -1; + addr_str = JS_ToCString(ctx, val); + if (!addr_str) + return -1; + void* sin_addr = family == AF_INET ? + (void*)&(((struct sockaddr_in *)sockaddr)->sin_addr): + (void*)&(((struct sockaddr_in6 *)sockaddr)->sin6_addr); + ret = inet_pton(sockaddr->ss_family, addr_str, sin_addr); + JS_FreeCString(ctx, addr_str); + JS_FreeValue(ctx, val); + if (ret != 1) + return -1; + + val = JS_GetPropertyStr(ctx, addr, "port"); + ret = JS_ToUint32(ctx, &port, val); + JS_FreeValue(ctx, val); + if (ret) + return -1; + if (family == AF_INET) + ((struct sockaddr_in *)sockaddr)->sin_port = htons(port); + if (family == AF_INET6) + ((struct sockaddr_in6 *)sockaddr)->sin6_port = htons(port); + + return 0; +} + +static JSValue JS_ToSockaddrObj(JSContext *ctx, struct sockaddr_storage *sockaddr) +{ + JSValue obj, prop; + char ip_str[INET6_ADDRSTRLEN]; // max(INET6_ADDRSTRLEN, INET_ADDRSTRLEN) + const char *ip_ptr; + struct sockaddr_in *sockaddr4 = (struct sockaddr_in *)sockaddr; + struct sockaddr_in6 *sockaddr6 = (struct sockaddr_in6 *)sockaddr; + + obj = JS_NewObject(ctx); + if (JS_IsException(obj)) + goto fail; + + prop = JS_NewUint32(ctx, sockaddr->ss_family); + JS_DefinePropertyValueStr(ctx, obj, "family", prop, JS_PROP_C_W_E); + + uint16_t sin_port = sockaddr->ss_family == AF_INET ? sockaddr4->sin_port : + sockaddr->ss_family == AF_INET6 ? sockaddr6->sin6_port : 0; + prop = JS_NewUint32(ctx, ntohs(sin_port)); + JS_DefinePropertyValueStr(ctx, obj, "port", prop, JS_PROP_C_W_E); + + void* sin_addr = sockaddr->ss_family == AF_INET ? (void*)&sockaddr4->sin_addr : + sockaddr->ss_family == AF_INET6 ? (void*)&sockaddr6->sin6_addr : NULL; + size_t sin_len = sockaddr->ss_family == AF_INET ? INET_ADDRSTRLEN: + sockaddr->ss_family == AF_INET6 ? INET6_ADDRSTRLEN: 0 ; + ip_ptr = inet_ntop(sockaddr->ss_family, sin_addr, ip_str, sin_len); + prop = ip_ptr ? JS_NewString(ctx, ip_ptr) : JS_NULL; + JS_DefinePropertyValueStr(ctx, obj, "addr", prop, JS_PROP_C_W_E); + return obj; + + fail: + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; +} + /* XXX: use 'o' and 'O' for object using JS_PrintValue() ? */ static JSValue js_printf_internal(JSContext *ctx, int argc, JSValueConst *argv, FILE *fp) @@ -928,6 +1045,18 @@ static ssize_t js_get_errno(ssize_t ret) return ret; } +static ssize_t js_get_sockerrno(ssize_t ret) +{ + #if defined(_WIN32) + if (ret == -1 || ret == INVALID_SOCKET) + ret = -WSAGetLastError(); + #else + if (ret == -1) + ret = -errno; + #endif + return ret; +} + static JSValue js_std_strerror(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { @@ -1614,6 +1743,9 @@ static const JSCFunctionListEntry js_std_error_props[] = { DEF(EPERM), DEF(EPIPE), DEF(EBADF), + DEF(EAGAIN), + DEF(EINPROGRESS), + DEF(EWOULDBLOCK), #undef DEF }; @@ -1976,20 +2108,27 @@ static JSOSRWHandler *find_rh(JSThreadState *ts, int fd) static void free_rw_handler(JSRuntime *rt, JSOSRWHandler *rh) { - int i; list_del(&rh->link); - for(i = 0; i < 2; i++) { - JS_FreeValueRT(rt, rh->rw_func[i]); - } + JS_FreeValueRT(rt, rh->r_func); + JS_FreeValueRT(rt, rh->w_func); js_free_rt(rt, rh); } -static JSValue js_os_setReadHandler(JSContext *ctx, JSValueConst this_val, +static void free_sock_handler(JSRuntime *rt, JSOSSockHandler *sh) +{ + list_del(&sh->link); + JS_FreeValueRT(rt, sh->resolve); + JS_FreeValueRT(rt, sh->reject); + js_free_rt(rt, sh); +} + +static JSValue js_os_setReadWriteHandler(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = JS_GetRuntimeOpaque(rt); JSOSRWHandler *rh; + BOOL isRead = magic == 0; int fd; JSValueConst func; @@ -1999,10 +2138,10 @@ static JSValue js_os_setReadHandler(JSContext *ctx, JSValueConst this_val, if (JS_IsNull(func)) { rh = find_rh(ts, fd); if (rh) { - JS_FreeValue(ctx, rh->rw_func[magic]); - rh->rw_func[magic] = JS_NULL; - if (JS_IsNull(rh->rw_func[0]) && - JS_IsNull(rh->rw_func[1])) { + JSValue* rwfunc = isRead ? &rh->r_func : &rh->w_func; + JS_FreeValue(ctx, *rwfunc); + *rwfunc = JS_NULL; + if (JS_IsNull(rh->r_func) && JS_IsNull(rh->w_func)) { /* remove the entry */ free_rw_handler(JS_GetRuntime(ctx), rh); } @@ -2016,12 +2155,13 @@ static JSValue js_os_setReadHandler(JSContext *ctx, JSValueConst this_val, if (!rh) return JS_EXCEPTION; rh->fd = fd; - rh->rw_func[0] = JS_NULL; - rh->rw_func[1] = JS_NULL; + rh->r_func = JS_NULL; + rh->w_func = JS_NULL; list_add_tail(&rh->link, &ts->os_rw_handlers); } - JS_FreeValue(ctx, rh->rw_func[magic]); - rh->rw_func[magic] = JS_DupValue(ctx, func); + JSValue* rwfunc = isRead ? &rh->r_func : &rh->w_func; + JS_FreeValue(ctx, *rwfunc); + *rwfunc = JS_DupValue(ctx, func); } return JS_UNDEFINED; } @@ -2327,8 +2467,7 @@ static void js_waker_close(JSWaker *w) static void js_free_message(JSWorkerMessage *msg); /* return 1 if a message was handled, 0 if no message */ -static int handle_posted_message(JSRuntime *rt, JSContext *ctx, - JSWorkerMessageHandler *port) +static int handle_posted_message(JSContext *ctx, JSWorkerMessageHandler *port) { JSWorkerMessagePipe *ps = port->recv_pipe; int ret; @@ -2383,13 +2522,85 @@ static int handle_posted_message(JSRuntime *rt, JSContext *ctx, return ret; } #else -static int handle_posted_message(JSRuntime *rt, JSContext *ctx, - JSWorkerMessageHandler *port) +static int handle_posted_message(JSContext *ctx, JSWorkerMessageHandler *port) { return 0; } #endif /* !USE_WORKER */ +static void handle_socket_message(JSContext *ctx, JSOSSockHandler *sh) +{ + #ifdef _WIN32 + WSANETWORKEVENTS netEvents; + WSAEnumNetworkEvents(sh->sockfd, sh->event, &netEvents); + #endif + + int err = 0; + struct sockaddr_storage sockaddr; + socklen_t addr_len = sizeof(sockaddr); + + if (sh->magic == MAGIC_SOCKET_RECV) { + err = js_get_sockerrno(recv(sh->sockfd, (char*) sh->buffer, sh->length, 0)); + } else if (sh->magic == MAGIC_SOCKET_SEND) { + err = js_get_sockerrno(send(sh->sockfd, (char*) sh->buffer, sh->length, 0)); + } else if (sh->magic == MAGIC_SOCKET_RECVFROM) { + err = js_get_sockerrno(recvfrom(sh->sockfd, (char*) sh->buffer, sh->length, 0, (struct sockaddr *)&sockaddr, &addr_len)); + } else if (sh->magic == MAGIC_SOCKET_SENDTO) { + err = js_get_sockerrno(sendto(sh->sockfd, (char*) sh->buffer, sh->length, 0, (const struct sockaddr *)&sh->sockaddr, addr_len)); + } else if (sh->magic == MAGIC_SOCKET_CONNECT) { + #ifdef _WIN32 + err = 0; + #else + socklen_t len = sizeof(err); + err = getsockopt(sh->sockfd, SOL_SOCKET, SO_ERROR, (char*) &err, &len) ? -errno : -err; + #endif + } else if (sh->magic == MAGIC_SOCKET_ACCEPT) { + err = js_get_sockerrno(accept(sh->sockfd, (struct sockaddr *)&sockaddr, &addr_len)); + } + +#ifdef _WIN32 + if (err == -WSAEWOULDBLOCK) + return; +#else + if (err == -EAGAIN || err == -EWOULDBLOCK) + return; +#endif + JSValue promiseval = JS_UNDEFINED; + if (sh->magic == MAGIC_SOCKET_ACCEPT) { + promiseval = JS_NewArray(ctx); + if (JS_IsException(promiseval)) + return; + JS_DefinePropertyValueUint32(ctx, promiseval, 0, JS_NewInt32(ctx, err), JS_PROP_C_W_E); + JS_DefinePropertyValueUint32(ctx, promiseval, 1, JS_ToSockaddrObj(ctx, &sockaddr), JS_PROP_C_W_E); + } else if (sh->magic == MAGIC_SOCKET_RECVFROM) { + JSValue addrObj = JS_ToSockaddrObj(ctx, &sockaddr); + if (JS_IsException(addrObj)) + return; + promiseval = JS_NewArray(ctx); + if (JS_IsException(promiseval)) + return; + JS_DefinePropertyValueUint32(ctx, promiseval, 0, JS_NewInt64(ctx, err), JS_PROP_C_W_E); + JS_DefinePropertyValueUint32(ctx, promiseval, 1, addrObj, JS_PROP_C_W_E); + } else { + promiseval = JS_NewInt32(ctx, err); + } + + /* 'func' might be destroyed when calling itself (if it frees the + handler), so must take extra care */ + JSValue func = JS_DupValue(ctx, err < 0 ? sh->reject : sh->resolve); + JSValue retval = JS_Call(ctx, func, JS_UNDEFINED, 1, (JSValueConst *)&promiseval); + JS_FreeValue(ctx, func); + + if (JS_IsException(retval)) { + js_std_dump_error(ctx); + } else { + JS_FreeValue(ctx, retval); + } + JS_FreeValue(ctx, promiseval); + JS_FreeValue(ctx, sh->bufval); + free_sock_handler(JS_GetRuntime(ctx), sh); +} + #if defined(_WIN32) static int js_os_poll(JSContext *ctx) @@ -2404,8 +2615,10 @@ static int js_os_poll(JSContext *ctx) /* XXX: handle signals if useful */ - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) && - list_empty(&ts->port_list)) { + if (list_empty(&ts->os_rw_handlers) && + list_empty(&ts->os_timers) && + list_empty(&ts->port_list) && + list_empty(&ts->os_sock_handlers)) { return -1; /* no more events */ } @@ -2435,13 +2648,21 @@ static int js_os_poll(JSContext *ctx) count = 0; list_for_each(el, &ts->os_rw_handlers) { rh = list_entry(el, JSOSRWHandler, link); - if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) { + if (rh->fd == 0 && !JS_IsNull(rh->r_func)) { handles[count++] = (HANDLE)_get_osfhandle(rh->fd); // stdin if (count == (int)countof(handles)) break; } } + list_for_each(el, &ts->os_sock_handlers) { + JSOSSockHandler *sh = list_entry(el, JSOSSockHandler, link); + //TODO: socket readability don't seems to be a winsock event => trigger manually + handles[count++] = sh->event; + if (count == (int)countof(handles)) + break; + } + list_for_each(el, &ts->port_list) { JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); if (JS_IsNull(port->on_message_func)) @@ -2457,22 +2678,32 @@ static int js_os_poll(JSContext *ctx) timeout = min_delay; ret = WaitForMultipleObjects(count, handles, FALSE, timeout); + // why iterate on every list instead of just handles[ret] ? if (ret < count) { list_for_each(el, &ts->os_rw_handlers) { rh = list_entry(el, JSOSRWHandler, link); - if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) { - call_handler(ctx, rh->rw_func[0]); + if (rh->fd == 0 && !JS_IsNull(rh->r_func)) { + call_handler(ctx, rh->r_func); /* must stop because the list may have been modified */ goto done; } } + list_for_each(el, &ts->os_sock_handlers) { + JSOSSockHandler *sh = list_entry(el, JSOSSockHandler, link); + if (sh->event == handles[ret]) { + handle_socket_message(ctx, sh); + WSAResetEvent(sh->event); // WSACloseEvent(sh->event); + goto done; + } + } + list_for_each(el, &ts->port_list) { JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); if (!JS_IsNull(port->on_message_func)) { JSWorkerMessagePipe *ps = port->recv_pipe; if (ps->waker.handle == handles[ret]) { - if (handle_posted_message(rt, ctx, port)) + if (handle_posted_message(ctx, port)) goto done; } } @@ -2495,6 +2726,7 @@ static int js_os_poll(JSContext *ctx) int64_t cur_time, delay; fd_set rfds, wfds; JSOSRWHandler *rh; + JSOSSockHandler *sh; struct list_head *el; struct timeval tv, *tvp; @@ -2515,7 +2747,9 @@ static int js_os_poll(JSContext *ctx) } } - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->os_timers) && + if (list_empty(&ts->os_rw_handlers) && + list_empty(&ts->os_sock_handlers) && + list_empty(&ts->os_timers) && list_empty(&ts->port_list)) return -1; /* no more events */ @@ -2551,12 +2785,27 @@ static int js_os_poll(JSContext *ctx) list_for_each(el, &ts->os_rw_handlers) { rh = list_entry(el, JSOSRWHandler, link); fd_max = max_int(fd_max, rh->fd); - if (!JS_IsNull(rh->rw_func[0])) + if (!JS_IsNull(rh->r_func)) FD_SET(rh->fd, &rfds); - if (!JS_IsNull(rh->rw_func[1])) + if (!JS_IsNull(rh->w_func)) FD_SET(rh->fd, &wfds); } + list_for_each(el, &ts->os_sock_handlers) { + sh = list_entry(el, JSOSSockHandler, link); + fd_max = max_int(fd_max, sh->sockfd); + if (sh->magic == MAGIC_SOCKET_ACCEPT || + sh->magic == MAGIC_SOCKET_RECV || + sh->magic == MAGIC_SOCKET_RECVFROM){ + FD_SET(sh->sockfd, &rfds); + } + if (sh->magic == MAGIC_SOCKET_CONNECT || + sh->magic == MAGIC_SOCKET_SEND || + sh->magic == MAGIC_SOCKET_SENDTO){ + FD_SET(sh->sockfd, &wfds); + } + } + list_for_each(el, &ts->port_list) { JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); if (!JS_IsNull(port->on_message_func)) { @@ -2570,26 +2819,32 @@ static int js_os_poll(JSContext *ctx) if (ret > 0) { list_for_each(el, &ts->os_rw_handlers) { rh = list_entry(el, JSOSRWHandler, link); - if (!JS_IsNull(rh->rw_func[0]) && - FD_ISSET(rh->fd, &rfds)) { - call_handler(ctx, rh->rw_func[0]); + if (!JS_IsNull(rh->r_func) && FD_ISSET(rh->fd, &rfds)) { + call_handler(ctx, rh->r_func); /* must stop because the list may have been modified */ goto done; } - if (!JS_IsNull(rh->rw_func[1]) && - FD_ISSET(rh->fd, &wfds)) { - call_handler(ctx, rh->rw_func[1]); + if (!JS_IsNull(rh->w_func) && FD_ISSET(rh->fd, &wfds)) { + call_handler(ctx, rh->w_func); /* must stop because the list may have been modified */ goto done; } } + list_for_each(el, &ts->os_sock_handlers) { + sh = list_entry(el, JSOSSockHandler, link); + if (FD_ISSET(sh->sockfd, &rfds) || FD_ISSET(sh->sockfd, &wfds)) { + handle_socket_message(ctx, sh); + goto done; + } + } + list_for_each(el, &ts->port_list) { JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); if (!JS_IsNull(port->on_message_func)) { JSWorkerMessagePipe *ps = port->recv_pipe; if (FD_ISSET(ps->waker.read_fd, &rfds)) { - if (handle_posted_message(rt, ctx, port)) + if (handle_posted_message(ctx, port)) goto done; } } @@ -3401,6 +3656,265 @@ static JSValue js_os_dup2(JSContext *ctx, JSValueConst this_val, #endif /* !_WIN32 */ +static JSValue js_os_socket(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int domain, type, protocol = 0; + + if (JS_ToInt32(ctx, &domain, argv[0])) + return JS_EXCEPTION; + if (JS_ToInt32(ctx, &type, argv[1])) + return JS_EXCEPTION; + if (argc >= 3 && JS_ToInt32(ctx, &protocol, argv[2])) + return JS_EXCEPTION; + + int socketfd = socket(domain, type, protocol); + int ret = js_get_sockerrno(socketfd); + if (ret < 0) + return JS_NewInt32(ctx, ret); +#if defined(_WIN32) + u_long mode = 1; + ret = ioctlsocket(ret, FIONBIO, &mode); +#else + ret = fcntl(ret, F_SETFL, fcntl(ret, F_GETFL, 0) | O_NONBLOCK); +#endif + if (ret < 0) + return JS_NewInt32(ctx, ret); + return JS_NewInt32(ctx, socketfd); +} + +static JSValue js_os_get_setsockopt(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + int sock, optname, ret; + uint8_t *optval; + socklen_t optlen; + size_t buflen; + + if (JS_ToInt32(ctx, &sock, argv[0])) + return JS_EXCEPTION; + if (JS_ToInt32(ctx, &optname, argv[1])) + return JS_EXCEPTION; + optval = JS_GetArrayBuffer(ctx, &buflen, argv[2]); + if (!optval) + return JS_EXCEPTION; + optlen = buflen; + + if (magic == 0) + ret = js_get_sockerrno(getsockopt(sock, SOL_SOCKET, optname, (char*)optval, &optlen)); + else + ret = js_get_sockerrno(setsockopt(sock, SOL_SOCKET, optname, (char*)optval, optlen)); + return JS_NewInt32(ctx, ret); +} + +static JSValue js_os_getaddrinfo(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int ret = -1; + if (!JS_IsString(argv[0])) + return JS_EXCEPTION; + const char* node = JS_ToCString(ctx, argv[0]); + + const char* service = NULL; + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + if (!JS_IsNull(argv[1]) && JS_IsObject(argv[1])) { + JSValue prop_service = JS_GetPropertyStr(ctx, argv[1], "service"); + JSValue prop_family = JS_GetPropertyStr(ctx, argv[1], "family"); + JSValue prop_socktype = JS_GetPropertyStr(ctx, argv[1], "socktype"); + if (JS_IsException(prop_service) || + JS_IsException(prop_family) || + JS_IsException(prop_socktype)) + goto fail; + if (!JS_IsUndefined(prop_service)) + service = JS_ToCString(ctx, prop_service); + if (!JS_IsUndefined(prop_family)) + JS_ToInt32(ctx, &hints.ai_family, prop_family); + if (!JS_IsUndefined(prop_socktype)) + JS_ToInt32(ctx, &hints.ai_socktype, prop_socktype); + } + + struct addrinfo *ai; + ret = js_get_sockerrno(getaddrinfo(node, service, &hints, &ai)); + if (ret) + goto fail; + + struct addrinfo *it; + socklen_t objLen; + JSValue obj = JS_NewArray(ctx); + for (objLen = 0, it = ai; it; it = it->ai_next, objLen++) { + JSValue addrObj = JS_ToSockaddrObj(ctx,(struct sockaddr_storage *)it->ai_addr); + JSValue prop_socktype = JS_NewUint32(ctx, it->ai_socktype); + JS_DefinePropertyValueStr(ctx, addrObj, "socktype", prop_socktype, JS_PROP_C_W_E); + JS_SetPropertyUint32(ctx,obj,objLen,addrObj); + } + + freeaddrinfo(ai); + JS_FreeCString(ctx, service); + JS_FreeCString(ctx, node); + return obj; +fail: + JS_FreeValue(ctx, obj); + JS_FreeCString(ctx, service); + JS_FreeCString(ctx, node); + return JS_NewInt32(ctx, ret); +} + +static JSValue js_os_bind(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int sockfd, ret = 0; + struct sockaddr_storage sockaddr; + + if (JS_ToInt32(ctx, &sockfd, argv[0])) + return JS_EXCEPTION; + if (JS_ToSockaddrStruct(ctx, argv[1], &sockaddr, sockfd)) + return JS_EXCEPTION; + socklen_t addr_len = sockaddr.ss_family == AF_INET ? sizeof(struct sockaddr_in) : + sockaddr.ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) : 0; + ret = js_get_sockerrno(bind(sockfd, (struct sockaddr *)&sockaddr, addr_len)); + return JS_NewInt32(ctx, ret); +} + +static JSValue js_os_listen(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int sockfd, backlog = SOMAXCONN, ret; + + if (JS_ToInt32(ctx, &sockfd, argv[0])) + return JS_EXCEPTION; + if (argc >= 2 && JS_ToInt32(ctx, &backlog, argv[1])) + return JS_EXCEPTION; + + ret = js_get_sockerrno(listen(sockfd, backlog)); + return JS_NewInt32(ctx, ret); +} + +static JSValue js_os_connect_accept(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + int sockfd, sockret = 0; + + if (JS_ToInt32(ctx, &sockfd, argv[0])) + return JS_EXCEPTION; + + struct sockaddr_storage sockaddr; + if (magic == MAGIC_SOCKET_CONNECT) { + if (JS_ToSockaddrStruct(ctx, argv[1], &sockaddr, sockfd) < 0){ + JS_ThrowTypeError(ctx, "invalid sockaddr"); + return JS_EXCEPTION; + } + socklen_t addr_len = sockaddr.ss_family == AF_INET ? sizeof(struct sockaddr_in) : + sockaddr.ss_family == AF_INET6 ? sizeof(struct sockaddr_in6) : 0; + sockret = js_get_sockerrno(connect(sockfd, (struct sockaddr *)&sockaddr, addr_len)); + #if defined(_WIN32) + if (sockret != -WSAEWOULDBLOCK) + #else + if (sockret != -EINPROGRESS) + #endif + { + JS_ThrowTypeError(ctx, "connect failed (%i)", sockret); + return JS_EXCEPTION; + } + } + /* accept() pooling done in handle_socket_message() to avoid code duplicate */ + + JSValue resolving_funcs[2]; + JSValue promise = JS_NewPromiseCapability(ctx, resolving_funcs); + if (JS_IsException(promise)) + return JS_EXCEPTION; + + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSOSSockHandler *sh = js_mallocz(ctx, sizeof(*sh)); + sh->sockfd = sockfd; + sh->magic = magic; + sh->resolve = JS_DupValue(ctx, resolving_funcs[0]); + sh->reject = JS_DupValue(ctx, resolving_funcs[1]); + #if defined(_WIN32) + sh->event = WSACreateEvent(); + WSAEventSelect(sh->sockfd, sh->event, magic == MAGIC_SOCKET_CONNECT ? FD_CONNECT : FD_ACCEPT); + #endif + list_add_tail(&sh->link, &ts->os_sock_handlers); + JS_FreeValue(ctx, resolving_funcs[0]); + JS_FreeValue(ctx, resolving_funcs[1]); + return promise; +} + +static JSValue js_os_recv_send(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv, int magic) +{ + JSThreadState *ts = JS_GetRuntimeOpaque(JS_GetRuntime(ctx)); + JSOSSockHandler *sh = js_mallocz(ctx, sizeof(*sh)); + if (!sh) + return JS_EXCEPTION; + + JSValue resolving_funcs[2]; + JSValue promise = JS_NewPromiseCapability(ctx, resolving_funcs); + if (JS_IsException(promise)) + goto fail; + + // store all our info for async processing + sh->magic = magic; + if (JS_ToInt32(ctx, &sh->sockfd, argv[0])) + goto fail; + unsigned bufArgvIdx = 1; // buffer is argv[1], unless sendto() + if (magic == MAGIC_SOCKET_SENDTO) { + bufArgvIdx = 2; + if (JS_ToSockaddrStruct(ctx, argv[1], &sh->sockaddr, sh->sockfd) < 0) { + JS_ThrowTypeError(ctx, "invalid sockaddr"); + goto fail; + } + } + sh->buffer = JS_GetArrayBuffer(ctx, &sh->length, argv[bufArgvIdx]); + if (!sh->buffer) + goto fail; + if (argc > bufArgvIdx + 1) { + size_t length; + if (JS_ToIndex(ctx, &length, argv[bufArgvIdx + 1])) { + JS_ThrowRangeError(ctx, "Invalid length given"); + goto fail; + } + if (length > sh->length) { + JS_ThrowRangeError(ctx, "recv/send array buffer overflow"); + goto fail; + } + sh->length = length; + } + #if defined(_WIN32) + sh->event = WSACreateEvent(); + int flags = (magic == MAGIC_SOCKET_SENDTO || magic == MAGIC_SOCKET_SEND) ? FD_WRITE : FD_READ; + WSAEventSelect(sh->sockfd, sh->event, flags); + #endif + sh->bufval = JS_DupValue(ctx, argv[bufArgvIdx]); + sh->resolve = JS_DupValue(ctx, resolving_funcs[0]); + sh->reject = JS_DupValue(ctx, resolving_funcs[1]); + list_add_tail(&sh->link, &ts->os_sock_handlers); + JS_FreeValue(ctx, resolving_funcs[0]); + JS_FreeValue(ctx, resolving_funcs[1]); + return promise; + + fail: + JS_FreeValue(ctx, promise); + JS_FreeValue(ctx, resolving_funcs[0]); + JS_FreeValue(ctx, resolving_funcs[1]); + js_free(ctx, sh); + return JS_EXCEPTION; +} + +static JSValue js_os_shutdown(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + int sockfd, how, ret; + + if (JS_ToInt32(ctx, &sockfd, argv[0])) + return JS_EXCEPTION; + if (JS_ToInt32(ctx, &how, argv[1])) + return JS_EXCEPTION; + + ret = js_get_sockerrno(shutdown(sockfd, how)); + return JS_NewInt32(ctx, ret); +} + #ifdef USE_WORKER /* Worker */ @@ -3875,8 +4389,8 @@ static const JSCFunctionListEntry js_os_funcs[] = { JS_CFUNC_DEF("ttySetRaw", 1, js_os_ttySetRaw ), JS_CFUNC_DEF("remove", 1, js_os_remove ), JS_CFUNC_DEF("rename", 2, js_os_rename ), - JS_CFUNC_MAGIC_DEF("setReadHandler", 2, js_os_setReadHandler, 0 ), - JS_CFUNC_MAGIC_DEF("setWriteHandler", 2, js_os_setReadHandler, 1 ), + JS_CFUNC_MAGIC_DEF("setReadHandler", 2, js_os_setReadWriteHandler, 0 ), + JS_CFUNC_MAGIC_DEF("setWriteHandler", 2, js_os_setReadWriteHandler, 1 ), JS_CFUNC_DEF("signal", 2, js_os_signal ), OS_FLAG(SIGINT), OS_FLAG(SIGABRT), @@ -3935,6 +4449,37 @@ static const JSCFunctionListEntry js_os_funcs[] = { JS_CFUNC_DEF("kill", 2, js_os_kill ), JS_CFUNC_DEF("dup", 1, js_os_dup ), JS_CFUNC_DEF("dup2", 2, js_os_dup2 ), +#endif + /* SOCKET I/O */ + JS_CFUNC_DEF("socket", 2, js_os_socket ), + JS_CFUNC_MAGIC_DEF("getsockopt", 3, js_os_get_setsockopt, 0 ), + JS_CFUNC_MAGIC_DEF("setsockopt", 3, js_os_get_setsockopt, 1 ), + JS_CFUNC_DEF("getaddrinfo", 2, js_os_getaddrinfo ), + JS_CFUNC_DEF("bind", 2, js_os_bind ), + JS_CFUNC_DEF("listen", 1, js_os_listen ), + JS_CFUNC_MAGIC_DEF("accept", 1, js_os_connect_accept, MAGIC_SOCKET_ACCEPT ), + JS_CFUNC_MAGIC_DEF("connect", 2, js_os_connect_accept, MAGIC_SOCKET_CONNECT ), + JS_CFUNC_MAGIC_DEF("recv", 3, js_os_recv_send, MAGIC_SOCKET_RECV ), + JS_CFUNC_MAGIC_DEF("send", 3, js_os_recv_send, MAGIC_SOCKET_SEND ), + JS_CFUNC_MAGIC_DEF("recvfrom", 4, js_os_recv_send, MAGIC_SOCKET_RECVFROM ), + JS_CFUNC_MAGIC_DEF("sendto", 3, js_os_recv_send, MAGIC_SOCKET_SENDTO ), + JS_CFUNC_DEF("shutdown", 2, js_os_shutdown ), + OS_FLAG(AF_INET), + OS_FLAG(AF_INET6), + OS_FLAG(SOCK_STREAM), + OS_FLAG(SOCK_DGRAM), + OS_FLAG(SOCK_RAW), + OS_FLAG(SO_REUSEADDR), + OS_FLAG(SO_RCVBUF), + OS_FLAG(SO_ERROR), +#if defined(_WIN32) + JS_PROP_INT32_DEF("SHUT_RD", SD_RECEIVE, JS_PROP_CONFIGURABLE), + JS_PROP_INT32_DEF("SHUT_WR", SD_SEND, JS_PROP_CONFIGURABLE), + JS_PROP_INT32_DEF("SHUT_RDWR", SD_BOTH, JS_PROP_CONFIGURABLE), +#else + OS_FLAG(SHUT_RD), + OS_FLAG(SHUT_WR), + OS_FLAG(SHUT_RDWR), #endif }; @@ -4070,6 +4615,7 @@ void js_std_init_handlers(JSRuntime *rt) } memset(ts, 0, sizeof(*ts)); init_list_head(&ts->os_rw_handlers); + init_list_head(&ts->os_sock_handlers); init_list_head(&ts->os_signal_handlers); init_list_head(&ts->os_timers); init_list_head(&ts->port_list); @@ -4101,6 +4647,11 @@ void js_std_free_handlers(JSRuntime *rt) free_rw_handler(rt, rh); } + list_for_each_safe(el, el1, &ts->os_sock_handlers) { + JSOSSockHandler *sh = list_entry(el, JSOSSockHandler, link); + free_sock_handler(rt, sh); + } + list_for_each_safe(el, el1, &ts->os_signal_handlers) { JSOSSignalHandler *sh = list_entry(el, JSOSSignalHandler, link); free_sh(rt, sh); diff --git a/quickjs.c b/quickjs.c index 475232c93..dda07e671 100644 --- a/quickjs.c +++ b/quickjs.c @@ -39,6 +39,11 @@ #elif defined(__FreeBSD__) #include #endif +#if defined(_WIN32) +#include +#include +#include +#endif #include "cutils.h" #include "list.h" @@ -302,6 +307,9 @@ struct JSRuntime { int shape_hash_count; /* number of hashed shapes */ JSShape **shape_hash; void *user_opaque; + #if defined(_WIN32) + WSADATA wsa_data; + #endif }; struct JSClass { @@ -475,6 +483,9 @@ struct JSContext { const char *input, size_t input_len, const char *filename, int flags, int scope_idx); void *user_opaque; + #if defined(_WIN32) + WSADATA wsa_data; + #endif }; typedef union JSFloat64Union { @@ -1626,6 +1637,10 @@ JSRuntime *JS_NewRuntime2(const JSMallocFunctions *mf, void *opaque) rt->current_exception = JS_UNINITIALIZED; +#if defined(_WIN32) + WSAStartup(MAKEWORD(2, 2), &rt->wsa_data); +#endif + return rt; fail: JS_FreeRuntime(rt); @@ -2077,6 +2092,9 @@ void JS_FreeRuntime(JSRuntime *rt) JSMallocState ms = rt->malloc_state; rt->mf.js_free(&ms, rt); } +#if defined(_WIN32) + WSACleanup(); +#endif } JSContext *JS_NewContextRaw(JSRuntime *rt)