def __download_file(self): """ Download file task """ # check values if self.url is None or self.drive is None: self.logger.debug( "No drive or url specified, install process stopped") return False # check if it is local file if self.url.startswith("file://"): # local file, nothing to download but fake download values self.iso = self.url.replace("file://", "") self.status = self.STATUS_DOWNLOADING self.percent = 100 self.total_percent = int(self.percent / 3) return True # init download helper self.dl = Download(self.context.paths.cache, self.__download_callback) # start download self.iso = self.dl.download_from_url(self.url, check_sha256=self.iso_sha256, cache=True) self.dl = None if self.iso is None: return False return True
def get_cached_files(self): """ Returns cached files Returns: list: list of cached files """ dl = Download(self.context.paths.cache) return dl.get_cached_files()
def purge_cached_files(self): """ Purge all cached files Returns: list: list of cached files """ dl = Download(self.context.paths.cache) dl.purge_files(force_all=True) return dl.get_cached_files()
def get_latest_cleep(self): """ Return latest cleep release Returns: tuple: cleep release files and release version:: ( [{ name (string): release name url (string): file url size (int): filesize timestamp (int): timestamp of release }], string: release name (usually version) ) """ # get releases infos from github release = self.github.get_latest_release() self.logger.debug("Cleep release: %s" % release) # check if release exists if not release: # no release found, surely rate limit reached on github api # fallback to cached releases download = Download(self.context.paths.cache) cached_releases = download.get_cached_files() self.logger.debug("Cached releases: %s" % cached_releases) if len(cached_releases) > 0: # sort to keep recent one cached_releases_sorted = sorted(cached_releases, key=itemgetter("timestamp")) cached_releases_sorted.reverse() for cached_release in cached_releases_sorted: if cached_release["filename"].startswith("cleep_"): # and return it return [{ "name": cached_release["filename"], "url": "file://%s" % cached_release["filepath"], "size": cached_release["filesize"], "timestamp": cached_release["timestamp"], }], cached_release["filename"].replace("cleep_", "").replace( ".zip", "") # no cleep cached return None, None else: # no file cached return None, None else: return self.github.get_release_assets_infos( release), release["name"]
def delete_cached_file(self, filename): """ Delete specified cached file Args: filename (string): file path Returns: list: list of cached files """ dl = Download(self.context.paths.cache) dl.delete_cached_file(filename) return dl.get_cached_files()
class Updates(CleepDesktopModule): """ Updates manager: it can update balena-cli and CleepDesktop files """ ETCHER_REPO = {"owner": "tangb", "repo": "etcher-cli"} INSTALL_ETCHER_COMMAND_LINUX = '%s/tools/install-etcher.linux.sh "%s" "%s" "%s"' INSTALL_ETCHER_COMMAND_WINDOWS = ( '%s\\tools\\install-etcher.windows.bat "%s" "%s" "%s"') INSTALL_ETCHER_COMMAND_MAC = '%s/tools/install-etcher.mac.sh "%s" "%s" "%s"' STATUS_IDLE = 0 STATUS_DOWNLOADING = 1 STATUS_INSTALLING = 2 STATUS_DONE = 3 STATUS_ERROR = 4 # ETCHER_VERSION_FORCED = "v1.2.0" ETCHER_VERSION_FORCED = None def __init__(self, context, debug_enabled): """ Constructor Args: context (AppContext): application context debug_enabled (bool): True if debug is enabled """ CleepDesktopModule.__init__(self, context, debug_enabled) # members self.__download_etcher = None self.__current_download = None self.etcher_status = self.STATUS_IDLE self.etcher_download_status = Download.STATUS_IDLE self.etcher_download_percent = 0 self.last_check = 0 self.env = platform.system().lower() self.last_update = 0 def _custom_stop(self): """ Stop process """ self.logger.debug("Stop update") self.cancel_download() def cancel_download(self): """ Cancel current download """ self.logger.debug("Cancel download") self.__download_etcher = None # and cancel current download if self.__current_download: self.__current_download.cancel() def get_status(self): """ Return current update status Returns: dict: current update status informations:: { etcherstatus (dict): etcher update status :: { version (int), status (int), downloadstatus (int), downloadpercent (int) } lastcheck (int): timestamp of last check } """ config = self.context.config.get_config() return { "etcherstatus": { "version": config["config"]["flashtool"]["version"], "status": self.etcher_status, "downloadstatus": self.etcher_download_status, "downloadpercent": self.etcher_download_percent, }, "lastcheck": self.last_check, } def __download_callback(self, status, size, percent): """ Download callback Args: status (int): status from Download class size (int): current downloaded bytes percent (int): current progress """ if self.etcher_status == self.STATUS_DOWNLOADING: self.etcher_download_status = status self.etcher_download_percent = percent self.context.update_ui("updates", self.get_status()) def run(self): """ Start update process. Does nothing until check_updates is called and trigger update if necessary """ self.logger.debug("Updates thread started") # copy cmdlogger to config folder self.__copy_cmdlogger() while self.running: if self.__download_etcher: try: self.logger.info("Downloading Etcher archive %s (%s)" % (self.__download_etcher.filename, self.__download_etcher.url)) # update etcher status self.etcher_status = self.STATUS_DOWNLOADING # new etcher update available self.__current_download = Download( None, self.__download_callback) # download it with no checksum (call is blocking) filepath = self.__current_download.download_from_url( self.__download_etcher.url) # end of dowload, trigger callback and reset member self.__current_download = None # update etcher status if self.etcher_download_status == Download.STATUS_DONE: self.etcher_status = self.STATUS_INSTALLING else: self.etcher_status = self.STATUS_ERROR self.context.update_ui("updates", self.get_status()) if filepath: self.logger.debug("Etcher archive downloaded") # process downloaded archive if not self.__update_etcher(filepath): self.etcher_status = self.STATUS_ERROR self.logger.error("Etcher installation failed") else: self.etcher_status = self.STATUS_DONE self.context.config.set_config_value( "etcher.version", self.__download_etcher.version) self.logger.info( "Etcher installation succeed (installed version is now %s)" % self.__download_etcher.version) self.context.update_ui("updates", self.get_status()) else: # error downloading etcher archive self.logger.error("Failed to download Etcher archive.") self.etcher_status = self.STATUS_ERROR self.context.update_ui("updates", self.get_status()) except: # exception during update self.logger.exception( "Exception during balena-cli update:") self.etcher_status = self.STATUS_ERROR self.context.update_ui("updates", self.get_status()) finally: # end of etcher update process, reset variables self.__download_etcher = None # release CPU time.sleep(1.0) self.logger.debug("Updates thread stopped") def __copy_cmdlogger(self): """ Workaround for this issue on linux only (AppImage) https://github.com/AppImage/AppImageKit/issues/146 Fastest way is to copy full cmdlogger folder to CleepDesktop directory like etcher """ # check OS if self.env != "linux": # problem appears only under linux with AppImage return # always perform a copy to make sure last version is copied try: # prepare paths src = os.path.join(self.context.paths.app, "tools", "cmdlogger-linux") dst = os.path.join(self.context.paths.config, "cmdlogger-linux") # make sure dirs exist os.makedirs(dst, exist_ok=True) # copy full cmdlogger folder content copy_tree(src, dst) except: self.logger.exception( 'Unable to copy cmdlogger from "%s" to "%s"' % (src, dst)) def __get_etcher_version_infos(self, assets): """ Search for file to download for current user environment Args: asset (dict): release assets Returns: tuple (string, string, int): release filename, release url (ready to download) and filesize (in bytes) """ # get environment and architecture pattern = None if self.env == "linux": pattern = "linux-x64" elif self.env == "darwin": pattern = "macos-x64" elif self.env == "windows": pattern = "windows-x64" self.logger.debug("Search release using pattern: %s" % pattern) # search for release for asset in assets: if "browser_download_url" and "size" and "name" in asset.keys(): name = asset["name"].lower() if name.find("balena-cli") >= 0 and name.find(pattern) >= 0: # version found, return infos self.logger.debug("Found release: %s" % asset) return asset["name"], asset["browser_download_url"], asset[ "size"] # nothing found raise Exception("No release info found") def __check_etcher_updates(self, etcher_version): """ Check if etcher updates are available Args: etcher_version (string): current installed etcher version Returns: UpdateInfos: UpdateInfos instance """ infos = UpdateInfos() github = Github(self.ETCHER_REPO["owner"], self.ETCHER_REPO["repo"]) # balena-cli path for test it is installed if self.env == "linux": balenacli_script_path = Install.FLASH_LINUX elif self.env == "darwin": balenacli_script_path = Install.FLASH_MAC elif self.env == "windows": balenacli_script_path = Install.FLASH_WINDOWS # handle forced version if (self.ETCHER_VERSION_FORCED is not None and self.ETCHER_VERSION_FORCED != etcher_version): # force balena-cli installation to specific version self.logger.debug( "Force balena-cli installation (forced version=%s, installed version=%s)" % (self.ETCHER_VERSION_FORCED, etcher_version)) try: # get forced release release = github.get_release(self.ETCHER_VERSION_FORCED) # get download url ( infos.filename, infos.url, infos.size, ) = self.__get_etcher_version_infos(release["assets"]) infos.version = self.ETCHER_VERSION_FORCED infos.update_available = True except: self.logger.exception("Forced balena-cli release not found:") return infos elif self.ETCHER_VERSION_FORCED is not None: # forced version already installed, stop statement return infos # handle latest release try: latest = github.get_latest_release() if latest is None: # probably unable to join github (no internet connection?) self.logger.warning( "Unable to check etcher updates (no internet connection?)") elif not os.path.exists( os.path.join(self.context.paths.config, "balena-cli")) or not os.path.exists( os.path.join(self.context.paths.config, balenacli_script_path)): # balena-cli is not installed self.logger.debug( "No balena-cli found. Installation is necessary") infos.version = latest["tag_name"] infos.update_available = True ( infos.filename, infos.url, infos.size, ) = self.__get_etcher_version_infos(latest["assets"]) elif latest["tag_name"] > etcher_version: # new version available, find cli version for current user platform self.logger.debug( "Update available (online version=%s, installed version=%s)" % (latest["tag_name"], etcher_version)) infos.version = latest["tag_name"] infos.update_available = True ( infos.filename, infos.url, infos.size, ) = self.__get_etcher_version_infos(latest["assets"]) else: self.logger.debug("No new balena-etcher version available") except: self.logger.exception("Latest balena-cli release not found:") infos.error = True return infos def check_updates(self): """ Check for available updates Returns: dict: check output:: { updateavailable (bool): True if update is available lastcheck (int): timestamp of last check } """ # update last check timestamp and versions self.last_check = int(time.time()) # check etcher config = self.context.config.get_config() infos = self.__check_etcher_updates( config["config"]["etcher"]["version"]) self.logger.debug("Check balena-cli version: %s" % infos) if infos.update_available and not infos.error: # set member to trigger download in run function self.__download_etcher = infos update_available = False if self.__download_etcher is not None: update_available = True return { "updateavailable": update_available, "lastcheck": self.last_check } def __update_etcher(self, archive_path): """ Update balena-cli software Args: archive_path (string): etcher archive file path Returns: bool: True if install succeed, False otherwise """ # prepare command command = None if self.env == "linux": command = self.INSTALL_ETCHER_COMMAND_LINUX % ( self.context.paths.app, archive_path, self.context.paths.app, self.context.paths.config, ) elif self.env == "darwin": command = self.INSTALL_ETCHER_COMMAND_MAC % ( self.context.paths.app, archive_path, self.context.paths.app, self.context.paths.config, ) elif self.env == "windows": command = self.INSTALL_ETCHER_COMMAND_WINDOWS % ( self.context.paths.app, archive_path, self.context.paths.app, self.context.paths.config, ) self.logger.debug("Command executed to install balena-cli: %s" % command) # execute command c = Console() resp = c.command(command, 20.0) if resp["error"] or resp["killed"]: self.logger.error("Unable to install balena-cli: stdout: %s" % resp) return False return True
def run(self): """ Start update process. Does nothing until check_updates is called and trigger update if necessary """ self.logger.debug("Updates thread started") # copy cmdlogger to config folder self.__copy_cmdlogger() while self.running: if self.__download_etcher: try: self.logger.info("Downloading Etcher archive %s (%s)" % (self.__download_etcher.filename, self.__download_etcher.url)) # update etcher status self.etcher_status = self.STATUS_DOWNLOADING # new etcher update available self.__current_download = Download( None, self.__download_callback) # download it with no checksum (call is blocking) filepath = self.__current_download.download_from_url( self.__download_etcher.url) # end of dowload, trigger callback and reset member self.__current_download = None # update etcher status if self.etcher_download_status == Download.STATUS_DONE: self.etcher_status = self.STATUS_INSTALLING else: self.etcher_status = self.STATUS_ERROR self.context.update_ui("updates", self.get_status()) if filepath: self.logger.debug("Etcher archive downloaded") # process downloaded archive if not self.__update_etcher(filepath): self.etcher_status = self.STATUS_ERROR self.logger.error("Etcher installation failed") else: self.etcher_status = self.STATUS_DONE self.context.config.set_config_value( "etcher.version", self.__download_etcher.version) self.logger.info( "Etcher installation succeed (installed version is now %s)" % self.__download_etcher.version) self.context.update_ui("updates", self.get_status()) else: # error downloading etcher archive self.logger.error("Failed to download Etcher archive.") self.etcher_status = self.STATUS_ERROR self.context.update_ui("updates", self.get_status()) except: # exception during update self.logger.exception( "Exception during balena-cli update:") self.etcher_status = self.STATUS_ERROR self.context.update_ui("updates", self.get_status()) finally: # end of etcher update process, reset variables self.__download_etcher = None # release CPU time.sleep(1.0) self.logger.debug("Updates thread stopped")
class Install(CleepDesktopModule): """ Install iso helper """ CACHE_DURATION = 900.0 TMP_FILE_PREFIX = "cleep_iso" STATUS_IDLE = 0 STATUS_DOWNLOADING = 1 STATUS_DOWNLOADING_NOSIZE = 2 STATUS_FLASHING = 3 STATUS_VALIDATING = 4 STATUS_REQUEST_WRITE_PERMISSIONS = 5 STATUS_DONE = 6 STATUS_CANCELED = 7 STATUS_ERROR = 8 STATUS_ERROR_INVALIDSIZE = 9 STATUS_ERROR_BADCHECKSUM = 10 STATUS_ERROR_FLASH = 11 STATUS_ERROR_NETWORK = 12 FLASH_LINUX = "balena-cli/flash.sh" FLASH_WINDOWS = "balena-cli\\flash.bat" FLASH_MAC = "balena-cli/flash.sh" CMDLOGGER_LINUX = "cmdlogger-linux/cmdlogger" CMDLOGGER_WINDOWS = "tools\\cmdlogger-windows\\cmdlogger.exe" CMDLOGGER_MAC = "tools/cmdlogger-mac/cmdlogger" RASPIOT_REPO = {"owner": "tangb", "repository": "cleep-os"} def __init__(self, context, debug_enabled): """ Contructor Args: context (AppContext): application context debug_enabled (bool): True if debug is enabled """ CleepDesktopModule.__init__(self, context, debug_enabled) # members self.env = platform.system().lower() self.console = None self.percent = 0 self.__last_percent = 0 self.total_percent = 0 self.eta = 0 self.status = self.STATUS_IDLE self.__last_status = self.STATUS_IDLE self.drive = None self.iso = None self.iso_sha256 = None self.isos = [] self.url = None self.cancel = False self.__etcher_output_pattern = ( r".*(Flashing|Validating)\s\[.*\]\s(\d+)%\seta\s(.*)") self.__flash_output_error = False self.wifi_config = None self.flashable_drives = [] self.github = Github(self.RASPIOT_REPO["owner"], self.RASPIOT_REPO["repository"]) self.raspios = Raspios(self.context.crash_report) self.isos_cached = { "lastupdate": 0, "isos": [], "cleepisos": 0, "raspiosisos": 0, "withraspiosisos": False, "withlocalisos": False, } # prepare specific tools and flash commands if self.env == "windows": self.flash_cmd = os.path.join(self.context.paths.config, self.FLASH_WINDOWS) self.windowsdrives = WindowsDrives() self.windowswirelessinterfaces = WindowsWirelessInterfaces() self.windowswirelessnetworks = WindowsWirelessNetworks() elif self.env == "linux": self.flash_cmd = os.path.join(self.context.paths.config, self.FLASH_LINUX) self.iw = Iw() self.iwlist = Iwlist() self.nmcli = Nmcli() self.lsblk = Lsblk() self.udevadm = Udevadm() elif self.env == "darwin": self.flash_cmd = os.path.join(self.context.paths.config, self.FLASH_MAC) self.diskutil = Diskutil() self.macwirelessinterfaces = MacWirelessInterfaces() self.macwirelessnetworks = MacWirelessNetworks() self.logger.debug("Flash command line: %s" % self.flash_cmd) def _custom_stop(self): """ Stop flash. Called before stopping application """ self.cancel = True def __update_ui(self): """ Update ui if necessary """ if self.__last_percent != self.percent or self.__last_status != self.status: self.context.update_ui("install", self.get_status()) self.__last_percent = self.percent self.__last_status = self.status def run(self): """ Start install background task. Does nothing until start_install is called """ self.logger.debug("Flashdrive thread started") # precache wifi networks at startup self.get_wifi_networks() while self.running: # check if process requested if self.url and self.drive: self.logger.info("Install process started") if self.__download_file(): # update ui self.status = self.STATUS_REQUEST_WRITE_PERMISSIONS self.logger.debug("Status after download: %s" % self.get_status()) self.__update_ui() # file downloaded successfully, launch flash+validation self.__flash_drive() # wait until end of flash (or if user cancel it) while self.console is not None: if self.cancel: break time.sleep(0.25) # end of process if self.cancel: # process canceled self.console.kill() self.status = self.STATUS_CANCELED if self.__flash_output_error: # error occured during flash self.status = self.STATUS_ERROR_FLASH else: # installation succeed self.status = self.STATUS_DONE # update ui self.__update_ui() elif self.cancel: # handle cancelation self.status = self.STATUS_CANCELED else: # download failed. Status should already be setted by __download_file function pass # reset everything self.logger.debug("Reset install variables") self.total_percent = 100 if self.iso and os.path.exists(self.iso): self.logger.debug("Purge downloaded file") dl = Download(self.context.paths.cache) dl.purge_files() self.iso = None self.drive = None self.url = None self.cancel = False self.console = None try: # remove temp wifi config file if self.wifi_config and os.path.exists(self.wifi_config): try: self.logger.debug("Remove wifi config file") os.remove(self.wifi_config) except: self.logger.exception( "Unable to delete wifi config file %s:" % self.wifi_config) except: pass self.logger.info("Install process terminated") # update ui self.__update_ui() else: # no process, release cpu time.sleep(0.25) self.logger.debug("Flashdrive thread stopped") def get_latest_raspios(self): """ Return latest raspios releases Returns: dict: raspios and raspios lite infos:: { raspios: { fileurl (string): file url sha256 (string): sha256 checksum, timestamp (int): timestamp of release }, raspios_lite: { fileurl (string): file url sha256 (string): sha256 checksum, timestamp (int): timestamp of release } } """ raspios_infos = None raspios_lite_infos = None # get releases releases = self.raspios.get_latest_raspios_releases() self.logger.debug("Raspios releases: %s" % releases) # get releases infos if releases["raspios"]: infos = self.raspios.get_raspios_release_infos(releases["raspios"]) if infos["url"] is not None: raspios_infos = infos self.logger.debug("Raspios release infos: %s" % raspios_infos) if releases["raspios_lite"]: infos = self.raspios.get_raspios_release_infos( releases["raspios_lite"]) if infos["url"] is not None: raspios_lite_infos = infos self.logger.debug("Raspios lite release infos: %s" % raspios_lite_infos) return {"raspios": raspios_infos, "raspios_lite": raspios_lite_infos} def get_latest_cleep(self): """ Return latest cleep release Returns: tuple: cleep release files and release version:: ( [{ name (string): release name url (string): file url size (int): filesize timestamp (int): timestamp of release }], string: release name (usually version) ) """ # get releases infos from github release = self.github.get_latest_release() self.logger.debug("Cleep release: %s" % release) # check if release exists if not release: # no release found, surely rate limit reached on github api # fallback to cached releases download = Download(self.context.paths.cache) cached_releases = download.get_cached_files() self.logger.debug("Cached releases: %s" % cached_releases) if len(cached_releases) > 0: # sort to keep recent one cached_releases_sorted = sorted(cached_releases, key=itemgetter("timestamp")) cached_releases_sorted.reverse() for cached_release in cached_releases_sorted: if cached_release["filename"].startswith("cleep_"): # and return it return [{ "name": cached_release["filename"], "url": "file://%s" % cached_release["filepath"], "size": cached_release["filesize"], "timestamp": cached_release["timestamp"], }], cached_release["filename"].replace("cleep_", "").replace( ".zip", "") # no cleep cached return None, None else: # no file cached return None, None else: return self.github.get_release_assets_infos( release), release["name"] def start_install(self, url, drive, wifi): """ Set install data before launching process Args: url (string): url of file to use during install drive (string): drive to install wifi (dict): wifi configuration:: { network (string): network name, password (string): network password, encryption (string): encryption (wpa|wpa2|wep|unsecured), hidden (bool): hidden network } """ if url is None or len(url) == 0: raise Exception('Invalid Url "%s"' % url) if drive is None or len(drive) == 0: raise Exception('Invalid drive "%s"' % url) if self.url is not None or self.drive is not None: raise Exception("Installation is already running") if (wifi and "network" in wifi and (not "password" in wifi or not "encryption" in wifi)): raise Exception("Missing wifi password or encryption value") # get checksum for iso in self.isos_cached["isos"]: if iso["url"] == url: self.logger.debug('Found sha256 "%s" for iso "%s"' % (iso["sha256"], url)) self.iso_sha256 = iso["sha256"] break # generate wifi config file is needed wifi_config = None if wifi and wifi["network"]: self.logger.debug("Start install: wifi infos available") try: # prepare content cleepwificonf = CleepWifiConf() conf = cleepwificonf.create_content( wifi["network"], wifi["password"], wifi["encryption"], wifi["hidden"], ) self.logger.debug("Generated wifi config file: %s" % conf) # write content wifi_config_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) wifi_config = wifi_config_file.name wifi_config_file.write(conf) wifi_config_file.close() except: self.logger.exception("Unable to store wifi config:") wifi_config = None else: self.logger.debug("Start install: no wifi info specified") # store data (setting self.url and self.drive will trigger install in run method) self.url = url self.drive = drive self.wifi_config = wifi_config self.logger.debug( "Start install: install will start with values: %s %s %s" % (self.url, self.drive, self.wifi_config)) def cancel_install(self): """ Cancel current process """ self.logger.debug("Install canceled") self.cancel = True def get_status(self): """ Return current install process percent Returns: int: install process percent """ return { "percent": self.percent, "total_percent": self.total_percent, "status": self.status, "eta": self.eta, } def get_flashable_drives(self): """ Return all flashable drives plugged on computer Returns: list: removable drives list [ { desc (string): drive description path (string): drive path readonly (bool): True if drive is readonly }, ... ] """ if self.env == "windows": self.flashable_drives = self.__get_flashable_drives_windows() elif self.env == "linux": self.flashable_drives = self.__get_flashable_drives_linux() elif self.env == "darwin": self.flashable_drives = self.__get_flashable_drives_mac() return self.flashable_drives def __get_flashable_drives_mac(self): """ Return list of flashbable drives on windows Returns: list: removable drives list [ { desc (string): drive description path (string): drive path readonly (bool): True if drive is readonly size (int): media size }, ... ] """ flashables = [] # get drives drives = self.diskutil.get_devices_infos() self.logger.debug("drives=%s" % drives) # fill flashable drives list for drive in drives: if drives[drive]["removable"]: # save entry flashables.append({ "desc": "%s" % drives[drive]["name"], "path": "%s" % drives[drive]["device"], "readonly": drives[drive]["protected"], "size": drives[drive]["totalsize"], }) return flashables def __get_flashable_drives_windows(self): """ Return list of flashbable drives on windows Returns: list: removable drives list [ { desc (string): drive description path (string): drive path readonly (bool): True if drive is readonly, size (int): media size }, ... ] """ flashables = [] # get system drives drives = self.windowsdrives.get_drives() self.logger.debug("drives=%s" % drives) # fill flashable drives list for drive in drives: if drive["deviceType"] == WindowsDrives.DEVICE_TYPE_REMOVABLE: # save entry flashables.append({ "desc": "%s (%s)" % (drive["description"], drive["displayName"]), "path": "%s" % drive["device"], "readonly": drive["protected"], "size": drive["size"], }) return flashables def __get_flashable_drives_linux(self): """ Return list of flashbable drives on linux Returns: list: removable drives list [ { desc (string): drive description path (string): drive path readonly (bool): True if drive is readonly, size (int): media size }, ... ] """ flashables = [] # get system drives drives = self.lsblk.get_disks() self.logger.debug("drives=%s" % drives) # get drives types for drive in drives: device_type = self.udevadm.get_device_type("/dev/%s" % drive) if device_type in (self.udevadm.TYPE_USB, self.udevadm.TYPE_SDCARD): # get human readble name for drive model = drives[drive]["drivemodel"] if model is None or len(model) == 0: if device_type == self.udevadm.TYPE_USB: desc = "Unknown USB (/dev/%s)" % drive if device_type == self.udevadm.TYPE_SDCARD: desc = "Unknown SD Card (/dev/%s)" % drive else: desc = "%s (/dev/%s)" % (model, drive) # save entry flashables.append({ "desc": desc, "path": "/dev/%s" % drive, "readonly": drives[drive]["readonly"], "size": drives[drive]["size"], }) return flashables def get_isos(self, force_refresh=False): """ Get list of isos file available Args: force_refresh (bool): force refresh Returns: dict: lastupdate (float): last update raspios (bool): with raspios iso, cleepisos (int): number of returned Cleep isos raspiosisos (int): number of returned raspios isos isos (list): list of isos available ordered by date [ { label (string): iso label, url (string): file url, timestamp (int): timestamp of isos, category (string): entry category ('cleep' or 'raspios') sha256 (string): sha256 checksum }, ... ], withraspiosisos (bool): raspios iso flag withlocalisos (bool): local iso flag """ with_raspios_isos = self.context.config.get_config_value( "cleep.isoraspios") with_local_isos = self.context.config.get_config_value( "cleep.isolocal") # return isos from cache refresh_isos = False if force_refresh is True: # refresh forced self.logger.debug("Force refresh isos enabled") refresh_isos = True elif self.isos_cached["lastupdate"] == 0 or len(self.isos) == 0: # force refresh first time self.logger.debug("First isos checking, refresh isos list") refresh_isos = True elif time.time( ) - self.isos_cached["lastupdate"] > self.CACHE_DURATION: # cache duration expired, refresh needed self.logger.debug("Cache expired, refresh isos list") refresh_isos = True elif self.isos_cached["withraspiosisos"] != with_raspios_isos: # preferences changed, refresh needed self.logger.debug("Preferences changes, refresh isos list") refresh_isos = True if not refresh_isos: # no refresh needed, return cache (with updated local isos value) self.logger.debug("Return isos list from cache") self.isos_cached["withlocalisos"] = with_local_isos return self.isos_cached # get cleep latest release isos = [] (cleep_release_file, cleep_release_name) = self.get_latest_cleep() self.logger.debug("Cleep %s: %s" % (cleep_release_name, cleep_release_file)) if cleep_release_file: # search for .zip and .sha256 files latest_cleep = { "label": None, "url": None, "timestamp": 0, "category": "cleep", "sha256": None, } # look for cleep iso files (img and sha256) for file in cleep_release_file: if file["name"].startswith( "cleepos_%s" % cleep_release_name) and file["name"].endswith(".zip"): # image file found latest_cleep["label"] = "Cleep %s" % (cleep_release_name) latest_cleep["timestamp"] = file["timestamp"] latest_cleep["url"] = file["url"] elif file["name"].startswith( "cleepos_%s" % cleep_release_name ) and file["name"].endswith(".sha256"): # checksum file, open it to get its content sha256 = self.github.get_file_content(file["url"]) if sha256 and len(sha256.split()) > 0: latest_cleep["sha256"] = sha256.split()[0] # save cleep iso if latest_cleep["label"] and latest_cleep["timestamp"] != 0: isos.append(latest_cleep) # get raspios isos if with_raspios_isos: raspios = self.get_latest_raspios() self.logger.debug("Raspios: %s" % raspios) if raspios["raspios_lite"] is not None: isos.append({ "label": "Raspios Lite", "url": raspios["raspios_lite"]["url"], "timestamp": raspios["raspios_lite"]["timestamp"], "category": "raspios", "sha256": raspios["raspios_lite"]["sha256"], }) if raspios["raspios"] is not None: isos.append({ "label": "Raspios desktop", "url": raspios["raspios"]["url"], "timestamp": raspios["raspios"]["timestamp"], "category": "raspios", "sha256": raspios["raspios"]["sha256"], }) self.logger.debug("Isos: %s" % isos) self.isos = sorted(isos, key=lambda i: i["timestamp"]) cleep_isos = 0 raspios_isos = 0 for iso in self.isos: if iso["category"] == "cleep": cleep_isos += 1 elif iso["category"] == "raspios": raspios_isos += 1 # save new cache self.isos_cached = { "lastupdate": time.time(), "isos": self.isos, "cleepisos": cleep_isos, "raspiosisos": raspios_isos, "withraspiosisos": with_raspios_isos, "withlocalisos": with_local_isos, } return self.isos_cached def __download_callback(self, status, filesize, percent): """ Download status callback Args: status (int): current download status filesize (int): downloaded filesize percent (int): percent of download """ # adjust internal status according to download status if status == Download.STATUS_IDLE: self.status = self.STATUS_DOWNLOADING elif status == Download.STATUS_DOWNLOADING: self.status = self.STATUS_DOWNLOADING elif status == Download.STATUS_DOWNLOADING_NOSIZE: self.status = self.STATUS_DOWNLOADING_NOSIZE elif status == Download.STATUS_ERROR: self.status = self.STATUS_ERROR elif status == Download.STATUS_ERROR_INVALIDSIZE: self.status = self.STATUS_ERROR_INVALIDSIZE elif status == Download.STATUS_ERROR_BADCHECKSUM: self.status = self.STATUS_ERROR_BADCHECKSUM elif status == Download.STATUS_ERROR_NETWORK: self.status = self.STATUS_ERROR_NETWORK elif status == Download.STATUS_CANCELED: self.status = self.STATUS_CANCELED elif status == Download.STATUS_DONE: self.status = self.STATUS_DOWNLOADING # save current progress percentage self.percent = percent self.total_percent = int(self.percent / 3) # save eta self.eta = "%.1fMo" % (float(filesize) / 1000000.0) if self.cancel: # cancel download if self.dl: self.dl.cancel() # update ui self.__update_ui() def __download_file(self): """ Download file task """ # check values if self.url is None or self.drive is None: self.logger.debug( "No drive or url specified, install process stopped") return False # check if it is local file if self.url.startswith("file://"): # local file, nothing to download but fake download values self.iso = self.url.replace("file://", "") self.status = self.STATUS_DOWNLOADING self.percent = 100 self.total_percent = int(self.percent / 3) return True # init download helper self.dl = Download(self.context.paths.cache, self.__download_callback) # start download self.iso = self.dl.download_from_url(self.url, check_sha256=self.iso_sha256, cache=True) self.dl = None if self.iso is None: return False return True def __install_callback(self, stdout, stderr): """ Install process callback Args: stdout (string): stdout message stderr (string): stderr message """ # handle current flasing/validating status try: # self.logger.debug('Install stdout=%s' % stdout) matches = re.finditer(self.__etcher_output_pattern, stdout, re.UNICODE | re.DOTALL) for _, match in enumerate(matches): group = match.group().strip() if len(group) > 0 and len(match.groups()) > 0: items = match.groups() # current operation percent try: self.percent = int(items[1]) except: self.percent = 0 # status if items[0] == "Flashing": self.status = self.STATUS_FLASHING elif items[0] == "Validating": self.status = self.STATUS_VALIDATING else: self.status = self.STATUS_VALIDATING # total percent if self.status == self.STATUS_FLASHING: self.total_percent = 33 + int(self.percent / 3) else: self.total_percent = 66 + int(self.percent / 3) # eta self.eta = items[2] # update ui self.__update_ui() except: if not self.__flash_output_error: self.context.crash_report.report_exception() self.logger.exception( "Exception occured during install callback:") self.__flash_output_error = True def __install_end_callback(self): """ Install process ended callback """ if self.console is None: # process surely canceled return # get console return code return_code = self.console.get_return_code() self.logger.info("Install process terminated with return code %s" % return_code) # check return code if return_code != 0: # install failed self.logger.error( "Install failed. Return code awaited is 0, received %s" % return_code) self.__flash_output_error = True else: # reset console and set status self.__flash_output_error = False # update ui self.__update_ui() # reset console self.console = None def __flash_drive(self): """ Flash drive """ if self.console is not None: raise Exception("Flashing operation is already running") self.status = self.STATUS_FLASHING try: # fix wifi config value, must be string wifi_config = self.wifi_config if wifi_config is None: wifi_config = "" # prepare command line cmd = [ self.flash_cmd, self.context.paths.config, self.drive, self.iso, wifi_config, ] self.logger.debug("Flash command to execute: %s" % cmd) # start command in admin endless console self.console = AdminEndlessConsole(cmd, self.__install_callback, self.__install_end_callback) if self.env == "windows": self.console.set_cmdlogger( os.path.join(self.context.paths.app, self.CMDLOGGER_WINDOWS)) elif self.env == "darwin": self.console.set_cmdlogger( os.path.join(self.context.paths.app, self.CMDLOGGER_MAC)) else: # workaround for AppImage issue https://github.com/AppImage/AppImageKit/issues/146 # cmdlogger is copied under config directory like etcher self.console.set_cmdlogger( os.path.join(self.context.paths.config, self.CMDLOGGER_LINUX)) self.console.start() except: self.logger.exception("Exception occured during drive flashing:") self.__flash_output_error = True self.console = None def get_wifi_adapter(self): """ Return wifi adapter status: exists or not Returns: dict: adapter status:: { adapter (bool): True if wifi adapter found } """ if self.env == "windows": adapter = self.__get_wifi_adapter_windows() elif self.env == "darwin": adapter = self.__get_wifi_adapter_mac() else: adapter = self.__get_wifi_adapter_linux() return {"adapter": adapter} def __get_wifi_adapter_linux(self): """ Return wifi adapter status under linux Returns: bool: True if adapter exists """ # system check if not self.iw.is_installed(): return False # get wifi interfaces wifi_adapters = self.iw.get_adapters() return len(wifi_adapters.keys()) > 0 def __get_wifi_adapter_windows(self): """ Return wifi adapter status under windows Returns: bool: True if adapter exists """ # handle supported windows version supported = False try: release = int(platform.release()) if release >= 10: supported = True except: self.logger.exception( "Unable to get wifi adapter status under windows:") if not supported: return False # get wifi interfaces wifi_interfaces = self.windowswirelessinterfaces.get_interfaces() return len(wifi_interfaces) > 0 def __get_wifi_adapter_mac(self): """ Return wifi adapter status under macos Returns: bool: True if adapter exists """ # system check if not self.macwirelessinterfaces.is_installed(): return False # get wifi interfaces wifi_interfaces = self.macwirelessinterfaces.get_interfaces() return len(wifi_interfaces) > 0 def get_wifi_networks(self): """ Return wifi networks and wifi infos Returns: dict: wifi infos:: { networks (list): networks list } """ if self.env == "windows": networks = self.__get_wifi_networks_windows() elif self.env == "darwin": networks = self.__get_wifi_networks_mac() else: networks = self.__get_wifi_networks_linux() # sort wifi networks by name networks["networks"] = sorted(networks["networks"], key=lambda x: x["network"]) self.logger.debug("wifi networks: %s" % networks) return networks def __get_wifi_networks_linux(self): """ Return wifi networks and wifi infos for linux Returns: dict: wifi infos:: { networks (list): networks list } """ wifi_networks = [] if self.nmcli.is_installed(): # priority to nmcli if installed self.logger.debug("Use nmcli to find wifi networks") interfaces = self.nmcli.get_wifi_interfaces() self.logger.debug("nmcli wifi interfaces: %s" % interfaces) if len(interfaces) > 0: # keep only first interface interface = interfaces[0] networks = self.nmcli.get_wifi_networks(interface) self.logger.debug("nmcli wifi networks: %s" % networks) # flatten dict wifi_networks = [v for k, v in networks.items()] elif self.iw.is_installed() and self.iwlist.is_installed(): # otherwise fallback to iw/iwlist commands self.logger.debug("Use iw to find wifi networks") # get wifi interfaces wifi_connections = self.iw.get_connections() self.logger.debug("wifi_connections: %s" % wifi_connections) # get wifi networks if len(wifi_connections.keys()) > 0: # keep only first wifi interface interface = list(wifi_connections.keys())[0] networks = self.iwlist.get_networks(interface) # flatten dict wifi_networks = [v for k, v in networks.items()] else: self.logger.info( "No system command available to get list of wifi networks. Consider wifi is not available on this computer" ) # build output return {"networks": wifi_networks} def __get_wifi_networks_windows(self): """ Return wifi networks and wifi infos for windows 10 and above only Returns: dict: wifi infos:: { networks (list): networks list adapter (bool): True if wifi adapter found } """ default = {"networks": []} # handle supported windows version supported = False try: release = int(platform.release()) if release >= 10: supported = True else: self.logger.warning( "Unable to get list of wifi networks, only windows>=10 is supported" ) except: self.logger.exception("Unable to get list of wifi networks:") if not supported: return default # get wifi interfaces wifi_interfaces = self.windowswirelessinterfaces.get_interfaces() self.logger.debug("wifi_interfaces: %s" % wifi_interfaces) # get wifi networks wifi_networks = [] if len(wifi_interfaces) > 0: interface = wifi_interfaces[0] networks = self.windowswirelessnetworks.get_networks(interface) self.logger.debug("networks: %s" % networks) # flatten dict wifi_networks = [v for k, v in networks.items()] # build output return {"networks": wifi_networks, "adapter": len(wifi_interfaces) > 0} def __get_wifi_networks_mac(self): """ Return wifi networks and wifi infos for macos Returns: dict: wifi infos:: { networks (list): networks list adapter (bool): True if wifi adapter found } """ default = {"networks": [], "adapter": False} # system check if not self.macwirelessinterfaces.is_installed(): self.logger.warning( "MacWirelessInterfaces associated command not found on your system, unable to get list of wifi networks" ) return default elif not self.macwirelessnetworks.is_installed(): self.logger.warning( "MacWirelessNetworks associated command not found on your system, unable to get list of wifi networks" ) return default # get wifi interfaces wifi_interfaces = self.macwirelessinterfaces.get_interfaces() self.logger.debug("wifi_interfaces: %s" % wifi_interfaces) # get wifi networks wifi_networks = [] if len(wifi_interfaces) > 0: # keep only first wifi interface interface = wifi_interfaces[0] networks = self.macwirelessnetworks.get_networks(interface) # flatten dict wifi_networks = [v for k, v in networks.items()] # build output return {"networks": wifi_networks, "adapter": len(wifi_interfaces) > 0}
def run(self): """ Start install background task. Does nothing until start_install is called """ self.logger.debug("Flashdrive thread started") # precache wifi networks at startup self.get_wifi_networks() while self.running: # check if process requested if self.url and self.drive: self.logger.info("Install process started") if self.__download_file(): # update ui self.status = self.STATUS_REQUEST_WRITE_PERMISSIONS self.logger.debug("Status after download: %s" % self.get_status()) self.__update_ui() # file downloaded successfully, launch flash+validation self.__flash_drive() # wait until end of flash (or if user cancel it) while self.console is not None: if self.cancel: break time.sleep(0.25) # end of process if self.cancel: # process canceled self.console.kill() self.status = self.STATUS_CANCELED if self.__flash_output_error: # error occured during flash self.status = self.STATUS_ERROR_FLASH else: # installation succeed self.status = self.STATUS_DONE # update ui self.__update_ui() elif self.cancel: # handle cancelation self.status = self.STATUS_CANCELED else: # download failed. Status should already be setted by __download_file function pass # reset everything self.logger.debug("Reset install variables") self.total_percent = 100 if self.iso and os.path.exists(self.iso): self.logger.debug("Purge downloaded file") dl = Download(self.context.paths.cache) dl.purge_files() self.iso = None self.drive = None self.url = None self.cancel = False self.console = None try: # remove temp wifi config file if self.wifi_config and os.path.exists(self.wifi_config): try: self.logger.debug("Remove wifi config file") os.remove(self.wifi_config) except: self.logger.exception( "Unable to delete wifi config file %s:" % self.wifi_config) except: pass self.logger.info("Install process terminated") # update ui self.__update_ui() else: # no process, release cpu time.sleep(0.25) self.logger.debug("Flashdrive thread stopped")