unc_workspaces/network/
server.rs1use 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
21const DEFAULT_RPC_HOST: &str = "127.0.0.1";
23
24fn rpc_socket(port: u16) -> String {
25 format!("{DEFAULT_RPC_HOST}:{}", port)
26}
27
28pub async fn pick_unused_port() -> Result<u16> {
30 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
44async 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 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 #[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_sandbox_logs_if_required();
120
121 let home_dir = init_home_dir_with_version(version).await?.into_path();
122 crate::network::config::set_sandbox_configs(&home_dir)?;
125
126 let (rpc_port, rpc_port_lock) = acquire_unused_port().await?;
128 let (net_port, net_port_lock) = acquire_unused_port().await?;
129 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 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
222fn 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 std::env::set_var("UNC_SANDBOX_LOG", "unc=error,stats=error,network=error");
237}