def setup_unix_signals(self): import fcntl read_fd, write_fd = os.pipe() cloexec_flag = getattr(fcntl, 'FD_CLOEXEC', 1) for fd in (read_fd, write_fd): flags = fcntl.fcntl(fd, fcntl.F_GETFD) fcntl.fcntl(fd, fcntl.F_SETFD, flags | cloexec_flag | os.O_NONBLOCK) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda x, y: None) signal.siginterrupt(sig, False) signal.set_wakeup_fd(write_fd) self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self) self.signal_notifier.setEnabled(True) self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection)
def handle_unix_signals(self): if iswindows: # TODO: test this on windows self.signal_read_socket, self.signal_write_socket = socket.socketpair( ) self.signal_read_socket.setblocking(False) self.signal_write_socket.setblocking(False) read_fd, write_fd = self.signal_read_socket.fileno( ), self.signal_write_socket.fileno() else: read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda x, y: None) signal.siginterrupt(sig, False) signal.set_wakeup_fd(write_fd) self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self) self.signal_notifier.setEnabled(True) self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection)
def handle_unix_signals(self): if iswindows: # TODO: test this on windows self.signal_read_socket, self.signal_write_socket = socket.socketpair() self.signal_read_socket.setblocking(False) self.signal_write_socket.setblocking(False) read_fd, write_fd = self.signal_read_socket.fileno(), self.signal_write_socket.fileno() else: read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda x, y: None) signal.siginterrupt(sig, False) signal.set_wakeup_fd(write_fd) self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self) self.signal_notifier.setEnabled(True) self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection)
class Application(QApplication): password_loaded = pyqtSignal(object, object) def __init__(self, master_password=None, urls=(), new_instance=False, shutdown=False, restart_state=None, no_session=False, run_local_server=True): if not isosx: # OS X turns this on automatically for v in ('QT_AUTO_SCREEN_SCALE_FACTOR', 'QT_SCALE_FACTOR', 'QT_SCREEN_SCALE_FACTORS', 'QT_DEVICE_PIXEL_RATIO'): if os.environ.get(v): break else: QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.__init__(self, [appname, '-name', appname]) self.setAttribute(Qt.AA_UseHighDpiPixmaps) self.setOrganizationName('kovidgoyal') self.setApplicationName(appname) self.setApplicationVersion(str_version) self.no_session = no_session self.handle_unix_signals() if not QSslSocket.supportsSsl(): raise SystemExit('Qt has been compiled without SSL support!') from .config import font_families, font_sizes ff = font_families().get('sans-serif') or 'default' if ff == 'default': ff = font_families().get('default') or 'default' f = self.font() if ff != 'default': f.setFamily(ff) fs = font_sizes().get('default-size') try: f.setPixelSize(int(fs)) except Exception: pass self.setFont(f) self.password_loaded.connect(self.on_password_load, type=Qt.QueuedConnection) if master_password is not None: password_db.start_load(master_password, self.password_loaded.emit) elif restart_state and 'key' in restart_state: password_db.start_load(restart_state.pop('key'), self.password_loaded.emit, pw_is_key=True) self.lastWindowClosed.connect(self.shutdown) if run_local_server: self.run_local_server(urls, new_instance, shutdown) sys.excepthook = self.on_unhandled_error self.windows = [] f = self.font() if (f.family(), f.pointSize()) == ( 'Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) if 'Ubuntu' in QFontDatabase().families(): f.setFamily('Ubuntu') self.setFont(f) self.downloads = Downloads(self) self.disk_cache = create_favicon_cache() self.key_filter = KeyFilter(self) self.installEventFilter(self.key_filter) def handle_unix_signals(self): if iswindows: # TODO: test this on windows self.signal_read_socket, self.signal_write_socket = socket.socketpair( ) self.signal_read_socket.setblocking(False) self.signal_write_socket.setblocking(False) read_fd, write_fd = self.signal_read_socket.fileno( ), self.signal_write_socket.fileno() else: read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda x, y: None) signal.siginterrupt(sig, False) signal.set_wakeup_fd(write_fd) self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self) self.signal_notifier.setEnabled(True) self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection) def signal_received(self, read_fd): try: data = self.signal_read_socket.recv( 1024) if iswindows else os.read(read_fd, 1024) except BlockingIOError: return if data: signals = struct.unpack('%uB' % len(data), data) if signal.SIGINT in signals or signal.SIGTERM in signals: self.shutdown() def show_password_load_error(self, error, tb, parent=None): error_dialog( parent or (self.windows[0] if self.windows else None), _('Failed to load password database'), _('There was an error processing the password database:') + '<br>' + str(error), det_msg=tb, show=True) def on_password_load(self, error, tb): if error: self.show_password_load_error(error, tb) def ask_for_master_password(self, parent=None): from .passwd.gui import AskForPassword with BusyCursor(): pw_loaded = password_db.join() if not pw_loaded: while True: d = AskForPassword( parent=parent, create_password=not password_db.has_password()) if d.exec_() != AskForPassword.Accepted: return with BusyCursor(): password_db.start_load(d.password) password_db.join() if password_db.error[0] is None: break self.show_password_load_error(password_db.error[0], password_db.error[1], parent=parent) return password_db.is_loaded def store_password(self, url, username, password): key = key_from_url(url) password_db.add_account(key, username, password) def run_local_server(self, urls, new_instance, shutdown): if shutdown: new_instance = False server_name = local_socket_address() s = QLocalSocket() s.connectToServer(server_name) if s.waitForConnected(500): if new_instance: return stream = QTextStream(s) stream.setCodec('UTF-8') if shutdown: cargs = json.dumps({'action': 'shutdown'}) else: cargs = json.dumps({'open': urls}, ensure_ascii=False) stream << cargs stream.flush() s.waitForBytesWritten() raise SystemExit(0) if shutdown: raise SystemExit('No running vise instance found') self.local_server = ls = QLocalServer(self) ls.newConnection.connect(self.another_instance_wants_to_talk) if not ls.listen(server_name): if ls.serverError() == QAbstractSocket.AddressInUseError: try: os.remove(server_name) except FileNotFoundError: pass if not ls.listen(server_name): raise SystemExit( 'Failed to establish local listening socket at: %s with error: %s' % (server_name, ls.errorString())) def another_instance_wants_to_talk(self): s = self.local_server.nextPendingConnection() if s is None: return s.waitForReadyRead(1000) stream = QTextStream(s) stream.setCodec('UTF-8') raw = stream.readAll() try: command = json.loads(raw) except Exception as e: self.error('Invalid data from other instance: %s' % e) return finally: s.close() del s if not isinstance(command, dict): self.error('Invalid data from other instance: %r' % command) return ac = command.get('action') if ac == 'shutdown': return self.shutdown() urls = command.get('open', []) if not isinstance(urls, list): self.error('Invalid data from other instance: %r' % command) return urls = [x for x in urls if isinstance(x, str)] if urls: self.open_urls(urls, in_current_tab='dynamic', switch_to_tab=True) def save_favicon_in_cache(self, icon, qurl): md = QNetworkCacheMetaData() md.setUrl(qurl) md.setSaveToDisk(True) dio = self.disk_cache.prepare(md) if dio: ic = icon_to_data(icon, w=None) if ic: while len(ic) > 0: written = dio.write(ic) if written < 0: print('Failed to save favicon with error:', dio.errorString()) return # error occurred ic = ic[written:] self.disk_cache.insert(dio) def shutdown(self): self.lastWindowClosed.disconnect() if not self.no_session: state = self.serialize_state() state = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) f = tempfile.NamedTemporaryFile(dir=cache_dir) try: f.write(state) os.replace(f.name, os.path.join(cache_dir, 'last-session.pickle')) finally: try: f.close() except FileNotFoundError: pass for w in self.windows: w.close() def new_window(self, is_private=False, restart_state=None): w = MainWindow(is_private=is_private, restart_state=restart_state) w.window_closed.connect(self.remove_window, type=Qt.QueuedConnection) self.windows.append(w) return w def remove_window(self, window): window.break_cycles() try: self.windows.remove(window) except ValueError: pass def open_urls(self, urls, in_current_tab=True, switch_to_tab=False): if not self.windows: self.new_window().show() w = self.activeWindow() or self.windows[0] if in_current_tab == 'dynamic': in_current_tab = w.current_tab is not None and w.current_tab.url( ) == WELCOME_URL for i, url in enumerate(urls): w.open_url(parse_url(url), in_current_tab=in_current_tab and i == 0, switch_to_tab=switch_to_tab and i == 0) def error(self, *args, **kwargs): kwargs['file'] = sys.stderr prefix = '[%s %s]' % (appname, datetime.now().isoformat(' ')) try: print(prefix, *args, **kwargs) except OSError: pass def on_unhandled_error(self, etype, value, tb): if etype == KeyboardInterrupt: return sys.__excepthook__(etype, value, tb) try: msg = str(value) except Exception: msg = repr(value) msg = '<p>' + msg + '<br>' + _( 'Click "Show details" for more information') det_msg = '%s: %s\n%s' % (appname, str_version, ''.join( traceback.format_exception(etype, value, tb))) parent = self.activeWindow() d = error_dialog(parent, _('Unhandled exception'), msg, det_msg=det_msg, show=False) b = d.shutdown_button = d.bb.addButton(_('Shutdown'), d.bb.ActionRole) b.clicked.connect(lambda: self.exit(1)) d.exec_() def break_cycles(self): # Make sure the application object has no references in python and the # other global objects can also be garbage collected # Reset excepthook otherwise we get a segfault on exit, since the application object is deleted # before we exit sys.excepthook = sys.__excepthook__ if hasattr(self, 'local_server'): self.local_server.close() del self.local_server self.downloads.break_cycles() for w in self.windows: w.break_cycles() w.deleteLater() for s in (self.password_loaded, ): s.disconnect() del self.windows def serialize_state(self, include_favicons=False, include_key=False): ans = { 'windows': [w.serialize_state(include_favicons) for w in self.windows] } w = self.activeWindow() if getattr(w, 'window_id', None) is not None: ans['active_window'] = w.window_id if include_key and password_db.is_loaded and password_db.key and not password_db.key_error: ans['key'] = password_db.key return ans def unserialize_state(self, state): active_window = state.get('active_window') for wstate in state['windows']: w = self.new_window(restart_state=wstate, is_private=wstate['is_private']) w.show() if wstate['window_id'] == active_window: active_window = w if hasattr(active_window, 'raise_') and self.activeWindow() != active_window and len( self.windows) > 1: w.raise_() def restart_app(self): password_db.join() state = self.serialize_state(include_key=True) self.restart_state = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) self.no_session = True self.shutdown()
class Application(QApplication): shutdown_signal_received = pyqtSignal() def __init__(self, args, force_calibre_style=False, override_program_name=None, headless=False, color_prefs=gprefs): self.file_event_hook = None if override_program_name: args = [override_program_name] + args[1:] if headless: if not args: args = sys.argv[:1] args.extend(['-platformpluginpath', sys.extensions_location, '-platform', 'headless']) self.headless = headless qargs = [i.encode('utf-8') if isinstance(i, unicode) else i for i in args] self.pi = plugins['progress_indicator'][0] if not isosx and not headless: # On OS X high dpi scaling is turned on automatically by the OS, so we dont need to set it explicitly setup_hidpi() QApplication.setOrganizationName('calibre-ebook.com') QApplication.setOrganizationDomain(QApplication.organizationName()) QApplication.setApplicationVersion(__version__) QApplication.setApplicationName(APP_UID) QApplication.__init__(self, qargs) self.setAttribute(Qt.AA_UseHighDpiPixmaps) self.setAttribute(Qt.AA_SynthesizeTouchForUnhandledMouseEvents, False) try: base_dir() except EnvironmentError as err: if not headless: show_temp_dir_error(err) raise SystemExit('Failed to create temporary directory') if DEBUG and not headless: prints('devicePixelRatio:', self.devicePixelRatio()) s = self.primaryScreen() if s: prints('logicalDpi:', s.logicalDotsPerInchX(), 'x', s.logicalDotsPerInchY()) prints('physicalDpi:', s.physicalDotsPerInchX(), 'x', s.physicalDotsPerInchY()) if not iswindows: self.setup_unix_signals() if islinux or isbsd: self.setAttribute(Qt.AA_DontUseNativeMenuBar, 'CALIBRE_NO_NATIVE_MENUBAR' in os.environ) self.setup_styles(force_calibre_style) self.setup_ui_font() if not self.using_calibre_style and self.style().objectName() == 'fusion': # Since Qt is using the fusion style anyway, specialize it self.load_calibre_style() fi = gprefs['font'] if fi is not None: font = QFont(*(fi[:4])) s = gprefs.get('font_stretch', None) if s is not None: font.setStretch(s) QApplication.setFont(font) self.line_height = max(12, QFontMetrics(self.font()).lineSpacing()) dl = QLocale(get_lang()) if unicode(dl.bcp47Name()) != u'C': QLocale.setDefault(dl) global gui_thread, qt_app gui_thread = QThread.currentThread() self._translator = None self.load_translations() qt_app = self self._file_open_paths = [] self._file_open_lock = RLock() if not isosx: # OS X uses a native color dialog that does not support custom # colors self.color_prefs = color_prefs self.read_custom_colors() self.lastWindowClosed.connect(self.save_custom_colors) if isxp: error_dialog(None, _('Windows XP not supported'), '<p>' + _( 'calibre versions newer than 2.0 do not run on Windows XP. This is' ' because the graphics toolkit calibre uses (Qt 5) crashes a lot' ' on Windows XP. We suggest you stay with <a href="%s">calibre 1.48</a>' ' which works well on Windows XP.') % 'http://download.calibre-ebook.com/1.48.0/', show=True) raise SystemExit(1) if iswindows: # On windows the highlighted colors for inactive widgets are the # same as non highlighted colors. This is a regression from Qt 4. # https://bugreports.qt-project.org/browse/QTBUG-41060 p = self.palette() for role in (p.Highlight, p.HighlightedText, p.Base, p.AlternateBase): p.setColor(p.Inactive, role, p.color(p.Active, role)) self.setPalette(p) # Prevent text copied to the clipboard from being lost on quit due to # Qt 5 bug: https://bugreports.qt-project.org/browse/QTBUG-41125 self.aboutToQuit.connect(self.flush_clipboard) def setup_ui_font(self): f = QFont(QApplication.font()) q = (f.family(), f.pointSize()) if iswindows: if q == ('MS Shell Dlg 2', 8): # Qt default setting # Microsoft recommends the default font be Segoe UI at 9 pt # https://msdn.microsoft.com/en-us/library/windows/desktop/dn742483(v=vs.85).aspx f.setFamily('Segoe UI') f.setPointSize(9) QApplication.setFont(f) else: if q == ('Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) QApplication.setFont(f) f = QFontInfo(f) self.original_font = (f.family(), f.pointSize(), f.weight(), f.italic(), 100) def flush_clipboard(self): try: if self.clipboard().ownsClipboard(): import ctypes ctypes.WinDLL(b'ole32.dll').OleFlushClipboard() except Exception: import traceback traceback.print_exc() def load_builtin_fonts(self, scan_for_fonts=False): if scan_for_fonts: from calibre.utils.fonts.scanner import font_scanner # Start scanning the users computer for fonts font_scanner load_builtin_fonts() def setup_styles(self, force_calibre_style): if iswindows or isosx: using_calibre_style = gprefs['ui_style'] != 'system' else: using_calibre_style = 'CALIBRE_USE_SYSTEM_THEME' not in os.environ if force_calibre_style: using_calibre_style = True self.using_calibre_style = using_calibre_style if DEBUG: prints('Using calibre Qt style:', self.using_calibre_style) if self.using_calibre_style: self.load_calibre_style() def load_calibre_style(self): icon_map = self.__icon_map_memory_ = {} pcache = {} for k, v in { 'DialogYesButton': u'ok.png', 'DialogNoButton': u'window-close.png', 'DialogCloseButton': u'window-close.png', 'DialogOkButton': u'ok.png', 'DialogCancelButton': u'window-close.png', 'DialogHelpButton': u'help.png', 'DialogOpenButton': u'document_open.png', 'DialogSaveButton': u'save.png', 'DialogApplyButton': u'ok.png', 'DialogDiscardButton': u'trash.png', 'MessageBoxInformation': u'dialog_information.png', 'MessageBoxWarning': u'dialog_warning.png', 'MessageBoxCritical': u'dialog_error.png', 'MessageBoxQuestion': u'dialog_question.png', 'BrowserReload': u'view-refresh.png', }.iteritems(): if v not in pcache: p = I(v) if isinstance(p, bytes): p = p.decode(filesystem_encoding) # if not os.path.exists(p): raise ValueError(p) pcache[v] = p v = pcache[v] icon_map[getattr(QStyle, 'SP_'+k)] = v self.pi.load_style(icon_map) def _send_file_open_events(self): with self._file_open_lock: if self._file_open_paths: self.file_event_hook(self._file_open_paths) self._file_open_paths = [] def load_translations(self): if self._translator is not None: self.removeTranslator(self._translator) self._translator = Translator(self) self.installTranslator(self._translator) def event(self, e): if callable(self.file_event_hook) and e.type() == QEvent.FileOpen: path = unicode(e.file()) if os.access(path, os.R_OK): with self._file_open_lock: self._file_open_paths.append(path) QTimer.singleShot(1000, self._send_file_open_events) return True else: return QApplication.event(self, e) @dynamic_property def current_custom_colors(self): from PyQt5.Qt import QColorDialog, QColor def fget(self): return [col.getRgb() for col in (QColorDialog.customColor(i) for i in xrange(QColorDialog.customCount()))] def fset(self, colors): num = min(len(colors), QColorDialog.customCount()) for i in xrange(num): QColorDialog.setCustomColor(i, QColor(*colors[i])) return property(fget=fget, fset=fset) def read_custom_colors(self): colors = self.color_prefs.get('custom_colors_for_color_dialog', None) if colors is not None: self.current_custom_colors = colors def save_custom_colors(self): # Qt 5 regression, it no longer saves custom colors colors = self.current_custom_colors if colors != self.color_prefs.get('custom_colors_for_color_dialog', None): self.color_prefs.set('custom_colors_for_color_dialog', colors) def __enter__(self): self.setQuitOnLastWindowClosed(False) def __exit__(self, *args): self.setQuitOnLastWindowClosed(True) def setup_unix_signals(self): import fcntl read_fd, write_fd = os.pipe() cloexec_flag = getattr(fcntl, 'FD_CLOEXEC', 1) for fd in (read_fd, write_fd): flags = fcntl.fcntl(fd, fcntl.F_GETFD) fcntl.fcntl(fd, fcntl.F_SETFD, flags | cloexec_flag | os.O_NONBLOCK) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda x, y: None) signal.siginterrupt(sig, False) signal.set_wakeup_fd(write_fd) self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self) self.signal_notifier.setEnabled(True) self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection) def signal_received(self, read_fd): try: os.read(read_fd, 1024) except EnvironmentError: return self.shutdown_signal_received.emit()
class Application(QApplication): password_loaded = pyqtSignal(object, object) def __init__(self, master_password=None, urls=(), new_instance=False, shutdown=False, restart_state=None, no_session=False, run_local_server=True): if not isosx: # OS X turns this on automatically for v in ('QT_AUTO_SCREEN_SCALE_FACTOR', 'QT_SCALE_FACTOR', 'QT_SCREEN_SCALE_FACTORS', 'QT_DEVICE_PIXEL_RATIO'): if os.environ.get(v): break else: QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) QApplication.__init__(self, [appname, '-name', appname]) self.setAttribute(Qt.AA_UseHighDpiPixmaps) self.setOrganizationName('kovidgoyal') self.setApplicationName(appname) self.setApplicationVersion(str_version) self.no_session = no_session self.handle_unix_signals() if not QSslSocket.supportsSsl(): raise SystemExit('Qt has been compiled without SSL support!') from .config import font_families, font_sizes ff = font_families().get('sans-serif') or 'default' if ff == 'default': ff = font_families().get('default') or 'default' f = self.font() if ff != 'default': f.setFamily(ff) fs = font_sizes().get('default-size') try: f.setPixelSize(int(fs)) except Exception: pass self.setFont(f) self.password_loaded.connect(self.on_password_load, type=Qt.QueuedConnection) if master_password is not None: password_db.start_load(master_password, self.password_loaded.emit) elif restart_state and 'key' in restart_state: password_db.start_load(restart_state.pop('key'), self.password_loaded.emit, pw_is_key=True) self.lastWindowClosed.connect(self.shutdown) if run_local_server: self.run_local_server(urls, new_instance, shutdown) sys.excepthook = self.on_unhandled_error self.windows = [] f = self.font() if (f.family(), f.pointSize()) == ('Sans Serif', 9): # Hard coded Qt settings, no user preference detected f.setPointSize(10) if 'Ubuntu' in QFontDatabase().families(): f.setFamily('Ubuntu') self.setFont(f) self.downloads = Downloads(self) self.disk_cache = create_favicon_cache() self.key_filter = KeyFilter(self) self.installEventFilter(self.key_filter) def handle_unix_signals(self): if iswindows: # TODO: test this on windows self.signal_read_socket, self.signal_write_socket = socket.socketpair() self.signal_read_socket.setblocking(False) self.signal_write_socket.setblocking(False) read_fd, write_fd = self.signal_read_socket.fileno(), self.signal_write_socket.fileno() else: read_fd, write_fd = os.pipe2(os.O_NONBLOCK | os.O_CLOEXEC) for sig in (signal.SIGINT, signal.SIGTERM): signal.signal(sig, lambda x, y: None) signal.siginterrupt(sig, False) signal.set_wakeup_fd(write_fd) self.signal_notifier = QSocketNotifier(read_fd, QSocketNotifier.Read, self) self.signal_notifier.setEnabled(True) self.signal_notifier.activated.connect(self.signal_received, type=Qt.QueuedConnection) def signal_received(self, read_fd): try: data = self.signal_read_socket.recv(1024) if iswindows else os.read(read_fd, 1024) except BlockingIOError: return if data: signals = struct.unpack('%uB' % len(data), data) if signal.SIGINT in signals or signal.SIGTERM in signals: self.shutdown() def show_password_load_error(self, error, tb, parent=None): error_dialog(parent or (self.windows[0] if self.windows else None), _('Failed to load password database'), _( 'There was an error processing the password database:') + '<br>' + str(error), det_msg=tb, show=True) def on_password_load(self, error, tb): if error: self.show_password_load_error(error, tb) def ask_for_master_password(self, parent=None): from .passwd.gui import AskForPassword with BusyCursor(): pw_loaded = password_db.join() if not pw_loaded: while True: d = AskForPassword(parent=parent, create_password=not password_db.has_password()) if d.exec_() != AskForPassword.Accepted: return with BusyCursor(): password_db.start_load(d.password) password_db.join() if password_db.error[0] is None: break self.show_password_load_error(password_db.error[0], password_db.error[1], parent=parent) return password_db.is_loaded def store_password(self, url, username, password): key = key_from_url(url) password_db.add_account(key, username, password) def run_local_server(self, urls, new_instance, shutdown): if shutdown: new_instance = False server_name = local_socket_address() s = QLocalSocket() s.connectToServer(server_name) if s.waitForConnected(500): if new_instance: return stream = QTextStream(s) stream.setCodec('UTF-8') if shutdown: cargs = json.dumps({'action': 'shutdown'}) else: cargs = json.dumps({'open': urls}, ensure_ascii=False) stream << cargs stream.flush() s.waitForBytesWritten() raise SystemExit(0) if shutdown: raise SystemExit('No running vise instance found') self.local_server = ls = QLocalServer(self) ls.newConnection.connect(self.another_instance_wants_to_talk) if not ls.listen(server_name): if ls.serverError() == QAbstractSocket.AddressInUseError: try: os.remove(server_name) except FileNotFoundError: pass if not ls.listen(server_name): raise SystemExit('Failed to establish local listening socket at: %s with error: %s' % ( server_name, ls.errorString())) def another_instance_wants_to_talk(self): s = self.local_server.nextPendingConnection() if s is None: return s.waitForReadyRead(1000) stream = QTextStream(s) stream.setCodec('UTF-8') raw = stream.readAll() try: command = json.loads(raw) except Exception as e: self.error('Invalid data from other instance: %s' % e) return finally: s.close() del s if not isinstance(command, dict): self.error('Invalid data from other instance: %r' % command) return ac = command.get('action') if ac == 'shutdown': return self.shutdown() urls = command.get('open', []) if not isinstance(urls, list): self.error('Invalid data from other instance: %r' % command) return urls = [x for x in urls if isinstance(x, str)] if urls: self.open_urls(urls, in_current_tab='dynamic', switch_to_tab=True) def save_favicon_in_cache(self, icon, qurl): md = QNetworkCacheMetaData() md.setUrl(qurl) md.setSaveToDisk(True) dio = self.disk_cache.prepare(md) if dio: ic = icon_to_data(icon, w=None) if ic: while len(ic) > 0: written = dio.write(ic) if written < 0: print('Failed to save favicon with error:', dio.errorString()) return # error occurred ic = ic[written:] self.disk_cache.insert(dio) def shutdown(self): self.lastWindowClosed.disconnect() if not self.no_session: state = self.serialize_state() state = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) f = tempfile.NamedTemporaryFile(dir=cache_dir) try: f.write(state) os.replace(f.name, os.path.join(cache_dir, 'last-session.pickle')) finally: try: f.close() except FileNotFoundError: pass for w in self.windows: w.close() def new_window(self, is_private=False, restart_state=None): w = MainWindow(is_private=is_private, restart_state=restart_state) w.window_closed.connect(self.remove_window, type=Qt.QueuedConnection) self.windows.append(w) return w def remove_window(self, window): window.break_cycles() try: self.windows.remove(window) except ValueError: pass def open_urls(self, urls, in_current_tab=True, switch_to_tab=False): if not self.windows: self.new_window().show() w = self.activeWindow() or self.windows[0] if in_current_tab == 'dynamic': in_current_tab = w.current_tab is not None and w.current_tab.url() == WELCOME_URL for i, url in enumerate(urls): w.open_url(parse_url(url), in_current_tab=in_current_tab and i == 0, switch_to_tab=switch_to_tab and i == 0) def error(self, *args, **kwargs): kwargs['file'] = sys.stderr prefix = '[%s %s]' % (appname, datetime.now().isoformat(' ')) try: print(prefix, *args, **kwargs) except OSError: pass def on_unhandled_error(self, etype, value, tb): if etype == KeyboardInterrupt: return sys.__excepthook__(etype, value, tb) try: msg = str(value) except Exception: msg = repr(value) msg = '<p>' + msg + '<br>' + _('Click "Show details" for more information') det_msg = '%s: %s\n%s' % (appname, str_version, ''.join(traceback.format_exception(etype, value, tb))) parent = self.activeWindow() d = error_dialog(parent, _('Unhandled exception'), msg, det_msg=det_msg, show=False) b = d.shutdown_button = d.bb.addButton(_('Shutdown'), d.bb.ActionRole) b.clicked.connect(lambda: self.exit(1)) d.exec_() def break_cycles(self): # Make sure the application object has no references in python and the # other global objects can also be garbage collected # Reset excepthook otherwise we get a segfault on exit, since the application object is deleted # before we exit sys.excepthook = sys.__excepthook__ if hasattr(self, 'local_server'): self.local_server.close() del self.local_server self.downloads.break_cycles() for w in self.windows: w.break_cycles() w.deleteLater() for s in (self.password_loaded,): s.disconnect() del self.windows def serialize_state(self, include_favicons=False, include_key=False): ans = {'windows': [w.serialize_state(include_favicons) for w in self.windows]} w = self.activeWindow() if getattr(w, 'window_id', None) is not None: ans['active_window'] = w.window_id if include_key and password_db.is_loaded and password_db.key and not password_db.key_error: ans['key'] = password_db.key return ans def unserialize_state(self, state): active_window = state.get('active_window') for wstate in state['windows']: w = self.new_window(restart_state=wstate, is_private=wstate['is_private']) w.show() if wstate['window_id'] == active_window: active_window = w if hasattr(active_window, 'raise_') and self.activeWindow() != active_window and len(self.windows) > 1: w.raise_() def restart_app(self): password_db.join() state = self.serialize_state(include_key=True) self.restart_state = pickle.dumps(state, pickle.HIGHEST_PROTOCOL) self.no_session = True self.shutdown()