sproc 0.5.6

Simple service management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
//! Sproc process manager
use clap::{Parser, Subcommand};
use server::APIReturn;
use std::{
    fs,
    io::{Error, ErrorKind, Result},
};

// ...
#[derive(Parser, Debug)]
#[command(version, about, long_about = Option::Some("Sproc process manager"))]
#[command(propagate_version = true)]
struct Sproc {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Load configuration file
    Pin { path: String },
    /// Run a configured service
    Run { names: Vec<String> },
    /// Spawn a service as a new task (HTTP server required: `srpoc serve`)
    Spawn { names: Vec<String> },
    /// Run all services
    RunAll {},
    /// Kill a running service
    Kill { names: Vec<String> },
    /// Kill all services
    KillAll {},
    /// Get information about a running service
    Info { name: String },
    /// Get information about all services
    InfoAll {},
    /// Wait for service to stop and update its state accordingly
    Track { name: String },
    /// Start server
    Serve {},
    /// View pinned config
    Pinned {},
    /// Merge services from given file into **source** configuration file (unpinned file)
    Merge { path: String },
    /// Pull services from given file into **pinned** configuration file (use `merge` for unpinned)
    Pull { path: String },
    /// Install services from the given remote registry address (HTTP assumed)
    Install {
        registry: String,
        names: Vec<String>,
    },
    /// "Uninstall" services given their names
    Uninstall { names: Vec<String> },
}

// ...
pub mod model;
pub mod server;

use model::{Service, ServiceState, ServiceType, ServicesConfiguration};

