artifact_app/api/
mod.rs

1use dev_prefix::*;
2
3use std::time::Duration;
4use std::mem;
5use std::sync::Mutex;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::thread;
8
9use nickel::{HttpRouter, MediaType, MiddlewareResult, Nickel, Request, Response,
10             StaticFilesHandler};
11use nickel::status::StatusCode;
12use serde_json;
13
14use tar::Archive;
15use tempdir::TempDir;
16
17use types::{Project, ServeCmd};
18use export::ProjectData;
19use ctrlc;
20
21mod constants;
22pub mod utils;
23mod handler;
24mod crud;
25
26#[cfg(test)]
27mod tests;
28
29const WEB_FRONTEND_TAR: &'static [u8] = include_bytes!("data/web-ui.tar");
30const REPLACE_FLAGS: &str = "{/* REPLACE WITH FLAGS */}";
31
32pub struct LockedData {
33    pub cmd: ServeCmd,
34    pub project: Project,
35    pub project_data: ProjectData,
36}
37
38lazy_static! {
39    static ref LOCKED: Mutex<LockedData> = Mutex::new(
40        LockedData {
41            cmd: ServeCmd::default(),
42            project_data: ProjectData::default(),
43            project: Project::default(),
44        }
45    );
46}
47
48fn setup_headers(res: &mut Response) {
49    let head = res.headers_mut();
50    let bv = |s: &str| Vec::from(s.as_bytes());
51    head.set_raw("Access-Control-Allow-Origin", vec![bv("*")]);
52    head.set_raw(
53        "Access-Control-Allow-Methods",
54        vec![bv("GET, POST, OPTIONS, PUT, PATCH, DELETE")],
55    );
56    head.set_raw(
57        "Access-Control-Allow-Headers",
58        vec![bv("X-Requested-With,content-type")],
59    );
60}
61
62fn config_json_res(res: &mut Response) {
63    res.set(MediaType::Json);
64    res.set(StatusCode::Ok);
65}
66
67fn handle_artifacts<'a>(req: &mut Request, mut res: Response<'a>) -> MiddlewareResult<'a> {
68    setup_headers(&mut res);
69    debug!("handling json-rpc request");
70
71    let mut body = vec![];
72    req.origin.read_to_end(&mut body).unwrap();
73    let body = match str::from_utf8(&body) {
74        Ok(b) => b,
75        Err(e) => {
76            res.set(StatusCode::BadRequest);
77            return res.send(format!("invalid utf8: {:?}", e));
78        }
79    };
80
81    debug!("body: {:?}", body);
82    match handler::RPC_HANDLER.handle_request_sync(body) {
83        Some(body) => {
84            trace!("- response {}", body);
85            config_json_res(&mut res);
86            res.send(body)
87        }
88        None => {
89            let msg = "InternalServerError: Got None from json-rpc handler";
90            error!("{}", msg);
91            res.set(StatusCode::InternalServerError);
92            res.send(msg)
93        }
94    }
95}
96
97fn handle_options<'a>(_: &mut Request, mut res: Response<'a>) -> MiddlewareResult<'a> {
98    setup_headers(&mut res);
99    res.set(StatusCode::Ok);
100    res.send("ok")
101}
102
103/// host the frontend web-server, returning the tempdir where it the
104/// static files are being held. It is important that this tempdir
105/// always be owned, ortherwise the files will be deleted!
106fn host_frontend(server: &mut Nickel, cmd: &ServeCmd) -> TempDir {
107    // it is important that tmp_dir never goes out of scope
108    // or the webapp will be deleted!
109    let tmp_dir = TempDir::new("artifact-web-ui").expect("unable to create temporary directory");
110    let dir = tmp_dir.path().to_path_buf(); // we have to clone this because *borrow*
111    info!("Unpacking web-ui at: {}", dir.display());
112
113    let mut archive = Archive::new(WEB_FRONTEND_TAR);
114    archive.unpack(&dir).expect("Unable to unpack web frontend");
115
116    // replace the default ip address with the real one
117    let app_js_path = dir.join("app.js");
118    let mut app_js = fs::OpenOptions::new()
119        .read(true)
120        .write(true)
121        .open(app_js_path)
122        .expect("couldn't open app.js");
123    let mut text = String::new();
124    app_js
125        .read_to_string(&mut text)
126        .expect("app.js couldn't be read");
127    app_js.seek(SeekFrom::Start(0)).unwrap();
128    app_js.set_len(0).unwrap(); // delete what is there
129    // the elm app uses a certain address by default, replace it
130
131    assert!(text.contains(REPLACE_FLAGS));
132    app_js
133        .write_all(
134            text.replace(REPLACE_FLAGS, &serde_json::to_string(cmd).unwrap())
135                .as_bytes(),
136        )
137        .unwrap();
138    app_js.flush().unwrap();
139
140    server.utilize(StaticFilesHandler::new(&dir));
141    println!("Hosting web ui at {}", cmd.addr);
142    tmp_dir
143}
144
145/// start the json-rpc API server
146#[allow(unused_variables)] // need to hold ownership of tmp_dir
147pub fn start_api(project: Project, cmd: &ServeCmd) {
148    // store artifacts and files into global mutex
149
150    {
151        let mut locked = LOCKED.lock().unwrap();
152        let lref = locked.deref_mut();
153        lref.project_data = project.to_data();
154        lref.project = project;
155        lref.cmd = cmd.clone();
156    }
157
158    let endpoint = "/json-rpc";
159    let mut server = Box::new(Nickel::new());
160
161    server.get(endpoint, handle_artifacts);
162    server.put(endpoint, handle_artifacts);
163    server.options(endpoint, handle_options);
164
165    let running = Arc::new(AtomicBool::new(true));
166    let r = running.clone();
167    ctrlc::set_handler(move || { r.store(false, Ordering::SeqCst); })
168        .expect("Error setting Ctrl-C handler");
169
170    // host the frontend files using a static file handler
171    // and own the tmpdir for as long as needed
172    let tmp_dir = host_frontend(&mut server, cmd);
173
174    // everything in a thread has to be owned by the thread
175    let addr = cmd.addr.clone();
176    let th = thread::spawn(move || {
177        server.listen(&addr).expect("cannot connect to port");
178    });
179
180    println!("exit with ctrlc+C or SIGINT");
181    while running.load(Ordering::SeqCst) {
182        thread::sleep(Duration::new(0, 10 * 1e6 as u32));
183    }
184
185    debug!("Got SIGINT, cleaning up");
186    let locked = LOCKED.lock().unwrap();
187    mem::forget(locked); // never unlock again
188    debug!("All cleaned up, exiting");
189}