@@ -2,9 +2,14 @@ import { spawn } from "child_process"
2
2
import path from "path"
3
3
import { logger } from "@coder/logger"
4
4
import split2 from "split2"
5
+ import delay from "delay"
6
+ import fs from "fs"
7
+ import { promisify } from "util"
8
+ import xdgBasedir from "xdg-basedir"
9
+
10
+ const coderCloudAgent = path . resolve ( __dirname , "../../lib/coder-cloud-agent" )
5
11
6
12
export async function coderCloudExpose ( serverName : string ) : Promise < void > {
7
- const coderCloudAgent = path . resolve ( __dirname , "../../lib/coder-cloud-agent" )
8
13
const agent = spawn ( coderCloudAgent , [ "link" , serverName ] , {
9
14
stdio : [ "inherit" , "inherit" , "pipe" ] ,
10
15
} )
@@ -28,3 +33,125 @@ export async function coderCloudExpose(serverName: string): Promise<void> {
28
33
} )
29
34
} )
30
35
}
36
+
37
+ export function coderCloudProxy ( addr : string ) {
38
+ // addr needs to be in host:port format.
39
+ // So we trim the protocol.
40
+ addr = addr . replace ( / ^ h t t p s ? : \/ \/ / , "" )
41
+
42
+ if ( ! xdgBasedir . config ) {
43
+ return
44
+ }
45
+
46
+ const sessionTokenPath = path . join ( xdgBasedir . config , "coder-cloud" , "session" )
47
+
48
+ const _proxy = async ( ) => {
49
+ await waitForPath ( sessionTokenPath )
50
+
51
+ logger . info ( "exposing coder-server with coder-cloud" )
52
+
53
+ const agent = spawn ( coderCloudAgent , [ "proxy" , "--code-server-addr" , addr ] , {
54
+ stdio : [ "inherit" , "inherit" , "pipe" ] ,
55
+ } )
56
+
57
+ agent . stderr . pipe ( split2 ( ) ) . on ( "data" , line => {
58
+ line = line . replace ( / ^ [ 0 - 9 - ] + [ 0 - 9 : ] + [ ^ ] + \t / , "" )
59
+ logger . info ( line )
60
+ } )
61
+
62
+ return new Promise ( ( res , rej ) => {
63
+ agent . on ( "error" , rej )
64
+
65
+ agent . on ( "close" , code => {
66
+ if ( code !== 0 ) {
67
+ rej ( {
68
+ message : `coder cloud agent exited with ${ code } ` ,
69
+ } )
70
+ return
71
+ }
72
+ res ( )
73
+ } )
74
+ } )
75
+ }
76
+
77
+ const proxy = async ( ) => {
78
+ try {
79
+ await _proxy ( )
80
+ } catch ( err ) {
81
+ logger . error ( err . message )
82
+ }
83
+ setTimeout ( proxy , 3000 )
84
+ }
85
+ proxy ( )
86
+ }
87
+
88
+ /**
89
+ * waitForPath efficiently implements waiting for the existence of a path.
90
+ *
91
+ * We intentionally do not use fs.watchFile as it is very slow from testing.
92
+ * I believe it polls instead of watching.
93
+ *
94
+ * The way this works is for each level of the path it will check if it exists
95
+ * and if not, it will wait for it. e.g. if the path is /home/nhooyr/.config/coder-cloud/session
96
+ * then first it will check if /home exists, then /home/nhooyr and so on.
97
+ *
98
+ * The wait works by first creating a watch promise for the p segment.
99
+ * We call fs.watch on the dirname of the p segment. When the dirname has a change,
100
+ * we check if the p segment exists and if it does, we resolve the watch promise.
101
+ * On any error or the watcher being closed, we reject the watch promise.
102
+ *
103
+ * Once that promise is setup, we check if the p segment exists with fs.exists
104
+ * and if it does, we close the watcher and return.
105
+ *
106
+ * Now we race the watch promise and a 2000ms delay promise. Once the race
107
+ * is complete, we close the watcher.
108
+ *
109
+ * If the watch promise was the one to resolve, we return.
110
+ * Otherwise we setup the watch promise again and retry.
111
+ *
112
+ * This combination of polling and watching is very reliable and efficient.
113
+ */
114
+ async function waitForPath ( p : string ) : Promise < void > {
115
+ const segs = p . split ( path . sep )
116
+ for ( let i = 0 ; i < segs . length ; i ++ ) {
117
+ const s = path . join ( "/" , ...segs . slice ( 0 , i + 1 ) )
118
+ // We need to wait for each segment to exist.
119
+ await _waitForPath ( s )
120
+ }
121
+ }
122
+
123
+ async function _waitForPath ( p : string ) : Promise < void > {
124
+ const watchDir = path . dirname ( p )
125
+
126
+ logger . debug ( `waiting for ${ p } ` )
127
+
128
+ for ( ; ; ) {
129
+ const w = fs . watch ( watchDir )
130
+ const watchPromise = new Promise < void > ( ( res , rej ) => {
131
+ w . on ( "change" , async ( ) => {
132
+ if ( await promisify ( fs . exists ) ( p ) ) {
133
+ res ( )
134
+ }
135
+ } )
136
+ w . on ( "close" , ( ) => rej ( new Error ( "watcher closed" ) ) )
137
+ w . on ( "error" , rej )
138
+ } )
139
+
140
+ // We want to ignore any errors from this promise being rejected if the file
141
+ // already exists below.
142
+ watchPromise . catch ( ( ) => { } )
143
+
144
+ if ( await promisify ( fs . exists ) ( p ) ) {
145
+ // The path exists!
146
+ w . close ( )
147
+ return
148
+ }
149
+
150
+ // Now we wait for either the watch promise to resolve/reject or 2000ms.
151
+ const s = await Promise . race ( [ watchPromise . then ( ( ) => "exists" ) , delay ( 2000 ) ] )
152
+ w . close ( )
153
+ if ( s === "exists" ) {
154
+ return
155
+ }
156
+ }
157
+ }
0 commit comments