1use crate::WorkerError;
2use crate::api;
3use crate::decode_base64;
4use base64::Engine as _;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(deny_unknown_fields)]
9pub struct ResourceLimits {
10 #[serde(default, skip_serializing_if = "Option::is_none")]
11 pub output_buffer_bytes: Option<u32>,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub max_runtime_ms: Option<u32>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct StartRequest {
19 pub program: String,
20 #[serde(default, skip_serializing_if = "Vec::is_empty")]
21 pub argv: Vec<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub cwd: Option<String>,
24 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
25 pub environment: std::collections::BTreeMap<String, String>,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub environment_removals: Vec<String>,
28 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
29 pub secret_references: std::collections::BTreeMap<String, String>,
30 pub client_key: String,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub resource_limits: Option<ResourceLimits>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct Status {
38 pub handle: String,
39 pub client_key: String,
40 pub state: String,
41 #[serde(default)]
42 pub exit_code: Option<i32>,
43 #[serde(default)]
44 pub termination_reason: Option<String>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49pub struct WriteResponse {
50 pub written: u32,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct ReadRequest {
56 pub handle: String,
57 #[serde(default)]
58 pub cursor: u64,
59 pub max_bytes: u32,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct ReadResponse {
65 pub data_base64: String,
66 pub cursor: u64,
67 pub eof: bool,
68 pub process_exit: bool,
69 pub stream_gap: bool,
70 #[serde(default)]
71 pub dropped_bytes: u64,
72}
73
74impl ReadResponse {
75 pub fn data(&self) -> Result<Vec<u8>, WorkerError> {
76 decode_base64(&self.data_base64)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct ExitResponse {
83 #[serde(default)]
84 pub exit_code: Option<i32>,
85 pub termination_reason: String,
86}
87
88#[derive(Serialize)]
89struct WriteRequest<'a> {
90 handle: &'a str,
91 data_base64: String,
92}
93
94#[derive(Serialize)]
95struct HandleRequest<'a> {
96 handle: &'a str,
97}
98
99#[derive(Serialize)]
100struct ClientKeyRequest<'a> {
101 client_key: &'a str,
102}
103
104pub fn start(request: &StartRequest) -> Result<Status, WorkerError> {
107 call("process.start", request)
108}
109
110pub fn attach(client_key: &str) -> Result<Status, WorkerError> {
111 call("process.attach", &ClientKeyRequest { client_key })
112}
113
114pub fn status(handle: &str) -> Result<Status, WorkerError> {
115 call("process.status", &HandleRequest { handle })
116}
117
118pub fn write_stdin(handle: &str, data: &[u8]) -> Result<WriteResponse, WorkerError> {
119 call(
120 "process.write_stdin",
121 &WriteRequest {
122 handle,
123 data_base64: base64::engine::general_purpose::STANDARD.encode(data),
124 },
125 )
126}
127
128pub fn close_stdin(handle: &str) -> Result<(), WorkerError> {
129 call::<_, serde_json::Value>("process.close_stdin", &HandleRequest { handle }).map(|_| ())
130}
131
132pub fn read_stdout(request: &ReadRequest) -> Result<ReadResponse, WorkerError> {
133 call("process.read_stdout", request)
134}
135
136pub fn read_stderr(request: &ReadRequest) -> Result<ReadResponse, WorkerError> {
137 call("process.read_stderr", request)
138}
139
140pub fn wait(handle: &str) -> Result<ExitResponse, WorkerError> {
141 call("process.wait", &HandleRequest { handle })
142}
143
144pub fn terminate(handle: &str) -> Result<(), WorkerError> {
145 call::<_, serde_json::Value>("process.terminate", &HandleRequest { handle }).map(|_| ())
146}
147
148pub fn kill(handle: &str) -> Result<(), WorkerError> {
149 call::<_, serde_json::Value>("process.kill", &HandleRequest { handle }).map(|_| ())
150}
151
152pub fn close(handle: &str) -> Result<(), WorkerError> {
153 call::<_, serde_json::Value>("process.close", &HandleRequest { handle }).map(|_| ())
154}
155
156fn call<RequestValue, ResponseValue>(
157 operation: &str,
158 request: &RequestValue,
159) -> Result<ResponseValue, WorkerError>
160where
161 RequestValue: Serialize,
162 ResponseValue: for<'de> Deserialize<'de>,
163{
164 api::call(operation, request).map_err(Into::into)
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn process_read_result_decodes_binary_data_without_text_assumptions() {
173 let result: ReadResponse = serde_json::from_value(serde_json::json!({
174 "data_base64": "AP8B",
175 "cursor": 3,
176 "eof": false,
177 "process_exit": false,
178 "stream_gap": false
179 }))
180 .unwrap();
181 assert_eq!(result.data().unwrap(), vec![0, 255, 1]);
182 }
183
184 #[test]
185 fn process_start_request_contains_secret_references_not_secret_values() {
186 let request = StartRequest {
187 program: "tool".into(),
188 argv: vec!["--json".into()],
189 cwd: None,
190 environment: std::collections::BTreeMap::new(),
191 environment_removals: Vec::new(),
192 secret_references: std::collections::BTreeMap::from([(
193 "TOKEN".into(),
194 "secret.token".into(),
195 )]),
196 client_key: "client".into(),
197 resource_limits: None,
198 };
199 let encoded = serde_json::to_value(request).unwrap();
200 assert_eq!(encoded["secret_references"]["TOKEN"], "secret.token");
201 assert!(encoded.get("TOKEN").is_none());
202 }
203}