@parlor/shell-desktop 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/assets/icons/128x128.png +0 -0
  4. package/assets/icons/128x128@2x.png +0 -0
  5. package/assets/icons/32x32.png +0 -0
  6. package/assets/icons/icon.icns +0 -0
  7. package/assets/icons/icon.ico +0 -0
  8. package/dist/build.d.ts +3 -0
  9. package/dist/build.d.ts.map +1 -0
  10. package/dist/build.js +18 -0
  11. package/dist/build.js.map +1 -0
  12. package/dist/dev.d.ts +9 -0
  13. package/dist/dev.d.ts.map +1 -0
  14. package/dist/dev.js +28 -0
  15. package/dist/dev.js.map +1 -0
  16. package/dist/index.d.ts +6 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +6 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/prerequisites.d.ts +4 -0
  21. package/dist/prerequisites.d.ts.map +1 -0
  22. package/dist/prerequisites.js +12 -0
  23. package/dist/prerequisites.js.map +1 -0
  24. package/dist/project.d.ts +48 -0
  25. package/dist/project.d.ts.map +1 -0
  26. package/dist/project.js +243 -0
  27. package/dist/project.js.map +1 -0
  28. package/dist/target-triple.d.ts +3 -0
  29. package/dist/target-triple.d.ts.map +1 -0
  30. package/dist/target-triple.js +10 -0
  31. package/dist/target-triple.js.map +1 -0
  32. package/dist/tauri-cli.d.ts +9 -0
  33. package/dist/tauri-cli.d.ts.map +1 -0
  34. package/dist/tauri-cli.js +42 -0
  35. package/dist/tauri-cli.js.map +1 -0
  36. package/package.json +45 -3
  37. package/rust/Cargo.toml +26 -0
  38. package/rust/src/control.rs +104 -0
  39. package/rust/src/device_key.rs +282 -0
  40. package/rust/src/dialog.rs +22 -0
  41. package/rust/src/host.rs +338 -0
  42. package/rust/src/keystore.rs +442 -0
  43. package/rust/src/lib.rs +20 -0
  44. package/rust/src/secret_store.rs +42 -0
  45. package/rust/src/session.rs +100 -0
  46. package/rust/src/vault.rs +133 -0
