mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-22 15:14:11 -04:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63b589318a |
No files matched your search
@@ -14,6 +14,8 @@
|
||||
//! URL query flags: `?cpu` forces the CPU canvas path (same as `PHOTOCRAFT_CPU_CANVAS=1`);
|
||||
//! `?webgl` forces the WebGL2 backend instead of WebGPU.
|
||||
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web;
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Last-resort crash guard (AGENTS.md, Never crash).
|
||||
//!
|
||||
//! Code must return errors, never panic. This is the safety net for a panic that escapes anyway:
|
||||
//! the hook logs it, and [`guard`] turns a panic during file import/export into an error the UI
|
||||
//! shows, so the open documents stay in place. Commands get the same treatment in
|
||||
//! `Session::execute`.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||
|
||||
/// Log every panic (with its location) before the default hook prints it.
|
||||
pub fn install_hook() {
|
||||
let default = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
log::error!("PhotoCraft internal error: {info}");
|
||||
default(info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Run `f`; a panic inside it becomes an `Err` naming `what`.
|
||||
pub fn guard<T>(what: &str, f: impl FnOnce() -> Result<T, String>) -> Result<T, String> {
|
||||
catch_unwind(AssertUnwindSafe(f)).unwrap_or_else(|_| Err(format!("{what} failed with an internal error (logged); your open documents are unchanged")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn guard_turns_a_panic_into_an_error() {
|
||||
let r: Result<(), String> = guard("Open", || panic!("boom"));
|
||||
assert!(r.unwrap_err().contains("Open failed"));
|
||||
assert_eq!(guard("Open", || Ok(3)), Ok(3));
|
||||
assert_eq!(guard::<()>("Open", || Err("bad file".into())), Err("bad file".into()));
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,10 @@
|
||||
// Release builds on Windows are GUI-subsystem apps, so launching from the Start Menu or Explorer
|
||||
// doesn't open a console window. (`--version` output then only shows when redirected.)
|
||||
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
mod control_server;
|
||||
mod crash_guard;
|
||||
mod services;
|
||||
|
||||
use photocraft_engine::Session;
|
||||
@@ -30,6 +32,7 @@ fn app_icon() -> egui::IconData {
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
crash_guard::install_hook();
|
||||
let mut control_port: Option<u16> = std::env::var("PHOTOCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
let mut args = std::env::args().skip(1);
|
||||
|
||||
@@ -54,13 +54,15 @@ pub fn native() -> Services {
|
||||
let savers: Rc<RefCell<HashMap<u64, Autosaver>>> = Rc::default();
|
||||
let savers2 = savers.clone();
|
||||
Services {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| photocraft_io::import(name, bytes).map(|r| r.document).map_err(|e| e.to_string()))),
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| {
|
||||
crate::crash_guard::guard("Open", || photocraft_io::import(name, bytes).map(|r| r.document).map_err(|e| e.to_string()))
|
||||
})),
|
||||
export: Some(Box::new(|doc: &Document, path: &str, settings: &photocraft_ui_egui::ExportSettings| {
|
||||
let mut opts = photocraft_io::ExportOptions::default();
|
||||
if let Some(q) = settings.jpeg_quality {
|
||||
opts.encode.jpeg_quality = q;
|
||||
}
|
||||
photocraft_io::export(doc, path, &opts).map(|r| r.bytes).map_err(|e| e.to_string())
|
||||
crate::crash_guard::guard("Export", || photocraft_io::export(doc, path, &opts).map(|r| r.bytes).map_err(|e| e.to_string()))
|
||||
})),
|
||||
pick_open: Some(Box::new(|| {
|
||||
let path = rfd::FileDialog::new().add_filter("Images", IMAGE_EXTS).pick_file()?;
|
||||
@@ -168,7 +170,7 @@ pub fn import_flat(name: &str, bytes: &[u8]) -> Result<Document, String> {
|
||||
}
|
||||
let mut layer = Layer::raster("Background", doc.pixel_format());
|
||||
let data = conv.to_normalized();
|
||||
layer.surface_mut().unwrap().write_region(Rect::from_xywh(0, 0, w, h), &data);
|
||||
layer.surface_mut().ok_or("new raster layer has no pixels")?.write_region(Rect::from_xywh(0, 0, w, h), &data);
|
||||
doc.layers.push(layer);
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
@@ -294,7 +294,11 @@ impl Session {
|
||||
// Pixel commands follow the Channels panel target unless the caller names one.
|
||||
let run_params = channel_cmds::inject_target(self, id, commands::inject_kind(id, params.clone()));
|
||||
self.color_restrict = channel_cmds::color_restriction(self, id, &run_params);
|
||||
let r = (spec.run)(self, &run_params);
|
||||
// Last-resort guard (AGENTS.md, Never crash): a command that panics anyway fails with an
|
||||
// error instead of taking the app down. `edit` only commits a document after its closure
|
||||
// returns, so the documents are unchanged.
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (spec.run)(self, &run_params)))
|
||||
.unwrap_or_else(|_| Err(EngineError::Other(format!("`{id}` failed with an internal error (logged); the document is unchanged"))));
|
||||
self.coalesce_request = None;
|
||||
self.color_restrict = None;
|
||||
let r = r?;
|
||||
|
||||
Reference in new issue
Block a user