map_gui/tools/
command.rs

1use std::collections::VecDeque;
2use std::time::Duration;
3
4use instant::Instant;
5use subprocess::{Communicator, Popen};
6
7use widgetry::tools::PopupMsg;
8use widgetry::{Color, EventCtx, GfxCtx, Line, Panel, State, Text, Transition, UpdateType};
9
10use crate::AppLike;
11
12/// Executes a command and displays STDOUT and STDERR in a loading screen window. Only works on
13/// native, of course.
14pub struct RunCommand<A: AppLike> {
15    p: Popen,
16    // Only wrapped in an Option so we can modify it when we're almost done.
17    comm: Option<Communicator>,
18    panel: Panel,
19    lines: VecDeque<String>,
20    max_capacity: usize,
21    started: Instant,
22    last_drawn: Instant,
23    show_success_popup: bool,
24    // Wrapped in an Option just to make calling from event() work. The bool is success, and the
25    // strings are the last lines of output.
26    on_load: Option<Box<dyn FnOnce(&mut EventCtx, &mut A, bool, Vec<String>) -> Transition<A>>>,
27}
28
29impl<A: AppLike + 'static> RunCommand<A> {
30    pub fn new_state(
31        ctx: &mut EventCtx,
32        show_success_popup: bool,
33        args: Vec<String>,
34        on_load: Box<dyn FnOnce(&mut EventCtx, &mut A, bool, Vec<String>) -> Transition<A>>,
35    ) -> Box<dyn State<A>> {
36        info!("RunCommand: {}", args.join(" "));
37        match subprocess::Popen::create(
38            &args,
39            subprocess::PopenConfig {
40                stdout: subprocess::Redirection::Pipe,
41                stderr: subprocess::Redirection::Merge,
42                ..Default::default()
43            },
44        ) {
45            Ok(mut p) => {
46                let comm = Some(
47                    p.communicate_start(None)
48                        .limit_time(Duration::from_millis(0)),
49                );
50                let panel = ctx.make_loading_screen(Text::from("Starting command..."));
51                let max_capacity =
52                    (0.8 * ctx.canvas.window_height / ctx.default_line_height()) as usize;
53                Box::new(RunCommand {
54                    p,
55                    comm,
56                    panel,
57                    lines: VecDeque::new(),
58                    max_capacity,
59                    started: Instant::now(),
60                    last_drawn: Instant::now(),
61                    show_success_popup,
62                    on_load: Some(on_load),
63                })
64            }
65            Err(err) => PopupMsg::new_state(
66                ctx,
67                "Error",
68                vec![format!("Couldn't start command: {}", err)],
69            ),
70        }
71    }
72
73    fn read_output(&mut self) {
74        let mut new_lines = Vec::new();
75        let (stdout, stderr) = match self.comm.as_mut().unwrap().read() {
76            Ok(pair) => pair,
77            // This is almost always a timeout.
78            Err(err) => err.capture,
79        };
80        assert!(stderr.is_none());
81        if let Some(bytes) = stdout {
82            if let Ok(string) = String::from_utf8(bytes) {
83                if !string.is_empty() {
84                    for line in string.split('\n') {
85                        new_lines.push(line.to_string());
86                    }
87                }
88            }
89        }
90        for line in new_lines {
91            if self.lines.len() == self.max_capacity {
92                self.lines.pop_front();
93            }
94            if line.contains('\r') {
95                // \r shows up in two cases:
96                // 1) As output from docker
97                // 2) As the "clear the current line" escape code
98                // TODO Assuming always 2 parts...
99                let parts = line.split('\r').collect::<Vec<_>>();
100                if parts[0].is_empty() {
101                    self.lines.pop_back();
102                    self.lines.push_back(parts[1].to_string());
103                } else {
104                    println!("> {}", parts[0]);
105                    self.lines.push_back(parts[0].to_string());
106                }
107            } else {
108                println!("> {}", line);
109                self.lines.push_back(line);
110            }
111        }
112    }
113}
114
115impl<A: AppLike + 'static> State<A> for RunCommand<A> {
116    fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Transition<A> {
117        ctx.request_update(UpdateType::Game);
118        if ctx.input.nonblocking_is_update_event().is_none() {
119            return Transition::Keep;
120        }
121
122        self.read_output();
123
124        // Throttle rerendering
125        if abstutil::elapsed_seconds(self.last_drawn) > 0.1 {
126            let mut txt = Text::from(
127                Line(format!(
128                    "Running command... {} so far",
129                    geom::Duration::realtime_elapsed(self.started)
130                ))
131                .small_heading(),
132            );
133            for line in &self.lines {
134                // Previously, map importing produced some very long lines, which slowed down
135                // rendering a bunch. The origin of the long output was fixed, but still skip very
136                // long lines generally -- this is just a loading screen showing command output, no
137                // need to display everything perfectly.
138                if line.len() < 300 {
139                    txt.add_line(line);
140                }
141            }
142            self.panel = ctx.make_loading_screen(txt);
143            self.last_drawn = Instant::now();
144        }
145
146        if let Some(status) = self.p.poll() {
147            // Make sure to grab all remaining output.
148            let comm = self.comm.take().unwrap();
149            self.comm = Some(comm.limit_time(Duration::from_secs(10)));
150            self.read_output();
151            // TODO Possible hack -- why is this last line empty?
152            if self.lines.back().map(|x| x.is_empty()).unwrap_or(false) {
153                self.lines.pop_back();
154            }
155
156            let success = status.success();
157            let mut lines: Vec<String> = self.lines.drain(..).collect();
158            if !success {
159                lines.push(format!("Command failed: {:?}", status));
160            }
161            let mut transitions = vec![
162                Transition::Pop,
163                (self.on_load.take().unwrap())(ctx, app, success, lines.clone()),
164            ];
165            if !success || self.show_success_popup {
166                transitions.push(Transition::Push(PopupMsg::new_state(
167                    ctx,
168                    if success { "Success!" } else { "Failure!" },
169                    lines,
170                )));
171            }
172            return Transition::Multi(transitions);
173        }
174
175        Transition::Keep
176    }
177
178    fn draw(&self, g: &mut GfxCtx, _: &A) {
179        g.clear(Color::BLACK);
180        self.panel.draw(g);
181    }
182}