tauri_plugin_background_service/desktop/
ipc.rs1use serde::{Deserialize, Serialize};
10
11use crate::error::ServiceError;
12use crate::models::StopReason;
13
14pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase", tag = "type")]
20#[non_exhaustive]
21pub enum IpcRequest {
22 Start {
24 config: crate::models::StartConfig,
26 },
27 Stop,
29 IsRunning,
31 GetState,
33 EnableAutoRestart {
35 config: Option<crate::models::StartConfig>,
37 },
38 DisableAutoRestart,
40 GetDesiredState,
42 ValidateSetup,
44 GetLifecycleStatus,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct IpcResponse {
52 pub ok: bool,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub data: Option<serde_json::Value>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub error: Option<String>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase", tag = "type")]
65#[non_exhaustive]
66pub enum IpcEvent {
67 Started,
69 Stopped { reason: StopReason },
71 Error { message: String },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(tag = "kind", rename_all = "camelCase")]
82pub enum IpcMessage {
83 Request(IpcRequest),
85 Response(IpcResponse),
87 Event(IpcEvent),
89}
90
91pub fn encode_frame<T: Serialize>(msg: &T) -> Result<Vec<u8>, serde_json::Error> {
95 let json = serde_json::to_vec(msg)?;
96 let len = json.len() as u32;
97 let mut buf = Vec::with_capacity(4 + json.len());
98 buf.extend_from_slice(&len.to_be_bytes());
99 buf.extend_from_slice(&json);
100 Ok(buf)
101}
102
103pub fn decode_frame(payload: &[u8]) -> Result<IpcMessage, serde_json::Error> {
110 serde_json::from_slice(payload)
111}
112
113pub fn socket_path(label: &str) -> Result<std::path::PathBuf, ServiceError> {
124 sanitize_label(label)?;
125 #[cfg(target_os = "linux")]
126 {
127 let dir = std::env::var("XDG_RUNTIME_DIR")
128 .unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));
129 Ok(std::path::PathBuf::from(format!("{dir}/{label}.sock")))
130 }
131 #[cfg(target_os = "macos")]
132 {
133 Ok(std::path::PathBuf::from(format!("/tmp/{label}.sock")))
134 }
135 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
136 {
137 Ok(std::path::PathBuf::from(format!("/tmp/{label}.sock")))
138 }
139}
140
141fn sanitize_label(label: &str) -> Result<std::path::PathBuf, ServiceError> {
143 if label.is_empty() {
144 return Err(ServiceError::Init("service label must not be empty".into()));
145 }
146 if label.contains('/') || label.contains('\\') {
147 return Err(ServiceError::Init(format!(
148 "service label must not contain path separators: {label}"
149 )));
150 }
151 if label.contains("..") {
152 return Err(ServiceError::Init(format!(
153 "service label must not contain '..': {label}"
154 )));
155 }
156 Ok(std::path::PathBuf::from(label))
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
166 fn ipc_request_start_serde_roundtrip() {
167 let req = IpcRequest::Start {
168 config: crate::models::StartConfig {
169 service_label: "Syncing".into(),
170 foreground_service_type: "dataSync".into(),
171 },
172 };
173 let json = serde_json::to_string(&req).unwrap();
174 let de: IpcRequest = serde_json::from_str(&json).unwrap();
175 match de {
176 IpcRequest::Start { config } => {
177 assert_eq!(config.service_label, "Syncing");
178 assert_eq!(config.foreground_service_type, "dataSync");
179 }
180 other => panic!("Expected Start, got {other:?}"),
181 }
182 }
183
184 #[test]
185 fn ipc_request_start_json_tag() {
186 let req = IpcRequest::Start {
187 config: crate::models::StartConfig::default(),
188 };
189 let json = serde_json::to_string(&req).unwrap();
190 assert!(json.contains("\"type\":\"start\""), "Tagged JSON: {json}");
191 }
192
193 #[test]
194 fn ipc_request_stop_serde_roundtrip() {
195 let req = IpcRequest::Stop;
196 let json = serde_json::to_string(&req).unwrap();
197 let de: IpcRequest = serde_json::from_str(&json).unwrap();
198 assert!(matches!(de, IpcRequest::Stop));
199 }
200
201 #[test]
202 fn ipc_request_stop_json_tag() {
203 let req = IpcRequest::Stop;
204 let json = serde_json::to_string(&req).unwrap();
205 assert!(json.contains("\"type\":\"stop\""), "Tagged JSON: {json}");
206 }
207
208 #[test]
209 fn ipc_request_is_running_serde_roundtrip() {
210 let req = IpcRequest::IsRunning;
211 let json = serde_json::to_string(&req).unwrap();
212 let de: IpcRequest = serde_json::from_str(&json).unwrap();
213 assert!(matches!(de, IpcRequest::IsRunning));
214 }
215
216 #[test]
217 fn ipc_request_is_running_json_tag() {
218 let req = IpcRequest::IsRunning;
219 let json = serde_json::to_string(&req).unwrap();
220 assert!(
221 json.contains("\"type\":\"isRunning\""),
222 "Tagged JSON: {json}"
223 );
224 }
225
226 #[test]
229 fn ipc_request_get_state_serde_roundtrip() {
230 let req = IpcRequest::GetState;
231 let json = serde_json::to_string(&req).unwrap();
232 let de: IpcRequest = serde_json::from_str(&json).unwrap();
233 assert!(matches!(de, IpcRequest::GetState));
234 }
235
236 #[test]
237 fn ipc_request_get_state_json_tag() {
238 let req = IpcRequest::GetState;
239 let json = serde_json::to_string(&req).unwrap();
240 assert!(
241 json.contains("\"type\":\"getState\""),
242 "Tagged JSON: {json}"
243 );
244 }
245
246 #[test]
249 fn ipc_response_success_roundtrip() {
250 let resp = IpcResponse {
251 ok: true,
252 data: Some(serde_json::json!({"running": true})),
253 error: None,
254 };
255 let json = serde_json::to_string(&resp).unwrap();
256 let de: IpcResponse = serde_json::from_str(&json).unwrap();
257 assert!(de.ok);
258 assert_eq!(de.data.unwrap()["running"], true);
259 assert!(de.error.is_none());
260 }
261
262 #[test]
263 fn ipc_response_error_roundtrip() {
264 let resp = IpcResponse {
265 ok: false,
266 data: None,
267 error: Some("Service is already running".into()),
268 };
269 let json = serde_json::to_string(&resp).unwrap();
270 let de: IpcResponse = serde_json::from_str(&json).unwrap();
271 assert!(!de.ok);
272 assert!(de.data.is_none());
273 assert_eq!(de.error.unwrap(), "Service is already running");
274 }
275
276 #[test]
277 fn ipc_response_skips_none_fields() {
278 let resp = IpcResponse {
279 ok: true,
280 data: None,
281 error: None,
282 };
283 let json = serde_json::to_string(&resp).unwrap();
284 assert!(!json.contains("\"data\""), "Should skip null data: {json}");
285 assert!(
286 !json.contains("\"error\""),
287 "Should skip null error: {json}"
288 );
289 }
290
291 #[test]
292 fn ipc_response_camel_case_keys() {
293 let resp = IpcResponse {
294 ok: true,
295 data: None,
296 error: None,
297 };
298 let json = serde_json::to_string(&resp).unwrap();
299 assert!(json.contains("\"ok\""), "ok key: {json}");
300 }
301
302 #[test]
305 fn ipc_event_started_serde_roundtrip() {
306 let event = IpcEvent::Started;
307 let json = serde_json::to_string(&event).unwrap();
308 let de: IpcEvent = serde_json::from_str(&json).unwrap();
309 assert!(matches!(de, IpcEvent::Started));
310 }
311
312 #[test]
313 fn ipc_event_started_json_tag() {
314 let event = IpcEvent::Started;
315 let json = serde_json::to_string(&event).unwrap();
316 assert!(json.contains("\"type\":\"started\""), "Tagged JSON: {json}");
317 }
318
319 #[test]
320 fn ipc_event_stopped_serde_roundtrip() {
321 let event = IpcEvent::Stopped {
322 reason: StopReason::UserStop,
323 };
324 let json = serde_json::to_string(&event).unwrap();
325 let de: IpcEvent = serde_json::from_str(&json).unwrap();
326 match de {
327 IpcEvent::Stopped { reason } => assert_eq!(reason, StopReason::UserStop),
328 other => panic!("Expected Stopped, got {other:?}"),
329 }
330 }
331
332 #[test]
333 fn ipc_event_stopped_json_keys() {
334 let event = IpcEvent::Stopped {
335 reason: StopReason::TaskCompleted,
336 };
337 let json = serde_json::to_string(&event).unwrap();
338 assert!(json.contains("\"type\":\"stopped\""), "Tag: {json}");
339 assert!(
340 json.contains("\"reason\":\"taskCompleted\""),
341 "Reason: {json}"
342 );
343 }
344
345 #[test]
346 fn ipc_event_error_serde_roundtrip() {
347 let event = IpcEvent::Error {
348 message: "init failed".into(),
349 };
350 let json = serde_json::to_string(&event).unwrap();
351 let de: IpcEvent = serde_json::from_str(&json).unwrap();
352 match de {
353 IpcEvent::Error { message } => assert_eq!(message, "init failed"),
354 other => panic!("Expected Error, got {other:?}"),
355 }
356 }
357
358 #[test]
359 fn ipc_event_error_json_keys() {
360 let event = IpcEvent::Error {
361 message: "oops".into(),
362 };
363 let json = serde_json::to_string(&event).unwrap();
364 assert!(json.contains("\"type\":\"error\""), "Tag: {json}");
365 assert!(json.contains("\"message\":\"oops\""), "Message: {json}");
366 }
367
368 #[test]
371 fn ipc_frame_encode_decode_request() {
372 let req = IpcRequest::Start {
373 config: crate::models::StartConfig::default(),
374 };
375 let msg = IpcMessage::Request(req);
376 let encoded = encode_frame(&msg).unwrap();
377 let decoded = decode_frame(&encoded[4..]).unwrap();
378 match decoded {
379 IpcMessage::Request(IpcRequest::Start { config }) => {
380 assert_eq!(config.service_label, "Service running");
381 }
382 other => panic!("Expected Request(Start), got {other:?}"),
383 }
384 }
385
386 #[test]
387 fn ipc_frame_encode_decode_response() {
388 let resp = IpcResponse {
389 ok: true,
390 data: Some(serde_json::json!(42)),
391 error: None,
392 };
393 let msg = IpcMessage::Response(resp);
394 let encoded = encode_frame(&msg).unwrap();
395 let decoded = decode_frame(&encoded[4..]).unwrap();
396 match decoded {
397 IpcMessage::Response(r) => {
398 assert!(r.ok);
399 assert_eq!(r.data.unwrap(), 42);
400 }
401 other => panic!("Expected Response, got {other:?}"),
402 }
403 }
404
405 #[test]
406 fn ipc_frame_encode_decode_event() {
407 let event = IpcEvent::Stopped {
408 reason: StopReason::TaskCompleted,
409 };
410 let msg = IpcMessage::Event(event);
411 let encoded = encode_frame(&msg).unwrap();
412 let decoded = decode_frame(&encoded[4..]).unwrap();
413 match decoded {
414 IpcMessage::Event(IpcEvent::Stopped { reason }) => {
415 assert_eq!(reason, StopReason::TaskCompleted)
416 }
417 other => panic!("Expected Event(Stopped), got {other:?}"),
418 }
419 }
420
421 #[test]
422 fn ipc_frame_length_prefix_is_big_endian() {
423 let req = IpcRequest::Stop;
424 let encoded = encode_frame(&req).unwrap();
425 let len = u32::from_be_bytes([encoded[0], encoded[1], encoded[2], encoded[3]]);
427 assert_eq!(len as usize, encoded.len() - 4);
428 }
429
430 #[test]
431 fn ipc_frame_decode_payload_without_length_prefix() {
432 let resp = IpcResponse {
434 ok: true,
435 data: Some(serde_json::json!({"status": "ok"})),
436 error: None,
437 };
438 let msg = IpcMessage::Response(resp);
439 let payload = serde_json::to_vec(&msg).unwrap();
440 let decoded = decode_frame(&payload).unwrap();
441 match decoded {
442 IpcMessage::Response(r) => {
443 assert!(r.ok);
444 assert_eq!(r.data.unwrap()["status"], "ok");
445 }
446 other => panic!("Expected Response, got {other:?}"),
447 }
448 }
449
450 #[test]
451 fn ipc_frame_malformed_json() {
452 let payload = b"{invalid";
453 let result = decode_frame(payload);
454 assert!(result.is_err(), "Expected JSON error for malformed payload");
455 }
456
457 #[test]
460 fn ipc_message_response_roundtrip() {
461 let msg = IpcMessage::Response(IpcResponse {
462 ok: true,
463 data: Some(serde_json::json!({"running": true})),
464 error: None,
465 });
466 let json = serde_json::to_string(&msg).unwrap();
467 assert!(json.contains("\"kind\":\"response\""), "kind tag: {json}");
468 let de: IpcMessage = serde_json::from_str(&json).unwrap();
469 match de {
470 IpcMessage::Response(resp) => {
471 assert!(resp.ok);
472 assert_eq!(resp.data.unwrap()["running"], true);
473 }
474 other => panic!("Expected Response, got {other:?}"),
475 }
476 }
477
478 #[test]
479 fn ipc_message_event_roundtrip() {
480 let msg = IpcMessage::Event(IpcEvent::Stopped {
481 reason: StopReason::UserStop,
482 });
483 let json = serde_json::to_string(&msg).unwrap();
484 assert!(json.contains("\"kind\":\"event\""), "kind tag: {json}");
485 let de: IpcMessage = serde_json::from_str(&json).unwrap();
486 match de {
487 IpcMessage::Event(IpcEvent::Stopped { reason }) => {
488 assert_eq!(reason, StopReason::UserStop);
489 }
490 other => panic!("Expected Event, got {other:?}"),
491 }
492 }
493
494 #[test]
495 fn ipc_message_ambiguous_frame_deterministic() {
496 let json = r#"{"kind":"event","type":"started","ok":true}"#;
499 let de: IpcMessage = serde_json::from_str(json).unwrap();
500 match de {
501 IpcMessage::Event(IpcEvent::Started) => {} other => panic!("Expected Event::Started, got {other:?}"),
503 }
504
505 let json2 = r#"{"kind":"response","ok":true,"data":{"type":"started"}}"#;
507 let de2: IpcMessage = serde_json::from_str(json2).unwrap();
508 match de2 {
509 IpcMessage::Response(resp) => {
510 assert!(resp.ok);
511 }
512 other => panic!("Expected Response, got {other:?}"),
513 }
514 }
515
516 #[test]
517 fn ipc_message_unknown_kind_rejected() {
518 let json = r#"{"kind":"unknown","ok":true}"#;
519 let result: Result<IpcMessage, _> = serde_json::from_str(json);
520 assert!(result.is_err(), "Expected error for unknown kind value");
521 }
522
523 #[test]
526 fn socket_path_unix_format() {
527 let path = socket_path("com.example.svc").unwrap();
528 #[cfg(target_os = "linux")]
529 {
530 let path_str = path.to_str().unwrap();
532 assert!(
533 path_str.ends_with("/com.example.svc.sock"),
534 "Expected path ending with /com.example.svc.sock, got: {path_str}"
535 );
536 if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
537 assert!(
538 path_str.starts_with(&xdg),
539 "Expected path under XDG_RUNTIME_DIR ({xdg}), got: {path_str}"
540 );
541 } else {
542 assert!(
543 path_str.contains("/run/user/"),
544 "Expected fallback /run/user/ path, got: {path_str}"
545 );
546 }
547 }
548 #[cfg(target_os = "macos")]
549 {
550 assert_eq!(path.to_str().unwrap(), "/tmp/com.example.svc.sock");
551 }
552 }
553
554 #[test]
555 fn socket_path_nonempty_label() {
556 let path = socket_path("my-app").unwrap();
557 #[cfg(unix)]
558 {
559 assert!(
560 path.to_str().unwrap().ends_with("my-app.sock"),
561 "Expected path ending with my-app.sock, got: {:?}",
562 path
563 );
564 }
565 }
566
567 #[test]
568 fn socket_path_rejects_slash_in_label() {
569 let result = socket_path("../etc/passwd");
570 assert!(result.is_err());
571 let err = result.unwrap_err().to_string();
572 assert!(err.contains("path separators"), "Error: {err}");
573 }
574
575 #[test]
576 fn socket_path_rejects_dotdot_in_label() {
577 let result = socket_path("..");
578 assert!(result.is_err());
579 let err = result.unwrap_err().to_string();
580 assert!(err.contains("'..'"), "Error: {err}");
581 }
582
583 #[test]
584 fn socket_path_rejects_backslash_in_label() {
585 let result = socket_path("foo\\bar");
586 assert!(result.is_err());
587 let err = result.unwrap_err().to_string();
588 assert!(err.contains("path separators"), "Error: {err}");
589 }
590
591 #[test]
592 fn socket_path_rejects_empty_label() {
593 let result = socket_path("");
594 assert!(result.is_err());
595 let err = result.unwrap_err().to_string();
596 assert!(err.contains("empty"), "Error: {err}");
597 }
598}