import { logger } from "@coder/logger" import * as express from "express" import * as http from "http" import * as net from "net" import * as nodeFetch from "node-fetch" import Websocket from "ws" import * as util from "../../src/common/util" import { ensureAddress } from "../../src/node/app" import { handleUpgrade } from "../../src/node/wsRouter" // Perhaps an abstraction similar to this should be used in app.ts as well. export class HttpServer { private readonly sockets = new Set<net.Socket>() private cleanupTimeout?: NodeJS.Timeout // See usage in test/integration.ts public constructor(private readonly hs = http.createServer()) { this.hs.on("connection", (socket) => { this.sockets.add(socket) socket.on("close", () => { this.sockets.delete(socket) if (this.cleanupTimeout && this.sockets.size === 0) { clearTimeout(this.cleanupTimeout) this.cleanupTimeout = undefined } }) }) } /** * listen starts the server on a random localhost port. * Use close to cleanup when done. */ public listen(fn: http.RequestListener): Promise<void> { this.hs.on("request", fn) let resolved = false return new Promise((res, rej) => { this.hs.listen(0, "localhost", () => { res() resolved = true }) this.hs.on("error", (err) => { if (!resolved) { rej(err) } else { // Promise resolved earlier so this is some other error. util.logError(logger, "http server error", err) } }) }) } /** * Send upgrade requests to an Express app. */ public listenUpgrade(app: express.Express): void { handleUpgrade(app, this.hs) } /** * close cleans up the server. */ public close(): Promise<void> { return new Promise((res, rej) => { // Close will not actually close anything; it just waits until everything // is closed. this.hs.close((err) => { if (err) { rej(err) return } res() }) // If there are sockets remaining we might need to force close them or // this promise might never resolve. if (this.sockets.size > 0) { // Give sockets a chance to close up shop. this.cleanupTimeout = setTimeout(() => { this.cleanupTimeout = undefined for (const socket of this.sockets.values()) { console.warn("a socket was left hanging") socket.destroy() } }, 1000) } }) } /** * fetch fetches the request path. * The request path must be rooted! */ public fetch(requestPath: string, opts?: nodeFetch.RequestInit): Promise<nodeFetch.Response> { return nodeFetch.default(`${ensureAddress(this.hs)}${requestPath}`, opts) } /** * Open a websocket against the requset path. */ public ws(requestPath: string): Websocket { return new Websocket(`${ensureAddress(this.hs).replace("http:", "ws:")}${requestPath}`) } public port(): number { const addr = this.hs.address() if (addr && typeof addr === "object") { return addr.port } throw new Error("server not listening or listening on unix socket") } }