Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/cli/src/commands/agent_host.rs
13383 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
use std::convert::Infallible;
7
use std::fs;
8
use std::io::{Read, Write};
9
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
10
use std::path::{Path, PathBuf};
11
use std::sync::Arc;
12
use std::time::Instant;
13
14
use ::http::{Request, Response};
15
use hyper::body::Incoming;
16
use hyper::service::service_fn;
17
use hyper_util::rt::{TokioExecutor, TokioIo};
18
use hyper_util::server::conn::auto::Builder as ServerBuilder;
19
use serde::{Deserialize, Serialize};
20
use tokio::net::TcpListener;
21
22
use crate::auth::Auth;
23
use crate::constants::{self, AGENT_HOST_PORT};
24
use crate::log;
25
use crate::tunnels::agent_host::{handle_request, AgentHostConfig, AgentHostManager};
26
use crate::tunnels::dev_tunnels::DevTunnels;
27
use crate::tunnels::shutdown_signal::ShutdownRequest;
28
use crate::update_service::Platform;
29
use crate::util::errors::{AnyError, CodeError};
30
use crate::util::http::{full_body, HyperBody, ReqwestSimpleHttp};
31
use crate::util::prereqs::PreReqChecker;
32
33
use super::args::AgentHostArgs;
34
use super::output;
35
use super::tunnels::fulfill_existing_tunnel_args;
36
use super::CommandContext;
37
38
/// Bookkeeping data written to the agent host lockfile so that other CLI
39
/// commands (e.g. `code agent ps`) can discover a running agent host.
40
#[derive(Debug, Clone, Serialize, Deserialize)]
41
pub struct AgentHostLockData {
42
/// WebSocket address the agent host is listening on (e.g. `ws://127.0.0.1:4567/`).
43
pub address: String,
44
/// PID of the CLI process running the agent host.
45
pub pid: u32,
46
/// Connection token, if any.
47
#[serde(skip_serializing_if = "Option::is_none")]
48
pub connection_token: Option<String>,
49
/// Tunnel name, if `--tunnel` was used.
50
#[serde(skip_serializing_if = "Option::is_none")]
51
pub tunnel_name: Option<String>,
52
}
53
54
/// Runs a local agent host server. Downloads the latest VS Code server on
55
/// demand, starts it with `--enable-remote-auto-shutdown`, and proxies
56
/// WebSocket connections from a local TCP port to the server's agent host
57
/// socket. The server auto-shuts down when idle; the CLI checks for updates
58
/// in the background and starts the latest version on the next connection.
59
pub async fn agent_host(ctx: CommandContext, mut args: AgentHostArgs) -> Result<i32, AnyError> {
60
let started = Instant::now();
61
62
let platform: Platform = PreReqChecker::new().verify().await?;
63
64
if !args.without_connection_token {
65
if let Some(p) = args.connection_token_file.as_deref() {
66
let token = fs::read_to_string(PathBuf::from(p))
67
.map_err(CodeError::CouldNotReadConnectionTokenFile)?;
68
args.connection_token = Some(token.trim().to_string());
69
} else {
70
let token_path = ctx.paths.root().join("agent-host-token");
71
let token = mint_connection_token(&token_path, args.connection_token.clone())
72
.map_err(CodeError::CouldNotCreateConnectionTokenFile)?;
73
args.connection_token = Some(token);
74
args.connection_token_file = Some(token_path.to_string_lossy().to_string());
75
}
76
}
77
78
let manager = AgentHostManager::new(
79
ctx.log.clone(),
80
platform,
81
ctx.paths.server_cache.clone(),
82
Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())),
83
AgentHostConfig {
84
server_data_dir: args.server_data_dir.clone(),
85
// The CLI proxy enforces the connection token itself, so the
86
// underlying server always runs without one. This lets tunnel
87
// connections (which bypass the proxy token check) reach the
88
// server without needing a token at all.
89
without_connection_token: true,
90
connection_token: None,
91
connection_token_file: None,
92
},
93
);
94
95
// Eagerly resolve the latest version so the first connection is fast.
96
// Skip when using a dev override since updates don't apply.
97
if option_env!("VSCODE_CLI_OVERRIDE_SERVER_PATH").is_none() {
98
match manager.get_latest_release().await {
99
Ok(release) => {
100
if let Err(e) = manager.ensure_downloaded(&release).await {
101
warning!(ctx.log, "Error downloading latest server version: {}", e);
102
}
103
}
104
Err(e) => warning!(ctx.log, "Error resolving initial server version: {}", e),
105
}
106
107
// Start background update checker
108
let manager_for_updates = manager.clone();
109
tokio::spawn(async move {
110
manager_for_updates.run_update_loop().await;
111
});
112
}
113
114
// Bind the HTTP/WebSocket proxy
115
let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]);
116
117
let addr: SocketAddr = match &args.host {
118
Some(h) => SocketAddr::new(h.parse().map_err(CodeError::InvalidHostAddress)?, args.port),
119
None => SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), args.port),
120
};
121
let listener = TcpListener::bind(addr)
122
.await
123
.map_err(CodeError::CouldNotListenOnInterface)?;
124
let bound_addr = listener
125
.local_addr()
126
.map_err(CodeError::CouldNotListenOnInterface)?;
127
128
let local_agent_host_url = format!("ws://{bound_addr}/");
129
130
let product = constants::QUALITYLESS_PRODUCT_NAME;
131
let token_suffix = args
132
.connection_token
133
.as_deref()
134
.map(|t| format!("?tkn={t}"))
135
.unwrap_or_default();
136
137
// If --tunnel is set, create a dev tunnel and serve connections directly.
138
let mut _tunnel_handle: Option<crate::tunnels::dev_tunnels::ActiveTunnel> = None;
139
let mut tunnel_name: Option<String> = None;
140
if args.tunnel {
141
let mut auth = Auth::new(&ctx.paths, ctx.log.clone());
142
auth.set_provider(crate::auth::AuthProvider::Github);
143
let mut dt = DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths);
144
145
let mut tunnel = if let Some(existing) =
146
fulfill_existing_tunnel_args(args.existing_tunnel.clone(), &args.name)
147
{
148
dt.start_existing_tunnel(existing).await
149
} else {
150
dt.start_new_launcher_tunnel(args.name.as_deref(), args.random_name, &[])
151
.await
152
}?;
153
154
// Receive tunnel connections directly (no TCP forwarding) and serve
155
// them without connection-token enforcement — the tunnel relay
156
// provides its own authentication.
157
let mut tunnel_port = tunnel.add_port_direct(AGENT_HOST_PORT).await?;
158
let mgr_for_tunnel = manager.clone();
159
let tunnel_log = ctx.log.clone();
160
tokio::spawn(async move {
161
while let Some(socket) = tunnel_port.recv().await {
162
let mgr = mgr_for_tunnel.clone();
163
let log = tunnel_log.clone();
164
tokio::spawn(async move {
165
debug!(log, "Serving tunnel agent host connection");
166
let rw = socket.into_rw();
167
let svc = service_fn(move |req| {
168
let mgr = mgr.clone();
169
async move { handle_request(mgr, req).await }
170
});
171
let io = TokioIo::new(rw);
172
if let Err(e) = ServerBuilder::new(TokioExecutor::new())
173
.serve_connection_with_upgrades(io, svc)
174
.await
175
{
176
debug!(log, "Tunnel agent host connection ended: {:?}", e);
177
}
178
});
179
}
180
});
181
182
tunnel_name = Some(tunnel.name.clone());
183
_tunnel_handle = Some(tunnel);
184
}
185
186
output::print_banner_header(&format!("{product} Agent Host"), started.elapsed());
187
if let (Some(base), Some(name)) = (constants::EDITOR_WEB_URL, &tunnel_name) {
188
output::print_banner_line("Tunnel", &format!("{base}/agents/tunnel/{name}"));
189
}
190
output::print_network_lines(bound_addr.port(), addr.ip(), &token_suffix);
191
output::print_banner_footer();
192
193
// Write lockfile so `code agent ps` can discover this instance.
194
let lockfile_path = ctx.paths.agent_host_lockfile();
195
let lock_data = AgentHostLockData {
196
address: local_agent_host_url,
197
pid: std::process::id(),
198
connection_token: args.connection_token.clone(),
199
tunnel_name: tunnel_name.clone(),
200
};
201
if let Err(e) = write_agent_host_lockfile(&lockfile_path, &lock_data) {
202
warning!(ctx.log, "Failed to write agent host lockfile: {}", e);
203
}
204
205
let manager_for_svc = manager.clone();
206
let expected_token = args.connection_token.clone();
207
208
// Accept loop: for each incoming TCP connection, serve it with hyper.
209
let accept_result: Result<(), AnyError> = loop {
210
tokio::select! {
211
_ = shutdown.wait() => break Ok(()),
212
accepted = listener.accept() => {
213
let (stream, _) = match accepted {
214
Ok(v) => v,
215
Err(e) => {
216
warning!(ctx.log, "Failed to accept connection: {}", e);
217
continue;
218
}
219
};
220
let mgr = manager_for_svc.clone();
221
let token = expected_token.clone();
222
tokio::spawn(async move {
223
let io = TokioIo::new(stream);
224
let svc = service_fn(move |req| {
225
let mgr = mgr.clone();
226
let token = token.clone();
227
async move { handle_request_with_auth(mgr, req, token).await }
228
});
229
if let Err(e) = ServerBuilder::new(TokioExecutor::new())
230
.serve_connection_with_upgrades(io, svc)
231
.await
232
{
233
// Connection-level errors are normal (client disconnect, etc.)
234
let _ = e;
235
}
236
});
237
}
238
}
239
};
240
241
manager.kill_running_server().await;
242
243
// Close the tunnel if one was created.
244
if let Some(mut tunnel) = _tunnel_handle.take() {
245
tunnel.close().await.ok();
246
}
247
248
// Clean up the lockfile.
249
let _ = fs::remove_file(&lockfile_path);
250
251
accept_result?;
252
253
Ok(0)
254
}
255
256
/// Wraps [`handle_request`] with connection-token enforcement.
257
///
258
/// When `expected_token` is `Some`, the proxy requires `?tkn=<token>` on
259
/// the request URI. This only applies to the local TCP listener; tunnel
260
/// connections are served directly via `add_port_direct` and bypass this
261
/// function entirely.
262
async fn handle_request_with_auth(
263
manager: Arc<AgentHostManager>,
264
req: Request<Incoming>,
265
expected_token: Option<String>,
266
) -> Result<Response<HyperBody>, Infallible> {
267
if let Some(ref token) = expected_token {
268
let uri_query = req.uri().query().unwrap_or("");
269
let has_valid_token = url::form_urlencoded::parse(uri_query.as_bytes())
270
.any(|(k, v)| k == "tkn" && v == token.as_str());
271
272
if !has_valid_token {
273
return Ok(Response::builder()
274
.status(403)
275
.body(full_body("Forbidden: missing or invalid connection token"))
276
.unwrap());
277
}
278
}
279
280
handle_request(manager, req).await
281
}
282
283
fn mint_connection_token(path: &Path, prefer_token: Option<String>) -> std::io::Result<String> {
284
#[cfg(not(windows))]
285
use std::os::unix::fs::OpenOptionsExt;
286
287
let mut file_options = fs::OpenOptions::new();
288
file_options.create(true);
289
file_options.write(true);
290
file_options.read(true);
291
#[cfg(not(windows))]
292
file_options.mode(0o600);
293
let mut file = file_options.open(path)?;
294
295
if prefer_token.is_none() {
296
let mut token = String::new();
297
file.read_to_string(&mut token)?;
298
let token = token.trim();
299
if !token.is_empty() {
300
return Ok(token.to_string());
301
}
302
}
303
304
file.set_len(0)?;
305
let prefer_token = prefer_token.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
306
file.write_all(prefer_token.as_bytes())?;
307
Ok(prefer_token)
308
}
309
310
fn write_agent_host_lockfile(path: &Path, lock_data: &AgentHostLockData) -> std::io::Result<()> {
311
#[cfg(not(windows))]
312
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
313
314
let mut file_options = fs::OpenOptions::new();
315
file_options.create(true);
316
file_options.write(true);
317
file_options.truncate(true);
318
#[cfg(not(windows))]
319
file_options.mode(0o600);
320
let mut file = file_options.open(path)?;
321
#[cfg(not(windows))]
322
file.set_permissions(fs::Permissions::from_mode(0o600))?;
323
file.write_all(serde_json::to_string(lock_data).unwrap().as_bytes())
324
}
325
326
#[cfg(test)]
327
mod tests {
328
use super::*;
329
use std::fs;
330
331
#[test]
332
fn mint_connection_token_generates_and_persists() {
333
let dir = tempfile::tempdir().unwrap();
334
let path = dir.path().join("token");
335
336
// First call with no preference generates a UUID and persists it
337
let token1 = mint_connection_token(&path, None).unwrap();
338
assert!(!token1.is_empty());
339
assert_eq!(fs::read_to_string(&path).unwrap(), token1);
340
341
// Second call with no preference reads the existing token
342
let token2 = mint_connection_token(&path, None).unwrap();
343
assert_eq!(token1, token2);
344
}
345
346
#[test]
347
fn mint_connection_token_respects_preferred() {
348
let dir = tempfile::tempdir().unwrap();
349
let path = dir.path().join("token");
350
351
// Providing a preferred token writes it to the file
352
let token = mint_connection_token(&path, Some("my-token".to_string())).unwrap();
353
assert_eq!(token, "my-token");
354
assert_eq!(fs::read_to_string(&path).unwrap(), "my-token");
355
}
356
357
#[test]
358
fn mint_connection_token_preferred_overwrites_existing() {
359
let dir = tempfile::tempdir().unwrap();
360
let path = dir.path().join("token");
361
362
mint_connection_token(&path, None).unwrap();
363
364
// Providing a preference overwrites any existing token
365
let token = mint_connection_token(&path, Some("override".to_string())).unwrap();
366
assert_eq!(token, "override");
367
assert_eq!(fs::read_to_string(&path).unwrap(), "override");
368
}
369
}
370
371