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