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