From 20ccc3972b629092da8a871826f09fb4a32b4ab4 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 18 Oct 2023 00:52:29 +0200 Subject: [PATCH 01/65] feat: add initial impl for node sockets --- packages/preview2-shim/lib/nodejs/index.js | 3 + packages/preview2-shim/lib/nodejs/sockets.js | 258 +++++------------- .../preview2-shim/lib/sockets/wasi-sockets.js | 106 +++++++ packages/preview2-shim/test/test.js | 12 +- 4 files changed, 193 insertions(+), 186 deletions(-) create mode 100644 packages/preview2-shim/lib/sockets/wasi-sockets.js diff --git a/packages/preview2-shim/lib/nodejs/index.js b/packages/preview2-shim/lib/nodejs/index.js index 45205c3dc..f53a8e4f8 100644 --- a/packages/preview2-shim/lib/nodejs/index.js +++ b/packages/preview2-shim/lib/nodejs/index.js @@ -15,3 +15,6 @@ export { sockets, cli } + +export { WasiSockets } from "../sockets/wasi-sockets.js"; + diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 11897d1e6..661c9189a 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -1,200 +1,90 @@ -export const instanceNetwork = { - instanceNetwork () { - console.log(`[sockets] instance network`); - } -}; +import { WasiSockets } from "../sockets/wasi-sockets.js"; -export const ipNameLookup = { - dropResolveAddressStream () { - - }, - subscribe () { - - }, - resolveAddresses () { - - }, - resolveNextAddress () { - - }, - nonBlocking () { - - }, - setNonBlocking () { - - }, -}; +const _network = new WasiSockets(); -export const network = { - dropNetwork () { +export const { instanceNetwork, network } = _network; - } +export const ipNameLookup = { + dropResolveAddressStream() {}, + subscribe() {}, + resolveAddresses() {}, + resolveNextAddress() {}, + nonBlocking() {}, + setNonBlocking() {}, }; export const tcpCreateSocket = { - createTcpSocket () { - - } + createTcpSocket(addressFamily) {}, }; export const tcp = { - subscribe () { - - }, - dropTcpSocket() { - - }, - bind() { - - }, - connect() { - - }, - listen() { - - }, - accept() { - - }, - localAddress() { - - }, - remoteAddress() { - - }, - addressFamily() { - - }, - ipv6Only() { - - }, - setIpv6Only() { - - }, - setListenBacklogSize() { - - }, - keepAlive() { - - }, - setKeepAlive() { - - }, - noDelay() { - - }, - setNoDelay() { - - }, - unicastHopLimit() { - - }, - setUnicastHopLimit() { - - }, - receiveBufferSize() { - - }, - setReceiveBufferSize() { - - }, - sendBufferSize() { - - }, - setSendBufferSize() { - - }, - nonBlocking() { - - }, - setNonBlocking() { - - }, - shutdown() { - - } + subscribe() {}, + dropTcpSocket() {}, + bind() {}, + connect() {}, + listen() {}, + accept() {}, + localAddress() {}, + remoteAddress() {}, + addressFamily() {}, + ipv6Only() {}, + setIpv6Only() {}, + setListenBacklogSize() {}, + keepAlive() {}, + setKeepAlive() {}, + noDelay() {}, + setNoDelay() {}, + unicastHopLimit() {}, + setUnicastHopLimit() {}, + receiveBufferSize() {}, + setReceiveBufferSize() {}, + sendBufferSize() {}, + setSendBufferSize() {}, + nonBlocking() {}, + setNonBlocking() {}, + shutdown() {}, }; export const udp = { - subscribe () { - - }, - - dropUdpSocket () { - - }, - - bind () { - - }, - - connect () { - - }, - - receive () { - - }, - - send () { - - }, - - localAddress () { - - }, - - remoteAddress () { - - }, - - addressFamily () { - - }, - - ipv6Only () { - - }, - - setIpv6Only () { - - }, - - unicastHopLimit () { - - }, - - setUnicastHopLimit () { - - }, - - receiveBufferSize () { - - }, - - setReceiveBufferSize () { - - }, - - sendBufferSize () { - - }, - - setSendBufferSize () { - - }, - - nonBlocking () { - - }, - - setNonBlocking () { - - } + subscribe() {}, + + dropUdpSocket() {}, + + bind() {}, + + connect() {}, + + receive() {}, + + send() {}, + + localAddress() {}, + + remoteAddress() {}, + + addressFamily() {}, + + ipv6Only() {}, + + setIpv6Only() {}, + + unicastHopLimit() {}, + + setUnicastHopLimit() {}, + + receiveBufferSize() {}, + + setReceiveBufferSize() {}, + + sendBufferSize() {}, + + setSendBufferSize() {}, + + nonBlocking() {}, + + setNonBlocking() {}, }; export const udpCreateSocket = { - createTcpSocket () { - - } + createTcpSocket() {}, }; diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js new file mode 100644 index 000000000..34ed644c7 --- /dev/null +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -0,0 +1,106 @@ +/** + * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network + * @typedef {import("../../types/interfaces/wasi-sockets-network").ErrorCode} ErrorCode + * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily + */ + +/** @type {ErrorCode} */ +export const errorCode = { + // ### GENERAL ERRORS ### + unknown: "unknown", + accessDenied: "access-denied", + notSupported: "not-supported", + outOfMemory: "out-of-memory", + timeout: "timeout", + concurrencyConflict: "concurrency-conflict", + notInProgress: "not-in-progress", + wouldBlock: "would-block", + + // ### IP ERRORS ### + addressFamilyNotSupported: "address-family-not-supported", + addressFamilyMismatch: "address-family-mismatch", + invalidRemoteAddress: "invalid-remote-address", + ipv4OnlyOperation: "ipv4-only-operation", + ipv6OnlyOperation: "ipv6-only-operation", + + // ### TCP & UDP SOCKET ERRORS ### + newSocketLimit: "new-socket-limit", + alreadyAttached: "already-attached", + alreadyBound: "already-bound", + alreadyConnected: "already-connected", + notBound: "not-bound", + notConnected: "not-connected", + addressNotBindable: "address-not-bindable", + addressInUse: "address-in-use", + ephemeralPortsExhausted: "ephemeral-ports-exhausted", + remoteUnreachable: "remote-unreachable", + + // ### TCP SOCKET ERRORS ### + alreadyListening: "already-listening", + notListening: "not-listening", + connectionRefused: "connection-refused", + connectionReset: "connection-reset", + + // ### UDP SOCKET ERRORS ### + datagramTooLarge: "datagram-too-large", + + // ### NAME LOOKUP ERRORS ### + invalidName: "invalid-name", + nameUnresolvable: "name-unresolvable", + temporaryResolverFailure: "temporary-resolver-failure", + permanentResolverFailure: "permanent-resolver-failure", +}; + +/** @type {IpAddressFamily} */ +export const ipAddressFamily = { + ipv4: "ipv4", + ipv6: "ipv6", +}; + +export class WasiSockets { + networkCnt = 0; + + /** @type {Map} */ networks = new Map(); + + constructor() { + const net = this; + this.networks = new Map(); + + class Network { + /** @type {number} */ id; + constructor() { + this.id = net.networkCnt++; + } + } + + this.instanceNetwork = { + /** + * @returns Network + */ + instanceNetwork() { + console.log(`[sockets] instance network`); + + let _network; + if (!_network) { + _network = new Network(); + } + return _network; + }, + }; + + this.network = { + /** + * @param {Network} networkId + **/ + dropNetwork(networkId) { + console.log(`[network] Drop network ${networkId}`); + + const network = net.networks.get(networkId); + if (!network) { + return; + } + net.networks.delete(networkId); + }, + }; + } +} diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 0dc82dabd..dcf8625fd 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,5 +1,5 @@ -import { ok, strictEqual } from "node:assert"; -import { fileURLToPath } from "node:url"; +import { equal, ok, strictEqual, } from 'node:assert'; +import { fileURLToPath } from 'node:url'; suite("Node.js Preview2", () => { test("Stdio", async () => { @@ -158,4 +158,12 @@ suite("Node.js Preview2", () => { ok(headers["content-type"].startsWith("text/html")); ok(responseBody.includes("WebAssembly")); }); + + test('Sockets', async () => { + const { sockets } = await import('@bytecodealliance/preview2-shim'); + const net = sockets.instanceNetwork.instanceNetwork(); + + equal(net.id, 0); + }); + }); From 3ee45c7d4aac73a4dc3717500ab464556c6114a1 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 18 Oct 2023 10:33:43 +0200 Subject: [PATCH 02/65] chore: update sockets tests --- packages/preview2-shim/test/test.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index dcf8625fd..87af16301 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,5 +1,5 @@ -import { equal, ok, strictEqual, } from 'node:assert'; -import { fileURLToPath } from 'node:url'; +import { equal, ok, strictEqual } from "node:assert"; +import { fileURLToPath } from "node:url"; suite("Node.js Preview2", () => { test("Stdio", async () => { @@ -159,11 +159,13 @@ suite("Node.js Preview2", () => { ok(responseBody.includes("WebAssembly")); }); - test('Sockets', async () => { - const { sockets } = await import('@bytecodealliance/preview2-shim'); - const net = sockets.instanceNetwork.instanceNetwork(); - - equal(net.id, 0); + suite("Sockets", async () => { + test("instanceNetwork", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const net1 = sockets.instanceNetwork.instanceNetwork(); + equal(net1.id, 1); + const net2 = sockets.instanceNetwork.instanceNetwork(); + equal(net2.id, 2); + }); }); - }); From 3e532deb80783fdbb678f81aa424e0af4fed16c8 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 18 Oct 2023 10:34:44 +0200 Subject: [PATCH 03/65] feat: add TcpSocketImpl --- .../lib/sockets/tcp-socket-impl.js | 251 ++++++++++++++++++ .../preview2-shim/lib/sockets/wasi-sockets.js | 35 ++- 2 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 packages/preview2-shim/lib/sockets/tcp-socket-impl.js diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js new file mode 100644 index 000000000..03dcca5a0 --- /dev/null +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -0,0 +1,251 @@ +/** + * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network + * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress + * @typedef {import("../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket + * @typedef {import("../../types/interfaces/wasi-sockets-tcp").InputStream} InputStream + * @typedef {import("../../types/interfaces/wasi-sockets-tcp").OutputStream} OutputStream + * @typedef {import("../../types/interfaces/wasi-sockets-tcp").IpAddressFamily} IpAddressFamily + * @typedef {import("../../types/interfaces/wasi-poll-poll").Pollable} Pollable + * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType + */ + +export class TcpSocketImpl { + /** @type {number} */ id; + + constructor(socketId) { + this.id = socketId; + } + + /** + * @param {TcpSocket} tcpSocket + * @param {Network} network + * @param {IpSocketAddress} localAddress + * @returns {void} + **/ + startBind(tcpSocket, network, localAddress) { + console.log(`[tcp] start bind socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {void} + **/ + finishBind(tcpSocket) { + console.log(`[tcp] finish bind socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {Network} network + * @param {IpSocketAddress} remoteAddress + * @returns {void} + * */ + startConnect(tcpSocket, network, remoteAddress) { + console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {Array} + * */ + finishConnect(tcpSocket) { + console.log(`[tcp] finish connect socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {void} + * */ + startListen(tcpSocket) {} + + /** + * @param {TcpSocket} tcpSocket + * @returns {void} + * */ + finishListen(tcpSocket) { + console.log(`[tcp] finish listen socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {Array} + * */ + accept(tcpSocket) { + console.log(`[tcp] accept socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {IpSocketAddress} + * */ + localAddress(tcpSocket) { + console.log(`[tcp] local address socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {IpSocketAddress} + * */ + remoteAddress(tcpSocket) { + console.log(`[tcp] remote address socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {IpAddressFamily} + * */ + addressFamily(tcpSocket) { + console.log(`[tcp] address family socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {boolean} + * */ + ipv6Only(tcpSocket) { + console.log(`[tcp] ipv6 only socket ${this_.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {boolean} value + * @returns {void} + * */ + setIpv6Only(tcpSocket, value) { + console.log(`[tcp] set ipv6 only socket ${tcpSocket.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {bigint} value + * @returns {void} + * */ + setListenBacklogSize(tcpSocket, value) { + console.log(`[tcp] set listen backlog size socket ${tcpSocket.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {boolean} + * */ + keepAlive(tcpSocket) { + console.log(`[tcp] keep alive socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {boolean} value + * @returns {void} + * */ + setKeepAlive(tcpSocket, value) { + console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {boolean} + * */ + noDelay(tcpSocket) { + console.log(`[tcp] no delay socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {void} + * */ + unicastHopLimit(tcpSocket) { + console.log(`[tcp] unicast hop limit socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {number} value + * @returns {void} + * */ + setUnicastHopLimit(tcpSocket, value) { + console.log(`[tcp] set unicast hop limit socket ${tcpSocket.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {bigint} + * */ + receiveBufferSize(tcpSocket) { + console.log(`[tcp] receive buffer size socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {bigint} value + * @returns {void} + * */ + setReceiveBufferSize(tcpSocket, value) { + console.log(`[tcp] set receive buffer size socket ${this_.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {bigint} + * */ + sendBufferSize(tcpSocket) { + console.log(`[tcp] send buffer size socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {bigint} value + * @returns {void} + * */ + setSendBufferSize(tcpSocket, value) { + console.log(`[tcp] set send buffer size socket ${tcpSocket.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {Pollable} + * */ + subscribe(tcpSocket) { + console.log(`[tcp] subscribe socket ${this_.id}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @param {ShutdownType} shutdownType + * @returns {void} + * */ + shutdown(tcpSocket, shutdownType) { + console.log(`[tcp] shutdown socket ${tcpSocket.id} with ${shutdownType}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {void} + * */ + dropTcpSocket(tcpSocket) { + console.log(`[tcp] drop socket ${tcpSocket.id}`); + throw new Error("not implemented"); + } +} diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index 34ed644c7..47edba23d 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -2,8 +2,11 @@ * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network * @typedef {import("../../types/interfaces/wasi-sockets-network").ErrorCode} ErrorCode * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily + * @typedef {import("../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket */ +import { TcpSocketImpl } from "./tcp-socket-impl.js"; + /** @type {ErrorCode} */ export const errorCode = { // ### GENERAL ERRORS ### @@ -51,25 +54,28 @@ export const errorCode = { permanentResolverFailure: "permanent-resolver-failure", }; -/** @type {IpAddressFamily} */ -export const ipAddressFamily = { - ipv4: "ipv4", - ipv6: "ipv6", -}; - export class WasiSockets { - networkCnt = 0; + networkCnt = 1; + tcpSocketCnt = 1; /** @type {Map} */ networks = new Map(); + /** @type {Map Date: Wed, 18 Oct 2023 10:39:45 +0200 Subject: [PATCH 04/65] chore: temp disable eslint in lib/sockets/** --- packages/preview2-shim/lib/nodejs/sockets.js | 2 ++ packages/preview2-shim/lib/sockets/tcp-socket-impl.js | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 661c9189a..1e4e739d7 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + import { WasiSockets } from "../sockets/wasi-sockets.js"; const _network = new WasiSockets(); diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 03dcca5a0..2135ac3c1 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -1,3 +1,5 @@ +/* eslint-disable no-unused-vars */ + /** * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress @@ -112,7 +114,7 @@ export class TcpSocketImpl { * @returns {boolean} * */ ipv6Only(tcpSocket) { - console.log(`[tcp] ipv6 only socket ${this_.id}`); + console.log(`[tcp] ipv6 only socket ${this.id}`); throw new Error("not implemented"); } @@ -198,7 +200,7 @@ export class TcpSocketImpl { * @returns {void} * */ setReceiveBufferSize(tcpSocket, value) { - console.log(`[tcp] set receive buffer size socket ${this_.id} to ${value}`); + console.log(`[tcp] set receive buffer size socket ${this.id} to ${value}`); throw new Error("not implemented"); } @@ -226,7 +228,7 @@ export class TcpSocketImpl { * @returns {Pollable} * */ subscribe(tcpSocket) { - console.log(`[tcp] subscribe socket ${this_.id}`); + console.log(`[tcp] subscribe socket ${this.id}`); throw new Error("not implemented"); } From 79e570dbe9c16f7e29f97aeee4e8edffd01bedb0 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 18 Oct 2023 23:21:18 +0200 Subject: [PATCH 05/65] chore: refactor instanceNetwork + dropNetwork --- packages/preview2-shim/lib/nodejs/sockets.js | 4 ++-- .../preview2-shim/lib/sockets/wasi-sockets.js | 22 +++++++++---------- packages/preview2-shim/test/test.js | 21 ++++++++++++++---- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 1e4e739d7..bf7d26393 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -2,9 +2,9 @@ import { WasiSockets } from "../sockets/wasi-sockets.js"; -const _network = new WasiSockets(); +const sockets = new WasiSockets(); -export const { instanceNetwork, network } = _network; +export const { instanceNetwork, network } = sockets; export const ipNameLookup = { dropResolveAddressStream() {}, diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index 47edba23d..280a66884 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -58,6 +58,7 @@ export class WasiSockets { networkCnt = 1; tcpSocketCnt = 1; + /** @type {Network} */ networkInstance = null; /** @type {Map} */ networks = new Map(); /** @type {Map { suite("Sockets", async () => { test("instanceNetwork", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - const net1 = sockets.instanceNetwork.instanceNetwork(); - equal(net1.id, 1); - const net2 = sockets.instanceNetwork.instanceNetwork(); - equal(net2.id, 2); + const network1 = sockets.instanceNetwork.instanceNetwork(); + equal(network1.id, 1); + const network2 = sockets.instanceNetwork.instanceNetwork(); + equal(network2.id, 1); + }); + + test("dropNetwork", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const net = sockets.instanceNetwork.instanceNetwork(); + + // drop existing network + const op1 = sockets.network.dropNetwork(net.id); + equal(op1, true); + + // drop non-existing network + const op2 = sockets.network.dropNetwork(99999); + equal(op2, false); }); }); }); From b9ad2230ae5efc073de210d8eb0f0dc24415004c Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 19 Oct 2023 09:05:35 +0200 Subject: [PATCH 06/65] chore: tcp socket impl wip --- .../lib/sockets/tcp-socket-impl.js | 107 ++++++++++++++++-- .../preview2-shim/lib/sockets/wasi-sockets.js | 7 +- 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 2135ac3c1..18c2a9543 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,11 +11,24 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ +import { Socket as NodeSocket } from "node:net"; + export class TcpSocketImpl { /** @type {number} */ id; + /** @type {boolean} */ isBound = false; + /** @type {NodeSocket} */ socket = null; + /** @type {Network} */ network = null; + /** @type {IpAddressFamily} */ addressFamily; + /** @type {IpSocketAddress} */ localAddress = null; + + ipv6Only = false; + state = "closed"; - constructor(socketId) { + constructor(socketId, addressFamily) { this.id = socketId; + this.addressFamily = addressFamily; + + this.socket = new NodeSocket(); } /** @@ -26,7 +39,14 @@ export class TcpSocketImpl { **/ startBind(tcpSocket, network, localAddress) { console.log(`[tcp] start bind socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + if (this.isBound) { + throw new Error("socket is already bound"); + } + + this.localAddress = localAddress; + this.network = network; + this.isBound = true; } /** @@ -35,7 +55,10 @@ export class TcpSocketImpl { **/ finishBind(tcpSocket) { console.log(`[tcp] finish bind socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + this.network = null; + this.localAddress = null; + this.isBound = false; } /** @@ -46,7 +69,46 @@ export class TcpSocketImpl { * */ startConnect(tcpSocket, network, remoteAddress) { console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); - throw new Error("not implemented"); + + this.network = network; + + this.socket.connect({ + localAddress: this.localAddress.address, + localPort: this.localAddress.port, + host: remoteAddress.address, + port: remoteAddress.port, + family: this.addressFamily, + }); + + this.socket.on("connect", () => { + console.log(`[tcp] connect on socket ${tcpSocket.id}`); + this.state = "connected"; + }); + + this.socket.on("ready", () => { + console.log(`[tcp] ready on socket ${tcpSocket.id}`); + this.state = "connection"; + }); + + this.socket.on("close", () => { + console.log(`[tcp] close on socket ${tcpSocket.id}`); + this.state = "closed"; + }); + + this.socket.on("end", () => { + console.log(`[tcp] end on socket ${tcpSocket.id}`); + this.state = "closed"; + }); + + this.socket.on("timeout", () => { + console.error(`[tcp] timeout on socket ${tcpSocket.id}`); + this.state = "closed"; + }); + + this.socket.on("error", (err) => { + console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); + this.state = "error"; + }); } /** @@ -55,14 +117,19 @@ export class TcpSocketImpl { * */ finishConnect(tcpSocket) { console.log(`[tcp] finish connect socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + this.socket.destroySoon(); } /** * @param {TcpSocket} tcpSocket * @returns {void} * */ - startListen(tcpSocket) {} + startListen(tcpSocket) { + console.log(`[tcp] start listen socket ${tcpSocket.id}`); + + this.socket.listen(); + } /** * @param {TcpSocket} tcpSocket @@ -88,7 +155,12 @@ export class TcpSocketImpl { * */ localAddress(tcpSocket) { console.log(`[tcp] local address socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + if (!this.isBound) { + throw new Error("not-bound"); + } + + return this.socket.localAddress(); } /** @@ -97,7 +169,17 @@ export class TcpSocketImpl { * */ remoteAddress(tcpSocket) { console.log(`[tcp] remote address socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + + if (!this.isBound) { + throw new Error("not-bound"); + } + + if (this.state !== "connected") { + throw new Error("not-connected"); + } + + return this.socket.remoteAddress(); } /** @@ -106,7 +188,8 @@ export class TcpSocketImpl { * */ addressFamily(tcpSocket) { console.log(`[tcp] address family socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + return this.socket.localFamily; } /** @@ -115,7 +198,8 @@ export class TcpSocketImpl { * */ ipv6Only(tcpSocket) { console.log(`[tcp] ipv6 only socket ${this.id}`); - throw new Error("not implemented"); + + return this.ipv6Only; } /** @@ -125,7 +209,8 @@ export class TcpSocketImpl { * */ setIpv6Only(tcpSocket, value) { console.log(`[tcp] set ipv6 only socket ${tcpSocket.id} to ${value}`); - throw new Error("not implemented"); + + this.ipv6Only = value; } /** diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index 280a66884..8a829b0f0 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -74,8 +74,9 @@ export class WasiSockets { } class TcpSocket extends TcpSocketImpl { - constructor() { - super(net.tcpSocketCnt++); + /** @param {IpAddressFamily} addressFamily */ + constructor(addressFamily) { + super(net.tcpSocketCnt++, addressFamily); net.tcpSockets.set(this.id, this); } } @@ -117,7 +118,7 @@ export class WasiSockets { createTcpSocket(addressFamily) { console.log(`[tcp] Create tcp socket ${addressFamily}`); - const socket = new TcpSocket(); + const socket = new TcpSocket(addressFamily); return socket.id; }, }; From 877c5ff077e4086a47cd6f5ecff2e60cc8b5432a Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 19 Oct 2023 10:28:01 +0200 Subject: [PATCH 07/65] chore: createTcpSocket/startBind/finishBind --- packages/preview2-shim/lib/nodejs/sockets.js | 125 +++++++++--------- .../lib/sockets/tcp-socket-impl.js | 19 ++- .../preview2-shim/lib/sockets/wasi-sockets.js | 25 +++- packages/preview2-shim/test/test.js | 53 +++++++- 4 files changed, 146 insertions(+), 76 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index bf7d26393..1ec11397b 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -3,90 +3,85 @@ import { WasiSockets } from "../sockets/wasi-sockets.js"; const sockets = new WasiSockets(); +export const { instanceNetwork, network, tcpCreateSocket } = sockets; -export const { instanceNetwork, network } = sockets; +// export const ipNameLookup = { +// dropResolveAddressStream() {}, +// subscribe() {}, +// resolveAddresses() {}, +// resolveNextAddress() {}, +// nonBlocking() {}, +// setNonBlocking() {}, +// }; -export const ipNameLookup = { - dropResolveAddressStream() {}, - subscribe() {}, - resolveAddresses() {}, - resolveNextAddress() {}, - nonBlocking() {}, - setNonBlocking() {}, -}; +// export const tcp = { +// subscribe() {}, +// dropTcpSocket() {}, +// bind() {}, +// connect() {}, +// listen() {}, +// accept() {}, +// localAddress() {}, +// remoteAddress() {}, +// addressFamily() {}, +// ipv6Only() {}, +// setIpv6Only() {}, +// setListenBacklogSize() {}, +// keepAlive() {}, +// setKeepAlive() {}, +// noDelay() {}, +// setNoDelay() {}, +// unicastHopLimit() {}, +// setUnicastHopLimit() {}, +// receiveBufferSize() {}, +// setReceiveBufferSize() {}, +// sendBufferSize() {}, +// setSendBufferSize() {}, +// nonBlocking() {}, +// setNonBlocking() {}, +// shutdown() {}, +// }; -export const tcpCreateSocket = { - createTcpSocket(addressFamily) {}, -}; +// export const udp = { +// subscribe() {}, -export const tcp = { - subscribe() {}, - dropTcpSocket() {}, - bind() {}, - connect() {}, - listen() {}, - accept() {}, - localAddress() {}, - remoteAddress() {}, - addressFamily() {}, - ipv6Only() {}, - setIpv6Only() {}, - setListenBacklogSize() {}, - keepAlive() {}, - setKeepAlive() {}, - noDelay() {}, - setNoDelay() {}, - unicastHopLimit() {}, - setUnicastHopLimit() {}, - receiveBufferSize() {}, - setReceiveBufferSize() {}, - sendBufferSize() {}, - setSendBufferSize() {}, - nonBlocking() {}, - setNonBlocking() {}, - shutdown() {}, -}; +// dropUdpSocket() {}, -export const udp = { - subscribe() {}, +// bind() {}, - dropUdpSocket() {}, +// connect() {}, - bind() {}, +// receive() {}, - connect() {}, +// send() {}, - receive() {}, +// localAddress() {}, - send() {}, +// remoteAddress() {}, - localAddress() {}, +// addressFamily() {}, - remoteAddress() {}, +// ipv6Only() {}, - addressFamily() {}, +// setIpv6Only() {}, - ipv6Only() {}, +// unicastHopLimit() {}, - setIpv6Only() {}, +// setUnicastHopLimit() {}, - unicastHopLimit() {}, +// receiveBufferSize() {}, - setUnicastHopLimit() {}, +// setReceiveBufferSize() {}, - receiveBufferSize() {}, +// sendBufferSize() {}, - setReceiveBufferSize() {}, +// setSendBufferSize() {}, - sendBufferSize() {}, +// nonBlocking() {}, - setSendBufferSize() {}, +// setNonBlocking() {}, +// }; - nonBlocking() {}, - - setNonBlocking() {}, -}; - -export const udpCreateSocket = { - createTcpSocket() {}, -}; +// export const udpCreateSocket = { +// createTcpSocket() {}, +// }; diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 18c2a9543..6bffd778d 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,7 +11,7 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ -import { Socket as NodeSocket } from "node:net"; +import { Socket as NodeSocket, SocketAddress as NodeSocketAddress } from "node:net"; export class TcpSocketImpl { /** @type {number} */ id; @@ -44,7 +44,12 @@ export class TcpSocketImpl { throw new Error("socket is already bound"); } - this.localAddress = localAddress; + this.socketAddress = new NodeSocketAddress({ + address: localAddress.val.address.join('.'), + port: localAddress.val.port, + family: this.addressFamily, + }); + this.network = network; this.isBound = true; } @@ -57,7 +62,7 @@ export class TcpSocketImpl { console.log(`[tcp] finish bind socket ${tcpSocket.id}`); this.network = null; - this.localAddress = null; + this.socketAddress = null; this.isBound = false; } @@ -73,10 +78,10 @@ export class TcpSocketImpl { this.network = network; this.socket.connect({ - localAddress: this.localAddress.address, - localPort: this.localAddress.port, - host: remoteAddress.address, - port: remoteAddress.port, + localAddress: this.socketAddress.address.join('.'), + localPort: this.socketAddress.port, + host: remoteAddress.val.address.join('.'), + port: remoteAddress.val.port, family: this.addressFamily, }); diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index 8a829b0f0..e0a8c16cb 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -54,6 +54,14 @@ export const errorCode = { permanentResolverFailure: "permanent-resolver-failure", }; +/** @type {IpAddressFamily[]} */ +const supportedAddressFamilies = ["ipv4", "ipv6"]; + +export const IpAddressFamily = { + ipv4: "ipv4", + ipv6: "ipv6", +}; + export class WasiSockets { networkCnt = 1; tcpSocketCnt = 1; @@ -97,6 +105,8 @@ export class WasiSockets { }; this.network = { + errorCode, + IpAddressFamily, /** * @param {Network} networkId * @returns {void} @@ -113,13 +123,22 @@ export class WasiSockets { this.tcpCreateSocket = { /** * @param {IpAddressFamily} addressFamily - * @returns {number} + * @returns {TcpSocket} + * @throws {Error} not-supported | address-family-not-supported | new-socket-limit */ createTcpSocket(addressFamily) { console.log(`[tcp] Create tcp socket ${addressFamily}`); - const socket = new TcpSocket(addressFamily); - return socket.id; + if (supportedAddressFamilies.includes(addressFamily) === false) { + throw new Error(errorCode.addressFamilyNotSupported); + } + + try { + return new TcpSocket(addressFamily); + } + catch (e) { + throw new Error(errorCode.notSupported); + } }, }; } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 9983f1ff0..7606c90eb 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,4 +1,4 @@ -import { equal, ok, strictEqual } from "node:assert"; +import { equal, ok, throws } from "node:assert"; import { fileURLToPath } from "node:url"; suite("Node.js Preview2", () => { @@ -180,5 +180,56 @@ suite("Node.js Preview2", () => { const op2 = sockets.network.dropNetwork(99999); equal(op2, false); }); + + test("createTcpSocket", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + + const { id } = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + equal(id, 1); + + throws( + () => { + sockets.tcpCreateSocket.createTcpSocket("abc"); + }, + { + name: "Error", + message: sockets.network.errorCode.addressFamilyNotSupported, + } + ); + }); + + suite("TCP", async () => { + test("tcpSocketBind", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + const network = sockets.instanceNetwork.instanceNetwork(); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + tcpSocket.startBind(tcpSocket, network, localAddress); + + equal(tcpSocket.isBound, true); + equal(tcpSocket.network.id, network.id); + equal(tcpSocket.socketAddress.family, "ipv4"); + equal(tcpSocket.socketAddress.address, "0.0.0.0"); + equal(tcpSocket.socketAddress.port, 0); + equal(tcpSocket.socketAddress.flowlabel, 0); + + tcpSocket.finishBind(tcpSocket); + + equal(tcpSocket.isBound, false); + equal(tcpSocket.network, null); + equal(tcpSocket.socketAddress, null); + + }); + }); }); }); From daacaecda52ec7ff375751048e9dc785b7e8baf7 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 19 Oct 2023 10:31:02 +0200 Subject: [PATCH 08/65] fix: dupe-class-members --- .../lib/sockets/tcp-socket-impl.js | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 6bffd778d..dc2cf17e3 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -18,15 +18,16 @@ export class TcpSocketImpl { /** @type {boolean} */ isBound = false; /** @type {NodeSocket} */ socket = null; /** @type {Network} */ network = null; - /** @type {IpAddressFamily} */ addressFamily; - /** @type {IpSocketAddress} */ localAddress = null; + /** @type {NodeSocketAddress} */ socketAddress = null; - ipv6Only = false; - state = "closed"; + + /** @type {IpAddressFamily} */ _addressFamily; + _ipv6Only = false; + _state = "closed"; constructor(socketId, addressFamily) { this.id = socketId; - this.addressFamily = addressFamily; + this._addressFamily = addressFamily; this.socket = new NodeSocket(); } @@ -47,7 +48,7 @@ export class TcpSocketImpl { this.socketAddress = new NodeSocketAddress({ address: localAddress.val.address.join('.'), port: localAddress.val.port, - family: this.addressFamily, + family: this._addressFamily, }); this.network = network; @@ -82,37 +83,37 @@ export class TcpSocketImpl { localPort: this.socketAddress.port, host: remoteAddress.val.address.join('.'), port: remoteAddress.val.port, - family: this.addressFamily, + family: this._addressFamily, }); this.socket.on("connect", () => { console.log(`[tcp] connect on socket ${tcpSocket.id}`); - this.state = "connected"; + this._state = "connected"; }); this.socket.on("ready", () => { console.log(`[tcp] ready on socket ${tcpSocket.id}`); - this.state = "connection"; + this._state = "connection"; }); this.socket.on("close", () => { console.log(`[tcp] close on socket ${tcpSocket.id}`); - this.state = "closed"; + this._state = "closed"; }); this.socket.on("end", () => { console.log(`[tcp] end on socket ${tcpSocket.id}`); - this.state = "closed"; + this._state = "closed"; }); this.socket.on("timeout", () => { console.error(`[tcp] timeout on socket ${tcpSocket.id}`); - this.state = "closed"; + this._state = "closed"; }); this.socket.on("error", (err) => { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); - this.state = "error"; + this._state = "error"; }); } @@ -180,7 +181,7 @@ export class TcpSocketImpl { throw new Error("not-bound"); } - if (this.state !== "connected") { + if (this._state !== "connected") { throw new Error("not-connected"); } @@ -204,7 +205,7 @@ export class TcpSocketImpl { ipv6Only(tcpSocket) { console.log(`[tcp] ipv6 only socket ${this.id}`); - return this.ipv6Only; + return this._ipv6Only; } /** @@ -215,7 +216,7 @@ export class TcpSocketImpl { setIpv6Only(tcpSocket, value) { console.log(`[tcp] set ipv6 only socket ${tcpSocket.id} to ${value}`); - this.ipv6Only = value; + this._ipv6Only = value; } /** From a4c4322f0162a53202b53b1df9969d6c0900561d Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 19 Oct 2023 18:45:26 +0200 Subject: [PATCH 09/65] chore: add throws entry to jsdoc --- .../lib/sockets/tcp-socket-impl.js | 112 ++++++++++++++---- packages/preview2-shim/test/test.js | 25 +++- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index dc2cf17e3..142481dd6 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,7 +11,7 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ -import { Socket as NodeSocket, SocketAddress as NodeSocketAddress } from "node:net"; +import { Socket as NodeSocket, SocketAddress as NodeSocketAddress } from "node:net"; export class TcpSocketImpl { /** @type {number} */ id; @@ -20,15 +20,13 @@ export class TcpSocketImpl { /** @type {Network} */ network = null; /** @type {NodeSocketAddress} */ socketAddress = null; - - /** @type {IpAddressFamily} */ _addressFamily; - _ipv6Only = false; - _state = "closed"; + /** @type {IpAddressFamily} */ #addressFamily; + #ipv6Only = false; + #state = "closed"; constructor(socketId, addressFamily) { this.id = socketId; - this._addressFamily = addressFamily; - + this.#addressFamily = addressFamily; this.socket = new NodeSocket(); } @@ -37,18 +35,21 @@ export class TcpSocketImpl { * @param {Network} network * @param {IpSocketAddress} localAddress * @returns {void} + * @throws {address-family-mismatch} The `local-address` has the wrong address family. (EINVAL) + * @throws {already-bound} The socket is already bound. (EINVAL) + * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) **/ startBind(tcpSocket, network, localAddress) { console.log(`[tcp] start bind socket ${tcpSocket.id}`); if (this.isBound) { - throw new Error("socket is already bound"); + throw new Error("already-bound"); } this.socketAddress = new NodeSocketAddress({ - address: localAddress.val.address.join('.'), + address: localAddress.val.address.join("."), port: localAddress.val.port, - family: this._addressFamily, + family: this.#addressFamily, }); this.network = network; @@ -58,6 +59,11 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket * @returns {void} + * @throws {ephemeral-ports-exhausted} No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + * @throws {address-in-use} Address is already in use. (EADDRINUSE) + * @throws {address-not-bindable} `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + * @throws {not-in-progress} A `bind` operation is not in progress. + * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind(tcpSocket) { console.log(`[tcp] finish bind socket ${tcpSocket.id}`); @@ -72,6 +78,13 @@ export class TcpSocketImpl { * @param {Network} network * @param {IpSocketAddress} remoteAddress * @returns {void} + * @throws {address-family-mismatch} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-remote-address} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + * @throws {invalid-remote-port} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + * @throws {already-attached} The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + * @throws {already-connected} The socket is already in the Connection state. (EISCONN) + * @throws {already-listening} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) + * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ startConnect(tcpSocket, network, remoteAddress) { console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); @@ -79,47 +92,54 @@ export class TcpSocketImpl { this.network = network; this.socket.connect({ - localAddress: this.socketAddress.address.join('.'), + localAddress: this.socketAddress.address.join("."), localPort: this.socketAddress.port, - host: remoteAddress.val.address.join('.'), + host: remoteAddress.val.address.join("."), port: remoteAddress.val.port, - family: this._addressFamily, + family: this.#addressFamily, }); this.socket.on("connect", () => { console.log(`[tcp] connect on socket ${tcpSocket.id}`); - this._state = "connected"; + this.#state = "connected"; }); this.socket.on("ready", () => { console.log(`[tcp] ready on socket ${tcpSocket.id}`); - this._state = "connection"; + this.#state = "connection"; }); this.socket.on("close", () => { console.log(`[tcp] close on socket ${tcpSocket.id}`); - this._state = "closed"; + this.#state = "closed"; }); this.socket.on("end", () => { console.log(`[tcp] end on socket ${tcpSocket.id}`); - this._state = "closed"; + this.#state = "closed"; }); this.socket.on("timeout", () => { console.error(`[tcp] timeout on socket ${tcpSocket.id}`); - this._state = "closed"; + this.#state = "closed"; }); this.socket.on("error", (err) => { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); - this._state = "error"; + this.#state = "error"; }); } /** * @param {TcpSocket} tcpSocket * @returns {Array} + * @throws {timeout} Connection timed out. (ETIMEDOUT) + * @throws {connection-refused} The connection was forcefully rejected. (ECONNREFUSED) + * @throws {connection-reset} The connection was reset. (ECONNRESET) + * @throws {remote-unreachable} The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {ephemeral-ports-exhausted} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * @throws {not-in-progress} A `connect` operation is not in progress. + * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) * */ finishConnect(tcpSocket) { console.log(`[tcp] finish connect socket ${tcpSocket.id}`); @@ -128,8 +148,12 @@ export class TcpSocketImpl { } /** - * @param {TcpSocket} tcpSocket + * @param {TcpSocket} tcpSocket * @returns {void} + * @throws {not-bound} The socket is not bound to any local address. (EDESTADDRREQ) + * @throws {already-connected} The socket is already in the Connection state. (EISCONN, EINVAL on BSD) + * @throws {already-listening} The socket is already in the Listener state. + * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) * */ startListen(tcpSocket) { console.log(`[tcp] start listen socket ${tcpSocket.id}`); @@ -140,6 +164,9 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket * @returns {void} + * @throws {ephemeral-ports-exhausted} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + * @throws {not-in-progress} A `listen` operation is not in progress. + * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) * */ finishListen(tcpSocket) { console.log(`[tcp] finish listen socket ${tcpSocket.id}`); @@ -149,6 +176,8 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket * @returns {Array} + * @throws {not-listening} Socket is not in the Listener state. (EINVAL) + * @throws {would-block} No pending connections at the moment. (EWOULDBLOCK, EAGAIN) * */ accept(tcpSocket) { console.log(`[tcp] accept socket ${tcpSocket.id}`); @@ -158,6 +187,7 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket * @returns {IpSocketAddress} + * @throws {not-bound} The socket is not bound to any local address. * */ localAddress(tcpSocket) { console.log(`[tcp] local address socket ${tcpSocket.id}`); @@ -172,16 +202,16 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket * @returns {IpSocketAddress} + * @throws {not-connected} The socket is not connected to a remote address. (ENOTCONN) * */ remoteAddress(tcpSocket) { console.log(`[tcp] remote address socket ${tcpSocket.id}`); - if (!this.isBound) { throw new Error("not-bound"); } - if (this._state !== "connected") { + if (this.#state !== "connected") { throw new Error("not-connected"); } @@ -201,28 +231,36 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket * @returns {boolean} + * @throws {ipv6-only-operation} (get/set) `this` socket is an IPv4 socket. + * */ ipv6Only(tcpSocket) { console.log(`[tcp] ipv6 only socket ${this.id}`); - return this._ipv6Only; + return this.#ipv6Only; } /** * @param {TcpSocket} tcpSocket * @param {boolean} value * @returns {void} + * @throws {ipv6-only-operation} (get/set) `this` socket is an IPv4 socket. + * @throws {already-bound} (set) The socket is already bound. + * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setIpv6Only(tcpSocket, value) { console.log(`[tcp] set ipv6 only socket ${tcpSocket.id} to ${value}`); - this._ipv6Only = value; + this.#ipv6Only = value; } /** * @param {TcpSocket} tcpSocket * @param {bigint} value * @returns {void} + * @throws {already-connected} (set) The socket is already in the Connection state. + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setListenBacklogSize(tcpSocket, value) { console.log(`[tcp] set listen backlog size socket ${tcpSocket.id} to ${value}`); @@ -239,9 +277,10 @@ export class TcpSocketImpl { } /** - * @param {TcpSocket} tcpSocket + * @param {TcpSocket} tcpSocket * @param {boolean} value * @returns {void} + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setKeepAlive(tcpSocket, value) { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); @@ -259,7 +298,19 @@ export class TcpSocketImpl { /** * @param {TcpSocket} tcpSocket + * @param {boolean} value * @returns {void} + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * */ + setNoDelay(tcpSocket, value) { + console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); + throw new Error("not implemented"); + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {void} + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ unicastHopLimit(tcpSocket) { console.log(`[tcp] unicast hop limit socket ${tcpSocket.id}`); @@ -270,6 +321,10 @@ export class TcpSocketImpl { * @param {TcpSocket} tcpSocket * @param {number} value * @returns {void} + * @throws {already-connected} (set) The socket is already in the Connection state. + * @throws {already-listening} (set) The socket is already in the Listener state. + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * */ setUnicastHopLimit(tcpSocket, value) { console.log(`[tcp] set unicast hop limit socket ${tcpSocket.id} to ${value}`); @@ -289,6 +344,9 @@ export class TcpSocketImpl { * @param {TcpSocket} tcpSocket * @param {bigint} value * @returns {void} + * @throws {already-connected} (set) The socket is already in the Connection state. + * @throws {already-listening} (set) The socket is already in the Listener state. + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setReceiveBufferSize(tcpSocket, value) { console.log(`[tcp] set receive buffer size socket ${this.id} to ${value}`); @@ -308,6 +366,9 @@ export class TcpSocketImpl { * @param {TcpSocket} tcpSocket * @param {bigint} value * @returns {void} + * @throws {already-connected} (set) The socket is already in the Connection state. + * @throws {already-listening} (set) The socket is already in the Listener state. + * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setSendBufferSize(tcpSocket, value) { console.log(`[tcp] set send buffer size socket ${tcpSocket.id} to ${value}`); @@ -327,6 +388,7 @@ export class TcpSocketImpl { * @param {TcpSocket} tcpSocket * @param {ShutdownType} shutdownType * @returns {void} + * @throws {not-connected} The socket is not in the Connection state. (ENOTCONN) * */ shutdown(tcpSocket, shutdownType) { console.log(`[tcp] shutdown socket ${tcpSocket.id} with ${shutdownType}`); diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 7606c90eb..907a4e2e3 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -200,13 +200,14 @@ suite("Node.js Preview2", () => { ); }); - suite("TCP", async () => { - test("tcpSocketBind", async () => { + suite("tcp", async () => { + test("startBind", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); + + const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( sockets.network.IpAddressFamily.ipv4 ); - const network = sockets.instanceNetwork.instanceNetwork(); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -223,6 +224,24 @@ suite("Node.js Preview2", () => { equal(tcpSocket.socketAddress.port, 0); equal(tcpSocket.socketAddress.flowlabel, 0); + + }); + + test("finishBind", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); equal(tcpSocket.isBound, false); From a76897d1246862ea0edacbfee88828dda52fd902 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 22 Oct 2023 11:57:04 +0200 Subject: [PATCH 10/65] chore: improve startBind() logic --- .../lib/sockets/tcp-socket-impl.js | 31 +++- packages/preview2-shim/test/test.js | 151 ++++++++++++------ 2 files changed, 128 insertions(+), 54 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 142481dd6..4bd516341 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,7 +11,15 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ -import { Socket as NodeSocket, SocketAddress as NodeSocketAddress } from "node:net"; +import { Socket as NodeSocket, SocketAddress as NodeSocketAddress, isIPv4, isIPv6, isIP } from "node:net"; + +function tupleToIPv6(arr) { + if (arr.length !== 8) { + return null; // Return null for invalid input + } + const ipv6Segments = arr.map((segment) => segment.toString(16)); + return ipv6Segments.join(":"); +} export class TcpSocketImpl { /** @type {number} */ id; @@ -46,9 +54,26 @@ export class TcpSocketImpl { throw new Error("already-bound"); } + let { address } = localAddress.val; + if (this.#addressFamily.toLocaleLowerCase() === "ipv4") { + address = address.join("."); + } else if (this.#addressFamily.toLocaleLowerCase() === "ipv6") { + address = tupleToIPv6(address); + } + + console.log({ + address, + }); + const port = localAddress.val.port; + const ipFamily = `ipv${isIP(address)}`; + + if (this.#addressFamily.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { + throw new Error("address-family-mismatch"); + } + this.socketAddress = new NodeSocketAddress({ - address: localAddress.val.address.join("."), - port: localAddress.val.port, + address, + port, family: this.#addressFamily, }); diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 907a4e2e3..26255911e 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -160,7 +160,7 @@ suite("Node.js Preview2", () => { }); suite("Sockets", async () => { - test("instanceNetwork", async () => { + test("sockets.instanceNetwork()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network1 = sockets.instanceNetwork.instanceNetwork(); equal(network1.id, 1); @@ -168,7 +168,7 @@ suite("Node.js Preview2", () => { equal(network2.id, 1); }); - test("dropNetwork", async () => { + test("sockets.dropNetwork()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const net = sockets.instanceNetwork.instanceNetwork(); @@ -181,12 +181,9 @@ suite("Node.js Preview2", () => { equal(op2, false); }); - test("createTcpSocket", async () => { + test("sockets.tcpCreateSocket()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - - const { id } = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const { id } = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); equal(id, 1); throws( @@ -199,56 +196,108 @@ suite("Node.js Preview2", () => { } ); }); + test("tcp.startBind(): should bind to a valid ipv4 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + tcpSocket.startBind(tcpSocket, network, localAddress); + + equal(tcpSocket.isBound, true); + equal(tcpSocket.network.id, network.id); + equal(tcpSocket.socketAddress.family, "ipv4"); + equal(tcpSocket.socketAddress.address, "0.0.0.0"); + equal(tcpSocket.socketAddress.port, 0); + equal(tcpSocket.socketAddress.flowlabel, 0); + }); + + test("tcp.startBind(): should bind to a valid ipv6 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv6); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], + port: 0, + }, + }; + tcpSocket.startBind(tcpSocket, network, localAddress); + + equal(tcpSocket.isBound, true); + equal(tcpSocket.network.id, network.id); + equal(tcpSocket.socketAddress.family, "ipv6"); + equal(tcpSocket.socketAddress.address, "::ffff:192.168.0.1"); + equal(tcpSocket.socketAddress.port, 0); + equal(tcpSocket.socketAddress.flowlabel, 0); + }); + + test("tcp.startBind(): should throw address-family-mismatch", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); - suite("tcp", async () => { - test("startBind", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); - - const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); - const localAddress = { - tag: sockets.network.IpAddressFamily.ipv4, - val: { - address: [0, 0, 0, 0], - port: 0, - }, - }; + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], + port: 0, + }, + }; + throws(() => { tcpSocket.startBind(tcpSocket, network, localAddress); + }, { + name: 'Error', + message: 'address-family-mismatch' + }) + }); + + test("tcp.startBind(): should throw already-bound", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); - equal(tcpSocket.isBound, true); - equal(tcpSocket.network.id, network.id); - equal(tcpSocket.socketAddress.family, "ipv4"); - equal(tcpSocket.socketAddress.address, "0.0.0.0"); - equal(tcpSocket.socketAddress.port, 0); - equal(tcpSocket.socketAddress.flowlabel, 0); - - - }); - - test("finishBind", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); - - const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); - const localAddress = { - tag: sockets.network.IpAddressFamily.ipv4, - val: { - address: [0, 0, 0, 0], - port: 0, - }, - }; + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + throws(() => { + tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); + }, { + name: 'Error', + message: 'already-bound' + }) + }); - equal(tcpSocket.isBound, false); - equal(tcpSocket.network, null); - equal(tcpSocket.socketAddress, null); + test("tcp.finishBind()", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); - }); + equal(tcpSocket.isBound, false); + equal(tcpSocket.network, null); + equal(tcpSocket.socketAddress, null); }); }); }); From 0867ff27c46a44a24fd01618dd74a4b187b51616 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 22 Oct 2023 19:49:21 +0200 Subject: [PATCH 11/65] chore: improve startConnect() + test --- .../lib/sockets/tcp-socket-impl.js | 56 ++++++++++++++----- packages/preview2-shim/test/test.js | 34 +++++++++++ 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 4bd516341..508b76ae2 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,7 +11,7 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ -import { Socket as NodeSocket, SocketAddress as NodeSocketAddress, isIPv4, isIPv6, isIP } from "node:net"; +import { Socket as NodeSocket, SocketAddress as NodeSocketAddress, isIP } from "node:net"; function tupleToIPv6(arr) { if (arr.length !== 8) { @@ -38,6 +38,17 @@ export class TcpSocketImpl { this.socket = new NodeSocket(); } + #computeIpAddress(localAddress) { + let { address } = localAddress.val; + if (this.#addressFamily.toLocaleLowerCase() === "ipv4") { + address = address.join("."); + } else if (this.#addressFamily.toLocaleLowerCase() === "ipv6") { + address = tupleToIPv6(address); + } + + return address; + } + /** * @param {TcpSocket} tcpSocket * @param {Network} network @@ -54,23 +65,14 @@ export class TcpSocketImpl { throw new Error("already-bound"); } - let { address } = localAddress.val; - if (this.#addressFamily.toLocaleLowerCase() === "ipv4") { - address = address.join("."); - } else if (this.#addressFamily.toLocaleLowerCase() === "ipv6") { - address = tupleToIPv6(address); - } + const address = this.#computeIpAddress(localAddress); - console.log({ - address, - }); - const port = localAddress.val.port; const ipFamily = `ipv${isIP(address)}`; - if (this.#addressFamily.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { throw new Error("address-family-mismatch"); } + const { port } = localAddress.val; this.socketAddress = new NodeSocketAddress({ address, port, @@ -114,12 +116,38 @@ export class TcpSocketImpl { startConnect(tcpSocket, network, remoteAddress) { console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); + if (this.network !== null && this.network.id !== network.id) { + throw new Error("already-attached"); + } this.network = network; + if (this.#state === "connected") { + throw new Error("already-connected"); + } + + if (this.#state === "connection") { + throw new Error("already-listening"); + } + + if (this.isBound === false) { + throw new Error("not-bound"); + } + + const host = this.#computeIpAddress(remoteAddress); + + const ipFamily = `ipv${isIP(host)}`; + if (ipFamily.toLocaleLowerCase() === "ipv0") { + throw new Error("invalid-remote-address"); + } + + if (this.#addressFamily.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { + throw new Error("address-family-mismatch"); + } + this.socket.connect({ - localAddress: this.socketAddress.address.join("."), + localAddress: this.socketAddress.address, localPort: this.socketAddress.port, - host: remoteAddress.val.address.join("."), + host, port: remoteAddress.val.port, family: this.#addressFamily, }); diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 26255911e..aeafd38f9 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,4 +1,5 @@ import { equal, ok, throws } from "node:assert"; +import { mock } from 'node:test'; import { fileURLToPath } from "node:url"; suite("Node.js Preview2", () => { @@ -299,5 +300,38 @@ suite("Node.js Preview2", () => { equal(tcpSocket.network, null); equal(tcpSocket.socketAddress, null); }); + test("tcp.startConnect(): should connect to a valid ipv4 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + const remoteAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [192, 168, 0, 1], + port: 80, + }, + }; + + mock.method(tcpSocket.socket, 'connect', () => console.log('connect called')); + + tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.startConnect(tcpSocket, network, remoteAddress); + + equal(tcpSocket.isBound, true); + equal(tcpSocket.network.id, network.id); + equal(tcpSocket.socketAddress.family, "ipv4"); + equal(tcpSocket.socketAddress.address, "0.0.0.0"); + equal(tcpSocket.socketAddress.port, 0); + equal(tcpSocket.socketAddress.flowlabel, 0); + }); }); + + }); From 0e978a84f7d279063bc16f1b85a2e685790f2222 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 22 Oct 2023 21:57:50 +0200 Subject: [PATCH 12/65] chore: improve tcp-socket impl --- .../lib/sockets/tcp-socket-impl.js | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 508b76ae2..c89711ef8 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -29,13 +29,24 @@ export class TcpSocketImpl { /** @type {NodeSocketAddress} */ socketAddress = null; /** @type {IpAddressFamily} */ #addressFamily; + #canReceive = true; + #canSend = true; #ipv6Only = false; #state = "closed"; + #noDelay = false; + #keepAlive = false; + + // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 + #backlog = 128; + constructor(socketId, addressFamily) { this.id = socketId; this.#addressFamily = addressFamily; - this.socket = new NodeSocket(); + this.socket = new NodeSocket({ + keepAlive: this.#keepAlive, + noDelay: this.#noDelay + }); } #computeIpAddress(localAddress) { @@ -210,8 +221,7 @@ export class TcpSocketImpl { * */ startListen(tcpSocket) { console.log(`[tcp] start listen socket ${tcpSocket.id}`); - - this.socket.listen(); + throw new Error("not implemented"); } /** @@ -317,7 +327,8 @@ export class TcpSocketImpl { * */ setListenBacklogSize(tcpSocket, value) { console.log(`[tcp] set listen backlog size socket ${tcpSocket.id} to ${value}`); - throw new Error("not implemented"); + + this.#backlog = value; } /** @@ -326,7 +337,8 @@ export class TcpSocketImpl { * */ keepAlive(tcpSocket) { console.log(`[tcp] keep alive socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + this.#keepAlive; } /** @@ -337,7 +349,9 @@ export class TcpSocketImpl { * */ setKeepAlive(tcpSocket, value) { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); - throw new Error("not implemented"); + + this.#keepAlive = value; + this.socket.setKeepAlive(value); } /** @@ -346,7 +360,8 @@ export class TcpSocketImpl { * */ noDelay(tcpSocket) { console.log(`[tcp] no delay socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + return this.#noDelay; } /** @@ -357,7 +372,9 @@ export class TcpSocketImpl { * */ setNoDelay(tcpSocket, value) { console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); - throw new Error("not implemented"); + + this.#noDelay = value; + this.socket.setNoDelay(value); } /** @@ -445,7 +462,20 @@ export class TcpSocketImpl { * */ shutdown(tcpSocket, shutdownType) { console.log(`[tcp] shutdown socket ${tcpSocket.id} with ${shutdownType}`); - throw new Error("not implemented"); + + if (this.#state !== "connected") { + throw new Error("not-connected"); + } + + if (shutdownType === "read") { + this.#canReceive = false; + } else if (shutdownType === "write") { + this.#canSend = false; + } + else if (shutdownType === 'both') { + this.#canReceive = false; + this.#canSend = false; + } } /** @@ -454,6 +484,9 @@ export class TcpSocketImpl { * */ dropTcpSocket(tcpSocket) { console.log(`[tcp] drop socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + this.finishBind(tcpSocket); + this.finishConnect(tcpSocket); + this.socket.destroy(); } } From f4abc22d0cbf8d8927ddc02a95b20c5807e506ec Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 23 Oct 2023 15:26:27 +0200 Subject: [PATCH 13/65] refactor: start-* finish-* methods to match specs --- .../lib/sockets/tcp-socket-impl.js | 103 +++++++++--------- packages/preview2-shim/test/test.js | 23 +--- 2 files changed, 55 insertions(+), 71 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index c89711ef8..e5334e27c 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -29,31 +29,29 @@ export class TcpSocketImpl { /** @type {NodeSocketAddress} */ socketAddress = null; /** @type {IpAddressFamily} */ #addressFamily; + #socketOptions = {}; #canReceive = true; #canSend = true; #ipv6Only = false; #state = "closed"; - #noDelay = false; - #keepAlive = false; - // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; constructor(socketId, addressFamily) { this.id = socketId; - this.#addressFamily = addressFamily; - this.socket = new NodeSocket({ - keepAlive: this.#keepAlive, - noDelay: this.#noDelay - }); + this.socket = new NodeSocket(); + + this.#socketOptions.family = addressFamily; + this.#socketOptions.keepAlive = false; + this.#socketOptions.noDelay = false; } #computeIpAddress(localAddress) { let { address } = localAddress.val; - if (this.#addressFamily.toLocaleLowerCase() === "ipv4") { + if (this.#socketOptions.family.toLocaleLowerCase() === "ipv4") { address = address.join("."); - } else if (this.#addressFamily.toLocaleLowerCase() === "ipv6") { + } else if (this.#socketOptions.family.toLocaleLowerCase() === "ipv6") { address = tupleToIPv6(address); } @@ -79,19 +77,14 @@ export class TcpSocketImpl { const address = this.#computeIpAddress(localAddress); const ipFamily = `ipv${isIP(address)}`; - if (this.#addressFamily.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { + if (this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { throw new Error("address-family-mismatch"); } const { port } = localAddress.val; - this.socketAddress = new NodeSocketAddress({ - address, - port, - family: this.#addressFamily, - }); - + this.#socketOptions.address = address; + this.#socketOptions.port = port; this.network = network; - this.isBound = true; } /** @@ -106,9 +99,14 @@ export class TcpSocketImpl { finishBind(tcpSocket) { console.log(`[tcp] finish bind socket ${tcpSocket.id}`); - this.network = null; - this.socketAddress = null; - this.isBound = false; + const { address, port, family } = this.#socketOptions; + this.socketAddress = new NodeSocketAddress({ + address, + port, + family, + }); + + this.isBound = true; } /** @@ -130,7 +128,6 @@ export class TcpSocketImpl { if (this.network !== null && this.network.id !== network.id) { throw new Error("already-attached"); } - this.network = network; if (this.#state === "connected") { throw new Error("already-connected"); @@ -151,16 +148,36 @@ export class TcpSocketImpl { throw new Error("invalid-remote-address"); } - if (this.#addressFamily.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { + if (this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { throw new Error("address-family-mismatch"); } + this.network = network; + this.#socketOptions.remoteAddress = host; + this.#socketOptions.remotePort = remoteAddress.val.port; + } + + /** + * @param {TcpSocket} tcpSocket + * @returns {Array} + * @throws {timeout} Connection timed out. (ETIMEDOUT) + * @throws {connection-refused} The connection was forcefully rejected. (ECONNREFUSED) + * @throws {connection-reset} The connection was reset. (ECONNRESET) + * @throws {remote-unreachable} The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {ephemeral-ports-exhausted} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * @throws {not-in-progress} A `connect` operation is not in progress. + * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + * */ + finishConnect(tcpSocket) { + console.log(`[tcp] finish connect socket ${tcpSocket.id}`); + + const { address, port, remoteAddress, remotePort, family } = this.#socketOptions; this.socket.connect({ - localAddress: this.socketAddress.address, - localPort: this.socketAddress.port, - host, - port: remoteAddress.val.port, - family: this.#addressFamily, + localAddress: address, + localPort: port, + host: remoteAddress, + port: remotePort, + family: family, }); this.socket.on("connect", () => { @@ -194,23 +211,6 @@ export class TcpSocketImpl { }); } - /** - * @param {TcpSocket} tcpSocket - * @returns {Array} - * @throws {timeout} Connection timed out. (ETIMEDOUT) - * @throws {connection-refused} The connection was forcefully rejected. (ECONNREFUSED) - * @throws {connection-reset} The connection was reset. (ECONNRESET) - * @throws {remote-unreachable} The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {ephemeral-ports-exhausted} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) - * @throws {not-in-progress} A `connect` operation is not in progress. - * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - * */ - finishConnect(tcpSocket) { - console.log(`[tcp] finish connect socket ${tcpSocket.id}`); - - this.socket.destroySoon(); - } - /** * @param {TcpSocket} tcpSocket * @returns {void} @@ -338,7 +338,7 @@ export class TcpSocketImpl { keepAlive(tcpSocket) { console.log(`[tcp] keep alive socket ${tcpSocket.id}`); - this.#keepAlive; + this.#socketOptions.keepAlive; } /** @@ -350,7 +350,7 @@ export class TcpSocketImpl { setKeepAlive(tcpSocket, value) { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); - this.#keepAlive = value; + this.#socketOptions.keepAlive = value; this.socket.setKeepAlive(value); } @@ -361,7 +361,7 @@ export class TcpSocketImpl { noDelay(tcpSocket) { console.log(`[tcp] no delay socket ${tcpSocket.id}`); - return this.#noDelay; + return this.#socketOptions.noDelay; } /** @@ -373,7 +373,7 @@ export class TcpSocketImpl { setNoDelay(tcpSocket, value) { console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); - this.#noDelay = value; + this.#socketOptions.noDelay = value; this.socket.setNoDelay(value); } @@ -471,8 +471,7 @@ export class TcpSocketImpl { this.#canReceive = false; } else if (shutdownType === "write") { this.#canSend = false; - } - else if (shutdownType === 'both') { + } else if (shutdownType === "both") { this.#canReceive = false; this.#canSend = false; } @@ -485,8 +484,6 @@ export class TcpSocketImpl { dropTcpSocket(tcpSocket) { console.log(`[tcp] drop socket ${tcpSocket.id}`); - this.finishBind(tcpSocket); - this.finishConnect(tcpSocket); this.socket.destroy(); } } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index aeafd38f9..37e5e7f07 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -209,6 +209,7 @@ suite("Node.js Preview2", () => { }, }; tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); equal(tcpSocket.isBound, true); equal(tcpSocket.network.id, network.id); @@ -230,6 +231,7 @@ suite("Node.js Preview2", () => { }, }; tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); equal(tcpSocket.isBound, true); equal(tcpSocket.network.id, network.id); @@ -275,6 +277,7 @@ suite("Node.js Preview2", () => { }; throws(() => { tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); tcpSocket.startBind(tcpSocket, network, localAddress); }, { name: 'Error', @@ -282,24 +285,6 @@ suite("Node.js Preview2", () => { }) }); - test("tcp.finishBind()", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); - const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); - const localAddress = { - tag: sockets.network.IpAddressFamily.ipv4, - val: { - address: [0, 0, 0, 0], - port: 0, - }, - }; - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); - - equal(tcpSocket.isBound, false); - equal(tcpSocket.network, null); - equal(tcpSocket.socketAddress, null); - }); test("tcp.startConnect(): should connect to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); @@ -322,7 +307,9 @@ suite("Node.js Preview2", () => { mock.method(tcpSocket.socket, 'connect', () => console.log('connect called')); tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); tcpSocket.startConnect(tcpSocket, network, remoteAddress); + tcpSocket.finishConnect(tcpSocket); equal(tcpSocket.isBound, true); equal(tcpSocket.network.id, network.id); From 1a178360f07992a68595a1f09c653fa057657df4 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 23 Oct 2023 18:01:07 +0200 Subject: [PATCH 14/65] chore: use assertion statement in tcp-socket-impl --- .../lib/sockets/tcp-socket-impl.js | 132 +++++++++--------- packages/preview2-shim/test/test.js | 3 +- 2 files changed, 68 insertions(+), 67 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index e5334e27c..c9c9f3fd8 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -21,41 +21,51 @@ function tupleToIPv6(arr) { return ipv6Segments.join(":"); } +function computeIpAddress(localAddress, family) { + let { address } = localAddress.val; + if (family.toLocaleLowerCase() === "ipv4") { + address = address.join("."); + } else if (family.toLocaleLowerCase() === "ipv6") { + address = tupleToIPv6(address); + } + + return address; +} + +function assert(condition, message) { + if (condition) { + throw new Error(message); + } +} + export class TcpSocketImpl { /** @type {number} */ id; /** @type {boolean} */ isBound = false; - /** @type {NodeSocket} */ socket = null; + /** @type {NodeSocket} */ #socket = null; /** @type {Network} */ network = null; /** @type {NodeSocketAddress} */ socketAddress = null; - /** @type {IpAddressFamily} */ #addressFamily; #socketOptions = {}; #canReceive = true; #canSend = true; #ipv6Only = false; #state = "closed"; + #inprogress = false; // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; constructor(socketId, addressFamily) { this.id = socketId; - this.socket = new NodeSocket(); + this.#socket = new NodeSocket(); this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; this.#socketOptions.noDelay = false; } - #computeIpAddress(localAddress) { - let { address } = localAddress.val; - if (this.#socketOptions.family.toLocaleLowerCase() === "ipv4") { - address = address.join("."); - } else if (this.#socketOptions.family.toLocaleLowerCase() === "ipv6") { - address = tupleToIPv6(address); - } - - return address; + socket() { + return this.#socket; } /** @@ -70,11 +80,10 @@ export class TcpSocketImpl { startBind(tcpSocket, network, localAddress) { console.log(`[tcp] start bind socket ${tcpSocket.id}`); - if (this.isBound) { - throw new Error("already-bound"); - } + assert(this.isBound, "already-bound"); + assert(this.#inprogress, "concurrency-conflict"); - const address = this.#computeIpAddress(localAddress); + const address = computeIpAddress(localAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(address)}`; if (this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { @@ -85,6 +94,7 @@ export class TcpSocketImpl { this.#socketOptions.address = address; this.#socketOptions.port = port; this.network = network; + this.#inprogress = true; } /** @@ -99,7 +109,11 @@ export class TcpSocketImpl { finishBind(tcpSocket) { console.log(`[tcp] finish bind socket ${tcpSocket.id}`); + assert(this.#inprogress === false, "not-in-progress"); + const { address, port, family } = this.#socketOptions; + assert(isIP(address) === 0, "address-not-bindable"); + this.socketAddress = new NodeSocketAddress({ address, port, @@ -107,6 +121,7 @@ export class TcpSocketImpl { }); this.isBound = true; + this.#inprogress = false; } /** @@ -125,36 +140,24 @@ export class TcpSocketImpl { startConnect(tcpSocket, network, remoteAddress) { console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); - if (this.network !== null && this.network.id !== network.id) { - throw new Error("already-attached"); - } - - if (this.#state === "connected") { - throw new Error("already-connected"); - } - - if (this.#state === "connection") { - throw new Error("already-listening"); - } - - if (this.isBound === false) { - throw new Error("not-bound"); - } - - const host = this.#computeIpAddress(remoteAddress); + assert(this.network !== null && this.network.id !== network.id, "already-attached"); + assert(this.#inprogress, "concurrency-conflict"); + assert(this.#state === "connected", "already-connected"); + assert(this.#state === "connection", "already-listening"); + assert(this.#state === "listening", "already-listening"); + assert(this.isBound === false, "already-bound"); + const host = computeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; - if (ipFamily.toLocaleLowerCase() === "ipv0") { - throw new Error("invalid-remote-address"); - } - if (this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { - throw new Error("address-family-mismatch"); - } + assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-remote-address"); + assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "address-family-mismatch"); this.network = network; this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; + + this.#inprogress = true; } /** @@ -172,7 +175,7 @@ export class TcpSocketImpl { console.log(`[tcp] finish connect socket ${tcpSocket.id}`); const { address, port, remoteAddress, remotePort, family } = this.#socketOptions; - this.socket.connect({ + this.#socket.connect({ localAddress: address, localPort: port, host: remoteAddress, @@ -180,35 +183,37 @@ export class TcpSocketImpl { family: family, }); - this.socket.on("connect", () => { + this.#socket.on("connect", () => { console.log(`[tcp] connect on socket ${tcpSocket.id}`); this.#state = "connected"; }); - this.socket.on("ready", () => { + this.#socket.on("ready", () => { console.log(`[tcp] ready on socket ${tcpSocket.id}`); this.#state = "connection"; }); - this.socket.on("close", () => { + this.#socket.on("close", () => { console.log(`[tcp] close on socket ${tcpSocket.id}`); this.#state = "closed"; }); - this.socket.on("end", () => { + this.#socket.on("end", () => { console.log(`[tcp] end on socket ${tcpSocket.id}`); this.#state = "closed"; }); - this.socket.on("timeout", () => { + this.#socket.on("timeout", () => { console.error(`[tcp] timeout on socket ${tcpSocket.id}`); this.#state = "closed"; }); - this.socket.on("error", (err) => { + this.#socket.on("error", (err) => { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); this.#state = "error"; }); + + this.#inprogress = false; } /** @@ -221,6 +226,9 @@ export class TcpSocketImpl { * */ startListen(tcpSocket) { console.log(`[tcp] start listen socket ${tcpSocket.id}`); + + this.#inprogress = true; + throw new Error("not implemented"); } @@ -233,6 +241,9 @@ export class TcpSocketImpl { * */ finishListen(tcpSocket) { console.log(`[tcp] finish listen socket ${tcpSocket.id}`); + + this.#inprogress = false; + throw new Error("not implemented"); } @@ -255,11 +266,9 @@ export class TcpSocketImpl { localAddress(tcpSocket) { console.log(`[tcp] local address socket ${tcpSocket.id}`); - if (!this.isBound) { - throw new Error("not-bound"); - } + assert(this.isBound === false, "not-bound"); - return this.socket.localAddress(); + return this.#socket.localAddress(); } /** @@ -270,15 +279,10 @@ export class TcpSocketImpl { remoteAddress(tcpSocket) { console.log(`[tcp] remote address socket ${tcpSocket.id}`); - if (!this.isBound) { - throw new Error("not-bound"); - } - - if (this.#state !== "connected") { - throw new Error("not-connected"); - } + assert(this.isBound === false, "not-bound"); + assert(this.#state !== "connected", "not-connected"); - return this.socket.remoteAddress(); + return this.#socket.remoteAddress(); } /** @@ -288,7 +292,7 @@ export class TcpSocketImpl { addressFamily(tcpSocket) { console.log(`[tcp] address family socket ${tcpSocket.id}`); - return this.socket.localFamily; + return this.#socket.localFamily; } /** @@ -351,7 +355,7 @@ export class TcpSocketImpl { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); this.#socketOptions.keepAlive = value; - this.socket.setKeepAlive(value); + this.#socket.setKeepAlive(value); } /** @@ -374,7 +378,7 @@ export class TcpSocketImpl { console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); this.#socketOptions.noDelay = value; - this.socket.setNoDelay(value); + this.#socket.setNoDelay(value); } /** @@ -463,9 +467,7 @@ export class TcpSocketImpl { shutdown(tcpSocket, shutdownType) { console.log(`[tcp] shutdown socket ${tcpSocket.id} with ${shutdownType}`); - if (this.#state !== "connected") { - throw new Error("not-connected"); - } + assert(this.#state !== "connected", "not-connected"); if (shutdownType === "read") { this.#canReceive = false; @@ -484,6 +486,6 @@ export class TcpSocketImpl { dropTcpSocket(tcpSocket) { console.log(`[tcp] drop socket ${tcpSocket.id}`); - this.socket.destroy(); + this.#socket.destroy(); } } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 37e5e7f07..c08e3cf9d 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -304,7 +304,7 @@ suite("Node.js Preview2", () => { }, }; - mock.method(tcpSocket.socket, 'connect', () => console.log('connect called')); + mock.method(tcpSocket.socket(), 'connect', () => console.log('connect called')); tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); @@ -316,7 +316,6 @@ suite("Node.js Preview2", () => { equal(tcpSocket.socketAddress.family, "ipv4"); equal(tcpSocket.socketAddress.address, "0.0.0.0"); equal(tcpSocket.socketAddress.port, 0); - equal(tcpSocket.socketAddress.flowlabel, 0); }); }); From 4d42d3e196e1d98b455b606d3e5a219db3242c17 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 23 Oct 2023 18:17:20 +0200 Subject: [PATCH 15/65] refactor: tcp-socket-impl --- .../lib/sockets/tcp-socket-impl.js | 35 +++++++------- packages/preview2-shim/test/test.js | 47 +++++++++---------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index c9c9f3fd8..366cd3ab5 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -39,18 +39,18 @@ function assert(condition, message) { } export class TcpSocketImpl { - /** @type {number} */ id; - /** @type {boolean} */ isBound = false; + id; /** @type {NodeSocket} */ #socket = null; /** @type {Network} */ network = null; /** @type {NodeSocketAddress} */ socketAddress = null; + #isBound = false; #socketOptions = {}; #canReceive = true; #canSend = true; #ipv6Only = false; #state = "closed"; - #inprogress = false; + #inProgress = false; // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; @@ -80,8 +80,8 @@ export class TcpSocketImpl { startBind(tcpSocket, network, localAddress) { console.log(`[tcp] start bind socket ${tcpSocket.id}`); - assert(this.isBound, "already-bound"); - assert(this.#inprogress, "concurrency-conflict"); + assert(this.#isBound, "already-bound"); + assert(this.#inProgress, "concurrency-conflict"); const address = computeIpAddress(localAddress, this.#socketOptions.family); @@ -94,7 +94,7 @@ export class TcpSocketImpl { this.#socketOptions.address = address; this.#socketOptions.port = port; this.network = network; - this.#inprogress = true; + this.#inProgress = true; } /** @@ -109,7 +109,7 @@ export class TcpSocketImpl { finishBind(tcpSocket) { console.log(`[tcp] finish bind socket ${tcpSocket.id}`); - assert(this.#inprogress === false, "not-in-progress"); + assert(this.#inProgress === false, "not-in-progress"); const { address, port, family } = this.#socketOptions; assert(isIP(address) === 0, "address-not-bindable"); @@ -120,8 +120,8 @@ export class TcpSocketImpl { family, }); - this.isBound = true; - this.#inprogress = false; + this.#isBound = true; + this.#inProgress = false; } /** @@ -141,11 +141,10 @@ export class TcpSocketImpl { console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); assert(this.network !== null && this.network.id !== network.id, "already-attached"); - assert(this.#inprogress, "concurrency-conflict"); assert(this.#state === "connected", "already-connected"); assert(this.#state === "connection", "already-listening"); - assert(this.#state === "listening", "already-listening"); - assert(this.isBound === false, "already-bound"); + assert(this.#inProgress, "concurrency-conflict"); + assert(this.#isBound === false, "not-bound"); const host = computeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; @@ -157,7 +156,7 @@ export class TcpSocketImpl { this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; - this.#inprogress = true; + this.#inProgress = true; } /** @@ -213,7 +212,7 @@ export class TcpSocketImpl { this.#state = "error"; }); - this.#inprogress = false; + this.#inProgress = false; } /** @@ -227,7 +226,7 @@ export class TcpSocketImpl { startListen(tcpSocket) { console.log(`[tcp] start listen socket ${tcpSocket.id}`); - this.#inprogress = true; + this.#inProgress = true; throw new Error("not implemented"); } @@ -242,7 +241,7 @@ export class TcpSocketImpl { finishListen(tcpSocket) { console.log(`[tcp] finish listen socket ${tcpSocket.id}`); - this.#inprogress = false; + this.#inProgress = false; throw new Error("not implemented"); } @@ -266,7 +265,7 @@ export class TcpSocketImpl { localAddress(tcpSocket) { console.log(`[tcp] local address socket ${tcpSocket.id}`); - assert(this.isBound === false, "not-bound"); + assert(this.#isBound === false, "not-bound"); return this.#socket.localAddress(); } @@ -279,7 +278,7 @@ export class TcpSocketImpl { remoteAddress(tcpSocket) { console.log(`[tcp] remote address socket ${tcpSocket.id}`); - assert(this.isBound === false, "not-bound"); + assert(this.#isBound === false, "not-bound"); assert(this.#state !== "connected", "not-connected"); return this.#socket.remoteAddress(); diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index c08e3cf9d..ab4819694 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,5 +1,5 @@ import { equal, ok, throws } from "node:assert"; -import { mock } from 'node:test'; +import { mock } from "node:test"; import { fileURLToPath } from "node:url"; suite("Node.js Preview2", () => { @@ -211,7 +211,6 @@ suite("Node.js Preview2", () => { tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); - equal(tcpSocket.isBound, true); equal(tcpSocket.network.id, network.id); equal(tcpSocket.socketAddress.family, "ipv4"); equal(tcpSocket.socketAddress.address, "0.0.0.0"); @@ -233,7 +232,6 @@ suite("Node.js Preview2", () => { tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); - equal(tcpSocket.isBound, true); equal(tcpSocket.network.id, network.id); equal(tcpSocket.socketAddress.family, "ipv6"); equal(tcpSocket.socketAddress.address, "::ffff:192.168.0.1"); @@ -245,9 +243,7 @@ suite("Node.js Preview2", () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { @@ -255,12 +251,15 @@ suite("Node.js Preview2", () => { port: 0, }, }; - throws(() => { - tcpSocket.startBind(tcpSocket, network, localAddress); - }, { - name: 'Error', - message: 'address-family-mismatch' - }) + throws( + () => { + tcpSocket.startBind(tcpSocket, network, localAddress); + }, + { + name: "Error", + message: "address-family-mismatch", + } + ); }); test("tcp.startBind(): should throw already-bound", async () => { @@ -275,14 +274,17 @@ suite("Node.js Preview2", () => { port: 0, }, }; - throws(() => { - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); - tcpSocket.startBind(tcpSocket, network, localAddress); - }, { - name: 'Error', - message: 'already-bound' - }) + throws( + () => { + tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); + tcpSocket.startBind(tcpSocket, network, localAddress); + }, + { + name: "Error", + message: "already-bound", + } + ); }); test("tcp.startConnect(): should connect to a valid ipv4 address", async () => { @@ -304,20 +306,17 @@ suite("Node.js Preview2", () => { }, }; - mock.method(tcpSocket.socket(), 'connect', () => console.log('connect called')); + mock.method(tcpSocket.socket(), "connect", () => console.log("connect called")); tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); tcpSocket.startConnect(tcpSocket, network, remoteAddress); tcpSocket.finishConnect(tcpSocket); - equal(tcpSocket.isBound, true); equal(tcpSocket.network.id, network.id); equal(tcpSocket.socketAddress.family, "ipv4"); equal(tcpSocket.socketAddress.address, "0.0.0.0"); equal(tcpSocket.socketAddress.port, 0); }); }); - - }); From b17122651bced73dbd08932d8d4d185c02cceee8 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 23 Oct 2023 18:47:20 +0200 Subject: [PATCH 16/65] chore: use err.code to catch sockets errors --- .../lib/sockets/tcp-socket-impl.js | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 366cd3ab5..5666c22b8 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -173,6 +173,8 @@ export class TcpSocketImpl { finishConnect(tcpSocket) { console.log(`[tcp] finish connect socket ${tcpSocket.id}`); + assert(this.#inProgress === false, "not-in-progress"); + const { address, port, remoteAddress, remotePort, family } = this.#socketOptions; this.#socket.connect({ localAddress: address, @@ -204,12 +206,26 @@ export class TcpSocketImpl { this.#socket.on("timeout", () => { console.error(`[tcp] timeout on socket ${tcpSocket.id}`); - this.#state = "closed"; + this.#state = "timeout"; }); this.#socket.on("error", (err) => { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); + this.#state = "error"; + + assert(err.code === "ERRADDRINUSE", "ephemeral-ports-exhausted"); + assert(err.code === "EADDRNOTAVAIL", "ephemeral-ports-exhausted"); + assert(err.code === "EAGAIN", "ephemeral-ports-exhausted"); + assert(err.code === "EADDRINUSE", "ephemeral-ports-exhausted"); + assert(err.code === "ECONNREFUSED", "connection-refused"); + assert(err.code === "ECONNRESET", "connection-reset"); + assert(err.code === "ETIMEDOUT", "timeout"); + assert(err.code === "EHOSTUNREACH", "remote-unreachable"); + assert(err.code === "EHOSTDOWN", "remote-unreachable"); + assert(err.code === "ENETUNREACH", "remote-unreachable"); + assert(err.code === "ENETDOWN", "remote-unreachable"); + }); this.#inProgress = false; @@ -228,7 +244,10 @@ export class TcpSocketImpl { this.#inProgress = true; - throw new Error("not implemented"); + assert(this.#isBound === false, "not-bound"); + assert(this.#state === "connected", "already-connected"); + assert(this.#state === "connection", "already-listening"); + assert(this.#inProgress, "concurrency-conflict"); } /** @@ -241,6 +260,10 @@ export class TcpSocketImpl { finishListen(tcpSocket) { console.log(`[tcp] finish listen socket ${tcpSocket.id}`); + assert(this.#inProgress === false, "not-in-progress"); + + this.#socket.listen(this.#backlog); + this.#inProgress = false; throw new Error("not implemented"); From b86f718f5da9362c2f5d1fb08a8225278d68b35a Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 24 Oct 2023 11:20:57 +0200 Subject: [PATCH 17/65] chore: migrate tcp-socket-impl to use tcp_wrap --- .../lib/sockets/tcp-socket-impl.js | 238 +++++++++++------- packages/preview2-shim/test/test.js | 110 ++++++-- 2 files changed, 234 insertions(+), 114 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 5666c22b8..1220d77aa 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,7 +11,14 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ -import { Socket as NodeSocket, SocketAddress as NodeSocketAddress, isIP } from "node:net"; +// See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc +const { + TCP, + TCPConnectWrap, + constants: TCPConstants, +} = process.binding("tcp_wrap"); +import { isIP, Socket as NodeSocket } from "node:net"; +import { EventEmitter } from "node:events"; function tupleToIPv6(arr) { if (arr.length !== 8) { @@ -21,14 +28,39 @@ function tupleToIPv6(arr) { return ipv6Segments.join(":"); } -function computeIpAddress(localAddress, family) { - let { address } = localAddress.val; +function tupleToIpv4(arr) { + if (arr.length !== 4) { + return null; // Return null for invalid input + } + const ipv4Segments = arr.map((segment) => segment.toString(10)); + return ipv4Segments.join("."); +} + +function ipv6ToTuple(ipv6) { + return ipv6.split(":").map((segment) => parseInt(segment, 16)); +} + +function ipv4ToTuple(ipv4) { + return ipv4.split(".").map((segment) => parseInt(segment, 10)); +} + +function serializeIpAddress(addr, family) { + let { address } = addr.val; if (family.toLocaleLowerCase() === "ipv4") { - address = address.join("."); + address = tupleToIpv4(address); } else if (family.toLocaleLowerCase() === "ipv6") { address = tupleToIPv6(address); } + return address; +} +function deserializeIpAddress(addr, family) { + let address = []; + if (family.toLocaleLowerCase() === "ipv4") { + address = ipv4ToTuple(addr); + } else if (family.toLocaleLowerCase() === "ipv6") { + address = ipv6ToTuple(addr); + } return address; } @@ -38,11 +70,10 @@ function assert(condition, message) { } } -export class TcpSocketImpl { +export class TcpSocketImpl extends EventEmitter { id; - /** @type {NodeSocket} */ #socket = null; + /** @type {TCP} */ #handle = null; /** @type {Network} */ network = null; - /** @type {NodeSocketAddress} */ socketAddress = null; #isBound = false; #socketOptions = {}; @@ -56,8 +87,17 @@ export class TcpSocketImpl { #backlog = 128; constructor(socketId, addressFamily) { + super(); this.id = socketId; - this.#socket = new NodeSocket(); + this.#handle = new TCP(TCPConstants.SERVER); + this.#handle.onconnection = (err, clientHandle) => { + console.log(`[tcp] connection on socket ${tcpSocket.id}`); + + if (err) { + console.error('Error accepting connection:', err); + return; + } + }; this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; @@ -65,7 +105,7 @@ export class TcpSocketImpl { } socket() { - return this.#socket; + return this.#handle; } /** @@ -83,16 +123,20 @@ export class TcpSocketImpl { assert(this.#isBound, "already-bound"); assert(this.#inProgress, "concurrency-conflict"); - const address = computeIpAddress(localAddress, this.#socketOptions.family); - + const address = serializeIpAddress( + localAddress, + this.#socketOptions.family + ); const ipFamily = `ipv${isIP(address)}`; - if (this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase()) { - throw new Error("address-family-mismatch"); - } + assert( + this.#socketOptions.family.toLocaleLowerCase() !== + ipFamily.toLocaleLowerCase(), + "address-family-mismatch" + ); const { port } = localAddress.val; - this.#socketOptions.address = address; - this.#socketOptions.port = port; + this.#socketOptions.localAddress = address; + this.#socketOptions.localPort = port; this.network = network; this.#inProgress = true; } @@ -111,14 +155,14 @@ export class TcpSocketImpl { assert(this.#inProgress === false, "not-in-progress"); - const { address, port, family } = this.#socketOptions; - assert(isIP(address) === 0, "address-not-bindable"); + const { localAddress, localPort, family } = this.#socketOptions; + assert(isIP(localAddress) === 0, "address-not-bindable"); - this.socketAddress = new NodeSocketAddress({ - address, - port, - family, - }); + const err = this.#handle.bind(localAddress, family, localPort); + if (err) { + console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); + this.#state = "error"; + } this.#isBound = true; this.#inProgress = false; @@ -138,19 +182,28 @@ export class TcpSocketImpl { * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ startConnect(tcpSocket, network, remoteAddress) { - console.log(`[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress} on network ${network.id}`); - - assert(this.network !== null && this.network.id !== network.id, "already-attached"); + console.log( + `[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress.val.address} on network ${network.id}` + ); + + assert( + this.network !== null && this.network.id !== network.id, + "already-attached" + ); assert(this.#state === "connected", "already-connected"); assert(this.#state === "connection", "already-listening"); assert(this.#inProgress, "concurrency-conflict"); assert(this.#isBound === false, "not-bound"); - const host = computeIpAddress(remoteAddress, this.#socketOptions.family); + const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-remote-address"); - assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "address-family-mismatch"); + assert( + this.#socketOptions.family.toLocaleLowerCase() !== + ipFamily.toLocaleLowerCase(), + "address-family-mismatch" + ); this.network = network; this.#socketOptions.remoteAddress = host; @@ -175,58 +228,49 @@ export class TcpSocketImpl { assert(this.#inProgress === false, "not-in-progress"); - const { address, port, remoteAddress, remotePort, family } = this.#socketOptions; - this.#socket.connect({ - localAddress: address, - localPort: port, - host: remoteAddress, - port: remotePort, - family: family, - }); + const { remoteAddress, remotePort } = this.#socketOptions; + const clientHandle = new TCP(TCPConstants.SOCKET); + const connectReq = new TCPConnectWrap(); + const err = clientHandle.connect(connectReq, remoteAddress, remotePort); - this.#socket.on("connect", () => { - console.log(`[tcp] connect on socket ${tcpSocket.id}`); - this.#state = "connected"; - }); - - this.#socket.on("ready", () => { - console.log(`[tcp] ready on socket ${tcpSocket.id}`); - this.#state = "connection"; - }); - - this.#socket.on("close", () => { - console.log(`[tcp] close on socket ${tcpSocket.id}`); - this.#state = "closed"; - }); - - this.#socket.on("end", () => { - console.log(`[tcp] end on socket ${tcpSocket.id}`); - this.#state = "closed"; - }); - - this.#socket.on("timeout", () => { - console.error(`[tcp] timeout on socket ${tcpSocket.id}`); - this.#state = "timeout"; - }); - - this.#socket.on("error", (err) => { + if (err) { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); - this.#state = "error"; + } + + connectReq.oncomplete = (err) => { + if (err) { + assert(err === -111, "connection-refused"); + assert(err === -104, "connection-reset"); + assert(err === -110, "timeout"); + assert(err === -113, "remote-unreachable"); + assert(err === -99, "ephemeral-ports-exhausted"); - assert(err.code === "ERRADDRINUSE", "ephemeral-ports-exhausted"); - assert(err.code === "EADDRNOTAVAIL", "ephemeral-ports-exhausted"); - assert(err.code === "EAGAIN", "ephemeral-ports-exhausted"); - assert(err.code === "EADDRINUSE", "ephemeral-ports-exhausted"); - assert(err.code === "ECONNREFUSED", "connection-refused"); - assert(err.code === "ECONNRESET", "connection-reset"); - assert(err.code === "ETIMEDOUT", "timeout"); - assert(err.code === "EHOSTUNREACH", "remote-unreachable"); - assert(err.code === "EHOSTDOWN", "remote-unreachable"); - assert(err.code === "ENETUNREACH", "remote-unreachable"); - assert(err.code === "ENETDOWN", "remote-unreachable"); + console.error({err}); + return; + } - }); + console.log(`[tcp] connect on socket ${tcpSocket.id}`); + this.#state = "connected"; + }; + + // this.#handle.on("error", (err) => { + // console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); + + // this.#state = "error"; + + // assert(err.code === "ERRADDRINUSE", "ephemeral-ports-exhausted"); + // assert(err.code === "EADDRNOTAVAIL", "ephemeral-ports-exhausted"); + // assert(err.code === "EAGAIN", "ephemeral-ports-exhausted"); + // assert(err.code === "EADDRINUSE", "ephemeral-ports-exhausted"); + // assert(err.code === "ECONNREFUSED", "connection-refused"); + // assert(err.code === "ECONNRESET", "connection-reset"); + // assert(err.code === "ETIMEDOUT", "timeout"); + // assert(err.code === "EHOSTUNREACH", "remote-unreachable"); + // assert(err.code === "EHOSTDOWN", "remote-unreachable"); + // assert(err.code === "ENETUNREACH", "remote-unreachable"); + // assert(err.code === "ENETDOWN", "remote-unreachable"); + // }); this.#inProgress = false; } @@ -242,12 +286,12 @@ export class TcpSocketImpl { startListen(tcpSocket) { console.log(`[tcp] start listen socket ${tcpSocket.id}`); - this.#inProgress = true; - assert(this.#isBound === false, "not-bound"); assert(this.#state === "connected", "already-connected"); assert(this.#state === "connection", "already-listening"); assert(this.#inProgress, "concurrency-conflict"); + + this.#inProgress = true; } /** @@ -262,11 +306,14 @@ export class TcpSocketImpl { assert(this.#inProgress === false, "not-in-progress"); - this.#socket.listen(this.#backlog); + const err = this.#handle.listen(this.#backlog); - this.#inProgress = false; + if (err) { + this.#handle.close(); + throw new Error(err); + } - throw new Error("not implemented"); + this.#inProgress = false; } /** @@ -290,7 +337,14 @@ export class TcpSocketImpl { assert(this.#isBound === false, "not-bound"); - return this.#socket.localAddress(); + const { localAddress, localPort, family } = this.#socketOptions; + return { + tag: family, + val: { + address: deserializeIpAddress(localAddress, family), + port: localPort, + }, + }; } /** @@ -304,7 +358,7 @@ export class TcpSocketImpl { assert(this.#isBound === false, "not-bound"); assert(this.#state !== "connected", "not-connected"); - return this.#socket.remoteAddress(); + return this.#socketOptions.remoteAddress; } /** @@ -314,7 +368,7 @@ export class TcpSocketImpl { addressFamily(tcpSocket) { console.log(`[tcp] address family socket ${tcpSocket.id}`); - return this.#socket.localFamily; + return this.#socketOptions.family; } /** @@ -352,7 +406,9 @@ export class TcpSocketImpl { * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setListenBacklogSize(tcpSocket, value) { - console.log(`[tcp] set listen backlog size socket ${tcpSocket.id} to ${value}`); + console.log( + `[tcp] set listen backlog size socket ${tcpSocket.id} to ${value}` + ); this.#backlog = value; } @@ -377,7 +433,7 @@ export class TcpSocketImpl { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); this.#socketOptions.keepAlive = value; - this.#socket.setKeepAlive(value); + this.#handle.setKeepAlive(value); } /** @@ -400,7 +456,7 @@ export class TcpSocketImpl { console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); this.#socketOptions.noDelay = value; - this.#socket.setNoDelay(value); + this.#handle.setNoDelay(value); } /** @@ -423,7 +479,9 @@ export class TcpSocketImpl { * */ setUnicastHopLimit(tcpSocket, value) { - console.log(`[tcp] set unicast hop limit socket ${tcpSocket.id} to ${value}`); + console.log( + `[tcp] set unicast hop limit socket ${tcpSocket.id} to ${value}` + ); throw new Error("not implemented"); } @@ -467,7 +525,9 @@ export class TcpSocketImpl { * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ setSendBufferSize(tcpSocket, value) { - console.log(`[tcp] set send buffer size socket ${tcpSocket.id} to ${value}`); + console.log( + `[tcp] set send buffer size socket ${tcpSocket.id} to ${value}` + ); throw new Error("not implemented"); } @@ -508,6 +568,6 @@ export class TcpSocketImpl { dropTcpSocket(tcpSocket) { console.log(`[tcp] drop socket ${tcpSocket.id}`); - this.#socket.destroy(); + this.#handle.destroy(); } } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index ab4819694..70b7ae8cd 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,4 +1,4 @@ -import { equal, ok, throws } from "node:assert"; +import { deepEqual, equal, ok, throws, strictEqual } from "node:assert"; import { mock } from "node:test"; import { fileURLToPath } from "node:url"; @@ -184,7 +184,9 @@ suite("Node.js Preview2", () => { test("sockets.tcpCreateSocket()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - const { id } = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const { id } = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); equal(id, 1); throws( @@ -197,10 +199,12 @@ suite("Node.js Preview2", () => { } ); }); - test("tcp.startBind(): should bind to a valid ipv4 address", async () => { + test("tcp.bind(): should bind to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -212,16 +216,22 @@ suite("Node.js Preview2", () => { tcpSocket.finishBind(tcpSocket); equal(tcpSocket.network.id, network.id); - equal(tcpSocket.socketAddress.family, "ipv4"); - equal(tcpSocket.socketAddress.address, "0.0.0.0"); - equal(tcpSocket.socketAddress.port, 0); - equal(tcpSocket.socketAddress.flowlabel, 0); + deepEqual(tcpSocket.localAddress(tcpSocket), { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }); + equal(tcpSocket.addressFamily(tcpSocket), "ipv4"); }); - test("tcp.startBind(): should bind to a valid ipv6 address", async () => { + test("tcp.bind(): should bind to a valid ipv6 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv6); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv6 + ); const localAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { @@ -233,17 +243,23 @@ suite("Node.js Preview2", () => { tcpSocket.finishBind(tcpSocket); equal(tcpSocket.network.id, network.id); - equal(tcpSocket.socketAddress.family, "ipv6"); - equal(tcpSocket.socketAddress.address, "::ffff:192.168.0.1"); - equal(tcpSocket.socketAddress.port, 0); - equal(tcpSocket.socketAddress.flowlabel, 0); + equal(tcpSocket.addressFamily(tcpSocket), "ipv6"); + deepEqual(tcpSocket.localAddress(tcpSocket), { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], + port: 0, + }, + }); }); - test("tcp.startBind(): should throw address-family-mismatch", async () => { + test("tcp.bind(): should throw address-family-mismatch", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); const localAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { @@ -262,11 +278,13 @@ suite("Node.js Preview2", () => { ); }); - test("tcp.startBind(): should throw already-bound", async () => { + test("tcp.bind(): should throw already-bound", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -287,10 +305,12 @@ suite("Node.js Preview2", () => { ); }); - test("tcp.startConnect(): should connect to a valid ipv4 address", async () => { + test("tcp.connect(): should connect to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -306,17 +326,57 @@ suite("Node.js Preview2", () => { }, }; - mock.method(tcpSocket.socket(), "connect", () => console.log("connect called")); - tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); tcpSocket.startConnect(tcpSocket, network, remoteAddress); tcpSocket.finishConnect(tcpSocket); equal(tcpSocket.network.id, network.id); - equal(tcpSocket.socketAddress.family, "ipv4"); - equal(tcpSocket.socketAddress.address, "0.0.0.0"); - equal(tcpSocket.socketAddress.port, 0); + equal(tcpSocket.addressFamily(tcpSocket), "ipv4"); + deepEqual(tcpSocket.localAddress(tcpSocket), { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }); + }); + + test("tcp.listen(): should listen to an ipv4 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + const remoteAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + + mock.method(tcpSocket.socket(), "listen", () => { + console.log("listen called"); + }); + + tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.finishBind(tcpSocket); + tcpSocket.startConnect(tcpSocket, network, remoteAddress); + tcpSocket.finishConnect(tcpSocket); + tcpSocket.startListen(tcpSocket); + tcpSocket.finishListen(tcpSocket); + + strictEqual(tcpSocket.socket().listen.mock.calls.length, 1); + + mock.reset(); }); }); }); From e112b105e765753b878c6897741a5c5b1b4073f7 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 24 Oct 2023 11:56:27 +0200 Subject: [PATCH 18/65] chore: mock listen and connect calls --- .../lib/sockets/tcp-socket-impl.js | 77 ++++++++----------- packages/preview2-shim/test/test.js | 10 ++- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 1220d77aa..e3248e63a 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -70,9 +70,11 @@ function assert(condition, message) { } } +// TODO: implement would-block errors export class TcpSocketImpl extends EventEmitter { id; - /** @type {TCP} */ #handle = null; + /** @type {TCP.TCPConstants.SERVER} */ #serverHandle = null; + /** @type {TCP.TCPConstants.SOCKET} */ #clientHandle = null; /** @type {Network} */ network = null; #isBound = false; @@ -89,23 +91,28 @@ export class TcpSocketImpl extends EventEmitter { constructor(socketId, addressFamily) { super(); this.id = socketId; - this.#handle = new TCP(TCPConstants.SERVER); - this.#handle.onconnection = (err, clientHandle) => { - console.log(`[tcp] connection on socket ${tcpSocket.id}`); - - if (err) { - console.error('Error accepting connection:', err); - return; - } - }; - + this.#clientHandle = new TCP(TCPConstants.SOCKET); + this.#serverHandle = new TCP(TCPConstants.SERVER); + this.#serverHandle.onconnection = this.#onServerConnection.bind(this); this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; this.#socketOptions.noDelay = false; } - socket() { - return this.#handle; + server() { + return this.#serverHandle; + } + client() { + return this.#clientHandle; + } + + #onServerConnection(err, clientHandle) { + console.log(`[tcp] connection on socket ${tcpSocket.id}`); + + if (err) { + console.error("Error accepting connection:", err); + return; + } } /** @@ -158,7 +165,7 @@ export class TcpSocketImpl extends EventEmitter { const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); - const err = this.#handle.bind(localAddress, family, localPort); + const err = this.#serverHandle.bind(localAddress, family, localPort); if (err) { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); this.#state = "error"; @@ -229,9 +236,12 @@ export class TcpSocketImpl extends EventEmitter { assert(this.#inProgress === false, "not-in-progress"); const { remoteAddress, remotePort } = this.#socketOptions; - const clientHandle = new TCP(TCPConstants.SOCKET); const connectReq = new TCPConnectWrap(); - const err = clientHandle.connect(connectReq, remoteAddress, remotePort); + const err = this.#clientHandle.connect( + connectReq, + remoteAddress, + remotePort + ); if (err) { console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); @@ -240,38 +250,19 @@ export class TcpSocketImpl extends EventEmitter { connectReq.oncomplete = (err) => { if (err) { - assert(err === -111, "connection-refused"); + assert(err === -99, "ephemeral-ports-exhausted"); assert(err === -104, "connection-reset"); assert(err === -110, "timeout"); + assert(err === -111, "connection-refused"); assert(err === -113, "remote-unreachable"); - assert(err === -99, "ephemeral-ports-exhausted"); - console.error({err}); - return; + throw new Error(err); } console.log(`[tcp] connect on socket ${tcpSocket.id}`); this.#state = "connected"; }; - // this.#handle.on("error", (err) => { - // console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); - - // this.#state = "error"; - - // assert(err.code === "ERRADDRINUSE", "ephemeral-ports-exhausted"); - // assert(err.code === "EADDRNOTAVAIL", "ephemeral-ports-exhausted"); - // assert(err.code === "EAGAIN", "ephemeral-ports-exhausted"); - // assert(err.code === "EADDRINUSE", "ephemeral-ports-exhausted"); - // assert(err.code === "ECONNREFUSED", "connection-refused"); - // assert(err.code === "ECONNRESET", "connection-reset"); - // assert(err.code === "ETIMEDOUT", "timeout"); - // assert(err.code === "EHOSTUNREACH", "remote-unreachable"); - // assert(err.code === "EHOSTDOWN", "remote-unreachable"); - // assert(err.code === "ENETUNREACH", "remote-unreachable"); - // assert(err.code === "ENETDOWN", "remote-unreachable"); - // }); - this.#inProgress = false; } @@ -306,10 +297,10 @@ export class TcpSocketImpl extends EventEmitter { assert(this.#inProgress === false, "not-in-progress"); - const err = this.#handle.listen(this.#backlog); + const err = this.#serverHandle.listen(this.#backlog); if (err) { - this.#handle.close(); + this.#serverHandle.close(); throw new Error(err); } @@ -433,7 +424,7 @@ export class TcpSocketImpl extends EventEmitter { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); this.#socketOptions.keepAlive = value; - this.#handle.setKeepAlive(value); + this.#serverHandle.setKeepAlive(value); } /** @@ -456,7 +447,7 @@ export class TcpSocketImpl extends EventEmitter { console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); this.#socketOptions.noDelay = value; - this.#handle.setNoDelay(value); + this.#serverHandle.setNoDelay(value); } /** @@ -568,6 +559,6 @@ export class TcpSocketImpl extends EventEmitter { dropTcpSocket(tcpSocket) { console.log(`[tcp] drop socket ${tcpSocket.id}`); - this.#handle.destroy(); + this.#serverHandle.destroy(); } } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 70b7ae8cd..9f389d274 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -161,7 +161,7 @@ suite("Node.js Preview2", () => { }); suite("Sockets", async () => { - test("sockets.instanceNetwork()", async () => { + test("sockets.instanceNetwork() should be a singleton", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network1 = sockets.instanceNetwork.instanceNetwork(); equal(network1.id, 1); @@ -363,9 +363,12 @@ suite("Node.js Preview2", () => { }, }; - mock.method(tcpSocket.socket(), "listen", () => { + mock.method(tcpSocket.server(), "listen", () => { console.log("listen called"); }); + mock.method(tcpSocket.client(), "connect", () => { + console.log("connect called"); + }); tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); @@ -374,7 +377,8 @@ suite("Node.js Preview2", () => { tcpSocket.startListen(tcpSocket); tcpSocket.finishListen(tcpSocket); - strictEqual(tcpSocket.socket().listen.mock.calls.length, 1); + strictEqual(tcpSocket.server().listen.mock.calls.length, 1); + strictEqual(tcpSocket.client().connect.mock.calls.length, 1); mock.reset(); }); From 5a844a0970eaf774ea5a0418ad707634a6bbd044 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 24 Oct 2023 12:13:39 +0200 Subject: [PATCH 19/65] fix: bind connectReq.oncomplete to class method --- .../lib/sockets/tcp-socket-impl.js | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index e3248e63a..272315681 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -93,7 +93,7 @@ export class TcpSocketImpl extends EventEmitter { this.id = socketId; this.#clientHandle = new TCP(TCPConstants.SOCKET); this.#serverHandle = new TCP(TCPConstants.SERVER); - this.#serverHandle.onconnection = this.#onServerConnection.bind(this); + this.#serverHandle.onconnection = this.onServerConnection.bind(this); this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; this.#socketOptions.noDelay = false; @@ -106,13 +106,31 @@ export class TcpSocketImpl extends EventEmitter { return this.#clientHandle; } - #onServerConnection(err, clientHandle) { - console.log(`[tcp] connection on socket ${tcpSocket.id}`); + onServerConnection(err, clientHandle) { + console.log(`[tcp] on server connection`); if (err) { - console.error("Error accepting connection:", err); - return; + throw new Error(err); + } + + const socket = new NodeSocket({ handle: clientHandle }); + // TODO: handle data received from the client + } + + onClientConnectComplete(err) { + console.log(`[tcp] on client connect complete`); + + if (err) { + assert(err === -99, "ephemeral-ports-exhausted"); + assert(err === -104, "connection-reset"); + assert(err === -110, "timeout"); + assert(err === -111, "connection-refused"); + assert(err === -113, "remote-unreachable"); + + throw new Error(err); } + + this.#state = "connected"; } /** @@ -248,21 +266,7 @@ export class TcpSocketImpl extends EventEmitter { this.#state = "error"; } - connectReq.oncomplete = (err) => { - if (err) { - assert(err === -99, "ephemeral-ports-exhausted"); - assert(err === -104, "connection-reset"); - assert(err === -110, "timeout"); - assert(err === -111, "connection-refused"); - assert(err === -113, "remote-unreachable"); - - throw new Error(err); - } - - console.log(`[tcp] connect on socket ${tcpSocket.id}`); - this.#state = "connected"; - }; - + connectReq.oncomplete = this.onClientConnectComplete.bind(this); this.#inProgress = false; } From e9e87bc6dc04746e334ae29ec7fafa13e35e0d84 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 25 Oct 2023 14:43:18 +0200 Subject: [PATCH 20/65] chore: reorder test suites --- packages/preview2-shim/test/test.js | 56 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 9f389d274..f7575473e 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -305,7 +305,7 @@ suite("Node.js Preview2", () => { ); }); - test("tcp.connect(): should connect to a valid ipv4 address", async () => { + test("tcp.listen(): should listen to an ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( @@ -318,36 +318,30 @@ suite("Node.js Preview2", () => { port: 0, }, }; - const remoteAddress = { - tag: sockets.network.IpAddressFamily.ipv4, - val: { - address: [192, 168, 0, 1], - port: 80, - }, - }; + + mock.method(tcpSocket.server(), "listen", () => { + console.log("mock listen called"); + }); tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); - tcpSocket.startConnect(tcpSocket, network, remoteAddress); - tcpSocket.finishConnect(tcpSocket); + tcpSocket.startListen(tcpSocket); + tcpSocket.finishListen(tcpSocket); - equal(tcpSocket.network.id, network.id); - equal(tcpSocket.addressFamily(tcpSocket), "ipv4"); - deepEqual(tcpSocket.localAddress(tcpSocket), { - tag: sockets.network.IpAddressFamily.ipv4, - val: { - address: [0, 0, 0, 0], - port: 0, - }, - }); + strictEqual(tcpSocket.server().listen.mock.calls.length, 1); + + tcpSocket.dropTcpSocket(tcpSocket); + + mock.reset(); }); - test("tcp.listen(): should listen to an ipv4 address", async () => { + test("tcp.connect(): should connect to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( sockets.network.IpAddressFamily.ipv4 ); + const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -358,29 +352,31 @@ suite("Node.js Preview2", () => { const remoteAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { - address: [0, 0, 0, 0], - port: 0, + address: [192, 168, 0, 1], + port: 80, }, }; - mock.method(tcpSocket.server(), "listen", () => { - console.log("listen called"); - }); mock.method(tcpSocket.client(), "connect", () => { - console.log("connect called"); + console.log("mock connect called"); }); tcpSocket.startBind(tcpSocket, network, localAddress); tcpSocket.finishBind(tcpSocket); tcpSocket.startConnect(tcpSocket, network, remoteAddress); tcpSocket.finishConnect(tcpSocket); - tcpSocket.startListen(tcpSocket); - tcpSocket.finishListen(tcpSocket); - strictEqual(tcpSocket.server().listen.mock.calls.length, 1); strictEqual(tcpSocket.client().connect.mock.calls.length, 1); - mock.reset(); + equal(tcpSocket.network.id, network.id); + equal(tcpSocket.addressFamily(tcpSocket), "ipv4"); + deepEqual(tcpSocket.localAddress(tcpSocket), { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }); }); }); }); From e241b8a2a0378f366371834a0af44cf13634e1eb Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 25 Oct 2023 14:43:54 +0200 Subject: [PATCH 21/65] chore: add e2e test for tcp-socket-impl --- test/preview2-wasi-sockets.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/preview2-wasi-sockets.js diff --git a/test/preview2-wasi-sockets.js b/test/preview2-wasi-sockets.js new file mode 100644 index 000000000..9ccfdbdba --- /dev/null +++ b/test/preview2-wasi-sockets.js @@ -0,0 +1,33 @@ +const { sockets } = await import("@bytecodealliance/preview2-shim"); +const network = sockets.instanceNetwork.instanceNetwork(); + +// server +const serverAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [127, 0, 0, 1], + port: 3000, + }, +}; +const server = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 +); +server.startBind(server, network, serverAddress); +server.finishBind(server); +server.startListen(server); +server.finishListen(server); +const {address, port} = server.localAddress(server).val; +console.log(`[wasi-sockets] Server listening on: ${address}:${port}`); + +// client + +const client = sockets.tcpCreateSocket.createTcpSocket( + sockets.network.IpAddressFamily.ipv4 +); + +client.startConnect(client, network, serverAddress); +client.finishConnect(client); +setTimeout(() => { + client.dropTcpSocket(client); + server.dropTcpSocket(server); +}, 2000); From 8e081028b59c5671d9dd2f14d831038312eaceb4 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 25 Oct 2023 15:32:50 +0200 Subject: [PATCH 22/65] chore: hook up internal events --- .../lib/sockets/tcp-socket-impl.js | 114 ++++++++++++++---- 1 file changed, 89 insertions(+), 25 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 272315681..f2d8d1a15 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -17,23 +17,22 @@ const { TCPConnectWrap, constants: TCPConstants, } = process.binding("tcp_wrap"); +const { ShutdownWrap } = process.binding("stream_wrap"); import { isIP, Socket as NodeSocket } from "node:net"; import { EventEmitter } from "node:events"; function tupleToIPv6(arr) { if (arr.length !== 8) { - return null; // Return null for invalid input + return null; } - const ipv6Segments = arr.map((segment) => segment.toString(16)); - return ipv6Segments.join(":"); + return arr.map((segment) => segment.toString(16)).join(":"); } function tupleToIpv4(arr) { if (arr.length !== 4) { - return null; // Return null for invalid input + return null; } - const ipv4Segments = arr.map((segment) => segment.toString(10)); - return ipv4Segments.join("."); + return arr.map((segment) => segment.toString(10)).join("."); } function ipv6ToTuple(ipv6) { @@ -70,13 +69,14 @@ function assert(condition, message) { } } -// TODO: implement would-block errors +// TODO: implement would-block exceptions +// TODO: implement concurrency-conflict exceptions export class TcpSocketImpl extends EventEmitter { - id; /** @type {TCP.TCPConstants.SERVER} */ #serverHandle = null; /** @type {TCP.TCPConstants.SOCKET} */ #clientHandle = null; /** @type {Network} */ network = null; + id = 0; #isBound = false; #socketOptions = {}; #canReceive = true; @@ -84,6 +84,7 @@ export class TcpSocketImpl extends EventEmitter { #ipv6Only = false; #state = "closed"; #inProgress = false; + #connections = 0; // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; @@ -91,12 +92,14 @@ export class TcpSocketImpl extends EventEmitter { constructor(socketId, addressFamily) { super(); this.id = socketId; - this.#clientHandle = new TCP(TCPConstants.SOCKET); - this.#serverHandle = new TCP(TCPConstants.SERVER); - this.#serverHandle.onconnection = this.onServerConnection.bind(this); this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; this.#socketOptions.noDelay = false; + + this.#clientHandle = new TCP(TCPConstants.SOCKET); + this.#serverHandle = new TCP(TCPConstants.SERVER); + this._handle = this.#serverHandle; + this._handle.onconnection = this.#handleConnection.bind(this); } server() { @@ -106,7 +109,7 @@ export class TcpSocketImpl extends EventEmitter { return this.#clientHandle; } - onServerConnection(err, clientHandle) { + #handleConnection(err, clientHandle) { console.log(`[tcp] on server connection`); if (err) { @@ -114,7 +117,22 @@ export class TcpSocketImpl extends EventEmitter { } const socket = new NodeSocket({ handle: clientHandle }); - // TODO: handle data received from the client + this.#connections++; + + // reserved + socket.server = this.#serverHandle; + socket._server = this.#serverHandle; + + this.emit("connection", socket); + + socket._handle.onread = (nread, buffer) => { + if (nread > 0) { + // TODO: handle data received from the client + const data = buffer.toString("utf8", 0, nread); + console.log("Received data:", data); + } + }; + socket._handle.readStart(); } onClientConnectComplete(err) { @@ -126,6 +144,7 @@ export class TcpSocketImpl extends EventEmitter { assert(err === -110, "timeout"); assert(err === -111, "connection-refused"); assert(err === -113, "remote-unreachable"); + assert(err === -125, "operation-cancelled"); throw new Error(err); } @@ -143,7 +162,9 @@ export class TcpSocketImpl extends EventEmitter { * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) **/ startBind(tcpSocket, network, localAddress) { - console.log(`[tcp] start bind socket ${tcpSocket.id}`); + console.log( + `[tcp] start bind socket ${tcpSocket.id} to ${localAddress.val.address}:${localAddress.val.port}` + ); assert(this.#isBound, "already-bound"); assert(this.#inProgress, "concurrency-conflict"); @@ -183,12 +204,17 @@ export class TcpSocketImpl extends EventEmitter { const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); - const err = this.#serverHandle.bind(localAddress, family, localPort); + const err = this.#serverHandle.bind(localAddress, localPort, family); if (err) { + this.#serverHandle.close(); console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); this.#state = "error"; } + console.log( + `[tcp] bound socket ${tcpSocket.id} to ${localAddress}:${localPort}` + ); + this.#isBound = true; this.#inProgress = false; } @@ -208,7 +234,7 @@ export class TcpSocketImpl extends EventEmitter { * */ startConnect(tcpSocket, network, remoteAddress) { console.log( - `[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress.val.address} on network ${network.id}` + `[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress.val.address}:${remoteAddress.val.port}` ); assert( @@ -218,7 +244,6 @@ export class TcpSocketImpl extends EventEmitter { assert(this.#state === "connected", "already-connected"); assert(this.#state === "connection", "already-listening"); assert(this.#inProgress, "concurrency-conflict"); - assert(this.#isBound === false, "not-bound"); const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; @@ -253,7 +278,8 @@ export class TcpSocketImpl extends EventEmitter { assert(this.#inProgress === false, "not-in-progress"); - const { remoteAddress, remotePort } = this.#socketOptions; + const { localAddress, localPort, remoteAddress, remotePort } = + this.#socketOptions; const connectReq = new TCPConnectWrap(); const err = this.#clientHandle.connect( connectReq, @@ -267,7 +293,23 @@ export class TcpSocketImpl extends EventEmitter { } connectReq.oncomplete = this.onClientConnectComplete.bind(this); + connectReq.address = remoteAddress; + connectReq.port = remotePort; + connectReq.localAddress = localAddress; + connectReq.localPort = localPort; + + this.#clientHandle.onread = (buffer) => { + // TODO: handle data received from the server + + console.log({ + buffer, + }); + }; + this.#clientHandle.readStart(); this.#inProgress = false; + + // TODO: return InputStream and OutputStream + return []; } /** @@ -279,7 +321,11 @@ export class TcpSocketImpl extends EventEmitter { * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) * */ startListen(tcpSocket) { - console.log(`[tcp] start listen socket ${tcpSocket.id}`); + console.log( + `[tcp] start listen socket ${tcpSocket.id} on ${ + this.#socketOptions.localAddress + }:${this.#socketOptions.localPort}` + ); assert(this.#isBound === false, "not-bound"); assert(this.#state === "connected", "already-connected"); @@ -297,13 +343,15 @@ export class TcpSocketImpl extends EventEmitter { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) * */ finishListen(tcpSocket) { - console.log(`[tcp] finish listen socket ${tcpSocket.id}`); + console.log( + `[tcp] finish listen socket ${tcpSocket.id} (backlog: ${this.#backlog})` + ); assert(this.#inProgress === false, "not-in-progress"); const err = this.#serverHandle.listen(this.#backlog); - if (err) { + console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); this.#serverHandle.close(); throw new Error(err); } @@ -319,7 +367,8 @@ export class TcpSocketImpl extends EventEmitter { * */ accept(tcpSocket) { console.log(`[tcp] accept socket ${tcpSocket.id}`); - throw new Error("not implemented"); + + assert(this.#state !== "listening", "not-listening"); } /** @@ -428,7 +477,7 @@ export class TcpSocketImpl extends EventEmitter { console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); this.#socketOptions.keepAlive = value; - this.#serverHandle.setKeepAlive(value); + this.#clientHandle.setKeepAlive(value); } /** @@ -531,7 +580,7 @@ export class TcpSocketImpl extends EventEmitter { * @returns {Pollable} * */ subscribe(tcpSocket) { - console.log(`[tcp] subscribe socket ${this.id}`); + console.log(`[tcp] subscribe socket ${tcpSocket.id}`); throw new Error("not implemented"); } @@ -554,6 +603,18 @@ export class TcpSocketImpl extends EventEmitter { this.#canReceive = false; this.#canSend = false; } + + const req = new ShutdownWrap(); + req.oncomplete = this.#afterShutdown.bind(this); + req.handle = this._handle; + req.callback = () => {}; + const err = this._handle.shutdown(req); + + assert(err === 1, "not-connected"); + } + + #afterShutdown() { + console.log(`[tcp] after shutdown socket ${this.id}`); } /** @@ -563,6 +624,9 @@ export class TcpSocketImpl extends EventEmitter { dropTcpSocket(tcpSocket) { console.log(`[tcp] drop socket ${tcpSocket.id}`); - this.#serverHandle.destroy(); + this._handle.close(); + this._handle = null; + this.#serverHandle.close(); + this.#clientHandle.close(); } } From 991a336ac983fd6269f9bb3fafd3580930f45491 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 25 Oct 2023 18:44:54 +0200 Subject: [PATCH 23/65] chore: update impl to match latest wit specs --- packages/preview2-shim/lib/nodejs/sockets.js | 83 +---- .../lib/sockets/tcp-socket-impl.js | 332 ++++++++---------- .../preview2-shim/lib/sockets/wasi-sockets.js | 106 ++++-- packages/preview2-shim/test/test.js | 79 ++--- test/preview2-wasi-sockets.js | 20 +- 5 files changed, 268 insertions(+), 352 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 1ec11397b..e27b60197 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -3,85 +3,4 @@ import { WasiSockets } from "../sockets/wasi-sockets.js"; const sockets = new WasiSockets(); -export const { instanceNetwork, network, tcpCreateSocket } = sockets; - -// export const ipNameLookup = { -// dropResolveAddressStream() {}, -// subscribe() {}, -// resolveAddresses() {}, -// resolveNextAddress() {}, -// nonBlocking() {}, -// setNonBlocking() {}, -// }; - -// export const tcp = { -// subscribe() {}, -// dropTcpSocket() {}, -// bind() {}, -// connect() {}, -// listen() {}, -// accept() {}, -// localAddress() {}, -// remoteAddress() {}, -// addressFamily() {}, -// ipv6Only() {}, -// setIpv6Only() {}, -// setListenBacklogSize() {}, -// keepAlive() {}, -// setKeepAlive() {}, -// noDelay() {}, -// setNoDelay() {}, -// unicastHopLimit() {}, -// setUnicastHopLimit() {}, -// receiveBufferSize() {}, -// setReceiveBufferSize() {}, -// sendBufferSize() {}, -// setSendBufferSize() {}, -// nonBlocking() {}, -// setNonBlocking() {}, -// shutdown() {}, -// }; - -// export const udp = { -// subscribe() {}, - -// dropUdpSocket() {}, - -// bind() {}, - -// connect() {}, - -// receive() {}, - -// send() {}, - -// localAddress() {}, - -// remoteAddress() {}, - -// addressFamily() {}, - -// ipv6Only() {}, - -// setIpv6Only() {}, - -// unicastHopLimit() {}, - -// setUnicastHopLimit() {}, - -// receiveBufferSize() {}, - -// setReceiveBufferSize() {}, - -// sendBufferSize() {}, - -// setSendBufferSize() {}, - -// nonBlocking() {}, - -// setNonBlocking() {}, -// }; - -// export const udpCreateSocket = { -// createTcpSocket() {}, -// }; +export const { instanceNetwork, network, tcpCreateSocket } = sockets; \ No newline at end of file diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index f2d8d1a15..89f043854 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -7,7 +7,7 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").InputStream} InputStream * @typedef {import("../../types/interfaces/wasi-sockets-tcp").OutputStream} OutputStream * @typedef {import("../../types/interfaces/wasi-sockets-tcp").IpAddressFamily} IpAddressFamily - * @typedef {import("../../types/interfaces/wasi-poll-poll").Pollable} Pollable + * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ @@ -21,6 +21,18 @@ const { ShutdownWrap } = process.binding("stream_wrap"); import { isIP, Socket as NodeSocket } from "node:net"; import { EventEmitter } from "node:events"; +const ShutdownType = { + receive: "receive", + send: "send", + both: "both", +}; + +const SocketState = { + Error: "Error", + Connection: "Connection", + Listener: "Listener", +} + function tupleToIPv6(arr) { if (arr.length !== 8) { return null; @@ -63,9 +75,12 @@ function deserializeIpAddress(addr, family) { return address; } -function assert(condition, message) { +function assert(condition, code, message) { if (condition) { - throw new Error(message); + const ex = new Error(message); + ex.name = "Error"; + ex.code = code; + throw ex; } } @@ -76,7 +91,6 @@ export class TcpSocketImpl extends EventEmitter { /** @type {TCP.TCPConstants.SOCKET} */ #clientHandle = null; /** @type {Network} */ network = null; - id = 0; #isBound = false; #socketOptions = {}; #canReceive = true; @@ -89,9 +103,8 @@ export class TcpSocketImpl extends EventEmitter { // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; - constructor(socketId, addressFamily) { + constructor(addressFamily) { super(); - this.id = socketId; this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; this.#socketOptions.noDelay = false; @@ -102,13 +115,6 @@ export class TcpSocketImpl extends EventEmitter { this._handle.onconnection = this.#handleConnection.bind(this); } - server() { - return this.#serverHandle; - } - client() { - return this.#clientHandle; - } - #handleConnection(err, clientHandle) { console.log(`[tcp] on server connection`); @@ -135,7 +141,7 @@ export class TcpSocketImpl extends EventEmitter { socket._handle.readStart(); } - onClientConnectComplete(err) { + #onClientConnectComplete(err) { console.log(`[tcp] on client connect complete`); if (err) { @@ -152,22 +158,26 @@ export class TcpSocketImpl extends EventEmitter { this.#state = "connected"; } + // TODO: is this needed? + #handleAfterShutdown() { + console.log(`[tcp] after shutdown socket ${this.id}`); + } + /** - * @param {TcpSocket} tcpSocket * @param {Network} network * @param {IpSocketAddress} localAddress * @returns {void} - * @throws {address-family-mismatch} The `local-address` has the wrong address family. (EINVAL) - * @throws {already-bound} The socket is already bound. (EINVAL) - * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - **/ - startBind(tcpSocket, network, localAddress) { + * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + * @throws {invalid-argument} `local-address` is not a unicast address. (EINVAL) + * @throws {invalid-argument} `local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL) + * @throws {invalid-state} The socket is already bound. (EINVAL) + * */ + startBind(network, localAddress) { console.log( - `[tcp] start bind socket ${tcpSocket.id} to ${localAddress.val.address}:${localAddress.val.port}` + `[tcp] start bind socket to ${localAddress.val.address}:${localAddress.val.port}` ); - assert(this.#isBound, "already-bound"); - assert(this.#inProgress, "concurrency-conflict"); + assert(this.#isBound, "invalid-state", "The socket is already bound"); const address = serializeIpAddress( localAddress, @@ -177,7 +187,15 @@ export class TcpSocketImpl extends EventEmitter { assert( this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), - "address-family-mismatch" + "invalid-argument", + "The `local-address` has the wrong address family" + ); + + // TODO: assert localAddress is not an unicast address + assert( + ipFamily.toLocaleLowerCase() === "ipv4" && this.ipv6Only(), + "invalid-argument", + "`local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled." ); const { port } = localAddress.val; @@ -188,16 +206,15 @@ export class TcpSocketImpl extends EventEmitter { } /** - * @param {TcpSocket} tcpSocket * @returns {void} - * @throws {ephemeral-ports-exhausted} No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + * @throws {address-in-use} No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) * @throws {address-in-use} Address is already in use. (EADDRINUSE) * @throws {address-not-bindable} `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) * @throws {not-in-progress} A `bind` operation is not in progress. * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ - finishBind(tcpSocket) { - console.log(`[tcp] finish bind socket ${tcpSocket.id}`); + finishBind() { + console.log(`[tcp] finish bind socket`); assert(this.#inProgress === false, "not-in-progress"); @@ -207,34 +224,33 @@ export class TcpSocketImpl extends EventEmitter { const err = this.#serverHandle.bind(localAddress, localPort, family); if (err) { this.#serverHandle.close(); - console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); + console.error(`[tcp] error on socket: ${err}`); this.#state = "error"; } - console.log( - `[tcp] bound socket ${tcpSocket.id} to ${localAddress}:${localPort}` - ); + console.log(`[tcp] bound socket to ${localAddress}:${localPort}`); this.#isBound = true; this.#inProgress = false; } /** - * @param {TcpSocket} tcpSocket * @param {Network} network * @param {IpSocketAddress} remoteAddress * @returns {void} - * @throws {address-family-mismatch} The `remote-address` has the wrong address family. (EAFNOSUPPORT) - * @throws {invalid-remote-address} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) - * @throws {invalid-remote-port} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) - * @throws {already-attached} The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. - * @throws {already-connected} The socket is already in the Connection state. (EISCONN) - * @throws {already-listening} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) - * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-argument} `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + * @throws {invalid-argument} `remote-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL, EADDRNOTAVAIL on Illumos) + * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + * @throws {invalid-argument} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + * @throws {invalid-argument} The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + * @throws {invalid-state} The socket is already in the Connection state. (EISCONN) + * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) * */ - startConnect(tcpSocket, network, remoteAddress) { + startConnect(network, remoteAddress) { console.log( - `[tcp] start connect socket ${tcpSocket.id} to ${remoteAddress.val.address}:${remoteAddress.val.port}` + `[tcp] start connect socket to ${remoteAddress.val.address}:${remoteAddress.val.port}` ); assert( @@ -263,18 +279,18 @@ export class TcpSocketImpl extends EventEmitter { } /** - * @param {TcpSocket} tcpSocket * @returns {Array} * @throws {timeout} Connection timed out. (ETIMEDOUT) * @throws {connection-refused} The connection was forcefully rejected. (ECONNREFUSED) * @throws {connection-reset} The connection was reset. (ECONNRESET) + * @throws {connection-aborted} The connection was aborted. (ECONNABORTED) * @throws {remote-unreachable} The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {ephemeral-ports-exhausted} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) * @throws {not-in-progress} A `connect` operation is not in progress. * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) * */ - finishConnect(tcpSocket) { - console.log(`[tcp] finish connect socket ${tcpSocket.id}`); + finishConnect() { + console.log(`[tcp] finish connect socket`); assert(this.#inProgress === false, "not-in-progress"); @@ -288,11 +304,11 @@ export class TcpSocketImpl extends EventEmitter { ); if (err) { - console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); - this.#state = "error"; + console.error(`[tcp] error on socket: ${err}`); + this.#state = SocketState.Error; } - connectReq.oncomplete = this.onClientConnectComplete.bind(this); + connectReq.oncomplete = this.#onClientConnectComplete.bind(this); connectReq.address = remoteAddress; connectReq.port = remotePort; connectReq.localAddress = localAddress; @@ -313,45 +329,39 @@ export class TcpSocketImpl extends EventEmitter { } /** - * @param {TcpSocket} tcpSocket * @returns {void} - * @throws {not-bound} The socket is not bound to any local address. (EDESTADDRREQ) - * @throws {already-connected} The socket is already in the Connection state. (EISCONN, EINVAL on BSD) - * @throws {already-listening} The socket is already in the Listener state. - * @throws {concurrency-conflict} Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) + * @throws {invalid-state} The socket is not bound to any local address. (EDESTADDRREQ) + * @throws {invalid-state} The socket is already in the Connection state. (EISCONN, EINVAL on BSD) + * @throws {invalid-state} The socket is already in the Listener state. * */ - startListen(tcpSocket) { + startListen() { console.log( - `[tcp] start listen socket ${tcpSocket.id} on ${ - this.#socketOptions.localAddress - }:${this.#socketOptions.localPort}` + `[tcp] start listen socket on ${this.#socketOptions.localAddress}:${ + this.#socketOptions.localPort + }` ); - assert(this.#isBound === false, "not-bound"); - assert(this.#state === "connected", "already-connected"); - assert(this.#state === "connection", "already-listening"); - assert(this.#inProgress, "concurrency-conflict"); + assert(this.#isBound === false, "invalid-state"); + assert(this.#state === SocketState.Connection, "invalid-state"); + assert(this.#state === SocketState.Listener, "invalid-state"); this.#inProgress = true; } /** - * @param {TcpSocket} tcpSocket * @returns {void} - * @throws {ephemeral-ports-exhausted} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) * @throws {not-in-progress} A `listen` operation is not in progress. * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) * */ - finishListen(tcpSocket) { - console.log( - `[tcp] finish listen socket ${tcpSocket.id} (backlog: ${this.#backlog})` - ); + finishListen() { + console.log(`[tcp] finish listen socket (backlog: ${this.#backlog})`); assert(this.#inProgress === false, "not-in-progress"); const err = this.#serverHandle.listen(this.#backlog); if (err) { - console.error(`[tcp] error on socket ${tcpSocket.id}: ${err}`); + console.error(`[tcp] error on socket: ${err}`); this.#serverHandle.close(); throw new Error(err); } @@ -360,26 +370,26 @@ export class TcpSocketImpl extends EventEmitter { } /** - * @param {TcpSocket} tcpSocket * @returns {Array} - * @throws {not-listening} Socket is not in the Listener state. (EINVAL) + * @throws {invalid-state} Socket is not in the Listener state. (EINVAL) * @throws {would-block} No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + * @throws {connection-aborted} An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) * */ - accept(tcpSocket) { - console.log(`[tcp] accept socket ${tcpSocket.id}`); + accept() { + console.log(`[tcp] accept socket`); assert(this.#state !== "listening", "not-listening"); } /** - * @param {TcpSocket} tcpSocket * @returns {IpSocketAddress} - * @throws {not-bound} The socket is not bound to any local address. + * @throws {invalid-state} The socket is not bound to any local address. * */ - localAddress(tcpSocket) { - console.log(`[tcp] local address socket ${tcpSocket.id}`); + localAddress() { + console.log(`[tcp] local address socket`); - assert(this.#isBound === false, "not-bound"); + assert(this.#isBound === false, "invalid-state"); const { localAddress, localPort, family } = this.#socketOptions; return { @@ -392,241 +402,201 @@ export class TcpSocketImpl extends EventEmitter { } /** - * @param {TcpSocket} tcpSocket * @returns {IpSocketAddress} - * @throws {not-connected} The socket is not connected to a remote address. (ENOTCONN) + * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) * */ - remoteAddress(tcpSocket) { - console.log(`[tcp] remote address socket ${tcpSocket.id}`); + remoteAddress() { + console.log(`[tcp] remote address socket`); - assert(this.#isBound === false, "not-bound"); - assert(this.#state !== "connected", "not-connected"); + assert(this.#state !== SocketState.Connection, "invalid-state"); return this.#socketOptions.remoteAddress; } /** - * @param {TcpSocket} tcpSocket * @returns {IpAddressFamily} * */ - addressFamily(tcpSocket) { - console.log(`[tcp] address family socket ${tcpSocket.id}`); + addressFamily() { + console.log(`[tcp] address family socket`); return this.#socketOptions.family; } /** - * @param {TcpSocket} tcpSocket * @returns {boolean} - * @throws {ipv6-only-operation} (get/set) `this` socket is an IPv4 socket. - + * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. * */ - ipv6Only(tcpSocket) { + ipv6Only() { console.log(`[tcp] ipv6 only socket ${this.id}`); return this.#ipv6Only; } /** - * @param {TcpSocket} tcpSocket * @param {boolean} value * @returns {void} - * @throws {ipv6-only-operation} (get/set) `this` socket is an IPv4 socket. - * @throws {already-bound} (set) The socket is already bound. + * @throws {invalid-state} (set) The socket is already bound. + * @throws {invalid-state} (get/set) `this` socket is an IPv4 socket. * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ - setIpv6Only(tcpSocket, value) { - console.log(`[tcp] set ipv6 only socket ${tcpSocket.id} to ${value}`); + setIpv6Only(value) { + console.log(`[tcp] set ipv6 only socket to ${value}`); this.#ipv6Only = value; } /** - * @param {TcpSocket} tcpSocket * @param {bigint} value * @returns {void} - * @throws {already-connected} (set) The socket is already in the Connection state. - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * @throws {not-supported} (set) The platform does not support changing the backlog size after the initial listen. + * @throws {invalid-state} (set) The socket is already in the Connection state. * */ - setListenBacklogSize(tcpSocket, value) { - console.log( - `[tcp] set listen backlog size socket ${tcpSocket.id} to ${value}` - ); + setListenBacklogSize(value) { + console.log(`[tcp] set listen backlog size socket to ${value}`); this.#backlog = value; } /** - * @param {TcpSocket} tcpSocket * @returns {boolean} * */ - keepAlive(tcpSocket) { - console.log(`[tcp] keep alive socket ${tcpSocket.id}`); + keepAlive() { + console.log(`[tcp] keep alive socket`); this.#socketOptions.keepAlive; } /** - * @param {TcpSocket} tcpSocket * @param {boolean} value * @returns {void} - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ - setKeepAlive(tcpSocket, value) { - console.log(`[tcp] set keep alive socket ${tcpSocket.id} to ${value}`); + setKeepAlive(value) { + console.log(`[tcp] set keep alive socket to ${value}`); this.#socketOptions.keepAlive = value; this.#clientHandle.setKeepAlive(value); } /** - * @param {TcpSocket} tcpSocket * @returns {boolean} * */ - noDelay(tcpSocket) { - console.log(`[tcp] no delay socket ${tcpSocket.id}`); + noDelay() { + console.log(`[tcp] no delay socket`); return this.#socketOptions.noDelay; } /** - * @param {TcpSocket} tcpSocket * @param {boolean} value * @returns {void} * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) * */ - setNoDelay(tcpSocket, value) { - console.log(`[tcp] set no delay socket ${tcpSocket.id} to ${value}`); + setNoDelay(value) { + console.log(`[tcp] set no delay socket to ${value}`); this.#socketOptions.noDelay = value; this.#serverHandle.setNoDelay(value); } /** - * @param {TcpSocket} tcpSocket - * @returns {void} - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * @returns {number} * */ - unicastHopLimit(tcpSocket) { - console.log(`[tcp] unicast hop limit socket ${tcpSocket.id}`); + unicastHopLimit() { + console.log(`[tcp] unicast hop limit socket`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket * @param {number} value * @returns {void} - * @throws {already-connected} (set) The socket is already in the Connection state. - * @throws {already-listening} (set) The socket is already in the Listener state. - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - + * @throws {invalid-argument} (set) The TTL value must be 1 or higher. + * @throws {invalid-state} (set) The socket is already in the Connection state. + * @throws {invalid-state} (set) The socket is already in the Listener state. * */ - setUnicastHopLimit(tcpSocket, value) { - console.log( - `[tcp] set unicast hop limit socket ${tcpSocket.id} to ${value}` - ); + setUnicastHopLimit(value) { + console.log(`[tcp] set unicast hop limit socket to ${value}`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket * @returns {bigint} * */ - receiveBufferSize(tcpSocket) { - console.log(`[tcp] receive buffer size socket ${tcpSocket.id}`); + receiveBufferSize() { + console.log(`[tcp] receive buffer size socket`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket - * @param {bigint} value + * @param {number} value * @returns {void} - * @throws {already-connected} (set) The socket is already in the Connection state. - * @throws {already-listening} (set) The socket is already in the Listener state. - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * @throws {invalid-state} (set) The socket is already in the Connection state. + * @throws {invalid-state} (set) The socket is already in the Listener state. * */ - setReceiveBufferSize(tcpSocket, value) { - console.log(`[tcp] set receive buffer size socket ${this.id} to ${value}`); + setReceiveBufferSize(value) { + console.log(`[tcp] set receive buffer size socket to ${value}`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket * @returns {bigint} * */ - sendBufferSize(tcpSocket) { - console.log(`[tcp] send buffer size socket ${tcpSocket.id}`); + sendBufferSize() { + console.log(`[tcp] send buffer size socket`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket * @param {bigint} value * @returns {void} - * @throws {already-connected} (set) The socket is already in the Connection state. - * @throws {already-listening} (set) The socket is already in the Listener state. - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + * @throws {invalid-state} (set) The socket is already in the Connection state. + * @throws {invalid-state} (set) The socket is already in the Listener state. * */ - setSendBufferSize(tcpSocket, value) { - console.log( - `[tcp] set send buffer size socket ${tcpSocket.id} to ${value}` - ); + setSendBufferSize(value) { + console.log(`[tcp] set send buffer size socket to ${value}`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket * @returns {Pollable} * */ - subscribe(tcpSocket) { - console.log(`[tcp] subscribe socket ${tcpSocket.id}`); + subscribe() { + console.log(`[tcp] subscribe socket`); throw new Error("not implemented"); } /** - * @param {TcpSocket} tcpSocket * @param {ShutdownType} shutdownType * @returns {void} - * @throws {not-connected} The socket is not in the Connection state. (ENOTCONN) + * @throws {invalid-state} The socket is not in the Connection state. (ENOTCONN) * */ - shutdown(tcpSocket, shutdownType) { - console.log(`[tcp] shutdown socket ${tcpSocket.id} with ${shutdownType}`); + shutdown(shutdownType) { + console.log(`[tcp] shutdown socket with type ${shutdownType}`); - assert(this.#state !== "connected", "not-connected"); - - if (shutdownType === "read") { + // TODO: figure out how to handle shutdownTypes + if (shutdownType === ShutdownType.receive) { this.#canReceive = false; - } else if (shutdownType === "write") { + } else if (shutdownType === ShutdownType.send) { this.#canSend = false; - } else if (shutdownType === "both") { + } else if (shutdownType === ShutdownType.both) { this.#canReceive = false; this.#canSend = false; } const req = new ShutdownWrap(); - req.oncomplete = this.#afterShutdown.bind(this); + req.oncomplete = this.#handleAfterShutdown.bind(this); req.handle = this._handle; - req.callback = () => {}; + req.callback = () => { + console.log(`[tcp] shutdown callback`); + }; const err = this._handle.shutdown(req); - assert(err === 1, "not-connected"); + assert(err === 1, "invalid-state"); } - #afterShutdown() { - console.log(`[tcp] after shutdown socket ${this.id}`); + server() { + return this.#serverHandle; } - - /** - * @param {TcpSocket} tcpSocket - * @returns {void} - * */ - dropTcpSocket(tcpSocket) { - console.log(`[tcp] drop socket ${tcpSocket.id}`); - - this._handle.close(); - this._handle = null; - this.#serverHandle.close(); - this.#clientHandle.close(); + client() { + return this.#clientHandle; } } diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index e0a8c16cb..fc39be9d4 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -10,47 +10,90 @@ import { TcpSocketImpl } from "./tcp-socket-impl.js"; /** @type {ErrorCode} */ export const errorCode = { // ### GENERAL ERRORS ### + + /// Unknown error unknown: "unknown", + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM accessDenied: "access-denied", + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP notSupported: "not-supported", + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalidArgument: "invalid-argument", + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY outOfMemory: "out-of-memory", + + /// The operation timed out before it could finish completely. timeout: "timeout", + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY concurrencyConflict: "concurrency-conflict", + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. notInProgress: "not-in-progress", - wouldBlock: "would-block", - // ### IP ERRORS ### - addressFamilyNotSupported: "address-family-not-supported", - addressFamilyMismatch: "address-family-mismatch", - invalidRemoteAddress: "invalid-remote-address", - ipv4OnlyOperation: "ipv4-only-operation", - ipv6OnlyOperation: "ipv6-only-operation", + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + wouldBlock: "would-block", // ### TCP & UDP SOCKET ERRORS ### + + /// The operation is not valid in the socket's current state. + invalidState: "invalid-state", + + /// A new socket resource could not be created because of a system limit. newSocketLimit: "new-socket-limit", - alreadyAttached: "already-attached", - alreadyBound: "already-bound", - alreadyConnected: "already-connected", - notBound: "not-bound", - notConnected: "not-connected", + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. addressNotBindable: "address-not-bindable", + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. addressInUse: "address-in-use", - ephemeralPortsExhausted: "ephemeral-ports-exhausted", + + /// The remote address is not reachable remoteUnreachable: "remote-unreachable", // ### TCP SOCKET ERRORS ### - alreadyListening: "already-listening", - notListening: "not-listening", + + /// The connection was forcefully rejected connectionRefused: "connection-refused", + + /// The connection was reset. connectionReset: "connection-reset", + /// A connection was aborted. + connectionAborted: "connection-aborted", + // ### UDP SOCKET ERRORS ### datagramTooLarge: "datagram-too-large", // ### NAME LOOKUP ERRORS ### - invalidName: "invalid-name", + + /// Name does not exist or has no suitable associated IP addresses. nameUnresolvable: "name-unresolvable", + + /// A temporary failure in name resolution occurred. temporaryResolverFailure: "temporary-resolver-failure", + + /// A permanent failure in name resolution occurred. permanentResolverFailure: "permanent-resolver-failure", }; @@ -64,7 +107,10 @@ export const IpAddressFamily = { export class WasiSockets { networkCnt = 1; - tcpSocketCnt = 1; + socketCnt = 1; + + // TODO: figure out what the max number of sockets should be + maxSockets = 100; /** @type {Network} */ networkInstance = null; /** @type {Map} */ networks = new Map(); @@ -84,7 +130,7 @@ export class WasiSockets { class TcpSocket extends TcpSocketImpl { /** @param {IpAddressFamily} addressFamily */ constructor(addressFamily) { - super(net.tcpSocketCnt++, addressFamily); + super(addressFamily); net.tcpSockets.set(this.id, this); } } @@ -107,36 +153,30 @@ export class WasiSockets { this.network = { errorCode, IpAddressFamily, - /** - * @param {Network} networkId - * @returns {void} - **/ - dropNetwork(networkId) { - console.log(`[network] Drop network ${networkId}`); - - // TODO: shouldn't we return a boolean to indicate success or failure? - // TODO: update tsdoc return type - return net.networks.delete(networkId); - }, }; this.tcpCreateSocket = { /** * @param {IpAddressFamily} addressFamily * @returns {TcpSocket} - * @throws {Error} not-supported | address-family-not-supported | new-socket-limit + * @throws {Error} not-supported + * @throws {Error} new-socket-limit */ createTcpSocket(addressFamily) { console.log(`[tcp] Create tcp socket ${addressFamily}`); if (supportedAddressFamilies.includes(addressFamily) === false) { - throw new Error(errorCode.addressFamilyNotSupported); + throw new Error(errorCode.notSupported); + } + + if (net.socketCnt + 1 > net.maxSockets) { + throw new Error(errorCode.newSocketLimit); } try { + net.socketCnt++; return new TcpSocket(addressFamily); - } - catch (e) { + } catch { throw new Error(errorCode.notSupported); } }, diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index f7575473e..3a8131133 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,4 +1,4 @@ -import { deepEqual, equal, ok, throws, strictEqual } from "node:assert"; +import { deepEqual, equal, ok, throws, strictEqual, notEqual } from "node:assert"; import { mock } from "node:test"; import { fileURLToPath } from "node:url"; @@ -169,25 +169,12 @@ suite("Node.js Preview2", () => { equal(network2.id, 1); }); - test("sockets.dropNetwork()", async () => { - const { sockets } = await import("@bytecodealliance/preview2-shim"); - const net = sockets.instanceNetwork.instanceNetwork(); - - // drop existing network - const op1 = sockets.network.dropNetwork(net.id); - equal(op1, true); - - // drop non-existing network - const op2 = sockets.network.dropNetwork(99999); - equal(op2, false); - }); - test("sockets.tcpCreateSocket()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - const { id } = sockets.tcpCreateSocket.createTcpSocket( + const socket = sockets.tcpCreateSocket.createTcpSocket( sockets.network.IpAddressFamily.ipv4 ); - equal(id, 1); + notEqual(socket, null); throws( () => { @@ -195,7 +182,7 @@ suite("Node.js Preview2", () => { }, { name: "Error", - message: sockets.network.errorCode.addressFamilyNotSupported, + message: sockets.network.errorCode.notSupported, } ); }); @@ -212,18 +199,18 @@ suite("Node.js Preview2", () => { port: 0, }, }; - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); + tcpSocket.startBind(network, localAddress); + tcpSocket.finishBind(); equal(tcpSocket.network.id, network.id); - deepEqual(tcpSocket.localAddress(tcpSocket), { + deepEqual(tcpSocket.localAddress(), { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], port: 0, }, }); - equal(tcpSocket.addressFamily(tcpSocket), "ipv4"); + equal(tcpSocket.addressFamily(), "ipv4"); }); test("tcp.bind(): should bind to a valid ipv6 address", async () => { @@ -239,12 +226,12 @@ suite("Node.js Preview2", () => { port: 0, }, }; - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); + tcpSocket.startBind(network, localAddress); + tcpSocket.finishBind(); equal(tcpSocket.network.id, network.id); - equal(tcpSocket.addressFamily(tcpSocket), "ipv6"); - deepEqual(tcpSocket.localAddress(tcpSocket), { + equal(tcpSocket.addressFamily(), "ipv6"); + deepEqual(tcpSocket.localAddress(), { tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], @@ -253,7 +240,7 @@ suite("Node.js Preview2", () => { }); }); - test("tcp.bind(): should throw address-family-mismatch", async () => { + test("tcp.bind(): should throw invalid-argument", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); @@ -261,6 +248,7 @@ suite("Node.js Preview2", () => { sockets.network.IpAddressFamily.ipv4 ); const localAddress = { + // invalid address family tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], @@ -269,16 +257,15 @@ suite("Node.js Preview2", () => { }; throws( () => { - tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.startBind(network, localAddress); }, { - name: "Error", - message: "address-family-mismatch", + code: "invalid-argument", } ); }); - test("tcp.bind(): should throw already-bound", async () => { + test("tcp.bind(): should throw invalid-state", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); @@ -294,13 +281,13 @@ suite("Node.js Preview2", () => { }; throws( () => { - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); - tcpSocket.startBind(tcpSocket, network, localAddress); + tcpSocket.startBind(network, localAddress); + tcpSocket.finishBind(); + // already bound + tcpSocket.startBind(network, localAddress); }, { - name: "Error", - message: "already-bound", + code: "invalid-state", } ); }); @@ -323,15 +310,13 @@ suite("Node.js Preview2", () => { console.log("mock listen called"); }); - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); - tcpSocket.startListen(tcpSocket); - tcpSocket.finishListen(tcpSocket); + tcpSocket.startBind(network, localAddress); + tcpSocket.finishBind(); + tcpSocket.startListen(); + tcpSocket.finishListen(); strictEqual(tcpSocket.server().listen.mock.calls.length, 1); - tcpSocket.dropTcpSocket(tcpSocket); - mock.reset(); }); @@ -361,16 +346,16 @@ suite("Node.js Preview2", () => { console.log("mock connect called"); }); - tcpSocket.startBind(tcpSocket, network, localAddress); - tcpSocket.finishBind(tcpSocket); - tcpSocket.startConnect(tcpSocket, network, remoteAddress); - tcpSocket.finishConnect(tcpSocket); + tcpSocket.startBind(network, localAddress); + tcpSocket.finishBind(); + tcpSocket.startConnect(network, remoteAddress); + tcpSocket.finishConnect(); strictEqual(tcpSocket.client().connect.mock.calls.length, 1); equal(tcpSocket.network.id, network.id); - equal(tcpSocket.addressFamily(tcpSocket), "ipv4"); - deepEqual(tcpSocket.localAddress(tcpSocket), { + equal(tcpSocket.addressFamily(), "ipv4"); + deepEqual(tcpSocket.localAddress(), { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], diff --git a/test/preview2-wasi-sockets.js b/test/preview2-wasi-sockets.js index 9ccfdbdba..fa1e60df5 100644 --- a/test/preview2-wasi-sockets.js +++ b/test/preview2-wasi-sockets.js @@ -12,11 +12,11 @@ const serverAddress = { const server = sockets.tcpCreateSocket.createTcpSocket( sockets.network.IpAddressFamily.ipv4 ); -server.startBind(server, network, serverAddress); -server.finishBind(server); -server.startListen(server); -server.finishListen(server); -const {address, port} = server.localAddress(server).val; +server.startBind(network, serverAddress); +server.finishBind(); +server.startListen(); +server.finishListen(); +const {address, port} = server.localAddress().val; console.log(`[wasi-sockets] Server listening on: ${address}:${port}`); // client @@ -25,9 +25,11 @@ const client = sockets.tcpCreateSocket.createTcpSocket( sockets.network.IpAddressFamily.ipv4 ); -client.startConnect(client, network, serverAddress); -client.finishConnect(client); +client.startConnect(network, serverAddress); +client.finishConnect(); + setTimeout(() => { - client.dropTcpSocket(client); - server.dropTcpSocket(server); + client.shutdown("send"); + server.shutdown("receive"); + process.exit(0); }, 2000); From 111ab21180de2fbcf57d040b4c5c2e6bd0fb83fd Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 26 Oct 2023 01:17:02 +0200 Subject: [PATCH 24/65] fix: support bind6 and connect6 --- .../lib/sockets/tcp-socket-impl.js | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 89f043854..98eb177ce 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -31,7 +31,9 @@ const SocketState = { Error: "Error", Connection: "Connection", Listener: "Listener", -} +}; + +function noop() {} function tupleToIPv6(arr) { if (arr.length !== 8) { @@ -79,6 +81,7 @@ function assert(condition, code, message) { if (condition) { const ex = new Error(message); ex.name = "Error"; + ex.message = message; ex.code = code; throw ex; } @@ -108,6 +111,7 @@ export class TcpSocketImpl extends EventEmitter { this.#socketOptions.family = addressFamily; this.#socketOptions.keepAlive = false; this.#socketOptions.noDelay = false; + this.#socketOptions.unicastHopLimit = 10; this.#clientHandle = new TCP(TCPConstants.SOCKET); this.#serverHandle = new TCP(TCPConstants.SERVER); @@ -173,17 +177,17 @@ export class TcpSocketImpl extends EventEmitter { * @throws {invalid-state} The socket is already bound. (EINVAL) * */ startBind(network, localAddress) { - console.log( - `[tcp] start bind socket to ${localAddress.val.address}:${localAddress.val.port}` - ); - - assert(this.#isBound, "invalid-state", "The socket is already bound"); - const address = serializeIpAddress( localAddress, this.#socketOptions.family ); const ipFamily = `ipv${isIP(address)}`; + + console.log( + `[tcp] start bind socket to ${address}:${localAddress.val.port}` + ); + + assert(this.#isBound, "invalid-state", "The socket is already bound"); assert( this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), @@ -221,11 +225,17 @@ export class TcpSocketImpl extends EventEmitter { const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); - const err = this.#serverHandle.bind(localAddress, localPort, family); + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = this.#serverHandle.bind(localAddress, localPort); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = this.#serverHandle.bind6(localAddress, localPort); + } + if (err) { this.#serverHandle.close(); - console.error(`[tcp] error on socket: ${err}`); - this.#state = "error"; + assert(err === -22, "address-in-use"); + assert(true, "", err); } console.log(`[tcp] bound socket to ${localAddress}:${localPort}`); @@ -294,17 +304,19 @@ export class TcpSocketImpl extends EventEmitter { assert(this.#inProgress === false, "not-in-progress"); - const { localAddress, localPort, remoteAddress, remotePort } = + const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; const connectReq = new TCPConnectWrap(); - const err = this.#clientHandle.connect( - connectReq, - remoteAddress, - remotePort - ); + + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = this.#clientHandle.connect(connectReq, remoteAddress, remotePort); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = this.#clientHandle.connect6(connectReq, remoteAddress, remotePort); + } if (err) { - console.error(`[tcp] error on socket: ${err}`); + console.error(`[tcp] connect error on socket: ${err}`); this.#state = SocketState.Error; } @@ -361,7 +373,7 @@ export class TcpSocketImpl extends EventEmitter { const err = this.#serverHandle.listen(this.#backlog); if (err) { - console.error(`[tcp] error on socket: ${err}`); + console.error(`[tcp] listen error on socket: ${err}`); this.#serverHandle.close(); throw new Error(err); } @@ -377,9 +389,7 @@ export class TcpSocketImpl extends EventEmitter { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) * */ accept() { - console.log(`[tcp] accept socket`); - - assert(this.#state !== "listening", "not-listening"); + return noop(); } /** @@ -503,7 +513,8 @@ export class TcpSocketImpl extends EventEmitter { * */ unicastHopLimit() { console.log(`[tcp] unicast hop limit socket`); - throw new Error("not implemented"); + + return this.#socketOptions.unicastHopLimit; } /** @@ -515,7 +526,8 @@ export class TcpSocketImpl extends EventEmitter { * */ setUnicastHopLimit(value) { console.log(`[tcp] set unicast hop limit socket to ${value}`); - throw new Error("not implemented"); + + this.#socketOptions.unicastHopLimit = value; } /** From 28db1f3f790ee877c0543127b8716f9f056af0df Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 27 Oct 2023 11:10:32 +0200 Subject: [PATCH 25/65] chore: move assert to own file --- packages/preview2-shim/lib/common/assert.js | 9 +++++ .../lib/sockets/tcp-socket-impl.js | 39 +++++++++---------- 2 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 packages/preview2-shim/lib/common/assert.js diff --git a/packages/preview2-shim/lib/common/assert.js b/packages/preview2-shim/lib/common/assert.js new file mode 100644 index 000000000..63e07c956 --- /dev/null +++ b/packages/preview2-shim/lib/common/assert.js @@ -0,0 +1,9 @@ +export function assert(condition, code, message) { + if (condition) { + const ex = new Error(message); + ex.name = "Error"; + ex.message = message; + ex.code = code; + throw ex; + } +} diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 98eb177ce..b95600c74 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,6 +11,8 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ +import { assert } from "../common/assert.js"; + // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc const { TCP, @@ -77,16 +79,6 @@ function deserializeIpAddress(addr, family) { return address; } -function assert(condition, code, message) { - if (condition) { - const ex = new Error(message); - ex.name = "Error"; - ex.message = message; - ex.code = code; - throw ex; - } -} - // TODO: implement would-block exceptions // TODO: implement concurrency-conflict exceptions export class TcpSocketImpl extends EventEmitter { @@ -102,16 +94,21 @@ export class TcpSocketImpl extends EventEmitter { #state = "closed"; #inProgress = false; #connections = 0; + #keepAlive = false; + #noDelay = false; + #unicastHopLimit = 10; // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; + /** + * @param {IpAddressFamily} addressFamily + * @returns + * */ constructor(addressFamily) { super(); + this.#socketOptions.family = addressFamily; - this.#socketOptions.keepAlive = false; - this.#socketOptions.noDelay = false; - this.#socketOptions.unicastHopLimit = 10; this.#clientHandle = new TCP(TCPConstants.SOCKET); this.#serverHandle = new TCP(TCPConstants.SERVER); @@ -128,7 +125,6 @@ export class TcpSocketImpl extends EventEmitter { const socket = new NodeSocket({ handle: clientHandle }); this.#connections++; - // reserved socket.server = this.#serverHandle; socket._server = this.#serverHandle; @@ -389,6 +385,7 @@ export class TcpSocketImpl extends EventEmitter { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) * */ accept() { + // uv_accept is automatically called by uv_listen when a new connection is received. return noop(); } @@ -473,7 +470,7 @@ export class TcpSocketImpl extends EventEmitter { keepAlive() { console.log(`[tcp] keep alive socket`); - this.#socketOptions.keepAlive; + this.#keepAlive; } /** @@ -483,7 +480,7 @@ export class TcpSocketImpl extends EventEmitter { setKeepAlive(value) { console.log(`[tcp] set keep alive socket to ${value}`); - this.#socketOptions.keepAlive = value; + this.#keepAlive = value; this.#clientHandle.setKeepAlive(value); } @@ -493,7 +490,7 @@ export class TcpSocketImpl extends EventEmitter { noDelay() { console.log(`[tcp] no delay socket`); - return this.#socketOptions.noDelay; + return this.#noDelay; } /** @@ -504,8 +501,8 @@ export class TcpSocketImpl extends EventEmitter { setNoDelay(value) { console.log(`[tcp] set no delay socket to ${value}`); - this.#socketOptions.noDelay = value; - this.#serverHandle.setNoDelay(value); + this.#noDelay = value; + this.#clientHandle.setNoDelay(value); } /** @@ -514,7 +511,7 @@ export class TcpSocketImpl extends EventEmitter { unicastHopLimit() { console.log(`[tcp] unicast hop limit socket`); - return this.#socketOptions.unicastHopLimit; + return this.#unicastHopLimit; } /** @@ -527,7 +524,7 @@ export class TcpSocketImpl extends EventEmitter { setUnicastHopLimit(value) { console.log(`[tcp] set unicast hop limit socket to ${value}`); - this.#socketOptions.unicastHopLimit = value; + this.#unicastHopLimit = value; } /** From d229465d49e97b3876834ee4081ad258b874214e Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 27 Oct 2023 11:11:05 +0200 Subject: [PATCH 26/65] chore: use assert --- .../preview2-shim/lib/sockets/wasi-sockets.js | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index fc39be9d4..72004f0df 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -6,6 +6,7 @@ */ import { TcpSocketImpl } from "./tcp-socket-impl.js"; +import { assert } from "../common/assert.js"; /** @type {ErrorCode} */ export const errorCode = { @@ -128,7 +129,10 @@ export class WasiSockets { } class TcpSocket extends TcpSocketImpl { - /** @param {IpAddressFamily} addressFamily */ + /** + * @param {IpAddressFamily} addressFamily + * @param {InputStream|OutputStream} io + * */ constructor(addressFamily) { super(addressFamily); net.tcpSockets.set(this.id, this); @@ -159,25 +163,29 @@ export class WasiSockets { /** * @param {IpAddressFamily} addressFamily * @returns {TcpSocket} - * @throws {Error} not-supported - * @throws {Error} new-socket-limit + * @throws {not-supported} The specified `address-family` is not supported. (EAFNOSUPPORT) + * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ createTcpSocket(addressFamily) { console.log(`[tcp] Create tcp socket ${addressFamily}`); - if (supportedAddressFamilies.includes(addressFamily) === false) { - throw new Error(errorCode.notSupported); - } + assert( + supportedAddressFamilies.includes(addressFamily) === false, + errorCode.notSupported, + "The specified `address-family` is not supported." + ); - if (net.socketCnt + 1 > net.maxSockets) { - throw new Error(errorCode.newSocketLimit); - } + assert( + net.socketCnt + 1 > net.maxSockets, + errorCode.newSocketLimit, + "The new socket resource could not be created because of a system limit" + ); try { net.socketCnt++; return new TcpSocket(addressFamily); - } catch { - throw new Error(errorCode.notSupported); + } catch (err) { + assert(true, errorCode.notSupported, err); } }, }; From 104c8f8199a7bb3c2edaf2bfb95d537ccd2feca7 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 27 Oct 2023 11:11:19 +0200 Subject: [PATCH 27/65] chore: use ipv6 in tests --- test/preview2-wasi-sockets.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/preview2-wasi-sockets.js b/test/preview2-wasi-sockets.js index fa1e60df5..5aab50cdf 100644 --- a/test/preview2-wasi-sockets.js +++ b/test/preview2-wasi-sockets.js @@ -2,17 +2,17 @@ const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); // server -const serverAddress = { - tag: sockets.network.IpAddressFamily.ipv4, +const serverAddressIpv6 = { + tag: sockets.network.IpAddressFamily.ipv6, val: { - address: [127, 0, 0, 1], + address: [0, 0, 0, 0, 0, 0, 0, 0x1], port: 3000, }, }; const server = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 + sockets.network.IpAddressFamily.ipv6 ); -server.startBind(network, serverAddress); +server.startBind(network, serverAddressIpv6); server.finishBind(); server.startListen(); server.finishListen(); @@ -20,12 +20,13 @@ const {address, port} = server.localAddress().val; console.log(`[wasi-sockets] Server listening on: ${address}:${port}`); // client - const client = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 + sockets.network.IpAddressFamily.ipv6 ); -client.startConnect(network, serverAddress); +client.setKeepAlive(true); +client.setNoDelay(true); +client.startConnect(network, serverAddressIpv6); client.finishConnect(); setTimeout(() => { From d84b70b40ddd779cd83a4cb3bdfdcb03a15e7b67 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 30 Oct 2023 17:41:07 +0100 Subject: [PATCH 28/65] chore: add streams to tcp-socket-impl (wip) --- .../lib/sockets/tcp-socket-impl.js | 53 ++++++++++++------- test/preview2-wasi-sockets.js | 13 +++-- 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index b95600c74..6a496181e 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -11,6 +11,9 @@ * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType */ +import { streams } from "../common/io.js"; +const { InputStream, OutputStream } = streams; + import { assert } from "../common/assert.js"; // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc @@ -81,7 +84,7 @@ function deserializeIpAddress(addr, family) { // TODO: implement would-block exceptions // TODO: implement concurrency-conflict exceptions -export class TcpSocketImpl extends EventEmitter { +export class TcpSocketImpl { /** @type {TCP.TCPConstants.SERVER} */ #serverHandle = null; /** @type {TCP.TCPConstants.SOCKET} */ #clientHandle = null; /** @type {Network} */ network = null; @@ -97,6 +100,7 @@ export class TcpSocketImpl extends EventEmitter { #keepAlive = false; #noDelay = false; #unicastHopLimit = 10; + #acceptedClient = null; // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; @@ -106,7 +110,6 @@ export class TcpSocketImpl extends EventEmitter { * @returns * */ constructor(addressFamily) { - super(); this.#socketOptions.family = addressFamily; @@ -114,31 +117,32 @@ export class TcpSocketImpl extends EventEmitter { this.#serverHandle = new TCP(TCPConstants.SERVER); this._handle = this.#serverHandle; this._handle.onconnection = this.#handleConnection.bind(this); + this._handle.onclose = this.#handleDisconnect.bind(this); } - #handleConnection(err, clientHandle) { + #handleConnection(err, newClientSocket) { console.log(`[tcp] on server connection`); if (err) { - throw new Error(err); + assert(true, "", err); } - const socket = new NodeSocket({ handle: clientHandle }); + this.#acceptedClient = new NodeSocket({ handle: newClientSocket }); this.#connections++; // reserved - socket.server = this.#serverHandle; - socket._server = this.#serverHandle; - - this.emit("connection", socket); - - socket._handle.onread = (nread, buffer) => { + this.#acceptedClient.server = this.#serverHandle; + this.#acceptedClient._server = this.#serverHandle; + this.#acceptedClient._handle.onread = (nread, buffer) => { if (nread > 0) { // TODO: handle data received from the client const data = buffer.toString("utf8", 0, nread); - console.log("Received data:", data); + console.log("accepted socket on read:", data); } }; - socket._handle.readStart(); + } + + #handleDisconnect(err) { + console.log(`[tcp] on server disconnect`); } #onClientConnectComplete(err) { @@ -160,7 +164,7 @@ export class TcpSocketImpl extends EventEmitter { // TODO: is this needed? #handleAfterShutdown() { - console.log(`[tcp] after shutdown socket ${this.id}`); + console.log(`[tcp] after shutdown socket`); } /** @@ -324,11 +328,8 @@ export class TcpSocketImpl extends EventEmitter { this.#clientHandle.onread = (buffer) => { // TODO: handle data received from the server - - console.log({ - buffer, - }); }; + this.#clientHandle.readStart(); this.#inProgress = false; @@ -386,7 +387,21 @@ export class TcpSocketImpl extends EventEmitter { * */ accept() { // uv_accept is automatically called by uv_listen when a new connection is received. - return noop(); + + const _this = this; + const outgoingStream = new OutputStream({ + write(bytes) { + _this.#acceptedClient.write(bytes); + }, + }); + const ingoingStream = new InputStream({ + read(len) { + console.log(`[tcp] read socket`); + return _this.#acceptedClient.read(len); + }, + }) + + return [this.#acceptedClient, ingoingStream, outgoingStream]; } /** diff --git a/test/preview2-wasi-sockets.js b/test/preview2-wasi-sockets.js index 5aab50cdf..4dd406c38 100644 --- a/test/preview2-wasi-sockets.js +++ b/test/preview2-wasi-sockets.js @@ -16,7 +16,7 @@ server.startBind(network, serverAddressIpv6); server.finishBind(); server.startListen(); server.finishListen(); -const {address, port} = server.localAddress().val; +const { address, port } = server.localAddress().val; console.log(`[wasi-sockets] Server listening on: ${address}:${port}`); // client @@ -30,7 +30,12 @@ client.startConnect(network, serverAddressIpv6); client.finishConnect(); setTimeout(() => { - client.shutdown("send"); - server.shutdown("receive"); - process.exit(0); + // const [socket, input, output] = server.accept(); + // output.write(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])); + // const buff = input.read(2); + // console.log(`[wasi-sockets] Server received: ${buff}`); + + // client.shutdown("send"); + // server.shutdown("receive"); + // process.exit(0); }, 2000); From de6e9626b98636430ceff3e80f7f30000a5e5ffd Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 30 Oct 2023 18:13:25 +0100 Subject: [PATCH 29/65] fix: add error code -49 address-not-bindable --- packages/preview2-shim/lib/sockets/tcp-socket-impl.js | 1 + packages/preview2-shim/test/test.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 6a496181e..4ca2fe6e8 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -235,6 +235,7 @@ export class TcpSocketImpl { if (err) { this.#serverHandle.close(); assert(err === -22, "address-in-use"); + assert(err === -49, "address-not-bindable"); assert(true, "", err); } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 3a8131133..895bda131 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -182,7 +182,7 @@ suite("Node.js Preview2", () => { }, { name: "Error", - message: sockets.network.errorCode.notSupported, + code: sockets.network.errorCode.notSupported, } ); }); From a62c6b3b14b0c7d5c76e74d37557647e44661cb1 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 30 Oct 2023 18:40:09 +0100 Subject: [PATCH 30/65] feat: add udp-socket-impl method signatures --- .../lib/sockets/tcp-socket-impl.js | 50 +++--- .../lib/sockets/udp-socket-impl.js | 161 ++++++++++++++++++ .../preview2-shim/lib/sockets/wasi-sockets.js | 38 ++++- 3 files changed, 223 insertions(+), 26 deletions(-) create mode 100644 packages/preview2-shim/lib/sockets/udp-socket-impl.js diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 4ca2fe6e8..d7bf3728f 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -108,7 +108,7 @@ export class TcpSocketImpl { /** * @param {IpAddressFamily} addressFamily * @returns - * */ + */ constructor(addressFamily) { this.#socketOptions.family = addressFamily; @@ -175,7 +175,7 @@ export class TcpSocketImpl { * @throws {invalid-argument} `local-address` is not a unicast address. (EINVAL) * @throws {invalid-argument} `local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL) * @throws {invalid-state} The socket is already bound. (EINVAL) - * */ + */ startBind(network, localAddress) { const address = serializeIpAddress( localAddress, @@ -258,7 +258,7 @@ export class TcpSocketImpl { * @throws {invalid-argument} The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. * @throws {invalid-state} The socket is already in the Connection state. (EISCONN) * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) - * */ + */ startConnect(network, remoteAddress) { console.log( `[tcp] start connect socket to ${remoteAddress.val.address}:${remoteAddress.val.port}` @@ -299,7 +299,7 @@ export class TcpSocketImpl { * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) * @throws {not-in-progress} A `connect` operation is not in progress. * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - * */ + */ finishConnect() { console.log(`[tcp] finish connect socket`); @@ -343,7 +343,7 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. (EDESTADDRREQ) * @throws {invalid-state} The socket is already in the Connection state. (EISCONN, EINVAL on BSD) * @throws {invalid-state} The socket is already in the Listener state. - * */ + */ startListen() { console.log( `[tcp] start listen socket on ${this.#socketOptions.localAddress}:${ @@ -363,7 +363,7 @@ export class TcpSocketImpl { * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) * @throws {not-in-progress} A `listen` operation is not in progress. * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) - * */ + */ finishListen() { console.log(`[tcp] finish listen socket (backlog: ${this.#backlog})`); @@ -385,7 +385,7 @@ export class TcpSocketImpl { * @throws {would-block} No pending connections at the moment. (EWOULDBLOCK, EAGAIN) * @throws {connection-aborted} An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) - * */ + */ accept() { // uv_accept is automatically called by uv_listen when a new connection is received. @@ -408,7 +408,7 @@ export class TcpSocketImpl { /** * @returns {IpSocketAddress} * @throws {invalid-state} The socket is not bound to any local address. - * */ + */ localAddress() { console.log(`[tcp] local address socket`); @@ -427,7 +427,7 @@ export class TcpSocketImpl { /** * @returns {IpSocketAddress} * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) - * */ + */ remoteAddress() { console.log(`[tcp] remote address socket`); @@ -438,7 +438,7 @@ export class TcpSocketImpl { /** * @returns {IpAddressFamily} - * */ + */ addressFamily() { console.log(`[tcp] address family socket`); @@ -448,7 +448,7 @@ export class TcpSocketImpl { /** * @returns {boolean} * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. - * */ + */ ipv6Only() { console.log(`[tcp] ipv6 only socket ${this.id}`); @@ -461,7 +461,7 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already bound. * @throws {invalid-state} (get/set) `this` socket is an IPv4 socket. * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) - * */ + */ setIpv6Only(value) { console.log(`[tcp] set ipv6 only socket to ${value}`); @@ -473,7 +473,7 @@ export class TcpSocketImpl { * @returns {void} * @throws {not-supported} (set) The platform does not support changing the backlog size after the initial listen. * @throws {invalid-state} (set) The socket is already in the Connection state. - * */ + */ setListenBacklogSize(value) { console.log(`[tcp] set listen backlog size socket to ${value}`); @@ -482,7 +482,7 @@ export class TcpSocketImpl { /** * @returns {boolean} - * */ + */ keepAlive() { console.log(`[tcp] keep alive socket`); @@ -492,7 +492,7 @@ export class TcpSocketImpl { /** * @param {boolean} value * @returns {void} - * */ + */ setKeepAlive(value) { console.log(`[tcp] set keep alive socket to ${value}`); @@ -502,7 +502,7 @@ export class TcpSocketImpl { /** * @returns {boolean} - * */ + */ noDelay() { console.log(`[tcp] no delay socket`); @@ -513,7 +513,7 @@ export class TcpSocketImpl { * @param {boolean} value * @returns {void} * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - * */ + */ setNoDelay(value) { console.log(`[tcp] set no delay socket to ${value}`); @@ -523,7 +523,7 @@ export class TcpSocketImpl { /** * @returns {number} - * */ + */ unicastHopLimit() { console.log(`[tcp] unicast hop limit socket`); @@ -536,7 +536,7 @@ export class TcpSocketImpl { * @throws {invalid-argument} (set) The TTL value must be 1 or higher. * @throws {invalid-state} (set) The socket is already in the Connection state. * @throws {invalid-state} (set) The socket is already in the Listener state. - * */ + */ setUnicastHopLimit(value) { console.log(`[tcp] set unicast hop limit socket to ${value}`); @@ -545,7 +545,7 @@ export class TcpSocketImpl { /** * @returns {bigint} - * */ + */ receiveBufferSize() { console.log(`[tcp] receive buffer size socket`); throw new Error("not implemented"); @@ -556,7 +556,7 @@ export class TcpSocketImpl { * @returns {void} * @throws {invalid-state} (set) The socket is already in the Connection state. * @throws {invalid-state} (set) The socket is already in the Listener state. - * */ + */ setReceiveBufferSize(value) { console.log(`[tcp] set receive buffer size socket to ${value}`); throw new Error("not implemented"); @@ -564,7 +564,7 @@ export class TcpSocketImpl { /** * @returns {bigint} - * */ + */ sendBufferSize() { console.log(`[tcp] send buffer size socket`); throw new Error("not implemented"); @@ -575,7 +575,7 @@ export class TcpSocketImpl { * @returns {void} * @throws {invalid-state} (set) The socket is already in the Connection state. * @throws {invalid-state} (set) The socket is already in the Listener state. - * */ + */ setSendBufferSize(value) { console.log(`[tcp] set send buffer size socket to ${value}`); throw new Error("not implemented"); @@ -583,7 +583,7 @@ export class TcpSocketImpl { /** * @returns {Pollable} - * */ + */ subscribe() { console.log(`[tcp] subscribe socket`); throw new Error("not implemented"); @@ -593,7 +593,7 @@ export class TcpSocketImpl { * @param {ShutdownType} shutdownType * @returns {void} * @throws {invalid-state} The socket is not in the Connection state. (ENOTCONN) - * */ + */ shutdown(shutdownType) { console.log(`[tcp] shutdown socket with type ${shutdownType}`); diff --git a/packages/preview2-shim/lib/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/sockets/udp-socket-impl.js new file mode 100644 index 000000000..039e1bd10 --- /dev/null +++ b/packages/preview2-shim/lib/sockets/udp-socket-impl.js @@ -0,0 +1,161 @@ +/* eslint-disable no-unused-vars */ + +/** + * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network + * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress + * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily + * @typedef {import("../../types/interfaces/wasi-sockets-udp").Datagram} Datagram + * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable + */ + +export class UdpSocketImpl { + /** + * + * @param {Network} network + * @param {IpAddressFamily} localAddress + * @returns {void} + */ + startBind(network, localAddress) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {void} + */ + finishBind() { + throw new Error("Not implemented"); + } + + /** + * + * @param {Network} network + * @param {IpAddressFamily} remoteAddress + * @returns {void} + */ + startConnect(network, remoteAddress) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {void} + */ + finishConnect() { + throw new Error("Not implemented"); + } + + /** + * @param {bigint} maxResults + * @returns {Datagram[]} + */ + receive(maxResults) { + throw new Error("Not implemented"); + } + + /** + * @param {Datagram[]} datagrams + * @returns {bigint} + */ + send(datagrams) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {IpSocketAddress} + */ + localAddress() { + throw new Error("Not implemented"); + } + + /** + * + * @returns {IpSocketAddress} + */ + remoteAddress() { + throw new Error("Not implemented"); + } + + /** + * + * @returns {IpAddressFamily} + */ + addressFamily() { + throw new Error("Not implemented"); + } + + /** + * + * @returns {boolean} + */ + ipv6Only() { + throw new Error("Not implemented"); + } + + /** + * + * @param {boolean} value + * @returns {void} + */ + setIpv6Only(value) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {number} + */ + unicastHopLimit() {} + + /** + * + * @param {number} value + * @returns {void} + */ + setUnicastHopLimit(value) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {bigint} + */ + receiveBufferSize() { + throw new Error("Not implemented"); + } + + /** + * + * @param {bigint} value + * @returns {void} + */ + setReceiveBufferSize(value) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {bigint} + */ + sendBufferSize() { + throw new Error("Not implemented"); + } + + /** + * + * @param {bigint} value + * @returns {void} + */ + setSendBufferSize(value) { + throw new Error("Not implemented"); + } + + /** + * + * @returns {Pollable} + */ + subscribe() { + throw new Error("Not implemented"); + } +} diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index 72004f0df..976db17fa 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -6,6 +6,7 @@ */ import { TcpSocketImpl } from "./tcp-socket-impl.js"; +import { UdpSocketImpl } from "./udp-socket-impl.js"; import { assert } from "../common/assert.js"; /** @type {ErrorCode} */ @@ -128,10 +129,19 @@ export class WasiSockets { } } + class UdpSocket extends UdpSocketImpl { + /** + * @param {IpAddressFamily} addressFamily + * */ + constructor(addressFamily) { + super(addressFamily); + net.udpSockets.set(this.id, this); + } + } + class TcpSocket extends TcpSocketImpl { /** * @param {IpAddressFamily} addressFamily - * @param {InputStream|OutputStream} io * */ constructor(addressFamily) { super(addressFamily); @@ -159,6 +169,32 @@ export class WasiSockets { IpAddressFamily, }; + this.udpCreateSocket = { + createUdpSocket(addressFamily) { + net.socketCnt++; + console.log(`[tcp] Create udp socket ${addressFamily}`); + + assert( + supportedAddressFamilies.includes(addressFamily) === false, + errorCode.notSupported, + "The specified `address-family` is not supported." + ); + + assert( + net.socketCnt + 1 > net.maxSockets, + errorCode.newSocketLimit, + "The new socket resource could not be created because of a system limit" + ); + + try { + net.socketCnt++; + return new UdpSocket(addressFamily); + } catch (err) { + assert(true, errorCode.notSupported, err); + } + }, + }; + this.tcpCreateSocket = { /** * @param {IpAddressFamily} addressFamily From dd0ed90418be6b05f449948af29d8330fb1c6c7d Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 30 Oct 2023 18:46:30 +0100 Subject: [PATCH 31/65] fix: handle error -99 --- packages/preview2-shim/lib/sockets/tcp-socket-impl.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index d7bf3728f..b93784bfd 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -236,6 +236,7 @@ export class TcpSocketImpl { this.#serverHandle.close(); assert(err === -22, "address-in-use"); assert(err === -49, "address-not-bindable"); + assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL assert(true, "", err); } From 2b29f5c09e481a00a1131c1450e07477533a93fa Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 3 Nov 2023 13:49:44 +0100 Subject: [PATCH 32/65] chore: document udp errors --- packages/preview2-shim/lib/nodejs/sockets.js | 7 +- .../lib/sockets/socket-common.js | 43 ++++++++ .../lib/sockets/tcp-socket-impl.js | 52 +--------- .../lib/sockets/udp-socket-impl.js | 98 ++++++++++++++++++- .../preview2-shim/lib/sockets/wasi-sockets.js | 4 +- packages/preview2-shim/test/test.js | 52 +++++++++- test/preview2-wasi-sockets.js | 2 +- 7 files changed, 200 insertions(+), 58 deletions(-) create mode 100644 packages/preview2-shim/lib/sockets/socket-common.js diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index e27b60197..a4a894915 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -3,4 +3,9 @@ import { WasiSockets } from "../sockets/wasi-sockets.js"; const sockets = new WasiSockets(); -export const { instanceNetwork, network, tcpCreateSocket } = sockets; \ No newline at end of file +export const { + instanceNetwork, + network, + tcpCreateSocket, + udpCreateSocket, +} = sockets; \ No newline at end of file diff --git a/packages/preview2-shim/lib/sockets/socket-common.js b/packages/preview2-shim/lib/sockets/socket-common.js new file mode 100644 index 000000000..7a4dab1a2 --- /dev/null +++ b/packages/preview2-shim/lib/sockets/socket-common.js @@ -0,0 +1,43 @@ +export function noop() {} + +function tupleToIPv6(arr) { + if (arr.length !== 8) { + return null; + } + return arr.map((segment) => segment.toString(16)).join(":"); +} + +function tupleToIpv4(arr) { + if (arr.length !== 4) { + return null; + } + return arr.map((segment) => segment.toString(10)).join("."); +} + +function ipv6ToTuple(ipv6) { + return ipv6.split(":").map((segment) => parseInt(segment, 16)); +} + +function ipv4ToTuple(ipv4) { + return ipv4.split(".").map((segment) => parseInt(segment, 10)); +} + +export function serializeIpAddress(addr, family) { + let { address } = addr.val; + if (family.toLocaleLowerCase() === "ipv4") { + address = tupleToIpv4(address); + } else if (family.toLocaleLowerCase() === "ipv6") { + address = tupleToIPv6(address); + } + return address; +} + +export function deserializeIpAddress(addr, family) { + let address = []; + if (family.toLocaleLowerCase() === "ipv4") { + address = ipv4ToTuple(addr); + } else if (family.toLocaleLowerCase() === "ipv6") { + address = ipv6ToTuple(addr); + } + return address; +} diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index b93784bfd..a0321afbd 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -24,7 +24,8 @@ const { } = process.binding("tcp_wrap"); const { ShutdownWrap } = process.binding("stream_wrap"); import { isIP, Socket as NodeSocket } from "node:net"; -import { EventEmitter } from "node:events"; + +import { serializeIpAddress, deserializeIpAddress } from "./socket-common.js"; const ShutdownType = { receive: "receive", @@ -38,50 +39,6 @@ const SocketState = { Listener: "Listener", }; -function noop() {} - -function tupleToIPv6(arr) { - if (arr.length !== 8) { - return null; - } - return arr.map((segment) => segment.toString(16)).join(":"); -} - -function tupleToIpv4(arr) { - if (arr.length !== 4) { - return null; - } - return arr.map((segment) => segment.toString(10)).join("."); -} - -function ipv6ToTuple(ipv6) { - return ipv6.split(":").map((segment) => parseInt(segment, 16)); -} - -function ipv4ToTuple(ipv4) { - return ipv4.split(".").map((segment) => parseInt(segment, 10)); -} - -function serializeIpAddress(addr, family) { - let { address } = addr.val; - if (family.toLocaleLowerCase() === "ipv4") { - address = tupleToIpv4(address); - } else if (family.toLocaleLowerCase() === "ipv6") { - address = tupleToIPv6(address); - } - return address; -} - -function deserializeIpAddress(addr, family) { - let address = []; - if (family.toLocaleLowerCase() === "ipv4") { - address = ipv4ToTuple(addr); - } else if (family.toLocaleLowerCase() === "ipv6") { - address = ipv6ToTuple(addr); - } - return address; -} - // TODO: implement would-block exceptions // TODO: implement concurrency-conflict exceptions export class TcpSocketImpl { @@ -224,11 +181,12 @@ export class TcpSocketImpl { const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); - + let err = null; if (family.toLocaleLowerCase() === "ipv4") { err = this.#serverHandle.bind(localAddress, localPort); } else if (family.toLocaleLowerCase() === "ipv6") { + console.log(`[tcp] xxxx bind socket to ${localAddress}:${localPort}`); err = this.#serverHandle.bind6(localAddress, localPort); } @@ -451,7 +409,7 @@ export class TcpSocketImpl { * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { - console.log(`[tcp] ipv6 only socket ${this.id}`); + console.log(`[tcp] ipv6 only socket`); return this.#ipv6Only; } diff --git a/packages/preview2-shim/lib/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/sockets/udp-socket-impl.js index 039e1bd10..8899509a2 100644 --- a/packages/preview2-shim/lib/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/udp-socket-impl.js @@ -1,5 +1,8 @@ /* eslint-disable no-unused-vars */ +import { assert } from "../common/assert.js"; +import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; + /** * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress @@ -8,23 +11,68 @@ * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable */ +// See: https://github.com/nodejs/node/blob/main/src/udp_wrap.cc +const { UDP, constants: UDPConstants } = process.binding("udp_wrap"); + export class UdpSocketImpl { + /** @type {Socket} */ #clientHandle = null; + /** @type {Socket} */ #serverHandle = null; + + /** @type {Network} */ network = null; + + #isBound = false; + #socketOptions = {}; + + /** + * @param {IpAddressFamily} addressFamily + * @returns + */ + constructor(addressFamily) { + this.#socketOptions.family = addressFamily; + + this.#clientHandle = new UDP(UDPConstants.SOCKET); + this.#serverHandle = new UDP(UDPConstants.SERVER); + } /** * * @param {Network} network * @param {IpAddressFamily} localAddress + * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + * @throws {invalid-state} The socket is already bound. (EINVAL) * @returns {void} */ startBind(network, localAddress) { - throw new Error("Not implemented"); + const address = serializeIpAddress( + localAddress, + this.#socketOptions.family + ); + + const { port } = localAddress.val; + this.#socketOptions.localAddress = address; + this.#socketOptions.localPort = port; + this.network = network; } /** * * @returns {void} - */ + * @throws {address-in-use} No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + * @throws {address-in-use} Address is already in use. (EADDRINUSE) + * @throws {address-not-bindable} `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + * @throws {not-in-progress} A `bind` operation is not in progress. + * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + **/ finishBind() { - throw new Error("Not implemented"); + const { localAddress, localPort, family } = this.#socketOptions; + + // TODO: see https://github.com/libuv/libuv/blob/93efccf4ee1ed3c740d10660b4bfb08edc68a1b5/src/unix/udp.c#L486 + const flags = 0; + let err = this.#serverHandle.bind(localAddress, localPort, flags); + if (err) { + throw new Error(err); + } + + this.#isBound = true; } /** @@ -32,6 +80,11 @@ export class UdpSocketImpl { * @param {Network} network * @param {IpAddressFamily} remoteAddress * @returns {void} + * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ startConnect(network, remoteAddress) { throw new Error("Not implemented"); @@ -40,6 +93,9 @@ export class UdpSocketImpl { /** * * @returns {void} + * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * @throws {not-in-progress} A `connect` operation is not in progress. + * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishConnect() { throw new Error("Not implemented"); @@ -48,14 +104,27 @@ export class UdpSocketImpl { /** * @param {bigint} maxResults * @returns {Datagram[]} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) */ receive(maxResults) { throw new Error("Not implemented"); } /** + * * @param {Datagram[]} datagrams * @returns {bigint} + * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + * @throws {invalid-state} The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. + * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) + * @throws {would-block} The send buffer is currently full. (EWOULDBLOCK, EAGAIN) */ send(datagrams) { throw new Error("Not implemented"); @@ -64,14 +133,27 @@ export class UdpSocketImpl { /** * * @returns {IpSocketAddress} + * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - throw new Error("Not implemented"); + console.log(`[udp] local address socket`); + + assert(this.#isBound === false, "invalid-state"); + + const { localAddress, localPort, family } = this.#socketOptions; + return { + tag: family, + val: { + address: deserializeIpAddress(localAddress, family), + port: localPort, + }, + }; } /** * * @returns {IpSocketAddress} + * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { throw new Error("Not implemented"); @@ -82,12 +164,15 @@ export class UdpSocketImpl { * @returns {IpAddressFamily} */ addressFamily() { - throw new Error("Not implemented"); + console.log(`[tcp] address family socket`); + + return this.#socketOptions.family; } /** * * @returns {boolean} + * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { throw new Error("Not implemented"); @@ -97,6 +182,9 @@ export class UdpSocketImpl { * * @param {boolean} value * @returns {void} + * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. + * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) + * @throws {invalid-state} (set) The socket is already bound. */ setIpv6Only(value) { throw new Error("Not implemented"); diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/sockets/wasi-sockets.js index 976db17fa..1d124be56 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/sockets/wasi-sockets.js @@ -3,6 +3,7 @@ * @typedef {import("../../types/interfaces/wasi-sockets-network").ErrorCode} ErrorCode * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily * @typedef {import("../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket + * @typedef {import("../../types/interfaces/wasi-sockets-udp").UdpSocket} UdpSocket */ import { TcpSocketImpl } from "./tcp-socket-impl.js"; @@ -117,6 +118,7 @@ export class WasiSockets { /** @type {Network} */ networkInstance = null; /** @type {Map} */ networks = new Map(); /** @type {Map { ok(responseBody.includes("WebAssembly")); }); - suite("Sockets", async () => { + suite("Sockets::TCP", async () => { test("sockets.instanceNetwork() should be a singleton", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network1 = sockets.instanceNetwork.instanceNetwork(); @@ -222,7 +222,7 @@ suite("Node.js Preview2", () => { const localAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { - address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], + address: [0, 0, 0, 0, 0, 0, 0, 0], port: 0, }, }; @@ -234,7 +234,7 @@ suite("Node.js Preview2", () => { deepEqual(tcpSocket.localAddress(), { tag: sockets.network.IpAddressFamily.ipv6, val: { - address: [0, 0, 0, 0, 0, 0xffff, 0xc0a8, 0x0001], + address: [0, 0, 0, 0, 0, 0, 0, 0], port: 0, }, }); @@ -364,4 +364,50 @@ suite("Node.js Preview2", () => { }); }); }); + + suite("Sockets::UDP", async () => { + test("sockets.udpCreateSocket()", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const socket = sockets.udpCreateSocket.createUdpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + notEqual(socket, null); + + throws( + () => { + sockets.udpCreateSocket.createUdpSocket("xyz"); + }, + { + name: "Error", + code: sockets.network.errorCode.notSupported, + } + ); + }); + test("udp.bind(): should bind to a valid ipv4 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const socket = sockets.udpCreateSocket.createUdpSocket( + sockets.network.IpAddressFamily.ipv4 + ); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + socket.startBind(network, localAddress); + socket.finishBind(); + + equal(socket.network.id, network.id); + deepEqual(socket.localAddress(), { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }); + equal(socket.addressFamily(), "ipv4"); + }); + }); }); diff --git a/test/preview2-wasi-sockets.js b/test/preview2-wasi-sockets.js index 4dd406c38..ac42dd20c 100644 --- a/test/preview2-wasi-sockets.js +++ b/test/preview2-wasi-sockets.js @@ -31,7 +31,7 @@ client.finishConnect(); setTimeout(() => { // const [socket, input, output] = server.accept(); - // output.write(new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f])); + // output.write('hello world'); // const buff = input.read(2); // console.log(`[wasi-sockets] Server received: ${buff}`); From 46be9ead1c065ebcd164b7f32e2eb52e0ecd3ef9 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 3 Nov 2023 22:09:23 +0100 Subject: [PATCH 33/65] chore: wip --- .../lib/sockets/tcp-socket-impl.js | 9 +- .../lib/sockets/udp-socket-impl.js | 29 ++++++- packages/preview2-shim/test/test.js | 84 +++++++++++-------- 3 files changed, 84 insertions(+), 38 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index a0321afbd..380e62939 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -13,9 +13,10 @@ import { streams } from "../common/io.js"; const { InputStream, OutputStream } = streams; - import { assert } from "../common/assert.js"; +const symbolDispose = Symbol.dispose || Symbol.for('dispose'); + // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc const { TCP, @@ -577,6 +578,12 @@ export class TcpSocketImpl { assert(err === 1, "invalid-state"); } + [symbolDispose] () { + console.log(`[tcp] dispose socket`); + this.#serverHandle.close(); + this.#clientHandle.close(); + } + server() { return this.#serverHandle; } diff --git a/packages/preview2-shim/lib/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/sockets/udp-socket-impl.js index 8899509a2..a6fd206a7 100644 --- a/packages/preview2-shim/lib/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/udp-socket-impl.js @@ -17,7 +17,6 @@ const { UDP, constants: UDPConstants } = process.binding("udp_wrap"); export class UdpSocketImpl { /** @type {Socket} */ #clientHandle = null; /** @type {Socket} */ #serverHandle = null; - /** @type {Network} */ network = null; #isBound = false; @@ -25,7 +24,7 @@ export class UdpSocketImpl { /** * @param {IpAddressFamily} addressFamily - * @returns + * @returns {void} */ constructor(addressFamily) { this.#socketOptions.family = addressFamily; @@ -87,7 +86,15 @@ export class UdpSocketImpl { * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ startConnect(network, remoteAddress) { - throw new Error("Not implemented"); + const address = serializeIpAddress( + remoteAddress, + this.#socketOptions.family + ); + + const { port } = remoteAddress.val; + this.#socketOptions.remoteAddress = address; + this.#socketOptions.remotePort = port; + this.network = network; } /** @@ -98,7 +105,8 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishConnect() { - throw new Error("Not implemented"); + const { remoteAddress, remotePort } = this.#socketOptions; + this.#clientHandle.connect(remoteAddress, remotePort); } /** @@ -246,4 +254,17 @@ export class UdpSocketImpl { subscribe() { throw new Error("Not implemented"); } + + [Symbol.dispose] () { + console.log(`[udp] dispose socket`); + this.#clientHandle.close(); + this.#serverHandle.close(); + } + + client() { + return this.#clientHandle; + } + server() { + return this.#serverHandle; + } } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index db5486e06..f2725f5b0 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -6,12 +6,8 @@ suite("Node.js Preview2", () => { test("Stdio", async () => { const { cli } = await import("@bytecodealliance/preview2-shim"); // todo: wrap in a process call to not spill to test output - cli.stdout - .getStdout() - .blockingWriteAndFlush(new TextEncoder().encode("test stdout")); - cli.stderr - .getStderr() - .blockingWriteAndFlush(new TextEncoder().encode("test stderr")); + cli.stdout.getStdout().blockingWriteAndFlush(new TextEncoder().encode("test stdout")); + cli.stderr.getStderr().blockingWriteAndFlush(new TextEncoder().encode("test stderr")); }); suite("Clocks", () => { @@ -171,9 +167,7 @@ suite("Node.js Preview2", () => { test("sockets.tcpCreateSocket()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - const socket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const socket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); notEqual(socket, null); throws( @@ -189,9 +183,7 @@ suite("Node.js Preview2", () => { test("tcp.bind(): should bind to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -216,9 +208,7 @@ suite("Node.js Preview2", () => { test("tcp.bind(): should bind to a valid ipv6 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv6 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv6); const localAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { @@ -244,9 +234,7 @@ suite("Node.js Preview2", () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { // invalid address family tag: sockets.network.IpAddressFamily.ipv6, @@ -269,9 +257,7 @@ suite("Node.js Preview2", () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -295,9 +281,7 @@ suite("Node.js Preview2", () => { test("tcp.listen(): should listen to an ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -323,9 +307,7 @@ suite("Node.js Preview2", () => { test("tcp.connect(): should connect to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const tcpSocket = sockets.tcpCreateSocket.createTcpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, @@ -368,9 +350,7 @@ suite("Node.js Preview2", () => { suite("Sockets::UDP", async () => { test("sockets.udpCreateSocket()", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - const socket = sockets.udpCreateSocket.createUdpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); notEqual(socket, null); throws( @@ -386,9 +366,7 @@ suite("Node.js Preview2", () => { test("udp.bind(): should bind to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); - const socket = sockets.udpCreateSocket.createUdpSocket( - sockets.network.IpAddressFamily.ipv4 - ); + const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); const localAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -409,5 +387,45 @@ suite("Node.js Preview2", () => { }); equal(socket.addressFamily(), "ipv4"); }); + test("udp.connect(): should connect to a valid ipv4 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }; + const remoteAddress = { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [192, 168, 0, 1], + port: 80, + }, + }; + + mock.method(socket.client(), "connect", () => { + console.log("mock connect called"); + }); + + socket.startBind(network, localAddress); + socket.finishBind(); + socket.startConnect(network, remoteAddress); + socket.finishConnect(); + + strictEqual(socket.client().connect.mock.calls.length, 1); + + equal(socket.network.id, network.id); + equal(socket.addressFamily(), "ipv4"); + deepEqual(socket.localAddress(), { + tag: sockets.network.IpAddressFamily.ipv4, + val: { + address: [0, 0, 0, 0], + port: 0, + }, + }); + }); }); }); From 701a54650155026333d6c5d126f4da97383d5d47 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 7 Nov 2023 16:35:20 +0100 Subject: [PATCH 34/65] feat(udp): wip impl --- .../lib/sockets/tcp-socket-impl.js | 17 +- .../lib/sockets/udp-socket-impl.js | 170 +++++++++++++++--- ...ockets.js => preview2-wasi-sockets-tcp.js} | 2 +- test/preview2-wasi-sockets-udp.js | 46 +++++ 4 files changed, 198 insertions(+), 37 deletions(-) rename test/{preview2-wasi-sockets.js => preview2-wasi-sockets-tcp.js} (93%) create mode 100644 test/preview2-wasi-sockets-udp.js diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js index 380e62939..f9c69fca1 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/tcp-socket-impl.js @@ -36,6 +36,7 @@ const ShutdownType = { const SocketState = { Error: "Error", + Closed: "Closed", Connection: "Connection", Listener: "Listener", }; @@ -52,7 +53,7 @@ export class TcpSocketImpl { #canReceive = true; #canSend = true; #ipv6Only = false; - #state = "closed"; + #state = SocketState.Closed; #inProgress = false; #connections = 0; #keepAlive = false; @@ -187,7 +188,6 @@ export class TcpSocketImpl { if (family.toLocaleLowerCase() === "ipv4") { err = this.#serverHandle.bind(localAddress, localPort); } else if (family.toLocaleLowerCase() === "ipv6") { - console.log(`[tcp] xxxx bind socket to ${localAddress}:${localPort}`); err = this.#serverHandle.bind6(localAddress, localPort); } @@ -242,10 +242,10 @@ export class TcpSocketImpl { "address-family-mismatch" ); - this.network = network; this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; - + + this.network = network; this.#inProgress = true; } @@ -275,7 +275,7 @@ export class TcpSocketImpl { } else if (family.toLocaleLowerCase() === "ipv6") { err = this.#clientHandle.connect6(connectReq, remoteAddress, remotePort); } - + if (err) { console.error(`[tcp] connect error on socket: ${err}`); this.#state = SocketState.Error; @@ -349,16 +349,17 @@ export class TcpSocketImpl { accept() { // uv_accept is automatically called by uv_listen when a new connection is received. - const _this = this; + const self = this; const outgoingStream = new OutputStream({ write(bytes) { - _this.#acceptedClient.write(bytes); + console.log(`[tcp] write socket`); + self.#acceptedClient.write(bytes); }, }); const ingoingStream = new InputStream({ read(len) { console.log(`[tcp] read socket`); - return _this.#acceptedClient.read(len); + return self.#acceptedClient.read(len); }, }) diff --git a/packages/preview2-shim/lib/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/sockets/udp-socket-impl.js index a6fd206a7..6abf2d21d 100644 --- a/packages/preview2-shim/lib/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/udp-socket-impl.js @@ -1,5 +1,6 @@ /* eslint-disable no-unused-vars */ +import { isIP } from "node:net"; import { assert } from "../common/assert.js"; import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; @@ -12,14 +13,23 @@ import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; */ // See: https://github.com/nodejs/node/blob/main/src/udp_wrap.cc -const { UDP, constants: UDPConstants } = process.binding("udp_wrap"); +const { UDP, SendWrap, constants: UDPConstants } = process.binding("udp_wrap"); + +const SocketState = { + Error: "Error", + Closed: "Closed", + Connection: "Connection", + Listener: "Listener", +}; export class UdpSocketImpl { - /** @type {Socket} */ #clientHandle = null; - /** @type {Socket} */ #serverHandle = null; + /** @type {Socket} */ #socket = null; /** @type {Network} */ network = null; #isBound = false; + #inProgress = false; + #ipv6Only = false; + #state = SocketState.Closed; #socketOptions = {}; /** @@ -29,8 +39,7 @@ export class UdpSocketImpl { constructor(addressFamily) { this.#socketOptions.family = addressFamily; - this.#clientHandle = new UDP(UDPConstants.SOCKET); - this.#serverHandle = new UDP(UDPConstants.SERVER); + this.#socket = new UDP(); } /** * @@ -41,15 +50,23 @@ export class UdpSocketImpl { * @returns {void} */ startBind(network, localAddress) { - const address = serializeIpAddress( - localAddress, - this.#socketOptions.family + const address = serializeIpAddress(localAddress, this.#socketOptions.family); + const ipFamily = `ipv${isIP(address)}`; + + console.log(`[tcp] start bind socket to ${address}:${localAddress.val.port}`); + + assert(this.#isBound, "invalid-state", "The socket is already bound"); + assert( + this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), + "invalid-argument", + "The `local-address` has the wrong address family" ); const { port } = localAddress.val; this.#socketOptions.localAddress = address; this.#socketOptions.localPort = port; this.network = network; + this.#inProgress = true; } /** @@ -62,13 +79,26 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { + console.log(`[udp] finish bind socket`); + + assert(this.#inProgress === false, "not-in-progress"); + const { localAddress, localPort, family } = this.#socketOptions; // TODO: see https://github.com/libuv/libuv/blob/93efccf4ee1ed3c740d10660b4bfb08edc68a1b5/src/unix/udp.c#L486 const flags = 0; - let err = this.#serverHandle.bind(localAddress, localPort, flags); + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = this.#socket.bind(localAddress, localPort, flags); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = this.#socket.bind6(localAddress, localPort, flags); + } + if (err) { - throw new Error(err); + assert(err === -22, "address-in-use"); + assert(err === -49, "address-not-bindable"); + assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL + assert(true, "", err); } this.#isBound = true; @@ -86,15 +116,21 @@ export class UdpSocketImpl { * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ startConnect(network, remoteAddress) { - const address = serializeIpAddress( - remoteAddress, - this.#socketOptions.family - ); + console.log(`[udp] start connect socket`); + + const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); + const ipFamily = `ipv${isIP(host)}`; + + assert(this.network !== null && this.network.id !== network.id, "invalid-argument"); + assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); + assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); const { port } = remoteAddress.val; - this.#socketOptions.remoteAddress = address; + this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = port; + this.network = network; + this.#inProgress = true; } /** @@ -105,8 +141,10 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishConnect() { + console.log(`[udp] finish connect socket`); + const { remoteAddress, remotePort } = this.#socketOptions; - this.#clientHandle.connect(remoteAddress, remotePort); + this.#socket.connect(remoteAddress, remotePort); } /** @@ -117,7 +155,20 @@ export class UdpSocketImpl { * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) */ receive(maxResults) { - throw new Error("Not implemented"); + console.log(`[udp] receive socket`); + + assert(this.#isBound === false, "invalid-state"); + assert(this.#inProgress === false, "not-in-progress"); + + if (maxResults === 0n) { + return []; + } + + this.#socket.onmessage = (...args) => console.log('recv onmessage', args[2].toString()); + this.#socket.onerror = (err) => console.log('recv error', err); + this.#socket.recvStart(); + const datagrams = []; + return datagrams; } /** @@ -135,7 +186,34 @@ export class UdpSocketImpl { * @throws {would-block} The send buffer is currently full. (EWOULDBLOCK, EAGAIN) */ send(datagrams) { - throw new Error("Not implemented"); + console.log(`[udp] send socket`); + + const req = new SendWrap(); + const doSend = (data, port, host, family) => { + console.log(`[udp] send socket to ${host}:${port}`); + + // setting hasCallback to false will make send() synchronous + // TODO: handle async send + const hasCallback = false; + + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = this.#socket.send(req, data, data.length, port, host, hasCallback); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = this.#socket.send6(req, data, data.length, port, host, hasCallback); + } + return err; + }; + + datagrams.forEach((datagram) => { + const { data, remoteAddress } = datagram; + const { tag: family, val } = remoteAddress; + const { address, port } = val; + const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); + console.error({ + err, + }); + }); } /** @@ -164,7 +242,11 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - throw new Error("Not implemented"); + console.log(`[udp] remote address socket`); + + assert(this.#state !== SocketState.Connection, "invalid-state"); + + return this.#socketOptions.remoteAddress; } /** @@ -172,7 +254,7 @@ export class UdpSocketImpl { * @returns {IpAddressFamily} */ addressFamily() { - console.log(`[tcp] address family socket`); + console.log(`[udp] address family socket`); return this.#socketOptions.family; } @@ -183,7 +265,9 @@ export class UdpSocketImpl { * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { - throw new Error("Not implemented"); + console.log(`[udp] ipv6 only socket`); + + return this.#ipv6Only; } /** @@ -195,14 +279,20 @@ export class UdpSocketImpl { * @throws {invalid-state} (set) The socket is already bound. */ setIpv6Only(value) { - throw new Error("Not implemented"); + console.log(`[udp] set ipv6 only socket to ${value}`); + + this.#ipv6Only = value; } /** * * @returns {number} */ - unicastHopLimit() {} + unicastHopLimit() { + console.log(`[udp] unicast hop limit socket`); + + throw new Error("Not implemented"); + } /** * @@ -210,6 +300,8 @@ export class UdpSocketImpl { * @returns {void} */ setUnicastHopLimit(value) { + console.log(`[udp] set unicast hop limit socket`); + throw new Error("Not implemented"); } @@ -218,6 +310,8 @@ export class UdpSocketImpl { * @returns {bigint} */ receiveBufferSize() { + console.log(`[udp] receive buffer size socket`); + throw new Error("Not implemented"); } @@ -227,6 +321,8 @@ export class UdpSocketImpl { * @returns {void} */ setReceiveBufferSize(value) { + console.log(`[udp] set receive buffer size socket`); + throw new Error("Not implemented"); } @@ -235,6 +331,8 @@ export class UdpSocketImpl { * @returns {bigint} */ sendBufferSize() { + console.log(`[udp] send buffer size socket`); + throw new Error("Not implemented"); } @@ -244,6 +342,8 @@ export class UdpSocketImpl { * @returns {void} */ setSendBufferSize(value) { + console.log(`[udp] set send buffer size socket`); + throw new Error("Not implemented"); } @@ -252,19 +352,33 @@ export class UdpSocketImpl { * @returns {Pollable} */ subscribe() { + console.log(`[udp] subscribe socket`); + throw new Error("Not implemented"); } - [Symbol.dispose] () { + [Symbol.dispose]() { console.log(`[udp] dispose socket`); - this.#clientHandle.close(); - this.#serverHandle.close(); + + let err = null; + err = this.#socket.recvStop((...args) => { + console.log("stop recv", args); + }); + + if (err) { + assert(err === -9, "invalid-state", "Interface is not currently Up"); + assert(err === -11, "not-in-progress"); + assert(true, "", err); + } + + this.#socket.close(); + this.#socket.close(); } client() { - return this.#clientHandle; + return this.#socket; } server() { - return this.#serverHandle; + return this.#socket; } } diff --git a/test/preview2-wasi-sockets.js b/test/preview2-wasi-sockets-tcp.js similarity index 93% rename from test/preview2-wasi-sockets.js rename to test/preview2-wasi-sockets-tcp.js index ac42dd20c..c3a6d598a 100644 --- a/test/preview2-wasi-sockets.js +++ b/test/preview2-wasi-sockets-tcp.js @@ -17,7 +17,7 @@ server.finishBind(); server.startListen(); server.finishListen(); const { address, port } = server.localAddress().val; -console.log(`[wasi-sockets] Server listening on: ${address}:${port}`); +console.log(`[wasi-sockets-tcp] Server listening on: ${address}:${port}`); // client const client = sockets.tcpCreateSocket.createTcpSocket( diff --git a/test/preview2-wasi-sockets-udp.js b/test/preview2-wasi-sockets-udp.js new file mode 100644 index 000000000..66318e84d --- /dev/null +++ b/test/preview2-wasi-sockets-udp.js @@ -0,0 +1,46 @@ +const { sockets } = await import("@bytecodealliance/preview2-shim"); +const network = sockets.instanceNetwork.instanceNetwork(); + +// server +const serverAddressIpv6 = { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0, 0, 0x1], + port: 3000, + }, +}; +const server = sockets.udpCreateSocket.createUdpSocket( + sockets.network.IpAddressFamily.ipv6 +); +server.startBind(network, serverAddressIpv6); +server.finishBind(); +const { address, port } = server.localAddress().val; +console.log(`[wasi-sockets-udp] Server listening on: ${address}:${port}`); + +// client +const client = sockets.udpCreateSocket.createUdpSocket( + sockets.network.IpAddressFamily.ipv6 +); + +client.startConnect(network, serverAddressIpv6); +client.finishConnect(); + +setTimeout(() => { + client.send([ + { + data: [Buffer.from('hello world')], + remoteAddress: serverAddressIpv6, + } + ]); + + const data = server.receive(); + console.log(`[wasi-sockets-udp] Server received`); + console.log({ + data + }); +}, 2000); + +setTimeout(() => { + server[Symbol.dispose](); + client[Symbol.dispose](); +}, 5000); \ No newline at end of file From 417e6d9dfe4768758431c7eee677015b3b424647 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 9 Nov 2023 09:47:35 +0100 Subject: [PATCH 35/65] chore: clean imports --- packages/preview2-shim/lib/sockets/udp-socket-impl.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/preview2-shim/lib/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/sockets/udp-socket-impl.js index 6abf2d21d..037c6f0b4 100644 --- a/packages/preview2-shim/lib/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/sockets/udp-socket-impl.js @@ -1,19 +1,18 @@ /* eslint-disable no-unused-vars */ -import { isIP } from "node:net"; -import { assert } from "../common/assert.js"; -import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; - /** * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily * @typedef {import("../../types/interfaces/wasi-sockets-udp").Datagram} Datagram * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable - */ +*/ // See: https://github.com/nodejs/node/blob/main/src/udp_wrap.cc -const { UDP, SendWrap, constants: UDPConstants } = process.binding("udp_wrap"); +const { UDP, SendWrap } = process.binding("udp_wrap"); +import { isIP } from "node:net"; +import { assert } from "../common/assert.js"; +import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; const SocketState = { Error: "Error", From 48f98cba3ba75016e5df600c0b248110ad7e5685 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 15 Nov 2023 10:21:50 +0100 Subject: [PATCH 36/65] chore: use new folder structure --- packages/preview2-shim/lib/nodejs/index.js | 2 +- packages/preview2-shim/lib/nodejs/sockets.js | 2 +- .../lib/{ => nodejs}/sockets/socket-common.js | 0 .../{ => nodejs}/sockets/tcp-socket-impl.js | 18 +++++++++--------- .../{ => nodejs}/sockets/udp-socket-impl.js | 2 +- .../lib/{ => nodejs}/sockets/wasi-sockets.js | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) rename packages/preview2-shim/lib/{ => nodejs}/sockets/socket-common.js (100%) rename packages/preview2-shim/lib/{ => nodejs}/sockets/tcp-socket-impl.js (96%) rename packages/preview2-shim/lib/{ => nodejs}/sockets/udp-socket-impl.js (99%) rename packages/preview2-shim/lib/{ => nodejs}/sockets/wasi-sockets.js (99%) diff --git a/packages/preview2-shim/lib/nodejs/index.js b/packages/preview2-shim/lib/nodejs/index.js index f53a8e4f8..8b00fa92b 100644 --- a/packages/preview2-shim/lib/nodejs/index.js +++ b/packages/preview2-shim/lib/nodejs/index.js @@ -16,5 +16,5 @@ export { cli } -export { WasiSockets } from "../sockets/wasi-sockets.js"; +export { WasiSockets } from "./sockets/wasi-sockets.js"; diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index a4a894915..562578fa5 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ -import { WasiSockets } from "../sockets/wasi-sockets.js"; +import { WasiSockets } from "./sockets/wasi-sockets.js"; const sockets = new WasiSockets(); export const { diff --git a/packages/preview2-shim/lib/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js similarity index 100% rename from packages/preview2-shim/lib/sockets/socket-common.js rename to packages/preview2-shim/lib/nodejs/sockets/socket-common.js diff --git a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js similarity index 96% rename from packages/preview2-shim/lib/sockets/tcp-socket-impl.js rename to packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index f9c69fca1..455a585d9 100644 --- a/packages/preview2-shim/lib/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -1,19 +1,19 @@ /* eslint-disable no-unused-vars */ /** - * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network - * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress - * @typedef {import("../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket - * @typedef {import("../../types/interfaces/wasi-sockets-tcp").InputStream} InputStream - * @typedef {import("../../types/interfaces/wasi-sockets-tcp").OutputStream} OutputStream - * @typedef {import("../../types/interfaces/wasi-sockets-tcp").IpAddressFamily} IpAddressFamily + * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").Network} Network + * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress + * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").TcpSocket} TcpSocket + * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").InputStream} InputStream + * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").OutputStream} OutputStream + * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable - * @typedef {import("../../types/interfaces/wasi-sockets-tcp").ShutdownType} ShutdownType + * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").ShutdownType} ShutdownType */ -import { streams } from "../common/io.js"; +import { streams } from "../io.js"; const { InputStream, OutputStream } = streams; -import { assert } from "../common/assert.js"; +import { assert } from "../../common/assert.js"; const symbolDispose = Symbol.dispose || Symbol.for('dispose'); diff --git a/packages/preview2-shim/lib/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js similarity index 99% rename from packages/preview2-shim/lib/sockets/udp-socket-impl.js rename to packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 037c6f0b4..3760a622e 100644 --- a/packages/preview2-shim/lib/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -11,7 +11,7 @@ // See: https://github.com/nodejs/node/blob/main/src/udp_wrap.cc const { UDP, SendWrap } = process.binding("udp_wrap"); import { isIP } from "node:net"; -import { assert } from "../common/assert.js"; +import { assert } from "../../common/assert.js"; import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; const SocketState = { diff --git a/packages/preview2-shim/lib/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js similarity index 99% rename from packages/preview2-shim/lib/sockets/wasi-sockets.js rename to packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 1d124be56..88c5e60d8 100644 --- a/packages/preview2-shim/lib/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -8,7 +8,7 @@ import { TcpSocketImpl } from "./tcp-socket-impl.js"; import { UdpSocketImpl } from "./udp-socket-impl.js"; -import { assert } from "../common/assert.js"; +import { assert } from "../../common/assert.js"; /** @type {ErrorCode} */ export const errorCode = { From e73db1122dbb697aa50fde44e935f71c87a5d58b Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 15 Nov 2023 10:59:31 +0100 Subject: [PATCH 37/65] chore(udp): use Symbols for local state --- .../lib/nodejs/sockets/udp-socket-impl.js | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 3760a622e..48dba7cf0 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -6,7 +6,7 @@ * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily * @typedef {import("../../types/interfaces/wasi-sockets-udp").Datagram} Datagram * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable -*/ + */ // See: https://github.com/nodejs/node/blob/main/src/udp_wrap.cc const { UDP, SendWrap } = process.binding("udp_wrap"); @@ -21,14 +21,19 @@ const SocketState = { Listener: "Listener", }; +const symbolState = Symbol("state"); + export class UdpSocketImpl { /** @type {Socket} */ #socket = null; /** @type {Network} */ network = null; - #isBound = false; - #inProgress = false; - #ipv6Only = false; - #state = SocketState.Closed; + [symbolState] = { + isBound: false, + inProgress: false, + ipv6Only: false, + state: SocketState.Closed, + }; + #socketOptions = {}; /** @@ -54,7 +59,7 @@ export class UdpSocketImpl { console.log(`[tcp] start bind socket to ${address}:${localAddress.val.port}`); - assert(this.#isBound, "invalid-state", "The socket is already bound"); + assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); assert( this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument", @@ -65,7 +70,7 @@ export class UdpSocketImpl { this.#socketOptions.localAddress = address; this.#socketOptions.localPort = port; this.network = network; - this.#inProgress = true; + this[symbolState].inProgress = true; } /** @@ -80,7 +85,7 @@ export class UdpSocketImpl { finishBind() { console.log(`[udp] finish bind socket`); - assert(this.#inProgress === false, "not-in-progress"); + assert(this[symbolState].inProgress === false, "not-in-progress"); const { localAddress, localPort, family } = this.#socketOptions; @@ -100,7 +105,7 @@ export class UdpSocketImpl { assert(true, "", err); } - this.#isBound = true; + this[symbolState].isBound = true; } /** @@ -129,7 +134,7 @@ export class UdpSocketImpl { this.#socketOptions.remotePort = port; this.network = network; - this.#inProgress = true; + this[symbolState].inProgress = true; } /** @@ -156,15 +161,15 @@ export class UdpSocketImpl { receive(maxResults) { console.log(`[udp] receive socket`); - assert(this.#isBound === false, "invalid-state"); - assert(this.#inProgress === false, "not-in-progress"); + assert(this[symbolState].isBound === false, "invalid-state"); + assert(this[symbolState].inProgress === false, "not-in-progress"); if (maxResults === 0n) { return []; } - this.#socket.onmessage = (...args) => console.log('recv onmessage', args[2].toString()); - this.#socket.onerror = (err) => console.log('recv error', err); + this.#socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); + this.#socket.onerror = (err) => console.log("recv error", err); this.#socket.recvStart(); const datagrams = []; return datagrams; @@ -223,7 +228,7 @@ export class UdpSocketImpl { localAddress() { console.log(`[udp] local address socket`); - assert(this.#isBound === false, "invalid-state"); + assert(this[symbolState].isBound === false, "invalid-state"); const { localAddress, localPort, family } = this.#socketOptions; return { @@ -243,7 +248,7 @@ export class UdpSocketImpl { remoteAddress() { console.log(`[udp] remote address socket`); - assert(this.#state !== SocketState.Connection, "invalid-state"); + assert(this[symbolState].state !== SocketState.Connection, "invalid-state"); return this.#socketOptions.remoteAddress; } @@ -266,7 +271,7 @@ export class UdpSocketImpl { ipv6Only() { console.log(`[udp] ipv6 only socket`); - return this.#ipv6Only; + return this[symbolState].ipv6Only; } /** @@ -280,7 +285,7 @@ export class UdpSocketImpl { setIpv6Only(value) { console.log(`[udp] set ipv6 only socket to ${value}`); - this.#ipv6Only = value; + this[symbolState].ipv6Only = value; } /** From a051016c0b3e9ff1f516bc6a8b2e92dcae3f92d9 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 15 Nov 2023 12:55:26 +0100 Subject: [PATCH 38/65] chore: sync impl with lastest specs --- .../lib/nodejs/sockets/tcp-socket-impl.js | 149 ++++++++---- .../lib/nodejs/sockets/udp-socket-impl.js | 213 ++++++++++++------ .../lib/nodejs/sockets/wasi-sockets.js | 10 +- packages/preview2-shim/test/test.js | 5 +- 4 files changed, 259 insertions(+), 118 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 455a585d9..700f8141b 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -7,22 +7,19 @@ * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").InputStream} InputStream * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").OutputStream} OutputStream * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").IpAddressFamily} IpAddressFamily - * @typedef {import("../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable + * @typedef {import("../../../types/interfaces/wasi-io-poll-poll").Pollable} Pollable * @typedef {import("../../../types/interfaces/wasi-sockets-tcp.js").ShutdownType} ShutdownType + * @typedef {import("../../../types/interfaces/wasi-clocks-monotonic-clock.js").Duration} Duration */ import { streams } from "../io.js"; const { InputStream, OutputStream } = streams; import { assert } from "../../common/assert.js"; -const symbolDispose = Symbol.dispose || Symbol.for('dispose'); +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc -const { - TCP, - TCPConnectWrap, - constants: TCPConstants, -} = process.binding("tcp_wrap"); +const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap"); const { ShutdownWrap } = process.binding("stream_wrap"); import { isIP, Socket as NodeSocket } from "node:net"; @@ -57,6 +54,9 @@ export class TcpSocketImpl { #inProgress = false; #connections = 0; #keepAlive = false; + #keepAliveCount = 1; + #keepAliveIdleTime = 1; + #keepAliveInterval = 1; #noDelay = false; #unicastHopLimit = 10; #acceptedClient = null; @@ -69,7 +69,6 @@ export class TcpSocketImpl { * @returns */ constructor(addressFamily) { - this.#socketOptions.family = addressFamily; this.#clientHandle = new TCP(TCPConstants.SOCKET); @@ -136,20 +135,14 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already bound. (EINVAL) */ startBind(network, localAddress) { - const address = serializeIpAddress( - localAddress, - this.#socketOptions.family - ); + const address = serializeIpAddress(localAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(address)}`; - console.log( - `[tcp] start bind socket to ${address}:${localAddress.val.port}` - ); + console.log(`[tcp] start bind socket to ${address}:${localAddress.val.port}`); assert(this.#isBound, "invalid-state", "The socket is already bound"); assert( - this.#socketOptions.family.toLocaleLowerCase() !== - ipFamily.toLocaleLowerCase(), + this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument", "The `local-address` has the wrong address family" ); @@ -183,7 +176,7 @@ export class TcpSocketImpl { const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); - + let err = null; if (family.toLocaleLowerCase() === "ipv4") { err = this.#serverHandle.bind(localAddress, localPort); @@ -220,14 +213,9 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) */ startConnect(network, remoteAddress) { - console.log( - `[tcp] start connect socket to ${remoteAddress.val.address}:${remoteAddress.val.port}` - ); + console.log(`[tcp] start connect socket to ${remoteAddress.val.address}:${remoteAddress.val.port}`); - assert( - this.network !== null && this.network.id !== network.id, - "already-attached" - ); + assert(this.network !== null && this.network.id !== network.id, "already-attached"); assert(this.#state === "connected", "already-connected"); assert(this.#state === "connection", "already-listening"); assert(this.#inProgress, "concurrency-conflict"); @@ -236,15 +224,11 @@ export class TcpSocketImpl { const ipFamily = `ipv${isIP(host)}`; assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-remote-address"); - assert( - this.#socketOptions.family.toLocaleLowerCase() !== - ipFamily.toLocaleLowerCase(), - "address-family-mismatch" - ); + assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "address-family-mismatch"); this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; - + this.network = network; this.#inProgress = true; } @@ -265,8 +249,7 @@ export class TcpSocketImpl { assert(this.#inProgress === false, "not-in-progress"); - const { localAddress, localPort, remoteAddress, remotePort, family } = - this.#socketOptions; + const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; const connectReq = new TCPConnectWrap(); let err = null; @@ -275,7 +258,7 @@ export class TcpSocketImpl { } else if (family.toLocaleLowerCase() === "ipv6") { err = this.#clientHandle.connect6(connectReq, remoteAddress, remotePort); } - + if (err) { console.error(`[tcp] connect error on socket: ${err}`); this.#state = SocketState.Error; @@ -305,11 +288,7 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. */ startListen() { - console.log( - `[tcp] start listen socket on ${this.#socketOptions.localAddress}:${ - this.#socketOptions.localPort - }` - ); + console.log(`[tcp] start listen socket on ${this.#socketOptions.localAddress}:${this.#socketOptions.localPort}`); assert(this.#isBound === false, "invalid-state"); assert(this.#state === SocketState.Connection, "invalid-state"); @@ -361,7 +340,7 @@ export class TcpSocketImpl { console.log(`[tcp] read socket`); return self.#acceptedClient.read(len); }, - }) + }); return [this.#acceptedClient, ingoingStream, outgoingStream]; } @@ -397,6 +376,12 @@ export class TcpSocketImpl { return this.#socketOptions.remoteAddress; } + isListening() { + console.log(`[tcp] is listening socket`); + + return this.#state === SocketState.Listener; + } + /** * @returns {IpAddressFamily} */ @@ -444,7 +429,7 @@ export class TcpSocketImpl { /** * @returns {boolean} */ - keepAlive() { + keepAliveEnabled() { console.log(`[tcp] keep alive socket`); this.#keepAlive; @@ -454,11 +439,89 @@ export class TcpSocketImpl { * @param {boolean} value * @returns {void} */ - setKeepAlive(value) { + setKeepAliveEnabled(value) { console.log(`[tcp] set keep alive socket to ${value}`); this.#keepAlive = value; this.#clientHandle.setKeepAlive(value); + + if (value) { + this.#clientHandle.setKeepAliveIdleTime(this.keepAliveIdleTime()); + this.#clientHandle.setKeepAliveInterval(this.keepAliveInterval()); + this.#clientHandle.setKeepAliveCount(this.keepAliveCount()); + } + } + + /** + * + * @returns {Duration} + */ + keepAliveIdleTime() { + console.log(`[tcp] keep alive idle time socket`); + + return this.#keepAliveIdleTime; + } + + /** + * + * @param {Duration} value + * @returns {void} + * @throws {invalid-argument} (set) The idle time must be 1 or higher. + */ + setKeepAliveIdleTime(value) { + console.log(`[tcp] set keep alive idle time socket to ${value}`); + + assert(value < 1, "invalid-argument", "The idle time must be 1 or higher."); + + this.#keepAliveIdleTime = value; + } + + /** + * + * @returns {Duration} + */ + keepAliveInterval() { + console.log(`[tcp] keep alive interval socket`); + + return this.#keepAliveInterval; + } + + /** + * + * @param {Duration} value + * @returns {void} + * @throws {invalid-argument} (set) The interval must be 1 or higher. + */ + setKeepAliveInterval(value) { + console.log(`[tcp] set keep alive interval socket to ${value}`); + + assert(value < 1, "invalid-argument", "The interval must be 1 or higher."); + + this.#keepAliveInterval = value; + } + + /** + * + * @returns {Duration} + */ + keepAliveCount() { + console.log(`[tcp] keep alive count socket`); + + return this.#keepAliveCount; + } + + /** + * + * @param {Duration} value + * @returns {void} + * @throws {invalid-argument} (set) The count must be 1 or higher. + */ + setKeepAliveCount(value) { + console.log(`[tcp] set keep alive count socket to ${value}`); + + assert(value < 1, "invalid-argument", "The count must be 1 or higher."); + + this.#keepAliveCount = value; } /** @@ -579,7 +642,7 @@ export class TcpSocketImpl { assert(err === 1, "invalid-state"); } - [symbolDispose] () { + [symbolDispose]() { console.log(`[tcp] dispose socket`); this.#serverHandle.close(); this.#clientHandle.close(); diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 48dba7cf0..4cff41fad 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -89,7 +89,8 @@ export class UdpSocketImpl { const { localAddress, localPort, family } = this.#socketOptions; - // TODO: see https://github.com/libuv/libuv/blob/93efccf4ee1ed3c740d10660b4bfb08edc68a1b5/src/unix/udp.c#L486 + // TODO: pass the right flags + // see https://github.com/libuv/libuv/blob/93efccf4ee1ed3c740d10660b4bfb08edc68a1b5/src/unix/udp.c#L486 const flags = 0; let err = null; if (family.toLocaleLowerCase() === "ipv4") { @@ -119,7 +120,7 @@ export class UdpSocketImpl { * @throws {invalid-argument} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ - startConnect(network, remoteAddress) { + #startConnect(network, remoteAddress) { console.log(`[udp] start connect socket`); const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); @@ -144,80 +145,32 @@ export class UdpSocketImpl { * @throws {not-in-progress} A `connect` operation is not in progress. * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ - finishConnect() { + #finishConnect() { console.log(`[udp] finish connect socket`); const { remoteAddress, remotePort } = this.#socketOptions; this.#socket.connect(remoteAddress, remotePort); } - /** - * @param {bigint} maxResults - * @returns {Datagram[]} - * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) - * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) - */ - receive(maxResults) { - console.log(`[udp] receive socket`); - - assert(this[symbolState].isBound === false, "invalid-state"); - assert(this[symbolState].inProgress === false, "not-in-progress"); - - if (maxResults === 0n) { - return []; - } - - this.#socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); - this.#socket.onerror = (err) => console.log("recv error", err); - this.#socket.recvStart(); - const datagrams = []; - return datagrams; - } - /** * - * @param {Datagram[]} datagrams - * @returns {bigint} + * @param {IpSocketAddress | undefined} remoteAddress + * @returns {Array} * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) - * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) - * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / :`). (EDESTADDRREQ, EADDRNOTAVAIL) * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) - * @throws {invalid-state} The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. - * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) - * @throws {would-block} The send buffer is currently full. (EWOULDBLOCK, EAGAIN) + * @throws {invalid-state} The socket is not bound. + * @throws {address-in-use} Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) */ - send(datagrams) { - console.log(`[udp] send socket`); - - const req = new SendWrap(); - const doSend = (data, port, host, family) => { - console.log(`[udp] send socket to ${host}:${port}`); + stream(remoteAddress = undefined) { + console.log(`[udp] stream socket remote address ${remoteAddress}`); - // setting hasCallback to false will make send() synchronous - // TODO: handle async send - const hasCallback = false; - - let err = null; - if (family.toLocaleLowerCase() === "ipv4") { - err = this.#socket.send(req, data, data.length, port, host, hasCallback); - } else if (family.toLocaleLowerCase() === "ipv6") { - err = this.#socket.send6(req, data, data.length, port, host, hasCallback); - } - return err; - }; - - datagrams.forEach((datagram) => { - const { data, remoteAddress } = datagram; - const { tag: family, val } = remoteAddress; - const { address, port } = val; - const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); - console.error({ - err, - }); - }); + this.#startConnect(this.network, remoteAddress); + this.#finishConnect(); + return [new IncomingDatagramStream(this.#socket), new OutgoingDatagramStream(this.#socket)]; } /** @@ -271,6 +224,8 @@ export class UdpSocketImpl { ipv6Only() { console.log(`[udp] ipv6 only socket`); + assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); + return this[symbolState].ipv6Only; } @@ -279,12 +234,15 @@ export class UdpSocketImpl { * @param {boolean} value * @returns {void} * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. - * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) * @throws {invalid-state} (set) The socket is already bound. + * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { console.log(`[udp] set ipv6 only socket to ${value}`); + assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); + assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); + this[symbolState].ipv6Only = value; } @@ -295,18 +253,21 @@ export class UdpSocketImpl { unicastHopLimit() { console.log(`[udp] unicast hop limit socket`); - throw new Error("Not implemented"); + return this.#socketOptions.unicastHopLimit; } /** * * @param {number} value * @returns {void} + * @throws {invalid-argument} The TTL value must be 1 or higher. */ setUnicastHopLimit(value) { console.log(`[udp] set unicast hop limit socket`); - throw new Error("Not implemented"); + assert(value < 1, "invalid-argument", "The TTL value must be 1 or higher"); + + this.#socketOptions.unicastHopLimit = value; } /** @@ -386,3 +347,121 @@ export class UdpSocketImpl { return this.#socket; } } + +class IncomingDatagramStream { + #socket = null; + + constructor(socket) { + this.#socket = socket; + } + + /** + * + * @param {bigint} maxResults + * @returns {Datagram[]} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) + * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) + */ + receive(maxResults) { + console.log(`[udp] receive socket`); + + assert(this[symbolState].isBound === false, "invalid-state"); + assert(this[symbolState].inProgress === false, "not-in-progress"); + + if (maxResults === 0n) { + return []; + } + + this.#socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); + this.#socket.onerror = (err) => console.log("recv error", err); + this.#socket.recvStart(); + const datagrams = []; + return datagrams; + } + + /** + * + * @returns {Pollable} A pollable which will resolve once the stream is ready to receive again. + */ + subscribe() { + console.log(`[udp] subscribe socket`); + + throw new Error("Not implemented"); + } +} + +class OutgoingDatagramStream { + #socket = null; + constructor(socket) { + this.#socket = socket; + } + + /** + * + * @returns {bigint} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + */ + checkSend() { + console.log(`[udp] check send socket`); + + throw new Error("Not implemented"); + } + + /** + * + * @param {Datagram[]} datagrams + * @returns {bigint} + * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + * @throws {invalid-argument} The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) + * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) + */ + send(datagrams) { + console.log(`[udp] send socket`); + + const req = new SendWrap(); + const doSend = (data, port, host, family) => { + console.log(`[udp] send socket to ${host}:${port}`); + + // setting hasCallback to false will make send() synchronous + // TODO: handle async send + const hasCallback = false; + + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = this.#socket.send(req, data, data.length, port, host, hasCallback); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = this.#socket.send6(req, data, data.length, port, host, hasCallback); + } + return err; + }; + + datagrams.forEach((datagram) => { + const { data, remoteAddress } = datagram; + const { tag: family, val } = remoteAddress; + const { address, port } = val; + const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); + console.error({ + err, + }); + }); + } + + /** + * + * @returns {Pollable} A pollable which will resolve once the stream is ready to send again. + */ + subscribe() { + console.log(`[udp] subscribe socket`); + + throw new Error("Not implemented"); + } +} diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 88c5e60d8..0c9e40d6e 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -1,9 +1,9 @@ /** - * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network - * @typedef {import("../../types/interfaces/wasi-sockets-network").ErrorCode} ErrorCode - * @typedef {import("../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily - * @typedef {import("../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket - * @typedef {import("../../types/interfaces/wasi-sockets-udp").UdpSocket} UdpSocket + * @typedef {import("../../../types/interfaces/wasi-sockets-network").Network} Network + * @typedef {import("../../../types/interfaces/wasi-sockets-network").ErrorCode} ErrorCode + * @typedef {import("../../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily + * @typedef {import("../../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket + * @typedef {import("../../../types/interfaces/wasi-sockets-udp").UdpSocket} UdpSocket */ import { TcpSocketImpl } from "./tcp-socket-impl.js"; diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index f2725f5b0..e2ba688d2 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -387,7 +387,7 @@ suite("Node.js Preview2", () => { }); equal(socket.addressFamily(), "ipv4"); }); - test("udp.connect(): should connect to a valid ipv4 address", async () => { + test("udp.stream(): should connect to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); @@ -412,8 +412,7 @@ suite("Node.js Preview2", () => { socket.startBind(network, localAddress); socket.finishBind(); - socket.startConnect(network, remoteAddress); - socket.finishConnect(); + socket.stream(remoteAddress); strictEqual(socket.client().connect.mock.calls.length, 1); From c2d18469eea4186ef7cbe90418ae7e5b14a3f8cf Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Wed, 15 Nov 2023 14:48:58 +0100 Subject: [PATCH 39/65] chore: remove useless logs --- .../lib/nodejs/sockets/tcp-socket-impl.js | 92 ++------------- .../lib/nodejs/sockets/udp-socket-impl.js | 48 -------- .../lib/nodejs/sockets/wasi-sockets.js | 5 - packages/preview2-shim/test/test.js | 108 +++++++++++++----- 4 files changed, 92 insertions(+), 161 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 700f8141b..0f196067b 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -79,8 +79,6 @@ export class TcpSocketImpl { } #handleConnection(err, newClientSocket) { - console.log(`[tcp] on server connection`); - if (err) { assert(true, "", err); } @@ -99,13 +97,9 @@ export class TcpSocketImpl { }; } - #handleDisconnect(err) { - console.log(`[tcp] on server disconnect`); - } + #handleDisconnect(err) {} #onClientConnectComplete(err) { - console.log(`[tcp] on client connect complete`); - if (err) { assert(err === -99, "ephemeral-ports-exhausted"); assert(err === -104, "connection-reset"); @@ -121,9 +115,7 @@ export class TcpSocketImpl { } // TODO: is this needed? - #handleAfterShutdown() { - console.log(`[tcp] after shutdown socket`); - } + #handleAfterShutdown() {} /** * @param {Network} network @@ -138,8 +130,6 @@ export class TcpSocketImpl { const address = serializeIpAddress(localAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(address)}`; - console.log(`[tcp] start bind socket to ${address}:${localAddress.val.port}`); - assert(this.#isBound, "invalid-state", "The socket is already bound"); assert( this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), @@ -170,8 +160,6 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { - console.log(`[tcp] finish bind socket`); - assert(this.#inProgress === false, "not-in-progress"); const { localAddress, localPort, family } = this.#socketOptions; @@ -192,8 +180,6 @@ export class TcpSocketImpl { assert(true, "", err); } - console.log(`[tcp] bound socket to ${localAddress}:${localPort}`); - this.#isBound = true; this.#inProgress = false; } @@ -213,8 +199,6 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) */ startConnect(network, remoteAddress) { - console.log(`[tcp] start connect socket to ${remoteAddress.val.address}:${remoteAddress.val.port}`); - assert(this.network !== null && this.network.id !== network.id, "already-attached"); assert(this.#state === "connected", "already-connected"); assert(this.#state === "connection", "already-listening"); @@ -245,8 +229,6 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishConnect() { - console.log(`[tcp] finish connect socket`); - assert(this.#inProgress === false, "not-in-progress"); const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; @@ -288,8 +270,6 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. */ startListen() { - console.log(`[tcp] start listen socket on ${this.#socketOptions.localAddress}:${this.#socketOptions.localPort}`); - assert(this.#isBound === false, "invalid-state"); assert(this.#state === SocketState.Connection, "invalid-state"); assert(this.#state === SocketState.Listener, "invalid-state"); @@ -304,8 +284,6 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishListen() { - console.log(`[tcp] finish listen socket (backlog: ${this.#backlog})`); - assert(this.#inProgress === false, "not-in-progress"); const err = this.#serverHandle.listen(this.#backlog); @@ -331,13 +309,11 @@ export class TcpSocketImpl { const self = this; const outgoingStream = new OutputStream({ write(bytes) { - console.log(`[tcp] write socket`); self.#acceptedClient.write(bytes); }, }); const ingoingStream = new InputStream({ read(len) { - console.log(`[tcp] read socket`); return self.#acceptedClient.read(len); }, }); @@ -350,8 +326,6 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - console.log(`[tcp] local address socket`); - assert(this.#isBound === false, "invalid-state"); const { localAddress, localPort, family } = this.#socketOptions; @@ -369,16 +343,12 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - console.log(`[tcp] remote address socket`); - assert(this.#state !== SocketState.Connection, "invalid-state"); return this.#socketOptions.remoteAddress; } isListening() { - console.log(`[tcp] is listening socket`); - return this.#state === SocketState.Listener; } @@ -386,8 +356,6 @@ export class TcpSocketImpl { * @returns {IpAddressFamily} */ addressFamily() { - console.log(`[tcp] address family socket`); - return this.#socketOptions.family; } @@ -396,8 +364,6 @@ export class TcpSocketImpl { * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { - console.log(`[tcp] ipv6 only socket`); - return this.#ipv6Only; } @@ -409,8 +375,6 @@ export class TcpSocketImpl { * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { - console.log(`[tcp] set ipv6 only socket to ${value}`); - this.#ipv6Only = value; } @@ -421,8 +385,6 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Connection state. */ setListenBacklogSize(value) { - console.log(`[tcp] set listen backlog size socket to ${value}`); - this.#backlog = value; } @@ -430,8 +392,6 @@ export class TcpSocketImpl { * @returns {boolean} */ keepAliveEnabled() { - console.log(`[tcp] keep alive socket`); - this.#keepAlive; } @@ -440,11 +400,9 @@ export class TcpSocketImpl { * @returns {void} */ setKeepAliveEnabled(value) { - console.log(`[tcp] set keep alive socket to ${value}`); - this.#keepAlive = value; this.#clientHandle.setKeepAlive(value); - + if (value) { this.#clientHandle.setKeepAliveIdleTime(this.keepAliveIdleTime()); this.#clientHandle.setKeepAliveInterval(this.keepAliveInterval()); @@ -453,72 +411,60 @@ export class TcpSocketImpl { } /** - * + * * @returns {Duration} */ keepAliveIdleTime() { - console.log(`[tcp] keep alive idle time socket`); - return this.#keepAliveIdleTime; } /** - * + * * @param {Duration} value * @returns {void} * @throws {invalid-argument} (set) The idle time must be 1 or higher. */ setKeepAliveIdleTime(value) { - console.log(`[tcp] set keep alive idle time socket to ${value}`); - assert(value < 1, "invalid-argument", "The idle time must be 1 or higher."); this.#keepAliveIdleTime = value; } /** - * + * * @returns {Duration} */ keepAliveInterval() { - console.log(`[tcp] keep alive interval socket`); - return this.#keepAliveInterval; } /** - * + * * @param {Duration} value * @returns {void} * @throws {invalid-argument} (set) The interval must be 1 or higher. */ setKeepAliveInterval(value) { - console.log(`[tcp] set keep alive interval socket to ${value}`); - assert(value < 1, "invalid-argument", "The interval must be 1 or higher."); this.#keepAliveInterval = value; } /** - * + * * @returns {Duration} */ keepAliveCount() { - console.log(`[tcp] keep alive count socket`); - return this.#keepAliveCount; } /** - * + * * @param {Duration} value * @returns {void} * @throws {invalid-argument} (set) The count must be 1 or higher. */ setKeepAliveCount(value) { - console.log(`[tcp] set keep alive count socket to ${value}`); - assert(value < 1, "invalid-argument", "The count must be 1 or higher."); this.#keepAliveCount = value; @@ -528,8 +474,6 @@ export class TcpSocketImpl { * @returns {boolean} */ noDelay() { - console.log(`[tcp] no delay socket`); - return this.#noDelay; } @@ -539,8 +483,6 @@ export class TcpSocketImpl { * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) */ setNoDelay(value) { - console.log(`[tcp] set no delay socket to ${value}`); - this.#noDelay = value; this.#clientHandle.setNoDelay(value); } @@ -549,8 +491,6 @@ export class TcpSocketImpl { * @returns {number} */ unicastHopLimit() { - console.log(`[tcp] unicast hop limit socket`); - return this.#unicastHopLimit; } @@ -562,8 +502,6 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Listener state. */ setUnicastHopLimit(value) { - console.log(`[tcp] set unicast hop limit socket to ${value}`); - this.#unicastHopLimit = value; } @@ -571,7 +509,6 @@ export class TcpSocketImpl { * @returns {bigint} */ receiveBufferSize() { - console.log(`[tcp] receive buffer size socket`); throw new Error("not implemented"); } @@ -582,7 +519,6 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Listener state. */ setReceiveBufferSize(value) { - console.log(`[tcp] set receive buffer size socket to ${value}`); throw new Error("not implemented"); } @@ -590,7 +526,6 @@ export class TcpSocketImpl { * @returns {bigint} */ sendBufferSize() { - console.log(`[tcp] send buffer size socket`); throw new Error("not implemented"); } @@ -601,7 +536,6 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Listener state. */ setSendBufferSize(value) { - console.log(`[tcp] set send buffer size socket to ${value}`); throw new Error("not implemented"); } @@ -609,7 +543,6 @@ export class TcpSocketImpl { * @returns {Pollable} */ subscribe() { - console.log(`[tcp] subscribe socket`); throw new Error("not implemented"); } @@ -619,8 +552,6 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not in the Connection state. (ENOTCONN) */ shutdown(shutdownType) { - console.log(`[tcp] shutdown socket with type ${shutdownType}`); - // TODO: figure out how to handle shutdownTypes if (shutdownType === ShutdownType.receive) { this.#canReceive = false; @@ -634,16 +565,13 @@ export class TcpSocketImpl { const req = new ShutdownWrap(); req.oncomplete = this.#handleAfterShutdown.bind(this); req.handle = this._handle; - req.callback = () => { - console.log(`[tcp] shutdown callback`); - }; + req.callback = () => {}; const err = this._handle.shutdown(req); assert(err === 1, "invalid-state"); } [symbolDispose]() { - console.log(`[tcp] dispose socket`); this.#serverHandle.close(); this.#clientHandle.close(); } diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 4cff41fad..ba21b3628 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -57,8 +57,6 @@ export class UdpSocketImpl { const address = serializeIpAddress(localAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(address)}`; - console.log(`[tcp] start bind socket to ${address}:${localAddress.val.port}`); - assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); assert( this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), @@ -83,8 +81,6 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { - console.log(`[udp] finish bind socket`); - assert(this[symbolState].inProgress === false, "not-in-progress"); const { localAddress, localPort, family } = this.#socketOptions; @@ -121,8 +117,6 @@ export class UdpSocketImpl { * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ #startConnect(network, remoteAddress) { - console.log(`[udp] start connect socket`); - const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; @@ -146,8 +140,6 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ #finishConnect() { - console.log(`[udp] finish connect socket`); - const { remoteAddress, remotePort } = this.#socketOptions; this.#socket.connect(remoteAddress, remotePort); } @@ -166,8 +158,6 @@ export class UdpSocketImpl { * @throws {connection-refused} The connection was refused. (ECONNREFUSED) */ stream(remoteAddress = undefined) { - console.log(`[udp] stream socket remote address ${remoteAddress}`); - this.#startConnect(this.network, remoteAddress); this.#finishConnect(); return [new IncomingDatagramStream(this.#socket), new OutgoingDatagramStream(this.#socket)]; @@ -179,8 +169,6 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - console.log(`[udp] local address socket`); - assert(this[symbolState].isBound === false, "invalid-state"); const { localAddress, localPort, family } = this.#socketOptions; @@ -199,8 +187,6 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - console.log(`[udp] remote address socket`); - assert(this[symbolState].state !== SocketState.Connection, "invalid-state"); return this.#socketOptions.remoteAddress; @@ -211,8 +197,6 @@ export class UdpSocketImpl { * @returns {IpAddressFamily} */ addressFamily() { - console.log(`[udp] address family socket`); - return this.#socketOptions.family; } @@ -222,8 +206,6 @@ export class UdpSocketImpl { * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { - console.log(`[udp] ipv6 only socket`); - assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); return this[symbolState].ipv6Only; @@ -238,8 +220,6 @@ export class UdpSocketImpl { * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { - console.log(`[udp] set ipv6 only socket to ${value}`); - assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); @@ -251,8 +231,6 @@ export class UdpSocketImpl { * @returns {number} */ unicastHopLimit() { - console.log(`[udp] unicast hop limit socket`); - return this.#socketOptions.unicastHopLimit; } @@ -263,8 +241,6 @@ export class UdpSocketImpl { * @throws {invalid-argument} The TTL value must be 1 or higher. */ setUnicastHopLimit(value) { - console.log(`[udp] set unicast hop limit socket`); - assert(value < 1, "invalid-argument", "The TTL value must be 1 or higher"); this.#socketOptions.unicastHopLimit = value; @@ -275,8 +251,6 @@ export class UdpSocketImpl { * @returns {bigint} */ receiveBufferSize() { - console.log(`[udp] receive buffer size socket`); - throw new Error("Not implemented"); } @@ -286,8 +260,6 @@ export class UdpSocketImpl { * @returns {void} */ setReceiveBufferSize(value) { - console.log(`[udp] set receive buffer size socket`); - throw new Error("Not implemented"); } @@ -296,8 +268,6 @@ export class UdpSocketImpl { * @returns {bigint} */ sendBufferSize() { - console.log(`[udp] send buffer size socket`); - throw new Error("Not implemented"); } @@ -307,8 +277,6 @@ export class UdpSocketImpl { * @returns {void} */ setSendBufferSize(value) { - console.log(`[udp] set send buffer size socket`); - throw new Error("Not implemented"); } @@ -317,14 +285,10 @@ export class UdpSocketImpl { * @returns {Pollable} */ subscribe() { - console.log(`[udp] subscribe socket`); - throw new Error("Not implemented"); } [Symbol.dispose]() { - console.log(`[udp] dispose socket`); - let err = null; err = this.#socket.recvStop((...args) => { console.log("stop recv", args); @@ -366,8 +330,6 @@ class IncomingDatagramStream { * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) */ receive(maxResults) { - console.log(`[udp] receive socket`); - assert(this[symbolState].isBound === false, "invalid-state"); assert(this[symbolState].inProgress === false, "not-in-progress"); @@ -387,8 +349,6 @@ class IncomingDatagramStream { * @returns {Pollable} A pollable which will resolve once the stream is ready to receive again. */ subscribe() { - console.log(`[udp] subscribe socket`); - throw new Error("Not implemented"); } } @@ -405,8 +365,6 @@ class OutgoingDatagramStream { * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) */ checkSend() { - console.log(`[udp] check send socket`); - throw new Error("Not implemented"); } @@ -425,12 +383,8 @@ class OutgoingDatagramStream { * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) */ send(datagrams) { - console.log(`[udp] send socket`); - const req = new SendWrap(); const doSend = (data, port, host, family) => { - console.log(`[udp] send socket to ${host}:${port}`); - // setting hasCallback to false will make send() synchronous // TODO: handle async send const hasCallback = false; @@ -460,8 +414,6 @@ class OutgoingDatagramStream { * @returns {Pollable} A pollable which will resolve once the stream is ready to send again. */ subscribe() { - console.log(`[udp] subscribe socket`); - throw new Error("Not implemented"); } } diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 0c9e40d6e..f3a707acf 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -156,8 +156,6 @@ export class WasiSockets { * @returns {Network} */ instanceNetwork() { - console.log(`[sockets] instance network`); - // TODO: should networkInstance be a singleton? if (!net.networkInstance) { net.networkInstance = new Network(); @@ -174,7 +172,6 @@ export class WasiSockets { this.udpCreateSocket = { createUdpSocket(addressFamily) { net.socketCnt++; - console.log(`[udp] Create udp socket ${addressFamily}`); assert( supportedAddressFamilies.includes(addressFamily) === false, @@ -205,8 +202,6 @@ export class WasiSockets { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ createTcpSocket(addressFamily) { - console.log(`[tcp] Create tcp socket ${addressFamily}`); - assert( supportedAddressFamilies.includes(addressFamily) === false, errorCode.notSupported, diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index e2ba688d2..0ae1960d7 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -91,17 +91,11 @@ suite("Node.js Preview2", () => { test("FS read", async () => { const { filesystem } = await import("@bytecodealliance/preview2-shim"); const [[rootDescriptor]] = filesystem.preopens.getDirectories(); - const childDescriptor = rootDescriptor.openAt( - {}, - fileURLToPath(import.meta.url), - {}, - {} - ); + const childDescriptor = rootDescriptor.openAt({}, fileURLToPath(import.meta.url), {}, {}); const stream = childDescriptor.readViaStream(0); stream.subscribe().block(); let buf = stream.read(10000n); - while (buf.byteLength === 0) - buf = stream.read(10000n); + while (buf.byteLength === 0) buf = stream.read(10000n); const source = new TextDecoder().decode(buf); ok(source.includes("UNIQUE STRING")); stream[Symbol.dispose](); @@ -136,9 +130,7 @@ suite("Node.js Preview2", () => { const responseHeaders = incomingResponse.headers().entries(); const decoder = new TextDecoder(); - const headers = Object.fromEntries( - responseHeaders.map(([k, v]) => [k, decoder.decode(v)]) - ); + const headers = Object.fromEntries(responseHeaders.map(([k, v]) => [k, decoder.decode(v)])); let responseBody; const incomingBody = incomingResponse.consume(); @@ -146,8 +138,7 @@ suite("Node.js Preview2", () => { const bodyStream = incomingBody.stream(); bodyStream.subscribe().block(); let buf = bodyStream.read(5000n); - while (buf.byteLength === 0) - buf = bodyStream.read(5000n); + while (buf.byteLength === 0) buf = bodyStream.read(5000n); responseBody = new TextDecoder().decode(buf); } @@ -156,7 +147,7 @@ suite("Node.js Preview2", () => { ok(responseBody.includes("WebAssembly")); }); - suite("Sockets::TCP", async () => { + suite("WASI Sockets (TCP)", async () => { test("sockets.instanceNetwork() should be a singleton", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network1 = sockets.instanceNetwork.instanceNetwork(); @@ -165,7 +156,7 @@ suite("Node.js Preview2", () => { equal(network2.id, 1); }); - test("sockets.tcpCreateSocket()", async () => { + test("sockets.tcpCreateSocket() should throw no-supported", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const socket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); notEqual(socket, null); @@ -230,7 +221,7 @@ suite("Node.js Preview2", () => { }); }); - test("tcp.bind(): should throw invalid-argument", async () => { + test("tcp.bind(): should throw invalid-argument when invalid address family", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); @@ -253,7 +244,7 @@ suite("Node.js Preview2", () => { ); }); - test("tcp.bind(): should throw invalid-state", async () => { + test("tcp.bind(): should throw invalid-state when already bound", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); @@ -291,7 +282,7 @@ suite("Node.js Preview2", () => { }; mock.method(tcpSocket.server(), "listen", () => { - console.log("mock listen called"); + // mock listen }); tcpSocket.startBind(network, localAddress); @@ -325,7 +316,7 @@ suite("Node.js Preview2", () => { }; mock.method(tcpSocket.client(), "connect", () => { - console.log("mock connect called"); + // mock connect }); tcpSocket.startBind(network, localAddress); @@ -347,11 +338,13 @@ suite("Node.js Preview2", () => { }); }); - suite("Sockets::UDP", async () => { - test("sockets.udpCreateSocket()", async () => { + suite("WASI Sockets (UDP)", async () => { + test("sockets.udpCreateSocket() should be a singleton", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); - const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); - notEqual(socket, null); + const socket1 = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); + notEqual(socket1.id, 1); + const socket2 = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); + notEqual(socket2.id, 1); throws( () => { @@ -387,6 +380,30 @@ suite("Node.js Preview2", () => { }); equal(socket.addressFamily(), "ipv4"); }); + test("udp.bind(): should bind to a valid ipv6 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv6); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0, 0, 0], + port: 0, + }, + }; + socket.startBind(network, localAddress); + socket.finishBind(); + + equal(socket.network.id, network.id); + deepEqual(socket.localAddress(), { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0, 0, 0], + port: 0, + }, + }); + equal(socket.addressFamily(), "ipv6"); + }); test("udp.stream(): should connect to a valid ipv4 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); @@ -407,7 +424,7 @@ suite("Node.js Preview2", () => { }; mock.method(socket.client(), "connect", () => { - console.log("mock connect called"); + // mock connect }); socket.startBind(network, localAddress); @@ -416,8 +433,8 @@ suite("Node.js Preview2", () => { strictEqual(socket.client().connect.mock.calls.length, 1); - equal(socket.network.id, network.id); - equal(socket.addressFamily(), "ipv4"); + strictEqual(socket.network.id, network.id); + strictEqual(socket.addressFamily(), "ipv4"); deepEqual(socket.localAddress(), { tag: sockets.network.IpAddressFamily.ipv4, val: { @@ -426,5 +443,44 @@ suite("Node.js Preview2", () => { }, }); }); + test("udp.stream(): should connect to a valid ipv6 address", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); + const network = sockets.instanceNetwork.instanceNetwork(); + const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv6); + const localAddress = { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0, 0, 0], + port: 0, + }, + }; + const remoteAddress = { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0, 0, 0], + port: 80, + }, + }; + + mock.method(socket.client(), "connect", () => { + // mock connect + }); + + socket.startBind(network, localAddress); + socket.finishBind(); + socket.stream(remoteAddress); + + strictEqual(socket.client().connect.mock.calls.length, 1); + + strictEqual(socket.network.id, network.id); + strictEqual(socket.addressFamily(), "ipv6"); + deepEqual(socket.localAddress(), { + tag: sockets.network.IpAddressFamily.ipv6, + val: { + address: [0, 0, 0, 0, 0, 0, 0, 0], + port: 0, + }, + }); + }); }); }); From 80e2d32a519841b8b53b9d8aed9ff466a8dee806 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 17 Nov 2023 00:12:43 +0100 Subject: [PATCH 40/65] chore(udp): fix according to conformance tests --- packages/preview2-shim/lib/common/assert.js | 10 +- packages/preview2-shim/lib/nodejs/sockets.js | 2 + .../lib/nodejs/sockets/socket-common.js | 19 +- .../lib/nodejs/sockets/udp-socket-impl.js | 299 ++++++++++-------- .../lib/nodejs/sockets/wasi-sockets.js | 7 + 5 files changed, 198 insertions(+), 139 deletions(-) diff --git a/packages/preview2-shim/lib/common/assert.js b/packages/preview2-shim/lib/common/assert.js index 63e07c956..0f151be53 100644 --- a/packages/preview2-shim/lib/common/assert.js +++ b/packages/preview2-shim/lib/common/assert.js @@ -1,9 +1,7 @@ -export function assert(condition, code, message) { +export function assert(condition, tag, _val) { if (condition) { - const ex = new Error(message); - ex.name = "Error"; - ex.message = message; - ex.code = code; - throw ex; + // TODO: throw meaningful errors + // NOTE: wasmtime conformance tests are expecting a string here (a tag) + throw tag; } } diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 562578fa5..4cc26a154 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -8,4 +8,6 @@ export const { network, tcpCreateSocket, udpCreateSocket, + tcp, + udp, } = sockets; \ No newline at end of file diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index 7a4dab1a2..eafc8208c 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -14,7 +14,20 @@ function tupleToIpv4(arr) { return arr.map((segment) => segment.toString(10)).join("."); } +// TODO: write a better (faste?) parser for ipv6 function ipv6ToTuple(ipv6) { + if (ipv6 === "::1") { + return [0, 0, 0, 0, 0, 0, 0, 1]; + } else if (ipv6 === "::") { + return [0, 0, 0, 0, 0, 0, 0, 0]; + } else if (ipv6.includes("::")) { + const [head, tail] = ipv6.split("::"); + const headSegments = head.split(":").map((segment) => parseInt(segment, 16)); + const tailSegments = tail.split(":").map((segment) => parseInt(segment, 16)); + const missingSegments = 8 - headSegments.length - tailSegments.length; + const middleSegments = Array(missingSegments).fill(0); + return headSegments.concat(middleSegments).concat(tailSegments); + } return ipv6.split(":").map((segment) => parseInt(segment, 16)); } @@ -22,7 +35,11 @@ function ipv4ToTuple(ipv4) { return ipv4.split(".").map((segment) => parseInt(segment, 10)); } -export function serializeIpAddress(addr, family) { +export function serializeIpAddress(addr = undefined, family) { + if (addr === undefined) { + addr = { val: { address: [] } }; + } + let { address } = addr.val; if (family.toLocaleLowerCase() === "ipv4") { address = tupleToIpv4(address); diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index ba21b3628..89e76a13f 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -13,15 +13,23 @@ const { UDP, SendWrap } = process.binding("udp_wrap"); import { isIP } from "node:net"; import { assert } from "../../common/assert.js"; import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; +import { pollableCreate } from "../../io/worker-io.js"; -const SocketState = { +const SocketConnectionState = { Error: "Error", Closed: "Closed", - Connection: "Connection", - Listener: "Listener", + Connecting: "Connecting", + Connected: "Connected", + Listening: "Listening", }; -const symbolState = Symbol("state"); +const symbolState = Symbol("SocketInternalState"); + +// see https://github.com/libuv/libuv/blob/master/docs/src/udp.rst +const flags = { + UV_UDP_IPV6ONLY: 1, + UV_UDP_REUSEADDR: 4, +}; export class UdpSocketImpl { /** @type {Socket} */ #socket = null; @@ -29,9 +37,9 @@ export class UdpSocketImpl { [symbolState] = { isBound: false, - inProgress: false, + operationInProgress: false, ipv6Only: false, - state: SocketState.Closed, + state: SocketConnectionState.Closed, }; #socketOptions = {}; @@ -44,12 +52,126 @@ export class UdpSocketImpl { this.#socketOptions.family = addressFamily; this.#socket = new UDP(); + + const self = this; + + class IncomingDatagramStream { + static _create() { + const stream = new IncomingDatagramStream(); + return stream; + } + + /** + * + * @param {bigint} maxResults + * @returns {Datagram[]} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) + * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) + */ + receive(maxResults) { + assert(self[symbolState].isBound === false, "invalid-state"); + assert(self[symbolState].operationInProgress === false, "not-in-progress"); + + if (maxResults === 0n) { + return []; + } + + const socket = self.#socket; + socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); + socket.onerror = (err) => console.log("recv error", err); + socket.recvStart(); + const datagrams = []; + return datagrams; + } + + /** + * + * @returns {Pollable} A pollable which will resolve once the stream is ready to receive again. + */ + subscribe() { + throw new Error("Not implemented"); + } + } + this.incomingDatagramStreamCreate = IncomingDatagramStream._create; + delete IncomingDatagramStream._create; + + class OutgoingDatagramStream { + static _create() { + const stream = new OutgoingDatagramStream(); + return stream; + } + + /** + * + * @returns {bigint} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + */ + checkSend() { + throw new Error("Not implemented"); + } + + /** + * + * @param {Datagram[]} datagrams + * @returns {bigint} + * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + * @throws {invalid-argument} The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) + * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) + */ + send(datagrams) { + const req = new SendWrap(); + const doSend = (data, port, host, family) => { + // setting hasCallback to false will make send() synchronous + // TODO: handle async send + const hasCallback = false; + const socket = self.#socket; + + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = socket.send(req, data, data.length, port, host, hasCallback); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = socket.send6(req, data, data.length, port, host, hasCallback); + } + return err; + }; + + datagrams.forEach((datagram) => { + const { data, remoteAddress } = datagram; + const { tag: family, val } = remoteAddress; + const { address, port } = val; + const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); + console.error({ + err, + }); + }); + } + + /** + * + * @returns {Pollable} A pollable which will resolve once the stream is ready to send again. + */ + subscribe() { + throw new Error("Not implemented"); + } + } + this.outgoingDatagramStreamCreate = OutgoingDatagramStream._create; + delete OutgoingDatagramStream._create; } /** * * @param {Network} network * @param {IpAddressFamily} localAddress * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) * @throws {invalid-state} The socket is already bound. (EINVAL) * @returns {void} */ @@ -63,12 +185,13 @@ export class UdpSocketImpl { "invalid-argument", "The `local-address` has the wrong address family" ); + assert(this[symbolState].ipv6Only, "invalid-argument", "The `local-address` has the wrong address family"); const { port } = localAddress.val; this.#socketOptions.localAddress = address; this.#socketOptions.localPort = port; this.network = network; - this[symbolState].inProgress = true; + this[symbolState].operationInProgress = true; } /** @@ -81,13 +204,16 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { - assert(this[symbolState].inProgress === false, "not-in-progress"); + assert(this[symbolState].operationInProgress === false, "not-in-progress"); const { localAddress, localPort, family } = this.#socketOptions; - // TODO: pass the right flags - // see https://github.com/libuv/libuv/blob/93efccf4ee1ed3c740d10660b4bfb08edc68a1b5/src/unix/udp.c#L486 - const flags = 0; + let flags = 0; + + if (this[symbolState].ipv6Only) { + flags |= flags.UV_UDP_IPV6ONLY; + } + let err = null; if (family.toLocaleLowerCase() === "ipv4") { err = this.#socket.bind(localAddress, localPort, flags); @@ -97,9 +223,10 @@ export class UdpSocketImpl { if (err) { assert(err === -22, "address-in-use"); + assert(err === -48, "address-in-use"); // macos assert(err === -49, "address-not-bindable"); assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL - assert(true, "", err); + assert(true, "unknown", err); } this[symbolState].isBound = true; @@ -108,7 +235,7 @@ export class UdpSocketImpl { /** * * @param {Network} network - * @param {IpAddressFamily} remoteAddress + * @param {IpAddressFamily | undefined} remoteAddress * @returns {void} * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) @@ -116,7 +243,7 @@ export class UdpSocketImpl { * @throws {invalid-argument} The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ - #startConnect(network, remoteAddress) { + #startConnect(network, remoteAddress = undefined) { const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; @@ -129,7 +256,7 @@ export class UdpSocketImpl { this.#socketOptions.remotePort = port; this.network = network; - this[symbolState].inProgress = true; + this[symbolState].operationInProgress = true; } /** @@ -160,7 +287,7 @@ export class UdpSocketImpl { stream(remoteAddress = undefined) { this.#startConnect(this.network, remoteAddress); this.#finishConnect(); - return [new IncomingDatagramStream(this.#socket), new OutgoingDatagramStream(this.#socket)]; + return [this.incomingDatagramStreamCreate(), this.outgoingDatagramStreamCreate()]; } /** @@ -171,12 +298,15 @@ export class UdpSocketImpl { localAddress() { assert(this[symbolState].isBound === false, "invalid-state"); - const { localAddress, localPort, family } = this.#socketOptions; + const out = {}; + this.#socket.getsockname(out); + + const { address, port, family } = out; return { - tag: family, + tag: family.toLocaleLowerCase(), val: { - address: deserializeIpAddress(localAddress, family), - port: localPort, + address: deserializeIpAddress(address, family), + port, }, }; } @@ -187,9 +317,23 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - assert(this[symbolState].state !== SocketState.Connection, "invalid-state"); + assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); + + const out = {}; + console.log({ + out + }) + this.#socket.getpeername(out); + - return this.#socketOptions.remoteAddress; + const { address, port, family } = out; + return { + tag: family.toLocaleLowerCase(), + val: { + address: deserializeIpAddress(address, family), + port, + }, + }; } /** @@ -285,7 +429,7 @@ export class UdpSocketImpl { * @returns {Pollable} */ subscribe() { - throw new Error("Not implemented"); + return pollableCreate(0); } [Symbol.dispose]() { @@ -307,113 +451,4 @@ export class UdpSocketImpl { client() { return this.#socket; } - server() { - return this.#socket; - } -} - -class IncomingDatagramStream { - #socket = null; - - constructor(socket) { - this.#socket = socket; - } - - /** - * - * @param {bigint} maxResults - * @returns {Datagram[]} - * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) - * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {remote-unreachable} The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - * @throws {connection-refused} The connection was refused. (ECONNREFUSED) - * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) - */ - receive(maxResults) { - assert(this[symbolState].isBound === false, "invalid-state"); - assert(this[symbolState].inProgress === false, "not-in-progress"); - - if (maxResults === 0n) { - return []; - } - - this.#socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); - this.#socket.onerror = (err) => console.log("recv error", err); - this.#socket.recvStart(); - const datagrams = []; - return datagrams; - } - - /** - * - * @returns {Pollable} A pollable which will resolve once the stream is ready to receive again. - */ - subscribe() { - throw new Error("Not implemented"); - } -} - -class OutgoingDatagramStream { - #socket = null; - constructor(socket) { - this.#socket = socket; - } - - /** - * - * @returns {bigint} - * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) - */ - checkSend() { - throw new Error("Not implemented"); - } - - /** - * - * @param {Datagram[]} datagrams - * @returns {bigint} - * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) - * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) - * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) - * @throws {invalid-argument} The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) - * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {connection-refused} The connection was refused. (ECONNREFUSED) - * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) - */ - send(datagrams) { - const req = new SendWrap(); - const doSend = (data, port, host, family) => { - // setting hasCallback to false will make send() synchronous - // TODO: handle async send - const hasCallback = false; - - let err = null; - if (family.toLocaleLowerCase() === "ipv4") { - err = this.#socket.send(req, data, data.length, port, host, hasCallback); - } else if (family.toLocaleLowerCase() === "ipv6") { - err = this.#socket.send6(req, data, data.length, port, host, hasCallback); - } - return err; - }; - - datagrams.forEach((datagram) => { - const { data, remoteAddress } = datagram; - const { tag: family, val } = remoteAddress; - const { address, port } = val; - const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); - console.error({ - err, - }); - }); - } - - /** - * - * @returns {Pollable} A pollable which will resolve once the stream is ready to send again. - */ - subscribe() { - throw new Error("Not implemented"); - } } diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index f3a707acf..9f83afce8 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -140,6 +140,9 @@ export class WasiSockets { net.udpSockets.set(this.id, this); } } + this.udp = { + UdpSocket + }; class TcpSocket extends TcpSocketImpl { /** @@ -150,6 +153,9 @@ export class WasiSockets { net.tcpSockets.set(this.id, this); } } + this.tcp = { + TcpSocket + }; this.instanceNetwork = { /** @@ -167,6 +173,7 @@ export class WasiSockets { this.network = { errorCode, IpAddressFamily, + Network, }; this.udpCreateSocket = { From 5c81f1cae8051c7b8a897bf6f94d96204d92e98e Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 17 Nov 2023 00:31:00 +0100 Subject: [PATCH 41/65] fix(tcp): removed deprecated methods --- .../lib/nodejs/sockets/tcp-socket-impl.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 0f196067b..6e309c758 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -57,7 +57,6 @@ export class TcpSocketImpl { #keepAliveCount = 1; #keepAliveIdleTime = 1; #keepAliveInterval = 1; - #noDelay = false; #unicastHopLimit = 10; #acceptedClient = null; @@ -470,23 +469,6 @@ export class TcpSocketImpl { this.#keepAliveCount = value; } - /** - * @returns {boolean} - */ - noDelay() { - return this.#noDelay; - } - - /** - * @param {boolean} value - * @returns {void} - * @throws {concurrency-conflict} (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) - */ - setNoDelay(value) { - this.#noDelay = value; - this.#clientHandle.setNoDelay(value); - } - /** * @returns {number} */ From 18990d4fa7671ca371c02f251b646158f3beb175 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sat, 18 Nov 2023 14:29:19 +0100 Subject: [PATCH 42/65] chore: try to reuse an existing udp socket (wip) --- .../lib/nodejs/sockets/udp-socket-impl.js | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 89e76a13f..f1d9fcb80 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -247,10 +247,15 @@ export class UdpSocketImpl { const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; - assert(this.network !== null && this.network.id !== network.id, "invalid-argument"); assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); + if (this[symbolState].state === SocketConnectionState.Connected) { + // reusing a connected socket. See #finishConnect() + return; + } else { + } + const { port } = remoteAddress.val; this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = port; @@ -268,7 +273,13 @@ export class UdpSocketImpl { */ #finishConnect() { const { remoteAddress, remotePort } = this.#socketOptions; - this.#socket.connect(remoteAddress, remotePort); + + if (this[symbolState].state === SocketConnectionState.Connected) { + // TODO: figure out how to reuse a connected socket + this.#socket.connect(); + } else { + this.#socket.connect(remoteAddress, remotePort); + } } /** @@ -285,6 +296,8 @@ export class UdpSocketImpl { * @throws {connection-refused} The connection was refused. (ECONNREFUSED) */ stream(remoteAddress = undefined) { + assert(this[symbolState].isBound === false, "invalid-state"); + this.#startConnect(this.network, remoteAddress); this.#finishConnect(); return [this.incomingDatagramStreamCreate(), this.outgoingDatagramStreamCreate()]; @@ -320,12 +333,8 @@ export class UdpSocketImpl { assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); const out = {}; - console.log({ - out - }) this.#socket.getpeername(out); - const { address, port, family } = out; return { tag: family.toLocaleLowerCase(), From 78c7b8bc31f02ae5298d6a517f680a5163c128d6 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 19 Nov 2023 04:31:10 +0100 Subject: [PATCH 43/65] fix: make conformance preview2_udp_states pass --- .../lib/nodejs/sockets/udp-socket-impl.js | 357 +++++++++++------- .../lib/nodejs/sockets/wasi-sockets.js | 9 +- 2 files changed, 234 insertions(+), 132 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index f1d9fcb80..d02400dcf 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -31,8 +31,136 @@ const flags = { UV_UDP_REUSEADDR: 4, }; +const bufferSizeFlags = { + SO_RCVBUF: true, + SO_SNDBUF: false, +}; + + +export class IncomingDatagramStream { + static _create(socket) { + const stream = new IncomingDatagramStream(socket); + return stream; + } + + #socket = null; + constructor(socket) { + this.#socket = socket; + } + + /** + * + * @param {bigint} maxResults + * @returns {Datagram[]} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) + * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) + */ + receive(maxResults) { + assert(self[symbolState].isBound === false, "invalid-state"); + assert(self[symbolState].operationInProgress === false, "not-in-progress"); + + if (maxResults === 0n) { + return []; + } + + const socket = this.#socket; + socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); + socket.onerror = (err) => console.log("recv error", err); + socket.recvStart(); + const datagrams = []; + return datagrams; + } + + /** + * + * @returns {Pollable} A pollable which will resolve once the stream is ready to receive again. + */ + subscribe() { + throw new Error("Not implemented"); + } +} +const incomingDatagramStreamCreate = IncomingDatagramStream._create; +delete IncomingDatagramStream._create; + +export class OutgoingDatagramStream { + static _create(socket) { + const stream = new OutgoingDatagramStream(socket); + return stream; + } + + #socket = null; + constructor(socket) { + this.#socket = socket; + } + + /** + * + * @returns {bigint} + * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) + */ + checkSend() { + throw new Error("Not implemented"); + } + + /** + * + * @param {Datagram[]} datagrams + * @returns {bigint} + * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + * @throws {invalid-argument} The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) + * @throws {connection-refused} The connection was refused. (ECONNREFUSED) + * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) + */ + send(datagrams) { + const req = new SendWrap(); + const doSend = (data, port, host, family) => { + // setting hasCallback to false will make send() synchronous + // TODO: handle async send + const hasCallback = false; + const socket = this.#socket; + + let err = null; + if (family.toLocaleLowerCase() === "ipv4") { + err = socket.send(req, data, data.length, port, host, hasCallback); + } else if (family.toLocaleLowerCase() === "ipv6") { + err = socket.send6(req, data, data.length, port, host, hasCallback); + } + return err; + }; + + datagrams.forEach((datagram) => { + const { data, remoteAddress } = datagram; + const { tag: family, val } = remoteAddress; + const { address, port } = val; + const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); + console.error({ + err, + }); + }); + } + + /** + * + * @returns {Pollable} A pollable which will resolve once the stream is ready to send again. + */ + subscribe() { + throw new Error("Not implemented"); + } +} +const outgoingDatagramStreamCreate = OutgoingDatagramStream._create; +delete OutgoingDatagramStream._create; + + export class UdpSocketImpl { - /** @type {Socket} */ #socket = null; + /** @type {UDP} */ #socket = null; /** @type {Network} */ network = null; [symbolState] = { @@ -40,9 +168,20 @@ export class UdpSocketImpl { operationInProgress: false, ipv6Only: false, state: SocketConnectionState.Closed, + + // TODO: what these default values should be? + unicastHopLimit: 1, + receiveBufferSize: 1, + sendBufferSize: 1, }; - #socketOptions = {}; + #socketOptions = { + family: "ipv4", + localAddress: "", + localPort: 0, + remoteAddress: "", + remotePort: 0, + }; /** * @param {IpAddressFamily} addressFamily @@ -52,120 +191,8 @@ export class UdpSocketImpl { this.#socketOptions.family = addressFamily; this.#socket = new UDP(); - - const self = this; - - class IncomingDatagramStream { - static _create() { - const stream = new IncomingDatagramStream(); - return stream; - } - - /** - * - * @param {bigint} maxResults - * @returns {Datagram[]} - * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) - * @throws {not-in-progress} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {remote-unreachable} The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - * @throws {connection-refused} The connection was refused. (ECONNREFUSED) - * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) - */ - receive(maxResults) { - assert(self[symbolState].isBound === false, "invalid-state"); - assert(self[symbolState].operationInProgress === false, "not-in-progress"); - - if (maxResults === 0n) { - return []; - } - - const socket = self.#socket; - socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); - socket.onerror = (err) => console.log("recv error", err); - socket.recvStart(); - const datagrams = []; - return datagrams; - } - - /** - * - * @returns {Pollable} A pollable which will resolve once the stream is ready to receive again. - */ - subscribe() { - throw new Error("Not implemented"); - } - } - this.incomingDatagramStreamCreate = IncomingDatagramStream._create; - delete IncomingDatagramStream._create; - - class OutgoingDatagramStream { - static _create() { - const stream = new OutgoingDatagramStream(); - return stream; - } - - /** - * - * @returns {bigint} - * @throws {invalid-state} The socket is not bound to any local address. (EINVAL) - */ - checkSend() { - throw new Error("Not implemented"); - } - - /** - * - * @param {Datagram[]} datagrams - * @returns {bigint} - * @throws {invalid-argument} The `remote-address` has the wrong address family. (EAFNOSUPPORT) - * @throws {invalid-argument} `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) - * @throws {invalid-argument} The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - * @throws {invalid-argument} The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - * @throws {invalid-argument} The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) - * @throws {invalid-argument} The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) - * @throws {remote-unreachable} The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - * @throws {connection-refused} The connection was refused. (ECONNREFUSED) - * @throws {datagram-too-large} The datagram is too large. (EMSGSIZE) - */ - send(datagrams) { - const req = new SendWrap(); - const doSend = (data, port, host, family) => { - // setting hasCallback to false will make send() synchronous - // TODO: handle async send - const hasCallback = false; - const socket = self.#socket; - - let err = null; - if (family.toLocaleLowerCase() === "ipv4") { - err = socket.send(req, data, data.length, port, host, hasCallback); - } else if (family.toLocaleLowerCase() === "ipv6") { - err = socket.send6(req, data, data.length, port, host, hasCallback); - } - return err; - }; - - datagrams.forEach((datagram) => { - const { data, remoteAddress } = datagram; - const { tag: family, val } = remoteAddress; - const { address, port } = val; - const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); - console.error({ - err, - }); - }); - } - - /** - * - * @returns {Pollable} A pollable which will resolve once the stream is ready to send again. - */ - subscribe() { - throw new Error("Not implemented"); - } - } - this.outgoingDatagramStreamCreate = OutgoingDatagramStream._create; - delete OutgoingDatagramStream._create; } + /** * * @param {Network} network @@ -176,6 +203,8 @@ export class UdpSocketImpl { * @returns {void} */ startBind(network, localAddress) { + this[symbolState].operationInProgress = false; + const address = serializeIpAddress(localAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(address)}`; @@ -221,7 +250,9 @@ export class UdpSocketImpl { err = this.#socket.bind6(localAddress, localPort, flags); } - if (err) { + if (err === 0) { + this[symbolState].isBound = true; + } else { assert(err === -22, "address-in-use"); assert(err === -48, "address-in-use"); // macos assert(err === -49, "address-not-bindable"); @@ -229,7 +260,15 @@ export class UdpSocketImpl { assert(true, "unknown", err); } - this[symbolState].isBound = true; + this[symbolState].operationInProgress = false; + } + + /** + * Alias for startBind() and finishBind() + */ + bind(network, localAddress) { + this.startBind(network, localAddress); + this.finishBind(); } /** @@ -244,6 +283,8 @@ export class UdpSocketImpl { * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ #startConnect(network, remoteAddress = undefined) { + this[symbolState].operationInProgress = false; + const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; @@ -273,6 +314,7 @@ export class UdpSocketImpl { */ #finishConnect() { const { remoteAddress, remotePort } = this.#socketOptions; + this[symbolState].state = SocketConnectionState.Connecting; if (this[symbolState].state === SocketConnectionState.Connected) { // TODO: figure out how to reuse a connected socket @@ -280,6 +322,17 @@ export class UdpSocketImpl { } else { this.#socket.connect(remoteAddress, remotePort); } + + this[symbolState].operationInProgress = false; + this[symbolState].state = SocketConnectionState.Connected; + } + + /** + * Alias for startBind() and finishBind() + */ + #connect(network, remoteAddress = undefined) { + this.#startConnect(network, remoteAddress); + this.#finishConnect(); } /** @@ -298,9 +351,8 @@ export class UdpSocketImpl { stream(remoteAddress = undefined) { assert(this[symbolState].isBound === false, "invalid-state"); - this.#startConnect(this.network, remoteAddress); - this.#finishConnect(); - return [this.incomingDatagramStreamCreate(), this.outgoingDatagramStreamCreate()]; + this.#connect(this.network, remoteAddress); + return [incomingDatagramStreamCreate(this.#socket), outgoingDatagramStreamCreate(this.#socket)]; } /** @@ -327,14 +379,26 @@ export class UdpSocketImpl { /** * * @returns {IpSocketAddress} - * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) + * @throws {invalid-state} The socket is not streaming to a specific remote address. (ENOTCONN) */ remoteAddress() { - assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state", "The socket is not streaming to a specific remote address"); const out = {}; this.#socket.getpeername(out); + // Note: getpeername() returns undefined if family is ipv6 + // TODO: investigate + if (out.address === undefined) { + return { + tag: "ipv6", + val: { + address: "", + port: 0, + }, + }; + } + const { address, port, family } = out; return { tag: family.toLocaleLowerCase(), @@ -373,8 +437,8 @@ export class UdpSocketImpl { * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { + assert(value === true && this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); - assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); this[symbolState].ipv6Only = value; } @@ -384,7 +448,7 @@ export class UdpSocketImpl { * @returns {number} */ unicastHopLimit() { - return this.#socketOptions.unicastHopLimit; + return this[symbolState].unicastHopLimit; } /** @@ -396,7 +460,8 @@ export class UdpSocketImpl { setUnicastHopLimit(value) { assert(value < 1, "invalid-argument", "The TTL value must be 1 or higher"); - this.#socketOptions.unicastHopLimit = value; + this.#socket.setTTL(value); + this[symbolState].unicastHopLimit = value; } /** @@ -404,16 +469,33 @@ export class UdpSocketImpl { * @returns {bigint} */ receiveBufferSize() { - throw new Error("Not implemented"); + const exceptionInfo = {}; + const value = this.#socket.bufferSize(0, bufferSizeFlags.SO_RCVBUF, exceptionInfo); + + if (exceptionInfo.code === "EBADF") { + // TODO: handle the case where bad file descriptor is returned + // This happens when the socket is not bound + return this[symbolState].receiveBufferSize; + } + + return value; } /** * * @param {bigint} value * @returns {void} + * @throws {invalid-argument} The provided value was 0. */ setReceiveBufferSize(value) { - throw new Error("Not implemented"); + assert(value === 0n, "invalid-argument", "The provided value was 0"); + + // Note: libuv expects a uint32. Passing a bigint will result in an assertion error in V8. + value = Number(value); + + const exceptionInfo = {}; + this.#socket.bufferSize(Number(value), bufferSizeFlags.SO_RCVBUF, exceptionInfo); + this[symbolState].receiveBufferSize = value; } /** @@ -421,16 +503,33 @@ export class UdpSocketImpl { * @returns {bigint} */ sendBufferSize() { - throw new Error("Not implemented"); + const exceptionInfo = {}; + const value = this.#socket.bufferSize(0, bufferSizeFlags.SO_SNDBUF, exceptionInfo); + + if (exceptionInfo.code === "EBADF") { + // TODO: handle the case where bad file descriptor is returned + // This happens when the socket is not bound + return this[symbolState].sendBufferSize; + } + + return value; } /** * * @param {bigint} value * @returns {void} + * @throws {invalid-argument} The provided value was 0. */ setSendBufferSize(value) { - throw new Error("Not implemented"); + assert(value === 0n, "invalid-argument", "The provided value was 0"); + + // Note: libuv expects a uint32. Passing a bigint will result in an assertion error in V8. + value = Number(value); + + const exceptionInfo = {}; + this.#socket.bufferSize(value, bufferSizeFlags.SO_SNDBUF, exceptionInfo); + this[symbolState].sendBufferSize = value; } /** diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 9f83afce8..9bc4edfb6 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -7,7 +7,7 @@ */ import { TcpSocketImpl } from "./tcp-socket-impl.js"; -import { UdpSocketImpl } from "./udp-socket-impl.js"; +import { UdpSocketImpl, OutgoingDatagramStream, IncomingDatagramStream } from "./udp-socket-impl.js"; import { assert } from "../../common/assert.js"; /** @type {ErrorCode} */ @@ -140,8 +140,11 @@ export class WasiSockets { net.udpSockets.set(this.id, this); } } + this.udp = { - UdpSocket + UdpSocket, + OutgoingDatagramStream, + IncomingDatagramStream, }; class TcpSocket extends TcpSocketImpl { @@ -154,7 +157,7 @@ export class WasiSockets { } } this.tcp = { - TcpSocket + TcpSocket, }; this.instanceNetwork = { From 484598f39f3c072019afd0087e9e20df2470dfb9 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 21 Nov 2023 16:42:46 +0100 Subject: [PATCH 44/65] feat: add ip-name-lookup (wip) --- packages/preview2-shim/lib/io/calls.js | 4 ++ .../preview2-shim/lib/io/worker-thread.js | 33 ++++++++--- packages/preview2-shim/lib/nodejs/sockets.js | 1 + .../sockets/resolve-address-stream-impl.js | 55 +++++++++++++++++++ .../lib/nodejs/sockets/wasi-sockets.js | 37 ++++++++++++- 5 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js diff --git a/packages/preview2-shim/lib/io/calls.js b/packages/preview2-shim/lib/io/calls.js index bcb54090e..f5059951e 100644 --- a/packages/preview2-shim/lib/io/calls.js +++ b/packages/preview2-shim/lib/io/calls.js @@ -45,3 +45,7 @@ export const HTTP_CREATE_REQUEST = ++call_id << 24; export const CLOCKS_NOW = ++call_id << 24; export const CLOCKS_DURATION_SUBSCRIBE = ++call_id << 24; export const CLOCKS_INSTANT_SUBSCRIBE = ++call_id << 24; + +// Sockets +export const SOCKET_CREATE_RESOLVE_ADDRESS_STREAM = ++call_id << 24; +export const SOCKET_RESOLVE_NEXT_ADDRESS = ++call_id << 24; \ No newline at end of file diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/lib/io/worker-thread.js index 9e7d0ff4f..b890c188c 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/lib/io/worker-thread.js @@ -1,8 +1,7 @@ -import { runAsWorker } from "../synckit/index.js"; -import { FILE, STDOUT, STDERR, STDIN } from "./stream-types.js"; import { createReadStream, createWriteStream } from "node:fs"; -import { Readable } from "node:stream"; import { hrtime } from "node:process"; +import { Readable } from "node:stream"; +import { runAsWorker } from "../synckit/index.js"; import { CALL_MASK, CALL_SHIFT, @@ -10,8 +9,8 @@ import { CLOCKS_DURATION_SUBSCRIBE, CLOCKS_INSTANT_SUBSCRIBE, CLOCKS_NOW, - FUTURE_DISPOSE_AND_GET_VALUE, FUTURE_DISPOSE, + FUTURE_DISPOSE_AND_GET_VALUE, HTTP_CREATE_REQUEST, INPUT_STREAM_BLOCKING_READ, INPUT_STREAM_BLOCKING_SKIP, @@ -30,12 +29,15 @@ import { OUTPUT_STREAM_FLUSH, OUTPUT_STREAM_SPLICE, OUTPUT_STREAM_SUBSCRIBE, - OUTPUT_STREAM_WRITE_ZEROES, OUTPUT_STREAM_WRITE, - POLL_POLL_LIST, + OUTPUT_STREAM_WRITE_ZEROES, POLL_POLLABLE_BLOCK, POLL_POLLABLE_READY, + POLL_POLL_LIST, + SOCKET_CREATE_RESOLVE_ADDRESS_STREAM, + SOCKET_RESOLVE_NEXT_ADDRESS, } from "./calls.js"; +import { FILE, STDERR, STDIN, STDOUT } from "./stream-types.js"; let streamCnt = 0, pollCnt = 0; @@ -120,6 +122,15 @@ function handle(call, id, payload) { return createFuture(createHttpRequest(method, url, headers, body)); } + // Sockets + case SOCKET_CREATE_RESOLVE_ADDRESS_STREAM: { + return createDnsResolvePoll(payload.hostname); + } + case SOCKET_RESOLVE_NEXT_ADDRESS: { + const future = unfinishedPolls.get(id); + return Promise.resolve(future); + } + // Stdio case OUTPUT_STREAM_DISPOSE | STDOUT: case OUTPUT_STREAM_DISPOSE | STDERR: @@ -434,14 +445,16 @@ function handle(call, id, payload) { } // poll promises must always resolve and never error +// once the future is resolved, it is removed from unfinishedPolls function createPoll(promise) { const pollId = ++pollCnt; unfinishedPolls.set( pollId, promise.then( () => void unfinishedPolls.delete(pollId), - () => { + (err) => { process._rawDebug("Unexpected poll error"); + process._rawDebug(err); process.exit(1); } ) @@ -485,4 +498,10 @@ async function createHttpRequest(method, url, headers, body) { }; } +async function createDnsResolvePoll(hostname) { + let future = resolve(hostname); + unfinishedPolls.set(++pollCnt, future); + return pollCnt; +} + runAsWorker(handle); diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 4cc26a154..757048dfb 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -4,6 +4,7 @@ import { WasiSockets } from "./sockets/wasi-sockets.js"; const sockets = new WasiSockets(); export const { + ipNameLookup, instanceNetwork, network, tcpCreateSocket, diff --git a/packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js b/packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js new file mode 100644 index 000000000..c085bd87b --- /dev/null +++ b/packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js @@ -0,0 +1,55 @@ +/** + * @typedef {import("../../../types/interfaces/wasi-sockets-network").IpAddress} IpAddress + */ + +import { isIP } from "net"; +import { SOCKET_RESOLVE_NEXT_ADDRESS } from "../../io/calls.js"; +import { ioCall, pollableCreate } from "../../io/worker-io.js"; +import { deserializeIpAddress } from "./socket-common.js"; + +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); + +export class ResolveAddressStreamImpl { + _pollId = 0; + _addressIterator = null; + constructor() {} + + /** + * + * @returns {IpAddress} + * @throws {name-unresolvable} Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + * @throws {temporary-resolver-failure} A temporary failure in name resolution occurred. (EAI_AGAIN) + * @throws {permanent-resolver-failure} A permanent failure in name resolution occurred. (EAI_FAIL) + * @throws {would-block} A result is not available yet. (EWOULDBLOCK, EAGAIN) + */ + resolveNextAddress() { + const pollId = this._pollId; + if (!pollId) return null; + + // FIXME: addresses is undefined + let addresses = ioCall(SOCKET_RESOLVE_NEXT_ADDRESS, pollId, null); + if (!addresses) return null; + + addresses = addresses.map((address) => { + const family = `ipv${isIP(address)}`; + return { + tag: family, + val: deserializeIpAddress(address, family), + }; + }); + + const ip = addresses[this._addressIterator++]; + + return ip; + } + + subscribe() { + if (this._pollId) return pollableCreate(this._pollId); + // 0 poll is immediately resolving + return pollableCreate(0); + } + + [symbolDispose]() { + // if (this._pollId) ioCall(FUTURE_DISPOSE, this._pollId); + } +} diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 9bc4edfb6..752e536b9 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -2,13 +2,17 @@ * @typedef {import("../../../types/interfaces/wasi-sockets-network").Network} Network * @typedef {import("../../../types/interfaces/wasi-sockets-network").ErrorCode} ErrorCode * @typedef {import("../../../types/interfaces/wasi-sockets-network").IpAddressFamily} IpAddressFamily + * @typedef {import("../../../types/interfaces/wasi-sockets-network").IpAddress} IpAddress * @typedef {import("../../../types/interfaces/wasi-sockets-tcp").TcpSocket} TcpSocket * @typedef {import("../../../types/interfaces/wasi-sockets-udp").UdpSocket} UdpSocket */ -import { TcpSocketImpl } from "./tcp-socket-impl.js"; -import { UdpSocketImpl, OutgoingDatagramStream, IncomingDatagramStream } from "./udp-socket-impl.js"; import { assert } from "../../common/assert.js"; +import { SOCKET_CREATE_RESOLVE_ADDRESS_STREAM } from "../../io/calls.js"; +import { ioCall } from "../../io/worker-io.js"; +import { ResolveAddressStreamImpl } from "./resolve-address-stream-impl.js"; +import { TcpSocketImpl } from "./tcp-socket-impl.js"; +import { IncomingDatagramStream, OutgoingDatagramStream, UdpSocketImpl } from "./udp-socket-impl.js"; /** @type {ErrorCode} */ export const errorCode = { @@ -156,6 +160,7 @@ export class WasiSockets { net.tcpSockets.set(this.id, this); } } + this.tcp = { TcpSocket, }; @@ -232,5 +237,33 @@ export class WasiSockets { } }, }; + + class ResolveAddressStream extends ResolveAddressStreamImpl { + static _create(hostname) { + const res = new ResolveAddressStream(); + res._pollId = ioCall(SOCKET_CREATE_RESOLVE_ADDRESS_STREAM, null, { + hostname, + }); + return res; + } + } + + const resolveAddressStreamCreate = ResolveAddressStream._create; + delete ResolveAddressStream._create; + + this.ipNameLookup = { + ResolveAddressStream, + + /** + * + * @param {Network} network + * @param {string} name + * @returns {ResolveAddressStream} + * @throws {invalid-argument} `name` is a syntactically invalid domain name or IP address. + */ + resolveAddresses(network, name) { + return resolveAddressStreamCreate(name); + }, + }; } } From 0e8eb1f7e7c2e7e2650ae4369604c2b0248dab8a Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 21 Nov 2023 13:10:26 -0800 Subject: [PATCH 45/65] chore: socket resolve addresses (#1) --- packages/preview2-shim/lib/io/calls.js | 5 +- .../preview2-shim/lib/io/worker-thread.js | 32 ++++++----- .../lib/nodejs/sockets/wasi-sockets.js | 55 +++++++++++++++++-- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/packages/preview2-shim/lib/io/calls.js b/packages/preview2-shim/lib/io/calls.js index f5059951e..003ad708f 100644 --- a/packages/preview2-shim/lib/io/calls.js +++ b/packages/preview2-shim/lib/io/calls.js @@ -47,5 +47,6 @@ export const CLOCKS_DURATION_SUBSCRIBE = ++call_id << 24; export const CLOCKS_INSTANT_SUBSCRIBE = ++call_id << 24; // Sockets -export const SOCKET_CREATE_RESOLVE_ADDRESS_STREAM = ++call_id << 24; -export const SOCKET_RESOLVE_NEXT_ADDRESS = ++call_id << 24; \ No newline at end of file +export const SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST = ++call_id << 24; +export const SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST = ++call_id << 24; +export const SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST = ++call_id << 24; diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/lib/io/worker-thread.js index b890c188c..4197cf6a6 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/lib/io/worker-thread.js @@ -34,8 +34,9 @@ import { POLL_POLLABLE_BLOCK, POLL_POLLABLE_READY, POLL_POLL_LIST, - SOCKET_CREATE_RESOLVE_ADDRESS_STREAM, - SOCKET_RESOLVE_NEXT_ADDRESS, + SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, + SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, + SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, } from "./calls.js"; import { FILE, STDERR, STDIN, STDOUT } from "./stream-types.js"; @@ -123,12 +124,21 @@ function handle(call, id, payload) { } // Sockets - case SOCKET_CREATE_RESOLVE_ADDRESS_STREAM: { - return createDnsResolvePoll(payload.hostname); - } - case SOCKET_RESOLVE_NEXT_ADDRESS: { - const future = unfinishedPolls.get(id); - return Promise.resolve(future); + case SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST: + return createFuture(resolve(payload.hostname)); + case SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST: + return void unfinishedFutures.delete(id); + case SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST: { + const future = unfinishedFutures.get(id); + if (!future) { + // future not ready yet + if (unfinishedPolls.get(id)) { + throw 'would-block'; + } + throw new Error("future already got and dropped"); + } + unfinishedFutures.delete(id); + return future; } // Stdio @@ -498,10 +508,4 @@ async function createHttpRequest(method, url, headers, body) { }; } -async function createDnsResolvePoll(hostname) { - let future = resolve(hostname); - unfinishedPolls.set(++pollCnt, future); - return pollCnt; -} - runAsWorker(handle); diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 752e536b9..b08d3f60a 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -8,12 +8,15 @@ */ import { assert } from "../../common/assert.js"; -import { SOCKET_CREATE_RESOLVE_ADDRESS_STREAM } from "../../io/calls.js"; -import { ioCall } from "../../io/worker-io.js"; -import { ResolveAddressStreamImpl } from "./resolve-address-stream-impl.js"; +import { SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST } from "../../io/calls.js"; +import { ioCall, pollableCreate } from "../../io/worker-io.js"; import { TcpSocketImpl } from "./tcp-socket-impl.js"; +import { deserializeIpAddress } from "./socket-common.js"; +import { isIP } from "net"; import { IncomingDatagramStream, OutgoingDatagramStream, UdpSocketImpl } from "./udp-socket-impl.js"; +const symbolDispose = Symbol.dispose || Symbol.for('dispose'); + /** @type {ErrorCode} */ export const errorCode = { // ### GENERAL ERRORS ### @@ -238,10 +241,46 @@ export class WasiSockets { }, }; - class ResolveAddressStream extends ResolveAddressStreamImpl { + class ResolveAddressStream { + #pollId; + #data; + #curItem = 0; + #error; + resolveNextAddress () { + if (this.#error) + throw this.#error; + if (!this.#data) { + const { value: addresses, error } = ioCall(SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, this.#pollId); + if (error) + throw (this.#error = convertResolveAddressError(error)); + this.#data = addresses.map((address) => { + const family = `ipv${isIP(address)}`; + return { + tag: family, + val: deserializeIpAddress(address, family), + }; + }); + } + if (this.#curItem < this.#data.length) + return this.#data[this.#curItem++]; + return undefined; + } + subscribe () { + if (this.#data) return pollableCreate(0); + return pollableCreate(this.#pollId); + } + [symbolDispose] () { + if (!this.#data) + ioCall(SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST); + } static _create(hostname) { const res = new ResolveAddressStream(); - res._pollId = ioCall(SOCKET_CREATE_RESOLVE_ADDRESS_STREAM, null, { + if (hostname === '0.0.0.0') { + res.#pollId = 0; + res.#data = { tag: 'ipv4', val: [0, 0, 0, 0] }; + return res; + } + res.#pollId = ioCall(SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, null, { hostname, }); return res; @@ -267,3 +306,9 @@ export class WasiSockets { }; } } + +function convertResolveAddressError (err) { + switch (err.code) { + default: return 'unknown'; + } +} \ No newline at end of file From e134d09b5a2feaeeb53422e4864df6f3f339a7d2 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 21 Nov 2023 22:57:44 +0100 Subject: [PATCH 46/65] chore: add missing import from node:dns/promises --- packages/preview2-shim/lib/io/worker-thread.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/lib/io/worker-thread.js index 4197cf6a6..4b9348e1e 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/lib/io/worker-thread.js @@ -1,3 +1,4 @@ +import { resolve } from "node:dns/promises"; import { createReadStream, createWriteStream } from "node:fs"; import { hrtime } from "node:process"; import { Readable } from "node:stream"; @@ -35,8 +36,8 @@ import { POLL_POLLABLE_READY, POLL_POLL_LIST, SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, - SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, + SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, } from "./calls.js"; import { FILE, STDERR, STDIN, STDOUT } from "./stream-types.js"; From 888f873ff49857167ce7a6fc6be1b301c8e64a10 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 21 Nov 2023 23:02:47 +0100 Subject: [PATCH 47/65] chore: resolve ipv6 :: and ::1 --- .../preview2-shim/lib/nodejs/sockets/wasi-sockets.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index b08d3f60a..59cadad26 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -280,6 +280,16 @@ export class WasiSockets { res.#data = { tag: 'ipv4', val: [0, 0, 0, 0] }; return res; } + else if (hostname === '::') { + res.#pollId = 0; + res.#data = { tag: 'ipv6', val: [0, 0, 0, 0, 0, 0, 0, 0] }; + return res; + } + else if (hostname === '::1') { + res.#pollId = 0; + res.#data = { tag: 'ipv6', val: [0, 0, 0, 0, 0, 0, 0, 1] }; + return res; + } res.#pollId = ioCall(SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, null, { hostname, }); @@ -301,6 +311,7 @@ export class WasiSockets { * @throws {invalid-argument} `name` is a syntactically invalid domain name or IP address. */ resolveAddresses(network, name) { + // TODO: bind to network return resolveAddressStreamCreate(name); }, }; From b9e43d79c634c7f5dd115d180da42e537e96ccca Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 23 Nov 2023 09:25:35 +0100 Subject: [PATCH 48/65] fix(udp): make preview2_udp_sockopts apss --- .../lib/nodejs/sockets/socket-common.js | 7 ++ .../lib/nodejs/sockets/tcp-socket-impl.js | 2 +- .../lib/nodejs/sockets/udp-socket-impl.js | 87 ++++++++++--------- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index eafc8208c..4750ec38a 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -1,3 +1,10 @@ +export function cappedUint32(value) { + // Note: cap the value to the highest possible BigInt value that can be represented as a + // unsigned 32-bit integer. + const width = 32n; + return BigInt.asUintN(Number(width), value); +} + export function noop() {} function tupleToIPv6(arr) { diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 6e309c758..d444bb613 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -176,7 +176,7 @@ export class TcpSocketImpl { assert(err === -22, "address-in-use"); assert(err === -49, "address-not-bindable"); assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL - assert(true, "", err); + assert(true, "unknown", err); } this.#isBound = true; diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index d02400dcf..d9820008d 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -12,7 +12,7 @@ const { UDP, SendWrap } = process.binding("udp_wrap"); import { isIP } from "node:net"; import { assert } from "../../common/assert.js"; -import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; +import { deserializeIpAddress, cappedUint32, serializeIpAddress } from "./socket-common.js"; import { pollableCreate } from "../../io/worker-io.js"; const SocketConnectionState = { @@ -26,17 +26,16 @@ const SocketConnectionState = { const symbolState = Symbol("SocketInternalState"); // see https://github.com/libuv/libuv/blob/master/docs/src/udp.rst -const flags = { +const Flags = { UV_UDP_IPV6ONLY: 1, UV_UDP_REUSEADDR: 4, }; -const bufferSizeFlags = { +const BufferSizeFlags = { SO_RCVBUF: true, SO_SNDBUF: false, }; - export class IncomingDatagramStream { static _create(socket) { const stream = new IncomingDatagramStream(socket); @@ -158,7 +157,6 @@ export class OutgoingDatagramStream { const outgoingDatagramStreamCreate = OutgoingDatagramStream._create; delete OutgoingDatagramStream._create; - export class UdpSocketImpl { /** @type {UDP} */ #socket = null; /** @type {Network} */ network = null; @@ -238,9 +236,8 @@ export class UdpSocketImpl { const { localAddress, localPort, family } = this.#socketOptions; let flags = 0; - if (this[symbolState].ipv6Only) { - flags |= flags.UV_UDP_IPV6ONLY; + flags |= Flags.UV_UDP_IPV6ONLY; } let err = null; @@ -285,18 +282,17 @@ export class UdpSocketImpl { #startConnect(network, remoteAddress = undefined) { this[symbolState].operationInProgress = false; + if (remoteAddress === undefined || this[symbolState].state === SocketConnectionState.Connected) { + // reusing a connected socket. See #finishConnect() + return; + } + const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); - if (this[symbolState].state === SocketConnectionState.Connected) { - // reusing a connected socket. See #finishConnect() - return; - } else { - } - const { port } = remoteAddress.val; this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = port; @@ -350,8 +346,12 @@ export class UdpSocketImpl { */ stream(remoteAddress = undefined) { assert(this[symbolState].isBound === false, "invalid-state"); - this.#connect(this.network, remoteAddress); + + console.log({ + state: this[symbolState], + }) + return [incomingDatagramStreamCreate(this.#socket), outgoingDatagramStreamCreate(this.#socket)]; } @@ -382,23 +382,22 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not streaming to a specific remote address. (ENOTCONN) */ remoteAddress() { - assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state", "The socket is not streaming to a specific remote address"); + assert( + this[symbolState].state !== SocketConnectionState.Connected, + "invalid-state", + "The socket is not streaming to a specific remote address" + ); const out = {}; - this.#socket.getpeername(out); - - // Note: getpeername() returns undefined if family is ipv6 - // TODO: investigate - if (out.address === undefined) { - return { - tag: "ipv6", - val: { - address: "", - port: 0, - }, - }; - } + const r = this.#socket.getpeername(out); + + assert( + out.address === undefined, + "invalid-state", + "The socket is not streaming to a specific remote address" + ); + const { address, port, family } = out; return { tag: family.toLocaleLowerCase(), @@ -437,7 +436,11 @@ export class UdpSocketImpl { * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { - assert(value === true && this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); + assert( + value === true && this.#socketOptions.family.toLocaleLowerCase() === "ipv4", + "not-supported", + "Socket is an IPv4 socket." + ); assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); this[symbolState].ipv6Only = value; @@ -470,7 +473,7 @@ export class UdpSocketImpl { */ receiveBufferSize() { const exceptionInfo = {}; - const value = this.#socket.bufferSize(0, bufferSizeFlags.SO_RCVBUF, exceptionInfo); + const value = this.#socket.bufferSize(0, BufferSizeFlags.SO_RCVBUF, exceptionInfo); if (exceptionInfo.code === "EBADF") { // TODO: handle the case where bad file descriptor is returned @@ -478,6 +481,10 @@ export class UdpSocketImpl { return this[symbolState].receiveBufferSize; } + console.log({ + value + }) + return value; } @@ -490,12 +497,10 @@ export class UdpSocketImpl { setReceiveBufferSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0"); - // Note: libuv expects a uint32. Passing a bigint will result in an assertion error in V8. - value = Number(value); - + const cappedValue = cappedUint32(value); const exceptionInfo = {}; - this.#socket.bufferSize(Number(value), bufferSizeFlags.SO_RCVBUF, exceptionInfo); - this[symbolState].receiveBufferSize = value; + this.#socket.bufferSize(Number(cappedValue), BufferSizeFlags.SO_RCVBUF, exceptionInfo); + this[symbolState].receiveBufferSize = cappedValue; } /** @@ -504,8 +509,8 @@ export class UdpSocketImpl { */ sendBufferSize() { const exceptionInfo = {}; - const value = this.#socket.bufferSize(0, bufferSizeFlags.SO_SNDBUF, exceptionInfo); - + const value = this.#socket.bufferSize(0, BufferSizeFlags.SO_SNDBUF, exceptionInfo); + if (exceptionInfo.code === "EBADF") { // TODO: handle the case where bad file descriptor is returned // This happens when the socket is not bound @@ -524,12 +529,10 @@ export class UdpSocketImpl { setSendBufferSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0"); - // Note: libuv expects a uint32. Passing a bigint will result in an assertion error in V8. - value = Number(value); - + const cappedValue = cappedUint32(value); const exceptionInfo = {}; - this.#socket.bufferSize(value, bufferSizeFlags.SO_SNDBUF, exceptionInfo); - this[symbolState].sendBufferSize = value; + this.#socket.bufferSize(Number(cappedValue), BufferSizeFlags.SO_SNDBUF, exceptionInfo); + this[symbolState].sendBufferSize = cappedValue; } /** From ef33ccb5916900598a4be34b96182d9ca439539e Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 23 Nov 2023 10:01:30 +0100 Subject: [PATCH 49/65] chore: delete unused code --- .../sockets/resolve-address-stream-impl.js | 55 ------------------- .../lib/nodejs/sockets/udp-socket-impl.js | 5 -- 2 files changed, 60 deletions(-) delete mode 100644 packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js diff --git a/packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js b/packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js deleted file mode 100644 index c085bd87b..000000000 --- a/packages/preview2-shim/lib/nodejs/sockets/resolve-address-stream-impl.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @typedef {import("../../../types/interfaces/wasi-sockets-network").IpAddress} IpAddress - */ - -import { isIP } from "net"; -import { SOCKET_RESOLVE_NEXT_ADDRESS } from "../../io/calls.js"; -import { ioCall, pollableCreate } from "../../io/worker-io.js"; -import { deserializeIpAddress } from "./socket-common.js"; - -const symbolDispose = Symbol.dispose || Symbol.for("dispose"); - -export class ResolveAddressStreamImpl { - _pollId = 0; - _addressIterator = null; - constructor() {} - - /** - * - * @returns {IpAddress} - * @throws {name-unresolvable} Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) - * @throws {temporary-resolver-failure} A temporary failure in name resolution occurred. (EAI_AGAIN) - * @throws {permanent-resolver-failure} A permanent failure in name resolution occurred. (EAI_FAIL) - * @throws {would-block} A result is not available yet. (EWOULDBLOCK, EAGAIN) - */ - resolveNextAddress() { - const pollId = this._pollId; - if (!pollId) return null; - - // FIXME: addresses is undefined - let addresses = ioCall(SOCKET_RESOLVE_NEXT_ADDRESS, pollId, null); - if (!addresses) return null; - - addresses = addresses.map((address) => { - const family = `ipv${isIP(address)}`; - return { - tag: family, - val: deserializeIpAddress(address, family), - }; - }); - - const ip = addresses[this._addressIterator++]; - - return ip; - } - - subscribe() { - if (this._pollId) return pollableCreate(this._pollId); - // 0 poll is immediately resolving - return pollableCreate(0); - } - - [symbolDispose]() { - // if (this._pollId) ioCall(FUTURE_DISPOSE, this._pollId); - } -} diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index d9820008d..f8c8c1ada 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -347,11 +347,6 @@ export class UdpSocketImpl { stream(remoteAddress = undefined) { assert(this[symbolState].isBound === false, "invalid-state"); this.#connect(this.network, remoteAddress); - - console.log({ - state: this[symbolState], - }) - return [incomingDatagramStreamCreate(this.#socket), outgoingDatagramStreamCreate(this.#socket)]; } From 01f36834c7690ecf2c10b5b76b01b8a34368c22d Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 23 Nov 2023 13:29:42 +0100 Subject: [PATCH 50/65] chore: add comments --- .../preview2-shim/lib/nodejs/sockets/udp-socket-impl.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index f8c8c1ada..bfacb7dad 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -65,6 +65,7 @@ export class IncomingDatagramStream { return []; } + // TODO: not sure this is the right API to use! const socket = this.#socket; socket.onmessage = (...args) => console.log("recv onmessage", args[2].toString()); socket.onerror = (err) => console.log("recv error", err); @@ -253,6 +254,7 @@ export class UdpSocketImpl { assert(err === -22, "address-in-use"); assert(err === -48, "address-in-use"); // macos assert(err === -49, "address-not-bindable"); + assert(err === -98, "address-in-use"); // WSL assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL assert(true, "unknown", err); } @@ -352,6 +354,7 @@ export class UdpSocketImpl { /** * + * Note: Concurrent invocations of this test can yield port to be 0 on Windows/WSL. * @returns {IpSocketAddress} * @throws {invalid-state} The socket is not bound to any local address. */ @@ -384,8 +387,7 @@ export class UdpSocketImpl { ); const out = {}; - const r = this.#socket.getpeername(out); - + this.#socket.getpeername(out); assert( out.address === undefined, From e55f43b03af042a5572977509c1b2a8e1f0049d2 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 23 Nov 2023 15:57:13 +0100 Subject: [PATCH 51/65] chore: more conformance tests passing! --- packages/preview2-shim/lib/io/stream-types.js | 2 + packages/preview2-shim/lib/nodejs/sockets.js | 3 +- .../lib/nodejs/sockets/tcp-socket-impl.js | 313 ++++++++++++------ .../lib/nodejs/sockets/udp-socket-impl.js | 9 +- .../lib/nodejs/sockets/wasi-sockets.js | 79 +++-- 5 files changed, 270 insertions(+), 136 deletions(-) diff --git a/packages/preview2-shim/lib/io/stream-types.js b/packages/preview2-shim/lib/io/stream-types.js index 0b75e43b3..e70312dd8 100644 --- a/packages/preview2-shim/lib/io/stream-types.js +++ b/packages/preview2-shim/lib/io/stream-types.js @@ -6,3 +6,5 @@ export const STDERR = ++cnt; export const FILE = ++cnt; export const INCOMING_BODY = ++cnt; export const OUTGOING_BODY = ++cnt; +export const SOCKET_INPUT_STREAM = ++cnt; +export const SOCKET_OUTPUT_STREAM = ++cnt; diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index 757048dfb..c581bfa66 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -2,7 +2,6 @@ import { WasiSockets } from "./sockets/wasi-sockets.js"; -const sockets = new WasiSockets(); export const { ipNameLookup, instanceNetwork, @@ -11,4 +10,4 @@ export const { udpCreateSocket, tcp, udp, -} = sockets; \ No newline at end of file +} = new WasiSockets();; \ No newline at end of file diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index d444bb613..eb93966f6 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -12,30 +12,47 @@ * @typedef {import("../../../types/interfaces/wasi-clocks-monotonic-clock.js").Duration} Duration */ +import { isIP, Socket as NodeSocket } from "node:net"; +import { assert } from "../../common/assert.js"; import { streams } from "../io.js"; const { InputStream, OutputStream } = streams; -import { assert } from "../../common/assert.js"; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); +const symbolState = Symbol("SocketInternalState"); // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc -const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap"); +const { + TCP, + TCPConnectWrap, + constants: TCPConstants, +} = process.binding("tcp_wrap"); const { ShutdownWrap } = process.binding("stream_wrap"); -import { isIP, Socket as NodeSocket } from "node:net"; - -import { serializeIpAddress, deserializeIpAddress } from "./socket-common.js"; +import { + SOCKET_INPUT_STREAM, + SOCKET_OUTPUT_STREAM, +} from "../../io/stream-types.js"; +import { + inputStreamCreate, + outputStreamCreate, + pollableCreate, +} from "../../io/worker-io.js"; +import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; + +// TODO: move to a common const ShutdownType = { receive: "receive", send: "send", both: "both", }; -const SocketState = { +// TODO: move to a common +const SocketConnectionState = { Error: "Error", Closed: "Closed", - Connection: "Connection", - Listener: "Listener", + Connecting: "Connecting", + Connected: "Connected", + Listening: "Listening", }; // TODO: implement would-block exceptions @@ -45,30 +62,45 @@ export class TcpSocketImpl { /** @type {TCP.TCPConstants.SOCKET} */ #clientHandle = null; /** @type {Network} */ network = null; - #isBound = false; #socketOptions = {}; - #canReceive = true; - #canSend = true; - #ipv6Only = false; - #state = SocketState.Closed; - #inProgress = false; #connections = 0; - #keepAlive = false; - #keepAliveCount = 1; - #keepAliveIdleTime = 1; - #keepAliveInterval = 1; - #unicastHopLimit = 10; - #acceptedClient = null; + + #pollId = null; + + [symbolState] = { + isBound: false, + operationInProgress: false, + ipv6Only: false, + state: SocketConnectionState.Closed, + acceptedClient: null, + canReceive: true, + canSend: true, + + // TODO: what these default values should be? + backlogSize: 1, + keepAlive: false, + keepAliveCount: 1, + keepAliveIdleTime: 1, + keepAliveInterval: 1, + hopLimit: 1, + receiveBufferSize: 1, + sendBufferSize: 1, + }; // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; +c + // this is set by the TcpSocket child class + tcpSocketChildClassType = null; /** * @param {IpAddressFamily} addressFamily + * @param {TcpSocket} childClassType * @returns */ - constructor(addressFamily) { + constructor(addressFamily, childClassType) { this.#socketOptions.family = addressFamily; + this.tcpSocketChildClassType = childClassType; this.#clientHandle = new TCP(TCPConstants.SOCKET); this.#serverHandle = new TCP(TCPConstants.SERVER); @@ -82,12 +114,14 @@ export class TcpSocketImpl { assert(true, "", err); } - this.#acceptedClient = new NodeSocket({ handle: newClientSocket }); + this[symbolState].acceptedClient = new NodeSocket({ + handle: newClientSocket, + }); this.#connections++; // reserved - this.#acceptedClient.server = this.#serverHandle; - this.#acceptedClient._server = this.#serverHandle; - this.#acceptedClient._handle.onread = (nread, buffer) => { + this[symbolState].acceptedClient.server = this.#serverHandle; + this[symbolState].acceptedClient._server = this.#serverHandle; + this[symbolState].acceptedClient._handle.onread = (nread, buffer) => { if (nread > 0) { // TODO: handle data received from the client const data = buffer.toString("utf8", 0, nread); @@ -110,7 +144,7 @@ export class TcpSocketImpl { throw new Error(err); } - this.#state = "connected"; + this[symbolState].state = "connected"; } // TODO: is this needed? @@ -126,12 +160,20 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already bound. (EINVAL) */ startBind(network, localAddress) { - const address = serializeIpAddress(localAddress, this.#socketOptions.family); + const address = serializeIpAddress( + localAddress, + this.#socketOptions.family + ); const ipFamily = `ipv${isIP(address)}`; - assert(this.#isBound, "invalid-state", "The socket is already bound"); assert( - this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), + this[symbolState].isBound, + "invalid-state", + "The socket is already bound" + ); + assert( + this.#socketOptions.family.toLocaleLowerCase() !== + ipFamily.toLocaleLowerCase(), "invalid-argument", "The `local-address` has the wrong address family" ); @@ -147,7 +189,7 @@ export class TcpSocketImpl { this.#socketOptions.localAddress = address; this.#socketOptions.localPort = port; this.network = network; - this.#inProgress = true; + this[symbolState].operationInProgress = true; } /** @@ -159,7 +201,7 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { - assert(this.#inProgress === false, "not-in-progress"); + assert(this[symbolState].operationInProgress === false, "not-in-progress"); const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); @@ -179,8 +221,8 @@ export class TcpSocketImpl { assert(true, "unknown", err); } - this.#isBound = true; - this.#inProgress = false; + this[symbolState].isBound = true; + this[symbolState].operationInProgress = false; } /** @@ -198,22 +240,29 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) */ startConnect(network, remoteAddress) { - assert(this.network !== null && this.network.id !== network.id, "already-attached"); - assert(this.#state === "connected", "already-connected"); - assert(this.#state === "connection", "already-listening"); - assert(this.#inProgress, "concurrency-conflict"); + assert( + this.network !== null && this.network.id !== network.id, + "already-attached" + ); + assert(this[symbolState].state === "connected", "already-connected"); + assert(this[symbolState].state === "connection", "already-listening"); + assert(this[symbolState].operationInProgress, "concurrency-conflict"); const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-remote-address"); - assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "address-family-mismatch"); + assert( + this.#socketOptions.family.toLocaleLowerCase() !== + ipFamily.toLocaleLowerCase(), + "address-family-mismatch" + ); this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; this.network = network; - this.#inProgress = true; + this[symbolState].operationInProgress = true; } /** @@ -228,21 +277,25 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishConnect() { - assert(this.#inProgress === false, "not-in-progress"); + assert(this[symbolState].operationInProgress === false, "not-in-progress"); - const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; + const { localAddress, localPort, remoteAddress, remotePort, family } = + this.#socketOptions; const connectReq = new TCPConnectWrap(); let err = null; + let connect = "connect"; if (family.toLocaleLowerCase() === "ipv4") { - err = this.#clientHandle.connect(connectReq, remoteAddress, remotePort); + connect = "connect"; } else if (family.toLocaleLowerCase() === "ipv6") { - err = this.#clientHandle.connect6(connectReq, remoteAddress, remotePort); + connect = "connect6"; } + err = this.#clientHandle[connect](connectReq, remoteAddress, remotePort); + if (err) { console.error(`[tcp] connect error on socket: ${err}`); - this.#state = SocketState.Error; + this[symbolState].state = SocketConnectionState.Error; } connectReq.oncomplete = this.#onClientConnectComplete.bind(this); @@ -256,10 +309,12 @@ export class TcpSocketImpl { }; this.#clientHandle.readStart(); - this.#inProgress = false; + this[symbolState].operationInProgress = false; - // TODO: return InputStream and OutputStream - return []; + const streamId = this.#connections++; + const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, streamId); + const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, streamId); + return [inputStream, outputStream]; } /** @@ -269,11 +324,17 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. */ startListen() { - assert(this.#isBound === false, "invalid-state"); - assert(this.#state === SocketState.Connection, "invalid-state"); - assert(this.#state === SocketState.Listener, "invalid-state"); + assert(this[symbolState].isBound === false, "invalid-state"); + assert( + this[symbolState].state === SocketConnectionState.Connected, + "invalid-state" + ); + assert( + this[symbolState].state === SocketConnectionState.Listener, + "invalid-state" + ); - this.#inProgress = true; + this[symbolState].operationInProgress = true; } /** @@ -283,16 +344,18 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishListen() { - assert(this.#inProgress === false, "not-in-progress"); + assert(this[symbolState].operationInProgress === false, "not-in-progress"); const err = this.#serverHandle.listen(this.#backlog); if (err) { console.error(`[tcp] listen error on socket: ${err}`); this.#serverHandle.close(); + + // TODO: handle errors throw new Error(err); } - this.#inProgress = false; + this[symbolState].operationInProgress = false; } /** @@ -303,21 +366,33 @@ export class TcpSocketImpl { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ accept() { - // uv_accept is automatically called by uv_listen when a new connection is received. - - const self = this; - const outgoingStream = new OutputStream({ - write(bytes) { - self.#acceptedClient.write(bytes); - }, - }); - const ingoingStream = new InputStream({ - read(len) { - return self.#acceptedClient.read(len); - }, - }); - - return [this.#acceptedClient, ingoingStream, outgoingStream]; + // because we have to return a valid TcpSocket type, we need to declare this method + // on the child class, TcpSocket, and not here. Otherwise, we get: + // Error: Resource error: Not a valid "TcpSocket" resource. + const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, this.id); + const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, this.id); + + /// The returned socket is bound and in the Connection state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `ipv6-only` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + const socket = new this.tcpSocketChildClassType(this.addressFamily); + socket.setIpv6Only(this.ipv6Only()); + socket.setKeepAliveEnabled(this.keepAliveEnabled()); + socket.setKeepAliveIdleTime(this.keepAliveIdleTime()); + socket.setKeepAliveInterval(this.keepAliveInterval()); + socket.setKeepAliveCount(this.keepAliveCount()); + socket.setHopLimit(this.hopLimit()); + socket.setReceiveBufferSize(this.receiveBufferSize()); + socket.setSendBufferSize(this.sendBufferSize()); + return [socket, inputStream, outputStream]; } /** @@ -325,7 +400,7 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - assert(this.#isBound === false, "invalid-state"); + assert(this[symbolState].isBound === false, "invalid-state"); const { localAddress, localPort, family } = this.#socketOptions; return { @@ -342,13 +417,16 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - assert(this.#state !== SocketState.Connection, "invalid-state"); + assert( + this[symbolState].state !== SocketConnectionState.Connected, + "invalid-state" + ); return this.#socketOptions.remoteAddress; } isListening() { - return this.#state === SocketState.Listener; + return this[symbolState].state === SocketConnectionState.Listener; } /** @@ -363,7 +441,7 @@ export class TcpSocketImpl { * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { - return this.#ipv6Only; + return this[symbolState].ipv6Only; } /** @@ -374,24 +452,31 @@ export class TcpSocketImpl { * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { - this.#ipv6Only = value; + this[symbolState].ipv6Only = value; } /** * @param {bigint} value * @returns {void} * @throws {not-supported} (set) The platform does not support changing the backlog size after the initial listen. + * @throws {invalid-argument} (set) The provided value was 0. * @throws {invalid-state} (set) The socket is already in the Connection state. */ setListenBacklogSize(value) { - this.#backlog = value; + assert(value === 0n, "invalid-argument", "The provided value was 0."); + assert( + this[symbolState].state === SocketConnectionState.Connected, + "invalid-state" + ); + + this[symbolState].backlogSize = value; } /** * @returns {boolean} */ keepAliveEnabled() { - this.#keepAlive; + return this[symbolState].keepAlive; } /** @@ -399,13 +484,13 @@ export class TcpSocketImpl { * @returns {void} */ setKeepAliveEnabled(value) { - this.#keepAlive = value; this.#clientHandle.setKeepAlive(value); + this[symbolState].keepAlive = value; if (value) { - this.#clientHandle.setKeepAliveIdleTime(this.keepAliveIdleTime()); - this.#clientHandle.setKeepAliveInterval(this.keepAliveInterval()); - this.#clientHandle.setKeepAliveCount(this.keepAliveCount()); + this.setKeepAliveIdleTime(this.keepAliveIdleTime()); + this.setKeepAliveInterval(this.keepAliveInterval()); + this.setKeepAliveCount(this.keepAliveCount()); } } @@ -414,7 +499,7 @@ export class TcpSocketImpl { * @returns {Duration} */ keepAliveIdleTime() { - return this.#keepAliveIdleTime; + return this[symbolState].keepAliveIdleTime; } /** @@ -426,7 +511,7 @@ export class TcpSocketImpl { setKeepAliveIdleTime(value) { assert(value < 1, "invalid-argument", "The idle time must be 1 or higher."); - this.#keepAliveIdleTime = value; + this[symbolState].keepAliveIdleTime = value; } /** @@ -434,7 +519,7 @@ export class TcpSocketImpl { * @returns {Duration} */ keepAliveInterval() { - return this.#keepAliveInterval; + return this[symbolState].keepAliveInterval; } /** @@ -446,7 +531,7 @@ export class TcpSocketImpl { setKeepAliveInterval(value) { assert(value < 1, "invalid-argument", "The interval must be 1 or higher."); - this.#keepAliveInterval = value; + this[symbolState].keepAliveInterval = value; } /** @@ -454,7 +539,7 @@ export class TcpSocketImpl { * @returns {Duration} */ keepAliveCount() { - return this.#keepAliveCount; + return this[symbolState].keepAliveCount; } /** @@ -466,14 +551,15 @@ export class TcpSocketImpl { setKeepAliveCount(value) { assert(value < 1, "invalid-argument", "The count must be 1 or higher."); - this.#keepAliveCount = value; + // TODO: set this on the client socket as well + this[symbolState].keepAliveCount = value; } /** * @returns {number} */ - unicastHopLimit() { - return this.#unicastHopLimit; + hopLimit() { + return this[symbolState].hopLimit; } /** @@ -483,49 +569,78 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Connection state. * @throws {invalid-state} (set) The socket is already in the Listener state. */ - setUnicastHopLimit(value) { - this.#unicastHopLimit = value; + setHopLimit(value) { + assert( + !value || value < 1, + "invalid-argument", + "The TTL value must be 1 or higher." + ); + assert( + this[symbolState].state === SocketConnectionState.Connected, + "invalid-state" + ); + + // TODO: set this on the client socket as well + this[symbolState].hopLimit = value; } /** * @returns {bigint} */ receiveBufferSize() { - throw new Error("not implemented"); + return this[symbolState].receiveBufferSize; } /** * @param {number} value * @returns {void} + * @throws {not-supported} (set) The platform does not support changing the backlog size after the initial listen. + * @throws {invalid-argument} (set) The provided value was 0. * @throws {invalid-state} (set) The socket is already in the Connection state. - * @throws {invalid-state} (set) The socket is already in the Listener state. */ setReceiveBufferSize(value) { - throw new Error("not implemented"); + assert(value === 0n, "invalid-argument", "The provided value was 0."); + assert( + this[symbolState].state === SocketConnectionState.Connected, + "invalid-state" + ); + + // TODO: set this on the client socket as well + this[symbolState].receiveBufferSize = value; } /** * @returns {bigint} */ sendBufferSize() { - throw new Error("not implemented"); + return this[symbolState].sendBufferSize; } /** * @param {bigint} value * @returns {void} + * @throws {invalid-argument} (set) The provided value was 0. * @throws {invalid-state} (set) The socket is already in the Connection state. * @throws {invalid-state} (set) The socket is already in the Listener state. */ setSendBufferSize(value) { - throw new Error("not implemented"); + assert(value === 0n, "invalid-argument", "The provided value was 0."); + assert( + this[symbolState].state === SocketConnectionState.Connected, + "invalid-state" + ); + + // TODO: set this on the client socket as well + this[symbolState].sendBufferSize = value; } /** * @returns {Pollable} */ subscribe() { - throw new Error("not implemented"); + if (this.#pollId) return pollableCreate(this.#pollId); + // 0 poll is immediately resolving + return pollableCreate(0); } /** @@ -536,12 +651,12 @@ export class TcpSocketImpl { shutdown(shutdownType) { // TODO: figure out how to handle shutdownTypes if (shutdownType === ShutdownType.receive) { - this.#canReceive = false; + this[symbolState].canReceive = false; } else if (shutdownType === ShutdownType.send) { - this.#canSend = false; + this[symbolState].canSend = false; } else if (shutdownType === ShutdownType.both) { - this.#canReceive = false; - this.#canSend = false; + this[symbolState].canReceive = false; + this[symbolState].canSend = false; } const req = new ShutdownWrap(); diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index bfacb7dad..eee8af8f4 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -15,6 +15,9 @@ import { assert } from "../../common/assert.js"; import { deserializeIpAddress, cappedUint32, serializeIpAddress } from "./socket-common.js"; import { pollableCreate } from "../../io/worker-io.js"; +const symbolState = Symbol("SocketInternalState"); + +// TODO: move to a common const SocketConnectionState = { Error: "Error", Closed: "Closed", @@ -23,14 +26,14 @@ const SocketConnectionState = { Listening: "Listening", }; -const symbolState = Symbol("SocketInternalState"); - // see https://github.com/libuv/libuv/blob/master/docs/src/udp.rst +// TODO: move to a common const Flags = { UV_UDP_IPV6ONLY: 1, UV_UDP_REUSEADDR: 4, }; +// TODO: move to a common const BufferSizeFlags = { SO_RCVBUF: true, SO_SNDBUF: false, @@ -359,7 +362,7 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - assert(this[symbolState].isBound === false, "invalid-state"); + // assert(this[symbolState].isBound === false, "invalid-state"); const out = {}; this.#socket.getsockname(out); diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 59cadad26..07734116f 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -7,15 +7,32 @@ * @typedef {import("../../../types/interfaces/wasi-sockets-udp").UdpSocket} UdpSocket */ +import { isIP } from "net"; import { assert } from "../../common/assert.js"; -import { SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST } from "../../io/calls.js"; -import { ioCall, pollableCreate } from "../../io/worker-io.js"; -import { TcpSocketImpl } from "./tcp-socket-impl.js"; +import { + SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, + SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, + SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, +} from "../../io/calls.js"; +import { + SOCKET_INPUT_STREAM, + SOCKET_OUTPUT_STREAM, +} from "../../io/stream-types.js"; +import { + inputStreamCreate, + ioCall, + outputStreamCreate, + pollableCreate, +} from "../../io/worker-io.js"; import { deserializeIpAddress } from "./socket-common.js"; -import { isIP } from "net"; -import { IncomingDatagramStream, OutgoingDatagramStream, UdpSocketImpl } from "./udp-socket-impl.js"; +import { TcpSocketImpl } from "./tcp-socket-impl.js"; +import { + IncomingDatagramStream, + OutgoingDatagramStream, + UdpSocketImpl, +} from "./udp-socket-impl.js"; -const symbolDispose = Symbol.dispose || Symbol.for('dispose'); +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); /** @type {ErrorCode} */ export const errorCode = { @@ -159,8 +176,8 @@ export class WasiSockets { * @param {IpAddressFamily} addressFamily * */ constructor(addressFamily) { - super(addressFamily); - net.tcpSockets.set(this.id, this); + super(addressFamily, TcpSocket); + net.tcpSockets.set(this.id); } } @@ -189,8 +206,6 @@ export class WasiSockets { this.udpCreateSocket = { createUdpSocket(addressFamily) { - net.socketCnt++; - assert( supportedAddressFamilies.includes(addressFamily) === false, errorCode.notSupported, @@ -236,7 +251,8 @@ export class WasiSockets { net.socketCnt++; return new TcpSocket(addressFamily); } catch (err) { - assert(true, errorCode.notSupported, err); + // assert(true, errorCode.unknown, err); + throw err; } }, }; @@ -246,13 +262,14 @@ export class WasiSockets { #data; #curItem = 0; #error; - resolveNextAddress () { - if (this.#error) - throw this.#error; + resolveNextAddress() { + if (this.#error) throw this.#error; if (!this.#data) { - const { value: addresses, error } = ioCall(SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, this.#pollId); - if (error) - throw (this.#error = convertResolveAddressError(error)); + const { value: addresses, error } = ioCall( + SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, + this.#pollId + ); + if (error) throw (this.#error = convertResolveAddressError(error)); this.#data = addresses.map((address) => { const family = `ipv${isIP(address)}`; return { @@ -265,29 +282,26 @@ export class WasiSockets { return this.#data[this.#curItem++]; return undefined; } - subscribe () { + subscribe() { if (this.#data) return pollableCreate(0); return pollableCreate(this.#pollId); } - [symbolDispose] () { - if (!this.#data) - ioCall(SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST); + [symbolDispose]() { + if (!this.#data) ioCall(SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST); } static _create(hostname) { const res = new ResolveAddressStream(); - if (hostname === '0.0.0.0') { + if (hostname === "0.0.0.0") { res.#pollId = 0; - res.#data = { tag: 'ipv4', val: [0, 0, 0, 0] }; + res.#data = { tag: "ipv4", val: [0, 0, 0, 0] }; return res; - } - else if (hostname === '::') { + } else if (hostname === "::") { res.#pollId = 0; - res.#data = { tag: 'ipv6', val: [0, 0, 0, 0, 0, 0, 0, 0] }; + res.#data = { tag: "ipv6", val: [0, 0, 0, 0, 0, 0, 0, 0] }; return res; - } - else if (hostname === '::1') { + } else if (hostname === "::1") { res.#pollId = 0; - res.#data = { tag: 'ipv6', val: [0, 0, 0, 0, 0, 0, 0, 1] }; + res.#data = { tag: "ipv6", val: [0, 0, 0, 0, 0, 0, 0, 1] }; return res; } res.#pollId = ioCall(SOCKET_RESOLVE_ADDRESS_CREATE_REQUEST, null, { @@ -318,8 +332,9 @@ export class WasiSockets { } } -function convertResolveAddressError (err) { +function convertResolveAddressError(err) { switch (err.code) { - default: return 'unknown'; + default: + return "unknown"; } -} \ No newline at end of file +} From 3a3869ed7ed7047bdbeadf68e19e94bbc3f3a3bf Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Thu, 23 Nov 2023 22:11:55 +0100 Subject: [PATCH 52/65] chore: use enums for socket conn state --- .../preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index eb93966f6..311c46942 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -144,7 +144,7 @@ c throw new Error(err); } - this[symbolState].state = "connected"; + this[symbolState].state = SocketConnectionState.Connected; } // TODO: is this needed? @@ -244,8 +244,8 @@ c this.network !== null && this.network.id !== network.id, "already-attached" ); - assert(this[symbolState].state === "connected", "already-connected"); - assert(this[symbolState].state === "connection", "already-listening"); + assert(this[symbolState].state === SocketConnectionState.Connected, "already-connected"); + assert(this[symbolState].state === SocketConnectionState.Listening, "already-listening"); assert(this[symbolState].operationInProgress, "concurrency-conflict"); const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); From 58e9bcea452b08f70d9f09e91a330c3d9647f9ad Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 24 Nov 2023 00:19:05 +0100 Subject: [PATCH 53/65] chore: make preview2_tcp_states pass --- .../lib/nodejs/sockets/tcp-socket-impl.js | 164 ++++++++---------- 1 file changed, 68 insertions(+), 96 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 311c46942..e682102d6 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -13,6 +13,7 @@ */ import { isIP, Socket as NodeSocket } from "node:net"; +import { platform } from "node:os"; import { assert } from "../../common/assert.js"; import { streams } from "../io.js"; const { InputStream, OutputStream } = streams; @@ -21,29 +22,18 @@ const symbolDispose = Symbol.dispose || Symbol.for("dispose"); const symbolState = Symbol("SocketInternalState"); // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc -const { - TCP, - TCPConnectWrap, - constants: TCPConstants, -} = process.binding("tcp_wrap"); +const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap"); const { ShutdownWrap } = process.binding("stream_wrap"); -import { - SOCKET_INPUT_STREAM, - SOCKET_OUTPUT_STREAM, -} from "../../io/stream-types.js"; -import { - inputStreamCreate, - outputStreamCreate, - pollableCreate, -} from "../../io/worker-io.js"; +import { SOCKET_INPUT_STREAM, SOCKET_OUTPUT_STREAM } from "../../io/stream-types.js"; +import { inputStreamCreate, outputStreamCreate, pollableCreate } from "../../io/worker-io.js"; import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; // TODO: move to a common const ShutdownType = { - receive: "receive", - send: "send", - both: "both", + Receive: "receive", + Send: "send", + Both: "both", }; // TODO: move to a common @@ -89,7 +79,7 @@ export class TcpSocketImpl { // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; -c + c; // this is set by the TcpSocket child class tcpSocketChildClassType = null; @@ -160,30 +150,23 @@ c * @throws {invalid-state} The socket is already bound. (EINVAL) */ startBind(network, localAddress) { - const address = serializeIpAddress( - localAddress, - this.#socketOptions.family - ); + assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); + + const address = serializeIpAddress(localAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(address)}`; assert( - this[symbolState].isBound, - "invalid-state", - "The socket is already bound" - ); - assert( - this.#socketOptions.family.toLocaleLowerCase() !== - ipFamily.toLocaleLowerCase(), + this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument", "The `local-address` has the wrong address family" ); // TODO: assert localAddress is not an unicast address - assert( - ipFamily.toLocaleLowerCase() === "ipv4" && this.ipv6Only(), - "invalid-argument", - "`local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled." - ); + // assert( + // this.#socketOptions.family.toLocaleLowerCase() === "ipv4" && this.ipv6Only(), + // "invalid-argument", + // "`local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled." + // ); const { port } = localAddress.val; this.#socketOptions.localAddress = address; @@ -201,6 +184,9 @@ c * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { + // we reset the bound state to false, in case the last call to bind failed + this[symbolState].isBound = false; + assert(this[symbolState].operationInProgress === false, "not-in-progress"); const { localAddress, localPort, family } = this.#socketOptions; @@ -241,26 +227,26 @@ c */ startConnect(network, remoteAddress) { assert( - this.network !== null && this.network.id !== network.id, - "already-attached" + this[symbolState].isBound === false || + this[symbolState].state === SocketConnectionState.Connected || + this[symbolState].state === SocketConnectionState.Listener, + "invalid-state" ); - assert(this[symbolState].state === SocketConnectionState.Connected, "already-connected"); - assert(this[symbolState].state === SocketConnectionState.Listening, "already-listening"); - assert(this[symbolState].operationInProgress, "concurrency-conflict"); + assert(network !== this.network, "invalid-argument"); const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); const ipFamily = `ipv${isIP(host)}`; - assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-remote-address"); + assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); + assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); assert( - this.#socketOptions.family.toLocaleLowerCase() !== - ipFamily.toLocaleLowerCase(), - "address-family-mismatch" + remoteAddress.val.port === 0 && platform() === "win32", + "invalid-argument", + "The port in `remote-address` is set to 0." ); this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; - this.network = network; this[symbolState].operationInProgress = true; } @@ -279,8 +265,7 @@ c finishConnect() { assert(this[symbolState].operationInProgress === false, "not-in-progress"); - const { localAddress, localPort, remoteAddress, remotePort, family } = - this.#socketOptions; + const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; const connectReq = new TCPConnectWrap(); let err = null; @@ -325,14 +310,8 @@ c */ startListen() { assert(this[symbolState].isBound === false, "invalid-state"); - assert( - this[symbolState].state === SocketConnectionState.Connected, - "invalid-state" - ); - assert( - this[symbolState].state === SocketConnectionState.Listener, - "invalid-state" - ); + assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolState].state === SocketConnectionState.Listener, "invalid-state"); this[symbolState].operationInProgress = true; } @@ -356,6 +335,7 @@ c } this[symbolState].operationInProgress = false; + this[symbolState].state === SocketConnectionState.Listener; } /** @@ -366,24 +346,28 @@ c * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ accept() { - // because we have to return a valid TcpSocket type, we need to declare this method - // on the child class, TcpSocket, and not here. Otherwise, we get: - // Error: Resource error: Not a valid "TcpSocket" resource. + assert(this[symbolState].state !== SocketConnectionState.Listener, "invalid-state"); + const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, this.id); const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, this.id); - /// The returned socket is bound and in the Connection state. The following properties are inherited from the listener socket: - /// - `address-family` - /// - `ipv6-only` - /// - `keep-alive-enabled` - /// - `keep-alive-idle-time` - /// - `keep-alive-interval` - /// - `keep-alive-count` - /// - `hop-limit` - /// - `receive-buffer-size` - /// - `send-buffer-size` - /// + // Because we have to return a valid TcpSocket resrouces type, + // we need to instantiate the correct child class + // const socket = new this.tcpSocketChildClassType(this.addressFamily); + + // The returned socket is bound and in the Connection state. + // The following properties are inherited from the listener socket: + // - `address-family` + // - `ipv6-only` + // - `keep-alive-enabled` + // - `keep-alive-idle-time` + // - `keep-alive-interval` + // - `keep-alive-count` + // - `hop-limit` + // - `receive-buffer-size` + // - `send-buffer-size` + // socket.setIpv6Only(this.ipv6Only()); socket.setKeepAliveEnabled(this.keepAliveEnabled()); socket.setKeepAliveIdleTime(this.keepAliveIdleTime()); @@ -417,10 +401,7 @@ c * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - assert( - this[symbolState].state !== SocketConnectionState.Connected, - "invalid-state" - ); + assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); return this.#socketOptions.remoteAddress; } @@ -441,6 +422,8 @@ c * @throws {not-supported} (get/set) `this` socket is an IPv4 socket. */ ipv6Only() { + assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported"); + return this[symbolState].ipv6Only; } @@ -452,6 +435,9 @@ c * @throws {not-supported} (set) Host does not support dual-stack sockets. (Implementations are not required to.) */ setIpv6Only(value) { + assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported"); + assert(this[symbolState].isBound, "invalid-state"); + this[symbolState].ipv6Only = value; } @@ -464,10 +450,7 @@ c */ setListenBacklogSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert( - this[symbolState].state === SocketConnectionState.Connected, - "invalid-state" - ); + assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); this[symbolState].backlogSize = value; } @@ -570,15 +553,8 @@ c * @throws {invalid-state} (set) The socket is already in the Listener state. */ setHopLimit(value) { - assert( - !value || value < 1, - "invalid-argument", - "The TTL value must be 1 or higher." - ); - assert( - this[symbolState].state === SocketConnectionState.Connected, - "invalid-state" - ); + assert(!value || value < 1, "invalid-argument", "The TTL value must be 1 or higher."); + assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well this[symbolState].hopLimit = value; @@ -600,10 +576,7 @@ c */ setReceiveBufferSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert( - this[symbolState].state === SocketConnectionState.Connected, - "invalid-state" - ); + assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well this[symbolState].receiveBufferSize = value; @@ -625,10 +598,7 @@ c */ setSendBufferSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert( - this[symbolState].state === SocketConnectionState.Connected, - "invalid-state" - ); + assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well this[symbolState].sendBufferSize = value; @@ -649,12 +619,14 @@ c * @throws {invalid-state} The socket is not in the Connection state. (ENOTCONN) */ shutdown(shutdownType) { + assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); + // TODO: figure out how to handle shutdownTypes - if (shutdownType === ShutdownType.receive) { + if (shutdownType === ShutdownType.Receive) { this[symbolState].canReceive = false; - } else if (shutdownType === ShutdownType.send) { + } else if (shutdownType === ShutdownType.Send) { this[symbolState].canSend = false; - } else if (shutdownType === ShutdownType.both) { + } else if (shutdownType === ShutdownType.Both) { this[symbolState].canReceive = false; this[symbolState].canSend = false; } From c0c2839b957221c137d6ece6e29afb48278c1e10 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 24 Nov 2023 12:24:13 +0100 Subject: [PATCH 54/65] fix: make preview2_tcp_connect pass (wip) --- .../lib/nodejs/sockets/socket-common.js | 21 ++++++++++++ .../lib/nodejs/sockets/tcp-socket-impl.js | 33 ++++++++++++++----- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index 4750ec38a..a7597f8ad 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -65,3 +65,24 @@ export function deserializeIpAddress(addr, family) { } return address; } + +export function isUnicastIpAddress(ipSocketAddress) { + return ipSocketAddress.val.address[0] !== 0xff; // 255 +} + +export function isMulticastIpAddress(ipSocketAddress) { + return ipSocketAddress.val.address[0] === 224 || ipSocketAddress.val.address[0] === 0xff00; +} + +export function isBroadcastIpAddress(ipSocketAddress) { + return ( + ipSocketAddress.val.address[0] === 0xff && // 255 + ipSocketAddress.val.address[1] === 0xff && // 255 + ipSocketAddress.val.address[2] === 0xff && // 255 + ipSocketAddress.val.address[3] === 0xff // 255 + ); +} + +export function isIPv4MappedAddress(ipSocketAddress) { + return ipSocketAddress.val.address[5] === 0xffff; +} diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index e682102d6..41099c8fc 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -27,7 +27,13 @@ const { ShutdownWrap } = process.binding("stream_wrap"); import { SOCKET_INPUT_STREAM, SOCKET_OUTPUT_STREAM } from "../../io/stream-types.js"; import { inputStreamCreate, outputStreamCreate, pollableCreate } from "../../io/worker-io.js"; -import { deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; +import { + deserializeIpAddress, + isIPv4MappedAddress, + isMulticastIpAddress, + isUnicastIpAddress, + serializeIpAddress, +} from "./socket-common.js"; // TODO: move to a common const ShutdownType = { @@ -79,7 +85,6 @@ export class TcpSocketImpl { // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 #backlog = 128; - c; // this is set by the TcpSocket child class tcpSocketChildClassType = null; @@ -101,7 +106,7 @@ export class TcpSocketImpl { #handleConnection(err, newClientSocket) { if (err) { - assert(true, "", err); + assert(true, "unknown", err); } this[symbolState].acceptedClient = new NodeSocket({ @@ -185,6 +190,7 @@ export class TcpSocketImpl { **/ finishBind() { // we reset the bound state to false, in case the last call to bind failed + // when this methods returns successfully, we set isBound=true again this[symbolState].isBound = false; assert(this[symbolState].operationInProgress === false, "not-in-progress"); @@ -226,6 +232,17 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) */ startConnect(network, remoteAddress) { + const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); + const ipFamily = `ipv${isIP(host)}`; + + assert(host === "0.0.0.0" || host === "0:0:0:0:0:0:0:0", "invalid-argument"); + assert(remoteAddress.val.port === 0, "invalid-argument"); + assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); + + assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument"); + assert(isMulticastIpAddress(remoteAddress) === true, "invalid-argument"); + assert(this.ipv6Only() && isIPv4MappedAddress(remoteAddress) === true, "invalid-argument"); + assert( this[symbolState].isBound === false || this[symbolState].state === SocketConnectionState.Connected || @@ -233,12 +250,7 @@ export class TcpSocketImpl { "invalid-state" ); assert(network !== this.network, "invalid-argument"); - - const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); - const ipFamily = `ipv${isIP(host)}`; - assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); - assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); assert( remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument", @@ -384,7 +396,10 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - assert(this[symbolState].isBound === false, "invalid-state"); + if (this.isListening()) { + // we are running in server mode + assert(this[symbolState].isBound === false, "invalid-state"); + } const { localAddress, localPort, family } = this.#socketOptions; return { From 9f77ac161bb14ed81305822e30248daa864e866d Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 24 Nov 2023 12:27:10 +0100 Subject: [PATCH 55/65] fix: refactor isUnicastIpAddress --- packages/preview2-shim/lib/nodejs/sockets/socket-common.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index a7597f8ad..c156ccfb8 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -67,14 +67,17 @@ export function deserializeIpAddress(addr, family) { } export function isUnicastIpAddress(ipSocketAddress) { - return ipSocketAddress.val.address[0] !== 0xff; // 255 + return !isMulticastIpAddress(ipSocketAddress) && !isBroadcastIpAddress(ipSocketAddress); } export function isMulticastIpAddress(ipSocketAddress) { + // ipv6: [0xff00, 0, 0, 0, 0, 0, 0, 0] + // ipv4: [224, 0, 0, 0] return ipSocketAddress.val.address[0] === 224 || ipSocketAddress.val.address[0] === 0xff00; } export function isBroadcastIpAddress(ipSocketAddress) { + // ipv4: [255, 255, 255, 255] return ( ipSocketAddress.val.address[0] === 0xff && // 255 ipSocketAddress.val.address[1] === 0xff && // 255 From 881baa6c4217893379e04e2124b78ac08a2d98ad Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Fri, 24 Nov 2023 16:50:06 +0100 Subject: [PATCH 56/65] fix: make preview2_tcp_bind pass --- .../lib/nodejs/sockets/tcp-socket-impl.js | 93 ++++++++++--------- .../lib/nodejs/sockets/udp-socket-impl.js | 2 +- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 41099c8fc..4d3bdca48 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -51,11 +51,16 @@ const SocketConnectionState = { Listening: "Listening", }; +// As a workaround, we store the bound address in a global map +// this is needed because 'address-in-use' is not always thrown when binding +// more than one socket to the same address +// TODO: remove this workaround when we figure out why! +const globalBoundAddresses = new Map(); + // TODO: implement would-block exceptions // TODO: implement concurrency-conflict exceptions export class TcpSocketImpl { - /** @type {TCP.TCPConstants.SERVER} */ #serverHandle = null; - /** @type {TCP.TCPConstants.SOCKET} */ #clientHandle = null; + /** @type {TCP.TCPConstants.SOCKET} */ #socket = null; /** @type {Network} */ network = null; #socketOptions = {}; @@ -91,15 +96,13 @@ export class TcpSocketImpl { /** * @param {IpAddressFamily} addressFamily * @param {TcpSocket} childClassType - * @returns */ constructor(addressFamily, childClassType) { this.#socketOptions.family = addressFamily; this.tcpSocketChildClassType = childClassType; - this.#clientHandle = new TCP(TCPConstants.SOCKET); - this.#serverHandle = new TCP(TCPConstants.SERVER); - this._handle = this.#serverHandle; + this.#socket = new TCP(TCPConstants.SOCKET | TCPConstants.SERVER); + this._handle = this.#socket; this._handle.onconnection = this.#handleConnection.bind(this); this._handle.onclose = this.#handleDisconnect.bind(this); } @@ -109,23 +112,30 @@ export class TcpSocketImpl { assert(true, "unknown", err); } + this.#connections++; + this[symbolState].acceptedClient = new NodeSocket({ handle: newClientSocket, }); - this.#connections++; - // reserved - this[symbolState].acceptedClient.server = this.#serverHandle; - this[symbolState].acceptedClient._server = this.#serverHandle; + this[symbolState].acceptedClient.server = this.#socket; + this[symbolState].acceptedClient._server = this.#socket; + + // TODO: handle data received from the client this[symbolState].acceptedClient._handle.onread = (nread, buffer) => { if (nread > 0) { - // TODO: handle data received from the client const data = buffer.toString("utf8", 0, nread); console.log("accepted socket on read:", data); } }; } - #handleDisconnect(err) {} + #handleDisconnect(err) { + if (err) { + assert(true, "unknown", err); + } + + this.#connections--; + } #onClientConnectComplete(err) { if (err) { @@ -166,12 +176,8 @@ export class TcpSocketImpl { "The `local-address` has the wrong address family" ); - // TODO: assert localAddress is not an unicast address - // assert( - // this.#socketOptions.family.toLocaleLowerCase() === "ipv4" && this.ipv6Only(), - // "invalid-argument", - // "`local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled." - // ); + assert(isUnicastIpAddress(localAddress) === false, "invalid-argument"); + assert(isIPv4MappedAddress(localAddress) && this.ipv6Only() === true, "invalid-argument"); const { port } = localAddress.val; this.#socketOptions.localAddress = address; @@ -192,21 +198,22 @@ export class TcpSocketImpl { // we reset the bound state to false, in case the last call to bind failed // when this methods returns successfully, we set isBound=true again this[symbolState].isBound = false; - + assert(this[symbolState].operationInProgress === false, "not-in-progress"); - + const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); + assert(globalBoundAddresses.has(localAddress), "address-in-use"); let err = null; if (family.toLocaleLowerCase() === "ipv4") { - err = this.#serverHandle.bind(localAddress, localPort); + err = this.#socket.bind(localAddress, localPort); } else if (family.toLocaleLowerCase() === "ipv6") { - err = this.#serverHandle.bind6(localAddress, localPort); + err = this.#socket.bind6(localAddress, localPort); } if (err) { - this.#serverHandle.close(); + this.#socket.close(); assert(err === -22, "address-in-use"); assert(err === -49, "address-not-bindable"); assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL @@ -215,6 +222,8 @@ export class TcpSocketImpl { this[symbolState].isBound = true; this[symbolState].operationInProgress = false; + + globalBoundAddresses.set(localAddress, this.#socket); } /** @@ -241,7 +250,7 @@ export class TcpSocketImpl { assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument"); assert(isMulticastIpAddress(remoteAddress) === true, "invalid-argument"); - assert(this.ipv6Only() && isIPv4MappedAddress(remoteAddress) === true, "invalid-argument"); + assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only() === true, "invalid-argument"); assert( this[symbolState].isBound === false || @@ -288,7 +297,7 @@ export class TcpSocketImpl { connect = "connect6"; } - err = this.#clientHandle[connect](connectReq, remoteAddress, remotePort); + err = this.#socket[connect](connectReq, remoteAddress, remotePort); if (err) { console.error(`[tcp] connect error on socket: ${err}`); @@ -301,11 +310,11 @@ export class TcpSocketImpl { connectReq.localAddress = localAddress; connectReq.localPort = localPort; - this.#clientHandle.onread = (buffer) => { + this.#socket.onread = (buffer) => { // TODO: handle data received from the server }; - this.#clientHandle.readStart(); + this.#socket.readStart(); this[symbolState].operationInProgress = false; const streamId = this.#connections++; @@ -337,10 +346,10 @@ export class TcpSocketImpl { finishListen() { assert(this[symbolState].operationInProgress === false, "not-in-progress"); - const err = this.#serverHandle.listen(this.#backlog); + const err = this.#socket.listen(this.#backlog); if (err) { console.error(`[tcp] listen error on socket: ${err}`); - this.#serverHandle.close(); + this.#socket.close(); // TODO: handle errors throw new Error(err); @@ -396,17 +405,17 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - if (this.isListening()) { - // we are running in server mode - assert(this[symbolState].isBound === false, "invalid-state"); - } + assert(this[symbolState].isBound === false, "invalid-state"); - const { localAddress, localPort, family } = this.#socketOptions; + const out = {}; + this.#socket.getsockname(out); + + const { address, port, family } = out; return { - tag: family, + tag: family.toLocaleLowerCase(), val: { - address: deserializeIpAddress(localAddress, family), - port: localPort, + address: deserializeIpAddress(address, family), + port, }, }; } @@ -482,7 +491,7 @@ export class TcpSocketImpl { * @returns {void} */ setKeepAliveEnabled(value) { - this.#clientHandle.setKeepAlive(value); + this.#socket.setKeepAlive(value); this[symbolState].keepAlive = value; if (value) { @@ -656,14 +665,14 @@ export class TcpSocketImpl { } [symbolDispose]() { - this.#serverHandle.close(); - this.#clientHandle.close(); + this.#socket.close(); + globalBoundAddresses.delete(this.#socketOptions.localAddress); } server() { - return this.#serverHandle; + return this.#socket; } client() { - return this.#clientHandle; + return this.#socket; } } diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index eee8af8f4..6ddc7ade8 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -362,7 +362,7 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - // assert(this[symbolState].isBound === false, "invalid-state"); + assert(this[symbolState].isBound === false, "invalid-state"); const out = {}; this.#socket.getsockname(out); From ab406e78f3301f6c8ad4707028354783f9cb3af4 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sat, 25 Nov 2023 00:01:03 +0100 Subject: [PATCH 57/65] fix: make preview2_tcp_connect pass --- .../lib/nodejs/sockets/socket-common.js | 14 ++++++ .../lib/nodejs/sockets/tcp-socket-impl.js | 48 +++++++++++-------- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index c156ccfb8..1491db455 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -66,6 +66,20 @@ export function deserializeIpAddress(addr, family) { return address; } +export function findUnsuedLocalAddress(family) { + let address = [127, 0, 0, 1]; + if (family.toLocaleLowerCase() === "ipv6") { + address = [0, 0, 0, 0, 0, 0, 0, 1]; + } + return { + tag: family, + val: { + address, + port: 0, + }, + }; +} + export function isUnicastIpAddress(ipSocketAddress) { return !isMulticastIpAddress(ipSocketAddress) && !isBroadcastIpAddress(ipSocketAddress); } diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 4d3bdca48..0acc690b1 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -29,6 +29,7 @@ import { SOCKET_INPUT_STREAM, SOCKET_OUTPUT_STREAM } from "../../io/stream-types import { inputStreamCreate, outputStreamCreate, pollableCreate } from "../../io/worker-io.js"; import { deserializeIpAddress, + findUnsuedLocalAddress, isIPv4MappedAddress, isMulticastIpAddress, isUnicastIpAddress, @@ -155,6 +156,14 @@ export class TcpSocketImpl { // TODO: is this needed? #handleAfterShutdown() {} + #autoBind(network, ipFamily) { + const unsusedLocalAddress = findUnsuedLocalAddress(ipFamily); + this.#socketOptions.localAddress = serializeIpAddress(unsusedLocalAddress, this.#socketOptions.family); + this.#socketOptions.localPort = unsusedLocalAddress.val.port; + this.startBind(network, unsusedLocalAddress); + this.finishBind(); + } + /** * @param {Network} network * @param {IpSocketAddress} localAddress @@ -177,7 +186,7 @@ export class TcpSocketImpl { ); assert(isUnicastIpAddress(localAddress) === false, "invalid-argument"); - assert(isIPv4MappedAddress(localAddress) && this.ipv6Only() === true, "invalid-argument"); + assert(isIPv4MappedAddress(localAddress) && this.ipv6Only(), "invalid-argument"); const { port } = localAddress.val; this.#socketOptions.localAddress = address; @@ -198,9 +207,9 @@ export class TcpSocketImpl { // we reset the bound state to false, in case the last call to bind failed // when this methods returns successfully, we set isBound=true again this[symbolState].isBound = false; - + assert(this[symbolState].operationInProgress === false, "not-in-progress"); - + const { localAddress, localPort, family } = this.#socketOptions; assert(isIP(localAddress) === 0, "address-not-bindable"); assert(globalBoundAddresses.has(localAddress), "address-in-use"); @@ -222,7 +231,7 @@ export class TcpSocketImpl { this[symbolState].isBound = true; this[symbolState].operationInProgress = false; - + globalBoundAddresses.set(localAddress, this.#socket); } @@ -247,24 +256,23 @@ export class TcpSocketImpl { assert(host === "0.0.0.0" || host === "0:0:0:0:0:0:0:0", "invalid-argument"); assert(remoteAddress.val.port === 0, "invalid-argument"); assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); - assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument"); - assert(isMulticastIpAddress(remoteAddress) === true, "invalid-argument"); - assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only() === true, "invalid-argument"); + assert(isMulticastIpAddress(remoteAddress), "invalid-argument"); + assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only(), "invalid-argument"); + + if (this[symbolState].isBound === false) { + this.#autoBind(network, ipFamily); + } assert( - this[symbolState].isBound === false || - this[symbolState].state === SocketConnectionState.Connected || + this[symbolState].state === SocketConnectionState.Connected || this[symbolState].state === SocketConnectionState.Listener, "invalid-state" ); + assert(network !== this.network, "invalid-argument"); assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); - assert( - remoteAddress.val.port === 0 && platform() === "win32", - "invalid-argument", - "The port in `remote-address` is set to 0." - ); + assert(remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument"); this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; @@ -290,10 +298,8 @@ export class TcpSocketImpl { const connectReq = new TCPConnectWrap(); let err = null; - let connect = "connect"; - if (family.toLocaleLowerCase() === "ipv4") { - connect = "connect"; - } else if (family.toLocaleLowerCase() === "ipv6") { + let connect = "connect"; // ipv4 + if (family.toLocaleLowerCase() === "ipv6") { connect = "connect6"; } @@ -367,6 +373,10 @@ export class TcpSocketImpl { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ accept() { + if (this[symbolState].isBound === false) { + this.#autoBind(this.network, ipFamily); + } + assert(this[symbolState].state !== SocketConnectionState.Listener, "invalid-state"); const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, this.id); @@ -375,7 +385,7 @@ export class TcpSocketImpl { // Because we have to return a valid TcpSocket resrouces type, // we need to instantiate the correct child class // - const socket = new this.tcpSocketChildClassType(this.addressFamily); + const socket = new this.tcpSocketChildClassType(this.addressFamily()); // The returned socket is bound and in the Connection state. // The following properties are inherited from the listener socket: From 5871ad407914671666db5ccee45e0a6aa1ccf2c8 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 26 Nov 2023 01:26:15 +0100 Subject: [PATCH 58/65] chore: refactor code --- .../lib/nodejs/sockets/socket-common.js | 13 +- .../lib/nodejs/sockets/tcp-socket-impl.js | 381 +++++++++++------- .../lib/nodejs/sockets/udp-socket-impl.js | 8 +- .../lib/nodejs/sockets/wasi-sockets.js | 11 +- 4 files changed, 264 insertions(+), 149 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index 1491db455..683a4ce25 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -42,17 +42,24 @@ function ipv4ToTuple(ipv4) { return ipv4.split(".").map((segment) => parseInt(segment, 10)); } -export function serializeIpAddress(addr = undefined, family) { +export function serializeIpAddress(addr = undefined, includePort = false) { if (addr === undefined) { addr = { val: { address: [] } }; } + const family = addr.tag; + let { address } = addr.val; if (family.toLocaleLowerCase() === "ipv4") { address = tupleToIpv4(address); } else if (family.toLocaleLowerCase() === "ipv6") { address = tupleToIPv6(address); } + + if (includePort) { + address = `${address}:${addr.val.port}`; + } + return address; } @@ -101,5 +108,9 @@ export function isBroadcastIpAddress(ipSocketAddress) { } export function isIPv4MappedAddress(ipSocketAddress) { + // ipv6: [0, 0, 0, 0, 0, 0xffff, 0, 0] + if (ipSocketAddress.val.address.length !== 8) { + return false; + } return ipSocketAddress.val.address[5] === 0xffff; } diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 0acc690b1..a53eab940 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -19,7 +19,8 @@ import { streams } from "../io.js"; const { InputStream, OutputStream } = streams; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); -const symbolState = Symbol("SocketInternalState"); +const symbolSocketState = Symbol("SocketInternalState"); +const symbolOperations = Symbol("SocketOperationsState"); // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap"); @@ -61,6 +62,7 @@ const globalBoundAddresses = new Map(); // TODO: implement would-block exceptions // TODO: implement concurrency-conflict exceptions export class TcpSocketImpl { + id = 1; /** @type {TCP.TCPConstants.SOCKET} */ #socket = null; /** @type {Network} */ network = null; @@ -69,17 +71,30 @@ export class TcpSocketImpl { #pollId = null; - [symbolState] = { + // track in-progress operations + // counter must be 0 for the operation to be considered complete + // we increment the counter when the operation starts + // and decrement it when the operation finishes + [symbolOperations] = { + bind: 0, + connect: 0, + listen: 0, + accept: 0, + }; + + [symbolSocketState] = { + errorState: null, isBound: false, - operationInProgress: false, ipv6Only: false, - state: SocketConnectionState.Closed, + connectionState: SocketConnectionState.Closed, acceptedClient: null, canReceive: true, canSend: true, + // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 + backlogSize: 128, + // TODO: what these default values should be? - backlogSize: 1, keepAlive: false, keepAliveCount: 1, keepAliveIdleTime: 1, @@ -89,23 +104,20 @@ export class TcpSocketImpl { sendBufferSize: 1, }; - // See: https://github.com/torvalds/linux/blob/fe3cfe869d5e0453754cf2b4c75110276b5e8527/net/core/request_sock.c#L19-L31 - #backlog = 128; // this is set by the TcpSocket child class - tcpSocketChildClassType = null; + #tcpSocketChildClassType = null; /** * @param {IpAddressFamily} addressFamily * @param {TcpSocket} childClassType */ - constructor(addressFamily, childClassType) { - this.#socketOptions.family = addressFamily; - this.tcpSocketChildClassType = childClassType; + constructor(addressFamily, childClassType, id) { + this.id = id; + + this.#socketOptions.family = addressFamily.toLocaleLowerCase(); + this.#tcpSocketChildClassType = childClassType; this.#socket = new TCP(TCPConstants.SOCKET | TCPConstants.SERVER); - this._handle = this.#socket; - this._handle.onconnection = this.#handleConnection.bind(this); - this._handle.onclose = this.#handleDisconnect.bind(this); } #handleConnection(err, newClientSocket) { @@ -115,14 +127,14 @@ export class TcpSocketImpl { this.#connections++; - this[symbolState].acceptedClient = new NodeSocket({ + this[symbolSocketState].acceptedClient = new NodeSocket({ handle: newClientSocket, }); - this[symbolState].acceptedClient.server = this.#socket; - this[symbolState].acceptedClient._server = this.#socket; - + this[symbolSocketState].acceptedClient.server = this.#socket; + this[symbolSocketState].acceptedClient._server = this.#socket; + // TODO: handle data received from the client - this[symbolState].acceptedClient._handle.onread = (nread, buffer) => { + this[symbolSocketState].acceptedClient._handle.onread = (nread, buffer) => { if (nread > 0) { const data = buffer.toString("utf8", 0, nread); console.log("accepted socket on read:", data); @@ -140,6 +152,9 @@ export class TcpSocketImpl { #onClientConnectComplete(err) { if (err) { + // TODO: figure out what theis error mean and why it is thrown + assert(err === -89, "unknown"); // on macos + assert(err === -99, "ephemeral-ports-exhausted"); assert(err === -104, "connection-reset"); assert(err === -110, "timeout"); @@ -150,7 +165,7 @@ export class TcpSocketImpl { throw new Error(err); } - this[symbolState].state = SocketConnectionState.Connected; + this[symbolSocketState].connectionState = SocketConnectionState.Connected; } // TODO: is this needed? @@ -164,6 +179,16 @@ export class TcpSocketImpl { this.finishBind(); } + #cacheBoundAddress() { + let { localIpSocketAddress: boundAddress, localPort } = this.#socketOptions; + // when port is 0, the OS will assign an ephemeral port + // we need to get the actual port assigned by the OS + if (localPort === 0) { + boundAddress = this.localAddress(); + } + globalBoundAddresses.set(serializeIpAddress(boundAddress, true), this.#socket); + } + /** * @param {Network} network * @param {IpSocketAddress} localAddress @@ -174,25 +199,32 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already bound. (EINVAL) */ startBind(network, localAddress) { - assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); - - const address = serializeIpAddress(localAddress, this.#socketOptions.family); - const ipFamily = `ipv${isIP(address)}`; - - assert( - this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), - "invalid-argument", - "The `local-address` has the wrong address family" - ); - - assert(isUnicastIpAddress(localAddress) === false, "invalid-argument"); - assert(isIPv4MappedAddress(localAddress) && this.ipv6Only(), "invalid-argument"); - - const { port } = localAddress.val; - this.#socketOptions.localAddress = address; - this.#socketOptions.localPort = port; - this.network = network; - this[symbolState].operationInProgress = true; + try { + assert(this[symbolSocketState].isBound, "invalid-state", "The socket is already bound"); + + const address = serializeIpAddress(localAddress); + const ipFamily = `ipv${isIP(address)}`; + + assert( + this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), + "invalid-argument", + "The `local-address` has the wrong address family" + ); + + assert(isUnicastIpAddress(localAddress) === false, "invalid-argument"); + assert(isIPv4MappedAddress(localAddress) && this.ipv6Only(), "invalid-argument"); + + const { port } = localAddress.val; + this.#socketOptions.localIpSocketAddress = localAddress; + this.#socketOptions.localAddress = address; + this.#socketOptions.localPort = port; + this.network = network; + this[symbolOperations].bind++; + this[symbolSocketState].errorState = null; + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; + } } /** @@ -204,35 +236,38 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { - // we reset the bound state to false, in case the last call to bind failed - // when this methods returns successfully, we set isBound=true again - this[symbolState].isBound = false; + try { + assert(this[symbolOperations].bind === 0, "not-in-progress"); - assert(this[symbolState].operationInProgress === false, "not-in-progress"); + const { localAddress, localIpSocketAddress, localPort, family } = this.#socketOptions; + assert(isIP(localAddress) === 0, "address-not-bindable"); + assert(globalBoundAddresses.has(serializeIpAddress(localIpSocketAddress, true)), "address-in-use"); - const { localAddress, localPort, family } = this.#socketOptions; - assert(isIP(localAddress) === 0, "address-not-bindable"); - assert(globalBoundAddresses.has(localAddress), "address-in-use"); + let err = null; + let bind = "bind"; // ipv4 + if (family.toLocaleLowerCase() === "ipv6") { + bind = "bind6"; + } - let err = null; - if (family.toLocaleLowerCase() === "ipv4") { - err = this.#socket.bind(localAddress, localPort); - } else if (family.toLocaleLowerCase() === "ipv6") { - err = this.#socket.bind6(localAddress, localPort); - } + err = this.#socket[bind](localAddress, localPort); - if (err) { - this.#socket.close(); - assert(err === -22, "address-in-use"); - assert(err === -49, "address-not-bindable"); - assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL - assert(true, "unknown", err); + if (err) { + this.#socket.close(); + assert(err === -22, "address-in-use"); + assert(err === -49, "address-not-bindable"); + assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL + assert(true, "unknown", err); + } + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; } - this[symbolState].isBound = true; - this[symbolState].operationInProgress = false; + this[symbolSocketState].errorState = null; + this[symbolSocketState].isBound = true; + this[symbolOperations].bind--; - globalBoundAddresses.set(localAddress, this.#socket); + this.#cacheBoundAddress(); } /** @@ -250,34 +285,44 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) */ startConnect(network, remoteAddress) { - const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); + const host = serializeIpAddress(remoteAddress); const ipFamily = `ipv${isIP(host)}`; + try { + assert(host === "0.0.0.0" || host === "0:0:0:0:0:0:0:0", "invalid-argument"); + assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); + assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument"); + assert(isMulticastIpAddress(remoteAddress), "invalid-argument"); + assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only(), "invalid-argument"); + + // TODO: test program `preview2_tcp_states.rs` for this assertion checks for invalid-state error + // however, WIT file specifies that this should throw an invalid-argument error + assert(remoteAddress.val.port === 0, "invalid-state"); + + if (this[symbolSocketState].isBound === false) { + this.#autoBind(network, ipFamily); + } - assert(host === "0.0.0.0" || host === "0:0:0:0:0:0:0:0", "invalid-argument"); - assert(remoteAddress.val.port === 0, "invalid-argument"); - assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); - assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument"); - assert(isMulticastIpAddress(remoteAddress), "invalid-argument"); - assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only(), "invalid-argument"); - - if (this[symbolState].isBound === false) { - this.#autoBind(network, ipFamily); + assert( + this[symbolSocketState].connectionState === SocketConnectionState.Connected || + this[symbolSocketState].connectionState === SocketConnectionState.Listener, + "invalid-state" + ); + + assert(network !== this.network, "invalid-argument"); + assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); + assert(remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument"); + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; } - assert( - this[symbolState].state === SocketConnectionState.Connected || - this[symbolState].state === SocketConnectionState.Listener, - "invalid-state" - ); - - assert(network !== this.network, "invalid-argument"); - assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); - assert(remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument"); + this[symbolSocketState].errorState = null; + this.#socketOptions.remoteIpSocketAddress = remoteAddress; this.#socketOptions.remoteAddress = host; this.#socketOptions.remotePort = remoteAddress.val.port; this.network = network; - this[symbolState].operationInProgress = true; + this[symbolOperations].connect++; } /** @@ -292,7 +337,14 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishConnect() { - assert(this[symbolState].operationInProgress === false, "not-in-progress"); + try { + assert(this[symbolOperations].connect === 0, "not-in-progress"); + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; + } + + this[symbolSocketState].errorState = null; const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; const connectReq = new TCPConnectWrap(); @@ -307,7 +359,7 @@ export class TcpSocketImpl { if (err) { console.error(`[tcp] connect error on socket: ${err}`); - this[symbolState].state = SocketConnectionState.Error; + this[symbolSocketState].connectionState = SocketConnectionState.Error; } connectReq.oncomplete = this.#onClientConnectComplete.bind(this); @@ -321,11 +373,14 @@ export class TcpSocketImpl { }; this.#socket.readStart(); - this[symbolState].operationInProgress = false; const streamId = this.#connections++; const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, streamId); const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, streamId); + + this[symbolOperations].connect--; + this[symbolSocketState].connectionState = SocketConnectionState.Connecting; + return [inputStream, outputStream]; } @@ -336,11 +391,18 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is already in the Listener state. */ startListen() { - assert(this[symbolState].isBound === false, "invalid-state"); - assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); - assert(this[symbolState].state === SocketConnectionState.Listener, "invalid-state"); + try { + assert(this[symbolSocketState].errorState !== null, "invalid-state"); + assert(this[symbolSocketState].isBound === false, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Listener, "invalid-state"); + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; + } - this[symbolState].operationInProgress = true; + this[symbolSocketState].errorState = null; + this[symbolOperations].listen++; } /** @@ -350,9 +412,16 @@ export class TcpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ finishListen() { - assert(this[symbolState].operationInProgress === false, "not-in-progress"); + try { + assert(this[symbolOperations].listen === 0, "not-in-progress"); + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; + } - const err = this.#socket.listen(this.#backlog); + this[symbolSocketState].errorState = null; + + const err = this.#socket.listen(this[symbolSocketState].backlogSize); if (err) { console.error(`[tcp] listen error on socket: ${err}`); this.#socket.close(); @@ -361,8 +430,8 @@ export class TcpSocketImpl { throw new Error(err); } - this[symbolState].operationInProgress = false; - this[symbolState].state === SocketConnectionState.Listener; + this[symbolSocketState].connectionState = SocketConnectionState.Listener; + this[symbolOperations].listen--; } /** @@ -373,19 +442,27 @@ export class TcpSocketImpl { * @throws {new-socket-limit} The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) */ accept() { - if (this[symbolState].isBound === false) { - this.#autoBind(this.network, ipFamily); + this[symbolOperations].accept++; + + try { + assert(this[symbolSocketState].connectionState !== SocketConnectionState.Listener, "invalid-state"); + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; } - assert(this[symbolState].state !== SocketConnectionState.Listener, "invalid-state"); + this[symbolSocketState].errorState = null; + if (this[symbolSocketState].isBound === false) { + this.#autoBind(this.network, this.addressFamily()); + } const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, this.id); const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, this.id); - // Because we have to return a valid TcpSocket resrouces type, + // Because we have to return a valid TcpSocket resrouce type, // we need to instantiate the correct child class - // - const socket = new this.tcpSocketChildClassType(this.addressFamily()); + // TODO: figure out a more elegant way to do this + const socket = new this.#tcpSocketChildClassType(this.addressFamily()); // The returned socket is bound and in the Connection state. // The following properties are inherited from the listener socket: @@ -399,14 +476,17 @@ export class TcpSocketImpl { // - `receive-buffer-size` // - `send-buffer-size` // - socket.setIpv6Only(this.ipv6Only()); - socket.setKeepAliveEnabled(this.keepAliveEnabled()); - socket.setKeepAliveIdleTime(this.keepAliveIdleTime()); - socket.setKeepAliveInterval(this.keepAliveInterval()); - socket.setKeepAliveCount(this.keepAliveCount()); - socket.setHopLimit(this.hopLimit()); - socket.setReceiveBufferSize(this.receiveBufferSize()); - socket.setSendBufferSize(this.sendBufferSize()); + socket[symbolSocketState].ipv6Only = this[symbolSocketState].ipv6Only; + socket[symbolSocketState].keepAlive = this[symbolSocketState].keepAlive; + socket[symbolSocketState].keepAliveIdleTime = this[symbolSocketState].keepAliveIdleTime; + socket[symbolSocketState].keepAliveInterval = this[symbolSocketState].keepAliveInterval; + socket[symbolSocketState].keepAliveCount = this[symbolSocketState].keepAliveCount; + socket[symbolSocketState].hopLimit = this[symbolSocketState].hopLimit; + socket[symbolSocketState].receiveBufferSize = this[symbolSocketState].receiveBufferSize; + socket[symbolSocketState].sendBufferSize = this[symbolSocketState].sendBufferSize; + + this[symbolOperations].accept--; + return [socket, inputStream, outputStream]; } @@ -415,12 +495,17 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - assert(this[symbolState].isBound === false, "invalid-state"); + assert(this[symbolSocketState].isBound === false, "invalid-state"); const out = {}; this.#socket.getsockname(out); const { address, port, family } = out; + + this.#socketOptions.localAddress = address; + this.#socketOptions.localPort = port; + this.#socketOptions.family = family.toLocaleLowerCase(); + return { tag: family.toLocaleLowerCase(), val: { @@ -435,13 +520,28 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not connected to a remote address. (ENOTCONN) */ remoteAddress() { - assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState !== SocketConnectionState.Connected, "invalid-state"); - return this.#socketOptions.remoteAddress; + const out = {}; + this.#socket.getpeername(out); + + const { address, port, family } = out; + + this.#socketOptions.remoteAddress = address; + this.#socketOptions.remotePort = port; + this.#socketOptions.family = family.toLocaleLowerCase(); + + return { + tag: family.toLocaleLowerCase(), + val: { + address: deserializeIpAddress(address, family), + port, + }, + }; } isListening() { - return this[symbolState].state === SocketConnectionState.Listener; + return this[symbolSocketState].connectionState === SocketConnectionState.Listener; } /** @@ -458,7 +558,7 @@ export class TcpSocketImpl { ipv6Only() { assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported"); - return this[symbolState].ipv6Only; + return this[symbolSocketState].ipv6Only; } /** @@ -470,9 +570,9 @@ export class TcpSocketImpl { */ setIpv6Only(value) { assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported"); - assert(this[symbolState].isBound, "invalid-state"); + assert(this[symbolSocketState].isBound, "invalid-state"); - this[symbolState].ipv6Only = value; + this[symbolSocketState].ipv6Only = value; } /** @@ -484,16 +584,16 @@ export class TcpSocketImpl { */ setListenBacklogSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); - this[symbolState].backlogSize = value; + this[symbolSocketState].backlogSize = value; } /** * @returns {boolean} */ keepAliveEnabled() { - return this[symbolState].keepAlive; + return this[symbolSocketState].keepAlive; } /** @@ -502,7 +602,7 @@ export class TcpSocketImpl { */ setKeepAliveEnabled(value) { this.#socket.setKeepAlive(value); - this[symbolState].keepAlive = value; + this[symbolSocketState].keepAlive = value; if (value) { this.setKeepAliveIdleTime(this.keepAliveIdleTime()); @@ -516,7 +616,7 @@ export class TcpSocketImpl { * @returns {Duration} */ keepAliveIdleTime() { - return this[symbolState].keepAliveIdleTime; + return this[symbolSocketState].keepAliveIdleTime; } /** @@ -528,7 +628,7 @@ export class TcpSocketImpl { setKeepAliveIdleTime(value) { assert(value < 1, "invalid-argument", "The idle time must be 1 or higher."); - this[symbolState].keepAliveIdleTime = value; + this[symbolSocketState].keepAliveIdleTime = value; } /** @@ -536,7 +636,7 @@ export class TcpSocketImpl { * @returns {Duration} */ keepAliveInterval() { - return this[symbolState].keepAliveInterval; + return this[symbolSocketState].keepAliveInterval; } /** @@ -548,7 +648,7 @@ export class TcpSocketImpl { setKeepAliveInterval(value) { assert(value < 1, "invalid-argument", "The interval must be 1 or higher."); - this[symbolState].keepAliveInterval = value; + this[symbolSocketState].keepAliveInterval = value; } /** @@ -556,7 +656,7 @@ export class TcpSocketImpl { * @returns {Duration} */ keepAliveCount() { - return this[symbolState].keepAliveCount; + return this[symbolSocketState].keepAliveCount; } /** @@ -569,14 +669,14 @@ export class TcpSocketImpl { assert(value < 1, "invalid-argument", "The count must be 1 or higher."); // TODO: set this on the client socket as well - this[symbolState].keepAliveCount = value; + this[symbolSocketState].keepAliveCount = value; } /** * @returns {number} */ hopLimit() { - return this[symbolState].hopLimit; + return this[symbolSocketState].hopLimit; } /** @@ -588,17 +688,17 @@ export class TcpSocketImpl { */ setHopLimit(value) { assert(!value || value < 1, "invalid-argument", "The TTL value must be 1 or higher."); - assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well - this[symbolState].hopLimit = value; + this[symbolSocketState].hopLimit = value; } /** * @returns {bigint} */ receiveBufferSize() { - return this[symbolState].receiveBufferSize; + return this[symbolSocketState].receiveBufferSize; } /** @@ -610,17 +710,17 @@ export class TcpSocketImpl { */ setReceiveBufferSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well - this[symbolState].receiveBufferSize = value; + this[symbolSocketState].receiveBufferSize = value; } /** * @returns {bigint} */ sendBufferSize() { - return this[symbolState].sendBufferSize; + return this[symbolSocketState].sendBufferSize; } /** @@ -632,10 +732,10 @@ export class TcpSocketImpl { */ setSendBufferSize(value) { assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert(this[symbolState].state === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well - this[symbolState].sendBufferSize = value; + this[symbolSocketState].sendBufferSize = value; } /** @@ -653,16 +753,16 @@ export class TcpSocketImpl { * @throws {invalid-state} The socket is not in the Connection state. (ENOTCONN) */ shutdown(shutdownType) { - assert(this[symbolState].state !== SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState !== SocketConnectionState.Connected, "invalid-state"); // TODO: figure out how to handle shutdownTypes if (shutdownType === ShutdownType.Receive) { - this[symbolState].canReceive = false; + this[symbolSocketState].canReceive = false; } else if (shutdownType === ShutdownType.Send) { - this[symbolState].canSend = false; + this[symbolSocketState].canSend = false; } else if (shutdownType === ShutdownType.Both) { - this[symbolState].canReceive = false; - this[symbolState].canSend = false; + this[symbolSocketState].canReceive = false; + this[symbolSocketState].canSend = false; } const req = new ShutdownWrap(); @@ -676,7 +776,12 @@ export class TcpSocketImpl { [symbolDispose]() { this.#socket.close(); - globalBoundAddresses.delete(this.#socketOptions.localAddress); + + // we only need to remove the bound address from the global map + // if the socket was already bound + if (this[symbolSocketState].isBound) { + globalBoundAddresses.delete(serializeIpAddress(this.#socketOptions.localIpSocketAddress, true)); + } } server() { diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 6ddc7ade8..575368eed 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -162,6 +162,7 @@ const outgoingDatagramStreamCreate = OutgoingDatagramStream._create; delete OutgoingDatagramStream._create; export class UdpSocketImpl { + id = 1; /** @type {UDP} */ #socket = null; /** @type {Network} */ network = null; @@ -189,7 +190,8 @@ export class UdpSocketImpl { * @param {IpAddressFamily} addressFamily * @returns {void} */ - constructor(addressFamily) { + constructor(addressFamily, id) { + this.id = id; this.#socketOptions.family = addressFamily; this.#socket = new UDP(); @@ -371,7 +373,7 @@ export class UdpSocketImpl { return { tag: family.toLocaleLowerCase(), val: { - address: deserializeIpAddress(address, family), + address: deserializeIpAddress(address), port, }, }; @@ -402,7 +404,7 @@ export class UdpSocketImpl { return { tag: family.toLocaleLowerCase(), val: { - address: deserializeIpAddress(address, family), + address: deserializeIpAddress(address), port, }, }; diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 07734116f..e7ba54849 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -148,7 +148,6 @@ export class WasiSockets { const net = this; class Network { - id = 1; constructor() { this.id = net.networkCnt++; net.networks.set(this.id, this); @@ -160,7 +159,7 @@ export class WasiSockets { * @param {IpAddressFamily} addressFamily * */ constructor(addressFamily) { - super(addressFamily); + super(addressFamily, net.socketCnt++); net.udpSockets.set(this.id, this); } } @@ -176,8 +175,8 @@ export class WasiSockets { * @param {IpAddressFamily} addressFamily * */ constructor(addressFamily) { - super(addressFamily, TcpSocket); - net.tcpSockets.set(this.id); + super(addressFamily, TcpSocket, net.socketCnt++); + net.tcpSockets.set(this.id, this); } } @@ -219,7 +218,6 @@ export class WasiSockets { ); try { - net.socketCnt++; return new UdpSocket(addressFamily); } catch (err) { assert(true, errorCode.notSupported, err); @@ -248,7 +246,6 @@ export class WasiSockets { ); try { - net.socketCnt++; return new TcpSocket(addressFamily); } catch (err) { // assert(true, errorCode.unknown, err); @@ -274,7 +271,7 @@ export class WasiSockets { const family = `ipv${isIP(address)}`; return { tag: family, - val: deserializeIpAddress(address, family), + val: deserializeIpAddress(address), }; }); } From 39949debf58f8365b13e17eda5ec2beaf7865b48 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 26 Nov 2023 01:56:29 +0100 Subject: [PATCH 59/65] chore: preview2_tcp_sample_application wip --- packages/preview2-shim/lib/io/stream-types.js | 3 +-- .../preview2-shim/lib/io/worker-thread.js | 8 +++++- .../lib/nodejs/sockets/tcp-socket-impl.js | 13 +++++----- .../lib/nodejs/sockets/wasi-sockets.js | 25 +++---------------- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/packages/preview2-shim/lib/io/stream-types.js b/packages/preview2-shim/lib/io/stream-types.js index e70312dd8..1185e1300 100644 --- a/packages/preview2-shim/lib/io/stream-types.js +++ b/packages/preview2-shim/lib/io/stream-types.js @@ -4,7 +4,6 @@ export const STDIN = ++cnt; export const STDOUT = ++cnt; export const STDERR = ++cnt; export const FILE = ++cnt; +export const SOCKET = ++cnt; export const INCOMING_BODY = ++cnt; export const OUTGOING_BODY = ++cnt; -export const SOCKET_INPUT_STREAM = ++cnt; -export const SOCKET_OUTPUT_STREAM = ++cnt; diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/lib/io/worker-thread.js index 4b9348e1e..23d2248ea 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/lib/io/worker-thread.js @@ -39,7 +39,7 @@ import { SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, } from "./calls.js"; -import { FILE, STDERR, STDIN, STDOUT } from "./stream-types.js"; +import { FILE, SOCKET, STDERR, STDIN, STDOUT } from "./stream-types.js"; let streamCnt = 0, pollCnt = 0; @@ -141,6 +141,12 @@ function handle(call, id, payload) { unfinishedFutures.delete(id); return future; } + case OUTPUT_STREAM_CREATE | SOCKET: { + throw new Error("not implemented"); + } + case INPUT_STREAM_CREATE | SOCKET: { + throw new Error("not implemented"); + } // Stdio case OUTPUT_STREAM_DISPOSE | STDOUT: diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index a53eab940..b50c914c9 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -26,8 +26,9 @@ const symbolOperations = Symbol("SocketOperationsState"); const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap"); const { ShutdownWrap } = process.binding("stream_wrap"); -import { SOCKET_INPUT_STREAM, SOCKET_OUTPUT_STREAM } from "../../io/stream-types.js"; -import { inputStreamCreate, outputStreamCreate, pollableCreate } from "../../io/worker-io.js"; +import { INPUT_STREAM_CREATE, OUTPUT_STREAM_CREATE } from "../../io/calls.js"; +import { SOCKET } from "../../io/stream-types.js"; +import { inputStreamCreate, ioCall, outputStreamCreate, pollableCreate } from "../../io/worker-io.js"; import { deserializeIpAddress, findUnsuedLocalAddress, @@ -36,6 +37,7 @@ import { isUnicastIpAddress, serializeIpAddress, } from "./socket-common.js"; +import { _setPreopens } from "../filesystem.js"; // TODO: move to a common const ShutdownType = { @@ -374,9 +376,8 @@ export class TcpSocketImpl { this.#socket.readStart(); - const streamId = this.#connections++; - const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, streamId); - const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, streamId); + const inputStream = inputStreamCreate(SOCKET, ioCall(INPUT_STREAM_CREATE | SOCKET, null, {})); + const outputStream = outputStreamCreate(SOCKET, ioCall(OUTPUT_STREAM_CREATE | SOCKET, null, {})); this[symbolOperations].connect--; this[symbolSocketState].connectionState = SocketConnectionState.Connecting; @@ -586,7 +587,7 @@ export class TcpSocketImpl { assert(value === 0n, "invalid-argument", "The provided value was 0."); assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); - this[symbolSocketState].backlogSize = value; + this[symbolSocketState].backlogSize = Number(value); } /** diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index e7ba54849..0d83f19e3 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -14,23 +14,10 @@ import { SOCKET_RESOLVE_ADDRESS_DISPOSE_REQUEST, SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, } from "../../io/calls.js"; -import { - SOCKET_INPUT_STREAM, - SOCKET_OUTPUT_STREAM, -} from "../../io/stream-types.js"; -import { - inputStreamCreate, - ioCall, - outputStreamCreate, - pollableCreate, -} from "../../io/worker-io.js"; +import { ioCall, pollableCreate } from "../../io/worker-io.js"; import { deserializeIpAddress } from "./socket-common.js"; import { TcpSocketImpl } from "./tcp-socket-impl.js"; -import { - IncomingDatagramStream, - OutgoingDatagramStream, - UdpSocketImpl, -} from "./udp-socket-impl.js"; +import { IncomingDatagramStream, OutgoingDatagramStream, UdpSocketImpl } from "./udp-socket-impl.js"; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); @@ -262,10 +249,7 @@ export class WasiSockets { resolveNextAddress() { if (this.#error) throw this.#error; if (!this.#data) { - const { value: addresses, error } = ioCall( - SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, - this.#pollId - ); + const { value: addresses, error } = ioCall(SOCKET_RESOLVE_ADDRESS_GET_AND_DISPOSE_REQUEST, this.#pollId); if (error) throw (this.#error = convertResolveAddressError(error)); this.#data = addresses.map((address) => { const family = `ipv${isIP(address)}`; @@ -275,8 +259,7 @@ export class WasiSockets { }; }); } - if (this.#curItem < this.#data.length) - return this.#data[this.#curItem++]; + if (this.#curItem < this.#data.length) return this.#data[this.#curItem++]; return undefined; } subscribe() { From bf5965cb65c018cc0c3add31ca2ed55f6a49c222 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Sun, 26 Nov 2023 15:14:53 +0100 Subject: [PATCH 60/65] fix: preview2_tcp_bind and preview2_udp_bind --- .../lib/nodejs/sockets/tcp-socket-impl.js | 28 ++- .../lib/nodejs/sockets/udp-socket-impl.js | 219 +++++++++++------- .../lib/nodejs/sockets/wasi-sockets.js | 3 +- 3 files changed, 155 insertions(+), 95 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index b50c914c9..a46225e1f 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -19,8 +19,8 @@ import { streams } from "../io.js"; const { InputStream, OutputStream } = streams; const symbolDispose = Symbol.dispose || Symbol.for("dispose"); -const symbolSocketState = Symbol("SocketInternalState"); -const symbolOperations = Symbol("SocketOperationsState"); +const symbolSocketState = Symbol.SocketInternalState || Symbol.for("SocketInternalState"); +const symbolOperations = Symbol.SocketOperationsState || Symbol.for("SocketOperationsState"); // See: https://github.com/nodejs/node/blob/main/src/tcp_wrap.cc const { TCP, TCPConnectWrap, constants: TCPConstants } = process.binding("tcp_wrap"); @@ -37,7 +37,6 @@ import { isUnicastIpAddress, serializeIpAddress, } from "./socket-common.js"; -import { _setPreopens } from "../filesystem.js"; // TODO: move to a common const ShutdownType = { @@ -68,7 +67,6 @@ export class TcpSocketImpl { /** @type {TCP.TCPConstants.SOCKET} */ #socket = null; /** @type {Network} */ network = null; - #socketOptions = {}; #connections = 0; #pollId = null; @@ -106,12 +104,21 @@ export class TcpSocketImpl { sendBufferSize: 1, }; + #socketOptions = { + family: "ipv4", + localAddress: "", + localPort: 0, + remoteAddress: "", + remotePort: 0, + }; + // this is set by the TcpSocket child class #tcpSocketChildClassType = null; /** * @param {IpAddressFamily} addressFamily * @param {TcpSocket} childClassType + * @param {number} id */ constructor(addressFamily, childClassType, id) { this.id = id; @@ -260,16 +267,16 @@ export class TcpSocketImpl { assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL assert(true, "unknown", err); } + + this[symbolSocketState].errorState = null; + this[symbolSocketState].isBound = true; + this[symbolOperations].bind--; + + this.#cacheBoundAddress(); } catch (err) { this[symbolSocketState].errorState = err; throw err; } - - this[symbolSocketState].errorState = null; - this[symbolSocketState].isBound = true; - this[symbolOperations].bind--; - - this.#cacheBoundAddress(); } /** @@ -502,7 +509,6 @@ export class TcpSocketImpl { this.#socket.getsockname(out); const { address, port, family } = out; - this.#socketOptions.localAddress = address; this.#socketOptions.localPort = port; this.#socketOptions.family = family.toLocaleLowerCase(); diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 575368eed..eef6e2146 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -12,10 +12,12 @@ const { UDP, SendWrap } = process.binding("udp_wrap"); import { isIP } from "node:net"; import { assert } from "../../common/assert.js"; -import { deserializeIpAddress, cappedUint32, serializeIpAddress } from "./socket-common.js"; import { pollableCreate } from "../../io/worker-io.js"; +import { cappedUint32, deserializeIpAddress, serializeIpAddress } from "./socket-common.js"; -const symbolState = Symbol("SocketInternalState"); +const symbolDispose = Symbol.dispose || Symbol.for("dispose"); +const symbolSocketState = Symbol.SocketInternalState || Symbol.for("SocketInternalState"); +const symbolOperations = Symbol.SocketOperationsState || Symbol.for("SocketOperationsState"); // TODO: move to a common const SocketConnectionState = { @@ -39,6 +41,12 @@ const BufferSizeFlags = { SO_SNDBUF: false, }; +// As a workaround, we store the bound address in a global map +// this is needed because 'address-in-use' is not always thrown when binding +// more than one socket to the same address +// TODO: remove this workaround when we figure out why! +const globalBoundAddresses = new Map(); + export class IncomingDatagramStream { static _create(socket) { const stream = new IncomingDatagramStream(socket); @@ -61,8 +69,8 @@ export class IncomingDatagramStream { * @throws {would-block} There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) */ receive(maxResults) { - assert(self[symbolState].isBound === false, "invalid-state"); - assert(self[symbolState].operationInProgress === false, "not-in-progress"); + assert(self[symbolSocketState].isBound === false, "invalid-state"); + assert(self[symbolOperations].receive === 0, "not-in-progress"); if (maxResults === 0n) { return []; @@ -84,6 +92,10 @@ export class IncomingDatagramStream { subscribe() { throw new Error("Not implemented"); } + + [symbolDispose]() { + throw new Error("Not implemented"); + } } const incomingDatagramStreamCreate = IncomingDatagramStream._create; delete IncomingDatagramStream._create; @@ -143,7 +155,7 @@ export class OutgoingDatagramStream { const { data, remoteAddress } = datagram; const { tag: family, val } = remoteAddress; const { address, port } = val; - const err = doSend(data, port, serializeIpAddress(remoteAddress, family), family); + const err = doSend(data, port, serializeIpAddress(remoteAddress), family); console.error({ err, }); @@ -157,6 +169,10 @@ export class OutgoingDatagramStream { subscribe() { throw new Error("Not implemented"); } + + [symbolDispose]() { + throw new Error("Not implemented"); + } } const outgoingDatagramStreamCreate = OutgoingDatagramStream._create; delete OutgoingDatagramStream._create; @@ -166,11 +182,24 @@ export class UdpSocketImpl { /** @type {UDP} */ #socket = null; /** @type {Network} */ network = null; - [symbolState] = { + // track in-progress operations + // counter must be 0 for the operation to be considered complete + // we increment the counter when the operation starts + // and decrement it when the operation finishes + [symbolOperations] = { + bind: 0, + connect: 0, + listen: 0, + accept: 0, + receive: 0, + send: 0, + }; + + [symbolSocketState] = { + errorState: null, isBound: false, - operationInProgress: false, ipv6Only: false, - state: SocketConnectionState.Closed, + connectionState: SocketConnectionState.Closed, // TODO: what these default values should be? unicastHopLimit: 1, @@ -197,34 +226,49 @@ export class UdpSocketImpl { this.#socket = new UDP(); } + #cacheBoundAddress() { + let { localIpSocketAddress: boundAddress, localPort } = this.#socketOptions; + // when port is 0, the OS will assign an ephemeral port + // we need to get the actual port assigned by the OS + if (localPort === 0) { + boundAddress = this.localAddress(); + } + globalBoundAddresses.set(serializeIpAddress(boundAddress, true), this.#socket); + } + /** * * @param {Network} network * @param {IpAddressFamily} localAddress - * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + * @returns {void} * @throws {invalid-argument} The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) * @throws {invalid-state} The socket is already bound. (EINVAL) - * @returns {void} */ startBind(network, localAddress) { - this[symbolState].operationInProgress = false; - - const address = serializeIpAddress(localAddress, this.#socketOptions.family); - const ipFamily = `ipv${isIP(address)}`; - - assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); - assert( - this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), - "invalid-argument", - "The `local-address` has the wrong address family" - ); - assert(this[symbolState].ipv6Only, "invalid-argument", "The `local-address` has the wrong address family"); - - const { port } = localAddress.val; - this.#socketOptions.localAddress = address; - this.#socketOptions.localPort = port; - this.network = network; - this[symbolState].operationInProgress = true; + try { + assert(this[symbolSocketState].isBound, "invalid-state", "The socket is already bound"); + + const address = serializeIpAddress(localAddress); + const ipFamily = `ipv${isIP(address)}`; + + assert( + this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), + "invalid-argument", + "The `local-address` has the wrong address family" + ); + assert(this[symbolSocketState].ipv6Only, "invalid-argument", "The `local-address` has the wrong address family"); + + const { port } = localAddress.val; + this.#socketOptions.localIpSocketAddress = localAddress; + this.#socketOptions.localAddress = address; + this.#socketOptions.localPort = port; + this.network = network; + this[symbolOperations].bind++; + this[symbolSocketState].errorState = null; + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; + } } /** @@ -237,34 +281,46 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) **/ finishBind() { - assert(this[symbolState].operationInProgress === false, "not-in-progress"); + try { + assert(this[symbolOperations].bind === 0, "not-in-progress"); - const { localAddress, localPort, family } = this.#socketOptions; + const { localAddress, localIpSocketAddress, localPort, family } = this.#socketOptions; + assert(isIP(localAddress) === 0, "address-not-bindable"); + assert(globalBoundAddresses.has(serializeIpAddress(localIpSocketAddress, true)), "address-in-use"); - let flags = 0; - if (this[symbolState].ipv6Only) { - flags |= Flags.UV_UDP_IPV6ONLY; - } + let flags = 0; + if (this[symbolSocketState].ipv6Only) { + flags |= Flags.UV_UDP_IPV6ONLY; + } - let err = null; - if (family.toLocaleLowerCase() === "ipv4") { - err = this.#socket.bind(localAddress, localPort, flags); - } else if (family.toLocaleLowerCase() === "ipv6") { - err = this.#socket.bind6(localAddress, localPort, flags); - } + let err = null; + let bind = "bind"; // ipv4 + if (family.toLocaleLowerCase() === "ipv6") { + bind = "bind6"; + } - if (err === 0) { - this[symbolState].isBound = true; - } else { - assert(err === -22, "address-in-use"); - assert(err === -48, "address-in-use"); // macos - assert(err === -49, "address-not-bindable"); - assert(err === -98, "address-in-use"); // WSL - assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL - assert(true, "unknown", err); - } + err = this.#socket[bind](localAddress, localPort, flags); + + if (err === 0) { + this[symbolSocketState].isBound = true; + } else { + assert(err === -22, "address-in-use"); + assert(err === -48, "address-in-use"); // macos + assert(err === -49, "address-not-bindable"); + assert(err === -98, "address-in-use"); // WSL + assert(err === -99, "address-not-bindable"); // EADDRNOTAVAIL + assert(true, "unknown", err); + } + + this[symbolSocketState].errorState = null; + this[symbolSocketState].isBound = true; + this[symbolOperations].bind--; - this[symbolState].operationInProgress = false; + this.#cacheBoundAddress(); + } catch (err) { + this[symbolSocketState].errorState = err; + throw err; + } } /** @@ -287,14 +343,14 @@ export class UdpSocketImpl { * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ #startConnect(network, remoteAddress = undefined) { - this[symbolState].operationInProgress = false; + this[symbolSocketState].operationInProgress = false; - if (remoteAddress === undefined || this[symbolState].state === SocketConnectionState.Connected) { + if (remoteAddress === undefined || this[symbolSocketState].state === SocketConnectionState.Connected) { // reusing a connected socket. See #finishConnect() return; } - const host = serializeIpAddress(remoteAddress, this.#socketOptions.family); + const host = serializeIpAddress(remoteAddress); const ipFamily = `ipv${isIP(host)}`; assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); @@ -305,7 +361,7 @@ export class UdpSocketImpl { this.#socketOptions.remotePort = port; this.network = network; - this[symbolState].operationInProgress = true; + this[symbolSocketState].operationInProgress = true; } /** @@ -317,17 +373,17 @@ export class UdpSocketImpl { */ #finishConnect() { const { remoteAddress, remotePort } = this.#socketOptions; - this[symbolState].state = SocketConnectionState.Connecting; + this[symbolSocketState].state = SocketConnectionState.Connecting; - if (this[symbolState].state === SocketConnectionState.Connected) { + if (this[symbolSocketState].state === SocketConnectionState.Connected) { // TODO: figure out how to reuse a connected socket this.#socket.connect(); } else { this.#socket.connect(remoteAddress, remotePort); } - this[symbolState].operationInProgress = false; - this[symbolState].state = SocketConnectionState.Connected; + this[symbolSocketState].operationInProgress = false; + this[symbolSocketState].state = SocketConnectionState.Connected; } /** @@ -352,7 +408,7 @@ export class UdpSocketImpl { * @throws {connection-refused} The connection was refused. (ECONNREFUSED) */ stream(remoteAddress = undefined) { - assert(this[symbolState].isBound === false, "invalid-state"); + assert(this[symbolSocketState].isBound === false, "invalid-state"); this.#connect(this.network, remoteAddress); return [incomingDatagramStreamCreate(this.#socket), outgoingDatagramStreamCreate(this.#socket)]; } @@ -364,16 +420,20 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not bound to any local address. */ localAddress() { - assert(this[symbolState].isBound === false, "invalid-state"); + assert(this[symbolSocketState].isBound === false, "invalid-state"); const out = {}; this.#socket.getsockname(out); const { address, port, family } = out; + this.#socketOptions.localAddress = address; + this.#socketOptions.localPort = port; + this.#socketOptions.family = family.toLocaleLowerCase(); + return { tag: family.toLocaleLowerCase(), val: { - address: deserializeIpAddress(address), + address: deserializeIpAddress(address, family), port, }, }; @@ -386,7 +446,7 @@ export class UdpSocketImpl { */ remoteAddress() { assert( - this[symbolState].state !== SocketConnectionState.Connected, + this[symbolSocketState].state !== SocketConnectionState.Connected, "invalid-state", "The socket is not streaming to a specific remote address" ); @@ -394,12 +454,8 @@ export class UdpSocketImpl { const out = {}; this.#socket.getpeername(out); - assert( - out.address === undefined, - "invalid-state", - "The socket is not streaming to a specific remote address" - ); - + assert(out.address === undefined, "invalid-state", "The socket is not streaming to a specific remote address"); + const { address, port, family } = out; return { tag: family.toLocaleLowerCase(), @@ -426,7 +482,7 @@ export class UdpSocketImpl { ipv6Only() { assert(this.#socketOptions.family.toLocaleLowerCase() === "ipv4", "not-supported", "Socket is an IPv4 socket."); - return this[symbolState].ipv6Only; + return this[symbolSocketState].ipv6Only; } /** @@ -443,9 +499,9 @@ export class UdpSocketImpl { "not-supported", "Socket is an IPv4 socket." ); - assert(this[symbolState].isBound, "invalid-state", "The socket is already bound"); + assert(this[symbolSocketState].isBound, "invalid-state", "The socket is already bound"); - this[symbolState].ipv6Only = value; + this[symbolSocketState].ipv6Only = value; } /** @@ -453,7 +509,7 @@ export class UdpSocketImpl { * @returns {number} */ unicastHopLimit() { - return this[symbolState].unicastHopLimit; + return this[symbolSocketState].unicastHopLimit; } /** @@ -466,7 +522,7 @@ export class UdpSocketImpl { assert(value < 1, "invalid-argument", "The TTL value must be 1 or higher"); this.#socket.setTTL(value); - this[symbolState].unicastHopLimit = value; + this[symbolSocketState].unicastHopLimit = value; } /** @@ -480,12 +536,12 @@ export class UdpSocketImpl { if (exceptionInfo.code === "EBADF") { // TODO: handle the case where bad file descriptor is returned // This happens when the socket is not bound - return this[symbolState].receiveBufferSize; + return this[symbolSocketState].receiveBufferSize; } console.log({ - value - }) + value, + }); return value; } @@ -502,7 +558,7 @@ export class UdpSocketImpl { const cappedValue = cappedUint32(value); const exceptionInfo = {}; this.#socket.bufferSize(Number(cappedValue), BufferSizeFlags.SO_RCVBUF, exceptionInfo); - this[symbolState].receiveBufferSize = cappedValue; + this[symbolSocketState].receiveBufferSize = cappedValue; } /** @@ -516,7 +572,7 @@ export class UdpSocketImpl { if (exceptionInfo.code === "EBADF") { // TODO: handle the case where bad file descriptor is returned // This happens when the socket is not bound - return this[symbolState].sendBufferSize; + return this[symbolSocketState].sendBufferSize; } return value; @@ -534,7 +590,7 @@ export class UdpSocketImpl { const cappedValue = cappedUint32(value); const exceptionInfo = {}; this.#socket.bufferSize(Number(cappedValue), BufferSizeFlags.SO_SNDBUF, exceptionInfo); - this[symbolState].sendBufferSize = cappedValue; + this[symbolSocketState].sendBufferSize = cappedValue; } /** @@ -545,7 +601,7 @@ export class UdpSocketImpl { return pollableCreate(0); } - [Symbol.dispose]() { + [symbolDispose]() { let err = null; err = this.#socket.recvStop((...args) => { console.log("stop recv", args); @@ -558,7 +614,6 @@ export class UdpSocketImpl { } this.#socket.close(); - this.#socket.close(); } client() { diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index 0d83f19e3..e5b66e2b0 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -157,12 +157,11 @@ export class WasiSockets { IncomingDatagramStream, }; - class TcpSocket extends TcpSocketImpl { + class TcpSocket { /** * @param {IpAddressFamily} addressFamily * */ constructor(addressFamily) { - super(addressFamily, TcpSocket, net.socketCnt++); net.tcpSockets.set(this.id, this); } } From 6e2bf22e7e084cbd70ceae02c69eec8ceb24c4e1 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 27 Nov 2023 12:42:43 +0100 Subject: [PATCH 61/65] chore: stashing wip fixes --- .../preview2-shim/lib/io/worker-thread.js | 4 +- .../lib/nodejs/sockets/socket-common.js | 2 +- .../lib/nodejs/sockets/tcp-socket-impl.js | 44 +++++++++-------- .../lib/nodejs/sockets/udp-socket-impl.js | 47 ++++++++++++------- .../lib/nodejs/sockets/wasi-sockets.js | 3 +- 5 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/preview2-shim/lib/io/worker-thread.js b/packages/preview2-shim/lib/io/worker-thread.js index 23d2248ea..e8b339037 100644 --- a/packages/preview2-shim/lib/io/worker-thread.js +++ b/packages/preview2-shim/lib/io/worker-thread.js @@ -142,10 +142,10 @@ function handle(call, id, payload) { return future; } case OUTPUT_STREAM_CREATE | SOCKET: { - throw new Error("not implemented"); + // TODO: implement } case INPUT_STREAM_CREATE | SOCKET: { - throw new Error("not implemented"); + // TODO: implement } // Stdio diff --git a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js index 683a4ce25..facfe9944 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/socket-common.js +++ b/packages/preview2-shim/lib/nodejs/sockets/socket-common.js @@ -44,7 +44,7 @@ function ipv4ToTuple(ipv4) { export function serializeIpAddress(addr = undefined, includePort = false) { if (addr === undefined) { - addr = { val: { address: [] } }; + return undefined; } const family = addr.tag; diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index a46225e1f..11a1bbb36 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -162,7 +162,7 @@ export class TcpSocketImpl { #onClientConnectComplete(err) { if (err) { // TODO: figure out what theis error mean and why it is thrown - assert(err === -89, "unknown"); // on macos + assert(err === -89, "-89"); // on macos assert(err === -99, "ephemeral-ports-exhausted"); assert(err === -104, "connection-reset"); @@ -297,26 +297,21 @@ export class TcpSocketImpl { const host = serializeIpAddress(remoteAddress); const ipFamily = `ipv${isIP(host)}`; try { + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Connecting, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state"); + assert(host === "0.0.0.0" || host === "0:0:0:0:0:0:0:0", "invalid-argument"); assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); assert(isUnicastIpAddress(remoteAddress) === false, "invalid-argument"); assert(isMulticastIpAddress(remoteAddress), "invalid-argument"); assert(isIPv4MappedAddress(remoteAddress) && this.ipv6Only(), "invalid-argument"); - - // TODO: test program `preview2_tcp_states.rs` for this assertion checks for invalid-state error - // however, WIT file specifies that this should throw an invalid-argument error - assert(remoteAddress.val.port === 0, "invalid-state"); + assert(remoteAddress.val.port === 0, "invalid-argument"); if (this[symbolSocketState].isBound === false) { this.#autoBind(network, ipFamily); } - assert( - this[symbolSocketState].connectionState === SocketConnectionState.Connected || - this[symbolSocketState].connectionState === SocketConnectionState.Listener, - "invalid-state" - ); - assert(network !== this.network, "invalid-argument"); assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); assert(remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument"); @@ -388,6 +383,10 @@ export class TcpSocketImpl { this[symbolOperations].connect--; this[symbolSocketState].connectionState = SocketConnectionState.Connecting; + + // TODO: this is a temporary workaround, move this to the connection callback + // when the connection is actually established + this[symbolSocketState].connectionState = SocketConnectionState.Connected; return [inputStream, outputStream]; } @@ -403,7 +402,7 @@ export class TcpSocketImpl { assert(this[symbolSocketState].errorState !== null, "invalid-state"); assert(this[symbolSocketState].isBound === false, "invalid-state"); assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); - assert(this[symbolSocketState].connectionState === SocketConnectionState.Listener, "invalid-state"); + assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state"); } catch (err) { this[symbolSocketState].errorState = err; throw err; @@ -438,7 +437,7 @@ export class TcpSocketImpl { throw new Error(err); } - this[symbolSocketState].connectionState = SocketConnectionState.Listener; + this[symbolSocketState].connectionState = SocketConnectionState.Listening; this[symbolOperations].listen--; } @@ -453,7 +452,7 @@ export class TcpSocketImpl { this[symbolOperations].accept++; try { - assert(this[symbolSocketState].connectionState !== SocketConnectionState.Listener, "invalid-state"); + assert(this[symbolSocketState].connectionState !== SocketConnectionState.Listening, "invalid-state"); } catch (err) { this[symbolSocketState].errorState = err; throw err; @@ -464,8 +463,8 @@ export class TcpSocketImpl { if (this[symbolSocketState].isBound === false) { this.#autoBind(this.network, this.addressFamily()); } - const inputStream = inputStreamCreate(SOCKET_INPUT_STREAM, this.id); - const outputStream = outputStreamCreate(SOCKET_OUTPUT_STREAM, this.id); + const inputStream = inputStreamCreate(SOCKET, ioCall(INPUT_STREAM_CREATE | SOCKET, null, {})); + const outputStream = outputStreamCreate(SOCKET, ioCall(OUTPUT_STREAM_CREATE | SOCKET, null, {})); // Because we have to return a valid TcpSocket resrouce type, // we need to instantiate the correct child class @@ -533,7 +532,6 @@ export class TcpSocketImpl { this.#socket.getpeername(out); const { address, port, family } = out; - this.#socketOptions.remoteAddress = address; this.#socketOptions.remotePort = port; this.#socketOptions.family = family.toLocaleLowerCase(); @@ -548,7 +546,7 @@ export class TcpSocketImpl { } isListening() { - return this[symbolSocketState].connectionState === SocketConnectionState.Listener; + return this[symbolSocketState].connectionState === SocketConnectionState.Listening; } /** @@ -695,8 +693,16 @@ export class TcpSocketImpl { */ setHopLimit(value) { assert(!value || value < 1, "invalid-argument", "The TTL value must be 1 or higher."); - assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); + console.log("setHopLimit", { + s: this[symbolSocketState], + o: this[symbolSocketState].errorState, + value + }); + // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); + // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connecting, "invalid-state"); + // assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state"); + // TODO: set this on the client socket as well this[symbolSocketState].hopLimit = value; } diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index eef6e2146..3d55bfe9f 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -94,7 +94,7 @@ export class IncomingDatagramStream { } [symbolDispose]() { - throw new Error("Not implemented"); + // TODO: stop receiving } } const incomingDatagramStreamCreate = IncomingDatagramStream._create; @@ -171,7 +171,7 @@ export class OutgoingDatagramStream { } [symbolDispose]() { - throw new Error("Not implemented"); + // TODO: stop sending } } const outgoingDatagramStreamCreate = OutgoingDatagramStream._create; @@ -209,9 +209,9 @@ export class UdpSocketImpl { #socketOptions = { family: "ipv4", - localAddress: "", + localAddress: undefined, localPort: 0, - remoteAddress: "", + remoteAddress: undefined, remotePort: 0, }; @@ -343,10 +343,11 @@ export class UdpSocketImpl { * @throws {invalid-argument} The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. */ #startConnect(network, remoteAddress = undefined) { - this[symbolSocketState].operationInProgress = false; + this[symbolOperations].connect++; - if (remoteAddress === undefined || this[symbolSocketState].state === SocketConnectionState.Connected) { - // reusing a connected socket. See #finishConnect() + if (remoteAddress === undefined || this[symbolSocketState].connectionState === SocketConnectionState.Connected) { + // TODO: should we reuse a connected socket if remoteAddress is undefined? + // See #finishConnect() return; } @@ -357,11 +358,10 @@ export class UdpSocketImpl { assert(this.#socketOptions.family.toLocaleLowerCase() !== ipFamily.toLocaleLowerCase(), "invalid-argument"); const { port } = remoteAddress.val; - this.#socketOptions.remoteAddress = host; + this.#socketOptions.remoteAddress = host; // can be undefined this.#socketOptions.remotePort = port; this.network = network; - this[symbolSocketState].operationInProgress = true; } /** @@ -372,18 +372,26 @@ export class UdpSocketImpl { * @throws {would-block} Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) */ #finishConnect() { + // Note: remoteAddress can be undefined const { remoteAddress, remotePort } = this.#socketOptions; - this[symbolSocketState].state = SocketConnectionState.Connecting; + this[symbolSocketState].connectionState = SocketConnectionState.Connecting; + + // TODO: figure out how to reuse a connected socket + console.log("connecing ", remoteAddress, remotePort); + const err = this.#socket.connect(remoteAddress ?? null, remotePort); - if (this[symbolSocketState].state === SocketConnectionState.Connected) { - // TODO: figure out how to reuse a connected socket - this.#socket.connect(); + console.log({ + err, + }); + + if (err === 0) { + this[symbolSocketState].connectionState = SocketConnectionState.Connected; } else { - this.#socket.connect(remoteAddress, remotePort); + assert(err === -22, "invalid-argument"); + assert(true, "unknown", err); } - this[symbolSocketState].operationInProgress = false; - this[symbolSocketState].state = SocketConnectionState.Connected; + this[symbolOperations].connect--; } /** @@ -445,8 +453,9 @@ export class UdpSocketImpl { * @throws {invalid-state} The socket is not streaming to a specific remote address. (ENOTCONN) */ remoteAddress() { + console.log("remoteAddress", this[symbolSocketState]); assert( - this[symbolSocketState].state !== SocketConnectionState.Connected, + this[symbolSocketState].connectionState !== SocketConnectionState.Connected, "invalid-state", "The socket is not streaming to a specific remote address" ); @@ -457,6 +466,10 @@ export class UdpSocketImpl { assert(out.address === undefined, "invalid-state", "The socket is not streaming to a specific remote address"); const { address, port, family } = out; + this.#socketOptions.remoteAddress = address; + this.#socketOptions.remotePort = port; + this.#socketOptions.family = family.toLocaleLowerCase(); + return { tag: family.toLocaleLowerCase(), val: { diff --git a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js index e5b66e2b0..0d83f19e3 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets/wasi-sockets.js @@ -157,11 +157,12 @@ export class WasiSockets { IncomingDatagramStream, }; - class TcpSocket { + class TcpSocket extends TcpSocketImpl { /** * @param {IpAddressFamily} addressFamily * */ constructor(addressFamily) { + super(addressFamily, TcpSocket, net.socketCnt++); net.tcpSockets.set(this.id, this); } } From 908297a82413e36ce61fed7b3bbdd95ba45e1ad8 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 27 Nov 2023 15:02:50 +0100 Subject: [PATCH 62/65] fix: improve tests and add more comments for hop limits --- .../lib/nodejs/sockets/tcp-socket-impl.js | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 11a1bbb36..028db6558 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -83,7 +83,7 @@ export class TcpSocketImpl { }; [symbolSocketState] = { - errorState: null, + lastErrorState: null, isBound: false, ipv6Only: false, connectionState: SocketConnectionState.Closed, @@ -229,9 +229,9 @@ export class TcpSocketImpl { this.#socketOptions.localPort = port; this.network = network; this[symbolOperations].bind++; - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } } @@ -268,13 +268,13 @@ export class TcpSocketImpl { assert(true, "unknown", err); } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; this[symbolSocketState].isBound = true; this[symbolOperations].bind--; this.#cacheBoundAddress(); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } } @@ -316,11 +316,11 @@ export class TcpSocketImpl { assert(ipFamily.toLocaleLowerCase() === "ipv0", "invalid-argument"); assert(remoteAddress.val.port === 0 && platform() === "win32", "invalid-argument"); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; this.#socketOptions.remoteIpSocketAddress = remoteAddress; this.#socketOptions.remoteAddress = host; @@ -344,11 +344,11 @@ export class TcpSocketImpl { try { assert(this[symbolOperations].connect === 0, "not-in-progress"); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; const { localAddress, localPort, remoteAddress, remotePort, family } = this.#socketOptions; const connectReq = new TCPConnectWrap(); @@ -383,7 +383,7 @@ export class TcpSocketImpl { this[symbolOperations].connect--; this[symbolSocketState].connectionState = SocketConnectionState.Connecting; - + // TODO: this is a temporary workaround, move this to the connection callback // when the connection is actually established this[symbolSocketState].connectionState = SocketConnectionState.Connected; @@ -399,16 +399,16 @@ export class TcpSocketImpl { */ startListen() { try { - assert(this[symbolSocketState].errorState !== null, "invalid-state"); + assert(this[symbolSocketState].lastErrorState !== null, "invalid-state"); assert(this[symbolSocketState].isBound === false, "invalid-state"); assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state"); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; this[symbolOperations].listen++; } @@ -422,11 +422,11 @@ export class TcpSocketImpl { try { assert(this[symbolOperations].listen === 0, "not-in-progress"); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; const err = this.#socket.listen(this[symbolSocketState].backlogSize); if (err) { @@ -454,11 +454,11 @@ export class TcpSocketImpl { try { assert(this[symbolSocketState].connectionState !== SocketConnectionState.Listening, "invalid-state"); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; if (this[symbolSocketState].isBound === false) { this.#autoBind(this.network, this.addressFamily()); @@ -679,6 +679,7 @@ export class TcpSocketImpl { /** * @returns {number} + * @description Not available on Node.js (see https://github.com/WebAssembly/wasi-sockets/blob/main/Posix-compatibility.md#socket-options) */ hopLimit() { return this[symbolSocketState].hopLimit; @@ -690,20 +691,11 @@ export class TcpSocketImpl { * @throws {invalid-argument} (set) The TTL value must be 1 or higher. * @throws {invalid-state} (set) The socket is already in the Connection state. * @throws {invalid-state} (set) The socket is already in the Listener state. + * @description Not available on Node.js (see https://github.com/WebAssembly/wasi-sockets/blob/main/Posix-compatibility.md#socket-options) */ setHopLimit(value) { - assert(!value || value < 1, "invalid-argument", "The TTL value must be 1 or higher."); - console.log("setHopLimit", { - s: this[symbolSocketState], - o: this[symbolSocketState].errorState, - value - }); + assert(value < 1, "invalid-argument", "The TTL value must be 1 or higher."); - // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); - // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connecting, "invalid-state"); - // assert(this[symbolSocketState].connectionState === SocketConnectionState.Listening, "invalid-state"); - - // TODO: set this on the client socket as well this[symbolSocketState].hopLimit = value; } @@ -722,8 +714,9 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Connection state. */ setReceiveBufferSize(value) { + // TODO: review these assertions based on WIT specs + // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well this[symbolSocketState].receiveBufferSize = value; @@ -744,8 +737,9 @@ export class TcpSocketImpl { * @throws {invalid-state} (set) The socket is already in the Listener state. */ setSendBufferSize(value) { + // TODO: review these assertions based on WIT specs + // assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); assert(value === 0n, "invalid-argument", "The provided value was 0."); - assert(this[symbolSocketState].connectionState === SocketConnectionState.Connected, "invalid-state"); // TODO: set this on the client socket as well this[symbolSocketState].sendBufferSize = value; From dead3c004f7656fd9b6d1fd2f0f0982f43c38f22 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Mon, 27 Nov 2023 19:00:18 +0100 Subject: [PATCH 63/65] chore: rename errorState property --- .../lib/nodejs/sockets/udp-socket-impl.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 3d55bfe9f..c008dc0ac 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -196,7 +196,7 @@ export class UdpSocketImpl { }; [symbolSocketState] = { - errorState: null, + lastErrorState: null, isBound: false, ipv6Only: false, connectionState: SocketConnectionState.Closed, @@ -264,9 +264,9 @@ export class UdpSocketImpl { this.#socketOptions.localPort = port; this.network = network; this[symbolOperations].bind++; - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } } @@ -312,13 +312,13 @@ export class UdpSocketImpl { assert(true, "unknown", err); } - this[symbolSocketState].errorState = null; + this[symbolSocketState].lastErrorState = null; this[symbolSocketState].isBound = true; this[symbolOperations].bind--; this.#cacheBoundAddress(); } catch (err) { - this[symbolSocketState].errorState = err; + this[symbolSocketState].lastErrorState = err; throw err; } } From d3ce7c0a5ff4b9ec6567113869e9510e85554079 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 28 Nov 2023 15:21:16 +0100 Subject: [PATCH 64/65] fix: make unit tests pass --- .../lib/nodejs/sockets/tcp-socket-impl.js | 5 +- .../lib/nodejs/sockets/udp-socket-impl.js | 9 +- packages/preview2-shim/test/test.js | 116 +++++++++++------- 3 files changed, 73 insertions(+), 57 deletions(-) diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index 028db6558..d59722aef 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -791,10 +791,7 @@ export class TcpSocketImpl { } } - server() { - return this.#socket; - } - client() { + handle() { return this.#socket; } } diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index c008dc0ac..084da7358 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -377,14 +377,9 @@ export class UdpSocketImpl { this[symbolSocketState].connectionState = SocketConnectionState.Connecting; // TODO: figure out how to reuse a connected socket - console.log("connecing ", remoteAddress, remotePort); const err = this.#socket.connect(remoteAddress ?? null, remotePort); - console.log({ - err, - }); - - if (err === 0) { + if (!err) { this[symbolSocketState].connectionState = SocketConnectionState.Connected; } else { assert(err === -22, "invalid-argument"); @@ -629,7 +624,7 @@ export class UdpSocketImpl { this.#socket.close(); } - client() { + handle() { return this.#socket; } } diff --git a/packages/preview2-shim/test/test.js b/packages/preview2-shim/test/test.js index 0ae1960d7..b2bc260af 100644 --- a/packages/preview2-shim/test/test.js +++ b/packages/preview2-shim/test/test.js @@ -1,4 +1,4 @@ -import { deepEqual, equal, ok, throws, strictEqual, notEqual } from "node:assert"; +import { deepEqual, deepStrictEqual, equal, notEqual, ok, strictEqual, throws } from "node:assert"; import { mock } from "node:test"; import { fileURLToPath } from "node:url"; @@ -156,7 +156,7 @@ suite("Node.js Preview2", () => { equal(network2.id, 1); }); - test("sockets.tcpCreateSocket() should throw no-supported", async () => { + test("sockets.tcpCreateSocket() should throw not-supported", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const socket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); notEqual(socket, null); @@ -165,10 +165,7 @@ suite("Node.js Preview2", () => { () => { sockets.tcpCreateSocket.createTcpSocket("abc"); }, - { - name: "Error", - code: sockets.network.errorCode.notSupported, - } + (err) => err === sockets.network.errorCode.notSupported ); }); test("tcp.bind(): should bind to a valid ipv4 address", async () => { @@ -179,7 +176,7 @@ suite("Node.js Preview2", () => { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], - port: 0, + port: 1337, }, }; tcpSocket.startBind(network, localAddress); @@ -190,13 +187,13 @@ suite("Node.js Preview2", () => { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], - port: 0, + port: 1337, }, }); equal(tcpSocket.addressFamily(), "ipv4"); }); - test("tcp.bind(): should bind to a valid ipv6 address", async () => { + test("tcp.bind(): should bind to a valid ipv6 address and port=0", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv6); @@ -212,13 +209,20 @@ suite("Node.js Preview2", () => { equal(tcpSocket.network.id, network.id); equal(tcpSocket.addressFamily(), "ipv6"); - deepEqual(tcpSocket.localAddress(), { + + const boundAddress = tcpSocket.localAddress(); + const expectedAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0, 0, 0], - port: 0, + // port will be assigned by the OS, so it should be > 0 + // port: 0, }, - }); + }; + + strictEqual(boundAddress.tag, expectedAddress.tag); + deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); + strictEqual(boundAddress.val.port > 0, true); }); test("tcp.bind(): should throw invalid-argument when invalid address family", async () => { @@ -238,9 +242,7 @@ suite("Node.js Preview2", () => { () => { tcpSocket.startBind(network, localAddress); }, - { - code: "invalid-argument", - } + (err) => err === sockets.network.errorCode.invalidArgument ); }); @@ -263,9 +265,7 @@ suite("Node.js Preview2", () => { // already bound tcpSocket.startBind(network, localAddress); }, - { - code: "invalid-state", - } + (err) => err === sockets.network.errorCode.invalidState ); }); @@ -281,7 +281,7 @@ suite("Node.js Preview2", () => { }, }; - mock.method(tcpSocket.server(), "listen", () => { + mock.method(tcpSocket.handle(), "listen", () => { // mock listen }); @@ -290,12 +290,12 @@ suite("Node.js Preview2", () => { tcpSocket.startListen(); tcpSocket.finishListen(); - strictEqual(tcpSocket.server().listen.mock.calls.length, 1); + strictEqual(tcpSocket.handle().listen.mock.calls.length, 1); mock.reset(); }); - test("tcp.connect(): should connect to a valid ipv4 address", async () => { + test("tcp.connect(): should connect to a valid ipv4 address and port=0", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const tcpSocket = sockets.tcpCreateSocket.createTcpSocket(sockets.network.IpAddressFamily.ipv4); @@ -315,7 +315,7 @@ suite("Node.js Preview2", () => { }, }; - mock.method(tcpSocket.client(), "connect", () => { + mock.method(tcpSocket.handle(), "connect", () => { // mock connect }); @@ -324,17 +324,23 @@ suite("Node.js Preview2", () => { tcpSocket.startConnect(network, remoteAddress); tcpSocket.finishConnect(); - strictEqual(tcpSocket.client().connect.mock.calls.length, 1); + strictEqual(tcpSocket.handle().connect.mock.calls.length, 1); equal(tcpSocket.network.id, network.id); equal(tcpSocket.addressFamily(), "ipv4"); - deepEqual(tcpSocket.localAddress(), { + + const boundAddress = tcpSocket.localAddress(); + const expectedAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], port: 0, }, - }); + }; + + strictEqual(boundAddress.tag, expectedAddress.tag); + deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); + strictEqual(boundAddress.val.port > 0, true); }); }); @@ -345,18 +351,18 @@ suite("Node.js Preview2", () => { notEqual(socket1.id, 1); const socket2 = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); notEqual(socket2.id, 1); + }); + test("sockets.udpCreateSocket() should not-support on invalid ip family", async () => { + const { sockets } = await import("@bytecodealliance/preview2-shim"); throws( () => { sockets.udpCreateSocket.createUdpSocket("xyz"); }, - { - name: "Error", - code: sockets.network.errorCode.notSupported, - } + (err) => err === sockets.network.errorCode.notSupported ); }); - test("udp.bind(): should bind to a valid ipv4 address", async () => { + test("udp.bind(): should bind to a valid ipv4 address and port=0", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv4); @@ -371,16 +377,21 @@ suite("Node.js Preview2", () => { socket.finishBind(); equal(socket.network.id, network.id); - deepEqual(socket.localAddress(), { + + const boundAddress = socket.localAddress(); + const expectedAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], port: 0, }, - }); + }; + strictEqual(boundAddress.tag, expectedAddress.tag); + deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); + strictEqual(boundAddress.val.port > 0, true); equal(socket.addressFamily(), "ipv4"); }); - test("udp.bind(): should bind to a valid ipv6 address", async () => { + test("udp.bind(): should bind to a valid ipv6 address and port=0", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); const network = sockets.instanceNetwork.instanceNetwork(); const socket = sockets.udpCreateSocket.createUdpSocket(sockets.network.IpAddressFamily.ipv6); @@ -395,13 +406,19 @@ suite("Node.js Preview2", () => { socket.finishBind(); equal(socket.network.id, network.id); - deepEqual(socket.localAddress(), { + + const boundAddress = socket.localAddress(); + const expectedAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0, 0, 0], - port: 0, + // port will be assigned by the OS, so it should be > 0 + // port: 0, }, - }); + }; + strictEqual(boundAddress.tag, expectedAddress.tag); + deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); + strictEqual(boundAddress.val.port > 0, true); equal(socket.addressFamily(), "ipv6"); }); test("udp.stream(): should connect to a valid ipv4 address", async () => { @@ -423,7 +440,7 @@ suite("Node.js Preview2", () => { }, }; - mock.method(socket.client(), "connect", () => { + mock.method(socket.handle(), "connect", () => { // mock connect }); @@ -431,17 +448,24 @@ suite("Node.js Preview2", () => { socket.finishBind(); socket.stream(remoteAddress); - strictEqual(socket.client().connect.mock.calls.length, 1); + strictEqual(socket.handle().connect.mock.calls.length, 1); strictEqual(socket.network.id, network.id); strictEqual(socket.addressFamily(), "ipv4"); - deepEqual(socket.localAddress(), { + + const boundAddress = socket.localAddress(); + const expectedAddress = { tag: sockets.network.IpAddressFamily.ipv4, val: { address: [0, 0, 0, 0], - port: 0, + // port will be assigned by the OS, so it should be > 0 + // port: 0, }, - }); + }; + + strictEqual(boundAddress.tag, expectedAddress.tag); + deepStrictEqual(boundAddress.val.address, expectedAddress.val.address); + strictEqual(boundAddress.val.port > 0, true); }); test("udp.stream(): should connect to a valid ipv6 address", async () => { const { sockets } = await import("@bytecodealliance/preview2-shim"); @@ -451,18 +475,18 @@ suite("Node.js Preview2", () => { tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0, 0, 0], - port: 0, + port: 1337, }, }; const remoteAddress = { tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0, 0, 0], - port: 80, + port: 1336, }, }; - mock.method(socket.client(), "connect", () => { + mock.method(socket.handle(), "connect", () => { // mock connect }); @@ -470,7 +494,7 @@ suite("Node.js Preview2", () => { socket.finishBind(); socket.stream(remoteAddress); - strictEqual(socket.client().connect.mock.calls.length, 1); + strictEqual(socket.handle().connect.mock.calls.length, 1); strictEqual(socket.network.id, network.id); strictEqual(socket.addressFamily(), "ipv6"); @@ -478,7 +502,7 @@ suite("Node.js Preview2", () => { tag: sockets.network.IpAddressFamily.ipv6, val: { address: [0, 0, 0, 0, 0, 0, 0, 0], - port: 0, + port: 1337, }, }); }); From 5371db28827bc609156ffc455650657a08c9ef47 Mon Sep 17 00:00:00 2001 From: Wassim Chegham Date: Tue, 28 Nov 2023 15:26:48 +0100 Subject: [PATCH 65/65] chore: fix linting --- packages/preview2-shim/lib/nodejs/sockets.js | 4 +- .../lib/nodejs/sockets/socketopts-bindings.js | 94 +++++++++++++++++++ .../lib/nodejs/sockets/tcp-socket-impl.js | 2 - .../lib/nodejs/sockets/udp-socket-impl.js | 2 - 4 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 packages/preview2-shim/lib/nodejs/sockets/socketopts-bindings.js diff --git a/packages/preview2-shim/lib/nodejs/sockets.js b/packages/preview2-shim/lib/nodejs/sockets.js index c581bfa66..2874e82f6 100644 --- a/packages/preview2-shim/lib/nodejs/sockets.js +++ b/packages/preview2-shim/lib/nodejs/sockets.js @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - import { WasiSockets } from "./sockets/wasi-sockets.js"; export const { @@ -10,4 +8,4 @@ export const { udpCreateSocket, tcp, udp, -} = new WasiSockets();; \ No newline at end of file +} = new WasiSockets(); \ No newline at end of file diff --git a/packages/preview2-shim/lib/nodejs/sockets/socketopts-bindings.js b/packages/preview2-shim/lib/nodejs/sockets/socketopts-bindings.js new file mode 100644 index 000000000..ee9bb27f7 --- /dev/null +++ b/packages/preview2-shim/lib/nodejs/sockets/socketopts-bindings.js @@ -0,0 +1,94 @@ +import { platform } from "node:os"; +import { _errnoException } from "node:util"; +import { types, refType } from "ref-napi"; +import { Library, errno as _errno } from "ffi-napi"; + +const tryGetUV = (() => { + let UV = null; + return () => { + if (UV === null) { + try { + UV = typeof process.binding === "function" ? process.binding("uv") : undefined; + } catch (ex) { + // Continue regardless + } + } + return UV; + }; +})(); + +const uvErrName = (errno) => { + const UV = tryGetUV(); + return UV && UV.errname ? UV.errname(errno) : "UNKNOWN"; +}; + +const errnoException = (errno, syscall, original) => { + if (_errnoException) { + return _errnoException(-errno, syscall, original); + } + + const errname = uvErrName(-errno), + message = original ? `${syscall} ${errname} (${errno}) ${original}` : `${syscall} ${errname} (${errno})`; + + const e = new Error(message); + e.code = errname; + e.errno = errname; + e.syscall = syscall; + return e; +}; + +const createFFI = () => { + const cInt = types.int; + const cVoid = types.void; + + return Library(null, { + //name ret 1 2 3 4 5 + setsockopt: [cInt, [cInt, cInt, cInt, refType(cVoid), cInt]], + getsockopt: [cInt, [cInt, cInt, cInt, refType(cVoid), refType(cInt)]], + }); +}; + +const ffi = (() => { + let instance; + return () => { + if (!instance) { + instance = createFFI(); + } + return instance; + }; +})(); + +const _setsockopt = (fd, level, name, value, valueLength) => { + if (fd == null) { + return false; + } + + const err = ffi().setsockopt(fd, level, name, value, valueLength); + + if (err !== 0) { + const errno = _errno(); + throw errnoException(errno, "setsockopt"); + } + + return true; +}; + +const _getsockopt = (fd, level, name, value, valueLength) => { + if (fd == null) { + return false; + } + + const err = ffi().getsockopt(fd, level, name, value, valueLength); + + if (err !== 0) { + const errno = _errno(); + throw errnoException(errno, "getsockopt"); + } + return true; +}; + +const noop = () => false; +const isWin32 = platform() === "win32"; + +export const setsockopt = isWin32 ? noop : _setsockopt; +export const getsockopt = isWin32 ? noop : _getsockopt; diff --git a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js index d59722aef..ac994976a 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/tcp-socket-impl.js @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - /** * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").Network} Network * @typedef {import("../../../types/interfaces/wasi-sockets-network.js").IpSocketAddress} IpSocketAddress diff --git a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js index 084da7358..8e39f4e82 100644 --- a/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js +++ b/packages/preview2-shim/lib/nodejs/sockets/udp-socket-impl.js @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - /** * @typedef {import("../../types/interfaces/wasi-sockets-network").Network} Network * @typedef {import("../../types/interfaces/wasi-sockets-network").IpSocketAddress} IpSocketAddress