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 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, 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 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 self.start_http_server();
170 }
171
172
173 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 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 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 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 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 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 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 self.clients[0].handle_event_with(cx, event, &mut | cx, wrap | {
328 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 }
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 }
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(_) => { 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 }
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 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 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 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 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 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{..}=>{}
544 }
545 }
546 });
547 }
548
549 pub fn discover_external_ip(&mut self, _cx:&mut Cx){
550 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 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 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}