forked from coder/code-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcoder-cloud.ts
More file actions
157 lines (135 loc) · 4.35 KB
/
Copy pathcoder-cloud.ts
File metadata and controls
157 lines (135 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
import { logger } from "@coder/logger"
import { spawn } from "child_process"
import delay from "delay"
import fs from "fs"
import path from "path"
import split2 from "split2"
import { promisify } from "util"
import xdgBasedir from "xdg-basedir"
const coderCloudAgent = path.resolve(__dirname, "../../lib/coder-cloud-agent")
export async function coderCloudLink(serverName: string): Promise<void> {
const agent = spawn(coderCloudAgent, ["link", serverName], {
stdio: ["inherit", "inherit", "pipe"],
})
agent.stderr.pipe(split2()).on("data", (line) => {
line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
logger.info(line)
})
return new Promise((res, rej) => {
agent.on("error", rej)
agent.on("close", (code) => {
if (code !== 0) {
rej({
message: `coder cloud agent exited with ${code}`,
})
return
}
res()
})
})
}
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"],
})
agent.stderr.pipe(split2()).on("data", (line) => {
line = line.replace(/^[0-9-]+ [0-9:]+ [^ ]+\t/, "")
logger.info(line)
})
return new Promise((res, rej) => {
agent.on("error", rej)
agent.on("close", (code) => {
if (code !== 0) {
rej({
message: `coder cloud agent exited with ${code}`,
})
return
}
res()
})
})
}
const proxy = async () => {
try {
await _proxy()
} catch (err) {
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
}
}
}