Let the console add and remove printers; keep them in a local SQLite store
The only way to give a gateway a printer was to edit printers.toml and restart it. A printer added from the console now arrives as a provision_printer message, is checked by actually connecting to it (so a mistyped access code is reported to the operator, not discovered later), and only then is saved to the gateway's own SQLite file and joined to the running fleet - no restart. remove_printer undoes it. Both are answered with a command_result carrying the request id. The access code exists only on the gateway: the cloud relays it once and keeps nothing, which is the platform's rule for printer credentials. The store file is created owner-only; the message type's Debug and the fleet entry's Debug redact the credential; and an unparseable message from the cloud is no longer logged with its payload, because the message that fails to parse is exactly the one carrying an access code. The cloud may not choose a certificate path or serial port on the gateway's disk, so provision_printer cannot carry them. Startup now reads printers.toml (strict, as before; a missing file is fine) and the store (a bad stored entry is skipped, not fatal), so the gateway comes up with its fleet with the API unreachable. Discovery treats stored printers as already configured. rusqlite (MIT, bundled SQLite) passes cargo deny.
This commit is contained in:
+68
-3
@@ -28,7 +28,7 @@
|
||||
|
||||
mod download;
|
||||
mod webrtc;
|
||||
mod protocol;
|
||||
pub mod protocol;
|
||||
|
||||
pub use protocol::{ClientMessage, DiscoveredPrinter, PrinterCommand, PrinterTelemetry, ServerMessage};
|
||||
|
||||
@@ -42,6 +42,9 @@ use tokio_tungstenite::tungstenite::handshake::client::{Request, generate_key};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::provision;
|
||||
use crate::store::Store;
|
||||
use protocol::ProvisionPrinter;
|
||||
use crate::printer::{GenericPrinter, PrinterHandle};
|
||||
|
||||
/// The websocket, once connected. Named because `connect` has to hand it to
|
||||
@@ -139,6 +142,7 @@ pub fn channel() -> (
|
||||
pub async fn run(
|
||||
config: Config,
|
||||
fleet: SharedFleet,
|
||||
store: Option<Arc<Store>>,
|
||||
outbound: tokio::sync::mpsc::Sender<ClientMessage>,
|
||||
mut inbox: tokio::sync::mpsc::Receiver<ClientMessage>,
|
||||
) {
|
||||
@@ -156,7 +160,7 @@ pub async fn run(
|
||||
// would not distinguish a connection that lived for an hour
|
||||
// from one that failed to open.
|
||||
delay = RETRY_DELAY;
|
||||
if let Err(err) = serve(ws_stream, &config, &fleet, &outbound, &mut inbox).await {
|
||||
if let Err(err) = serve(ws_stream, &config, &fleet, store.as_ref(), &outbound, &mut inbox).await {
|
||||
warn!(?err, "uplink connection dropped, reconnecting in {delay:?}");
|
||||
}
|
||||
}
|
||||
@@ -200,6 +204,7 @@ async fn serve(
|
||||
ws_stream: WsStream,
|
||||
config: &Config,
|
||||
fleet: &SharedFleet,
|
||||
store: Option<&Arc<Store>>,
|
||||
outbound: &tokio::sync::mpsc::Sender<ClientMessage>,
|
||||
inbox: &mut tokio::sync::mpsc::Receiver<ClientMessage>,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -306,13 +311,25 @@ async fn serve(
|
||||
outbound.clone(),
|
||||
);
|
||||
}
|
||||
// Spawned: connecting to the new printer can take
|
||||
// many seconds, and awaiting it here would stall
|
||||
// heartbeats and telemetry for the whole farm.
|
||||
Ok(ServerMessage::ProvisionPrinter { request_id, printer }) => {
|
||||
spawn_provision(fleet.clone(), store.cloned(), request_id, Some(printer), outbound.clone());
|
||||
}
|
||||
Ok(ServerMessage::RemovePrinter { request_id, printer_id }) => {
|
||||
spawn_remove(fleet.clone(), store.cloned(), request_id, printer_id, outbound.clone());
|
||||
}
|
||||
Ok(other) => info!(?other, "received from cloud"),
|
||||
// Loudly. This used to be an `if let Ok(..)` with
|
||||
// no else, which is the same silent-drop bug that
|
||||
// hid a real `print.push_status` failure for
|
||||
// weeks - and here it would mean commands
|
||||
// vanishing with the cloud never being told.
|
||||
Err(err) => warn!(%err, payload = %text, "unparseable message from cloud, ignoring"),
|
||||
// The payload is NOT logged. A `provision_printer` message
|
||||
// carries an access code, and one that failed to parse is
|
||||
// exactly the message that would end up here.
|
||||
Err(err) => warn!(%err, bytes = text.len(), "unparseable message from cloud, ignoring"),
|
||||
}
|
||||
}
|
||||
Some(Ok(Message::Close(_))) | None => return Ok(()),
|
||||
@@ -456,6 +473,54 @@ fn spawn_long_command(
|
||||
});
|
||||
}
|
||||
|
||||
/// Adds a printer off the connection's task and always reports how it went:
|
||||
/// the console is holding a form open waiting for exactly this.
|
||||
fn spawn_provision(
|
||||
fleet: SharedFleet,
|
||||
store: Option<Arc<Store>>,
|
||||
request_id: String,
|
||||
request: Option<ProvisionPrinter>,
|
||||
outbound: tokio::sync::mpsc::Sender<ClientMessage>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let result = match (store, request) {
|
||||
(Some(store), Some(request)) => provision::add(&fleet, &store, request).await,
|
||||
(None, _) => Err("this gateway has no local store, so it cannot add printers".to_string()),
|
||||
(_, None) => Err("no printer in the request".to_string()),
|
||||
};
|
||||
report(request_id, result, &outbound).await;
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_remove(
|
||||
fleet: SharedFleet,
|
||||
store: Option<Arc<Store>>,
|
||||
request_id: String,
|
||||
printer_id: String,
|
||||
outbound: tokio::sync::mpsc::Sender<ClientMessage>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let result = match store {
|
||||
Some(store) => provision::remove(&fleet, &store, &printer_id).await,
|
||||
None => Err("this gateway has no local store".to_string()),
|
||||
};
|
||||
report(request_id, result, &outbound).await;
|
||||
});
|
||||
}
|
||||
|
||||
async fn report(request_id: String, result: Result<(), String>, outbound: &tokio::sync::mpsc::Sender<ClientMessage>) {
|
||||
let msg = match result {
|
||||
Ok(()) => ClientMessage::CommandResult { command_id: request_id, ok: true, error: None },
|
||||
Err(error) => {
|
||||
warn!(%error, "printer request failed");
|
||||
ClientMessage::CommandResult { command_id: request_id, ok: false, error: Some(error) }
|
||||
}
|
||||
};
|
||||
if outbound.send(msg).await.is_err() {
|
||||
warn!("uplink is shutting down; the result was not reported");
|
||||
}
|
||||
}
|
||||
|
||||
/// Answers one WebRTC offer, off this connection's task.
|
||||
///
|
||||
/// Always answers, success or failure: the cloud is holding a browser's
|
||||
|
||||
Reference in New Issue
Block a user