Commit cd5b73a4 authored by rgaudin's avatar rgaudin
Browse files

Fixed #263, #264: added SD-card wipe dialog and partition table check on start

parent 9686ada9
2.0-rc16
* fixed etcher-cli call on macOS GUI (space escaping)
* Added partition number check on startup (must be one)
* Added SD-card wipe dialog
2.0-rc15
* Fixed concurrent access error on Windows
......
......@@ -2,6 +2,7 @@
# vim: ai ts=4 sts=4 et sw=4 nu
import os
import re
import sys
import time
import shlex
......@@ -171,8 +172,9 @@ class EtcherWriterThread(threading.Thread):
logger.step("Copy image to sd card using etcher-cli")
from_cli = logger is None or type(logger) == CLILogger
cmd, log_to_file, log_file = get_etcher_command(
image_fpath, device_fpath, logger
image_fpath, device_fpath, logger, from_cli
)
process = subprocess.Popen(
......@@ -309,8 +311,7 @@ def restore_sleep_policy(reference, logger):
return
def get_etcher_command(image_fpath, device_fpath, logger):
from_cli = logger is None or type(logger) == CLILogger
def get_etcher_command(image_fpath, device_fpath, logger, from_cli):
# on macOS, GUI sudo captures stdout so we use a log file
log_to_file = not from_cli and sys.platform == "darwin"
......@@ -342,12 +343,72 @@ def get_etcher_command(image_fpath, device_fpath, logger):
return cmd, log_to_file, log_file
def flash_image_with_etcher(image_fpath, device_fpath, logger):
cmd, log_to_file, log_file = get_etcher_command(image_fpath, device_fpath, logger)
subprocess_pretty_check_call(cmd, logger)
def flash_image_with_etcher(image_fpath, device_fpath, retcode):
""" flash an image onto SD-card
use only with small image as there is no output capture on OSX
and it is not really cancellable.
retcode is a multiprocessing.Value """
logger = CLILogger()
cmd, log_to_file, log_file = get_etcher_command(
image_fpath, device_fpath, logger, False
)
returncode, _ = subprocess_pretty_call(cmd, check=False, logger=logger)
retcode.value = returncode
if log_to_file:
log_file.close()
try:
subprocess_pretty_call(["/bin/cat", log_file.name], logger, decode=True)
log_file.close()
os.unlink(log_file.name)
except Exception as exp:
logger.err(str(exp))
return returncode == 0
def sd_has_single_partition(sd_card, logger):
""" whether sd_card consists of a single partition (expected to be clean) """
try:
if sys.platform == "darwin":
disk_prefix = re.sub(r"\/dev\/disk([0-9]+)", r"disk\1s", sd_card)
lines = subprocess_pretty_call(
["diskutil", "list", sd_card], logger, check=True, decode=True
)
nb_partitions = len(
[
line.strip().rsplit(" ", 1)[-1].replace(disk_prefix, "").strip()
for line in lines
if disk_prefix in line
]
)
return nb_partitions == 1
elif sys.platform == "win32":
disk_prefix = re.sub(
r".+PHYSICALDRIVE([0-9+])", r"Disk #\1, Partition #", sd_card
)
lines = subprocess_pretty_call(
["wmic", "partition"], logger, check=True, decode=True
)
nb_partitions = len(
[
re.sub(r".+" + disk_prefix + r"([0-9]+).+", r"\1", line)
for line in lines
if disk_prefix in line
]
)
return nb_partitions == 1
elif sys.platform == "linux":
disk_prefix = re.sub(r"\/dev\/([a-z0-9]+)", r"─\1", sd_card)
lines = subprocess_pretty_call(
["/bin/lsblk", sd_card], logger, check=True, decode=True
)
nb_partitions = len(
[
line.strip().split(" ", 1)[0].replace(disk_prefix, "").strip()
for line in lines
if disk_prefix in line
]
)
return nb_partitions == 1
except Exception as exp:
logger.err(str(exp))
return False
......@@ -40,6 +40,7 @@ from util import split_proxy, save_prefs
from backend.catalog import get_catalogs
from util import CancelEvent, ProgressHelper
from run_installation import run_installation
from backend.util import sd_has_single_partition, flash_image_with_etcher
from backend.mount import open_explorer_for_imdisk
from util import human_readable_size, ONE_GB, ONE_MiB
from backend.cache import clean_cache, reset_cache
......@@ -309,6 +310,9 @@ class Application:
# cache
self.component.clean_cache_button.connect("clicked", self.activate_menu_cache)
# sd clean
self.component.clean_sd_button.connect("clicked", self.activate_sd_clean)
# wifi password
self.component.wifi_password_switch.connect(
"notify::active",
......@@ -351,6 +355,9 @@ class Application:
self.component.sd_card_combobox.connect(
"changed", lambda _: self.update_free_space()
)
self.component.sd_card_combobox.connect(
"changed", lambda w: self.activate_sd_clean_button(w)
)
self.component.sd_card_refresh_button.connect(
"clicked", self.sd_card_refresh_button_clicked
)
......@@ -1105,6 +1112,138 @@ class Application:
else:
dialog.close()
def activate_sd_clean(self, widget):
sd_card = self.get_sd_card()
class SDCleanDialog(Gtk.Dialog):
def __init__(self, parent, parent_ui):
Gtk.Dialog.__init__(
self,
"Wipe your SD-card clean before for installation",
parent,
0,
(
"Wipe {}".format(sd_card),
Gtk.ResponseType.OK,
"Close",
Gtk.ResponseType.CANCEL,
),
)
self.parent = parent
self.parent_ui = parent_ui
self.set_default_size(300, 100)
label = Gtk.Label()
label.set_markup(
"\nFor Kiwix Hotspot to work properly,\n"
"you need your SD-card to be cleaned before starting,\n"
"hence having just a single FAT-like partition.\n\n"
"This process you only take a few minutes.\n"
"If this does not end within 10mn,\n"
"cancel-it and try clean your SD-card using a different tool.\n\n"
)
label.set_alignment(0, 0.5)
self.thread = None
self.retcode = multiprocessing.Value("i", -1)
self.run_progressbar = Gtk.ProgressBar()
self.cancel_button = Gtk.Button("Cancel")
self.cancel_button.connect("clicked", self.stop_clean_operation)
box = self.get_content_area()
box.add(label)
box.add(self.run_progressbar)
box.add(self.cancel_button)
box.add(Gtk.Label("")) # spacer
self.show_all()
self.run_progressbar.set_visible(False)
self.cancel_button.set_visible(False)
def start_clean_operation(self):
# do nothing if the thread is running
if self.thread is not None and self.thread.is_alive():
return
# show progress bar and cancel button
self.run_progressbar.set_visible(True)
self.cancel_button.set_label("Cancel Wiping")
self.cancel_button.set_visible(True)
# start progress bar animation
self.timeout_id = GObject.timeout_add(50, self.on_timeout)
self.thread = multiprocessing.Process(
target=flash_image_with_etcher,
args=(
os.path.join(data.data_dir, "mbr.img"),
sd_card,
self.retcode,
),
)
self.thread.start()
def on_timeout(self):
# display post-thread dialog on cancelled thread
if self.thread is not None and not self.thread.is_alive():
self.display_post_clean_operation_dialog()
return False
elif self.thread is None: # stop progress anim if thread not running
return False
new_value = self.run_progressbar.get_fraction() + 0.035
# inverse direction if end reached
if new_value > 1:
new_value = 0
# switch from left-to-right to right-to-left at bounds
self.run_progressbar.set_inverted(
not self.run_progressbar.get_inverted()
)
self.run_progressbar.set_fraction(new_value)
return True # returns True so it continues to get called
def stop_clean_operation(self, *args, **kwargs):
if self.thread is not None and self.thread.is_alive():
self.thread.terminate()
self.thread = None
self.cancel_button.set_visible(False)
self.run_progressbar.set_visible(False)
self.close()
def display_post_clean_operation_dialog(self):
if self.retcode.value == 0:
title = "SD-card Cleaning Completed"
content = (
"Your SD-card ({}) has been wiped.\n\n"
"You now need to unplug then replug your device.\n"
"Once done, come back and hit the refresh button."
).format(sd_card)
else:
title = "SD-card Cleaning Failed"
content = (
"You SD-card HAS NOT been wiped.\n\n"
"Please use a different tool to clean it."
)
msg_box = Gtk.MessageDialog(
self.parent, None, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, title
)
msg_box.format_secondary_text(content)
msg_box.set_modal(True)
msg_box.run()
msg_box.destroy()
self.close()
self.parent_ui.sd_card_refresh_button_clicked("")
dialog = SDCleanDialog(self.component.window, self)
ret = dialog.run()
if ret == Gtk.ResponseType.OK:
dialog.start_clean_operation()
else:
dialog.close()
self.sd_card_refresh_button_clicked("")
def installation_done(self, error):
ok = error is None
validate_label(self.component.done_label, ok)
......@@ -1518,6 +1657,17 @@ class Application:
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(text_buffer.get_text(start, end, hidden), -1)
def get_sd_card(self):
if self.component.output_stack.get_visible_child_name() == "sd_card":
sd_card_id = self.component.sd_card_combobox.get_active()
if sd_card_id == -1:
return None
else:
device_index = sd_card_info.get_device_index()
return self.component.sd_card_list_store[sd_card_id][device_index]
return None
def run_installation_button_clicked(self, button):
all_valid = True
......@@ -1588,23 +1738,22 @@ class Application:
all_valid = all_valid and valid_admin_login and valid_admin_pwd
output_size = self.get_output_size()
sd_card = self.get_sd_card()
if self.component.output_stack.get_visible_child_name() == "sd_card":
sd_card_id = self.component.sd_card_combobox.get_active()
condition = sd_card_id != -1
condition = sd_card is not None
validate_label(self.component.sd_card_label, condition)
all_valid = all_valid and condition
if sd_card_id == -1:
sd_card = None
else:
device_index = sd_card_info.get_device_index()
sd_card = self.component.sd_card_list_store[sd_card_id][device_index]
else:
sd_card = None
condition = output_size > 0
validate_label(self.component.size_label, condition)
all_valid = all_valid and condition
# check that SD card has a single partition (clean state)
condition = sd_has_single_partition(sd_card, self.logger)
validate_label(self.component.sd_card_label, condition)
all_valid = all_valid and condition
condition = self.update_free_space() >= 0
validate_label(self.component.free_space_name_label, condition)
all_valid = all_valid and condition
......@@ -1700,6 +1849,10 @@ class Application:
self.component.run_window.show()
threading.Thread(target=target, daemon=True).start()
def activate_sd_clean_button(self, button):
has_card = self.component.sd_card_combobox.get_active() != -1
self.component.clean_sd_button.set_visible(has_card)
def sd_card_refresh_button_clicked(self, button):
self.refresh_disk_list()
self.update_free_space()
......
......@@ -36,8 +36,8 @@ from backend.mount import (
guess_next_loop_device,
)
from backend.util import EtcherWriterThread
from backend.util import prevent_sleep, restore_sleep_policy
from backend.mount import can_write_on, allow_write_on, restore_mode
from backend.util import flash_image_with_etcher, prevent_sleep, restore_sleep_policy
from backend.sysreq import host_matches_requirements, requirements_url
......@@ -115,15 +115,6 @@ def run_installation(
else:
previous_loop_mode = None
# Prepare SD Card
if sd_card:
logger.step("Flash clean MBR onto SD-card ({})".format(sd_card))
flash_image_with_etcher(
image_fpath=os.path.join(data.data_dir, "mbr.img"),
device_fpath=sd_card,
logger=logger,
)
# Download Base image
logger.stage("master")
logger.step("Retrieving base image file")
......
......@@ -999,7 +999,7 @@ In File mode the image is created at build path, you can choose the size
<object class="GtkLabel" id="sd_card_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">SD card</property>
<property name="tooltip_text" translatable="yes">SD-card must contain only a single FAT-like partition</property>
<property name="label" translatable="yes">SD card :</property>
</object>
<packing>
......@@ -1012,6 +1012,7 @@ In File mode the image is created at build path, you can choose the size
<object class="GtkBox" id="no_id_18">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="sd_card_refresh_button">
<property name="label" translatable="yes">Refresh</property>
......@@ -1036,6 +1037,19 @@ In File mode the image is created at build path, you can choose the size
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="clean_sd_button">
<property name="label" translatable="yes">Wipe</property>
<property name="tooltip_text" translatable="yes">Wipe SD-card clean</property>
<property name="visible">False</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment