makepad_studio/build_manager/
build_manager.rs

1
2use {
3    crate::{
4        file_system::file_system::FileSystem,
5        makepad_micro_serde::*,
6        makepad_platform::*,
7        makepad_widgets::*,
8        makepad_platform::makepad_live_compiler::LiveFileChange,
9        makepad_platform::os::cx_stdin::{
10            HostToStdin,
11            StdinToHost,
12        },
13        build_manager::{
14            run_view::*,
15            build_protocol::*,
16            build_client::BuildClient
17        },
18        makepad_shell::*,
19    },
20    makepad_code_editor::{text::Position,decoration::{Decoration}},
21    makepad_http::server::*,
22    std::{
23        collections::HashMap,
24        env,
25        io::prelude::*,
26        fs::File,
27    },
28    std::sync::mpsc,
29    std::thread,
30    std::time,
31    std::net::{UdpSocket,SocketAddr},
32    std::time::{Instant, Duration},
33};
34
35live_design!{
36    import makepad_draw::shader::std::*;
37    import makepad_widgets::base::*;
38    import makepad_widgets::theme_desktop_dark::*;
39    
40    BuildManager = {{BuildManager}} {
41        recompile_timeout: 0.2
42    }
43}
44pub const MAX_SWAPCHAIN_HISTORY:usize = 4;
45pub struct ActiveBuild {
46    pub log_index: String,
47    pub item_id: LiveId,
48    pub process: BuildProcess,
49    pub run_view_id: LiveId,
50    pub cmd_id: Option<BuildCmdId>,
51
52    pub swapchain: Option<cx_stdin::Swapchain<Texture>>,
53
54    /// Some previous value of `swapchain`, which holds the image still being
55    /// the most recent to have been presented after a successful client draw,
56    /// and needs to be kept around to avoid deallocating the backing texture.
57    ///
58    /// While not strictly necessary, it can also accept *new* draws to any of
59    /// its images, which allows the client to catch up a frame or two, visually.
60    pub last_swapchain_with_completed_draws: Option<cx_stdin::Swapchain<Texture>>,
61
62    pub aux_chan_host_endpoint: Option<cx_stdin::aux_chan::HostEndpoint>,
63}
64
65#[derive(Clone, Debug, Default, Eq, Hash, Copy, PartialEq, FromLiveId)]
66pub struct ActiveBuildId(pub LiveId);
67
68#[derive(Default)]
69pub struct ActiveBuilds {
70    pub builds: HashMap<ActiveBuildId, ActiveBuild>
71}
72
73impl ActiveBuilds {
74    pub fn item_id_active(&self, item_id:LiveId)->bool{
75        for (_k, v) in &self.builds {
76            if v.item_id == item_id{
77                return true
78            }
79        }
80        false
81    }
82    
83    pub fn any_binary_active(&self, binary:&str)->bool{
84        for (_k, v) in &self.builds {
85            if v.process.binary == binary{
86                return true
87            }
88        }
89        false
90    }
91    
92    pub fn build_id_from_cmd_id(&self, cmd_id: BuildCmdId) -> Option<ActiveBuildId> {
93        for (k, v) in &self.builds {
94            if v.cmd_id == Some(cmd_id) {
95                return Some(*k);
96            }
97        }
98        None
99    }
100    
101    pub fn build_id_from_run_view_id(&self, run_view_id: LiveId) -> Option<ActiveBuildId> {
102        for (k, v) in &self.builds {
103            if v.run_view_id == run_view_id {
104                return Some(*k);
105            }
106        }
107        None
108    }
109    
110    
111    pub fn run_view_id_from_cmd_id(&self, cmd_id: BuildCmdId) -> Option<LiveId> {
112        for v in self.builds.values() {
113            if v.cmd_id == Some(cmd_id) {
114                return Some(v.run_view_id);
115            }
116        }
117        None
118    }
119    
120    pub fn cmd_id_from_run_view_id(&self, run_view_id: LiveId) -> Option<BuildCmdId> {
121        for v in self.builds.values() {
122            if v.run_view_id == run_view_id {
123                return v.cmd_id
124            }
125        }
126        None
127    }
128    
129}
130
131#[derive(Live, LiveHook)]
132pub struct BuildManager {
133    #[live] path: String,
134    #[live(8001usize)] http_port: usize,
135    #[rust] pub clients: Vec<BuildClient>,
136    #[rust] pub log: Vec<(ActiveBuildId, LogItem)>,
137    #[live] recompile_timeout: f64,
138    #[rust] recompile_timer: Timer,
139    #[rust] pub binaries: Vec<BuildBinary>,
140    #[rust] pub active: ActiveBuilds,
141    #[rust] pub studio_http: String,
142    #[rust] pub recv_external_ip: ToUIReceiver<SocketAddr>,
143    #[rust] pub send_file_change: FromUISender<LiveFileChange>
144}
145
146pub struct BuildBinary {
147    pub open: f64,
148    pub name: String
149}
150
151
152pub enum BuildManagerAction {
153    RedrawDoc, // {doc_id: DocumentId},
154    StdinToHost {run_view_id: LiveId, msg: StdinToHost},
155    RedrawLog,
156    ClearLog,
157    None
158}
159
160impl BuildManager {
161    
162    pub fn init(&mut self, cx: &mut Cx) {
163        // not great but it will do.
164        self.clients = vec![BuildClient::new_with_local_server(&self.path)];
165        self.update_run_list(cx);
166        self.recompile_timer = cx.start_timeout(self.recompile_timeout);
167        self.discover_external_ip(cx);
168        // alright lets start our http server
169        self.start_http_server();
170    }
171    
172    
173    /*pub fn get_process_texture(&self, run_view_id: LiveId) -> Option<Texture> {
174        for v in self.active.builds.values() {
175            if v.run_view_id == run_view_id {
176                return Some(v.texture.clone())
177            }
178        }
179        None
180    }*/
181    
182    pub fn send_host_to_stdin(&self, run_view_id: LiveId, msg: HostToStdin) {
183        if let Some(cmd_id) = self.active.cmd_id_from_run_view_id(run_view_id) {
184            self.clients[0].send_cmd_with_id(cmd_id, BuildCmd::HostToStdin(msg.to_json()));
185        }
186    }
187    
188    pub fn update_run_list(&mut self, _cx: &mut Cx) {
189        let cwd = std::env::current_dir().unwrap();
190        self.binaries.clear();
191        match shell_env_cap(&[], &cwd, "cargo", &["run", "--bin"]) {
192            Ok(_) => {}
193            // we expect it on stderr
194            Err(e) => {
195                let mut after_av = false;
196                for line in e.split("\n") {
197                    if after_av {
198                        let binary = line.trim().to_string();
199                        if binary.len()>0 {
200                            self.binaries.push(BuildBinary {
201                                open: 0.0,
202                                name: binary
203                            });
204                        }
205                    }
206                    if line.contains("Available binaries:") {
207                        after_av = true;
208                    }
209                }
210            }
211        }
212    }
213    
214    pub fn handle_tab_close(&mut self, tab_id:LiveId)->bool{
215        let len = self.active.builds.len();
216        self.active.builds.retain(|_,v|{
217            if v.run_view_id == tab_id{
218                if let Some(cmd_id) = v.cmd_id {
219                    self.clients[0].send_cmd_with_id(cmd_id, BuildCmd::Stop);
220                }
221                return false
222            }
223            true
224        });
225        if len != self.active.builds.len(){
226            self.log.clear();
227            true
228        }
229        else{
230            false
231        }
232    }
233    
234    pub fn start_recompile(&mut self, _cx: &mut Cx) {
235        // alright so. a file was changed. now what.
236        for active_build in self.active.builds.values_mut() {
237            if let Some(cmd_id) = active_build.cmd_id {
238                self.clients[0].send_cmd_with_id(cmd_id, BuildCmd::Stop);
239            }
240            let cmd_id = self.clients[0].send_cmd(BuildCmd::Run(active_build.process.clone(), self.studio_http.clone()));
241            active_build.cmd_id = Some(cmd_id);
242            active_build.swapchain = None;
243            active_build.last_swapchain_with_completed_draws = None;
244            active_build.aux_chan_host_endpoint = None;
245        }
246    }
247    
248    pub fn clear_active_builds(&mut self) {
249        // alright so. a file was changed. now what.
250        for active_build in self.active.builds.values_mut() {
251            if let Some(cmd_id) = active_build.cmd_id {
252                self.clients[0].send_cmd_with_id(cmd_id, BuildCmd::Stop);
253            }
254        }
255        self.active.builds.clear();
256    }
257    
258    pub fn clear_log(&mut self, cx:&mut Cx, dock:&DockRef, file_system:&mut FileSystem) {
259        // lets clear all log related decorations
260        file_system.clear_all_decorations();
261        file_system.redraw_all_views(cx, dock);
262        self.log.clear();
263    }
264    
265    pub fn start_recompile_timer(&mut self, cx: &mut Cx, ui: &WidgetRef) {
266        cx.stop_timer(self.recompile_timer);
267        self.recompile_timer = cx.start_timeout(self.recompile_timeout);
268        for active_build in self.active.builds.values_mut() {
269            let view = ui.run_view(&[active_build.run_view_id]);
270            view.recompile_started(cx);
271        }
272    }
273    
274    pub fn handle_event(&mut self, cx: &mut Cx, event: &Event, file_system:&mut FileSystem, dock:&DockRef) -> Vec<BuildManagerAction> {
275        let mut actions = Vec::new();
276        self.handle_event_with(cx, event, file_system, dock, &mut | _, action | actions.push(action));
277        actions
278    }
279    
280    pub fn live_reload_needed(&mut self, live_file_change:LiveFileChange){
281        // lets send this filechange to all our stdin stuff
282        for active_build in self.active.builds.values_mut() {
283            if let Some(cmd_id) = active_build.cmd_id{
284                self.clients[0].send_cmd_with_id(cmd_id, BuildCmd::HostToStdin(HostToStdin::ReloadFile{
285                    file: live_file_change.file_name.clone(),
286                    contents: live_file_change.content.clone()
287                }.to_json()));
288            }
289        }
290        let _ = self.send_file_change.send(live_file_change);
291    }
292    
293    pub fn handle_event_with(&mut self, cx: &mut Cx, event: &Event, file_system:&mut FileSystem, dock:&DockRef, dispatch_event: &mut dyn FnMut(&mut Cx, BuildManagerAction)) {
294        if let Event::Signal = event{
295            if let Ok(mut addr) = self.recv_external_ip.try_recv(){
296                addr.set_port(self.http_port as u16);
297                self.studio_http = format!("{}",addr);
298            }
299        }
300        
301        if self.recompile_timer.is_event(event).is_some() {
302            self.start_recompile(cx);
303            self.clear_log(cx, &dock,file_system);
304            
305            if let Some(mut dock) = dock.borrow_mut(){
306                for (_id, (_, item)) in dock.items().iter(){
307                    if let Some(mut run_view) = item.as_run_view().borrow_mut(){
308                        run_view.resend_framebuffer(cx);
309                    }
310                }
311            }
312            dispatch_event(cx, BuildManagerAction::RedrawLog)
313        }
314        
315        // process events on all run_views
316        if let Some(mut dock) = dock.borrow_mut(){
317            for (id, (_, item)) in dock.items().iter(){
318                if let Some(mut run_view) = item.as_run_view().borrow_mut(){
319                    run_view.pump_event_loop(cx, event, *id, self);
320                }
321            }
322        }
323        
324        let log = &mut self.log;
325        let active = &mut self.active;
326        //let editor_state = &mut state.editor_state;
327        self.clients[0].handle_event_with(cx, event, &mut | cx, wrap | {
328            //let msg_id = editor_state.messages.len();
329            // ok we have a cmd_id in wrap.msg
330            match wrap.item {
331                LogItem::Location(loc) => {
332                    let pos = Position{
333                        line_index: loc.start.line_index.max(1) - 1,
334                        byte_index: loc.start.byte_index.max(1) - 1
335                    };
336                    if let Some(file_id) = file_system.path_to_file_node_id(&loc.file_name){
337                        file_system.add_decoration(file_id, Decoration::new(
338                            0,pos ,pos + loc.length
339                        ));
340                        file_system.redraw_view_by_file_id(cx, file_id, dock);
341                    }
342                    if let Some(id) = active.build_id_from_cmd_id(wrap.cmd_id) {
343                        log.push((id, LogItem::Location(loc)));
344                        dispatch_event(cx, BuildManagerAction::RedrawLog)
345                    }
346                    //if let Some(doc) = file_system.open_documents.get(&path){
347                        
348                    //}
349                    /*if let Some(doc_id) = editor_state.documents_by_path.get(UnixPath::new(&loc.file_name)) {
350                        let doc = &mut editor_state.documents[*doc_id];
351                        if let Some(inner) = &mut doc.inner {
352                            inner.msg_cache.add_range(&inner.text, msg_id, loc.range);
353                        }
354                        dispatch_event(cx, BuildManagerAction::RedrawDoc {
355                            doc_id: *doc_id
356                        })
357                    }*/
358                    //editor_state.messages.push(BuildMsg::Location(loc));
359                }
360                LogItem::Bare(bare) => {
361                    if let Some(id) = active.build_id_from_cmd_id(wrap.cmd_id) {
362                        log.push((id, LogItem::Bare(bare)));
363                        dispatch_event(cx, BuildManagerAction::RedrawLog)
364                    }
365                    //editor_state.messages.push(wrap.msg);
366                }
367                LogItem::StdinToHost(line) => {
368                    let msg: Result<StdinToHost, DeJsonErr> = DeJson::deserialize_json(&line);
369                    match msg {
370                        Ok(msg) => {
371                            dispatch_event(cx, BuildManagerAction::StdinToHost {
372                                run_view_id: active.run_view_id_from_cmd_id(wrap.cmd_id).unwrap_or(LiveId(0)),
373                                msg
374                            });
375                        }
376                        Err(_) => { // we should output a log string
377                            if let Some(id) = active.build_id_from_cmd_id(wrap.cmd_id) {
378                                log.push((id, LogItem::Bare(LogItemBare {
379                                    level: LogItemLevel::Log,
380                                    line: line.trim().to_string()
381                                })));
382                                dispatch_event(cx, BuildManagerAction::RedrawLog)
383                            }
384                            /*editor_state.messages.push(BuildMsg::Bare(BuildMsgBare {
385                                level: BuildMsgLevel::Log,
386                                line
387                            }));*/
388                        }
389                    }
390                }
391                LogItem::AuxChanHostEndpointCreated(aux_chan_host_endpoint) => {
392                    for active_build in active.builds.values_mut() {
393                        if active_build.cmd_id == Some(wrap.cmd_id) {
394                            //assert!(active_build.aux_chan_host_endpoint.is_none());
395                            active_build.aux_chan_host_endpoint = Some(aux_chan_host_endpoint);
396                            break;
397                        }
398                    }
399                }
400            }
401        });
402    }
403    
404    pub fn start_http_server(&mut self){
405        let addr = SocketAddr::new("0.0.0.0".parse().unwrap(),self.http_port as u16);
406        let (tx_request, rx_request) = mpsc::channel::<HttpServerRequest> ();
407        
408        log!("Http server at http://127.0.0.1:{}/ for wasm examples and mobile", self.http_port);
409        start_http_server(HttpServer{
410            listen_address:addr,
411            post_max_size: 1024*1024,
412            request: tx_request
413        });
414        
415        let rx_file_change = self.send_file_change.receiver();
416        let (tx_live_file, rx_live_file) = mpsc::channel::<HttpServerRequest> ();
417        
418        // livecoding observer
419        std::thread::spawn(move || {
420            loop{
421                let mut last_change = None;
422                let mut addrs = Vec::new();
423                if let Ok(change) = rx_file_change.recv_timeout(Duration::from_millis(5000)){
424                    last_change = Some(change);
425                    addrs.clear();
426                }
427                while let Ok(change) = rx_file_change.try_recv(){
428                    last_change = Some(change);
429                    addrs.clear();
430                }
431                while let Ok(HttpServerRequest::Get{headers, response_sender}) = rx_live_file.try_recv(){
432                    let body = if addrs.contains(&headers.addr){
433                        vec![]
434                    }
435                    else if let Some(last_change) = &last_change{
436                        addrs.push(headers.addr);
437                        format!("{}$$$makepad_live_change$$${}",last_change.file_name, last_change.content).as_bytes().to_vec()
438                    }
439                    else{
440                        vec![]
441                    };
442                    let header = format!(
443                        "HTTP/1.1 200 OK\r\n\
444                        Content-Type: application/json\r\n\
445                        Content-encoding: none\r\n\
446                        Cache-Control: max-age:0\r\n\
447                        Content-Length: {}\r\n\
448                        Connection: close\r\n\r\n",
449                        body.len()
450                    );
451                    let _ = response_sender.send(HttpServerResponse{header, body});
452                }
453            }
454        });
455        
456        std::thread::spawn(move || {
457
458            // TODO fix this proper:
459            let makepad_path = "./".to_string();
460            let abs_makepad_path = std::env::current_dir().unwrap().join(makepad_path.clone()).canonicalize().unwrap().to_str().unwrap().to_string();
461            let remaps = [
462                (format!("/makepad/{}/",abs_makepad_path),makepad_path.clone()),
463                (format!("/makepad/{}/",std::env::current_dir().unwrap().display()),"".to_string()),
464                ("/makepad//".to_string(),makepad_path.clone()),
465                ("/makepad/".to_string(),makepad_path.clone()),
466                ("/".to_string(),"".to_string())
467            ];
468            
469            while let Ok(message) = rx_request.recv() {
470                // only store last change, fix later
471                match message{
472                    HttpServerRequest::ConnectWebSocket {web_socket_id:_, response_sender:_, headers:_}=>{
473                    },
474                    HttpServerRequest::DisconnectWebSocket {web_socket_id:_}=>{
475                    },
476                    HttpServerRequest::BinaryMessage {web_socket_id:_, response_sender:_, data:_}=>{
477                    }
478                    HttpServerRequest::Get{headers, response_sender}=>{
479                        let path = &headers.path;
480                        
481                        if path == "/$live_file_change"{
482                            let _ =tx_live_file.send(HttpServerRequest::Get{headers, response_sender});
483                            continue
484                        }
485                        // alright wasm http server
486                        if path == "/$watch"{
487                            let header = "HTTP/1.1 200 OK\r\n\
488                                    Cache-Control: max-age:0\r\n\
489                                    Connection: close\r\n\r\n".to_string();
490                            let _ = response_sender.send(HttpServerResponse{header, body:vec![]});
491                            continue
492                        }
493                         if path == "/favicon.ico"{
494                            let header = "HTTP/1.1 200 OK\r\n\r\n".to_string();
495                            let _ = response_sender.send(HttpServerResponse{header, body:vec![]});
496                            continue
497                        }
498                        
499                        let mime_type = if path.ends_with(".html") {"text/html"}
500                        else if path.ends_with(".wasm") {"application/wasm"}
501                        else if path.ends_with(".css") {"text/css"}
502                        else if path.ends_with(".js") {"text/javascript"}
503                        else if path.ends_with(".ttf") {"application/ttf"}
504                        else if path.ends_with(".png") {"image/png"}
505                        else if path.ends_with(".jpg") {"image/jpg"}
506                        else if path.ends_with(".svg") {"image/svg+xml"}
507                        else {continue};
508        
509                        if path.contains("..") || path.contains('\\'){
510                            continue
511                        }
512                        
513                        let mut strip = None;
514                        for remap in &remaps{
515                            if let Some(s) = path.strip_prefix(&remap.0){
516                                strip = Some(format!("{}{}",remap.1, s));
517                                break;
518                            }
519                        }
520                        if let Some(base) = strip{
521                            if let Ok(mut file_handle) = File::open(base) {
522                                let mut body = Vec::<u8>::new();
523                                if file_handle.read_to_end(&mut body).is_ok() {
524                                    let header = format!(
525                                        "HTTP/1.1 200 OK\r\n\
526                                        Content-Type: {}\r\n\
527                                        Cross-Origin-Embedder-Policy: require-corp\r\n\
528                                        Cross-Origin-Opener-Policy: same-origin\r\n\
529                                        Content-encoding: none\r\n\
530                                        Cache-Control: max-age:0\r\n\
531                                        Content-Length: {}\r\n\
532                                        Connection: close\r\n\r\n",
533                                        mime_type,
534                                        body.len()
535                                    );
536                                    let _ = response_sender.send(HttpServerResponse{header, body});
537                                }
538                            }
539                        }
540                        
541                    }
542                    HttpServerRequest::Post{..}=>{//headers, body, response}=>{
543                    }
544                }
545            }
546        });
547    }
548    
549    pub fn discover_external_ip(&mut self, _cx:&mut Cx){
550        // figure out some kind of unique id. bad but whatever.
551        let studio_uid = LiveId::from_str(&format!("{:?}{:?}", Instant::now(), std::time::SystemTime::now()));
552        
553        let write_discovery = UdpSocket::bind("0.0.0.0:41534");
554        if write_discovery.is_err(){
555            return
556        }
557        let write_discovery = write_discovery.unwrap();
558        write_discovery.set_read_timeout(Some(Duration::new(0, 1))).unwrap();
559        write_discovery.set_broadcast(true).unwrap();
560        // start a broadcast
561        std::thread::spawn(move || {
562            let dummy = studio_uid.0.to_be_bytes();
563            loop {
564                let _ = write_discovery.send_to(&dummy, "255.255.255.255:41533");
565                thread::sleep(time::Duration::from_millis(100));
566            }
567        });
568        // listen for bounced back udp packets to get our external ip
569        let ip_sender = self.recv_external_ip.sender();
570        std::thread::spawn(move || {
571            let discovery = UdpSocket::bind("0.0.0.0:41533").unwrap();
572            discovery.set_read_timeout(Some(Duration::new(0, 1))).unwrap();
573            discovery.set_broadcast(true).unwrap();
574            
575            let mut other_uid = [0u8; 8];
576            'outer: loop{
577                while let Ok((_, addr)) = discovery.recv_from(&mut other_uid) {
578                    let recv_uid = u64::from_be_bytes(other_uid);
579                    if studio_uid.0 == recv_uid {
580                        let _ = ip_sender.send(addr);
581                        break 'outer;
582                    }
583                }
584                std::thread::sleep(Duration::from_millis(50));
585            }
586        });
587    }
588}