mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-17 20:44:27 +01:00
* tunnels: host the agent host with the code-tunnel CLI Replaces the TypeScript dev-tunnels SDK hosting path with the `code-tunnel` binary, and gives the shared process a single owner for the tunnel process. The editor no longer creates, adopts, or reconciles dev tunnels itself: the CLI owns naming, reuse, and lifetime, and the editor supplies only intent. - Adds `--agent-host-only` to `code tunnel`, which serves the agent-host port without the control port, so remote session sharing does not also grant full remote editor access. - Adds `--delegate-to-editor`, which pins the selection gateway to the live editor agent host and stops it from starting a dedicated agent host. A dedicated host behind an editor-bound tunnel outlives the tunnel and cannot be reached. Clients that do not send `delegatedInstanceId`, which includes older editors and every background reconnect, get the bound host instead of an error. - Adds `--user-data-dir` to `code tunnel`. The gateway read the platform default registry, so it could not see the editor agent host in portable, custom, or development installations. - Adds a machine-readable status stream, enabled with `VSCODE_CLI_MACHINE_STATUS`, and removes the matching of human-readable output. The editor matched a string the CLI no longer prints, so Remote Tunnel Access never became connected. - Makes registry liveness require a reachable endpoint, not only a running process ID. Operating systems reuse process IDs, so a dead entry could look alive and be selected in preference to the live one. - Adds `TunnelProcessCoordinator`, which owns the single tunnel process, the tunnel name, and the CLI login. Both services previously started their own process with the same name, which made the CLI fall back to a random name, and both logged in to the same credential store. - Stops the editor from connecting to the tunnel that it hosts. - Raises the Windows stack size for development builds only. The default 1MB main thread stack overflows before `code tunnel` finishes starting. Fixes https://github.com/microsoft/vscode/issues/319297 Fixes https://github.com/microsoft/vscode/issues/329985 (Commit message generated by Copilot) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * cli: fix clippy lints and a registry read race on Windows CI runs `cargo clippy -- -D warnings` without `--all-targets`, so lib-only warnings fail the build. Fix the four it reported: - use `?` instead of a match in `get_tunnel_web_url` - drop a redundant rebinding of `delegate_to_editor` - group `serve()`'s agent-host parameters into `AgentHostServeOptions` - box both `GatewayTargetWs` variants (boxing only the larger one just inverts the imbalance) Separately, `read_registry` failed intermittently on Windows with `PermissionDenied`. A file removed by a concurrent prune stays listed in the directory until its last handle closes, and opening it in that window fails with `PermissionDenied` rather than the `NotFound` the code already handled. The error propagated out of `read_entry_file` and aborted the whole read, so one unreadable entry hid every other endpoint. Per-entry read and directory-enumeration failures are now logged and skipped instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * tunnels: address review feedback on CLI-hosted tunnels Four fixes from PR review: - Agent host sharing hard-rejected every non-GitHub request, but `remote.tunnels.access.enableMicrosoftAuth` still exposes Microsoft accounts and the renderer prefers them when enabled. Carry `authProvider` through to `tunnel user login` instead of hard-coding GitHub. - The pending service uninstall lived in one queued generation, so a concurrent sharing update could preempt the reconcile that owed it and leave the tunnel service installed. Persist it on the coordinator until an uninstall succeeds. - `getTunnelName()` is also called while access is inactive, to compare the name this machine would use against a previously used one. Returning the running tunnel's name yielded undefined and permanently skipped the remote-extension recommendation. Expose the coordinator's intended name. - Machine-status events were written straight to the emitting process's stdout, so when the editor attached to an existing tunnel the singleton server's token errors never reached it and token expiry was never surfaced. Events are now always generated, relayed to attached clients over a new singleton notification, and printed only where a process-global stdout toggle is set. Also converts a runtime protocol-version assertion added by this branch into a const assertion, which `clippy --all-targets` rejects as an assertion on a constant. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
263 lines
7.8 KiB
Rust
263 lines
7.8 KiB
Rust
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
const FILE_HEADER: &str = "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/";
|
|
|
|
use std::{
|
|
collections::HashMap,
|
|
env, fs, io,
|
|
path::{Path, PathBuf},
|
|
process::{self},
|
|
str::FromStr,
|
|
};
|
|
|
|
use serde::{de::DeserializeOwned, Deserialize};
|
|
use serde_json::Value;
|
|
|
|
fn main() {
|
|
let files = enumerate_source_files().expect("expected to enumerate files");
|
|
ensure_file_headers(&files).expect("expected to ensure file headers");
|
|
apply_build_environment_variables();
|
|
apply_win32_version_resources();
|
|
apply_debug_stack_size();
|
|
}
|
|
|
|
/// Windows gives the main thread a 1MB stack by default, where Linux and macOS
|
|
/// give 8MB. `#[tokio::main]` runs the whole async runtime on that thread, and
|
|
/// unoptimized builds do not collapse nested async state machines, so a debug
|
|
/// build of `code tunnel` can overflow it before it finishes starting up.
|
|
///
|
|
/// Raise it to match the Unix default, for debug builds only: release builds
|
|
/// optimize those futures down and should not have their link flags changed
|
|
/// without separate scrutiny.
|
|
fn apply_debug_stack_size() {
|
|
let is_windows = env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows");
|
|
let is_debug = env::var("PROFILE").as_deref() == Ok("debug");
|
|
if is_windows && is_debug {
|
|
println!("cargo:rustc-link-arg-bins=/STACK:8388608");
|
|
}
|
|
}
|
|
|
|
fn camel_case_to_constant_case(key: &str) -> String {
|
|
let mut output = String::new();
|
|
let mut prev_upper = false;
|
|
for c in key.chars() {
|
|
if c.is_uppercase() {
|
|
if prev_upper {
|
|
output.push(c.to_ascii_lowercase());
|
|
} else {
|
|
output.push('_');
|
|
output.push(c.to_ascii_uppercase());
|
|
}
|
|
prev_upper = true;
|
|
} else if c.is_lowercase() {
|
|
output.push(c.to_ascii_uppercase());
|
|
prev_upper = false;
|
|
} else {
|
|
output.push(c);
|
|
prev_upper = false;
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
fn set_env_vars_from_map_keys(prefix: &str, map: impl IntoIterator<Item = (String, Value)>) {
|
|
let mut win32_app_ids = vec![];
|
|
|
|
for (key, value) in map {
|
|
//#region special handling
|
|
let value = match key.as_str() {
|
|
"tunnelServerQualities" | "serverLicense" => {
|
|
Value::String(serde_json::to_string(&value).unwrap())
|
|
}
|
|
"nameLong" => {
|
|
if let Value::String(s) = &value {
|
|
let idx = s.find(" - ");
|
|
println!(
|
|
"cargo:rustc-env=VSCODE_CLI_QUALITYLESS_PRODUCT_NAME={}",
|
|
idx.map(|i| &s[..i]).unwrap_or(s)
|
|
);
|
|
}
|
|
|
|
value
|
|
}
|
|
"tunnelApplicationConfig" => {
|
|
if let Value::Object(v) = value {
|
|
set_env_vars_from_map_keys(&format!("{}_{}", prefix, "TUNNEL"), v);
|
|
}
|
|
continue;
|
|
}
|
|
_ => value,
|
|
};
|
|
if key.contains("win32") && key.contains("AppId") {
|
|
if let Value::String(s) = value {
|
|
win32_app_ids.push(s);
|
|
continue;
|
|
}
|
|
}
|
|
//#endregion
|
|
|
|
if let Value::String(s) = value {
|
|
println!(
|
|
"cargo:rustc-env={}_{}={}",
|
|
prefix,
|
|
camel_case_to_constant_case(&key),
|
|
s
|
|
);
|
|
}
|
|
}
|
|
|
|
if !win32_app_ids.is_empty() {
|
|
println!(
|
|
"cargo:rustc-env=VSCODE_CLI_WIN32_APP_IDS={}",
|
|
win32_app_ids.join(",")
|
|
);
|
|
}
|
|
}
|
|
|
|
fn read_json_from_path<T>(path: &Path) -> T
|
|
where
|
|
T: DeserializeOwned,
|
|
{
|
|
let mut file = fs::File::open(path).expect("failed to open file");
|
|
serde_json::from_reader(&mut file).expect("failed to deserialize JSON")
|
|
}
|
|
|
|
fn apply_build_from_product_json(path: &Path) {
|
|
let json: HashMap<String, Value> = read_json_from_path(path);
|
|
set_env_vars_from_map_keys("VSCODE_CLI", json);
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct PackageJson {
|
|
pub version: String,
|
|
}
|
|
|
|
fn apply_build_environment_variables() {
|
|
let repo_dir = env::current_dir().unwrap().join("..");
|
|
let package_json = read_json_from_path::<PackageJson>(&repo_dir.join("package.json"));
|
|
println!(
|
|
"cargo:rustc-env=VSCODE_CLI_VERSION={}",
|
|
package_json.version
|
|
);
|
|
|
|
match env::var("VSCODE_CLI_PRODUCT_JSON") {
|
|
Ok(v) => {
|
|
let path = if cfg!(windows) {
|
|
PathBuf::from_str(&v.replace('/', "\\")).unwrap()
|
|
} else {
|
|
PathBuf::from_str(&v).unwrap()
|
|
};
|
|
println!("cargo:warning=loading product.json from <{path:?}>");
|
|
apply_build_from_product_json(&path);
|
|
}
|
|
|
|
Err(_) => {
|
|
apply_build_from_product_json(&repo_dir.join("product.json"));
|
|
|
|
let overrides = repo_dir.join("product.overrides.json");
|
|
if overrides.exists() {
|
|
apply_build_from_product_json(&overrides);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
fn apply_win32_version_resources() {
|
|
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
|
|
return;
|
|
}
|
|
|
|
let repo_dir = env::current_dir().unwrap().join("..");
|
|
let package_json = read_json_from_path::<PackageJson>(&repo_dir.join("package.json"));
|
|
|
|
let product_json_path = match env::var("VSCODE_CLI_PRODUCT_JSON") {
|
|
Ok(v) => {
|
|
if cfg!(windows) {
|
|
PathBuf::from_str(&v.replace('/', "\\")).unwrap()
|
|
} else {
|
|
PathBuf::from_str(&v).unwrap()
|
|
}
|
|
}
|
|
Err(_) => repo_dir.join("product.json"),
|
|
};
|
|
|
|
let product: HashMap<String, Value> = read_json_from_path(&product_json_path);
|
|
let name_long = product
|
|
.get("nameLong")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("Code - OSS");
|
|
let application_name = product
|
|
.get("applicationName")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("code");
|
|
let exe_name = format!("{application_name}.exe");
|
|
|
|
let base_version = package_json.version.split('-').next().unwrap_or("0.0.0");
|
|
let version_parts: Vec<&str> = base_version.split('.').collect();
|
|
let major: u64 = version_parts.first().and_then(|v| v.parse().ok()).unwrap_or(0);
|
|
let minor: u64 = version_parts.get(1).and_then(|v| v.parse().ok()).unwrap_or(0);
|
|
let patch: u64 = version_parts.get(2).and_then(|v| v.parse().ok()).unwrap_or(0);
|
|
|
|
let mut res = winresource::WindowsResource::new();
|
|
res.set("ProductName", name_long);
|
|
res.set("FileDescription", name_long);
|
|
res.set("CompanyName", "Microsoft Corporation");
|
|
res.set("LegalCopyright", "Copyright (C) 2026 Microsoft. All rights reserved");
|
|
res.set("FileVersion", &package_json.version);
|
|
res.set("ProductVersion", &package_json.version);
|
|
res.set("InternalName", &exe_name);
|
|
res.set("OriginalFilename", &exe_name);
|
|
res.set_version_info(winresource::VersionInfo::FILEVERSION, (major << 48) | (minor << 16) | patch);
|
|
res.set_version_info(winresource::VersionInfo::PRODUCTVERSION, (major << 48) | (minor << 16) | patch);
|
|
res.compile().expect("failed to compile Windows resources");
|
|
}
|
|
|
|
fn ensure_file_headers(files: &[PathBuf]) -> Result<(), io::Error> {
|
|
let mut ok = true;
|
|
|
|
let crlf_header_str = str::replace(FILE_HEADER, "\n", "\r\n");
|
|
let crlf_header = crlf_header_str.as_bytes();
|
|
let lf_header = FILE_HEADER.as_bytes();
|
|
for file in files {
|
|
let contents = fs::read(file)?;
|
|
|
|
if !(contents.starts_with(lf_header) || contents.starts_with(crlf_header)) {
|
|
eprintln!("File missing copyright header: {}", file.display());
|
|
ok = false;
|
|
}
|
|
}
|
|
|
|
if !ok {
|
|
process::exit(1);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Gets all "rs" files in the source directory
|
|
fn enumerate_source_files() -> Result<Vec<PathBuf>, io::Error> {
|
|
let mut files = vec![];
|
|
let mut queue = vec![];
|
|
|
|
let current_dir = env::current_dir()?.join("src");
|
|
queue.push(current_dir);
|
|
|
|
while !queue.is_empty() {
|
|
for entry in fs::read_dir(queue.pop().unwrap())? {
|
|
let entry = entry?;
|
|
let ftype = entry.file_type()?;
|
|
if ftype.is_dir() {
|
|
queue.push(entry.path());
|
|
} else if ftype.is_file() && entry.file_name().to_string_lossy().ends_with(".rs") {
|
|
files.push(entry.path());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(files)
|
|
}
|