onetaskgraph_core/subprocess/
serve.rs1use std::collections::BTreeMap;
15use std::io::{BufRead, Write};
16
17use onetaskgraph_plugin_api::{SecretResolver, SourceError, SourceName, TaskSource};
18use secrecy::SecretString;
19use serde::Deserialize;
20use serde_json::{Value, json};
21
22use super::connection::{Line, MAX_LINE, read_line};
23use super::wire::{
24 DeleteParams, DependencyParams, HandshakePluginKind, IdParams, InitializeParams,
25 InitializeResult, LabelParams, PROTOCOL_VERSION, ProjectQueryParams, ProjectWriteParams,
26 Request, Response, TaskQueryParams, TaskWriteParams,
27};
28use crate::registry::PluginKind;
29
30#[derive(Debug, Clone, Deserialize)]
37struct HostedSettings {
38 kind: PluginKind,
40 #[serde(default)]
42 config: Value,
43}
44
45pub async fn serve(input: impl BufRead, output: impl Write) -> std::io::Result<()> {
55 serve_kind(input, output, None).await
56}
57
58pub async fn serve_plugin(
63 input: impl BufRead,
64 output: impl Write,
65 kind: PluginKind,
66) -> std::io::Result<()> {
67 serve_kind(input, output, Some(kind)).await
68}
69
70async fn serve_kind(
71 mut input: impl BufRead,
72 mut output: impl Write,
73 kind: Option<PluginKind>,
74) -> std::io::Result<()> {
75 let mut source: Option<Box<dyn TaskSource>> = None;
76 loop {
77 let line = match read_line(&mut input) {
78 Line::Read(line) => line,
79 Line::Ended => return Ok(()),
80 Line::Failed(error) => return Err(error),
81 Line::TooLong => {
85 eprintln!(
86 "onetaskgraph-source: a request ran past {MAX_LINE} bytes without \
87 ending its line; closing the connection"
88 );
89 return Ok(());
90 }
91 };
92 if line.trim().is_empty() {
93 continue;
94 }
95 let Some(id) = addressed(&line) else {
96 eprintln!("onetaskgraph-source: ignoring a line with no request id: {line}");
97 continue;
98 };
99 let response = match serde_json::from_str::<Request>(&line) {
100 Ok(request) => answer(&mut source, request, kind).await,
101 Err(error) => Response::failed(
102 id,
103 SourceError::Malformed {
104 message: format!("that is not a request envelope: {error}"),
105 },
106 ),
107 };
108 let finished = ended_the_connection(&response);
109 writeln!(
110 output,
111 "{}",
112 serde_json::to_string(&response).expect("a response is plain data")
114 )?;
115 output.flush()?;
116 if finished {
117 return Ok(());
118 }
119 }
120}
121
122fn addressed(line: &str) -> Option<String> {
129 serde_json::from_str::<Value>(line)
130 .ok()?
131 .get("id")?
132 .as_str()
133 .map(str::to_owned)
134}
135
136fn ended_the_connection(response: &Response) -> bool {
138 matches!(
139 response.error.as_ref(),
140 Some(SourceError::Config { message }) if message.starts_with(VERSION_REFUSAL)
141 )
142}
143
144const VERSION_REFUSAL: &str = "protocol version ";
147
148async fn answer(
150 source: &mut Option<Box<dyn TaskSource>>,
151 request: Request,
152 kind: Option<PluginKind>,
153) -> Response {
154 let Request { id, method, params } = request;
155 if method == "initialize" {
156 return match source {
157 Some(_) => Response::failed(
158 id,
159 SourceError::Malformed {
160 message: "this connection was already initialized".to_owned(),
161 },
162 ),
163 None => initialize(source, id, params, kind),
164 };
165 }
166 let Some(built) = source.as_deref() else {
167 return Response::failed(
168 id,
169 SourceError::Malformed {
170 message: format!("{method} arrived before the handshake"),
171 },
172 );
173 };
174 match dispatch(built, &method, params).await {
175 Ok(result) => Response::ok(id, result),
176 Err(error) => Response::failed(id, error),
177 }
178}
179
180fn initialize(
182 source: &mut Option<Box<dyn TaskSource>>,
183 id: String,
184 params: Value,
185 kind: Option<PluginKind>,
186) -> Response {
187 let params: InitializeParams = match serde_json::from_value(params) {
188 Ok(params) => params,
189 Err(error) => {
190 return Response::failed(
191 id,
192 SourceError::Config {
193 message: format!("that is not an initialize request: {error}"),
194 },
195 );
196 }
197 };
198 if params.protocol_version != PROTOCOL_VERSION {
199 return Response::failed(
200 id,
201 SourceError::Config {
202 message: format!(
203 "{VERSION_REFUSAL}{} is not supported by this plugin; it speaks \
204 version {PROTOCOL_VERSION}",
205 params.protocol_version
206 ),
207 },
208 );
209 }
210 match build(¶ms, kind) {
211 Ok(built) => {
212 let kind = match HandshakePluginKind::new(built.kind()) {
213 Ok(kind) => kind,
214 Err(error) => {
215 return Response::failed(
216 id,
217 SourceError::Malformed {
218 message: format!("the hosted plugin reported an invalid kind: {error}"),
219 },
220 );
221 }
222 };
223 let result = InitializeResult {
224 protocol_version: Some(PROTOCOL_VERSION),
225 kind,
226 capabilities: built.capabilities(),
227 writes: Some(built.writes()),
228 };
229 *source = Some(built);
230 Response::ok(
232 id,
233 serde_json::to_value(&result).expect("a result is plain data"),
234 )
235 }
236 Err(error) => Response::failed(id, error),
237 }
238}
239
240fn build(
242 params: &InitializeParams,
243 selected: Option<PluginKind>,
244) -> Result<Box<dyn TaskSource>, SourceError> {
245 let (kind, config) = match selected {
246 Some(kind) => (kind, ¶ms.config),
247 None => {
248 let settings: HostedSettings = serde_json::from_value(params.config.clone()).map_err(
249 |error| SourceError::Config {
250 message: format!(
251 "this host serves a plugin of this build, and its settings must name one \
252 as {{\"kind\": …, \"config\": …}}: {error}"
253 ),
254 },
255 )?;
256 return build_plugin(params, settings.kind, &settings.config);
257 }
258 };
259 build_plugin(params, kind, config)
260}
261
262fn build_plugin(
263 params: &InitializeParams,
264 kind: PluginKind,
265 config: &Value,
266) -> Result<Box<dyn TaskSource>, SourceError> {
267 let name = SourceName::new(params.source_name.clone())?;
268 kind.plugin()
269 .build(&name, config, &Handshake(¶ms.secrets))
270}
271
272struct Handshake<'a>(&'a BTreeMap<String, String>);
279
280impl SecretResolver for Handshake<'_> {
281 fn get(&self, var: &str) -> Option<SecretString> {
282 self.0
283 .get(var)
284 .map(|value| SecretString::from(value.clone()))
285 }
286}
287
288async fn dispatch(
290 source: &dyn TaskSource,
291 method: &str,
292 params: Value,
293) -> Result<Value, SourceError> {
294 match method {
295 "health" => encode(source.health().await?),
296 "get_task" => {
297 let params: IdParams = decode(method, params)?;
298 encode(json!({ "task": source.get_task(¶ms.id).await? }))
299 }
300 "get_project" => {
301 let params: IdParams = decode(method, params)?;
302 encode(json!({ "project": source.get_project(¶ms.id).await? }))
303 }
304 "query_tasks" => {
305 let params: TaskQueryParams = decode(method, params)?;
306 encode(source.query_tasks(¶ms.query, ¶ms.page).await?)
307 }
308 "query_projects" => {
309 let params: ProjectQueryParams = decode(method, params)?;
310 encode(source.query_projects(¶ms.query, ¶ms.page).await?)
311 }
312 "labels" => {
313 let params: LabelParams = decode(method, params)?;
314 encode(source.labels(¶ms.page).await?)
315 }
316 "task_dependencies" => {
317 let params: DependencyParams = decode(method, params)?;
318 encode(
319 source
320 .task_dependencies(¶ms.id, params.direction, ¶ms.page)
321 .await?,
322 )
323 }
324 "project_dependencies" => {
325 let params: DependencyParams = decode(method, params)?;
326 encode(
327 source
328 .project_dependencies(¶ms.id, params.direction, ¶ms.page)
329 .await?,
330 )
331 }
332 "write_task" => {
333 let params: TaskWriteParams = decode(method, params)?;
334 encode(json!({ "id": source.write_task(¶ms.write).await? }))
335 }
336 "write_project" => {
337 let params: ProjectWriteParams = decode(method, params)?;
338 encode(json!({ "id": source.write_project(¶ms.write).await? }))
339 }
340 "delete_task" => {
341 let params: DeleteParams = decode(method, params)?;
342 source.delete_task(¶ms.id).await?;
343 encode(json!({}))
344 }
345 "delete_project" => {
346 let params: DeleteParams = decode(method, params)?;
347 source.delete_project(¶ms.id).await?;
348 encode(json!({}))
349 }
350 other => Err(SourceError::Malformed {
351 message: format!("protocol version {PROTOCOL_VERSION} has no method called {other:?}"),
352 }),
353 }
354}
355
356fn decode<T: for<'de> Deserialize<'de>>(method: &str, params: Value) -> Result<T, SourceError> {
358 serde_json::from_value(params).map_err(|error| SourceError::Malformed {
359 message: format!("the parameters of {method} are not the shape it takes: {error}"),
360 })
361}
362
363fn encode<T: serde::Serialize>(value: T) -> Result<Value, SourceError> {
365 serde_json::to_value(value).map_err(|error| SourceError::Malformed {
366 message: format!("this source returned data that will not serialize: {error}"),
367 })
368}