#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Copyright (C) 2018 Andy Stewart # # Author: Andy Stewart # Maintainer: Andy Stewart # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # NOTE # QtWebEngine will throw error "ImportError: QtWebEngineWidgets must be imported before a QCoreApplication instance is created" # So we import browser module before start Qt application instance to avoid this error, but we never use this module. from PyQt5 import QtWebEngineWidgets as NeverUsed # noqa from PyQt5 import QtWidgets from PyQt5.QtCore import QLibraryInfo, QTimer from PyQt5.QtNetwork import QNetworkProxy, QNetworkProxyFactory from PyQt5.QtWidgets import QApplication from core.utils import PostGui, string_to_base64, eval_in_emacs, init_epc_client, close_epc_client, message_to_emacs, list_string_to_list, get_emacs_vars, get_emacs_config_dir, to_camel_case from core.view import View from epc.server import ThreadingEPCServer from sys import version_info import importlib import json import logging import os import platform import socket import subprocess import threading if platform.system() == "Windows": import pygetwindow as gw class EAF(object): def __init__(self, args): global emacs_width, emacs_height, proxy_string # Parse init arguments. (emacs_width, emacs_height, emacs_server_port) = args emacs_width = int(emacs_width) emacs_height = int(emacs_height) # Init variables. self.module_dict = {} self.buffer_dict = {} self.view_dict = {} for name in ["scroll_other_buffer", "eval_js_function", "eval_js_code", "action_quit", "send_key", "send_key_sequence", "handle_search_forward", "handle_search_backward", "set_focus_text"]: self.build_buffer_function(name) for name in ["execute_js_function", "execute_js_code", "execute_function", "execute_function_with_args"]: self.build_buffer_return_function(name) # Init EPC client port. init_epc_client(int(emacs_server_port)) # Build EPC server. self.server = ThreadingEPCServer(('localhost', 0), log_traceback=True) # self.server = ThreadingEPCServer(('localhost', 0) # self.server.logger.setLevel(logging.DEBUG) self.server.allow_reuse_address = True eaf_config_dir = get_emacs_config_dir() self.session_file = os.path.join(eaf_config_dir, "session.json") if not os.path.exists(eaf_config_dir): os.makedirs(eaf_config_dir); # ch = logging.FileHandler(filename=os.path.join(eaf_config_dir, 'epc_log.txt'), mode='w') # formatter = logging.Formatter('%(asctime)s | %(levelname)-8s | %(lineno)04d | %(message)s') # ch.setFormatter(formatter) # ch.setLevel(logging.DEBUG) # self.server.logger.addHandler(ch) self.server.register_instance(self) # register instance functions let elisp side call # Start EPC server with sub-thread, avoid block Qt main loop. self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.start() # Pass epc port and webengine codec information to Emacs when first start EAF. eval_in_emacs('eaf--first-start', [self.server.server_address[1]]) # Disable use system proxy, avoid page slow when no network connected. QNetworkProxyFactory.setUseSystemConfiguration(False) # Set Network proxy. (proxy_host, proxy_port, proxy_type) = get_emacs_vars([ "eaf-proxy-host", "eaf-proxy-port", "eaf-proxy-type"]) self.proxy = (proxy_type, proxy_host, proxy_port) self.is_proxy = False if proxy_type != "" and proxy_host != "" and proxy_port != "": self.enable_proxy() def enable_proxy(self): global proxy_string proxy_string = "{0}://{1}:{2}".format(self.proxy[0], self.proxy[1], self.proxy[2]) proxy = QNetworkProxy() if self.proxy[0] == "socks5": proxy.setType(QNetworkProxy.Socks5Proxy) elif self.proxy[0] == "http": proxy.setType(QNetworkProxy.HttpProxy) proxy.setHostName(self.proxy[1]) proxy.setPort(int(self.proxy[2])) self.is_proxy = True QNetworkProxy.setApplicationProxy(proxy) def disable_proxy(self): global proxy_string proxy_string = "" proxy = QNetworkProxy() proxy.setType(QNetworkProxy.NoProxy) self.is_proxy = False QNetworkProxy.setApplicationProxy(proxy) def toggle_proxy(self): if self.is_proxy: self.disable_proxy() else: self.enable_proxy() @PostGui() def update_buffer_with_url(self, module_path, buffer_url, update_data): ''' Update buffer with url ''' if type(buffer_id) == str and buffer_id in self.buffer_dict: buffer = self.buffer_dict[buffer_id] if buffer.module_path == module_path and buffer.url == buffer_url: buffer.update_with_data(update_data) @PostGui() def new_buffer(self, buffer_id, url, module_path, arguments): ''' New buffer. new_buffer just clone of create_buffer with @PostGui elisp call asynchronously. ''' self.create_buffer(buffer_id, url, module_path, arguments) def create_buffer(self, buffer_id, url, module_path, arguments): ''' Create buffer. create_buffer can't wrap with @PostGui, because need call by createNewWindow signal of browser.''' global emacs_width, emacs_height, proxy_string # Load module with app absolute path. if module_path in self.module_dict: module = self.module_dict[module_path] else: spec = importlib.util.spec_from_file_location("AppBuffer", module_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) self.module_dict[module_path] = module # Create application buffer. app_buffer = module.AppBuffer(buffer_id, url, arguments) # Add module_path. app_buffer.module_path = module_path # Add buffer to buffer dict. self.buffer_dict[buffer_id] = app_buffer # Resize buffer with emacs max window size, # view (QGraphicsView) will adjust visual area along with emacs window changed. app_buffer.buffer_widget.resize(emacs_width, emacs_height) # Handle dev tools signal. if getattr(app_buffer, "open_devtools_tab", False) and getattr(app_buffer.open_devtools_tab, "connect", False): app_buffer.open_devtools_tab.connect(self.open_devtools_tab) # Add create buffer interface for createWindow signal. if app_buffer.base_class_name() == "BrowserBuffer": app_buffer.create_buffer = self.create_buffer # Set proxy for browser. if app_buffer.base_class_name() == "BrowserBuffer": app_buffer.proxy_string = proxy_string # If arguments is devtools, create devtools page. if app_buffer.base_class_name() == "BrowserBuffer" and arguments == "devtools" and self.devtools_page: self.devtools_page.setDevToolsPage(app_buffer.buffer_widget.web_page) self.devtools_page = None # Restore buffer session. self.restore_buffer_session(app_buffer) return app_buffer @PostGui() def update_views(self, args): ''' Update views.''' view_infos = args.split(",") # Do something if buffer's all view hide after update_views operation. old_view_buffer_ids = list(set(map(lambda v: v.buffer_id, self.view_dict.values()))) new_view_buffer_ids = list(set(map(lambda v: v.split(":")[0], view_infos))) # Call all_views_hide interface when buffer's all views will hide. # We do something in app's buffer interface, such as videoplayer will pause video when all views hide. # Note, we must call this function before last view destroy, # such as QGraphicsVideoItem will report "Internal data stream error" error. for old_view_buffer_id in old_view_buffer_ids: if old_view_buffer_id not in new_view_buffer_ids: if old_view_buffer_id in self.buffer_dict: self.buffer_dict[old_view_buffer_id].all_views_hide() # Remove old key from view dict and destroy old view. for key in list(self.view_dict): if key not in view_infos: self.destroy_view_later(key) # NOTE: # Create new view and REPARENT view to Emacs window. if view_infos != ['']: for view_info in view_infos: if view_info not in self.view_dict: (buffer_id, _, _, _, _, _) = view_info.split(":") view = View(self.buffer_dict[buffer_id], view_info) self.view_dict[view_info] = view # Call some_view_show interface when buffer's view switch back. # Note, this must call after new view create, otherwise some buffer, # such as QGraphicsVideoItem will report "Internal data stream error" error. if view_infos != ['']: for new_view_buffer_id in new_view_buffer_ids: if new_view_buffer_id not in old_view_buffer_ids: if new_view_buffer_id in self.buffer_dict: self.buffer_dict[new_view_buffer_id].some_view_show() # Adjust buffer size along with views change. # Note: just buffer that option `fit_to_view' is False need to adjust, # if buffer option fit_to_view is True, buffer render adjust by view.resizeEvent() for buffer in list(self.buffer_dict.values()): if not buffer.fit_to_view: buffer_views = list(filter(lambda v: v.buffer_id == buffer.buffer_id, list(self.view_dict.values()))) # Adjust buffer size to max view's size. if len(buffer_views) > 0: max_view = max(buffer_views, key=lambda v: v.width * v.height) buffer.buffer_widget.resize(max_view.width, max_view.height) # Adjust buffer size to emacs window size if not match view found. else: buffer.buffer_widget.resize(emacs_width, emacs_height) # Send resize signal to buffer. buffer.resize_view() # NOTE: # When you do switch buffer or kill buffer in Emacs, will call Python function 'update_views. # Screen will flick if destroy old view BEFORE reparent new view. # # So we call function 'destroy_view_now' at last to make sure destroy old view AFTER reparent new view. # Then screen won't flick. self.destroy_view_now() def destroy_view_later(self, key): '''Just record view id in global list 'destroy_view_list', and not destroy old view immediately.''' global destroy_view_list destroy_view_list.append(key) def destroy_view_now(self): '''Destroy all old view immediately.''' global destroy_view_list for key in destroy_view_list: if key in self.view_dict: self.view_dict[key].destroy_view() self.view_dict.pop(key, None) destroy_view_list = [] @PostGui() def kill_buffer(self, buffer_id): ''' Kill all view based on buffer_id and clean buffer from buffer dict.''' # Kill all view base on buffer_id. for key in list(self.view_dict): if buffer_id == self.view_dict[key].buffer_id: self.destroy_view_later(key) # Clean buffer from buffer dict. if buffer_id in self.buffer_dict: # Save buffer session. self.save_buffer_session(self.buffer_dict[buffer_id]) self.buffer_dict[buffer_id].destroy_buffer() self.buffer_dict.pop(buffer_id, None) @PostGui() def kill_emacs(self): ''' Kill all buffurs from buffer dict.''' tmp_buffer_dict = {} for buffer_id in self.buffer_dict: tmp_buffer_dict[buffer_id] = self.buffer_dict[buffer_id] for buffer_id in tmp_buffer_dict: self.kill_buffer(buffer_id) def build_buffer_function(self, name): @PostGui() def _do(*args): buffer_id = args[0] if type(buffer_id) == str and buffer_id in self.buffer_dict: try: getattr(self.buffer_dict[buffer_id], name)(*args[1:]) except AttributeError: import traceback traceback.print_exc() message_to_emacs("Got error with : " + name + " (" + buffer_id + ")") setattr(self, name, _do) def build_buffer_return_function(self, name): def _do(*args): buffer_id = args[0] if type(buffer_id) == str and buffer_id in self.buffer_dict: try: return getattr(self.buffer_dict[buffer_id], name)(*args[1:]) except AttributeError: import traceback traceback.print_exc() message_to_emacs("Got error with : " + name + " (" + buffer_id + ")") return None setattr(self, name, _do) @PostGui() def eval_function(self, buffer_id, function_name, event_string): ''' Execute function and do not return anything. ''' if type(buffer_id) == str and buffer_id in self.buffer_dict: try: buffer = self.buffer_dict[buffer_id] buffer.current_event_string = event_string buffer.eval_function(function_name) except AttributeError: import traceback traceback.print_exc() message_to_emacs("Cannot execute function: " + function_name + " (" + buffer_id + ")") def get_emacs_wsl_window_id(self): if platform.system() == "Windows": return gw.getActiveWindow()._hWnd def activate_emacs_win32_window(self, frame_title): if platform.system() == "Windows": w = gw.getWindowsWithTitle(frame_title) w[0].activate() @PostGui() def handle_input_response(self, buffer_id, callback_tag, callback_result): ''' Handle input message for specified buffer.''' if type(buffer_id) == str and buffer_id in self.buffer_dict: buffer = self.buffer_dict[buffer_id] buffer.handle_input_response(callback_tag, callback_result) buffer.stop_search_input_monitor_thread() @PostGui() def cancel_input_response(self, buffer_id, callback_tag): ''' Cancel input message for specified buffer.''' if type(buffer_id) == str and buffer_id in self.buffer_dict: buffer = self.buffer_dict[buffer_id] buffer.cancel_input_response(callback_tag) buffer.stop_marker_input_monitor_thread() buffer.stop_search_input_monitor_thread() @PostGui() def show_top_views(self): for key in list(self.view_dict): self.view_dict[key].try_show_top_view() @PostGui() def hide_top_views(self): for key in list(self.view_dict): self.view_dict[key].try_hide_top_view() def open_devtools_tab(self, web_page): ''' Open devtools tab''' self.devtools_page = web_page eval_in_emacs('eaf-open-devtool-page', []) def save_buffer_session(self, buf): ''' Save buffer session to file.''' # Create config file it not exist. if not os.path.exists(self.session_file): basedir = os.path.dirname(self.session_file) if not os.path.exists(basedir): os.makedirs(basedir) with open(self.session_file, 'a'): os.utime(self.session_file, None) print("Create session file %s" % (self.session_file)) # Save buffer session to file. buf_session_data = buf.save_session_data() if buf_session_data != "": with open(self.session_file, "r+") as session_file: # Init session dict. session_dict = {} try: session_dict = json.load(session_file) except ValueError: pass # Init module path dict. if buf.module_path not in session_dict: session_dict[buf.module_path] = {} # Update session data. session_dict[buf.module_path].update({buf.url: buf_session_data}) # Clean session file and update new content. session_file.seek(0) session_file.truncate(0) json.dump(session_dict, session_file) print("Saved session: ", buf.module_path, buf.url, buf_session_data) def restore_buffer_session(self, buf): ''' Restore buffer session from file.''' if os.path.exists(self.session_file): with open(self.session_file, "r+") as session_file: session_dict = {} try: session_dict = json.load(session_file) except ValueError: pass if buf.module_path in session_dict: if buf.url in session_dict[buf.module_path]: buf.restore_session_data(session_dict[buf.module_path][buf.url]) def cleanup(self): '''Do some cleanup before exit python process.''' close_epc_client() if __name__ == "__main__": import sys import signal proxy_string = "" emacs_width = emacs_height = 0 destroy_view_list = [] hardware_acceleration_args = [] if platform.system() != "Windows": hardware_acceleration_args += [ "--ignore-gpu-blocklist", "--enable-gpu-rasterization", "--enable-native-gpu-memory-buffers"] app = QApplication(sys.argv + ["--disable-web-security"] + hardware_acceleration_args) eaf = EAF(sys.argv[1:]) signal.signal(signal.SIGINT, signal.SIG_DFL) sys.exit(app.exec_())