##// END OF EJS Templates
rust-chg: move set_current_dir() to Locator...
Yuya Nishihara -
r45163:0a2516ef default
parent child Browse files
Show More
@@ -1,266 +1,272 b''
1 1 // Copyright 2011, 2018 Yuya Nishihara <yuya@tcha.org>
2 2 //
3 3 // This software may be used and distributed according to the terms of the
4 4 // GNU General Public License version 2 or any later version.
5 5
6 6 //! Utility for locating command-server process.
7 7
8 8 use futures::future::{self, Either, Loop};
9 9 use std::env;
10 10 use std::ffi::{OsStr, OsString};
11 11 use std::fs::{self, DirBuilder};
12 12 use std::io;
13 13 use std::os::unix::ffi::{OsStrExt, OsStringExt};
14 14 use std::os::unix::fs::{DirBuilderExt, MetadataExt};
15 15 use std::path::{Path, PathBuf};
16 16 use std::process::{self, Command};
17 17 use std::time::Duration;
18 18 use tokio::prelude::*;
19 19 use tokio_hglib::UnixClient;
20 20 use tokio_process::{Child, CommandExt};
21 21 use tokio_timer;
22 22
23 use super::clientext::ChgClientExt;
23 24 use super::message::ServerSpec;
24 25 use super::procutil;
25 26
26 27 const REQUIRED_SERVER_CAPABILITIES: &[&str] = &["attachio", "chdir", "runcommand"];
27 28
28 29 /// Helper to connect to and spawn a server process.
29 30 #[derive(Clone, Debug)]
30 31 pub struct Locator {
31 32 hg_command: OsString,
32 33 current_dir: PathBuf,
33 34 env_vars: Vec<(OsString, OsString)>,
34 35 process_id: u32,
35 36 base_sock_path: PathBuf,
36 37 timeout: Duration,
37 38 }
38 39
39 40 impl Locator {
40 41 /// Creates locator capturing the current process environment.
41 42 ///
42 43 /// If no `$CHGSOCKNAME` is specified, the socket directory will be
43 44 /// created as necessary.
44 45 pub fn prepare_from_env() -> io::Result<Locator> {
45 46 Ok(Locator {
46 47 hg_command: default_hg_command(),
47 48 current_dir: env::current_dir()?,
48 49 env_vars: env::vars_os().collect(),
49 50 process_id: process::id(),
50 51 base_sock_path: prepare_server_socket_path()?,
51 52 timeout: default_timeout(),
52 53 })
53 54 }
54 55
55 56 /// Temporary socket path for this client process.
56 57 fn temp_sock_path(&self) -> PathBuf {
57 58 let src = self.base_sock_path.as_os_str().as_bytes();
58 59 let mut buf = Vec::with_capacity(src.len() + 6); // "{src}.{pid}".len()
59 60 buf.extend_from_slice(src);
60 61 buf.extend_from_slice(format!(".{}", self.process_id).as_bytes());
61 62 OsString::from_vec(buf).into()
62 63 }
63 64
64 65 /// Connects to the server.
65 66 ///
66 67 /// The server process will be spawned if not running.
67 68 pub fn connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
68 69 self.try_connect()
69 70 }
70 71
71 72 /// Tries to connect to the existing server, or spawns new if not running.
72 73 fn try_connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
73 74 debug!("try connect to {}", self.base_sock_path.display());
74 75 UnixClient::connect(self.base_sock_path.clone())
75 76 .then(|res| match res {
76 77 Ok(client) => Either::A(future::ok((self, client))),
77 78 Err(_) => Either::B(self.spawn_connect()),
78 79 })
79 80 .and_then(|(loc, client)| {
80 81 check_server_capabilities(client.server_spec())?;
81 82 Ok((loc, client))
82 83 })
84 .and_then(|(loc, client)| {
85 client
86 .set_current_dir(&loc.current_dir)
87 .map(|client| (loc, client))
88 })
83 89 }
84 90
85 91 /// Spawns new server process and connects to it.
86 92 ///
87 93 /// The server will be spawned at the current working directory, then
88 94 /// chdir to "/", so that the server will load configs from the target
89 95 /// repository.
90 96 fn spawn_connect(self) -> impl Future<Item = (Self, UnixClient), Error = io::Error> {
91 97 let sock_path = self.temp_sock_path();
92 98 debug!("start cmdserver at {}", sock_path.display());
93 99 Command::new(&self.hg_command)
94 100 .arg("serve")
95 101 .arg("--cmdserver")
96 102 .arg("chgunix")
97 103 .arg("--address")
98 104 .arg(&sock_path)
99 105 .arg("--daemon-postexec")
100 106 .arg("chdir:/")
101 107 .current_dir(&self.current_dir)
102 108 .env_clear()
103 109 .envs(self.env_vars.iter().cloned())
104 110 .env("CHGINTERNALMARK", "")
105 111 .spawn_async()
106 112 .into_future()
107 113 .and_then(|server| self.connect_spawned(server, sock_path))
108 114 .and_then(|(loc, client, sock_path)| {
109 115 debug!(
110 116 "rename {} to {}",
111 117 sock_path.display(),
112 118 loc.base_sock_path.display()
113 119 );
114 120 fs::rename(&sock_path, &loc.base_sock_path)?;
115 121 Ok((loc, client))
116 122 })
117 123 }
118 124
119 125 /// Tries to connect to the just spawned server repeatedly until timeout
120 126 /// exceeded.
121 127 fn connect_spawned(
122 128 self,
123 129 server: Child,
124 130 sock_path: PathBuf,
125 131 ) -> impl Future<Item = (Self, UnixClient, PathBuf), Error = io::Error> {
126 132 debug!("try connect to {} repeatedly", sock_path.display());
127 133 let connect = future::loop_fn(sock_path, |sock_path| {
128 134 UnixClient::connect(sock_path.clone()).then(|res| {
129 135 match res {
130 136 Ok(client) => Either::A(future::ok(Loop::Break((client, sock_path)))),
131 137 Err(_) => {
132 138 // try again with slight delay
133 139 let fut = tokio_timer::sleep(Duration::from_millis(10))
134 140 .map(|()| Loop::Continue(sock_path))
135 141 .map_err(|err| io::Error::new(io::ErrorKind::Other, err));
136 142 Either::B(fut)
137 143 }
138 144 }
139 145 })
140 146 });
141 147
142 148 // waits for either connection established or server failed to start
143 149 connect
144 150 .select2(server)
145 151 .map_err(|res| res.split().0)
146 152 .timeout(self.timeout)
147 153 .map_err(|err| {
148 154 err.into_inner().unwrap_or_else(|| {
149 155 io::Error::new(
150 156 io::ErrorKind::TimedOut,
151 157 "timed out while connecting to server",
152 158 )
153 159 })
154 160 })
155 161 .and_then(|res| {
156 162 match res {
157 163 Either::A(((client, sock_path), server)) => {
158 164 server.forget(); // continue to run in background
159 165 Ok((self, client, sock_path))
160 166 }
161 167 Either::B((st, _)) => Err(io::Error::new(
162 168 io::ErrorKind::Other,
163 169 format!("server exited too early: {}", st),
164 170 )),
165 171 }
166 172 })
167 173 }
168 174 }
169 175
170 176 /// Determines the server socket to connect to.
171 177 ///
172 178 /// If no `$CHGSOCKNAME` is specified, the socket directory will be created
173 179 /// as necessary.
174 180 fn prepare_server_socket_path() -> io::Result<PathBuf> {
175 181 if let Some(s) = env::var_os("CHGSOCKNAME") {
176 182 Ok(PathBuf::from(s))
177 183 } else {
178 184 let mut path = default_server_socket_dir();
179 185 create_secure_dir(&path)?;
180 186 path.push("server");
181 187 Ok(path)
182 188 }
183 189 }
184 190
185 191 /// Determines the default server socket path as follows.
186 192 ///
187 193 /// 1. `$XDG_RUNTIME_DIR/chg`
188 194 /// 2. `$TMPDIR/chg$UID`
189 195 /// 3. `/tmp/chg$UID`
190 196 pub fn default_server_socket_dir() -> PathBuf {
191 197 // XDG_RUNTIME_DIR should be ignored if it has an insufficient permission.
192 198 // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
193 199 if let Some(Ok(s)) = env::var_os("XDG_RUNTIME_DIR").map(check_secure_dir) {
194 200 let mut path = PathBuf::from(s);
195 201 path.push("chg");
196 202 path
197 203 } else {
198 204 let mut path = env::temp_dir();
199 205 path.push(format!("chg{}", procutil::get_effective_uid()));
200 206 path
201 207 }
202 208 }
203 209
204 210 /// Determines the default hg command.
205 211 pub fn default_hg_command() -> OsString {
206 212 // TODO: maybe allow embedding the path at compile time (or load from hgrc)
207 213 env::var_os("CHGHG")
208 214 .or(env::var_os("HG"))
209 215 .unwrap_or(OsStr::new("hg").to_owned())
210 216 }
211 217
212 218 fn default_timeout() -> Duration {
213 219 let secs = env::var("CHGTIMEOUT")
214 220 .ok()
215 221 .and_then(|s| s.parse().ok())
216 222 .unwrap_or(60);
217 223 Duration::from_secs(secs)
218 224 }
219 225
220 226 /// Creates a directory which the other users cannot access to.
221 227 ///
222 228 /// If the directory already exists, tests its permission.
223 229 fn create_secure_dir<P>(path: P) -> io::Result<()>
224 230 where
225 231 P: AsRef<Path>,
226 232 {
227 233 DirBuilder::new()
228 234 .mode(0o700)
229 235 .create(path.as_ref())
230 236 .or_else(|err| {
231 237 if err.kind() == io::ErrorKind::AlreadyExists {
232 238 check_secure_dir(path).map(|_| ())
233 239 } else {
234 240 Err(err)
235 241 }
236 242 })
237 243 }
238 244
239 245 fn check_secure_dir<P>(path: P) -> io::Result<P>
240 246 where
241 247 P: AsRef<Path>,
242 248 {
243 249 let a = fs::symlink_metadata(path.as_ref())?;
244 250 if a.is_dir() && a.uid() == procutil::get_effective_uid() && (a.mode() & 0o777) == 0o700 {
245 251 Ok(path)
246 252 } else {
247 253 Err(io::Error::new(io::ErrorKind::Other, "insecure directory"))
248 254 }
249 255 }
250 256
251 257 fn check_server_capabilities(spec: &ServerSpec) -> io::Result<()> {
252 258 let unsupported: Vec<_> = REQUIRED_SERVER_CAPABILITIES
253 259 .iter()
254 260 .cloned()
255 261 .filter(|&s| !spec.capabilities.contains(s))
256 262 .collect();
257 263 if unsupported.is_empty() {
258 264 Ok(())
259 265 } else {
260 266 let msg = format!(
261 267 "insufficient server capabilities: {}",
262 268 unsupported.join(", ")
263 269 );
264 270 Err(io::Error::new(io::ErrorKind::Other, msg))
265 271 }
266 272 }
@@ -1,102 +1,100 b''
1 1 // Copyright 2018 Yuya Nishihara <yuya@tcha.org>
2 2 //
3 3 // This software may be used and distributed according to the terms of the
4 4 // GNU General Public License version 2 or any later version.
5 5
6 6 extern crate chg;
7 7 extern crate futures;
8 8 extern crate log;
9 9 extern crate tokio;
10 10 extern crate tokio_hglib;
11 11
12 12 use chg::locator::Locator;
13 13 use chg::procutil;
14 14 use chg::{ChgClientExt, ChgUiHandler};
15 15 use futures::sync::oneshot;
16 16 use std::env;
17 17 use std::io;
18 18 use std::process;
19 19 use std::time::Instant;
20 20 use tokio::prelude::*;
21 21
22 22 struct DebugLogger {
23 23 start: Instant,
24 24 }
25 25
26 26 impl DebugLogger {
27 27 pub fn new() -> DebugLogger {
28 28 DebugLogger {
29 29 start: Instant::now(),
30 30 }
31 31 }
32 32 }
33 33
34 34 impl log::Log for DebugLogger {
35 35 fn enabled(&self, metadata: &log::Metadata) -> bool {
36 36 metadata.target().starts_with("chg::")
37 37 }
38 38
39 39 fn log(&self, record: &log::Record) {
40 40 if self.enabled(record.metadata()) {
41 41 // just make the output looks similar to chg of C
42 42 let l = format!("{}", record.level()).to_lowercase();
43 43 let t = self.start.elapsed();
44 44 writeln!(
45 45 io::stderr(),
46 46 "chg: {}: {}.{:06} {}",
47 47 l,
48 48 t.as_secs(),
49 49 t.subsec_micros(),
50 50 record.args()
51 51 )
52 52 .unwrap_or(());
53 53 }
54 54 }
55 55
56 56 fn flush(&self) {}
57 57 }
58 58
59 59 fn main() {
60 60 if env::var_os("CHGDEBUG").is_some() {
61 61 log::set_boxed_logger(Box::new(DebugLogger::new()))
62 62 .expect("any logger should not be installed yet");
63 63 log::set_max_level(log::LevelFilter::Debug);
64 64 }
65 65
66 66 // TODO: add loop detection by $CHGINTERNALMARK
67 67
68 68 let code = run().unwrap_or_else(|err| {
69 69 writeln!(io::stderr(), "chg: abort: {}", err).unwrap_or(());
70 70 255
71 71 });
72 72 process::exit(code);
73 73 }
74 74
75 75 fn run() -> io::Result<i32> {
76 let current_dir = env::current_dir()?;
77 76 let loc = Locator::prepare_from_env()?;
78 77 let handler = ChgUiHandler::new();
79 78 let (result_tx, result_rx) = oneshot::channel();
80 79 let fut = loc
81 80 .connect()
82 .and_then(|(_, client)| client.set_current_dir(current_dir))
83 .and_then(|client| client.attach_io(io::stdin(), io::stdout(), io::stderr()))
81 .and_then(|(_, client)| client.attach_io(io::stdin(), io::stdout(), io::stderr()))
84 82 .and_then(|client| {
85 83 let pid = client.server_spec().process_id.unwrap();
86 84 let pgid = client.server_spec().process_group_id;
87 85 procutil::setup_signal_handler_once(pid, pgid)?;
88 86 Ok(client)
89 87 })
90 88 .and_then(|client| client.run_command_chg(handler, env::args_os().skip(1)))
91 89 .map(|(_client, _handler, code)| {
92 90 procutil::restore_signal_handler_once()?;
93 91 Ok(code)
94 92 })
95 93 .or_else(|err| Ok(Err(err))) // pass back error to caller
96 94 .map(|res| result_tx.send(res).unwrap());
97 95 tokio::run(fut);
98 96 result_rx.wait().unwrap_or(Err(io::Error::new(
99 97 io::ErrorKind::Other,
100 98 "no exit code set",
101 99 )))
102 100 }
General Comments 0
You need to be logged in to leave comments. Login now