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
use wsb::prelude::*;
use axum::{extract::State, http::StatusCode, Json};
use crate::web::state::WebState;
/// The version of the workspace this route was reached through.
///
/// From the state rather than from the process's directory: one daemon serves
/// every workspace on the machine, and its own directory belongs to none of
/// them.
pub async fn get_version(
State(state): State<WebState>,
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
let workspace_root = state.workspace_root.clone();
let workspace_version =
wsb::state::read_version_from_path(&workspace_root).map_err(internal_error)?;
Ok(Json(serde_json::json!({
"version": wsb::get_version(),
// The machine the workbench is being served FROM, which is not always
// the machine it is being read on: the daemon can serve a remote
// browser, and a page that looked identical on two hosts is one an
// operator can act on while looking at the wrong workspace.
"host": machine_hostname(),
"backend_build_id": env!("WSB_BACKEND_BUILD_ID"),
"frontend_build_id": env!("WSB_FRONTEND_BUILD_ID"),
"read_only": false,
"workspace_root": workspace_root.display().to_string(),
"workspace_version": workspace_version.full_version
})))
}
/// This machine's name, as the operating system gives it.
///
/// `None` when it cannot be read. It is a label rather than a fact anything
/// acts on, so the page shows what it has and omits what it does not —
/// inventing "localhost" would be a wrong answer dressed as a right one.
#[cfg(unix)]
fn machine_hostname() -> Option<String> {
let mut buffer = vec![0u8; 256];
// SAFETY: the pointer and length describe `buffer`, which is live for the
// whole call, and gethostname writes at most `len` bytes into it.
let answered = unsafe { libc::gethostname(buffer.as_mut_ptr().cast(), buffer.len()) };
if answered != 0 {
return None;
}
// POSIX does not promise a terminator when the name was truncated, so the
// whole buffer is the bound rather than a NUL that may not be there.
let end = buffer.iter().position(|byte| *byte == 0).unwrap_or(buffer.len());
buffer.truncate(end);
let name = String::from_utf8(buffer).ok()?;
let name = name.trim().to_string();
(!name.is_empty()).then_some(name)
}
/// Windows publishes it in the environment, set by the system rather than by
/// a shell — so unlike unix's `HOSTNAME`, it is there to be read.
#[cfg(windows)]
fn machine_hostname() -> Option<String> {
let name = std::env::var("COMPUTERNAME").ok()?;
let name = name.trim().to_string();
(!name.is_empty()).then_some(name)
}
fn internal_error(error: anyhow::Error) -> (StatusCode, Json<serde_json::Value>) {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": error.to_string()
})),
)
}