app
Chip Fetcher (cf)
Public Made by Adomby adom
Crawls the web on your behalf to gather everything about the chips you care about: datasheets, symbols, footprints, 3D models, stock and pricing, straight from the manufacturer first, into your EDA tool. Say "cf" to your AI.
master
John Lauer
1.3.10: newbie panel (say strip + LEDs + getting started) is one closable block with a Run-a-demo-fetch button; empty libraries auto-seed 3 sample wiki bundles; detail pane auto-selects a sample; cf sweep (README, page title, sites pack); sites skills get Parent-skill lines + rich triggers; dashboard-flow playbook is pup-always
cae1bf5
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
//! Background jobs with a live console, for the settings modal's Install /
//! Update rows (settings-layout: "silent multi-minute buttons are forbidden").
//! `POST /api/jobs {action}` starts one; `GET /api/jobs/<id>` streams its lines.
use serde_json::{json, Value};
use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
pub struct Job {
pub id: u64,
pub action: String,
pub state: String, // running | done | failed
pub exit_code: Option<i32>,
pub lines: Vec<String>,
pub started: u64,
pub ended: Option<u64>,
}
static JOBS: Mutex<Option<HashMap<u64, Arc<Mutex<Job>>>>> = Mutex::new(None);
static NEXT_ID: Mutex<u64> = Mutex::new(1);
fn now() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
fn registry() -> std::sync::MutexGuard<'static, Option<HashMap<u64, Arc<Mutex<Job>>>>> {
let mut g = JOBS.lock().unwrap();
if g.is_none() {
*g = Some(HashMap::new());
}
g
}
/// The shell command for an action, or None when the action is unknown.
pub fn command_for(action: &str, body: &Value) -> Option<(String, Vec<String>)> {
let sh = |script: &str| Some(("sh".to_string(), vec!["-c".to_string(), format!("export PATH=\"$HOME/.local/bin:$PATH\"; {script}")]));
match action {
"install_tools" => sh("adom-chip-fetcher doctor --fix 2>&1"),
"install_skill" => sh("adom-chip-fetcher install 2>&1"),
"refresh_status" => sh("adom-chip-fetcher doctor 2>&1; true"),
"demo_fetch" => sh("adom-chip-fetcher demo-fetch 2>&1"),
"import_logins" => {
let hosts = body
.get("hostFilter")
.cloned()
.unwrap_or_else(|| json!(crate::vault::DEFAULT_VENDOR_HOSTS));
let args = json!({"hostFilter": hosts}).to_string();
let thread = crate::bridge::ai_thread();
let script = format!(
"echo 'Scanning the browser profiles on your desktop (read only)...'; \
adom-bridge --ai-thread {t} pup_import_scan '{{}}' 2>&1; echo; \
echo 'Importing vendor logins into pup'\"'\"'s OS-keychain vault. Approve the Adom Bridge consent dialog and the Windows UAC prompt on your desktop.'; \
adom-bridge --ai-thread {t} pup_import_browser_logins '{a}' 2>&1",
t = shell_quote(&thread),
a = args.replace('\'', "'\\''")
);
sh(&script)
}
_ => None,
}
}
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn start(action: &str, body: &Value) -> Result<u64, String> {
let (prog, args) = command_for(action, body).ok_or_else(|| format!("unknown job action '{action}'"))?;
let id = {
let mut n = NEXT_ID.lock().unwrap();
let id = *n;
*n += 1;
id
};
let job = Arc::new(Mutex::new(Job {
id,
action: action.to_string(),
state: "running".into(),
exit_code: None,
lines: vec![format!("$ {}", args.last().cloned().unwrap_or_default())],
started: now(),
ended: None,
}));
registry().as_mut().unwrap().insert(id, job.clone());
crate::dock::console_push("info", &format!("job {id} {action} started"), "jobs");
std::thread::spawn(move || {
let child = Command::new(&prog).args(&args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn();
let mut child = match child {
Ok(c) => c,
Err(e) => {
let mut j = job.lock().unwrap();
j.lines.push(format!("ERROR: could not start: {e}"));
j.state = "failed".into();
j.ended = Some(now());
return;
}
};
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let j2 = job.clone();
let t_err = std::thread::spawn(move || {
if let Some(e) = stderr {
for line in BufReader::new(e).lines().map_while(Result::ok) {
let mut j = j2.lock().unwrap();
j.lines.push(line);
if j.lines.len() > 2000 {
j.lines.remove(0);
}
}
}
});
if let Some(o) = stdout {
for line in BufReader::new(o).lines().map_while(Result::ok) {
let mut j = job.lock().unwrap();
j.lines.push(line);
if j.lines.len() > 2000 {
j.lines.remove(0);
}
}
}
let _ = t_err.join();
let status = child.wait();
let mut j = job.lock().unwrap();
let code = status.ok().and_then(|s| s.code());
j.exit_code = code;
j.state = if code == Some(0) { "done".into() } else { "failed".into() };
j.ended = Some(now());
j.lines.push(format!("[exit {}]", code.map(|c| c.to_string()).unwrap_or_else(|| "?".into())));
crate::dock::console_push("info", &format!("job {} {} {}", j.id, j.action, j.state), "jobs");
});
Ok(id)
}
pub fn get_json(id: u64) -> Option<String> {
let reg = registry();
let job = reg.as_ref().unwrap().get(&id)?.clone();
let j = job.lock().unwrap();
Some(
json!({
"ok": true, "id": j.id, "action": j.action, "state": j.state, "exit_code": j.exit_code,
"lines": j.lines, "started": j.started, "ended": j.ended,
})
.to_string(),
)
}
pub fn list_json() -> String {
let reg = registry();
let items: Vec<Value> = reg
.as_ref()
.unwrap()
.values()
.map(|j| {
let j = j.lock().unwrap();
json!({"id": j.id, "action": j.action, "state": j.state, "started": j.started, "ended": j.ended})
})
.collect();
json!({"ok": true, "jobs": items}).to_string()
}