unc_workspaces/network/
server.rs

1use std::fs::File;
2use std::net::{Ipv4Addr, SocketAddrV4};
3use std::path::PathBuf;
4
5use crate::error::{ErrorKind, SandboxErrorCode};
6use crate::result::Result;
7use crate::types::SecretKey;
8
9use fs2::FileExt;
10
11use unc_account_id::AccountId;
12use reqwest::Url;
13use tempfile::TempDir;
14use tokio::process::Child;
15
16use tracing::info;
17
18use unc_sandbox_utils as sandbox;
19use tokio::net::TcpListener;
20
21// Must be an IP address as `uncd` expects socket address for network address.
22const DEFAULT_RPC_HOST: &str = "127.0.0.1";
23
24fn rpc_socket(port: u16) -> String {
25    format!("{DEFAULT_RPC_HOST}:{}", port)
26}
27
28/// Request an unused port from the OS.
29pub async fn pick_unused_port() -> Result<u16> {
30    // Port 0 means the OS gives us an unused port
31    // Important to use localhost as using 0.0.0.0 leads to users getting brief firewall popups to
32    // allow inbound connections on MacOS.
33    let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
34    let listener = TcpListener::bind(addr)
35        .await
36        .map_err(|err| ErrorKind::Io.full("failed to bind to random port", err))?;
37    let port = listener
38        .local_addr()
39        .map_err(|err| ErrorKind::Io.full("failed to get local address for random port", err))?
40        .port();
41    Ok(port)
42}
43
44/// Acquire an unused port and lock it for the duration until the sandbox server has
45/// been started.
46async fn acquire_unused_port() -> Result<(u16, File)> {
47    loop {
48        let port = pick_unused_port().await?;
49        let lockpath = std::env::temp_dir().join(format!("unc-sandbox-port{}.lock", port));
50        let lockfile = File::create(lockpath).map_err(|err| {
51            ErrorKind::Io.full(format!("failed to create lockfile for port {}", port), err)
52        })?;
53        if lockfile.try_lock_exclusive().is_ok() {
54            break Ok((port, lockfile));
55        }
56    }
57}
58
59#[allow(dead_code)]
60async fn init_home_dir() -> Result<TempDir> {
61    init_home_dir_with_version(sandbox::DEFAULT_UNC_SANDBOX_VERSION).await
62}
63
64async fn init_home_dir_with_version(version: &str) -> Result<TempDir> {
65    let home_dir = tempfile::tempdir().map_err(|e| ErrorKind::Io.custom(e))?;
66
67    let output = sandbox::init_with_version(&home_dir, version)
68        .map_err(|e| SandboxErrorCode::InitFailure.custom(e))?
69        .wait_with_output()
70        .await
71        .map_err(|e| SandboxErrorCode::InitFailure.custom(e))?;
72
73    info!(target: "workspaces", "sandbox init: {:?}", output);
74
75    Ok(home_dir)
76}
77
78#[derive(Debug)]
79#[non_exhaustive]
80pub enum ValidatorKey {
81    HomeDir(PathBuf),
82    Known(AccountId, SecretKey),
83}
84
85pub struct SandboxServer {
86    pub(crate) validator_key: ValidatorKey,
87    rpc_addr: Url,
88    net_port: Option<u16>,
89    rpc_port_lock: Option<File>,
90    net_port_lock: Option<File>,
91    process: Option<Child>,
92}
93
94impl SandboxServer {
95    /// Connect a sandbox server that's already been running, provided we know the rpc_addr
96    /// and home_dir pointing to the sandbox process.
97    pub(crate) async fn connect(rpc_addr: String, validator_key: ValidatorKey) -> Result<Self> {
98        let rpc_addr = Url::parse(&rpc_addr).map_err(|e| {
99            SandboxErrorCode::InitFailure.full(format!("Invalid rpc_url={rpc_addr}"), e)
100        })?;
101        Ok(Self {
102            validator_key,
103            rpc_addr,
104            net_port: None,
105            rpc_port_lock: None,
106            net_port_lock: None,
107            process: None,
108        })
109    }
110
111    /// Run a new SandboxServer, spawning the sandbox node in the process.
112    #[allow(dead_code)]
113    pub(crate) async fn run_new() -> Result<Self> {
114        Self::run_new_with_version(sandbox::DEFAULT_UNC_SANDBOX_VERSION).await
115    }
116
117    pub(crate) async fn run_new_with_version(version: &str) -> Result<Self> {
118        // Suppress logs for the sandbox binary by default:
119        suppress_sandbox_logs_if_required();
120
121        let home_dir = init_home_dir_with_version(version).await?.into_path();
122        // Configure `$home_dir/config.json` to our liking. Sandbox requires extra settings
123        // for the best user experience, and being able to offer patching large state payloads.
124        crate::network::config::set_sandbox_configs(&home_dir)?;
125
126        // Try running the server with the follow provided rpc_ports and net_ports
127        let (rpc_port, rpc_port_lock) = acquire_unused_port().await?;
128        let (net_port, net_port_lock) = acquire_unused_port().await?;
129        // It's important that the address doesn't have a scheme, since the sandbox expects
130        // a valid socket address.
131        let rpc_addr = rpc_socket(rpc_port);
132        let net_addr = rpc_socket(net_port);
133
134        info!(target: "workspaces", "Starting up sandbox at localhost:{}", rpc_port);
135
136        let options = &[
137            "--home",
138            home_dir
139                .as_os_str()
140                .to_str()
141                .expect("home_dir is valid utf8"),
142            "run",
143            "--rpc-addr",
144            &rpc_addr,
145            "--network-addr",
146            &net_addr,
147        ];
148
149        let child = sandbox::run_with_options_with_version(options, version)
150            .map_err(|e| SandboxErrorCode::RunFailure.custom(e))?;
151
152        info!(target: "workspaces", "Started up sandbox at localhost:{} with pid={:?}", rpc_port, child.id());
153
154        let rpc_addr: Url = format!("http://{rpc_addr}")
155            .parse()
156            .expect("static scheme and host name with variable u16 port numbers form valid urls");
157
158        Ok(Self {
159            validator_key: ValidatorKey::HomeDir(home_dir),
160            rpc_addr,
161            net_port: Some(net_port),
162            rpc_port_lock: Some(rpc_port_lock),
163            net_port_lock: Some(net_port_lock),
164            process: Some(child),
165        })
166    }
167
168    /// Unlock port lockfiles that were used to avoid port contention when starting up
169    /// the sandbox node.
170    pub(crate) fn unlock_lockfiles(&mut self) -> Result<()> {
171        if let Some(rpc_port_lock) = self.rpc_port_lock.take() {
172            rpc_port_lock.unlock().map_err(|e| {
173                ErrorKind::Io.full(
174                    format!(
175                        "failed to unlock lockfile for rpc_port={:?}",
176                        self.rpc_port()
177                    ),
178                    e,
179                )
180            })?;
181        }
182        if let Some(net_port_lock) = self.net_port_lock.take() {
183            net_port_lock.unlock().map_err(|e| {
184                ErrorKind::Io.full(
185                    format!("failed to unlock lockfile for net_port={:?}", self.net_port),
186                    e,
187                )
188            })?;
189        }
190
191        Ok(())
192    }
193
194    pub fn rpc_port(&self) -> Option<u16> {
195        self.rpc_addr.port()
196    }
197
198    pub fn net_port(&self) -> Option<u16> {
199        self.net_port
200    }
201
202    pub fn rpc_addr(&self) -> String {
203        self.rpc_addr.to_string()
204    }
205}
206
207impl Drop for SandboxServer {
208    fn drop(&mut self) {
209        if let Some(mut child) = self.process.take() {
210            info!(
211                target: "workspaces",
212                "Cleaning up sandbox: pid={:?}",
213                child.id()
214            );
215
216            child.start_kill().expect("failed to kill sandbox");
217            let _ = child.try_wait();
218        }
219    }
220}
221
222/// Turn off uncd-sandbox logs by default. Users can turn them back on with
223/// UNC_ENABLE_SANDBOX_LOG=1 and specify further parameters with the custom
224/// UNC_SANDBOX_LOG for higher levels of specificity. UNC_SANDBOX_LOG args
225/// will be forward into RUST_LOG environment variable as to not conflict
226/// with similar named log targets.
227fn suppress_sandbox_logs_if_required() {
228    if let Ok(val) = std::env::var("UNC_ENABLE_SANDBOX_LOG") {
229        if val != "0" {
230            return;
231        }
232    }
233
234    // non-exhaustive list of targets to suppress, since choosing a default LogLevel
235    // does nothing in this case, since unccore seems to be overriding it somehow:
236    std::env::set_var("UNC_SANDBOX_LOG", "unc=error,stats=error,network=error");
237}