@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.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/assets/icons/128x128.png +0 -0
- package/assets/icons/128x128@2x.png +0 -0
- package/assets/icons/32x32.png +0 -0
- package/assets/icons/icon.icns +0 -0
- package/assets/icons/icon.ico +0 -0
- package/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +18 -0
- package/dist/build.js.map +1 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +28 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/prerequisites.d.ts +4 -0
- package/dist/prerequisites.d.ts.map +1 -0
- package/dist/prerequisites.js +12 -0
- package/dist/prerequisites.js.map +1 -0
- package/dist/project.d.ts +48 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +243 -0
- package/dist/project.js.map +1 -0
- package/dist/target-triple.d.ts +3 -0
- package/dist/target-triple.d.ts.map +1 -0
- package/dist/target-triple.js +10 -0
- package/dist/target-triple.js.map +1 -0
- package/dist/tauri-cli.d.ts +9 -0
- package/dist/tauri-cli.d.ts.map +1 -0
- package/dist/tauri-cli.js +42 -0
- package/dist/tauri-cli.js.map +1 -0
- package/package.json +45 -3
- package/rust/Cargo.toml +26 -0
- package/rust/src/control.rs +104 -0
- package/rust/src/device_key.rs +282 -0
- package/rust/src/dialog.rs +22 -0
- package/rust/src/host.rs +338 -0
- package/rust/src/keystore.rs +442 -0
- package/rust/src/lib.rs +20 -0
- package/rust/src/secret_store.rs +42 -0
- package/rust/src/session.rs +100 -0
- package/rust/src/vault.rs +133 -0
package/rust/src/host.rs
ADDED
|
@@ -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
|
+
}
|