async def test_login_no_available_devices(aqtbot, gui_factory, autoclose_dialog, core_config, alice): password = "******" save_device_with_password_in_config(core_config.config_dir, alice, password) device = list_available_devices(core_config.config_dir)[0] gui = await gui_factory() ParsecApp.add_connected_device(device.organization_id, device.device_id) lw = gui.test_get_login_widget() lw.reload_devices() def _devices_listed(): assert lw.widget.layout().count() > 0 await aqtbot.wait_until(_devices_listed) no_device_w = lw.widget.layout().itemAt(0).widget() assert isinstance(no_device_w, LoginNoDevicesWidget) # 0 is spacer, 1 is label assert no_device_w.layout().itemAt( 2).widget().text() == "Create an organization" assert no_device_w.layout().itemAt( 3).widget().text() == "Join an organization" assert no_device_w.layout().itemAt(4).widget().text() == "Recover a device"
def on_core_run_done(self): assert self.running_core_job.is_finished() if self.core: ParsecApp.remove_connected_device( self.core.device.organization_addr.organization_id, self.core.device.device_id) self.core.event_bus.disconnect(CoreEvent.GUI_CONFIG_CHANGED, self.on_core_config_updated) self.running_core_job = None self.logged_out.emit()
def on_run_core_ready(self, core, core_jobs_ctx): self.core = core self.core_jobs_ctx = core_jobs_ctx self.core.event_bus.connect(CoreEvent.GUI_CONFIG_CHANGED, self.on_core_config_updated) self.event_bus.send( CoreEvent.GUI_CONFIG_CHANGED, gui_last_device=self.core.device.device_id.str ) ParsecApp.add_connected_device( self.core.device.organization_addr.organization_id, self.core.device.device_id ) self.logged_in.emit()
def on_core_run_done(self): assert self.running_core_job.is_finished() if self.core: ParsecApp.remove_connected_device( self.core.device.organization_addr.organization_id, self.core.device.device_id) self.core.event_bus.disconnect("gui.config.changed", self.on_core_config_updated) self.running_core_job = None self.core_jobs_ctx = None self.core = None self.logged_out.emit()
def on_run_core_ready(self, core, core_jobs_ctx): self.core = core self.core_jobs_ctx = core_jobs_ctx self.core.event_bus.connect("gui.config.changed", self.on_core_config_updated) self.event_bus.send( "gui.config.changed", gui_last_device="{}:{}".format( self.core.device.organization_addr.organization_id, self.core.device.device_id), ) ParsecApp.add_connected_device( self.core.device.organization_addr.organization_id, self.core.device.device_id) self.logged_in.emit()
def __init__(self, center_widget, title, parent, hide_close=False): super().__init__(None) self.setupUi(self) self.setModal(True) self.setObjectName("GreyedDialog") self.setWindowModality(Qt.ApplicationModal) self.button_close.apply_style() if platform.system() == "Windows": # SplashScreen on Windows freezes the Window self.setWindowFlags(Qt.FramelessWindowHint) else: # FramelessWindowHint on Linux (at least xfce) is less pretty self.setWindowFlags(Qt.SplashScreen) self.setAttribute(Qt.WA_TranslucentBackground) self.center_widget = center_widget self.main_layout.addWidget(center_widget) if not title and hide_close: self.widget_title.hide() if title: self.label_title.setText(title) if hide_close: self.button_close.hide() main_win = ParsecApp.get_main_window() if main_win: if main_win.isVisible(): self.setParent(main_win) self.resize(main_win.size()) else: self.showMaximized() self.move(0, 0) else: logger.error("GreyedDialog did not find the main window, this is probably a bug") self.setFocus() self.accepted.connect(self.on_finished) self.rejected.connect(self.on_finished)
def reload_devices(self): while self.combo_username.count(): self.combo_username.removeItem(0) # Display devices in `<organization>:<user>@<device>` format self.devices = {} for available_device in list_available_devices(self.config.config_dir): if not ParsecApp.is_device_connected( available_device.organization_id, available_device.device_id): name = f"{available_device.organization_id}: {available_device.user_display} @ {available_device.device_display}" self.combo_username.addItem(name) self.devices[name] = available_device last_device = self.config.gui_last_device if last_device and last_device in self.devices: self.combo_username.setCurrentText(last_device) if len(self.devices): self.widget_no_device.hide() self.widget_login.show() else: self.widget_no_device.show() self.widget_login.hide() if ParsecApp.connected_devices: self.label_no_device.setText( _("TEXT_LOGIN_NO_AVAILABLE_DEVICE")) else: self.label_no_device.setText( _("TEXT_LOGIN_NO_DEVICE_ON_MACHINE"))
async def login_with_smartcard(self, key_file): message = None exception = None try: device = await load_device_with_smartcard(key_file) if ParsecApp.is_device_connected( device.organization_addr.organization_id, device.device_id ): message = _("TEXT_LOGIN_ERROR_ALREADY_CONNECTED") else: self.start_core(device) except LocalDeviceCertificatePinCodeUnavailableError: # User cancelled the prompt self.login_failed.emit() except LocalDeviceError as exc: message = _("TEXT_LOGIN_ERROR_AUTHENTICATION_FAILED") exception = exc except ModuleNotFoundError as exc: message = _("TEXT_LOGIN_SMARTCARD_NOT_AVAILABLE") exception = exc except (RuntimeError, MountpointConfigurationError, MountpointDriverCrash) as exc: message = _("TEXT_LOGIN_MOUNTPOINT_ERROR") exception = exc except Exception as exc: message = _("TEXT_LOGIN_UNKNOWN_ERROR") exception = exc logger.exception("Unhandled error during login") finally: if message: show_error(self, message, exception=exception) self.login_failed.emit()
def login_with_password(self, key_file, password): message = None exception = None try: device = load_device_with_password(key_file, password) if ParsecApp.is_device_connected( device.organization_addr.organization_id, device.device_id ): message = _("TEXT_LOGIN_ERROR_ALREADY_CONNECTED") else: self.start_core(device) except LocalDeviceError as exc: message = _("TEXT_LOGIN_ERROR_AUTHENTICATION_FAILED") exception = exc except (RuntimeError, MountpointConfigurationError, MountpointDriverCrash) as exc: message = _("TEXT_LOGIN_MOUNTPOINT_ERROR") exception = exc except Exception as exc: message = _("TEXT_LOGIN_UNKNOWN_ERROR") exception = exc logger.exception("Unhandled error during login") finally: if message: show_error(self, message, exception=exception) self.login_failed.emit()
async def test_login_no_available_devices(aqtbot, gui_factory, autoclose_dialog, core_config, alice, qt_thread_gateway): password = "******" save_device_with_password(core_config.config_dir, alice, password) device = list_available_devices(core_config.config_dir)[0] gui = await gui_factory() ParsecApp.add_connected_device(device.organization_id, device.device_id) lw = gui.test_get_login_widget() def _reload_devices(): lw.reload_devices() await qt_thread_gateway.send_action(_reload_devices) no_device_w = lw.widget.layout().itemAt(0).widget() assert isinstance(no_device_w, LoginNoDevicesWidget)
def start_core(self, device): assert not self.running_core_job assert not self.core assert not self.core_jobs_ctx self.config = ParsecApp.get_main_window().config self.running_core_job = self.jobs_ctx.submit_job( self.run_core_success, self.run_core_error, _do_run_core, self.config, device, self.run_core_ready, )
def start_core(self, device): assert not self.running_core_job assert not self.core assert not self.core_jobs_ctx self.config = ParsecApp.get_main_window().config self.running_core_job = self.jobs_ctx.submit_job( ThreadSafeQtSignal(self, "run_core_success"), ThreadSafeQtSignal(self, "run_core_error"), _do_run_core, self.config, device, ThreadSafeQtSignal(self, "run_core_ready", object, object), )
def move_popup(self): main_window = ParsecApp.get_main_window() if not main_window: return offset = 10 height = 101 if platform.system() == "Windows" else 75 width = min(500, main_window.size().width() - 40) self.resize(QSize(width, height)) x = main_window.size().width() - width - 20 y = main_window.size().height() - ((height + offset) * (self.index + 1)) # Hide the snackbar if the main window does not have enough space to show it self.set_visible(y > 30) pos = main_window.mapToGlobal(QPoint(x, y)) self.setGeometry(pos.x(), pos.y(), width, height)
def run_gui(config: CoreConfig, start_arg: Optional[str] = None, diagnose: bool = False): logger.info("Starting UI") # Needed for High DPI usage of QIcons, otherwise only QImages are well scaled QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) # The parsec app needs to be instanciated before qtrio runs in order # to be the default QApplication instance app = ParsecApp() assert QApplication.instance() is app return qtrio.run(_run_gui, app, config, start_arg, diagnose)
def __init__( self, center_widget, title, parent, hide_close=False, width=None, close_on_click=False ): super().__init__(None) self.setupUi(self) self.setModal(True) self.setObjectName("GreyedDialog") self.setWindowModality(Qt.ApplicationModal) self.button_close.apply_style() self.close_on_click = close_on_click if sys.platform == "win32": # SplashScreen on Windows freezes the Window self.setWindowFlags(Qt.FramelessWindowHint) else: # FramelessWindowHint on Linux (at least xfce) is less pretty self.setWindowFlags(Qt.SplashScreen) self.setAttribute(Qt.WA_TranslucentBackground) self.center_widget = center_widget self.main_layout.addWidget(center_widget) if not title and hide_close: self.widget_title.hide() if title: self.label_title.setText(title) if hide_close: self.button_close.hide() main_win = ParsecApp.get_main_window() if width: if width < main_win.size().width(): spacing = int((main_win.size().width() - width) / 2) self._get_spacer_right().changeSize( spacing, 0, QSizePolicy.Preferred, QSizePolicy.Preferred ) self._get_spacer_left().changeSize( spacing, 0, QSizePolicy.Preferred, QSizePolicy.Preferred ) if main_win: if main_win.isVisible(): self.setParent(main_win) self.resize(main_win.size()) else: self.showMaximized() self.move(0, 0) elif parent is not None: logger.error("GreyedDialog did not find the main window, this is probably a bug") self.setFocus() self.accepted.connect(self.on_finished) self.rejected.connect(self.on_finished)
def list_devices_and_enrollments(self): pendings = PkiEnrollmentSubmitterSubmittedCtx.list_from_disk( config_dir=self.config.config_dir) devices = [ device for device in list_available_devices(self.config.config_dir) if not ParsecApp.is_device_connected(device.organization_id, device.device_id) ] if not len(devices) and not len(pendings): no_device_widget = LoginNoDevicesWidget() no_device_widget.create_organization_clicked.connect( self.create_organization_clicked.emit) no_device_widget.join_organization_clicked.connect( self.join_organization_clicked.emit) no_device_widget.recover_device_clicked.connect( self.recover_device_clicked.emit) self.widget.layout().addWidget(no_device_widget) no_device_widget.setFocus() elif len(devices) == 1 and not len(pendings): self._on_account_clicked(devices[0], hide_back=True) else: # If the GUI has a last used device, we look for it in our devices list # and insert it to the front, so it will be shown first if self.config.gui_last_device: last_used = next( (d for d in devices if d.device_id.str == self.config.gui_last_device), None) if last_used: devices.remove(last_used) devices.insert(0, last_used) accounts_widget = LoginAccountsWidget(self.config, self.jobs_ctx, devices, pendings) accounts_widget.account_clicked.connect(self._on_account_clicked) accounts_widget.pending_finalize_clicked.connect( self._on_pending_finalize_clicked) accounts_widget.pending_clear_clicked.connect( self._on_pending_clear_clicked) self.widget.layout().addWidget(accounts_widget) accounts_widget.setFocus()
def reload_devices(self): self._clear_widget() devices = [ device for device in list_available_devices(self.config.config_dir) if not ParsecApp.is_device_connected(device.organization_id, device.device_id) ] if not len(devices): no_device_widget = LoginNoDevicesWidget() no_device_widget.create_organization_clicked.connect( self.create_organization_clicked.emit) no_device_widget.join_organization_clicked.connect( self.join_organization_clicked.emit) self.widget.layout().addWidget(no_device_widget) no_device_widget.setFocus() elif len(devices) == 1: self._on_account_clicked(devices[0], hide_back=True) else: accounts_widget = LoginAccountsWidget(devices) accounts_widget.account_clicked.connect(self._on_account_clicked) self.widget.layout().addWidget(accounts_widget) accounts_widget.setFocus()
def reload_devices(self): while self.combo_username.count(): self.combo_username.removeItem(0) devices = list_available_devices(self.config.config_dir) # Display devices in `<organization>:<device_id>` format self.devices = {} for o, d, t, kf in devices: if not ParsecApp.is_device_connected(o, d): self.combo_username.addItem(f"{o}:{d}") self.devices[f"{o}:{d}"] = (o, d, t, kf) last_device = self.config.gui_last_device if last_device and last_device in self.devices: self.combo_username.setCurrentText(last_device) if len(self.devices): self.widget_no_device.hide() self.widget_login.show() else: self.widget_no_device.show() self.widget_login.hide() if ParsecApp.connected_devices: self.label_no_device.setText(_("TEXT_LOGIN_NO_AVAILABLE_DEVICE")) else: self.label_no_device.setText(_("TEXT_LOGIN_NO_DEVICE_ON_MACHINE"))
def switch_to_tab(self, idx): if not ParsecApp.has_active_modal(): self.tab_center.setCurrentIndex(idx)
def _inner_proxy(): if ParsecApp.has_active_modal(): return funct()
def right_main_window(): assert ParsecApp.get_main_window() is main_w
async def _run_gui(app: ParsecApp, config: CoreConfig, start_arg: str = None, diagnose: bool = False): app.load_stylesheet() app.load_font() lang_key = lang.switch_language(config) event_bus = EventBus() async with run_trio_job_scheduler() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, quit_callback=jobs_ctx.close, event_bus=event_bus, config=config, minimize_on_close=config.gui_tray_enabled and systray_available(), ) # Attempt to run an IPC server if Parsec is not already started try: await jobs_ctx.nursery.start(_run_ipc_server, config, win, start_arg) # Another instance of Parsec already started, nothing more to do except IPCServerAlreadyRunning: return # If we are here, it's either the IPC server has successfully started # or it has crashed without being able to communicate with an existing # IPC server. Such case is of course not supposed to happen but if it # does we nevertheless keep the application running as a kind of # failsafe mode (and the crash reason is logged and sent to telemetry). # Systray is not displayed on MacOS, having natively a menu with similar functions. if systray_available() and sys.platform != "darwin": systray = Systray(parent=win) win.systray_notification.connect(systray.on_systray_notification) systray.on_close.connect(win.close_app) systray.on_show.connect(win.show_top) app.aboutToQuit.connect(before_quit(systray)) if config.gui_tray_enabled: app.setQuitOnLastWindowClosed(False) if config.gui_check_version_at_startup and not diagnose: CheckNewVersion(jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, parent=win) win.show_window(skip_dialogs=diagnose) win.show_top() win.new_instance_needed.emit(start_arg) if sys.platform == "darwin": # macFUSE is not bundled with Parsec and must be manually installed by the user # so we detect early such need to provide a help dialogue ;-) # TODO: provide a similar mechanism on Windows&Linux to handle mountpoint runner not available from parsec.core.gui.instance_widget import ensure_macfuse_available_or_show_dialogue ensure_macfuse_available_or_show_dialogue(win) def kill_window(*args): win.close_app(force=True) signal.signal(signal.SIGINT, kill_window) # QTimer wakes up the event loop periodically which allows us to close # the window even when it is in background. timer = QTimer() timer.start(400) timer.timeout.connect(lambda: None) if diagnose: diagnose_timer = QTimer() diagnose_timer.start(1000) diagnose_timer.timeout.connect(kill_window) if lang_key: event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_language=lang_key) with QDialogInProcess.manage_pools(): if diagnose: with fail_on_first_exception(kill_window): await trio.sleep_forever() else: with log_pyqt_exceptions(): await trio.sleep_forever()
def run_gui(config: CoreConfig, start_arg: str = None, diagnose: bool = False): logger.info("Starting UI") # Needed for High DPI usage of QIcons, otherwise only QImages are well scaled QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) QApplication.setHighDpiScaleFactorRoundingPolicy( Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) app = ParsecApp() app.load_stylesheet() app.load_font() lang_key = lang.switch_language(config) event_bus = EventBus() with run_trio_thread() as jobs_ctx: win = MainWindow( jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, minimize_on_close=config.gui_tray_enabled and systray_available(), ) result_queue = Queue(maxsize=1) class ThreadSafeNoQtSignal(ThreadSafeQtSignal): def __init__(self): self.qobj = None self.signal_name = "" self.args_types = () def emit(self, *args): pass jobs_ctx.submit_job( ThreadSafeNoQtSignal(), ThreadSafeNoQtSignal(), _start_ipc_server, config, win, start_arg, result_queue, ) if result_queue.get() == "already_running": # Another instance of Parsec already started, nothing more to do return if systray_available(): systray = Systray(parent=win) win.systray_notification.connect(systray.on_systray_notification) systray.on_close.connect(win.close_app) systray.on_show.connect(win.show_top) app.aboutToQuit.connect(before_quit(systray)) if config.gui_tray_enabled: app.setQuitOnLastWindowClosed(False) if config.gui_check_version_at_startup and not diagnose: CheckNewVersion(jobs_ctx=jobs_ctx, event_bus=event_bus, config=config, parent=win) win.show_window(skip_dialogs=diagnose, invitation_link=start_arg) win.show_top() win.new_instance_needed.emit(start_arg) def kill_window(*args): win.close_app(force=True) QApplication.quit() signal.signal(signal.SIGINT, kill_window) # QTimer wakes up the event loop periodically which allows us to close # the window even when it is in background. timer = QTimer() timer.start(1000 if diagnose else 400) timer.timeout.connect(kill_window if diagnose else lambda: None) if diagnose: diagnose_timer = QTimer() diagnose_timer.start(1000) diagnose_timer.timeout.connect(kill_window) if lang_key: event_bus.send(CoreEvent.GUI_CONFIG_CHANGED, gui_language=lang_key) if diagnose: with fail_on_first_exception(kill_window): return app.exec_() else: with log_pyqt_exceptions(): return app.exec_()
def __init__(self): super().__init__() self.snackbars = [] ParsecApp.get_main_window().installEventFilter(self) self.setParent(ParsecApp.get_main_window()) self.destroyed.connect(self._on_destroyed)