Example #1
0
    def delete_selected_files(self):
        button = QMessageBox.question(
            get_main_window(), 'Delete Files',
            'Irreversibly deleting selected files and directories.',
            QMessageBox.Ok, QMessageBox.Cancel)

        if not self.is_alive() or button != QMessageBox.Ok:
            return

        selected_name_items = self.get_directly_selected_name_items()

        if len(selected_name_items) == 0:
            return

        script_instance_ref = [None]

        def progress_canceled():
            script_instance = script_instance_ref[0]

            if script_instance == None:
                return

            self.script_manager.abort_script(script_instance)

        progress = ExpandingProgressDialog(self)
        progress.set_progress_text_visible(False)
        progress.setModal(True)
        progress.setWindowTitle('Delete Files')
        progress.setLabelText('Collecting files and directories to delete')
        progress.setRange(0, 0)
        progress.canceled.connect(progress_canceled)
        progress.show()

        files_to_delete = []
        dirs_to_delete = []
        all_done = False

        while not all_done:
            all_done = True

            for selected_name_item in list(selected_name_items):
                item_done = False
                parent = selected_name_item.parent()

                while not item_done and parent != None:
                    if parent in selected_name_items:
                        selected_name_items.remove(selected_name_item)
                        item_done = True
                    else:
                        parent = parent.parent()

                if item_done:
                    all_done = False
                    break

        for selected_name_item in selected_name_items:
            path = get_full_item_path(selected_name_item)
            item_type = selected_name_item.data(USER_ROLE_ITEM_TYPE)

            if item_type == ITEM_TYPE_DIRECTORY:
                dirs_to_delete.append(posixpath.join(self.bin_directory, path))
            else:
                files_to_delete.append(posixpath.join(self.bin_directory,
                                                      path))

        message = 'Deleting '

        if len(files_to_delete) == 1:
            message += '1 file '
        elif len(files_to_delete) > 1:
            message += '{0} files '.format(len(files_to_delete))

        if len(dirs_to_delete) == 1:
            if len(files_to_delete) > 0:
                message += 'and '

            message += '1 directory'
        elif len(dirs_to_delete) > 1:
            if len(files_to_delete) > 0:
                message += 'and '

            message += '{0} directories'.format(len(dirs_to_delete))

        progress.setLabelText(message)

        def cb_delete(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            progress.cancel()
            self.refresh_files()

            if aborted:
                QMessageBox.information(get_main_window(), 'Delete Files',
                                        'Delete operation was aborted.')
                return

            report_script_result(
                result, 'Delete Files Error',
                'Could not delete selected files/directories:')

        script_instance_ref[0] = self.script_manager.execute_script(
            'delete',
            cb_delete,
            [json.dumps(files_to_delete),
             json.dumps(dirs_to_delete)],
            execute_as_user=True)
class REDTabImportExportImport(QWidget, Ui_REDTabImportExportImport):
    def __init__(self):
        QWidget.__init__(self)

        self.setupUi(self)

        self.session             = None # Set from REDTabImportExport
        self.script_manager      = None # Set from REDTabImportExport
        self.image_version       = None # Set from REDTabImportExport
        self.refresh_in_progress = False
        self.progress            = None
        self.chunked_uploader    = None

        self.button_browse_archive.clicked.connect(self.browse_archive)
        self.edit_archive.textChanged.connect(self.update_ui_state)
        self.tree_programs.setColumnWidth(0, 150)
        self.tree_programs.setColumnWidth(1, 150)
        self.tree_programs.setColumnWidth(2, 150)
        self.tree_programs.itemSelectionChanged.connect(self.update_ui_state)
        self.button_refresh_programs.clicked.connect(self.refresh_program_list)
        self.button_import.clicked.connect(self.import_archive)

        self.update_ui_state()

    def tab_on_focus(self):
        QTimer.singleShot(1, self.refresh_program_list)

    def tab_off_focus(self):
        pass

    def tab_destroy(self):
        pass

    def update_ui_state(self):
        if self.refresh_in_progress:
            self.edit_archive.setEnabled(False)
            self.button_browse_archive.setEnabled(False)
            self.progress_refresh_programs.setVisible(True)
            self.button_refresh_programs.setText('Refreshing...')
            self.button_refresh_programs.setEnabled(False)
            self.button_import.setEnabled(False)
        else:
            self.edit_archive.setEnabled(True)
            self.button_browse_archive.setEnabled(True)
            self.progress_refresh_programs.setVisible(False)
            self.button_refresh_programs.setText('Refresh')
            self.button_refresh_programs.setEnabled(len(self.edit_archive.text()) > 0)
            self.button_import.setEnabled(len(self.tree_programs.selectedItems()) > 0)

    def browse_archive(self):
        if len(self.edit_archive.text()) > 0:
            last_directory = os.path.dirname(os.path.realpath(self.edit_archive.text()))
        else:
            last_directory = get_home_path()

        filename = get_open_file_name(get_main_window(), 'Open Archive', last_directory, '*.tfrba')

        if len(filename) > 0:
            self.edit_archive.setText(filename)
            self.refresh_program_list()

    def refresh_program_list(self):
        filename = self.edit_archive.text()

        if len(filename) == 0:
            return

        def refresh_async():
            try:
                a = tarfile.open(filename, 'r:gz')
            except Exception as e:
                return [], 'Could not open archive:\n\n{0}'.format(e)

            with contextlib.closing(a):
                try:
                    v = a.extractfile('tfrba-version')
                except Exception as e:
                    return [], 'Could not extract tfrba-version:\n\n{0}'.format(e)

                version = v.read()
                v.close()

                if version != b'1':
                    return [], 'Unknown tfrba-version {0}'.format(version)

                programs = {}

                for member in a.getnames():
                    if member.startswith('programs/') and member.endswith('/program.conf'):
                        try:
                            c = a.extractfile(member)
                        except Exception as e:
                            return [], 'Could not extract {0}:\n\n{1}'.format(member, e)

                        conf = list(map(lambda line: line.decode('utf-8'), c.readlines()))
                        c.close()

                        name         = '<unknown>'
                        identifier   = member.split('/')[1]
                        language     = '<unknown>'
                        first_upload = 0

                        for line in conf:
                            if line.startswith('custom.name ='):
                                try:
                                    name = line[len('custom.name ='):].strip().encode('ascii').decode('unicode_escape')
                                except:
                                    pass
                            elif line.startswith('custom.language ='):
                                try:
                                    language = Constants.get_language_display_name(line[len('custom.language ='):].strip().encode('ascii').decode('unicode_escape'))
                                except:
                                    pass
                            elif line.startswith('custom.first_upload ='):
                                try:
                                    first_upload = int(line[len('custom.first_upload ='):].strip())
                                except:
                                    pass

                        programs[identifier] = [name, identifier, language, 'New', first_upload]

                try:
                    existing_programs = get_lite_programs(self.session)
                except Exception as e:
                    return [], 'Could not get existing program list:\n\n{0}'.format(e)

                for existing_program in existing_programs:
                    identifier = existing_program.identifier

                    if identifier in programs:
                        programs[identifier][3] = 'Existing'

                return programs.values(), None

        def cb_success(result):
            programs, message = result

            if message != None:
                QMessageBox.critical(get_main_window(), 'Import Error', message)
            else:
                sorted_programs = {}

                for program in programs:
                    first_upload = program[4]

                    if first_upload in sorted_programs:
                        sorted_programs[first_upload][program[1]] = program
                    else:
                        sorted_programs[first_upload] = {program[1]: program}

                for first_upload in sorted(sorted_programs.keys()):
                    for identifier in sorted(sorted_programs[first_upload].keys()):
                        program = sorted_programs[first_upload][identifier]
                        item    = QTreeWidgetItem(program[0:4])

                        self.tree_programs.addTopLevelItem(item)

                        item.setSelected(True)

            self.refresh_in_progress = False
            self.update_ui_state()
            self.tree_programs.setFocus()

        def cb_error(error):
            pass # FIXME: report error

        self.refresh_in_progress = True
        self.update_ui_state()
        self.tree_programs.invisibleRootItem().takeChildren()

        async_call(refresh_async, None, cb_success, cb_error, pass_exception_to_error_callback=True)

    def import_archive(self):
        source_path = self.edit_archive.text()

        if len(source_path) == 0:
            return

        script_instance_ref = [None]

        def progress_canceled():
            chunked_uploader = self.chunked_uploader

            if chunked_uploader != None:
                chunked_uploader.canceled = True

            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Import Archive')
        self.progress.setLabelText('Step 1 of 4: Creating import directory')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        import_directory_ref = [None]

        def extract_archive():
            def cb_import_extract(result):
                script_instance = script_instance_ref[0]

                if script_instance != None:
                    aborted = script_instance.abort
                else:
                    aborted = False

                script_instance_ref[0] = None

                if aborted:
                    return

                if not report_script_result(result, 'Import Error', 'Could not extract archive',
                                            before_message_box=self.progress.close):
                    return

                def cb_restart_reboot_shutdown(result):
                    self.progress.close()

                    report_script_result(result, 'Import Error', 'Could not reboot RED Brick to finish program import')

                # step 4/4: reboot
                self.progress.setLabelText('Step 4 of 4: Rebooting RED Brick')
                self.progress.setRange(0, 0)

                self.script_manager.execute_script('restart_reboot_shutdown_systemd',
                                                   cb_restart_reboot_shutdown, ['1'])

                def close_progress():
                    # use a closure to capture self and ansure that it's safe
                    # to call this even if the tab was official destroyed already
                    self.progress.close()

                QTimer.singleShot(1500, close_progress)

            # step 3/4: extract uploaded archive
            self.progress.setLabelText('Step 3 of 4: Extracting archive')
            self.progress.setRange(0, 0)

            script_instance_ref[0] = self.script_manager.execute_script('import_extract', cb_import_extract,
                                                                        [import_directory_ref[0]] + selected_identifiers)

        def cb_import_directory(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(result, 'Import Error', 'Could not create import directory',
                                        before_message_box=self.progress.close):
                return

            # step 2/4: upload archive to temporary import directory
            import_directory_ref[0] = result.stdout.strip()
            target_path             = posixpath.join(import_directory_ref[0], 'archive.tfrba')
            self.chunked_uploader   = ChunkedUploader(self, extract_archive)

            if not self.chunked_uploader.prepare(source_path):
                return

            try:
                target_file = REDFile(self.session).open(target_path, REDFile.FLAG_WRITE_ONLY | REDFile.FLAG_CREATE | REDFile.FLAG_NON_BLOCKING | REDFile.FLAG_EXCLUSIVE, 0o644, 1000, 1000) # FIXME: async_call
            except (Error, REDError) as e:
                QMessageBox.information(get_main_window(), 'Import Error',
                                        'Could not open target file {0}: {1}'.format(target_path, e))
                return

            self.progress.setLabelText('Step 2 of 4: Uploading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_uploader.start(target_path, target_file)

        # step 1/4: create temporary import directory
        script_instance_ref[0] = self.script_manager.execute_script('import_directory', cb_import_directory, execute_as_user=True)
Example #3
0
    def delete_selected_files(self):
        button = QMessageBox.question(get_main_window(), 'Delete Files',
                                      'Irreversibly deleting selected files and directories.',
                                      QMessageBox.Ok, QMessageBox.Cancel)

        if not self.is_alive() or button != QMessageBox.Ok:
            return

        selected_name_items = self.get_directly_selected_name_items()

        if len(selected_name_items) == 0:
            return

        script_instance_ref = [None]

        def progress_canceled():
            script_instance = script_instance_ref[0]

            if script_instance == None:
                return

            self.script_manager.abort_script(script_instance)

        progress = ExpandingProgressDialog(self)
        progress.set_progress_text_visible(False)
        progress.setModal(True)
        progress.setWindowTitle('Delete Files')
        progress.setLabelText('Collecting files and directories to delete')
        progress.setRange(0, 0)
        progress.canceled.connect(progress_canceled)
        progress.show()

        files_to_delete = []
        dirs_to_delete  = []
        all_done        = False

        while not all_done:
            all_done = True

            for selected_name_item in list(selected_name_items):
                item_done = False
                parent = selected_name_item.parent()

                while not item_done and parent != None:
                    if parent in selected_name_items:
                        selected_name_items.remove(selected_name_item)
                        item_done = True
                    else:
                        parent = parent.parent()

                if item_done:
                    all_done = False
                    break

        for selected_name_item in selected_name_items:
            path      = get_full_item_path(selected_name_item)
            item_type = selected_name_item.data(USER_ROLE_ITEM_TYPE)

            if item_type == ITEM_TYPE_DIRECTORY:
                dirs_to_delete.append(posixpath.join(self.bin_directory, path))
            else:
                files_to_delete.append(posixpath.join(self.bin_directory, path))

        message = 'Deleting '

        if len(files_to_delete) == 1:
            message += '1 file '
        elif len(files_to_delete) > 1:
            message += '{0} files '.format(len(files_to_delete))

        if len(dirs_to_delete) == 1:
            if len(files_to_delete) > 0:
                message += 'and '

            message += '1 directory'
        elif len(dirs_to_delete) > 1:
            if len(files_to_delete) > 0:
                message += 'and '

            message += '{0} directories'.format(len(dirs_to_delete))

        progress.setLabelText(message)

        def cb_delete(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            progress.cancel()
            self.refresh_files()

            if aborted:
                QMessageBox.information(get_main_window(), 'Delete Files',
                                        'Delete operation was aborted.')
                return

            report_script_result(result, 'Delete Files Error', 'Could not delete selected files/directories:')

        script_instance_ref[0] = self.script_manager.execute_script('delete', cb_delete,
                                                                    [json.dumps(files_to_delete), json.dumps(dirs_to_delete)],
                                                                    execute_as_user=True)
Example #4
0
            def get_main_classes():
                script_instance_ref = [None]

                def progress_canceled():
                    script_instance = script_instance_ref[0]

                    if script_instance == None:
                        return

                    self.wizard().script_manager.abort_script(script_instance)

                progress = ExpandingProgressDialog(self.wizard())
                progress.set_progress_text_visible(False)
                progress.setWindowTitle('Edit Program')
                progress.setLabelText('Collecting Java main classes')
                progress.setModal(True)
                progress.setRange(0, 0)
                progress.canceled.connect(progress_canceled)
                progress.show()

                def cb_java_main_classes(result):
                    script_instance = script_instance_ref[0]

                    if script_instance != None:
                        aborted = script_instance.abort
                    else:
                        aborted = False

                    script_instance_ref[0] = None

                    def done():
                        progress.cancel()
                        self.combo_main_class.setEnabled(True)
                        self.completeChanged.emit()

                    if aborted:
                        done()
                        return

                    okay, message = check_script_result(result,
                                                        decode_stderr=True)

                    if not okay:
                        self.label_main_class_error.setText(
                            '<b>Error:</b> ' + html.escape(message))
                        self.label_main_class_error.setVisible(True)
                        done()
                        return

                    def expand_async(data):
                        try:
                            main_classes = json.loads(
                                zlib.decompress(
                                    memoryview(data)).decode('utf-8'))

                            if not isinstance(main_classes, dict):
                                main_classes = {}
                        except:
                            main_classes = {}

                        return main_classes

                    def cb_expand_success(main_classes):
                        self.combo_main_class.clear()

                        for cls in sorted(main_classes.keys()):
                            self.combo_main_class.addItem(
                                cls, main_classes[cls])

                        self.combo_main_class_checker.set_current_text(
                            program.cast_custom_option_value(
                                'java.main_class', str, ''))
                        done()

                    def cb_expand_error():
                        self.label_main_class_error.setText(
                            '<b>Error:</b> Internal async error')
                        self.label_main_class_error.setVisible(True)
                        done()

                    async_call(expand_async, result.stdout, cb_expand_success,
                               cb_expand_error)

                script_instance_ref[0] = self.wizard(
                ).script_manager.execute_script('java_main_classes',
                                                cb_java_main_classes,
                                                [self.bin_directory],
                                                max_length=1024 * 1024,
                                                decode_output_as_utf8=False)
    def import_archive(self):
        source_path = self.edit_archive.text()

        if len(source_path) == 0:
            return

        script_instance_ref = [None]

        def progress_canceled():
            chunked_uploader = self.chunked_uploader

            if chunked_uploader != None:
                chunked_uploader.canceled = True

            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Import Archive')
        self.progress.setLabelText('Step 1 of 4: Creating import directory')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        import_directory_ref = [None]

        def extract_archive():
            def cb_import_extract(result):
                script_instance = script_instance_ref[0]

                if script_instance != None:
                    aborted = script_instance.abort
                else:
                    aborted = False

                script_instance_ref[0] = None

                if aborted:
                    return

                if not report_script_result(result, 'Import Error', 'Could not extract archive',
                                            before_message_box=self.progress.close):
                    return

                def cb_restart_reboot_shutdown(result):
                    self.progress.close()

                    report_script_result(result, 'Import Error', 'Could not reboot RED Brick to finish program import')

                # step 4/4: reboot
                self.progress.setLabelText('Step 4 of 4: Rebooting RED Brick')
                self.progress.setRange(0, 0)

                self.script_manager.execute_script('restart_reboot_shutdown_systemd',
                                                   cb_restart_reboot_shutdown, ['1'])

                def close_progress():
                    # use a closure to capture self and ansure that it's safe
                    # to call this even if the tab was official destroyed already
                    self.progress.close()

                QTimer.singleShot(1500, close_progress)

            # step 3/4: extract uploaded archive
            self.progress.setLabelText('Step 3 of 4: Extracting archive')
            self.progress.setRange(0, 0)

            script_instance_ref[0] = self.script_manager.execute_script('import_extract', cb_import_extract,
                                                                        [import_directory_ref[0]] + selected_identifiers)

        def cb_import_directory(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(result, 'Import Error', 'Could not create import directory',
                                        before_message_box=self.progress.close):
                return

            # step 2/4: upload archive to temporary import directory
            import_directory_ref[0] = result.stdout.strip()
            target_path             = posixpath.join(import_directory_ref[0], 'archive.tfrba')
            self.chunked_uploader   = ChunkedUploader(self, extract_archive)

            if not self.chunked_uploader.prepare(source_path):
                return

            try:
                target_file = REDFile(self.session).open(target_path, REDFile.FLAG_WRITE_ONLY | REDFile.FLAG_CREATE | REDFile.FLAG_NON_BLOCKING | REDFile.FLAG_EXCLUSIVE, 0o644, 1000, 1000) # FIXME: async_call
            except (Error, REDError) as e:
                QMessageBox.information(get_main_window(), 'Import Error',
                                        'Could not open target file {0}: {1}'.format(target_path, e))
                return

            self.progress.setLabelText('Step 2 of 4: Uploading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_uploader.start(target_path, target_file)

        # step 1/4: create temporary import directory
        script_instance_ref[0] = self.script_manager.execute_script('import_directory', cb_import_directory, execute_as_user=True)
Example #6
0
class REDTabImportExportImport(QWidget, Ui_REDTabImportExportImport):
    def __init__(self):
        QWidget.__init__(self)

        self.setupUi(self)

        self.session = None  # Set from REDTabImportExport
        self.script_manager = None  # Set from REDTabImportExport
        self.image_version = None  # Set from REDTabImportExport
        self.refresh_in_progress = False
        self.progress = None
        self.chunked_uploader = None

        self.button_browse_archive.clicked.connect(self.browse_archive)
        self.edit_archive.textChanged.connect(self.update_ui_state)
        self.tree_programs.setColumnWidth(0, 150)
        self.tree_programs.setColumnWidth(1, 150)
        self.tree_programs.setColumnWidth(2, 150)
        self.tree_programs.itemSelectionChanged.connect(self.update_ui_state)
        self.button_refresh_programs.clicked.connect(self.refresh_program_list)
        self.button_import.clicked.connect(self.import_archive)

        self.update_ui_state()

    def tab_on_focus(self):
        QTimer.singleShot(1, self.refresh_program_list)

    def tab_off_focus(self):
        pass

    def tab_destroy(self):
        pass

    def update_ui_state(self):
        if self.refresh_in_progress:
            self.edit_archive.setEnabled(False)
            self.button_browse_archive.setEnabled(False)
            self.progress_refresh_programs.setVisible(True)
            self.button_refresh_programs.setText('Refreshing...')
            self.button_refresh_programs.setEnabled(False)
            self.button_import.setEnabled(False)
        else:
            self.edit_archive.setEnabled(True)
            self.button_browse_archive.setEnabled(True)
            self.progress_refresh_programs.setVisible(False)
            self.button_refresh_programs.setText('Refresh')
            self.button_refresh_programs.setEnabled(
                len(self.edit_archive.text()) > 0)
            self.button_import.setEnabled(
                len(self.tree_programs.selectedItems()) > 0)

    def browse_archive(self):
        if len(self.edit_archive.text()) > 0:
            last_directory = os.path.dirname(
                os.path.realpath(self.edit_archive.text()))
        else:
            last_directory = get_home_path()

        filename = get_open_file_name(
            get_main_window(), 'Open Archive', last_directory,
            'Tinkerforge RED-Brick Archives(*.tfrba);;All Files(*)')

        if len(filename) > 0:
            self.edit_archive.setText(filename)
            self.refresh_program_list()

    def refresh_program_list(self):
        filename = self.edit_archive.text()

        if len(filename) == 0:
            return

        def refresh_async():
            try:
                a = tarfile.open(filename, 'r:gz')
            except Exception as e:
                return [], 'Could not open archive:\n\n{0}'.format(e)

            with contextlib.closing(a):
                try:
                    v = a.extractfile('tfrba-version')
                except Exception as e:
                    return [], 'Could not extract tfrba-version:\n\n{0}'.format(
                        e)

                version = v.read()
                v.close()

                if version != b'1':
                    return [], 'Unknown tfrba-version {0}'.format(version)

                programs = {}

                for member in a.getnames():
                    if member.startswith('programs/') and member.endswith(
                            '/program.conf'):
                        try:
                            c = a.extractfile(member)
                        except Exception as e:
                            return [], 'Could not extract {0}:\n\n{1}'.format(
                                member, e)

                        conf = list(
                            map(lambda line: line.decode('utf-8'),
                                c.readlines()))
                        c.close()

                        name = '<unknown>'
                        identifier = member.split('/')[1]
                        language = '<unknown>'
                        first_upload = 0

                        for line in conf:
                            if line.startswith('custom.name ='):
                                try:
                                    name = line[len('custom.name ='):].strip(
                                    ).encode('ascii').decode('unicode_escape')
                                except:
                                    pass
                            elif line.startswith('custom.language ='):
                                try:
                                    language = Constants.get_language_display_name(
                                        line[len('custom.language ='):].strip(
                                        ).encode('ascii').decode(
                                            'unicode_escape'))
                                except:
                                    pass
                            elif line.startswith('custom.first_upload ='):
                                try:
                                    first_upload = int(
                                        line[len('custom.first_upload ='
                                                 ):].strip())
                                except:
                                    pass

                        programs[identifier] = [
                            name, identifier, language, 'New', first_upload
                        ]

                try:
                    existing_programs = get_lite_programs(self.session)
                except Exception as e:
                    return [], 'Could not get existing program list:\n\n{0}'.format(
                        e)

                for existing_program in existing_programs:
                    identifier = existing_program.identifier

                    if identifier in programs:
                        programs[identifier][3] = 'Existing'

                return programs.values(), None

        def cb_success(result):
            programs, message = result

            if message != None:
                QMessageBox.critical(get_main_window(), 'Import Error',
                                     message)
            else:
                sorted_programs = {}

                for program in programs:
                    first_upload = program[4]

                    if first_upload in sorted_programs:
                        sorted_programs[first_upload][program[1]] = program
                    else:
                        sorted_programs[first_upload] = {program[1]: program}

                for first_upload in sorted(sorted_programs.keys()):
                    for identifier in sorted(
                            sorted_programs[first_upload].keys()):
                        program = sorted_programs[first_upload][identifier]
                        item = QTreeWidgetItem(program[0:4])

                        self.tree_programs.addTopLevelItem(item)

                        item.setSelected(True)

            self.refresh_in_progress = False
            self.update_ui_state()
            self.tree_programs.setFocus()

        def cb_error(error):
            pass  # FIXME: report error

        self.refresh_in_progress = True
        self.update_ui_state()
        self.tree_programs.invisibleRootItem().takeChildren()

        async_call(refresh_async,
                   None,
                   cb_success,
                   cb_error,
                   pass_exception_to_error_callback=True)

    def import_archive(self):
        source_path = self.edit_archive.text()

        if len(source_path) == 0:
            return

        script_instance_ref = [None]

        def progress_canceled():
            chunked_uploader = self.chunked_uploader

            if chunked_uploader != None:
                chunked_uploader.canceled = True

            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Import Archive')
        self.progress.setLabelText('Step 1 of 4: Creating import directory')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        import_directory_ref = [None]

        def extract_archive():
            def cb_import_extract(result):
                script_instance = script_instance_ref[0]

                if script_instance != None:
                    aborted = script_instance.abort
                else:
                    aborted = False

                script_instance_ref[0] = None

                if aborted:
                    return

                if not report_script_result(
                        result,
                        'Import Error',
                        'Could not extract archive',
                        before_message_box=self.progress.close):
                    return

                def cb_restart_reboot_shutdown(result):
                    self.progress.close()

                    report_script_result(
                        result, 'Import Error',
                        'Could not reboot RED Brick to finish program import')

                # step 4/4: reboot
                self.progress.setLabelText('Step 4 of 4: Rebooting RED Brick')
                self.progress.setRange(0, 0)

                self.script_manager.execute_script(
                    'restart_reboot_shutdown_systemd',
                    cb_restart_reboot_shutdown, ['1'])

                def close_progress():
                    # use a closure to capture self and ansure that it's safe
                    # to call this even if the tab was official destroyed already
                    self.progress.close()

                QTimer.singleShot(1500, close_progress)

            # step 3/4: extract uploaded archive
            self.progress.setLabelText('Step 3 of 4: Extracting archive')
            self.progress.setRange(0, 0)

            script_instance_ref[0] = self.script_manager.execute_script(
                'import_extract', cb_import_extract,
                [import_directory_ref[0]] + selected_identifiers)

        def cb_import_directory(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(
                    result,
                    'Import Error',
                    'Could not create import directory',
                    before_message_box=self.progress.close):
                return

            # step 2/4: upload archive to temporary import directory
            import_directory_ref[0] = result.stdout.strip()
            target_path = posixpath.join(import_directory_ref[0],
                                         'archive.tfrba')
            self.chunked_uploader = ChunkedUploader(self, extract_archive)

            if not self.chunked_uploader.prepare(source_path):
                return

            try:
                target_file = REDFile(self.session).open(
                    target_path, REDFile.FLAG_WRITE_ONLY | REDFile.FLAG_CREATE
                    | REDFile.FLAG_NON_BLOCKING | REDFile.FLAG_EXCLUSIVE,
                    0o644, 1000, 1000)  # FIXME: async_call
            except (Error, REDError) as e:
                QMessageBox.information(
                    get_main_window(), 'Import Error',
                    'Could not open target file {0}: {1}'.format(
                        target_path, e))
                return

            self.progress.setLabelText('Step 2 of 4: Uploading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_uploader.start(target_path, target_file)

        # step 1/4: create temporary import directory
        script_instance_ref[0] = self.script_manager.execute_script(
            'import_directory', cb_import_directory, execute_as_user=True)
Example #7
0
    def initializePage(self):
        self.set_formatted_sub_title(
            'Specify how the {language} program [{name}] should be executed.')

        self.update_combo_version('java', self.combo_version)

        self.combo_start_mode.setCurrentIndex(
            Constants.DEFAULT_JAVA_START_MODE)
        self.combo_jar_file_selector.reset()
        self.class_path_list_editor.reset()
        self.check_show_class_path.setChecked(False)
        self.check_show_advanced_options.setChecked(False)
        self.combo_working_directory_selector.reset()
        self.option_list_editor.reset()

        # if a program exists then this page is used in an edit wizard
        program = self.wizard().program

        if program != None:
            self.bin_directory = posixpath.join(program.root_directory, 'bin')
        else:
            identifier = self.get_field('identifier')
            self.bin_directory = posixpath.join('/', 'home', 'tf', 'programs',
                                                identifier, 'bin')

        # collect class path entries
        self.class_path_candidates = ['.']

        for filename in sorted(self.wizard().available_files):
            directroy = posixpath.split(filename)[0]

            if len(directroy
                   ) > 0 and directroy not in self.class_path_candidates:
                self.class_path_candidates.append(directroy)

            if filename.endswith('.class') or filename.endswith('.properties'):
                if program == None:
                    self.class_path_list_editor.add_item(directroy)
            elif filename.endswith('.jar'):
                self.class_path_candidates.append(filename)

                if program == None:
                    self.class_path_list_editor.add_item(filename)

        self.class_path_list_editor.add_item(
            '/usr/tinkerforge/bindings/java/Tinkerforge.jar')
        self.class_path_candidates.append(
            '/usr/tinkerforge/bindings/java/Tinkerforge.jar')

        self.combo_main_class.clear()
        self.combo_main_class.clearEditText()

        # collect main classes
        if program != None:
            self.combo_main_class.setEnabled(False)

            def get_main_classes():
                script_instance_ref = [None]

                def progress_canceled():
                    script_instance = script_instance_ref[0]

                    if script_instance == None:
                        return

                    self.wizard().script_manager.abort_script(script_instance)

                progress = ExpandingProgressDialog(self.wizard())
                progress.set_progress_text_visible(False)
                progress.setWindowTitle('Edit Program')
                progress.setLabelText('Collecting Java main classes')
                progress.setModal(True)
                progress.setRange(0, 0)
                progress.canceled.connect(progress_canceled)
                progress.show()

                def cb_java_main_classes(result):
                    script_instance = script_instance_ref[0]

                    if script_instance != None:
                        aborted = script_instance.abort
                    else:
                        aborted = False

                    script_instance_ref[0] = None

                    def done():
                        progress.cancel()
                        self.combo_main_class.setEnabled(True)
                        self.completeChanged.emit()

                    if aborted:
                        done()
                        return

                    okay, message = check_script_result(result,
                                                        decode_stderr=True)

                    if not okay:
                        self.label_main_class_error.setText(
                            '<b>Error:</b> ' + html.escape(message))
                        self.label_main_class_error.setVisible(True)
                        done()
                        return

                    def expand_async(data):
                        try:
                            main_classes = json.loads(
                                zlib.decompress(
                                    memoryview(data)).decode('utf-8'))

                            if not isinstance(main_classes, dict):
                                main_classes = {}
                        except:
                            main_classes = {}

                        return main_classes

                    def cb_expand_success(main_classes):
                        self.combo_main_class.clear()

                        for cls in sorted(main_classes.keys()):
                            self.combo_main_class.addItem(
                                cls, main_classes[cls])

                        self.combo_main_class_checker.set_current_text(
                            program.cast_custom_option_value(
                                'java.main_class', str, ''))
                        done()

                    def cb_expand_error():
                        self.label_main_class_error.setText(
                            '<b>Error:</b> Internal async error')
                        self.label_main_class_error.setVisible(True)
                        done()

                    async_call(expand_async, result.stdout, cb_expand_success,
                               cb_expand_error)

                script_instance_ref[0] = self.wizard(
                ).script_manager.execute_script('java_main_classes',
                                                cb_java_main_classes,
                                                [self.bin_directory],
                                                max_length=1024 * 1024,
                                                decode_output_as_utf8=False)

            # need to decouple this with a timer, otherwise it's executed at
            # a time where the progress bar cannot properly enter model state
            # to block the parent widget
            QTimer.singleShot(0, get_main_classes)
        elif self.wizard().hasVisitedPage(Constants.PAGE_FILES):
            uploads = self.wizard().page(Constants.PAGE_FILES).get_uploads()

            if len(uploads) > 0:
                abort_ref = [False]

                def progress_canceled():
                    abort_ref[0] = True

                progress = ExpandingProgressDialog(self)
                progress.set_progress_text_visible(False)
                progress.setWindowTitle('New Program')
                progress.setLabelText('Collecting Java main classes')
                progress.setModal(True)
                progress.setRange(0, 0)
                progress.canceled.connect(progress_canceled)
                progress.show()

                def cb_main_classes(main_classes):
                    for main_class in main_classes:
                        self.combo_main_class.addItem(main_class)

                    if self.combo_main_class.count() > 1:
                        self.combo_main_class.clearEditText()

                    progress.cancel()

                    self.combo_main_class.setEnabled(True)
                    self.completeChanged.emit()

                def cb_main_classes_error():
                    self.label_main_class_error.setText(
                        '<b>Error:</b> Internal async error')
                    self.label_main_class_error.setVisible(True)

                    progress.cancel()

                    self.combo_main_class.clearEditText()
                    self.combo_main_class.setEnabled(True)
                    self.completeChanged.emit()

                def get_main_classes_async(uploads):
                    return sorted(
                        get_main_classes_from_class_or_jar(uploads, abort_ref))

                self.combo_main_class.setEnabled(False)

                async_call(get_main_classes_async, uploads, cb_main_classes,
                           cb_main_classes_error)

        # if a program exists then this page is used in an edit wizard
        if program != None:
            # start mode
            start_mode_api_name = program.cast_custom_option_value(
                'java.start_mode', str, '<unknown>')
            start_mode = Constants.get_java_start_mode(start_mode_api_name)

            self.combo_start_mode.setCurrentIndex(start_mode)

            # main class
            self.combo_main_class_checker.set_current_text(
                program.cast_custom_option_value('java.main_class', str, ''))

            # jar file
            self.combo_jar_file_selector.set_current_text(
                program.cast_custom_option_value('java.jar_file', str, ''))

            # class path
            self.class_path_list_editor.clear()

            for class_path_entry in program.cast_custom_option_value_list(
                    'java.class_path', str, []):
                self.class_path_list_editor.add_item(class_path_entry)

            # working directory
            self.combo_working_directory_selector.set_current_text(
                program.working_directory)

            # options
            self.option_list_editor.clear()

            for option in program.cast_custom_option_value_list(
                    'java.options', str, []):
                self.option_list_editor.add_item(option)

        self.update_ui_state()
Example #8
0
    def import_archive(self):
        source_path = self.edit_archive.text()

        if len(source_path) == 0:
            return

        script_instance_ref = [None]

        def progress_canceled():
            chunked_uploader = self.chunked_uploader

            if chunked_uploader != None:
                chunked_uploader.canceled = True

            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Import Archive')
        self.progress.setLabelText('Step 1 of 4: Creating import directory')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        import_directory_ref = [None]

        def extract_archive():
            def cb_import_extract(result):
                script_instance = script_instance_ref[0]

                if script_instance != None:
                    aborted = script_instance.abort
                else:
                    aborted = False

                script_instance_ref[0] = None

                if aborted:
                    return

                if not report_script_result(
                        result,
                        'Import Error',
                        'Could not extract archive',
                        before_message_box=self.progress.close):
                    return

                def cb_restart_reboot_shutdown(result):
                    self.progress.close()

                    report_script_result(
                        result, 'Import Error',
                        'Could not reboot RED Brick to finish program import')

                # step 4/4: reboot
                self.progress.setLabelText('Step 4 of 4: Rebooting RED Brick')
                self.progress.setRange(0, 0)

                self.script_manager.execute_script(
                    'restart_reboot_shutdown_systemd',
                    cb_restart_reboot_shutdown, ['1'])

                def close_progress():
                    # use a closure to capture self and ansure that it's safe
                    # to call this even if the tab was official destroyed already
                    self.progress.close()

                QTimer.singleShot(1500, close_progress)

            # step 3/4: extract uploaded archive
            self.progress.setLabelText('Step 3 of 4: Extracting archive')
            self.progress.setRange(0, 0)

            script_instance_ref[0] = self.script_manager.execute_script(
                'import_extract', cb_import_extract,
                [import_directory_ref[0]] + selected_identifiers)

        def cb_import_directory(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(
                    result,
                    'Import Error',
                    'Could not create import directory',
                    before_message_box=self.progress.close):
                return

            # step 2/4: upload archive to temporary import directory
            import_directory_ref[0] = result.stdout.strip()
            target_path = posixpath.join(import_directory_ref[0],
                                         'archive.tfrba')
            self.chunked_uploader = ChunkedUploader(self, extract_archive)

            if not self.chunked_uploader.prepare(source_path):
                return

            try:
                target_file = REDFile(self.session).open(
                    target_path, REDFile.FLAG_WRITE_ONLY | REDFile.FLAG_CREATE
                    | REDFile.FLAG_NON_BLOCKING | REDFile.FLAG_EXCLUSIVE,
                    0o644, 1000, 1000)  # FIXME: async_call
            except (Error, REDError) as e:
                QMessageBox.information(
                    get_main_window(), 'Import Error',
                    'Could not open target file {0}: {1}'.format(
                        target_path, e))
                return

            self.progress.setLabelText('Step 2 of 4: Uploading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_uploader.start(target_path, target_file)

        # step 1/4: create temporary import directory
        script_instance_ref[0] = self.script_manager.execute_script(
            'import_directory', cb_import_directory, execute_as_user=True)
Example #9
0
class REDTabImportExportExport(QWidget, Ui_REDTabImportExportExport):
    def __init__(self):
        QWidget.__init__(self)

        self.setupUi(self)

        self.session             = None # Set from REDTabImportExport
        self.script_manager      = None # Set from REDTabImportExport
        self.image_version       = None # Set from REDTabImportExport
        self.first_tab_on_focus  = True
        self.refresh_in_progress = False
        self.last_directory      = get_home_path()
        self.progress            = None
        self.chunked_downloader  = None

        self.tree_programs.setColumnWidth(0, 150)
        self.tree_programs.setColumnWidth(1, 150)
        self.tree_programs.itemSelectionChanged.connect(self.update_ui_state)
        self.button_refresh_programs.clicked.connect(self.refresh_program_list)
        self.button_export.clicked.connect(self.export_archive)

        self.update_ui_state()

    def tab_on_focus(self):
        QTimer.singleShot(1, self.refresh_program_list)

    def tab_off_focus(self):
        pass

    def tab_destroy(self):
        pass

    def update_ui_state(self):
        if self.refresh_in_progress:
            self.progress_refresh_programs.setVisible(True)
            self.button_refresh_programs.setText('Refreshing...')
            self.button_refresh_programs.setEnabled(False)
            self.button_export.setEnabled(False)
        else:
            self.progress_refresh_programs.setVisible(False)
            self.button_refresh_programs.setText('Refresh')
            self.button_refresh_programs.setEnabled(True)
            self.button_export.setEnabled(len(self.tree_programs.selectedItems()) > 0)

    def refresh_program_list(self):
        def refresh_async():
            return get_lite_programs(self.session)

        def cb_success(programs):
            sorted_programs = {}

            for program in programs:
                first_upload = program.cast_custom_option_value('first_upload', int, 0)

                if first_upload in sorted_programs:
                    sorted_programs[first_upload][program.identifier] = program
                else:
                    sorted_programs[first_upload] = {program.identifier: program}

            for first_upload in sorted(sorted_programs.keys()):
                for identifier in sorted(sorted_programs[first_upload].keys()):
                    program  = sorted_programs[first_upload][identifier]
                    language = program.cast_custom_option_value('language', str, '<unknown>')

                    try:
                        language = Constants.get_language_display_name(language)
                    except:
                        pass

                    item = QTreeWidgetItem([program.cast_custom_option_value('name', str, '<unknown>'), identifier, language])

                    self.tree_programs.addTopLevelItem(item)
                    item.setSelected(True)

            self.refresh_in_progress = False
            self.update_ui_state()
            self.tree_programs.setFocus()

        def cb_error(error):
            pass # FIXME: report error

        self.refresh_in_progress = True
        self.update_ui_state()
        self.tree_programs.invisibleRootItem().takeChildren()

        async_call(refresh_async, None, cb_success, cb_error, pass_exception_to_error_callback=True)

    def export_archive(self):
        #FIXME: fromTime_t is obsolete: https://doc.qt.io/qt-5/qdatetime-obsolete.html#toTime_t
        timestamp   = QDateTime.fromTime_t(int(time.time())).toString('yyyyMMdd-HHmmss')
        target_path = os.path.join(self.last_directory, 'red-brick-export-{0}.tfrba'.format(timestamp))
        target_path = get_save_file_name(get_main_window(), 'Save Archive', target_path, 'Tinkerforge RED-Brick Archive(*.tfrba)')

        if len(target_path) == 0:
            return

        self.last_directory = os.path.split(target_path)[0]
        script_instance_ref = [None]

        def progress_canceled():
            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

            chunked_downloader = self.chunked_downloader

            if chunked_downloader != None:
                chunked_downloader.canceled = True

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Export Archive')
        self.progress.setLabelText('Step 1 of 2: Archiving selected programs')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        def cb_export(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(result, 'Export Error', 'Could not archive selected programs',
                                        before_message_box=self.progress.close):
                return

            # step 2/2: download created archive
            source_path             = posixpath.join(result.stdout.strip(), 'archive.tfrba')
            self.chunked_downloader = ChunkedDownloader(self)

            if not self.chunked_downloader.prepare(source_path):
                return

            self.progress.setLabelText('Step 2 of 2: Downloading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_downloader.start(target_path)

        # step 1/2: run export script to create archive
        script_instance_ref[0] = self.script_manager.execute_script('export', cb_export, selected_identifiers)
Example #10
0
    def export_archive(self):
        #FIXME: fromTime_t is obsolete: https://doc.qt.io/qt-5/qdatetime-obsolete.html#toTime_t
        timestamp   = QDateTime.fromTime_t(int(time.time())).toString('yyyyMMdd-HHmmss')
        target_path = os.path.join(self.last_directory, 'red-brick-export-{0}.tfrba'.format(timestamp))
        target_path = get_save_file_name(get_main_window(), 'Save Archive', target_path, 'Tinkerforge RED-Brick Archive(*.tfrba)')

        if len(target_path) == 0:
            return

        self.last_directory = os.path.split(target_path)[0]
        script_instance_ref = [None]

        def progress_canceled():
            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

            chunked_downloader = self.chunked_downloader

            if chunked_downloader != None:
                chunked_downloader.canceled = True

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Export Archive')
        self.progress.setLabelText('Step 1 of 2: Archiving selected programs')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        def cb_export(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(result, 'Export Error', 'Could not archive selected programs',
                                        before_message_box=self.progress.close):
                return

            # step 2/2: download created archive
            source_path             = posixpath.join(result.stdout.strip(), 'archive.tfrba')
            self.chunked_downloader = ChunkedDownloader(self)

            if not self.chunked_downloader.prepare(source_path):
                return

            self.progress.setLabelText('Step 2 of 2: Downloading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_downloader.start(target_path)

        # step 1/2: run export script to create archive
        script_instance_ref[0] = self.script_manager.execute_script('export', cb_export, selected_identifiers)
class REDTabImportExportExport(QWidget, Ui_REDTabImportExportExport):
    def __init__(self):
        QWidget.__init__(self)

        self.setupUi(self)

        self.session             = None # Set from REDTabImportExport
        self.script_manager      = None # Set from REDTabImportExport
        self.image_version       = None # Set from REDTabImportExport
        self.first_tab_on_focus  = True
        self.refresh_in_progress = False
        self.last_directory      = get_home_path()
        self.progress            = None
        self.chunked_downloader  = None

        self.tree_programs.setColumnWidth(0, 150)
        self.tree_programs.setColumnWidth(1, 150)
        self.tree_programs.itemSelectionChanged.connect(self.update_ui_state)
        self.button_refresh_programs.clicked.connect(self.refresh_program_list)
        self.button_export.clicked.connect(self.export_archive)

        self.update_ui_state()

    def tab_on_focus(self):
        QTimer.singleShot(1, self.refresh_program_list)

    def tab_off_focus(self):
        pass

    def tab_destroy(self):
        pass

    def update_ui_state(self):
        if self.refresh_in_progress:
            self.progress_refresh_programs.setVisible(True)
            self.button_refresh_programs.setText('Refreshing...')
            self.button_refresh_programs.setEnabled(False)
            self.button_export.setEnabled(False)
        else:
            self.progress_refresh_programs.setVisible(False)
            self.button_refresh_programs.setText('Refresh')
            self.button_refresh_programs.setEnabled(True)
            self.button_export.setEnabled(len(self.tree_programs.selectedItems()) > 0)

    def refresh_program_list(self):
        def refresh_async():
            return get_lite_programs(self.session)

        def cb_success(programs):
            sorted_programs = {}

            for program in programs:
                first_upload = program.cast_custom_option_value('first_upload', int, 0)

                if first_upload in sorted_programs:
                    sorted_programs[first_upload][program.identifier] = program
                else:
                    sorted_programs[first_upload] = {program.identifier: program}

            for first_upload in sorted(sorted_programs.keys()):
                for identifier in sorted(sorted_programs[first_upload].keys()):
                    program  = sorted_programs[first_upload][identifier]
                    language = program.cast_custom_option_value('language', str, '<unknown>')

                    try:
                        language = Constants.get_language_display_name(language)
                    except:
                        pass

                    item = QTreeWidgetItem([program.cast_custom_option_value('name', str, '<unknown>'), identifier, language])

                    self.tree_programs.addTopLevelItem(item)
                    item.setSelected(True)

            self.refresh_in_progress = False
            self.update_ui_state()
            self.tree_programs.setFocus()

        def cb_error(error):
            pass # FIXME: report error

        self.refresh_in_progress = True
        self.update_ui_state()
        self.tree_programs.invisibleRootItem().takeChildren()

        async_call(refresh_async, None, cb_success, cb_error, pass_exception_to_error_callback=True)

    def export_archive(self):
        #FIXME: fromTime_t is obsolete: https://doc.qt.io/qt-5/qdatetime-obsolete.html#toTime_t
        timestamp   = QDateTime.fromTime_t(int(time.time())).toString('yyyyMMdd-HHmmss')
        target_path = os.path.join(self.last_directory, 'red-brick-export-{0}.tfrba'.format(timestamp))
        target_path = get_save_file_name(get_main_window(), 'Save Archive', target_path, '*.tfrba')

        if len(target_path) == 0:
            return

        self.last_directory = os.path.split(target_path)[0]
        script_instance_ref = [None]

        def progress_canceled():
            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

            chunked_downloader = self.chunked_downloader

            if chunked_downloader != None:
                chunked_downloader.canceled = True

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Export Archive')
        self.progress.setLabelText('Step 1 of 2: Archiving selected programs')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        def cb_export(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(result, 'Export Error', 'Could not archive selected programs',
                                        before_message_box=self.progress.close):
                return

            # step 2/2: download created archive
            source_path             = posixpath.join(result.stdout.strip(), 'archive.tfrba')
            self.chunked_downloader = ChunkedDownloader(self)

            if not self.chunked_downloader.prepare(source_path):
                return

            self.progress.setLabelText('Step 2 of 2: Downloading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_downloader.start(target_path)

        # step 1/2: run export script to create archive
        script_instance_ref[0] = self.script_manager.execute_script('export', cb_export, selected_identifiers)
    def export_archive(self):
        #FIXME: fromTime_t is obsolete: https://doc.qt.io/qt-5/qdatetime-obsolete.html#toTime_t
        timestamp   = QDateTime.fromTime_t(int(time.time())).toString('yyyyMMdd-HHmmss')
        target_path = os.path.join(self.last_directory, 'red-brick-export-{0}.tfrba'.format(timestamp))
        target_path = get_save_file_name(get_main_window(), 'Save Archive', target_path, '*.tfrba')

        if len(target_path) == 0:
            return

        self.last_directory = os.path.split(target_path)[0]
        script_instance_ref = [None]

        def progress_canceled():
            script_instance = script_instance_ref[0]

            if script_instance != None:
                self.script_manager.abort_script(script_instance)

            chunked_downloader = self.chunked_downloader

            if chunked_downloader != None:
                chunked_downloader.canceled = True

        self.progress = ExpandingProgressDialog(self)
        self.progress.set_progress_text_visible(False)
        self.progress.setModal(True)
        self.progress.setWindowTitle('Export Archive')
        self.progress.setLabelText('Step 1 of 2: Archiving selected programs')
        self.progress.setRange(0, 0)
        self.progress.setAutoClose(False)
        self.progress.canceled.connect(progress_canceled)
        self.progress.show()

        selected_identifiers = []

        for selected_item in self.tree_programs.selectedItems():
            selected_identifiers.append(selected_item.text(1))

        def cb_export(result):
            script_instance = script_instance_ref[0]

            if script_instance != None:
                aborted = script_instance.abort
            else:
                aborted = False

            script_instance_ref[0] = None

            if aborted:
                return

            if not report_script_result(result, 'Export Error', 'Could not archive selected programs',
                                        before_message_box=self.progress.close):
                return

            # step 2/2: download created archive
            source_path             = posixpath.join(result.stdout.strip(), 'archive.tfrba')
            self.chunked_downloader = ChunkedDownloader(self)

            if not self.chunked_downloader.prepare(source_path):
                return

            self.progress.setLabelText('Step 2 of 2: Downloading archive')
            self.progress.set_progress_text_visible(True)
            self.chunked_downloader.start(target_path)

        # step 1/2: run export script to create archive
        script_instance_ref[0] = self.script_manager.execute_script('export', cb_export, selected_identifiers)