use serde::Serialize; use serde_json::Value; use std::io::Write; use std::process::{Command, Stdio}; const NODE_ALIAS: &str = "xx-gz-001"; const CHANNEL_ROOT: &str = "/var/lib/guanghu/education-data/channels/GH-EDU-CHANNEL-XX-001"; #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ChannelSnapshot { node: Value, fifth_domain_link: Value, module_registry: Value, update_authority: Value, channel: Value, code_channel: Value, } fn ssh_output(remote_command: &str) -> Result { let output = Command::new("/usr/bin/ssh") .args([ "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", NODE_ALIAS, remote_command, ]) .output() .map_err(|error| format!("SSH_LAUNCH_FAILED:{error}"))?; if !output.status.success() { return Err(format!( "SSH_REMOTE_FAILED:{}", String::from_utf8_lossy(&output.stderr).trim() )); } String::from_utf8(output.stdout).map_err(|error| format!("SSH_OUTPUT_INVALID_UTF8:{error}")) } fn read_remote_json(path: &str) -> Result { let payload = ssh_output(&format!("cat {path}"))?; serde_json::from_str(&payload).map_err(|error| format!("REMOTE_JSON_INVALID:{path}:{error}")) } #[tauri::command] fn connect_channel() -> Result { let code_version = ssh_output("curl -fsS http://127.0.0.1:3901/api/v1/version")?; let repository_search = ssh_output("curl -fsS 'http://127.0.0.1:3901/api/v1/repos/search?limit=20'")?; Ok(ChannelSnapshot { node: read_remote_json("/var/lib/guanghu/protocol/node.json")?, fifth_domain_link: read_remote_json( "/var/lib/guanghu/receipts/XX-GZ-001/FIFTH-DOMAIN-LINK-CURRENT.json", )?, module_registry: read_remote_json("/opt/guanghu/protocol/current/module-registry.json")?, update_authority: read_remote_json("/opt/guanghu/protocol/current/update-authority.json")?, channel: read_remote_json("/opt/guanghu/protocol/current/xiaoxin-channel.json")?, code_channel: serde_json::json!({ "version": serde_json::from_str::(&code_version) .map_err(|error| format!("CODE_CHANNEL_VERSION_INVALID:{error}"))?, "repositories": serde_json::from_str::(&repository_search) .map_err(|error| format!("CODE_CHANNEL_REPOSITORIES_INVALID:{error}"))? }), }) } fn channel_file(kind: &str) -> Result<(&'static str, &'static str), String> { match kind { "document" => Ok(("teacher-training-plan.md", "GH-EDU-OFFICE-DOC-001")), "sheet" => Ok(("teacher-training-data.csv", "GH-EDU-OFFICE-SHEET-001")), _ => Err(format!("UNREGISTERED_CHANNEL_FILE:{kind}")), } } #[tauri::command] fn read_channel_file(kind: String) -> Result { let (filename, _) = channel_file(&kind)?; ssh_output(&format!( "path='{CHANNEL_ROOT}/documents/{filename}'; if [ -f \"$path\" ]; then cat \"$path\"; fi" )) } #[tauri::command] fn save_channel_file(kind: String, content: String) -> Result { let (filename, module_id) = channel_file(&kind)?; let path = format!("{CHANNEL_ROOT}/documents/{filename}"); let receipt = format!("{CHANNEL_ROOT}/receipts/{kind}-CURRENT.json"); let remote_command = format!( "set -eu; \ install -d -m 700 '{CHANNEL_ROOT}/documents' '{CHANNEL_ROOT}/receipts'; \ path='{path}'; tmp=\"${{path}}.tmp.$$\"; umask 077; cat > \"$tmp\"; mv \"$tmp\" \"$path\"; \ sha=$(sha256sum \"$path\" | awk '{{print $1}}'); \ now=$(date -u +%Y-%m-%dT%H:%M:%SZ); \ receipt='{receipt}'; rtmp=\"${{receipt}}.tmp.$$\"; \ printf '{{\"schema\":\"guanghu.channel-write-receipt/v1\",\"channelId\":\"GH-EDU-CHANNEL-XX-001\",\"nodeId\":\"XX-GZ-001\",\"moduleId\":\"{module_id}\",\"kind\":\"{kind}\",\"sha256\":\"%s\",\"writtenAt\":\"%s\",\"state\":\"PASS_100\"}}\\n' \"$sha\" \"$now\" > \"$rtmp\"; \ mv \"$rtmp\" \"$receipt\"; cat \"$receipt\"" ); let mut child = Command::new("/usr/bin/ssh") .args([ "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", NODE_ALIAS, &remote_command, ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|error| format!("SSH_SAVE_LAUNCH_FAILED:{error}"))?; child .stdin .take() .ok_or_else(|| "SSH_SAVE_STDIN_UNAVAILABLE".to_string())? .write_all(content.as_bytes()) .map_err(|error| format!("SSH_SAVE_WRITE_FAILED:{error}"))?; let output = child .wait_with_output() .map_err(|error| format!("SSH_SAVE_WAIT_FAILED:{error}"))?; if !output.status.success() { return Err(format!( "SSH_SAVE_REMOTE_FAILED:{}", String::from_utf8_lossy(&output.stderr).trim() )); } serde_json::from_slice(&output.stdout) .map_err(|error| format!("SSH_SAVE_RECEIPT_INVALID:{error}")) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ connect_channel, read_channel_file, save_channel_file ]) .run(tauri::generate_context!()) .expect("failed to run Guanghu Education Subdomain OS"); }