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