use std::collections::HashMap;
use crate::daemon::services::{ServiceDef, ServiceError, ServiceRecord};
use crate::daemon::services_snapshot::{resurrect_from_snapshot, save_snapshot};
use crate::proto::daemon::{
DaemonRequest, DaemonResponse, ServiceConfig, ServiceDeleteResponse, ServiceDescribeResponse,
ServiceFlushResponse, ServiceListResponse, ServiceLogsResponse, ServiceRestartResponse,
ServiceResurrectResponse, ServiceSaveResponse, ServiceStartResponse, ServiceState,
ServiceStopResponse, StatusCode,
};
use super::DaemonState;
fn def_from_config(config: ServiceConfig) -> ServiceDef {
ServiceDef {
name: config.name,
cmd: config.cmd,
cwd: config.cwd,
env: config.env.into_iter().collect(),
autorestart: config.autorestart,
max_restarts: config.max_restarts,
restart_delay_ms: config.restart_delay_ms,
kill_timeout_ms: config.kill_timeout_ms,
min_uptime_ms: config.min_uptime_ms,
}
}
fn config_from_def(def: &ServiceDef) -> ServiceConfig {
let env: HashMap<String, String> = def.env.iter().cloned().collect();
ServiceConfig {
name: def.name.clone(),
cmd: def.cmd.clone(),
cwd: def.cwd.clone(),
env,
autorestart: def.autorestart,
max_restarts: def.max_restarts,
restart_delay_ms: def.restart_delay_ms,
kill_timeout_ms: def.kill_timeout_ms,
min_uptime_ms: def.min_uptime_ms,
}
}
fn state_from_record(rec: &ServiceRecord) -> ServiceState {
ServiceState {
name: rec.def.name.clone(),
id: rec.id,
status: rec.status.as_str().to_string(),
pid: rec.pid,
restart_count: rec.restart_count,
last_started_at: rec.last_started_at,
last_exited_at: rec.last_exited_at,
last_exit_code: rec.last_exit_code,
config: Some(config_from_def(&rec.def)),
}
}
fn status_for(err: &ServiceError) -> StatusCode {
match err {
ServiceError::NotFound(_) => StatusCode::NotFound,
ServiceError::AlreadyExists(_) | ServiceError::InvalidConfig(_) => {
StatusCode::InvalidArgument
}
ServiceError::Spawn(_) | ServiceError::Db(_) => StatusCode::Internal,
}
}
fn err_response(request: &DaemonRequest, err: ServiceError) -> DaemonResponse {
DaemonResponse {
request_id: request.id,
code: status_for(&err) as i32,
message: err.to_string(),
..Default::default()
}
}
pub fn handle_service_start(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let Some(req) = request.service_start.as_ref() else {
return err_response(
request,
ServiceError::InvalidConfig("missing service_start payload".into()),
);
};
let Some(config) = req.config.clone() else {
return err_response(
request,
ServiceError::InvalidConfig("missing service config".into()),
);
};
match state.services.start(def_from_config(config)) {
Ok(rec) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_start: Some(ServiceStartResponse {
service: Some(state_from_record(&rec)),
}),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_stop(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let target = request
.service_stop
.as_ref()
.map(|r| r.target.clone())
.unwrap_or_default();
match state.services.stop(&target) {
Ok(stopped_count) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_stop: Some(ServiceStopResponse { stopped_count }),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_restart(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let target = request
.service_restart
.as_ref()
.map(|r| r.target.clone())
.unwrap_or_default();
match state.services.restart(&target) {
Ok(restarted_count) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_restart: Some(ServiceRestartResponse { restarted_count }),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_delete(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let target = request
.service_delete
.as_ref()
.map(|r| r.target.clone())
.unwrap_or_default();
match state.services.delete(&target) {
Ok(deleted_count) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_delete: Some(ServiceDeleteResponse { deleted_count }),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_list(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
match state.services.list() {
Ok(records) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_list: Some(ServiceListResponse {
services: records.iter().map(state_from_record).collect(),
}),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_describe(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let target = request
.service_describe
.as_ref()
.map(|r| r.target.clone())
.unwrap_or_default();
match state.services.describe(&target) {
Ok(rec) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_describe: Some(ServiceDescribeResponse {
service: Some(state_from_record(&rec)),
}),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
const LOGS_RESPONSE_BYTE_BUDGET: usize = 64 * 1024;
const LOGS_DEFAULT_LINES: u32 = 100;
pub fn handle_service_logs(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let Some(req) = request.service_logs.as_ref() else {
return err_response(
request,
ServiceError::InvalidConfig("missing service_logs payload".into()),
);
};
match state.services.read_logs(
&req.target,
req.lines,
LOGS_DEFAULT_LINES,
LOGS_RESPONSE_BYTE_BUDGET,
) {
Ok(log_text) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_logs: Some(ServiceLogsResponse { log_text }),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_flush(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
let target = request
.service_flush
.as_ref()
.map(|r| r.target.clone())
.unwrap_or_default();
match state.services.flush_logs(&target) {
Ok(flushed_count) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_flush: Some(ServiceFlushResponse { flushed_count }),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_save(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
match save_snapshot(&state.services) {
Ok((path, count)) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_save: Some(ServiceSaveResponse {
snapshot_path: path.to_string_lossy().into_owned(),
service_count: count,
}),
..Default::default()
},
Err(e) => err_response(request, e),
}
}
pub fn handle_service_resurrect(request: &DaemonRequest, state: &DaemonState) -> DaemonResponse {
match resurrect_from_snapshot(&state.services) {
Ok((restored, _restarted)) => DaemonResponse {
request_id: request.id,
code: StatusCode::Ok as i32,
message: String::new(),
service_resurrect: Some(ServiceResurrectResponse {
restored_count: restored,
}),
..Default::default()
},
Err(e) => err_response(request, e),
}
}