coder-cloud.ts 4.4 KB
Newer Older
A
Anmol Sethi 已提交
1
import { logger } from "@coder/logger"
A
Anmol Sethi 已提交
2
import { spawn } from "child_process"
A
Anmol Sethi 已提交
3 4
import delay from "delay"
import fs from "fs"
A
Anmol Sethi 已提交
5 6
import path from "path"
import split2 from "split2"
A
Anmol Sethi 已提交
7 8 9 10
import { promisify } from "util"
import xdgBasedir from "xdg-basedir"

const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent")
A
Anmol Sethi 已提交
11

12
export async function coderCloudLink(serverName: string): Promise<void> {
A
Anmol Sethi 已提交
13 14 15 16
  const agent = spawn(coderCloudAgent, ["link", serverName], {
    stdio: ["inherit", "inherit", "pipe"],
  })

A
Anmol Sethi 已提交
17
  agent.stderr.pipe(split2()).on("data", (line) => {
A
Anmol Sethi 已提交
18 19 20 21 22 23 24
    line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
    logger.info(line)
  })

  return new Promise((res, rej) => {
    agent.on("error", rej)

A
Anmol Sethi 已提交
25
    agent.on("close", (code) => {
A
Anmol Sethi 已提交
26 27 28 29 30 31 32 33 34 35
      if (code !== 0) {
        rej({
          message: `coder cloud agent exited with ${code}`,
        })
        return
      }
      res()
    })
  })
}
A
Anmol Sethi 已提交
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56

export function coderCloudProxy(addr: string) {
  // addr needs to be in host:port format.
  // So we trim the protocol.
  addr = addr.replace(/^https?:\/\//, "")

  if (!xdgBasedir.config) {
    return
  }

  const sessionTokenPath = path.join(xdgBasedir.config, "coder-cloud", "session")

  const _proxy = async () => {
    await waitForPath(sessionTokenPath)

    logger.info("exposing coder-server with coder-cloud")

    const agent = spawn(coderCloudAgent, ["proxy", "--code-server-addr", addr], {
      stdio: ["inherit", "inherit", "pipe"],
    })

A
Anmol Sethi 已提交
57
    agent.stderr.pipe(split2()).on("data", (line) => {
A
Anmol Sethi 已提交
58 59 60 61 62 63 64
      line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
      logger.info(line)
    })

    return new Promise((res, rej) => {
      agent.on("error", rej)

A
Anmol Sethi 已提交
65
      agent.on("close", (code) => {
A
Anmol Sethi 已提交
66 67 68 69 70 71 72 73 74 75 76 77 78 79
        if (code !== 0) {
          rej({
            message: `coder cloud agent exited with ${code}`,
          })
          return
        }
        res()
      })
    })
  }

  const proxy = async () => {
    try {
      await _proxy()
A
Anmol Sethi 已提交
80
    } catch (err) {
A
Anmol Sethi 已提交
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
      logger.error(err.message)
    }
    setTimeout(proxy, 3000)
  }
  proxy()
}

/**
 * waitForPath efficiently implements waiting for the existence of a path.
 *
 * We intentionally do not use fs.watchFile as it is very slow from testing.
 * I believe it polls instead of watching.
 *
 * The way this works is for each level of the path it will check if it exists
 * and if not, it will wait for it. e.g. if the path is /home/nhooyr/.config/coder-cloud/session
 * then first it will check if /home exists, then /home/nhooyr and so on.
 *
 * The wait works by first creating a watch promise for the p segment.
 * We call fs.watch on the dirname of the p segment. When the dirname has a change,
 * we check if the p segment exists and if it does, we resolve the watch promise.
 * On any error or the watcher being closed, we reject the watch promise.
 *
 * Once that promise is setup, we check if the p segment exists with fs.exists
 * and if it does, we close the watcher and return.
 *
 * Now we race the watch promise and a 2000ms delay promise. Once the race
 * is complete, we close the watcher.
 *
 * If the watch promise was the one to resolve, we return.
 * Otherwise we setup the watch promise again and retry.
 *
 * This combination of polling and watching is very reliable and efficient.
 */
async function waitForPath(p: string): Promise<void> {
  const segs = p.split(path.sep)
  for (let i = 0; i < segs.length; i++) {
    const s = path.join("/", ...segs.slice(0, i + 1))
    // We need to wait for each segment to exist.
    await _waitForPath(s)
  }
}

async function _waitForPath(p: string): Promise<void> {
  const watchDir = path.dirname(p)

  logger.debug(`waiting for ${p}`)

  for (;;) {
    const w = fs.watch(watchDir)
    const watchPromise = new Promise<void>((res, rej) => {
      w.on("change", async () => {
        if (await promisify(fs.exists)(p)) {
          res()
        }
      })
      w.on("close", () => rej(new Error("watcher closed")))
      w.on("error", rej)
    })

    // We want to ignore any errors from this promise being rejected if the file
    // already exists below.
    watchPromise.catch(() => {})

    if (await promisify(fs.exists)(p)) {
      // The path exists!
      w.close()
      return
    }

    // Now we wait for either the watch promise to resolve/reject or 2000ms.
    const s = await Promise.race([watchPromise.then(() => "exists"), delay(2000)])
    w.close()
    if (s === "exists") {
      return
    }
  }
}