Line data Source code
1 : //! dashql: the command line tool of DashQL.
2 : //! `dashql agent` reaches the DashQL session that is open in the browser or the desktop app through the local agent
3 : //! server, which `dashql agent serve` runs. `dashql lint` lints and `dashql format` pretty prints SQL files on disk with
4 : //! the native dashql-core.
5 :
6 : mod agent;
7 : mod format_files;
8 : mod inputs;
9 : mod lint;
10 : mod lint_files;
11 : mod report;
12 :
13 : use agent::client::{self, CliEnvironment, EXIT_FAILED, EXIT_OK, EXIT_USAGE};
14 : use agent::config::{agent_home, load_config};
15 : use agent::server::{log_line, AgentServer, AgentServerOptions, AGENT_RUN_ENV};
16 : use std::io::Write;
17 : use std::process::ExitCode;
18 :
19 : const USAGE: &str = "Usage: dashql <command> [options]
20 :
21 : Commands:
22 : agent <command> Work with the DashQL session that is open in the browser or the desktop app,
23 : through the local agent server, see dashql agent --help
24 : format <file.sql|dir|->... Pretty print SQL files on disk with dashql-core, see dashql format --help
25 : lint <file.sql|dir|->... Lint SQL files on disk with dashql-core, see dashql lint --help";
26 :
27 0 : fn env_var(name: &str) -> Option<String> {
28 0 : std::env::var(name).ok()
29 0 : }
30 :
31 : /// Run the agent server in the foreground until it is interrupted, or until stdin closes with exit_with_stdin.
32 : /// DashQL.app holds the other end of stdin, so the server stops with the app even when the app crashes.
33 0 : fn serve(exit_with_stdin: bool) -> ExitCode {
34 : // An agent in a harness run may not start a server, whatever its harness allows
35 0 : if env_var(AGENT_RUN_ENV).is_some() {
36 0 : eprintln!("dashql agent serve cannot run inside an agent run");
37 0 : return ExitCode::from(EXIT_USAGE);
38 0 : }
39 0 : let home = agent_home(&env_var);
40 0 : let config = match load_config(&home, &env_var) {
41 0 : Ok(config) => config,
42 0 : Err(e) => {
43 0 : eprintln!("Cannot read the agent configuration: {e}");
44 0 : return ExitCode::from(EXIT_USAGE);
45 : }
46 : };
47 0 : let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
48 0 : Ok(runtime) => runtime,
49 0 : Err(e) => {
50 0 : eprintln!("Cannot start the agent server: {e}");
51 0 : return ExitCode::from(EXIT_FAILED);
52 : }
53 : };
54 0 : runtime.block_on(async {
55 0 : let port = config.port;
56 0 : let mut options = AgentServerOptions::new(config, home);
57 : // Harness runs call this executable as dashql
58 0 : options.cli = std::env::current_exe().ok();
59 0 : options.log = true;
60 0 : let server = match AgentServer::listen(options).await {
61 0 : Ok(server) => server,
62 0 : Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {
63 0 : eprintln!(
64 : "Port {port} is taken. Is DashQL already running? Set DASHQL_AGENT_PORT to use another port."
65 : );
66 0 : return ExitCode::from(EXIT_FAILED);
67 : }
68 0 : Err(e) => {
69 0 : eprintln!("Cannot start the agent server: {e}");
70 0 : return ExitCode::from(EXIT_FAILED);
71 : }
72 : };
73 0 : let stdin_closed = async {
74 0 : if !exit_with_stdin {
75 0 : return std::future::pending().await;
76 0 : }
77 0 : let (closed, on_closed) = tokio::sync::oneshot::channel::<()>();
78 0 : std::thread::spawn(move || {
79 0 : let _ = std::io::copy(&mut std::io::stdin(), &mut std::io::sink());
80 0 : let _ = closed.send(());
81 0 : });
82 0 : let _ = on_closed.await;
83 0 : };
84 0 : tokio::select! {
85 0 : _ = shutdown_signal() => log_line("Stopping: received a stop signal"),
86 0 : _ = stdin_closed => log_line("Stopping: stdin closed"),
87 : }
88 0 : server.close().await;
89 0 : ExitCode::from(EXIT_OK)
90 0 : })
91 0 : }
92 :
93 0 : async fn shutdown_signal() {
94 : #[cfg(unix)]
95 : {
96 : use tokio::signal::unix::{signal, SignalKind};
97 0 : if let Ok(mut terminate) = signal(SignalKind::terminate()) {
98 0 : tokio::select! {
99 0 : _ = tokio::signal::ctrl_c() => {}
100 0 : _ = terminate.recv() => {}
101 : }
102 0 : return;
103 0 : }
104 : }
105 0 : let _ = tokio::signal::ctrl_c().await;
106 0 : }
107 :
108 : /// Run `dashql agent` with the arguments after `agent`
109 0 : fn agent(argv: &[String]) -> ExitCode {
110 0 : let options = match client::parse_args(argv) {
111 0 : Ok(options) => options,
112 0 : Err(e) => {
113 0 : eprintln!("{e}\n\n{}", client::usage());
114 0 : return ExitCode::from(EXIT_USAGE);
115 : }
116 : };
117 0 : if options.help || options.positionals.first().map(String::as_str) == Some("help") {
118 0 : println!("{}", client::usage());
119 0 : return ExitCode::SUCCESS;
120 0 : }
121 0 : if options.positionals == ["serve"] {
122 0 : return serve(options.exit_with_stdin);
123 0 : }
124 0 : let config = match load_config(&agent_home(&env_var), &env_var) {
125 0 : Ok(config) => config,
126 0 : Err(e) => {
127 0 : eprintln!("Cannot read the agent configuration: {e}");
128 0 : return ExitCode::from(EXIT_USAGE);
129 : }
130 : };
131 0 : let read_file = |file: &str| {
132 0 : let path = inputs::working_directory().map_err(|e| format!("working directory: {e}"))?.join(file);
133 0 : std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))
134 0 : };
135 0 : let write_file = |file: &str, bytes: &[u8]| {
136 0 : let path = inputs::working_directory().map_err(|e| format!("working directory: {e}"))?.join(file);
137 0 : std::fs::write(&path, bytes).map_err(|e| format!("{}: {e}", path.display()))
138 0 : };
139 0 : let output = client::run_cli(
140 0 : &options,
141 : &CliEnvironment {
142 0 : session: env_var("DASHQL_SESSION"),
143 0 : read_stdin: &|| std::io::read_to_string(std::io::stdin()),
144 0 : read_file: &read_file,
145 0 : write_file: &write_file,
146 0 : request: &|method, path, body| client::request(&config, method, path, body),
147 : },
148 : );
149 0 : let _ = std::io::stdout().write_all(output.stdout.as_bytes());
150 0 : let _ = std::io::stderr().write_all(output.stderr.as_bytes());
151 0 : ExitCode::from(output.exit_code)
152 0 : }
153 :
154 0 : fn main() -> ExitCode {
155 0 : let argv: Vec<String> = std::env::args().skip(1).collect();
156 0 : match argv.first().map(String::as_str) {
157 0 : Some("agent") => agent(&argv[1..]),
158 0 : Some("format") => format_files::main(&argv[1..]),
159 0 : Some("lint") => lint_files::main(&argv[1..]),
160 0 : Some("help" | "-h" | "--help") => {
161 0 : println!("{USAGE}");
162 0 : ExitCode::SUCCESS
163 : }
164 0 : Some(command) => {
165 0 : eprintln!("Unknown command: {command}\n\n{USAGE}");
166 0 : ExitCode::from(EXIT_USAGE)
167 : }
168 : None => {
169 0 : eprintln!("{USAGE}");
170 0 : ExitCode::from(EXIT_USAGE)
171 : }
172 : }
173 0 : }
|