Task {
30 | /// Performs the asynchronous USB device flashing.
31 | pub async fn process(mut self, buf: &mut [u8]) -> anyhow::Result<()> {
32 | self.copy(buf).await.context("failed to copy ISO")?;
33 |
34 | if self.check {
35 | self.seek().await.context("failed to seek devices to start")?;
36 | self.validate(buf).await.context("validation error")?;
37 | }
38 |
39 | for (_, pb) in self.state.values_mut() {
40 | pb.finish();
41 | }
42 |
43 | Ok(())
44 | }
45 |
46 | pub fn subscribe(&mut self, file: File, device: P::Device, progress: P) -> &mut Self {
47 | let entity = self.writer.insert(file);
48 | self.state.insert(entity, (device, progress));
49 | self
50 | }
51 |
52 | async fn copy(&mut self, buf: &mut [u8]) -> anyhow::Result<()> {
53 | let mut stream = self.writer.copy(&mut self.image, buf);
54 | let mut total = 0;
55 | let mut last = Instant::now();
56 | while let Some(event) = stream.next().await {
57 | match event {
58 | CopyEvent::Progress(written) => {
59 | total += written as u64;
60 | let now = Instant::now();
61 | if now.duration_since(last).as_millis() > self.millis_between as u128 {
62 | last = now;
63 | for (_, pb) in self.state.values_mut() {
64 | pb.set(total);
65 | }
66 | }
67 | }
68 | CopyEvent::Failure(entity, why) => {
69 | let (device, mut pb) = self.state.remove(&entity).expect("missing entity");
70 | pb.message(&device, "E", &format!("{}", why));
71 | pb.finish();
72 | }
73 | CopyEvent::SourceFailure(why) => {
74 | for (device, pb) in self.state.values_mut() {
75 | pb.message(device, "E", &format!("{}", why));
76 | pb.finish();
77 | }
78 |
79 | return Err(why).context("error reading from source");
80 | }
81 | CopyEvent::NoWriters => return Err(anyhow!("no writers left")),
82 | }
83 | }
84 |
85 | Ok(())
86 | }
87 |
88 | async fn seek(&mut self) -> anyhow::Result<()> {
89 | for (path, pb) in self.state.values_mut() {
90 | pb.set(0);
91 | pb.message(path, "S", "");
92 | }
93 |
94 | self.image.seek(SeekFrom::Start(0)).await?;
95 |
96 | let mut stream = self.writer.seek(SeekFrom::Start(0));
97 | while let Some((entity, why)) = stream.next().await {
98 | let (path, mut pb) = self.state.remove(&entity).expect("missing entity");
99 | pb.message(&path, "E", &format!("errored seeking to start: {}", why));
100 | pb.finish();
101 | }
102 |
103 | Ok(())
104 | }
105 |
106 | async fn validate(&mut self, buf: &mut [u8]) -> anyhow::Result<()> {
107 | for (path, pb) in self.state.values_mut() {
108 | pb.set(0);
109 | pb.message(path, "V", "");
110 | }
111 |
112 | let copy_bufs = &mut Vec::new();
113 | let mut total = 0;
114 | let mut stream = self.writer.validate(&mut self.image, buf, copy_bufs);
115 |
116 | while let Some(event) = stream.next().await {
117 | match event {
118 | ValidationEvent::Progress(written) => {
119 | total += written as u64;
120 | for (_, pb) in self.state.values_mut() {
121 | pb.set(total);
122 | }
123 | }
124 | ValidationEvent::Failure(entity, why) => {
125 | let (path, mut pb) = self.state.remove(&entity).expect("missing entity");
126 | pb.message(&path, "E", &format!("{}", why));
127 | pb.finish();
128 | }
129 | ValidationEvent::SourceFailure(why) => {
130 | for (path, pb) in self.state.values_mut() {
131 | pb.message(path, "E", &format!("error reading from source: {}", why));
132 | pb.finish();
133 | }
134 |
135 | return Err(why).context("error reading from source");
136 | }
137 | ValidationEvent::NoWriters => return Err(anyhow!("no writers left")),
138 | }
139 | }
140 |
141 | Ok(())
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/gtk/src/flash.rs:
--------------------------------------------------------------------------------
1 | use crate::app::events::FlashResult;
2 | use atomic::Atomic;
3 | use dbus::arg::{OwnedFd, RefArg, Variant};
4 | use dbus::blocking::{Connection, Proxy};
5 | use dbus_udisks2::DiskDevice;
6 | use futures::executor;
7 | use popsicle::{Progress, Task};
8 | use std::cell::Cell;
9 | use std::collections::HashMap;
10 | use std::fmt::{self, Debug, Display, Formatter};
11 | use std::fs::File;
12 | use std::os::unix::io::FromRawFd;
13 | use std::str;
14 | use std::sync::atomic::Ordering;
15 | use std::sync::{Arc, Mutex};
16 | use std::time::Duration;
17 |
18 | type UDisksOptions = HashMap<&'static str, Variant>>;
19 |
20 | #[derive(Clone, Copy, PartialEq)]
21 | pub enum FlashStatus {
22 | Inactive,
23 | Active,
24 | Killing,
25 | }
26 |
27 | unsafe impl bytemuck::NoUninit for FlashStatus {}
28 |
29 | pub struct FlashRequest {
30 | source: Option,
31 | destinations: Vec>,
32 | status: Arc>,
33 | progress: Arc>>,
34 | finished: Arc>>,
35 | }
36 |
37 | pub struct FlashTask {
38 | pub progress: Arc>>,
39 | pub previous: Arc>>,
40 | pub finished: Arc>>,
41 | }
42 |
43 | struct FlashProgress<'a> {
44 | request: &'a FlashRequest,
45 | id: usize,
46 | errors: &'a [Cell>],
47 | }
48 |
49 | #[derive(Clone, Debug)]
50 | pub struct FlashError {
51 | kind: String,
52 | message: String,
53 | }
54 |
55 | impl Display for FlashError {
56 | fn fmt(&self, f: &mut Formatter) -> fmt::Result {
57 | write!(f, "{}: {}", self.kind, self.message)
58 | }
59 | }
60 |
61 | impl std::error::Error for FlashError {}
62 |
63 | impl<'a> Progress for FlashProgress<'a> {
64 | type Device = ();
65 |
66 | fn message(&mut self, _device: &(), kind: &str, message: &str) {
67 | self.errors[self.id]
68 | .set(Err(FlashError { kind: kind.to_string(), message: message.to_string() }));
69 | }
70 |
71 | fn finish(&mut self) {
72 | self.request.finished[self.id].store(true, Ordering::SeqCst);
73 | }
74 |
75 | fn set(&mut self, value: u64) {
76 | self.request.progress[self.id].store(value, Ordering::SeqCst);
77 | }
78 | }
79 |
80 | impl FlashRequest {
81 | pub fn new(
82 | source: File,
83 | destinations: Vec>,
84 | status: Arc>,
85 | progress: Arc>>,
86 | finished: Arc>>,
87 | ) -> FlashRequest {
88 | FlashRequest { source: Some(source), destinations, status, progress, finished }
89 | }
90 |
91 | pub fn write(mut self) -> FlashResult {
92 | self.status.store(FlashStatus::Active, Ordering::SeqCst);
93 |
94 | let source = self.source.take().unwrap();
95 | let res = self.write_inner(source);
96 |
97 | for atomic in self.finished.iter() {
98 | atomic.store(true, Ordering::SeqCst);
99 | }
100 |
101 | self.status.store(FlashStatus::Inactive, Ordering::SeqCst);
102 |
103 | res
104 | }
105 |
106 | fn write_inner(&self, source: File) -> FlashResult {
107 | // Unmount the devices beforehand.
108 | for device in &self.destinations {
109 | let _ = udisks_unmount(&device.parent.path);
110 | for partition in &device.partitions {
111 | let _ = udisks_unmount(&partition.path);
112 | }
113 | }
114 |
115 | // Then open them for writing to.
116 | let mut files = Vec::new();
117 | for device in &self.destinations {
118 | let file = udisks_open(&device.parent.path)?;
119 | files.push(file);
120 | }
121 |
122 | let mut errors = vec![Ok(()); files.len()];
123 | let errors_cells = Cell::from_mut(&mut errors as &mut [_]).as_slice_of_cells();
124 |
125 | // How many bytes to write at a given time.
126 | let mut bucket = [0u8; 64 * 1024];
127 |
128 | let mut task = Task::new(source.into(), false);
129 | for (i, file) in files.into_iter().enumerate() {
130 | let progress = FlashProgress { request: self, errors: errors_cells, id: i };
131 | task.subscribe(file.into(), (), progress);
132 | }
133 |
134 | let res = executor::block_on(task.process(&mut bucket));
135 |
136 | Ok((res, errors))
137 | }
138 | }
139 |
140 | fn udisks_unmount(dbus_path: &str) -> anyhow::Result<()> {
141 | let connection = Connection::new_system()?;
142 |
143 | let dbus_path = ::dbus::strings::Path::new(dbus_path).map_err(anyhow::Error::msg)?;
144 |
145 | let proxy = Proxy::new("org.freedesktop.UDisks2", dbus_path, Duration::new(25, 0), &connection);
146 |
147 | let mut options = UDisksOptions::new();
148 | options.insert("force", Variant(Box::new(true)));
149 | let res: Result<(), _> =
150 | proxy.method_call("org.freedesktop.UDisks2.Filesystem", "Unmount", (options,));
151 |
152 | if let Err(err) = res {
153 | if err.name() != Some("org.freedesktop.UDisks2.Error.NotMounted") {
154 | return Err(anyhow::Error::new(err));
155 | }
156 | }
157 |
158 | Ok(())
159 | }
160 |
161 | fn udisks_open(dbus_path: &str) -> anyhow::Result {
162 | let connection = Connection::new_system()?;
163 |
164 | let dbus_path = ::dbus::strings::Path::new(dbus_path).map_err(anyhow::Error::msg)?;
165 |
166 | let proxy =
167 | Proxy::new("org.freedesktop.UDisks2", &dbus_path, Duration::new(25, 0), &connection);
168 |
169 | let mut options = UDisksOptions::new();
170 | options.insert("flags", Variant(Box::new(libc::O_SYNC)));
171 | let res: (OwnedFd,) =
172 | proxy.method_call("org.freedesktop.UDisks2.Block", "OpenDevice", ("rw", options))?;
173 |
174 | Ok(unsafe { File::from_raw_fd(res.0.into_fd()) })
175 | }
176 |
--------------------------------------------------------------------------------
/gtk/src/app/views/images.rs:
--------------------------------------------------------------------------------
1 | use super::View;
2 | use crate::fl;
3 | use bytesize;
4 | use gtk::prelude::*;
5 | use gtk::*;
6 | use pango::{AttrColor, AttrList, EllipsizeMode};
7 | use std::path::Path;
8 |
9 | pub struct ImageView {
10 | pub view: View,
11 | pub check: Button,
12 | pub chooser_container: Stack,
13 | pub chooser: Button,
14 | pub image_path: Label,
15 | pub hash: ComboBoxText,
16 | pub hash_label: Entry,
17 | }
18 |
19 | impl ImageView {
20 | pub fn new() -> ImageView {
21 | let chooser = cascade! {
22 | Button::with_label(&fl!("choose-image-button"));
23 | ..set_halign(Align::Center);
24 | ..set_margin_bottom(6);
25 | };
26 |
27 | let image_label = format!("{}", fl!("no-image-selected"));
28 |
29 | let image_path = cascade! {
30 | Label::new(Some(&image_label));
31 | ..set_use_markup(true);
32 | ..set_justify(Justification::Center);
33 | ..set_ellipsize(EllipsizeMode::End);
34 | };
35 |
36 | let button_box = cascade! {
37 | Box::new(Orientation::Vertical, 0);
38 | ..pack_start(&chooser, false, false, 0);
39 | ..pack_start(&image_path, false, false, 0);
40 | };
41 |
42 | let spinner = Spinner::new();
43 | spinner.start();
44 |
45 | let spinner_label = cascade! {
46 | Label::new(Some(&fl!("generating-checksum")));
47 | ..style_context().add_class("bold");
48 | };
49 |
50 | let spinner_box = cascade! {
51 | Box::new(Orientation::Vertical, 0);
52 | ..pack_start(&spinner, false, false, 0);
53 | ..pack_start(&spinner_label, false, false, 0);
54 | };
55 |
56 | let hash = cascade! {
57 | ComboBoxText::new();
58 | ..append_text(&fl!("none"));
59 | ..append_text("SHA512");
60 | ..append_text("SHA256");
61 | ..append_text("SHA1");
62 | ..append_text("MD5");
63 | ..append_text("BLAKE2b");
64 | ..set_active(Some(0));
65 | ..set_sensitive(false);
66 | };
67 |
68 | let hash_label = cascade! {
69 | Entry::new();
70 | ..set_sensitive(false);
71 | };
72 |
73 | let label = cascade! {
74 | Label::new(Some(&fl!("hash-label")));
75 | ..set_margin_end(6);
76 | };
77 |
78 | let check = cascade! {
79 | Button::with_label(&fl!("check-label"));
80 | ..style_context().add_class(&STYLE_CLASS_SUGGESTED_ACTION);
81 | ..set_sensitive(false);
82 | };
83 |
84 | let hash_label_clone = hash_label.clone();
85 | let check_clone = check.clone();
86 | hash.connect_changed(move |combo_box| {
87 | let sensitive = combo_box.active_text().is_some_and(|text| text.as_str() != "None");
88 |
89 | hash_label_clone.set_sensitive(sensitive);
90 | check_clone.set_sensitive(sensitive);
91 | });
92 |
93 | let combo_container = cascade! {
94 | Box::new(Orientation::Horizontal, 0);
95 | ..add(&hash);
96 | ..pack_start(&hash_label, true, true, 0);
97 | ..style_context().add_class("linked");
98 | };
99 |
100 | let hash_container = cascade! {
101 | let tmp = Box::new(Orientation::Horizontal, 0);
102 | ..pack_start(&label, false, false, 0);
103 | ..pack_start(&combo_container, true, true, 0);
104 | ..pack_start(&check, false, false, 0);
105 | ..set_border_width(6);
106 | };
107 |
108 | let chooser_container = cascade! {
109 | Stack::new();
110 | ..add_named(&button_box, "chooser");
111 | ..add_named(&spinner_box, "checksum");
112 | ..set_visible_child_name("chooser");
113 | ..set_margin_top(12);
114 | ..set_margin_bottom(24);
115 | };
116 |
117 | let view = View::new(
118 | "application-x-cd-image",
119 | &fl!("image-view-title"),
120 | &fl!("image-view-description"),
121 | |right_panel| {
122 | right_panel.pack_start(&chooser_container, true, false, 0);
123 | right_panel.pack_start(&hash_container, false, false, 0);
124 | },
125 | );
126 |
127 | ImageView { view, check, chooser_container, chooser, image_path, hash, hash_label }
128 | }
129 |
130 | pub fn set_hash_sensitive(&self, sensitive: bool) {
131 | self.hash.set_sensitive(sensitive);
132 | }
133 |
134 | pub fn set_hash(&self, hash: &str) {
135 | let text = self.hash_label.text();
136 | if !text.is_empty() {
137 | let fg = if text.eq_ignore_ascii_case(hash) {
138 | AttrColor::new_foreground(0, u16::MAX, 0)
139 | } else {
140 | AttrColor::new_foreground(u16::MAX, 0, 0)
141 | };
142 | let attrs = AttrList::new();
143 | attrs.insert(fg);
144 | self.hash_label.set_attributes(&attrs);
145 | } else {
146 | self.hash_label.set_text(hash);
147 | }
148 | }
149 |
150 | pub fn set_image(&self, path: &Path, size: u64, warning: Option<&str>) {
151 | let size_str = bytesize::to_string(size, true);
152 | let mut label: String = match path.file_name() {
153 | Some(name) => format!("{}\n{}", name.to_string_lossy(), size_str),
154 | None => format!("{}", fl!("cannot-select-directories")),
155 | };
156 |
157 | if let Some(warning) = warning {
158 | let subject = fl!("warning");
159 | label += &format!("\n{}: {}", subject, warning);
160 | };
161 |
162 | self.image_path.set_markup(&label);
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[macro_use]
2 | extern crate anyhow;
3 | #[macro_use]
4 | extern crate derive_new;
5 | #[macro_use]
6 | extern crate thiserror;
7 |
8 | pub extern crate mnt;
9 |
10 | pub mod codec;
11 |
12 | mod task;
13 |
14 | pub use self::task::{Progress, Task};
15 |
16 | use anyhow::Context;
17 | use as_result::MapResult;
18 | use async_std::{
19 | fs::{self, File, OpenOptions},
20 | os::unix::fs::OpenOptionsExt,
21 | path::{Path, PathBuf},
22 | };
23 | use futures::{executor, prelude::*};
24 | use mnt::MountEntry;
25 | use std::{
26 | io,
27 | os::unix::{ffi::OsStrExt, fs::FileTypeExt},
28 | process::Command,
29 | };
30 | use usb_disk_probe::stream::UsbDiskProbe;
31 |
32 | #[derive(Debug, Error)]
33 | #[rustfmt::skip]
34 | pub enum ImageError {
35 | #[error("image could not be opened: {}", why)]
36 | Open { why: io::Error },
37 | #[error("unable to get image metadata: {}", why)]
38 | Metadata { why: io::Error },
39 | #[error("image was not a file")]
40 | NotAFile,
41 | #[error("unable to read image: {}", why)]
42 | ReadError { why: io::Error },
43 | #[error("reached EOF prematurely")]
44 | Eof,
45 | }
46 |
47 | #[derive(Debug, Error)]
48 | #[rustfmt::skip]
49 | pub enum DiskError {
50 | #[error("failed to fetch devices from USB device stream: {}", _0)]
51 | DeviceStream(anyhow::Error),
52 | #[error("unable to open directory at '{}': {}", dir, why)]
53 | Directory { dir: &'static str, why: io::Error },
54 | #[error("writing to the device was killed")]
55 | Killed,
56 | #[error("unable to read directory entry at '{}': invalid UTF-8", dir.display())]
57 | UTF8 { dir: Box },
58 | #[error("unable to find disk '{}': {}", disk.display(), why)]
59 | NoDisk { disk: Box, why: io::Error },
60 | #[error("failed to unmount {}: {}", path.display(), why)]
61 | UnmountCommand { path: Box, why: io::Error },
62 | #[error("error using disk '{}': {} already mounted at {}", arg.display(), source_.display(), dest.display())]
63 | AlreadyMounted { arg: Box, source_: Box, dest: Box },
64 | #[error("'{}' is not a block device", arg.display())]
65 | NotABlock { arg: Box },
66 | #[error("unable to get metadata of disk '{}': {}", arg.display(), why)]
67 | Metadata { arg: Box, why: io::Error },
68 | #[error("unable to open disk '{}': {}", disk.display(), why)]
69 | Open { disk: Box, why: io::Error },
70 | #[error("error writing disk '{}': {}", disk.display(), why)]
71 | Write { disk: Box, why: io::Error },
72 | #[error("error writing disk '{}': reached EOF", disk.display())]
73 | WriteEOF { disk: Box },
74 | #[error("unable to flush disk '{}': {}", disk.display(), why)]
75 | Flush { disk: Box, why: io::Error },
76 | #[error("error seeking disk '{}': seeked to {} instead of 0", disk.display(), invalid)]
77 | SeekInvalid { disk: Box, invalid: u64 },
78 | #[error("error seeking disk '{}': {}", disk.display(), why)]
79 | Seek { disk: Box, why: io::Error },
80 | #[error("error verifying disk '{}': {}", disk.display(), why)]
81 | Verify { disk: Box, why: io::Error },
82 | #[error("error verifying disk '{}': reached EOF", disk.display())]
83 | VerifyEOF { disk: Box },
84 | #[error("error verifying disk '{}': mismatch at {}:{}", disk.display(), x, y)]
85 | VerifyMismatch { disk: Box, x: usize, y: usize },
86 | }
87 |
88 | pub async fn usb_disk_devices(disks: &mut Vec>) -> anyhow::Result<()> {
89 | let mut stream = UsbDiskProbe::new().await.context("failed to create USB disk probe")?;
90 |
91 | while let Some(device_result) = stream.next().await {
92 | match device_result {
93 | Ok(disk) => disks.push(PathBuf::from(&*disk).into_boxed_path()),
94 | Err(why) => {
95 | eprintln!("failed to reach device path: {}", why);
96 | }
97 | }
98 | }
99 |
100 | Ok(())
101 | }
102 |
103 | /// Stores all discovered USB disk paths into the supplied `disks` vector.
104 | pub fn get_disk_args(disks: &mut Vec>) -> Result<(), DiskError> {
105 | executor::block_on(
106 | async move { usb_disk_devices(disks).await.map_err(DiskError::DeviceStream) },
107 | )
108 | }
109 |
110 | pub async fn disks_from_args>>(
111 | disk_args: D,
112 | mounts: &[MountEntry],
113 | unmount: bool,
114 | ) -> Result, File)>, DiskError> {
115 | let mut disks = Vec::new();
116 |
117 | for disk_arg in disk_args {
118 | let canonical_path = fs::canonicalize(&disk_arg)
119 | .await
120 | .map_err(|why| DiskError::NoDisk { disk: disk_arg.clone(), why })?;
121 |
122 | for mount in mounts {
123 | if mount.spec.as_bytes().starts_with(canonical_path.as_os_str().as_bytes()) {
124 | if unmount {
125 | eprintln!(
126 | "unmounting '{}': {:?} is mounted at {:?}",
127 | disk_arg.display(),
128 | mount.spec,
129 | mount.file
130 | );
131 |
132 | Command::new("umount").arg(&mount.spec).status().map_result().map_err(
133 | |why| DiskError::UnmountCommand {
134 | path: PathBuf::from(mount.spec.clone()).into_boxed_path(),
135 | why,
136 | },
137 | )?;
138 | } else {
139 | return Err(DiskError::AlreadyMounted {
140 | arg: disk_arg.clone(),
141 | source_: PathBuf::from(mount.spec.clone()).into_boxed_path(),
142 | dest: PathBuf::from(mount.file.clone()).into_boxed_path(),
143 | });
144 | }
145 | }
146 | }
147 |
148 | let metadata = canonical_path
149 | .metadata()
150 | .await
151 | .map_err(|why| DiskError::Metadata { arg: disk_arg.clone(), why })?;
152 |
153 | if !metadata.file_type().is_block_device() {
154 | return Err(DiskError::NotABlock { arg: disk_arg.clone() });
155 | }
156 |
157 | let disk = OpenOptions::new()
158 | .read(true)
159 | .write(true)
160 | .custom_flags(libc::O_SYNC)
161 | .open(&canonical_path)
162 | .await
163 | .map_err(|why| DiskError::Open { disk: disk_arg.clone(), why })?;
164 |
165 | disks.push((canonical_path.into_boxed_path(), disk));
166 | }
167 |
168 | Ok(disks)
169 | }
170 |
--------------------------------------------------------------------------------
/gtk/src/app/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod events;
2 | pub mod signals;
3 | pub mod state;
4 | pub mod views;
5 | pub mod widgets;
6 |
7 | use self::events::*;
8 | use self::state::*;
9 | use self::views::*;
10 | use self::widgets::*;
11 |
12 | use crate::fl;
13 | use gtk::{self, prelude::*};
14 | use std::{fs::File, process, rc::Rc, sync::Arc};
15 |
16 | const CSS: &str = include_str!("ui.css");
17 |
18 | pub struct App {
19 | pub ui: Rc,
20 | pub state: Arc,
21 | }
22 |
23 | impl App {
24 | pub fn new(state: State) -> Self {
25 | if gtk::init().is_err() {
26 | eprintln!("failed to initialize GTK Application");
27 | process::exit(1);
28 | }
29 |
30 | App { ui: Rc::new(GtkUi::new()), state: Arc::new(state) }
31 | }
32 |
33 | pub fn connect_events(self) -> Self {
34 | self.connect_back();
35 | self.connect_next();
36 | self.connect_ui_events();
37 | self.connect_image_chooser();
38 | self.connect_image_drag_and_drop();
39 | self.connect_hash();
40 | self.connect_view_ready();
41 |
42 | self
43 | }
44 |
45 | pub fn then_execute(self) {
46 | self.ui.window.show_all();
47 | gtk::main();
48 | }
49 | }
50 |
51 | pub struct GtkUi {
52 | window: gtk::Window,
53 | header: Header,
54 | content: Content,
55 | }
56 |
57 | impl GtkUi {
58 | pub fn new() -> Self {
59 | // Create a the headerbar and it's associated content.
60 | let header = Header::new();
61 | let content = Content::new();
62 |
63 | // Create a new top level window.
64 | let window = cascade! {
65 | gtk::Window::new(gtk::WindowType::Toplevel);
66 | // Set the headerbar as the title bar widget.
67 | ..set_titlebar(Some(&header.container));
68 | // Set the title of the window.
69 | ..set_title("Popsicle");
70 | // The default size of the window to create.
71 | ..set_default_size(500, 250);
72 | ..add(&content.container);
73 | };
74 |
75 | // Add a custom CSS style
76 | let screen = WidgetExt::screen(&window).unwrap();
77 | let style = gtk::CssProvider::new();
78 | let _ = style.load_from_data(CSS.as_bytes());
79 | gtk::StyleContext::add_provider_for_screen(
80 | &screen,
81 | &style,
82 | gtk::STYLE_PROVIDER_PRIORITY_USER,
83 | );
84 |
85 | // The icon the app will display.
86 | gtk::Window::set_default_icon_name("com.system76.Popsicle");
87 |
88 | // Programs what to do when the exit button is used.
89 | window.connect_delete_event(move |_, _| {
90 | gtk::main_quit();
91 | gtk::Inhibit(false)
92 | });
93 |
94 | GtkUi { header, window, content }
95 | }
96 |
97 | pub fn errorck(
98 | &self,
99 | state: &State,
100 | result: Result,
101 | context: &str,
102 | ) -> Result {
103 | result.map_err(|why| {
104 | self.content.error_view.view.description.set_text(&format!("{}: {}", context, why));
105 | self.switch_to(state, ActiveView::Error);
106 | })
107 | }
108 |
109 | pub fn errorck_option(
110 | &self,
111 | state: &State,
112 | result: Option,
113 | context: &'static str,
114 | ) -> Result {
115 | result.ok_or_else(|| {
116 | self.content.error_view.view.description.set_text(&format!(
117 | "{}: {}",
118 | context,
119 | fl!("no-value-found")
120 | ));
121 | self.switch_to(state, ActiveView::Error);
122 | })
123 | }
124 |
125 | pub fn switch_to(&self, state: &State, view: ActiveView) {
126 | let back = &self.header.back;
127 | let next = &self.header.next;
128 | let stack = &self.content.container;
129 |
130 | let back_ctx = back.style_context();
131 | let next_ctx = next.style_context();
132 |
133 | let widget = match view {
134 | ActiveView::Images => {
135 | back.set_label(&fl!("cancel"));
136 | back_ctx.remove_class("back-button");
137 | back_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
138 |
139 | next.set_label(&fl!("next"));
140 | next.set_visible(true);
141 | next.set_sensitive(true);
142 | next_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
143 | next_ctx.add_class(gtk::STYLE_CLASS_SUGGESTED_ACTION);
144 |
145 | &self.content.image_view.view.container
146 | }
147 | ActiveView::Devices => {
148 | next_ctx.remove_class(gtk::STYLE_CLASS_SUGGESTED_ACTION);
149 | next_ctx.add_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
150 | next.set_sensitive(false);
151 |
152 | let _ = state.back_event_tx.send(BackgroundEvent::RefreshDevices);
153 | &self.content.devices_view.view.container
154 | }
155 | ActiveView::Flashing => {
156 | match self.errorck(
157 | state,
158 | File::open(&*state.image_path.borrow()),
159 | &fl!("iso-open-failed"),
160 | ) {
161 | Ok(file) => *state.image.borrow_mut() = Some(file),
162 | Err(()) => return,
163 | };
164 |
165 | let all_devices = state.available_devices.borrow();
166 | let mut devices = state.selected_devices.borrow_mut();
167 |
168 | devices.clear();
169 |
170 | for active_id in self.content.devices_view.is_active_ids() {
171 | devices.push(all_devices[active_id].clone());
172 | }
173 |
174 | back_ctx.remove_class("back-button");
175 | back_ctx.add_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
176 |
177 | next.set_visible(false);
178 | &self.content.flash_view.view.container
179 | }
180 | ActiveView::Summary => {
181 | back_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
182 | back.set_label(&fl!("flash-again"));
183 |
184 | next_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
185 | next.set_visible(true);
186 | next.set_label(&fl!("done"));
187 | &self.content.summary_view.view.container
188 | }
189 | ActiveView::Error => {
190 | back.set_label(&fl!("flash-again"));
191 | back_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
192 |
193 | next.set_visible(true);
194 | next.set_label(&fl!("close"));
195 | next_ctx.remove_class(gtk::STYLE_CLASS_DESTRUCTIVE_ACTION);
196 | next_ctx.remove_class(gtk::STYLE_CLASS_SUGGESTED_ACTION);
197 |
198 | &self.content.error_view.view.container
199 | }
200 | };
201 |
202 | stack.set_visible_child(widget);
203 | state.active_view.set(view);
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/cli/src/main.rs:
--------------------------------------------------------------------------------
1 | //! CLI application for flashing multiple drives concurrently.
2 |
3 | #[macro_use]
4 | extern crate anyhow;
5 | #[macro_use]
6 | extern crate cascade;
7 | #[macro_use]
8 | extern crate derive_new;
9 | #[macro_use]
10 | extern crate fomat_macros;
11 |
12 | mod localize;
13 |
14 | use anyhow::Context;
15 | use async_std::{
16 | fs::OpenOptions,
17 | os::unix::fs::OpenOptionsExt,
18 | path::{Path, PathBuf},
19 | };
20 |
21 | use clap::{builder::Arg, ArgAction, ArgMatches, Command};
22 | use futures::{
23 | channel::{mpsc, oneshot},
24 | executor, join,
25 | prelude::*,
26 | };
27 | use i18n_embed::DesktopLanguageRequester;
28 | use once_cell::sync::Lazy;
29 | use pbr::{MultiBar, Pipe, ProgressBar, Units};
30 | use popsicle::{mnt, Progress, Task};
31 | use std::{
32 | io::{self, Write},
33 | process, thread,
34 | };
35 |
36 | static ARG_IMAGE: Lazy = Lazy::new(|| fl!("arg-image"));
37 | static ARG_DISKS: Lazy = Lazy::new(|| fl!("arg-disks"));
38 |
39 | fn main() {
40 | translate();
41 | better_panic::install();
42 |
43 | let matches = Command::new(env!("CARGO_PKG_NAME"))
44 | .about(env!("CARGO_PKG_DESCRIPTION"))
45 | .version(env!("CARGO_PKG_VERSION"))
46 | .arg(Arg::new(&**ARG_IMAGE).help(&fl!("arg-image-desc")).required(true))
47 | .arg(Arg::new(&**ARG_DISKS).help(&fl!("arg-disks-desc")))
48 | .arg(
49 | Arg::new("all")
50 | .help(&fl!("arg-all-desc"))
51 | .short('a')
52 | .long("all")
53 | .action(ArgAction::SetTrue),
54 | )
55 | .arg(
56 | Arg::new("check")
57 | .help(&fl!("arg-check-desc"))
58 | .short('c')
59 | .long("check")
60 | .action(ArgAction::SetTrue),
61 | )
62 | .arg(
63 | Arg::new("unmount")
64 | .help(&fl!("arg-unmount-desc"))
65 | .short('u')
66 | .long("unmount")
67 | .action(ArgAction::SetTrue),
68 | )
69 | .arg(
70 | Arg::new("yes")
71 | .help(&fl!("arg-yes-desc"))
72 | .short('y')
73 | .long("yes")
74 | .action(ArgAction::SetTrue),
75 | )
76 | .get_matches();
77 |
78 | let (rtx, rrx) = oneshot::channel::>();
79 |
80 | let result = executor::block_on(async move {
81 | match popsicle(rtx, matches).await {
82 | Err(why) => Err(why),
83 | _ => match rrx.await {
84 | Ok(Err(why)) => Err(why),
85 | _ => Ok(()),
86 | },
87 | }
88 | });
89 |
90 | if let Err(why) = result {
91 | eprintln!("popsicle: {}", why);
92 | for source in why.chain().skip(1) {
93 | epintln!(" " (fl!("error-caused-by")) ": " (source))
94 | }
95 |
96 | process::exit(1);
97 | }
98 | }
99 |
100 | async fn popsicle(
101 | rtx: oneshot::Sender>,
102 | matches: ArgMatches,
103 | ) -> anyhow::Result<()> {
104 | let image_path = matches
105 | .get_one::(&fl!("arg-image"))
106 | .with_context(|| fl!("error-image-not-set"))?
107 | .clone();
108 |
109 | let image = OpenOptions::new()
110 | .custom_flags(libc::O_SYNC)
111 | .read(true)
112 | .open(&image_path)
113 | .await
114 | .with_context(|| fl!("error-image-open", image_path = image_path.clone()))?;
115 |
116 | let image_size = image
117 | .metadata()
118 | .await
119 | .map(|x| x.len())
120 | .with_context(|| fl!("error-image-metadata", image_path = image_path.clone()))?;
121 |
122 | let mut disk_args = Vec::new();
123 | if matches.get_flag("all") {
124 | popsicle::usb_disk_devices(&mut disk_args)
125 | .await
126 | .with_context(|| fl!("error-disks-fetch"))?;
127 | } else if let Some(disks) = matches.get_many::(&fl!("arg-disks")) {
128 | disk_args.extend(disks.map(PathBuf::from).map(Box::from));
129 | }
130 |
131 | if disk_args.is_empty() {
132 | return Err(anyhow!(fl!("error-no-disks-specified")));
133 | }
134 |
135 | let mounts = mnt::get_submounts(Path::new("/")).with_context(|| fl!("error-reading-mounts"))?;
136 |
137 | let disks =
138 | popsicle::disks_from_args(disk_args.into_iter(), &mounts, matches.get_flag("unmount"))
139 | .await
140 | .with_context(|| fl!("error-opening-disks"))?;
141 |
142 | let is_tty = atty::is(atty::Stream::Stdout);
143 |
144 | if is_tty && !matches.get_flag("yes") {
145 | epint!(
146 | (fl!("question", image_path = image_path)) "\n"
147 | for (path, _) in &disks {
148 | " - " (path.display()) "\n"
149 | }
150 | (fl!("yn")) ": "
151 | );
152 |
153 | io::stdout().flush().unwrap();
154 |
155 | let mut confirm = String::new();
156 | io::stdin().read_line(&mut confirm).unwrap();
157 |
158 | if confirm.trim() != fl!("y") && confirm.trim() != "yes" {
159 | return Err(anyhow!(fl!("error-exiting")));
160 | }
161 | }
162 |
163 | let check = matches.get_flag("check");
164 |
165 | // If this is a TTY, display a progress bar. If not, display machine-readable info.
166 | if is_tty {
167 | println!();
168 |
169 | let mb = MultiBar::new();
170 | let mut task = Task::new(image, check);
171 |
172 | for (disk_path, disk) in disks {
173 | let pb = InteractiveProgress::new(cascade! {
174 | mb.create_bar(image_size);
175 | ..set_units(Units::Bytes);
176 | ..message(&format!("W {}: ", disk_path.display()));
177 | });
178 |
179 | task.subscribe(disk, disk_path, pb);
180 | }
181 |
182 | thread::spawn(|| {
183 | executor::block_on(async move {
184 | let buf = &mut [0u8; 64 * 1024];
185 | let _ = rtx.send(task.process(buf).await);
186 | })
187 | });
188 |
189 | mb.listen();
190 | } else {
191 | let (etx, erx) = mpsc::unbounded();
192 | let mut paths = Vec::new();
193 | let mut task = Task::new(image, check);
194 |
195 | for (disk_path, disk) in disks {
196 | let pb = MachineProgress::new(paths.len(), etx.clone());
197 | paths.push(disk_path.clone());
198 | task.subscribe(disk, disk_path, pb);
199 | }
200 |
201 | drop(etx);
202 |
203 | let task = async move {
204 | let buf = &mut [0u8; 64 * 1024];
205 | let _ = rtx.send(task.process(buf).await);
206 | };
207 |
208 | join!(machine_output(erx, &paths, image_size), task);
209 | }
210 |
211 | Ok(())
212 | }
213 |
214 | /// An event for creating a machine-readable output
215 | pub enum Event {
216 | Message(usize, Box),
217 | Finished(usize),
218 | Set(usize, u64),
219 | }
220 |
221 | /// Tracks progress
222 | #[derive(new)]
223 | pub struct MachineProgress {
224 | id: usize,
225 |
226 | handle: mpsc::UnboundedSender,
227 | }
228 |
229 | impl Progress for MachineProgress {
230 | type Device = Box;
231 |
232 | fn message(&mut self, _path: &Box, kind: &str, message: &str) {
233 | let _ = self.handle.unbounded_send(Event::Message(
234 | self.id,
235 | if message.is_empty() { kind.into() } else { [kind, " ", message].concat().into() },
236 | ));
237 | }
238 |
239 | fn finish(&mut self) {
240 | let _ = self.handle.unbounded_send(Event::Finished(self.id));
241 | }
242 |
243 | fn set(&mut self, written: u64) {
244 | let _ = self.handle.unbounded_send(Event::Set(self.id, written));
245 | }
246 | }
247 |
248 | #[derive(new)]
249 | pub struct InteractiveProgress {
250 | pipe: ProgressBar,
251 | }
252 |
253 | impl Progress for InteractiveProgress {
254 | type Device = Box;
255 |
256 | fn message(&mut self, path: &Box, kind: &str, message: &str) {
257 | self.pipe.message(&format!("{} {}: {}", kind, path.display(), message));
258 | }
259 |
260 | fn finish(&mut self) {
261 | self.pipe.finish();
262 | }
263 |
264 | fn set(&mut self, written: u64) {
265 | self.pipe.set(written);
266 | }
267 | }
268 |
269 | /// Writes a machine-friendly output, when this program is being piped into another.
270 | async fn machine_output(
271 | mut rx: mpsc::UnboundedReceiver,
272 | paths: &[Box],
273 | image_size: u64,
274 | ) {
275 | let stdout = io::stdout();
276 | let stdout = &mut stdout.lock();
277 |
278 | let _ = wite!(
279 | stdout,
280 | "Size(" (image_size) ")\n"
281 | for path in paths {
282 | "Device(\"" (path.display()) "\")\n"
283 | }
284 | );
285 |
286 | while let Some(event) = rx.next().await {
287 | match event {
288 | Event::Message(id, message) => {
289 | let _ = witeln!(stdout, "Message(\"" (paths[id].display()) "\",\"" (message) "\")");
290 | }
291 | Event::Finished(id) => {
292 | let _ = witeln!(stdout, "Finished(\"" (paths[id].display()) "\")");
293 | }
294 | Event::Set(id, written) => {
295 | let _ = witeln!(stdout, "Set(\"" (paths[id].display()) "\"," (written) ")");
296 | }
297 | }
298 | }
299 | }
300 |
301 | fn translate() {
302 | let requested_languages = DesktopLanguageRequester::requested_languages();
303 | let localizer = crate::localize::localizer();
304 |
305 | if let Err(error) = localizer.select(&requested_languages) {
306 | eprintln!("Error while loading languages for popsicle-cli {}", error);
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/gtk/src/app/signals/mod.rs:
--------------------------------------------------------------------------------
1 | mod devices;
2 | mod images;
3 |
4 | use crate::app::events::{BackgroundEvent, UiEvent};
5 | use crate::app::state::ActiveView;
6 | use crate::app::App;
7 | use crate::fl;
8 | use crate::flash::{FlashRequest, FlashStatus, FlashTask};
9 | use crate::misc;
10 | use atomic::Atomic;
11 | use crossbeam_channel::TryRecvError;
12 | use gtk::{self, prelude::*};
13 | use iso9660::ISO9660;
14 | use std::fmt::Write;
15 | use std::fs::File;
16 | use std::sync::atomic::Ordering;
17 | use std::sync::{Arc, Mutex};
18 | use std::time::{Duration, Instant};
19 |
20 | impl App {
21 | pub fn connect_back(&self) {
22 | let state = self.state.clone();
23 | let ui = self.ui.clone();
24 |
25 | self.ui.header.connect_back(move || {
26 | let back = match state.active_view.get() {
27 | ActiveView::Images => {
28 | gtk::main_quit();
29 | return;
30 | }
31 | _ => ActiveView::Images,
32 | };
33 |
34 | let _ = state.ui_event_tx.send(UiEvent::Reset);
35 | ui.content.devices_view.reset();
36 |
37 | ui.switch_to(&state, back);
38 | });
39 | }
40 |
41 | pub fn connect_next(&self) {
42 | let state = self.state.clone();
43 | let ui = self.ui.clone();
44 |
45 | self.ui.header.connect_next(move || {
46 | let next = match state.active_view.get() {
47 | ActiveView::Images => ActiveView::Devices,
48 | ActiveView::Devices => ActiveView::Flashing,
49 | _ => {
50 | gtk::main_quit();
51 | return;
52 | }
53 | };
54 |
55 | ui.switch_to(&state, next);
56 | });
57 | }
58 |
59 | pub fn connect_ui_events(&self) {
60 | let state = self.state.clone();
61 | let ui = self.ui.clone();
62 |
63 | let mut last_device_refresh = Instant::now();
64 | let mut flashing_devices: Vec<(gtk::ProgressBar, gtk::Label)> = Vec::new();
65 | let flash_status = Arc::new(Atomic::new(FlashStatus::Inactive));
66 | let mut flash_handles = None;
67 | let mut tasks = None;
68 |
69 | glib::timeout_add_local(Duration::from_millis(16), move || {
70 | match state.ui_event_rx.try_recv() {
71 | Err(TryRecvError::Disconnected) => return Continue(false),
72 | Err(TryRecvError::Empty) => (),
73 | Ok(UiEvent::SetHash(hash)) => {
74 | ui.content.image_view.set_hash(&match hash {
75 | Ok(hash) => hash,
76 | Err(why) => fl!("error", why = format!("{}", why)),
77 | });
78 | ui.content.image_view.set_hash_sensitive(true);
79 |
80 | ui.content.image_view.chooser_container.set_visible_child_name("chooser");
81 | }
82 | Ok(UiEvent::SetImageLabel(path)) => {
83 | if let Ok(file) = File::open(&path) {
84 | let image_size = file.metadata().ok().map_or(0, |m| m.len());
85 |
86 | let warning = if is_windows_iso(&file) {
87 | Some(fl!("win-isos-not-supported"))
88 | } else {
89 | None
90 | };
91 |
92 | ui.content.image_view.set_image(&path, image_size, warning.as_deref());
93 | ui.content.image_view.set_hash_sensitive(true);
94 | ui.header.next.set_sensitive(true);
95 |
96 | state.image_size.store(image_size, Ordering::SeqCst);
97 | *state.image_path.borrow_mut() = path;
98 | }
99 | }
100 | Ok(UiEvent::RefreshDevices(devices)) => {
101 | let size = state.image_size.load(Ordering::SeqCst);
102 | ui.content.devices_view.refresh(&devices, size);
103 | *state.available_devices.borrow_mut() = devices;
104 | }
105 | Ok(UiEvent::Flash(handle)) => flash_handles = Some(handle),
106 | Ok(UiEvent::Reset) => {
107 | match flash_status.load(Ordering::SeqCst) {
108 | FlashStatus::Active => {
109 | flash_status.store(FlashStatus::Killing, Ordering::SeqCst)
110 | }
111 | FlashStatus::Inactive | FlashStatus::Killing => (),
112 | }
113 |
114 | flash_handles = None;
115 | tasks = None;
116 | flashing_devices.clear();
117 | }
118 | }
119 |
120 | match state.active_view.get() {
121 | ActiveView::Devices => {
122 | let now = Instant::now();
123 |
124 | // Only attempt to refresh the devices if the last refresh was >= 3 seconds ago.
125 | if now.duration_since(last_device_refresh).as_secs() >= 3 {
126 | last_device_refresh = now;
127 | let _ = state.back_event_tx.send(BackgroundEvent::RefreshDevices);
128 | }
129 | }
130 | ActiveView::Flashing => match state.image.borrow_mut().take() {
131 | // When the flashing view is active, and an image has not started flashing.
132 | Some(image) => {
133 | let summary_grid = &ui.content.flash_view.progress_list;
134 | summary_grid.foreach(|w| summary_grid.remove(w));
135 | let mut destinations = Vec::new();
136 |
137 | let selected_devices = state.selected_devices.borrow_mut();
138 | for (id, device) in selected_devices.iter().enumerate() {
139 | let id = id as i32;
140 |
141 | let pbar = cascade! {
142 | gtk::ProgressBar::new();
143 | ..set_hexpand(true);
144 | };
145 |
146 | let label = cascade! {
147 | gtk::Label::new(Some(&misc::device_label(device)));
148 | ..set_justify(gtk::Justification::Right);
149 | ..style_context().add_class("bold");
150 | };
151 |
152 | let bar_label = cascade! {
153 | gtk::Label::new(None);
154 | ..set_halign(gtk::Align::Center);
155 | };
156 |
157 | let bar_container = cascade! {
158 | gtk::Box::new(gtk::Orientation::Vertical, 0);
159 | ..add(&pbar);
160 | ..add(&bar_label);
161 | };
162 |
163 | summary_grid.attach(&label, 0, id, 1, 1);
164 | summary_grid.attach(&bar_container, 1, id, 1, 1);
165 |
166 | flashing_devices.push((pbar, bar_label));
167 | destinations.push(device.clone());
168 | }
169 |
170 | summary_grid.show_all();
171 | let ndestinations = destinations.len();
172 | let progress = Arc::new(
173 | (0..ndestinations).map(|_| Atomic::new(0u64)).collect::>(),
174 | );
175 | let finished = Arc::new(
176 | (0..ndestinations).map(|_| Atomic::new(false)).collect::>(),
177 | );
178 |
179 | let _ =
180 | state.back_event_tx.send(BackgroundEvent::Flash(FlashRequest::new(
181 | image,
182 | destinations,
183 | flash_status.clone(),
184 | progress.clone(),
185 | finished.clone(),
186 | )));
187 |
188 | tasks = Some(FlashTask {
189 | previous: Arc::new(Mutex::new(vec![[0; 7]; ndestinations])),
190 | progress,
191 | finished,
192 | });
193 | }
194 | // When the flashing view is active, and thus an image is flashing.
195 | None => {
196 | let now = Instant::now();
197 |
198 | // Only attempt to refresh the devices if the last refresh was >= 500ms ago.
199 | let time_since = now.duration_since(last_device_refresh);
200 | if time_since.as_secs() > 1 || time_since.subsec_millis() >= 500 {
201 | last_device_refresh = now;
202 |
203 | let mut all_tasks_finished = true;
204 | let length = state.image_size.load(Ordering::SeqCst);
205 | let tasks = tasks.as_mut().expect("no flash task");
206 | let mut previous = tasks.previous.lock().expect("mutex lock");
207 |
208 | for (id, (pbar, label)) in flashing_devices.iter().enumerate() {
209 | let prev_values = &mut previous[id];
210 | let progress = &tasks.progress[id];
211 | let finished = &tasks.finished[id];
212 |
213 | let raw_value = progress.load(Ordering::SeqCst);
214 | let task_is_finished = finished.load(Ordering::SeqCst);
215 | let value = if task_is_finished {
216 | 1.0f64
217 | } else {
218 | all_tasks_finished = false;
219 | raw_value as f64 / length as f64
220 | };
221 |
222 | pbar.set_fraction(value);
223 |
224 | if task_is_finished {
225 | label.set_label(&fl!("task-finished"));
226 | } else {
227 | prev_values[1] = prev_values[2];
228 | prev_values[2] = prev_values[3];
229 | prev_values[3] = prev_values[4];
230 | prev_values[4] = prev_values[5];
231 | prev_values[5] = prev_values[6];
232 | prev_values[6] = raw_value - prev_values[0];
233 | prev_values[0] = raw_value;
234 |
235 | let sum: u64 = prev_values.iter().skip(1).sum();
236 | let per_second = sum / 3;
237 | label.set_label(&format!(
238 | "{}/s",
239 | bytesize::to_string(per_second, true)
240 | ));
241 | }
242 | }
243 |
244 | drop(previous);
245 |
246 | if all_tasks_finished {
247 | eprintln!("all tasks finished");
248 |
249 | let taken_handles = match ui.errorck_option(
250 | &state,
251 | flash_handles.take(),
252 | "Taking flash handles failed",
253 | ) {
254 | Ok(results) => {
255 | results.join().map_err(|why| format!("{:?}", why))
256 | }
257 | Err(()) => return Continue(true),
258 | };
259 |
260 | let handle = match ui.errorck(
261 | &state,
262 | taken_handles,
263 | "Failed to join flash thread",
264 | ) {
265 | Ok(results) => results,
266 | Err(()) => return Continue(true),
267 | };
268 |
269 | let (result, results) = match ui.errorck(
270 | &state,
271 | handle,
272 | "Errored starting flashing process",
273 | ) {
274 | Ok(result) => result,
275 | Err(()) => return Continue(true),
276 | };
277 |
278 | let mut errors = Vec::new();
279 | let mut selected_devices = state.selected_devices.borrow_mut();
280 | let ntasks = selected_devices.len();
281 |
282 | for (device, result) in
283 | selected_devices.drain(..).zip(results.into_iter())
284 | {
285 | if let Err(why) = result {
286 | errors.push((device, why));
287 | }
288 | }
289 |
290 | ui.switch_to(&state, ActiveView::Summary);
291 | let list = &ui.content.summary_view.list;
292 | let description = &ui.content.summary_view.view.description;
293 |
294 | if result.is_ok() && errors.is_empty() {
295 | let desc = fl!("successful-flash", total = ntasks);
296 | description.set_text(&desc);
297 | list.hide();
298 | } else {
299 | ui.content
300 | .summary_view
301 | .view
302 | .topic
303 | .set_text(&fl!("flashing-completed-with-errors"));
304 |
305 | let mut desc = fl!(
306 | "partial-flash",
307 | number = { ntasks - errors.len() },
308 | total = ntasks
309 | );
310 |
311 | if let Err(why) = result {
312 | let _ = write!(desc, ": {}", why);
313 | }
314 |
315 | description.set_markup(&desc);
316 |
317 | for (device, why) in errors {
318 | let device =
319 | gtk::Label::new(Some(&misc::device_label(&device)));
320 | let why =
321 | gtk::Label::new(Some(format!("{}", why).as_str()));
322 | why.style_context().add_class("bold");
323 |
324 | let container = cascade! {
325 | gtk::Box::new(gtk::Orientation::Horizontal, 6);
326 | ..pack_start(&device, false, false, 0);
327 | ..pack_start(&why, true, true, 0);
328 | };
329 |
330 | let row = cascade! {
331 | gtk::ListBoxRow::new();
332 | ..set_selectable(false);
333 | ..add(&container);
334 | };
335 |
336 | list.add(&row);
337 | }
338 |
339 | list.show_all();
340 | }
341 | }
342 | }
343 | }
344 | },
345 | _ => (),
346 | }
347 |
348 | Continue(true)
349 | });
350 | }
351 | }
352 |
353 | fn is_windows_iso(file: &File) -> bool {
354 | if let Ok(fs) = ISO9660::new(file) {
355 | return fs.publisher_identifier() == "MICROSOFT CORPORATION";
356 | }
357 | false
358 | }
359 |
--------------------------------------------------------------------------------