@@ -0,0 +1,338 @@
1
+ use std::io::{BufRead, BufReader, Write};
2
+ use std::path::{Path, PathBuf};
3
+ use std::process::{Command, Stdio};
4
+ use std::sync::{Arc, Condvar, Mutex};
5
+ use std::time::Duration;
6
+
7
+ use serde_json::{Value, json};
8
+ use tauri::path::BaseDirectory;
9
+ use tauri::{AppHandle, Manager, State};
10
+ use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
11
+
12
+ use crate::control::{self, home, socket_path};
13
+ use crate::dialog;
14
+
15
+ const SIDECAR: &str = "parlor-host";
16
+ const SIGNATURE: &str = "parlor-host.sig";
17
+ const ENSURE_WAIT: Duration = Duration::from_secs(20);
18
+
19
+ #[derive(Clone, Default)]
20
+ pub struct EnsureGate(Arc<(Mutex<bool>, Condvar)>);
21
+
22
+ impl EnsureGate {
23
+ fn open(&self) {
24
+ let (done, signal) = &*self.0;
25
+ *done.lock().unwrap() = true;
26
+ signal.notify_all();
27
+ }
28
+
29
+ fn wait(&self, timeout: Duration) {
30
+ let (done, signal) = &*self.0;
31
+ let guard = done.lock().unwrap();
32
+ let _ = signal
33
+ .wait_timeout_while(guard, timeout, |finished| !*finished)
34
+ .unwrap();
35
+ }
36
+ }
37
+
38
+ pub fn log_path(home: &Path, app_id: &str) -> PathBuf {
39
+ home.join("Library")
40
+ .join("Logs")
41
+ .join(app_id)
42
+ .join("parlor-host.log")
43
+ }
44
+
45
+ pub fn append_line(file: &Path, line: &str) -> std::io::Result<()> {
46
+ if let Some(parent) = file.parent() {
47
+ std::fs::create_dir_all(parent)?;
48
+ }
49
+ let mut out = std::fs::OpenOptions::new()
50
+ .create(true)
51
+ .append(true)
52
+ .open(file)?;
53
+ writeln!(out, "{}", line.trim_end())
54
+ }
55
+
56
+ pub fn is_ensure_line(line: &str) -> bool {
57
+ serde_json::from_str::<Value>(line)
58
+ .ok()
59
+ .and_then(|value| {
60
+ value
61
+ .get("event")
62
+ .and_then(Value::as_str)
63
+ .map(|e| e == "ensure")
64
+ })
65
+ .unwrap_or(false)
66
+ }
67
+
68
+ fn run_ensure(binary: &Path, signature: &Path, app_id: &str, log: &Path) {
69
+ let spawned = Command::new(binary)
70
+ .args(["daemon", "--ensure", "--app", app_id, "--binary"])
71
+ .arg(binary)
72
+ .arg("--signature")
73
+ .arg(signature)
74
+ .stdin(Stdio::null())
75
+ .stdout(Stdio::piped())
76
+ .stderr(Stdio::null())
77
+ .spawn();
78
+ let mut child = match spawned {
79
+ Ok(child) => child,
80
+ Err(error) => {
81
+ let failed = json!({"event": "ensure_failed", "error": error.to_string()});
82
+ let _ = append_line(log, &failed.to_string());
83
+ return;
84
+ }
85
+ };
86
+ let mut recorded = false;
87
+ if let Some(stdout) = child.stdout.take() {
88
+ for line in BufReader::new(stdout).lines().map_while(Result::ok) {
89
+ if is_ensure_line(&line) {
90
+ let _ = append_line(log, &line);
91
+ recorded = true;
92
+ break;
93
+ }
94
+ }
95
+ }
96
+ let status = child.wait();
97
+ if !recorded {
98
+ let failed = json!({"event": "ensure_failed", "status": format!("{status:?}")});
99
+ let _ = append_line(log, &failed.to_string());
100
+ }
101
+ }
102
+
103
+ fn signature_path(app: &AppHandle, sidecar: &Path) -> PathBuf {
104
+ let beside = sidecar.with_file_name(SIGNATURE);
105
+ match app.path().resolve(SIGNATURE, BaseDirectory::Resource) {
106
+ Ok(resolved) if resolved.exists() => resolved,
107
+ _ if beside.exists() => beside,
108
+ Ok(resolved) => resolved,
109
+ Err(_) => beside,
110
+ }
111
+ }
112
+
113
+ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
114
+ let gate = EnsureGate::default();
115
+ app.manage(gate.clone());
116
+ let handle = app.handle().clone();
117
+ let app_id = app.config().identifier.clone();
118
+ std::thread::spawn(move || {
119
+ let outcome = (|| -> Result<(), String> {
120
+ let sidecar = std::env::current_exe()
121
+ .map_err(|e| e.to_string())?
122
+ .with_file_name(SIDECAR);
123
+ let signature = signature_path(&handle, &sidecar);
124
+ let log = log_path(&home()?, &app_id);
125
+ run_ensure(&sidecar, &signature, &app_id, &log);
126
+ Ok(())
127
+ })();
128
+ if let Err(error) = outcome {
129
+ eprintln!("parlor-host ensure: {error}");
130
+ }
131
+ gate.open();
132
+ });
133
+ Ok(())
134
+ }
135
+
136
+ pub fn stopped() -> Value {
137
+ json!({"state": "stopped"})
138
+ }
139
+
140
+ pub fn status_of(reply: &Value) -> Value {
141
+ reply
142
+ .get("status")
143
+ .filter(|status| status.get("state").and_then(Value::as_str) == Some("running"))
144
+ .cloned()
145
+ .unwrap_or_else(stopped)
146
+ }
147
+
148
+ pub fn finish_result(connect: bool, reply: &Result<Value, String>) -> Value {
149
+ match reply {
150
+ Ok(_) if connect => json!({"connected": true}),
151
+ Ok(_) => json!({"connected": false, "reason": "declined_locally"}),
152
+ Err(error) if error == "declined_locally" => {
153
+ json!({"connected": false, "reason": "declined_locally"})
154
+ }
155
+ Err(_) => json!({"connected": false, "reason": "failed"}),
156
+ }
157
+ }
158
+
159
+ async fn blocking<T: Send + 'static>(
160
+ work: impl FnOnce() -> T + Send + 'static,
161
+ ) -> Result<T, String> {
162
+ tauri::async_runtime::spawn_blocking(work)
163
+ .await
164
+ .map_err(|e| e.to_string())
165
+ }
166
+
167
+ #[tauri::command]
168
+ pub async fn parlor_host_bundled() -> bool {
169
+ true
170
+ }
171
+
172
+ #[tauri::command]
173
+ pub async fn parlor_host_status(gate: State<'_, EnsureGate>) -> Result<Value, String> {
174
+ let gate = gate.inner().clone();
175
+ blocking(move || {
176
+ gate.wait(ENSURE_WAIT);
177
+ let reply =
178
+ home().and_then(|home| control::request(&socket_path(&home), json!({"op": "status"})));
179
+ reply
180
+ .map(|reply| status_of(&reply))
181
+ .unwrap_or_else(|_| stopped())
182
+ })
183
+ .await
184
+ }
185
+
186
+ #[tauri::command]
187
+ pub async fn parlor_host_begin(
188
+ gate: State<'_, EnsureGate>,
189
+ app_origin: String,
190
+ ) -> Result<Value, String> {
191
+ let gate = gate.inner().clone();
192
+ blocking(move || {
193
+ gate.wait(ENSURE_WAIT);
194
+ let reply = control::request(
195
+ &socket_path(&home()?),
196
+ json!({"op": "begin_local_pairing", "appOrigin": app_origin}),
197
+ )?;
198
+ reply
199
+ .get("pairing")
200
+ .cloned()
201
+ .ok_or_else(|| "Parlor Host sent no pairing.".to_string())
202
+ })
203
+ .await?
204
+ }
205
+
206
+ fn text_of<'a>(reply: &'a Value, field: &str) -> Result<&'a str, String> {
207
+ reply
208
+ .get(field)
209
+ .and_then(Value::as_str)
210
+ .ok_or_else(|| format!("Parlor Host sent no {field}."))
211
+ }
212
+
213
+ #[tauri::command]
214
+ pub async fn parlor_host_finish(
215
+ app: AppHandle,
216
+ app_origin: String,
217
+ code: String,
218
+ device: Option<Value>,
219
+ ) -> Result<Value, String> {
220
+ let socket = socket_path(&home()?);
221
+ let finishing = {
222
+ let (socket, app_origin) = (socket.clone(), app_origin.clone());
223
+ blocking(move || {
224
+ control::request(
225
+ &socket,
226
+ json!({"op": "finish_local_pairing", "appOrigin": app_origin, "code": code}),
227
+ )
228
+ })
229
+ .await?
230
+ };
231
+ let reply = match finishing {
232
+ Ok(reply) => reply,
233
+ Err(_) => return Ok(json!({"connected": false, "reason": "failed"})),
234
+ };
235
+ let (account, matching_code) =
236
+ match (text_of(&reply, "account"), text_of(&reply, "matchingCode")) {
237
+ (Ok(account), Ok(code)) => (account.to_string(), code.to_string()),
238
+ _ => return Ok(json!({"connected": false, "reason": "failed"})),
239
+ };
240
+ let app_name = app
241
+ .config()
242
+ .product_name
243
+ .clone()
244
+ .unwrap_or_else(|| app.config().identifier.clone());
245
+ let connect = {
246
+ let (title, message) = (
247
+ dialog::title(&app_name),
248
+ dialog::message(&app_name, &account, &matching_code),
249
+ );
250
+ let app = app.clone();
251
+ blocking(move || {
252
+ app.dialog()
253
+ .message(message)
254
+ .title(title)
255
+ .buttons(MessageDialogButtons::OkCancelCustom(
256
+ "Connect".to_string(),
257
+ "Cancel".to_string(),
258
+ ))
259
+ .blocking_show()
260
+ })
261
+ .await?
262
+ };
263
+ let mut confirmation = json!({
264
+ "op": "confirm_local_pairing",
265
+ "appOrigin": app_origin,
266
+ "connect": connect,
267
+ });
268
+ if let Some(device) = device {
269
+ confirmation["device"] = device;
270
+ }
271
+ let confirmed = blocking(move || control::request(&socket, confirmation)).await?;
272
+ Ok(finish_result(connect, &confirmed))
273
+ }
274
+
275
+ #[cfg(test)]
276
+ mod tests {
277
+ use super::*;
278
+
279
+ #[test]
280
+ fn the_log_lives_under_the_apps_library_logs_directory() {
281
+ assert_eq!(
282
+ log_path(Path::new("/Users/a"), "dev.parlor.hello"),
283
+ PathBuf::from("/Users/a/Library/Logs/dev.parlor.hello/parlor-host.log")
284
+ );
285
+ }
286
+
287
+ #[test]
288
+ fn append_line_creates_the_directory_and_keeps_earlier_lines() {
289
+ let dir = std::env::temp_dir().join(format!("ps-log-{}", std::process::id()));
290
+ let file = log_path(&dir, "dev.parlor.hello");
291
+ append_line(&file, "{\"event\":\"ensure\",\"outcome\":\"using\"}\n").unwrap();
292
+ append_line(&file, "{\"event\":\"ensure\",\"outcome\":\"installed\"}").unwrap();
293
+ let text = std::fs::read_to_string(&file).unwrap();
294
+ std::fs::remove_dir_all(&dir).ok();
295
+ assert_eq!(
296
+ text,
297
+ "{\"event\":\"ensure\",\"outcome\":\"using\"}\n{\"event\":\"ensure\",\"outcome\":\"installed\"}\n"
298
+ );
299
+ }
300
+
301
+ #[test]
302
+ fn only_the_ensure_event_is_logged() {
303
+ assert!(is_ensure_line(
304
+ "{\"event\":\"ensure\",\"outcome\":\"using\",\"ms\":3}"
305
+ ));
306
+ assert!(!is_ensure_line("{\"event\":\"daemon_started\"}"));
307
+ assert!(!is_ensure_line("starting"));
308
+ }
309
+
310
+ #[test]
311
+ fn status_maps_a_running_host_and_anything_else_to_stopped() {
312
+ let running = json!({"ok": true, "status": {"state": "running", "version": "1.0.0", "apps": ["http://a"]}});
313
+ assert_eq!(status_of(&running)["version"], "1.0.0");
314
+ assert_eq!(status_of(&json!({"ok": true})), stopped());
315
+ assert_eq!(
316
+ status_of(&json!({"status": {"state": "stopped"}})),
317
+ stopped()
318
+ );
319
+ }
320
+
321
+ #[test]
322
+ fn finish_maps_the_control_reply_to_the_capability_result() {
323
+ let ok = Ok(json!({"ok": true}));
324
+ assert_eq!(finish_result(true, &ok), json!({"connected": true}));
325
+ assert_eq!(
326
+ finish_result(false, &ok),
327
+ json!({"connected": false, "reason": "declined_locally"})
328
+ );
329
+ assert_eq!(
330
+ finish_result(false, &Err("declined_locally".to_string())),
331
+ json!({"connected": false, "reason": "declined_locally"})
332
+ );
333
+ assert_eq!(
334
+ finish_result(true, &Err("boom".to_string())),
335
+ json!({"connected": false, "reason": "failed"})
336
+ );
337
+ }
338
+ }