app.ts 3.7 KB
Newer Older
A
Asher 已提交
1
import { logger } from "@coder/logger"
2
import compression from "compression"
A
Asher 已提交
3 4 5 6
import express, { Express } from "express"
import { promises as fs } from "fs"
import http from "http"
import * as httpolyglot from "httpolyglot"
7
import { Disposable } from "../common/emitter"
8
import * as util from "../common/util"
A
Asher 已提交
9
import { DefaultedArgs } from "./cli"
10
import { disposer } from "./http"
11
import { isNodeJSErrnoException } from "./util"
A
Asher 已提交
12
import { handleUpgrade } from "./wsRouter"
A
Asher 已提交
13

14 15
type ListenOptions = Pick<DefaultedArgs, "socket" | "port" | "host">

16 17 18 19 20 21 22 23 24
export interface App extends Disposable {
  /** Handles regular HTTP requests. */
  router: Express
  /** Handles websocket requests. */
  wsRouter: Express
  /** The underlying HTTP server. */
  server: http.Server
}

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
const listen = (server: http.Server, { host, port, socket }: ListenOptions) => {
  return new Promise<void>(async (resolve, reject) => {
    server.on("error", reject)

    const onListen = () => {
      // Promise resolved earlier so this is an unrelated error.
      server.off("error", reject)
      server.on("error", (err) => util.logError(logger, "http server error", err))

      resolve()
    }

    if (socket) {
      try {
        await fs.unlink(socket)
      } catch (error: any) {
        handleArgsSocketCatchError(error)
      }

      server.listen(socket, onListen)
    } else {
      // [] is the correct format when using :: but Node errors with them.
      server.listen(port, host.replace(/^\[|\]$/g, ""), onListen)
    }
  })
}

A
Asher 已提交
52 53 54
/**
 * Create an Express app and an HTTP/S server to serve it.
 */
55 56 57
export const createApp = async (args: DefaultedArgs): Promise<App> => {
  const router = express()
  router.use(compression())
58

A
Asher 已提交
59 60 61 62 63 64
  const server = args.cert
    ? httpolyglot.createServer(
        {
          cert: args.cert && (await fs.readFile(args.cert.value)),
          key: args["cert-key"] && (await fs.readFile(args["cert-key"])),
        },
65
        router,
A
Asher 已提交
66
      )
67 68 69
    : http.createServer(router)

  const dispose = disposer(server)
A
Asher 已提交
70

71
  await listen(server, args)
A
Asher 已提交
72

73 74
  const wsRouter = express()
  handleUpgrade(wsRouter, server)
A
Asher 已提交
75

76
  return { router, wsRouter, server, dispose }
A
Asher 已提交
77 78 79
}

/**
A
Asher 已提交
80
 * Get the address of a server as a string (protocol *is* included) while
A
Asher 已提交
81
 * ensuring there is one (will throw if there isn't).
82 83
 *
 * The address might be a URL or it might be a pipe or socket path.
A
Asher 已提交
84
 */
85
export const ensureAddress = (server: http.Server, protocol: string): URL | string => {
A
Asher 已提交
86
  const addr = server.address()
87

A
Asher 已提交
88
  if (!addr) {
89
    throw new Error("Server has no address")
A
Asher 已提交
90
  }
91

A
Asher 已提交
92
  if (typeof addr !== "string") {
93
    return new URL(`${protocol}://${addr.address}:${addr.port}`)
A
Asher 已提交
94
  }
95 96

  // If this is a string then it is a pipe or Unix socket.
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
  return addr
}

/**
 * Handles error events from the server.
 *
 * If the outlying Promise didn't resolve
 * then we reject with the error.
 *
 * Otherwise, we log the error.
 *
 * We extracted into a function so that we could
 * test this logic more easily.
 */
export const handleServerError = (resolved: boolean, err: Error, reject: (err: Error) => void) => {
  // Promise didn't resolve earlier so this means it's an error
  // that occurs before the server can successfully listen.
  // Possibly triggered by listening on an invalid port or socket.
  if (!resolved) {
    reject(err)
  } else {
    // Promise resolved earlier so this is an unrelated error.
    util.logError(logger, "http server error", err)
  }
}

/**
 * Handles the error that occurs in the catch block
 * after we try fs.unlink(args.socket).
 *
 * We extracted into a function so that we could
 * test this logic more easily.
 */
export const handleArgsSocketCatchError = (error: any) => {
  if (!isNodeJSErrnoException(error) || error.code !== "ENOENT") {
    logger.error(error.message ? error.message : error)
  }
A
Asher 已提交
134
}