Skip to main content

lib/dekit/
main.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::anyhow;
4use clap::{Arg, Command};
5use rquickjs::CatchResultExt;
6
7use crate::{
8  attach_client::client_main,
9  daemon::{
10    lockfile, socket::connect_client_socket, spawn::spawn_server_daemon,
11  },
12  dekit::{rpc_client::rpc_request, server::run_server},
13  js::js_vm::JsVm,
14  lualib::init_std,
15  protocol::{CltToSrv, DkRequest, DkResponse, SrvToClt},
16};
17
18fn print_task_list(resp: DkResponse) {
19  match resp {
20    DkResponse::TaskList(tasks) => {
21      if tasks.is_empty() {
22        println!("No tasks.");
23      } else {
24        for t in &tasks {
25          println!("{}\t{}", t.path, t.status);
26        }
27      }
28    }
29    DkResponse::Error(e) => eprintln!("Error: {}", e),
30    _ => eprintln!("Unexpected response"),
31  }
32}
33
34pub async fn dekit_main() -> anyhow::Result<()> {
35  println!("* Welcome to dekit — playground for future features *\n");
36
37  let cmd = clap::command!()
38    .subcommands([
39      Command::new("attach"),
40      Command::new("up"),
41      Command::new("down"),
42      Command::new("spawn")
43        .about("Create and start a new task at the given path")
44        .arg(
45          Arg::new("path")
46            .long("path")
47            .required(true)
48            .help("Task path (e.g. /services/web)"),
49        )
50        .arg(
51          Arg::new("cwd")
52            .long("cwd")
53            .help("Working directory for the process"),
54        )
55        .arg(
56          Arg::new("cmd")
57            .required(true)
58            .num_args(1..)
59            .last(true)
60            .help("Command to run"),
61        ),
62      Command::new("ls")
63        .about("List tasks")
64        .arg(Arg::new("glob").help("Optional glob pattern")),
65      Command::new("start")
66        .about("Start a stopped task")
67        .arg(Arg::new("path").required(true).help("Task path")),
68      Command::new("stop")
69        .about("Gracefully stop a task")
70        .arg(Arg::new("path").required(true).help("Task path")),
71      Command::new("kill")
72        .about("Force kill a task")
73        .arg(Arg::new("path").required(true).help("Task path")),
74      Command::new("restart")
75        .about("Restart a task")
76        .arg(Arg::new("path").required(true).help("Task path")),
77      Command::new("screen")
78        .about("Print the current screen of a task")
79        .arg(Arg::new("path").required(true).help("Task path")),
80      Command::new("server").subcommands([
81        Command::new("run")
82          .arg(
83            Arg::new("dir")
84              .long("dir")
85              .required(true)
86              .help("Working directory this daemon manages"),
87          )
88          .arg(
89            Arg::new("log-level")
90              .long("log-level")
91              .help("Diagnostic log level (off|error|warn|info|debug|trace, or env_logger spec). Falls back to $DK_LOG, $RUST_LOG, then 'error' (release) or 'trace' (debug)."),
92          ),
93        Command::new("start")
94          .about("Start the daemon for the current directory"),
95        Command::new("stop").about("Stop the daemon for the current directory"),
96        Command::new("status")
97          .about("Show daemon status for the current directory"),
98        Command::new("list").about("List all daemons on this machine"),
99        Command::new("clean").about("Remove stale lock files"),
100      ]),
101    ])
102    .arg(
103      Arg::new("files")
104        .action(clap::ArgAction::Append)
105        .trailing_var_arg(true),
106    );
107  let matches = cmd.get_matches();
108
109  match matches.subcommand() {
110    Some(("attach", _sub_m)) => {
111      let working_dir = std::env::current_dir()?;
112      let (sender, receiver) =
113        connect_client_socket::<CltToSrv, SrvToClt>(&working_dir, false)
114          .await?;
115      client_main(sender, receiver).await?;
116    }
117    Some(("spawn", sub_m)) => {
118      let working_dir = std::env::current_dir()?;
119      let path = sub_m.get_one::<String>("path").unwrap().clone();
120      let cwd = sub_m.get_one::<String>("cwd").cloned();
121      let cmd: Vec<String> =
122        sub_m.get_many::<String>("cmd").unwrap().cloned().collect();
123      let resp =
124        rpc_request(&working_dir, DkRequest::Spawn { path, cmd, cwd }, true)
125          .await?;
126      match resp {
127        DkResponse::Ok => println!("Spawned."),
128        DkResponse::Error(e) => eprintln!("Error: {}", e),
129        _ => eprintln!("Unexpected response"),
130      }
131    }
132    Some(("ls", sub_m)) => {
133      let working_dir = std::env::current_dir()?;
134      let glob = sub_m.get_one::<String>("glob").cloned();
135      let resp =
136        rpc_request(&working_dir, DkRequest::Ls { glob }, false).await?;
137      print_task_list(resp);
138    }
139    Some(("start", sub_m)) => {
140      let working_dir = std::env::current_dir()?;
141      let path = sub_m.get_one::<String>("path").unwrap().clone();
142      let resp =
143        rpc_request(&working_dir, DkRequest::Start { path }, true).await?;
144      match resp {
145        DkResponse::Ok => println!("Started."),
146        DkResponse::Error(e) => eprintln!("Error: {}", e),
147        _ => eprintln!("Unexpected response"),
148      }
149    }
150    Some(("stop", sub_m)) => {
151      let working_dir = std::env::current_dir()?;
152      let path = sub_m.get_one::<String>("path").unwrap().clone();
153      let resp =
154        rpc_request(&working_dir, DkRequest::Stop { path }, false).await?;
155      match resp {
156        DkResponse::Ok => println!("Stopped."),
157        DkResponse::Error(e) => eprintln!("Error: {}", e),
158        _ => eprintln!("Unexpected response"),
159      }
160    }
161    Some(("kill", sub_m)) => {
162      let working_dir = std::env::current_dir()?;
163      let path = sub_m.get_one::<String>("path").unwrap().clone();
164      let resp =
165        rpc_request(&working_dir, DkRequest::Kill { path }, false).await?;
166      match resp {
167        DkResponse::Ok => println!("Killed."),
168        DkResponse::Error(e) => eprintln!("Error: {}", e),
169        _ => eprintln!("Unexpected response"),
170      }
171    }
172    Some(("restart", sub_m)) => {
173      let working_dir = std::env::current_dir()?;
174      let path = sub_m.get_one::<String>("path").unwrap().clone();
175      let resp =
176        rpc_request(&working_dir, DkRequest::Restart { path }, true).await?;
177      match resp {
178        DkResponse::Ok => println!("Restarted."),
179        DkResponse::Error(e) => eprintln!("Error: {}", e),
180        _ => eprintln!("Unexpected response"),
181      }
182    }
183    Some(("screen", sub_m)) => {
184      let working_dir = std::env::current_dir()?;
185      let path = sub_m.get_one::<String>("path").unwrap().clone();
186      let resp =
187        rpc_request(&working_dir, DkRequest::Screen { path }, false).await?;
188      match resp {
189        DkResponse::Screen(Some(content)) => {
190          print!("{}", content);
191          // Reset terminal attributes after printing
192          print!("\x1b[0m\n");
193        }
194        DkResponse::Screen(None) => {
195          eprintln!("No screen content for this task.");
196        }
197        DkResponse::Error(e) => eprintln!("Error: {}", e),
198        _ => eprintln!("Unexpected response"),
199      }
200    }
201    Some(("up", _sub_m)) => {
202      let working_dir = std::env::current_dir()?;
203      let resp =
204        rpc_request(&working_dir, DkRequest::Ls { glob: None }, true).await?;
205      print_task_list(resp);
206    }
207    Some(("down", _sub_m)) => {
208      let working_dir = std::env::current_dir()?;
209      lockfile::stop_daemon(&working_dir)?;
210      println!("Daemon stopped.");
211    }
212    Some(("server", sub_m)) => match sub_m.subcommand() {
213      Some(("run", run_m)) => {
214        let dir = run_m.get_one::<String>("dir").unwrap();
215        let log_level =
216          run_m.get_one::<String>("log-level").map(String::as_str);
217        run_server(PathBuf::from(dir), log_level).await?;
218      }
219      Some(("start", _sub_m)) => {
220        let working_dir = std::env::current_dir()?;
221        // Check if already running.
222        if let Some(info) = lockfile::get_daemon_status(&working_dir)? {
223          if info.is_running {
224            println!("Daemon already running (pid={}).", info.contents.pid);
225            return Ok(());
226          }
227          // Stale -- clean up and start fresh.
228          lockfile::cleanup_stale(&working_dir)?;
229        }
230        spawn_server_daemon(&working_dir)?;
231        println!("Daemon started for {}.", working_dir.display());
232      }
233      Some(("stop", _sub_m)) => {
234        let working_dir = std::env::current_dir()?;
235        lockfile::stop_daemon(&working_dir)?;
236        println!("Daemon stopped.");
237      }
238      Some(("status", _sub_m)) => {
239        let working_dir = std::env::current_dir()?;
240        match lockfile::get_daemon_status(&working_dir)? {
241          Some(info) => {
242            let status = if info.is_running { "running" } else { "stale" };
243            println!(
244              "[{}] pid={} socket={} version={}",
245              status,
246              info.contents.pid,
247              info.contents.socket,
248              info.contents.version,
249            );
250          }
251          None => {
252            println!("No daemon for this directory.");
253          }
254        }
255      }
256      Some(("list", _sub_m)) => {
257        let daemons = lockfile::list_daemons()?;
258        if daemons.is_empty() {
259          println!("No daemons found.");
260        } else {
261          for d in &daemons {
262            let status = if d.is_running { "running" } else { "stale" };
263            println!(
264              "[{}] pid={} dir={} socket={} version={}",
265              status,
266              d.contents.pid,
267              d.contents.working_dir,
268              d.contents.socket,
269              d.contents.version,
270            );
271          }
272        }
273      }
274      Some(("clean", _sub_m)) => {
275        let count = lockfile::cleanup_all_stale()?;
276        println!("Removed {} stale lock file(s).", count);
277      }
278      _ => {
279        println!("Expected more arguments after `dk server`");
280      }
281    },
282    Some((arg, _sub_m)) => {
283      println!("Unknown: {}", arg);
284    }
285    None => {
286      let paths = matches
287        .get_many::<String>("files")
288        .map(|p| p.collect::<Vec<_>>())
289        .unwrap_or_default();
290
291      if let Some(first) = paths.first() {
292        // .lua
293        if first.ends_with(".lua") {
294          let src = std::fs::read_to_string(first)?;
295
296          let lua = mlua::Lua::new();
297          let cancel = tokio_util::sync::CancellationToken::new();
298          lua.set_app_data(cancel.clone());
299          lua
300            .globals()
301            .set("std", init_std(&lua).map_err(|e| anyhow!("{}", e))?)
302            .map_err(|e| anyhow!("{}", e))?;
303
304          let chunk = lua.load(src.clone());
305          let f: mlua::Function = chunk.eval().map_err(|e| anyhow!("{}", e))?;
306          let r = f
307            .call_async::<mlua::Value>(())
308            .await
309            .map_err(|e| anyhow!("{}", e))?;
310          println!("-> {:?}", r);
311          cancel.cancel();
312        }
313        // .js
314        else if first.ends_with(".js") {
315          let src = std::fs::read_to_string(first)?;
316
317          let vm = JsVm::new().await?;
318          let root =
319            vm.eval_file(Path::new("dekit.js"), src.as_bytes()).await?;
320
321          let r: anyhow::Result<()> =
322            rquickjs::async_with!(vm.context => |ctx| {
323              run_module_main(&ctx, &root).await
324            })
325            .await;
326          r?;
327        }
328      } else {
329        // No args: connect to daemon for current dir, starting it on
330        // demand.
331        let working_dir = std::env::current_dir()?;
332        let (sender, receiver) =
333          connect_client_socket::<CltToSrv, SrvToClt>(&working_dir, true)
334            .await?;
335        client_main(sender, receiver).await?;
336      }
337    }
338  }
339
340  Ok(())
341}
342
343async fn run_module_main(
344  ctx: &rquickjs::Ctx<'_>,
345  root: &rquickjs::Persistent<rquickjs::Object<'static>>,
346) -> anyhow::Result<()> {
347  let m = map_js_error(
348    ctx,
349    root.clone().restore(ctx),
350    "Failed to restore module namespace",
351  )?;
352  let main = map_js_error(
353    ctx,
354    m.get::<_, rquickjs::Value>("main"),
355    "Failed to read exported `main`",
356  )?;
357
358  let val = match main.type_of() {
359    rquickjs::Type::Constructor => map_js_error(
360      ctx,
361      main
362        .into_constructor()
363        .expect("Type checked as constructor")
364        .call::<_, rquickjs::Value>(()),
365      "Error while calling exported constructor `main`",
366    )?,
367    rquickjs::Type::Function => map_js_error(
368      ctx,
369      main
370        .into_function()
371        .expect("Type checked as function")
372        .call(()),
373      "Error while calling exported function `main`",
374    )?,
375    t => anyhow::bail!("Exported `main` is not a function ({}).", t.as_str()),
376  };
377
378  let val = if let Some(promise) = val.clone().into_promise() {
379    map_js_error(
380      ctx,
381      promise.into_future::<rquickjs::Value<'_>>().await,
382      "Unhandled rejection in exported `main`",
383    )?
384  } else {
385    val
386  };
387
388  println!("-> {:?}", val);
389  Ok(())
390}
391
392fn map_js_error<T>(
393  ctx: &rquickjs::Ctx<'_>,
394  result: rquickjs::Result<T>,
395  scope: &str,
396) -> anyhow::Result<T> {
397  result.catch(ctx).map_err(|err| anyhow!("{scope}:\n{err}"))
398}