// real main
async fn sproc<'a>() -> Result<&'a str> {
    // init
    let args = Sproc::parse();

    // get current config
    let mut services = ServicesConfiguration::get_config();

    // ...
    match &args.command {
        // pin
        Commands::Pin { path } => {
            match fs::read_to_string(path) {
                Ok(s) => {
                    // make sure no services are running
                    for service in services.service_states {
                        if service.1 .0 == ServiceState::Running {
                            return Err(Error::new(ErrorKind::Other, "Cannot pin config with active service. Please run \"sproc kill-all\""));
                        }
                    }

                    // ...
                    let mut config: ServicesConfiguration = toml::from_str(&s).unwrap();

                    // set source to absolute path
                    config.source = std::fs::canonicalize(path)?
                        .as_path()
                        .to_str()
                        .unwrap()
                        .to_string();

                    // return
                    ServicesConfiguration::update_config(config)?;
                    Ok("Services loaded.")
                }
                Err(e) => Err(e),
            }
        }
        // run
        Commands::Run { names } => {
            if names.len() == 0 {
                return Err(Error::new(
                    ErrorKind::InvalidInput,
                    "Please provide at least 1 service name.",
                ));
            }

            for name in names {
                match services.services.get(name) {
                    Some(_) => {
                        let mut process = Service::run(name.to_string(), services.clone())?;

                        // if this is an application, wait for it to close and then continue
                        if process.0.r#type == ServiceType::Application {
                            process.1.wait()?;
                            continue; // we must continue so we don't try to add the service pid
                        }

                        // ...
                        services
                            .service_states
                            .insert(name.to_string(), (ServiceState::Running, process.1.id()));
                    }
                    None => return Err(Error::new(ErrorKind::NotFound, "Service does not exist.")),
                }
            }

            ServicesConfiguration::update_config(services)?;
            Ok("Started all requested services.")
        }
        // spawn
        Commands::Spawn { names } => {
            if names.len() == 0 {
                return Err(Error::new(
                    ErrorKind::InvalidInput,
                    "Please provide at least 1 service name.",
                ));
            }

            // post request
            let client = reqwest::Client::new();

            for name in names {
                match services.services.get(name) {
                    Some(_) => {
                        match client
                            .post(format!("http://localhost:{}/start", services.server.port))
                            .body(format!(
                                "{{ \"service\":\"{}\",\"key\":\"{}\" }}",
                                name, services.server.key
                            ))
                            .header("Content-Type", "application/json")
                            .send()
                            .await
                        {
                            Ok(r) => {
                                let res = r.text().await.expect("Failed to read body");
                                println!("info: body: {}", res);
                            }
                            Err(e) => {
                                return Err(Error::new(ErrorKind::NotConnected, e.to_string()))
                            }
                        }
                    }
                    None => return Err(Error::new(ErrorKind::NotFound, "Service does not exist.")),
                }
            }

            Ok("Sent all requested requests.")
        }
        // runall
        Commands::RunAll {} => {
            for service in &services.services {
                let mut process = Service::run(service.0.to_string(), services.clone())?;

                // if this is an application, immediately exit
                if process.0.r#type == ServiceType::Application {
                    process.1.kill()?;
                    continue;
                }

                // ...
                services.service_states.insert(
                    service.0.to_string(),
                    (ServiceState::Running, process.1.id()),
                );
            }

            ServicesConfiguration::update_config(services)?;
            Ok("Started all services.")
        }
        // kill
        Commands::Kill { names } => {
            if names.len() == 0 {
                return Err(Error::new(
                    ErrorKind::InvalidInput,
                    "Please provide at least 1 service name.",
                ));
            }

            for name in names {
                match services.services.get(name) {
                    Some(_) => {
                        Service::kill(name.to_string(), services.clone())?;
                        services.service_states.remove(name);
                    }
                    None => return Err(Error::new(ErrorKind::NotFound, "Service does not exist.")),
                }
            }

            // return
            ServicesConfiguration::update_config(services.clone())?;
            Ok("Stopped all given services.")
        }
        // kill-all
        Commands::KillAll {} => {
            for service in &services.services {
                if let Err(e) = Service::kill(service.0.to_string(), services.clone()) {
                    println!("warn: {}", e.to_string());
                }

                // if we couldn't get the pid then the service probably ran and exited already
                services.service_states.remove(service.0);
            }

            // return
            ServicesConfiguration::update_config(services)?;
            Ok("Stopped all services.")
        }
        // info
        Commands::Info { name } => match services.service_states.get(name) {
            Some(_) => {
                println!(
                    "{}",
                    Service::info(name.to_string(), services.service_states.clone())?
                );

                Ok("Finished.")
            }
            None => Err(Error::new(ErrorKind::NotFound, "Service is not loaded.")),
        },
        // info-all
        Commands::InfoAll {} => {
            for service in &services.service_states {
                if let Ok(i) = Service::info(service.0.to_string(), services.service_states.clone())
                {
                    println!("{i}");
                }
            }

            // return
            Ok("Finished.")
        }
        // track
        Commands::Track { name } => match services.services.get(name) {
            Some(_) => {
                Service::observe(name.to_string(), services.service_states.clone()).await?;

                services.service_states.remove(name);
                ServicesConfiguration::update_config(services)?;

                // return
                Ok("Service stopped.")
            }
            None => Err(Error::new(ErrorKind::NotFound, "Service does not exist.")),
        },
        // serve
        Commands::Serve {} => {
            server::server(services).await;
            Ok("Finished.")
        }
        // pinned
        Commands::Pinned {} => {
            println!("{}", toml::to_string_pretty(&services).unwrap());
            Ok("Finished.")
        }
        // merge
        Commands::Merge { path } => {
            // read file
            let other_config = ServicesConfiguration::read(std::fs::read_to_string(path)?);

            // merge and write
            services.merge_config(other_config);
            std::fs::write(
                services.source.clone(),
                toml::to_string_pretty(&services).unwrap(),
            )?;

            // return
            Ok("Merged configuration. (source + other)")
        }
        // pull
        Commands::Pull { path } => {
            // read file
            let other_config = ServicesConfiguration::read(std::fs::read_to_string(path)?);

            // merge and write
            services.merge_config(other_config);
            ServicesConfiguration::update_config(services)?;

            // return
            Ok("Pulled configuration. (pinned + other)")
        }
        // install
        Commands::Install { registry, names } => {
            if names.len() == 0 {
                return Err(Error::new(
                    ErrorKind::InvalidInput,
                    "Please provide at least 1 service name.",
                ));
            }

            // post requests
            let client = reqwest::Client::new();

            for name in names {
                match client
                    .get(format!("http://{}/registry/{}", registry, name))
                    .send()
                    .await
                {
                    Ok(r) => {
                        let res: APIReturn<String> = r.json().await.expect("Failed to read body");

                        if res.ok == false {
                            return Err(Error::new(
                                ErrorKind::Other,
                                format!("remote: {}", res.data),
                            ));
                        }

                        // add service
                        let home = std::env::var("HOME").expect("failed to read $HOME");
                        let mut service: Service = match toml::from_str(&res.data) {
                            Ok(s) => s,
                            Err(e) => {
                                return Err(Error::new(ErrorKind::InvalidData, e.to_string()))
                            }
                        };

                        // run build
                        service.bootstrap(name.to_owned()).await?;

                        // make relative home exact
                        service.working_directory = service.working_directory.replace("~", &home);

                        // make build dir exact
                        service.working_directory = service
                            .working_directory
                            .replace("@", &format!("{home}/.config/sproc/modules/{name}"));

                        // push service
                        services.services.insert(name.to_owned(), service);

                        // log
                        println!("info: installed service to pinned file: {}", name);
                    }
                    Err(e) => return Err(Error::new(ErrorKind::NotConnected, e.to_string())),
                }
            }

            ServicesConfiguration::update_config(services.clone())?;
            Ok("Sent all requested requests.")
        }
        // uninstall
        Commands::Uninstall { names } => {
            if names.len() == 0 {
                return Err(Error::new(
                    ErrorKind::InvalidInput,
                    "Please provide at least 1 service name.",
                ));
            }

            for name in names {
                if services.service_states.contains_key(name) {
                    // kill service if it's running
                    Service::kill(name.to_owned(), services.clone())?;
                }

                // remove directory
                let home = std::env::var("HOME").expect("failed to read $HOME");
                if let Ok(_) = fs::read_dir(format!("{home}/.config/sproc/modules/{name}")) {
                    fs::remove_dir_all(format!("{home}/.config/sproc/modules/{name}"))?
                }

                // remove service
                services.services.remove(name);
            }

            ServicesConfiguration::update_config(services.clone())?;
            Ok("Finished.")
        }
    }
}

// fake main
#[tokio::main]
async fn main() {
    match sproc().await {
        Ok(s) => yes(s),
        Err(e) => no(&e.to_string()),
    }
}

fn no(msg: &str) -> () {
    println!("\x1b[91m{}\x1b[0m", format!("error:\x1b[0m {msg}"));
    std::process::exit(1);
}

fn yes(msg: &str) -> () {
    println!("\x1b[92m{}\x1b[0m", format!("success:\x1b[0m {msg}"));
    std::process::exit(0);
}