#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ############################################################################# # Apache2 2019-2020 - manatlan manatlan[at]gmail(dot)com # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # more: https://github.com/manatlan/guy # ############################################################################# #python3.7 -m pytest --cov-report html --cov=guy . #TODO: # logger for each part # cookiejar __version__="0.7.5" import os,sys,re,traceback,copy,types,shutil from urllib.parse import urlparse from threading import Thread import tornado.web import tornado.websocket import tornado.platform.asyncio import tornado.autoreload import tornado.httpclient from tornado.websocket import websocket_connect from tornado.ioloop import IOLoop from tornado import gen import platform import json import asyncio import time import socket from datetime import datetime,date import tempfile import subprocess import webbrowser import concurrent import inspect import uuid import logging import io class FULLSCREEN: pass ISANDROID = "android" in sys.executable FOLDERSTATIC="static" CHROMECACHE=".cache" WSGUY=None # or "wss://example.com" (ws server) class JSException(Exception): pass handler = logging.StreamHandler() handler.setFormatter( logging.Formatter('-%(asctime)s %(name)s [%(levelname)s]: %(message)s') ) handler.setLevel(logging.ERROR) logger = logging.getLogger("guy") logger.addHandler(handler) logger.setLevel(logging.ERROR) def isFree(ip, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(1) return not (s.connect_ex((ip,port)) == 0) #=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=# https={} def http(regex): # decorator if not regex.startswith("/"): raise Exception("http decoraton, path regex should start with '/'") def _(method): https["^"+regex[1:]+"$"] = method return _ async def callhttp(web,path): # web: RequestHandler for name,method in https.items(): g=re.match(name,path) if g: if asyncio.iscoroutinefunction( method ): ret=await method(web,*g.groups()) else: ret=method(web,*g.groups()) if isinstance(ret,Guy): ret.parent = web.instance web.write( ret._renderHtml() ) return True #=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=#=# def wsquery(wsurl,msg): # Synchrone call, with tornado """ In a simple world, could be (with websocket_client pypi): ws = websocket.create_connection(wsurl) ws.send(msg) resp=ws.recv() ws.close() return resp """ @gen.coroutine def fct(ioloop,u,content): cnx=yield websocket_connect(u) cnx.write_message(msg) resp=yield cnx.read_message() cnx.close() ioloop.stop() ioloop.response=resp ioloop = IOLoop.instance() fct(ioloop,wsurl,msg) ioloop.start() return ioloop.response class readTextFile(str): filename=None encoding=None def __new__(cls,fn:str): for e in ["utf8","cp1252"]: try: with io.open(fn,"r",encoding=e) as fid: obj=str.__new__(cls,fid.read()) obj.filename=fn obj.encoding=e return obj except UnicodeDecodeError: pass raise Exception("Can't read '%s'"%fn) def serialize(obj): def toJSDate(d): assert type(d) in [datetime, date] d = datetime(d.year, d.month, d.day, 0, 0, 0, 0) if type(d) == date else d return d.isoformat() + "Z" if isinstance(obj, (datetime, date)): return toJSDate(obj) if isinstance(obj, bytes): return str(obj, "utf8") if hasattr(obj, "__dict__"): return obj.__dict__ else: return str(obj) def unserialize(obj): if type(obj) == str: if re.search(r"^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$", obj): return datetime.strptime(obj, "%Y-%m-%dT%H:%M:%S.%fZ") elif re.search(r"^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ$", obj): return datetime.strptime(obj, "%Y-%m-%dT%H:%M:%SZ") elif type(obj) == list: return [unserialize(i) for i in obj] return obj def jDumps(obj): return json.dumps(obj, default=serialize) def jLoads(s): return unserialize( json.loads(s, object_pairs_hook=lambda obj: {k: unserialize(v) for k, v in obj}) ) class JDict: def __init__(self, f: str): self.__f = f try: with open(self.__f, "r+") as fid: self.__d = ( json.load( fid, object_pairs_hook=lambda obj: { k: unserialize(v) for k, v in obj }, ) or {} ) except FileNotFoundError as e: self.__d = {} def set(self, k: str, v): self.__d[k] = v self.__save() def get(self, k: str = None): return self.__d.get(k, None) if k else self.__d def __save(self): with open(self.__f, "w+") as fid: json.dump(self.__d, fid, indent=4, sort_keys=True, default=serialize) class GuyJSHandler(tornado.web.RequestHandler): def initialize(self, instance): self.instance=instance async def get(self,id): o=Guy._instances.get( id ) if o: self.write(o._renderJs(id)) else: raise tornado.web.HTTPError(status_code=404) class FavIconHandler(tornado.web.RequestHandler): def initialize(self, instance): self.instance=instance async def get(self): self.write(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00@\x00\x00\x00@\x08\x06\x00\x00\x00\xaaiq\xde\x00\x00\x00\x06bKGD\x00\xd4\x00{\x00\xff\xf0\x90\n\xda\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xe4\x04\x0e\x0f)\x02\xf5J\x9b=\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\x06RIDATx\xda\xdd\x9b_\x88Te\x14\xc0\x7f\xe7\x9bu\xcd\x08D\x82\xfe=\xf4\x92\x88\xceV\xfe\x99\x9d\x82z(0{\x8a\x08\xa2\x07\xa9\xb0Q\x14\xb7\xb2\xcdXVLb\xd42)\x82\xb0\x82\xc5\xd5\x1a,\xd9\xe7\x02{)\x08|*[w\xdd\x9d\xd04\t\x0c\xa2\x1e\x82\xd0(1\xd7\x9d\xd3\xc3\xfc\xd9;3\xf7\xde\xef\xbbw\xef\xfc\xd9.\x0c\xbb;\xf7\xfbs\xbe\xdfw\xce\xf9\xce9\xf7\xae\xd0\x82\xeb\xccKz\'p\x0fp7p\x17p\xbb\x96\xb8\x15X\n\xdc\x02\xdc\x0c\xf4\x02=\x80\x00\x8arCK\\\x07\xae\x02\x7f\x03\x7f\x01\x7f\x02\x7f\x00\xbf\x03\xbf\x02\x972\xa3\xf2\xb3w\xae\xf1MJ\xf6\x98\xc4\x965V\xcf\xd3[\x95\xfe#\xe5\xaegvh\x1f%\xb2\xc0\xa3\xc0\xc3\xc0r\xbf>\xaa\x80Z\x06V\xd0\x92\x83\x00%\xae\x00\xdf\x02\'\x81S\xc0\xf7\x99\xa3\xf2O[\x00L\rj/\xb0^K\xe4\x80g<BY/um\xa3N\x10\x1a\x01\x9fG9\x0c|\xd9\xff\x89\\\x048\x9dS\xfa\x0b\x92\x0c\x80\xa9A]\x05\x0c\x03/\x04\xeeh\xe7!T\xfb\xfd\x06\xbc\xcc\x0c_\xf4\x7f&\xa5\xf1\x9c\x92\r\x00!!\x0bf\xcd!ajPW\x02c\xc0Z\'\xb5\xee4\x84f(\xcfg\x0br<\xb2\x06L\xbf\xaa\x8bU9\n<\x17*\xe8\xc2\x800\x0b\xdc\x9b-\xc8\xf9Fm0u\x8b\xde\xa9\xd5\x9f+\x10\xae\x89\t_<\x80\x88\x0fF\xe3\xe0|\\\xdbHtO\xd68\xb6\x08)\xe0\xc7\xf1\x9cn\xcd\x16\x84\xf1\x9c6\x8bZ\x1cRV\xbf/L\xef\xd4\r\xc0\x85H\x82\xfa\t\x99$\x04\x17\x00v\x08\x00\xa3\xe39=\xe0\x85P\xd7m\xfa5\xdd\x00|\xe5\xa7v\x1aW\xad\x932\x87Y\x07\x10>\xe6\xe8\x1d[\xe7\xee\xbd\x98-\xc8H\x1d\x80\xe2\x90.\x07.\xd6:\xfc\xff!\xac\xcc\x16\xe4\x82)\x0ekUeO\xd5\xa9\x8e$\xa8\xb2\x92\x909\xa4\x9a\xbe\x9a\x01\x0e\x00kQ\x1e\x00>\xb4\x99\x83\xc7\\?\xae\x89V\x1c\xd6-\xc0Q\xefn-\x00M\xc8\xd3\xcb\xfe\xccG\xf5\xab\x9d\xd8\xa6\x068\x82\xb2\xd9A\x13VT\xd9|\xd0\xb8[-\xd1\x04\x93\x98&\xe43\xa3\xb2\x9f\x99\xe6{\x99Q)eFe\x0b\xc2X\xd8\xd8" \xc2\x93\xa68\xac\x0fU\x92\x93&\x8f\xda\xa5\x10~Z7"\xfb\'\x07\x94\xcca\x7f\xdb\x9a\x1cP2\xa3\xf2,\x86k\x96\xb1\xef3\xc0\xe3a\xc7J\x17B(\x00\xac\x1b\tv,\x9e{\x9f\xfb\xcd\xe5\x19\xfb&\x03\xac\xb6\x9d\xad]\x06\xe1d\x84\xdcm,h\xae\xca\xd87\x0cp\x9bK\x80\x11\x06!\xb6\x97\x8f\xd7o&\x02\x80\x7f\xc3\x80\x8b)\x7f=\xe3\x1ae\xd5\x8e\x10q\x08\x87]\x16\xe3\xd8\xaf\xa1\xcd\x83\x93\x03\xf6\x04ab\x9b\x02<e\xd1:5\x92r *s\x9fN@h\x18;\x17f\xff\x9e\xd3\x00`\x93\xc5\xf4\xd4\x04\x04\x18\xdd\x0c!39\xa0\x1b-;\xcf\xc46\xddSw\xba\x05@0!QV(\x04:\x0balr@\x9f\x06\x98\xd8\xaeM;?9\xa0;\x80\xb7B\x07+\xcf\xaf\xf2\xc3n\xfd\x1ax,R\xbc]\xf2\xa9\x05h\xf4\x1a\xa0\xfaE\x87.\xfd\xe6\xda\x9c\x04F\x80\xe9J\x81\xb5\x1f\xd8\x01\xacs\xac/\x1e\xeb\xf1\x8b\xb7\xad\x10LYp\x91\x8a\x1cJ\xb5\xb6[\xb7[\xaav\x85R\xd3\x00A\xec\x00<c?R\xf9\x04kk\xf8Xj\x1c\x93\x8e@;\x920s0nV\xd5\xc9\x82\x8a\x99W!")\x08\x9d\xab*\xa9\t\xdb\x9a\xc8\x10Xx\x10\x8cM?#A0\x0b\x0e\x82\x1a\'#\x956Ch_}Q]\xf6\xd7\xbd:k\xec\xc2tA-\xa1\x0e@\x8fkV!\xc6\xb1N_="\xab\xed}\x8e\xa2\xda\xbd\xf2u9t>"\x1e\x91\xc6~\xfe{\x8e\xfa\xab\xce\x00Z\x04av\xcd!YF\x07/\x13\xb5Cls\x08\xa8%L\r*\x0b\n@\xd2\x10:}\x191\x01\x9e7\x01\x87\xe3]\xb4+\x84\xe2P{5\xa2\xa7v\xdec\x8f\xdd}\x9d\x89\xcd\'\xc8\\\x92\xe3\xe7\x13\x1aA\xde\xff\x9eP\x1c\xd2\xab\x18\xae;\xc9\xe0\xf5/\x12\x96u\xf9:GO2\x14\x17\x82\x99?\x04\x1f\xd3Y\x02,q\x99_\x03\xb2\xd1\xc0\x0c\xb6\xfey\xc7\x12\xd3\x14\xf9\xb5\xca\x1c\x82\xea\x8bI\xf9\x05\x89\xe1\x8f\x04LS\xc7\xb8\x10R\x11!t\xc2!\xfa\x04j\xc6w\x17b\n\x17\xbb\xb4\x96\x90\xa6\xc5\xc9[L\x90*\x8ai\x0b\x04\x89t\x9a\x88e\xfc\x18\xc9\x9b\tKz\xda\x00a&\xce\xee[!D\x88Q\x8c-\xf3k1\x04\xb5\x86b\x01\xbb\x1e*\x97q\x87`\\\xaa\xb3-\x85\x10\xd7\xee\xc5\x0e\xc1\xc5\x97\x19\xdfI\xba\x04\x82\xb5\xaf\x84\x07\xf3\x91j\x82\xdd\x06\xc1\xb5\x8f\xcc\x13\x82\t]`\x92\x10L\xeb\x80\xcd\x07\x82\xb1\n*\xf3[L\xe4#*\xae\xb9\x88\xc3\x06\x88?\x80\x1b\xd6\xb0\xd2D\x9f\xb0\x13\x10\xe2\xa4\xf1=\x94\xdf\xcboZ\x9c6&\x18~Op\\\x92\x90\x00\x08\x95\xa4D\x80\xa5\xc5a\xf5\xaa\xfd\x95VC\xf0>\xf9\xea\xa1\xfcf5q \xf8\xb6\x89\x10\xd5\xa1,\x06.\'q\\FN\xe3g\xe7\x96t!\xd4\xae,\xe6 13\xba\xaa:\xb6{\xf1\x8d\x8e\xd6\x00\xe3V\xe7\xd2J\x08\x1d\xbc$\x05\xa6o\x9fL8y\xd8\x16AH4\xe7\x8f\x9b!K\x8a\xe3\xb1\x8e\x996C\x10K\xa2\x13G\xa3*\xefIp(\xa9\'6-\x83\xe0XE\x8a\n\xc1\x00\xa4\xf3r\x1a\xf8.I\x08IV|\xc4\xd4\xafY\x12\x84\xe0m\xba\xd1\xb5\xb3\xd3\xb3\xbb\x84 \x04\xc9\x93\x14\x84Z\xb3t^.\x01\x9b\xbb\t\x82-,o\x82\x10#o\xa959\xf7\xa6\x92\xceK\x01x\xb7\x1b \x04\xbeY\x1a\x06!F\xf26\xa7\x01o\x08\xe7\xf6)\xe9\xbc\xec\x02\xf6\xcc\x0b\x82_\xa5y\xbe\x89\x8d\xb1@\x88\x99\xbc\xd5\xddJ\xe7\x85\xb3{\x95t^\xde\x06\xd6\xc7\x86\x10\xf3M\x0f\xd7\x98\xc3\xba\xd3\xc6\x1dB\xd3\xd7}{\x85\xf3\xef\x94H\xe7\xe5\x1bJ\xf4\x02\x07\x93\x82`-kG\x01g;n\x1d!\x98\xdd\x9ch\xfar\xe5\xaeZ\xc9t&\x9d\x97\xd7)\xbfr\xfa\n\xf0K\xab 8G\x9b!\xe3\x8bD\x87 \x00\xbb9!\x07y"0\xa7;\x9bW\xfa\xf6I\xf5\xf7;*\xe6\x91\x05VQ\xfe\x17\xf9e\xc0\xa2j{ux\xfb\xb3\xb1M\x9d`\n\xa4\xea\xfe\x9a\x05\xae\x89!\x05\xa4\x9a\xc6\xaf\xfc-=,\xd2RSnj\xfc\xdeF\xad<S\xfc\xf4?\xd9\xf1\xf7\xdeE\\\xb8\xa0\x00\x00\x00\x00IEND\xaeB`\x82') class MainHandler(tornado.web.RequestHandler): def initialize(self, instance): self.instance=instance async def get(self,page): # page doesn't contains a dot '.' ##################################################### if not await callhttp(self,page): ##################################################### if page=="" or page==self.instance._name: logger.debug("MainHandler: Render Main Instance (%s)",self.instance._name) self.write( self.instance._renderHtml() ) else: chpage=self.instanciate(page) # auto-instanciate each time ! chpage.parent = self.instance if chpage: logger.debug("MainHandler: Render Children (%s)",page) self.write( chpage._renderHtml() ) else: raise tornado.web.HTTPError(status_code=404) async def post(self,page): # page doesn't contains a dot '.' await self._callhttp(page) async def put(self,page): # page doesn't contains a dot '.' await self._callhttp(page) async def delete(self,page): # page doesn't contains a dot '.' await self._callhttp(page) async def options(self,page): # page doesn't contains a dot '.' await self._callhttp(page) async def head(self,page): # page doesn't contains a dot '.' await self._callhttp(page) async def patch(self,page): # page doesn't contains a dot '.' await self._callhttp(page) def instanciate(self,page): declared = {cls.__name__:cls for cls in Guy.__subclasses__()} gclass=declared[page] logger.debug("MainHandler: Auto instanciate (%s)",page) x=inspect.signature(gclass.__init__) args=[self.get_argument(i) for i in list(x.parameters)[1:]] return gclass(*args) async def _callhttp(self,page): if not await callhttp(self,page): raise tornado.web.HTTPError(status_code=404) class ProxyHandler(tornado.web.RequestHandler): def initialize(self, instance): self.instance=instance async def get(self,**kwargs): await self._do("GET",None,kwargs) async def post(self,**kwargs): await self._do("POST",self.request.body,kwargs) async def put(self,**kwargs): await self._do("PUT",self.request.body,kwargs) async def delete(self,**kwargs): await self._do("DELETE",self.request.body,kwargs) async def _do(self,method,body,qargs): url = str(qargs.get('url')) if not urlparse(url.lower()).scheme: url="http://%s:%s/%s"% (self.instance._webserver[0],self.instance._webserver[1],url.lstrip("/")) if self.request.query: url = url + "?" + self.request.query headers = {k[4:]: v for k, v in self.request.headers.items() if k.lower().startswith("set-")} http_client = tornado.httpclient.AsyncHTTPClient() logger.debug("PROXY FETCH (%s %s %s %s)",method,url,headers,body) try: response = await http_client.fetch(url, method=method,body=body,headers=headers,validate_cert = False) self.set_status(response.code) for k, v in response.headers.items(): if k.lower() in ["content-type", "date", "expires", "cache-control"]: self.set_header(k,v) logger.debug("PROXY FETCH, return=%s, size=%s",response.code,len(response.body)) self.write(response.body) except Exception as e: logger.debug("PROXY FETCH ERROR (%s), return 0",e) self.set_status(0) self.write(str(e)) async def sockwrite(theSock, **kwargs ): if theSock: try: await theSock.write_message(jDumps(kwargs)) except Exception as e: logger.error("Socket write : can't:%s",theSock) if theSock in WebSocketHandler.clients: del WebSocketHandler.clients[theSock] async def emit(event,*args): logger.debug(">>> Emit ALL: %s (%s)",event,args) for i in list( WebSocketHandler.clients.keys() ): await sockwrite(i,event=event,args=args) class WebSocketHandler(tornado.websocket.WebSocketHandler): clients={} returns={} def initialize(self, instance): self.instance=instance def open(self,id): o=Guy._instances.get( id ) if o: logger.debug("Connect %s",id) async def doInit( instance ): init=instance._getRoutage("init") if init: if asyncio.iscoroutinefunction( init ): await instance(self,"init") else: instance(self,"init") asyncio.ensure_future( doInit(o) ) WebSocketHandler.clients[self]=o def on_close(self): current=WebSocketHandler.clients[self] logger.debug("Disconnect %s",current._id) del WebSocketHandler.clients[self] async def on_message(self, message): current = WebSocketHandler.clients.get(self,None) if current is None: return o = jLoads(message) logger.debug("WS RECEPT: %s",o) method,args,uuid = o["command"],o.get("args"),o["uuid"] if method == "emit": event, *args = args await emit( event, *args ) # emit all elif method == "return": logger.debug(" as JS Response %s : %s",uuid,args) WebSocketHandler.returns[uuid]=args else: async def execution(function, uuid,mode): logger.debug(" as Execute (%s) %s(%s)",mode,method,args) try: ret = await function() ############################################################## if type(ret)==dict and "script" in ret: #evil mode s=ret["script"] del ret["script"] r = dict(result=ret,script=s, uuid=uuid) #evil mode else: ############################################################## r = dict(result=ret, uuid=uuid) except concurrent.futures._base.CancelledError as e: r = dict(error="task cancelled", uuid=uuid) except Exception as e: r = dict(error=str(e), uuid=uuid) logger.error("================================= in %s %s", method, mode) logger.error(traceback.format_exc().strip()) logger.error("=================================") logger.debug(">>> (%s) %s",mode,r) await sockwrite(self,**r) fct=current._getRoutage(method) if asyncio.iscoroutinefunction( fct ): async def function(): return await current(self,method,*args) #asyncio.create_task( execution( function, uuid, "ASYNC") ) #py37 asyncio.ensure_future ( execution( function, uuid, "ASYNC") ) #py35 else: async def function(): return current(self,method,*args) await execution( function, uuid, "SYNC" ) def check_origin(self, origin): return True class WebServer(Thread): # the webserver is ran on a separated thread port = 39000 def __init__(self,instance,host="localhost",port=None,autoreload=False): super(WebServer, self).__init__() self.app=None self.instance=instance self.host=host self.autoreload=autoreload if port is not None: self.port = port while not isFree("localhost", self.port): self.port += 1 self.instance._webserver=(self.host,self.port) try: # https://bugs.python.org/issue37373 FIX: tornado/py3.8 on windows if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) except: pass def run(self): statics = os.path.join( self.instance._folder, FOLDERSTATIC) asyncio.set_event_loop(asyncio.new_event_loop()) tornado.platform.asyncio.AsyncIOMainLoop().install() if self.autoreload: print("**AUTORELOAD**") tornado.autoreload.start() tornado.autoreload.watch( sys.argv[0] ) if os.path.isdir(statics): for p in os.listdir( statics ) : tornado.autoreload.watch(os.path.abspath(os.path.join(statics, p))) self.app=tornado.web.Application([ (r'/_/(?P<url>.+)', ProxyHandler,dict(instance=self.instance)), (r'/(?P<id>[^/]+)-ws', WebSocketHandler,dict(instance=self.instance)), (r'/(?P<id>[^/]+)-js', GuyJSHandler,dict(instance=self.instance)), (r'/(?P<page>[^\.]*)', MainHandler,dict(instance=self.instance)), (r'/favicon.ico', FavIconHandler,dict(instance=self.instance)), (r'/(.*)', tornado.web.StaticFileHandler, dict(path=statics )) ], compress_response=True) self.app.listen(self.port,address=self.host) self.loop=asyncio.get_event_loop() async def _waitExit(): while self._exit==False: await asyncio.sleep(0.1) self._exit=False self.loop.run_until_complete(_waitExit()) # gracefull death try: tasks = asyncio.all_tasks(self.loop) #py37 except: tasks = asyncio.Task.all_tasks(self.loop) #py35 for task in tasks: task.cancel() try: self.loop.run_until_complete(asyncio.gather(*tasks)) except concurrent.futures._base.CancelledError: pass def exit(self): self._exit=True @property def startPage(self): return "http://localhost:%s/#%s" % (self.port,self.instance._name) #anchor is important ! (to uniqify ressource in webbrowser) class ChromeApp: def __init__(self, url, appname="driver",size=None,lockPort=None,chromeargs=[]): def find_chrome_win(): import winreg # TODO: pip3 install winreg reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" for install_type in winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE: try: with winreg.OpenKey(install_type, reg_path, 0, winreg.KEY_READ) as reg_key: return winreg.QueryValue(reg_key, None) except WindowsError: pass def find_chrome_mac(): default_dir = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" if os.path.exists(default_dir): return default_dir if sys.platform[:3] == "win": exe = find_chrome_win() elif sys.platform == "darwin": exe = find_chrome_mac() else: for i in ["chromium-browser", "chromium", "google-chrome", "chrome"]: try: exe = webbrowser.get(i).name break except webbrowser.Error: exe = None if not exe: raise Exception("no chrome browser, no app-mode !") else: args = [ #https://peter.sh/experiments/chromium-command-line-switches/ exe, "--app=" + url, # need to be a real http page ! "--app-id=%s" % (appname), "--app-auto-launched", "--no-first-run", "--no-default-browser-check", "--disable-notifications", "--disable-features=TranslateUI", #~ "--no-proxy-server", ] + chromeargs if size: if size == FULLSCREEN: args.append("--start-fullscreen") else: args.append( "--window-size=%s,%s" % (size[0],size[1]) ) if lockPort: #enable reusable cache folder (coz only one instance can be runned) self.cacheFolderToRemove=None args.append("--remote-debugging-port=%s" % lockPort) args.append("--disk-cache-dir=%s" % CHROMECACHE) args.append("--user-data-dir=%s/%s" % (CHROMECACHE,appname)) else: self.cacheFolderToRemove=os.path.join(tempfile.gettempdir(),appname+"_"+str(os.getpid())) args.append("--user-data-dir=" + self.cacheFolderToRemove) args.append("--aggressive-cache-discard") args.append("--disable-cache") args.append("--disable-application-cache") args.append("--disable-offline-load-stale-cache") args.append("--disk-cache-size=0") logger.debug("CHROME APP-MODE: %s"," ".join(args)) # self._p = subprocess.Popen(args) self._p = subprocess.Popen(args,stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) #~ if lockPort: #~ http_client = tornado.httpclient.HTTPClient() #~ self._ws = None #~ while self._ws == None: #~ try: #~ url = http_client.fetch("http://localhost:%s/json" % debugport).body #~ self._ws = json.loads(url)[0]["webSocketDebuggerUrl"] #~ except Exception as e: #~ self._ws = None def wait(self): self._p.wait() def __del__(self): # really important ! self._p.kill() if self.cacheFolderToRemove: shutil.rmtree(self.cacheFolderToRemove, ignore_errors=True) #~ def _com(self, payload: dict): #~ """ https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-close """ #~ payload["id"] = 1 #~ r=json.loads(wsquery(self._ws,json.dumps(payload)))["result"] #~ return r or True #~ def focus(self): # not used #~ return self._com(dict(method="Page.bringToFront")) #~ def navigate(self, url): # not used #~ return self._com(dict(method="Page.navigate", params={"url": url})) def exit(self): #~ self._com(dict(method="Browser.close")) self._p.kill() class CefApp: def __init__(self, url, size=None, chromeArgs=None,lockPort=None): # chromeArgs is not used import pkgutil assert pkgutil.find_loader("cefpython3"), "cefpython3 not available" def cefbrowser(): from cefpython3 import cefpython as cef import ctypes isWin = platform.system() == "Windows" windowInfo = cef.WindowInfo() windowInfo.windowName = "Guy-CefPython3" if type(size) == tuple: w, h = size[0], size[1] windowInfo.SetAsChild(0, [0, 0, w, h]) # not win else: w, h = None, None sys.excepthook = cef.ExceptHook settings = { "product_version": "Guy/%s" % __version__, "user_agent": "Guy/%s (%s)" % (__version__, platform.system()), "context_menu": dict( enabled=True, navigation=False, print=False, view_source=False, external_browser=False, devtools=True, ), } if lockPort: settings["remote_debugging_port"]=lockPort settings["cache_path"]= CHROMECACHE cef.Initialize(settings, {}) b = cef.CreateBrowserSync(windowInfo, url=url) if isWin and w and h: window_handle = b.GetOuterWindowHandle() SWP_NOMOVE = 0x0002 # X,Y ignored with SWP_NOMOVE flag ctypes.windll.user32.SetWindowPos( window_handle, 0, 0, 0, w, h, SWP_NOMOVE ) # ===--- def guyInit(width, height): if size == FULLSCREEN: if isWin: b.ToggleFullscreen() # win only else: b.SetBounds(0, 0, width, height) # not win bindings = cef.JavascriptBindings() bindings.SetFunction("guyInit", guyInit) b.SetJavascriptBindings(bindings) b.ExecuteJavascript("guyInit(window.screen.width,window.screen.height)") # ===--- class GuyClientHandler(object): def OnLoadEnd(self, browser, **_): pass # could serve in the future (?) class GuyDisplayHandler(object): def OnTitleChange(self, browser, title): try: cef.WindowUtils.SetTitle(browser, title) except AttributeError: logger.warning( "**WARNING** : title changed '%s' not work on linux",title ) b.SetClientHandler(GuyClientHandler()) b.SetClientHandler(GuyDisplayHandler()) logger.debug("CEFPYTHON : %s",url) return cef self.__instance=cefbrowser() def wait(self): self.__instance.MessageLoop() def exit(self): self.__instance.Shutdown() def chromeBringToFront(port): if not isFree("localhost", port): http_client = tornado.httpclient.HTTPClient() url = http_client.fetch("http://localhost:%s/json" % port).body wsurl= json.loads(url)[0]["webSocketDebuggerUrl"] wsquery(wsurl,json.dumps(dict(id=1,method="Page.bringToFront"))) return True class LockPortFile: def __init__(self,name): self._file = os.path.join(CHROMECACHE,name,"lockport") def bringToFront(self): if os.path.isfile(self._file): # the file is here, perhaps it's running with open(self._file,"r") as fid: port=fid.read() if not isFree("localhost", int(port)): # if port is taken, perhaps it's running http_client = tornado.httpclient.HTTPClient() url = http_client.fetch("http://localhost:%s/json" % port).body wsurl= json.loads(url)[0]["webSocketDebuggerUrl"] print("*** ALREADY RUNNING") wsquery(wsurl,json.dumps(dict(id=1,method="Page.bringToFront"))) return True def create(self) -> int: if os.path.isfile(self._file): os.unlink(self._file) # find a freeport port=9990 while not isFree("localhost", port): port += 1 if not os.path.isdir( os.path.dirname(self._file)): os.makedirs( os.path.dirname(self._file) ) with open(self._file,"w") as fid: fid.write(str(port)) return port class GuyBase: def run(self,log=False,autoreload=False,one=False,args=[]): """ Run the guy's app in a windowed env (one client)""" self._log=log if log: handler.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG) if ISANDROID: #TODO: add executable for kivy/iOs mac/apple runAndroid(self) else: lockPort=None if one: lp=LockPortFile(self._name) if lp.bringToFront(): return else: lockPort = lp.create() ws=WebServer( self, autoreload=autoreload ) ws.start() app=ChromeApp(ws.startPage,self._name,self.size,lockPort=lockPort,chromeargs=args) self.RETOUR=None def exit(v=None): self.RETOUR=v ws.exit() app.exit() tornado.autoreload.add_reload_hook(exit) self._callbackExit = exit try: app.wait() # block except KeyboardInterrupt: print("-Process stopped") ws.exit() ws.join() return self.RETOUR def runCef(self,log=False,autoreload=False,one=False): """ Run the guy's app in a windowed cefpython3 (one client)""" self._log=log if log: handler.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG) lockPort=None if one: lp=LockPortFile(self._name) if lp.bringToFront(): return else: lockPort = lp.create() ws=WebServer( self, autoreload=autoreload ) ws.start() self.RETOUR=None try: app=CefApp(ws.startPage,self.size,lockPort=lockPort) def cefexit(v=None): self.RETOUR=v app.exit() tornado.autoreload.add_reload_hook(app.exit) self._callbackExit = cefexit try: app.wait() # block except KeyboardInterrupt: print("-Process stopped") except Exception as e: print("Trouble with CEF:",e) ws.exit() ws.join() return self.RETOUR def serve(self,port=8000,log=False,open=True,autoreload=False): """ Run the guy's app for multiple clients (web/server mode) """ self._log=log if log: handler.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG) ws=WebServer( self ,"0.0.0.0",port=port, autoreload=autoreload ) ws.start() self.RETOUR=None def exit(v=None): self.RETOUR=v ws.exit() self._callbackExit = exit print("Running", ws.startPage ) if open: #auto open browser try: import webbrowser webbrowser.open_new_tab(ws.startPage) except: pass try: ws.join() #important ! except KeyboardInterrupt: print("-Process stopped") ws.exit() return self.RETOUR def _renderJs(self,id): if self.size and self.size is not FULLSCREEN: size=self.size else: size=None routes=[k for k,v in self._routes.items() if not v.__func__.__qualname__.startswith("Guy.")] logger.debug("ROUTES: %s",routes) js = """ document.addEventListener("DOMContentLoaded", function(event) { %s },true) function setupWS( cbCnx ) { var url=%s+"/%s-ws" var ws=new WebSocket( url ); ws.onmessage = function(evt) { var r = guy._jsonParse(evt.data); guy.log("** WS RECEPT:",r) if(r.uuid) // that's a response from call py ! document.dispatchEvent( new CustomEvent('guy-'+r.uuid,{ detail: r} ) ); else if(r.jsmethod) { // call from py : self.js.<methodjs>() function sendBackReturn( response ) { var cmd={ command: "return", args: response, uuid: r.key, }; ws.send( JSON.stringify(cmd) ); guy.log("call jsmethod from py:",r.jsmethod,r.args,"-->",cmd.args) } let jsmethod=window[r.jsmethod]; if(!jsmethod) sendBackReturn( {error:"Unknown JS method "+r.jsmethod} ) else { if(jsmethod.constructor.name == 'AsyncFunction') { jsmethod.apply(window,r.args).then( function(x) { sendBackReturn( { value: x } ); }).catch(function(e) { sendBackReturn( { error: `JS Exception calling '${r.jsmethod}(...)' : ${e}` } ); }) } else { try { sendBackReturn( { value: jsmethod.apply(window,r.args) } ); } catch(e) { sendBackReturn( { error: `JS Exception calling '${r.jsmethod}(...)' : ${e}` } ); } } } } else if(r.event){ // that's an event from anywhere ! document.dispatchEvent( new CustomEvent(r.event,{ detail: r.args } ) ); } }; ws.onclose = function(evt) { guy.log("** WS Disconnected"); setTimeout( function() {setupWS(cbCnx)}, 500); }; ws.onerror = function(evt) { guy.log("** WS Disconnected"); setTimeout( function() {setupWS(cbCnx)}, 500); }; ws.onopen=function(evt) { guy.log("** WS Connected") cbCnx(ws); } return ws; } var guy={ _jsonParse: function(x) { function reviver(key, value) { const dateFormat = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?Z$/; if (typeof value === "string" && dateFormat.test(value)) return new Date(value); else return value; } return JSON.parse(x, reviver ) }, _log: %s, log:function(_) { if(guy._log) { var args=Array.prototype.slice.call(arguments) args.unshift("--") console.log.apply(console.log,args.map( function(x) {return x==null?"NULL":x})); } }, _ws: setupWS( function(ws){guy._ws = ws; document.dispatchEvent( new CustomEvent("init") )} ), on: function( evt, callback ) { // to register an event on a callback guy.log("guy.on:","DECLARE",evt,callback.name) var listener=function(e) { callback.apply(callback,e.detail) }; document.addEventListener(evt,listener) return function() { document.removeEventListener(evt, listener) } }, emitMe: function( _) { // to emit to itself let ll=Array.prototype.slice.call(arguments) let evt=ll.shift() guy.log("guy.emitMe:", evt,ll) document.dispatchEvent( new CustomEvent(evt,{ detail: ll }) ); }, emit: function( _ ) { // to emit a event to all clients var args=Array.prototype.slice.call(arguments) guy.log("guy.emit:", args) return guy._call("emit", args) }, init: function( callback ) { function start() { guy.log("guy.init:",callback.name) document.removeEventListener("init", start) callback() } if(guy._ws.readyState == guy._ws.OPEN) start() else document.addEventListener("init", start) }, _cptFetch: 0, _applyClass: function(i) { guy._cptFetch+=i; if(guy._cptFetch>0) document.body.classList.add("wsguy") else document.body.classList.remove("wsguy") }, _call: function( method, args ) { guy._applyClass(1); guy.log("guy.call:","CALL",method,args) var cmd={ command: method, args: args, uuid: method+"-"+Math.random().toString(36).substring(2), // stamp the exchange, so the callback can be called back (thru customevent), }; if(guy._ws) { guy._ws.send( JSON.stringify(cmd) ); return new Promise( function (resolve, reject) { document.addEventListener('guy-'+cmd.uuid, function handler(x) { guy._applyClass(-1); guy.log("guy.call:","RESPONSE",method,"-->",x.detail) this.removeEventListener('guy-'+cmd.uuid, handler); var x=x.detail; if(x && x.result!==undefined) { if(x.script) resolve( eval(x.script) ) else resolve(x.result) } else if(x && x.error!==undefined) reject(x.error) }); }) } else return new Promise( function (resolve, reject) { reject("not connected"); }) }, fetch: function(url,obj) { guy.log("guy.fetch:", url, "body:",obj) var h={"cache-control": "no-cache"}; // !!! if(obj && obj.headers) Object.keys(obj.headers).forEach( function(k) { h["set-"+k]=obj.headers[k]; }) var newObj = Object.assign({}, obj) newObj.headers=h; newObj.credentials= 'same-origin'; return fetch( "/_/"+url,newObj ) }, cfg: new Proxy({}, { get: function (obj, prop) { return guy._call("cfg_get",[prop]) }, set: function (obj, prop, value) { return guy._call("cfg_set",[prop,value]); }, }), exit: function(x) {guy._call("exit",[x])}, }; var self= { exit:function(x) {guy.exit(x)}, %s }; """ % ( 'if(!document.title) document.title="%s";' % self._name, 'window.location.origin.replace("http","ws")' if WSGUY is None else '"%s"'%WSGUY, id, # for the socket "true" if self._log else "false", "\n".join(["""\n%s:function(_) {return guy._call("%s", Array.prototype.slice.call(arguments) )},""" % (k, k) for k in routes]) ) return js def _renderHtml(self,includeGuyJs=True): cid=self._id path=self._folder html=self.__doc__ def rep(x): d=self.__dict__ d.update(self.__class__.__dict__) for rep in re.findall("<<[^><]+>>", x): var = rep[2:-2] if var in d: o=d[var] if type(o)==str: x=x.replace(rep, o) else: x=x.replace(rep, jDumps( o )) return x def repgjs(x): return re.sub('''src *= *(?P<quote>["'])[^(?P=quote)]*guy\\.js[^(?P=quote)]*(?P=quote)''','src="%s-js"'%(cid,),x) def _caller(self,method:str,args=[]): isBound=hasattr(method, '__self__') if isBound: r=method(*args) else: r=method(self, *args) return r if hasattr(self,"render"): html = _caller(self, self.render, [path] ) html=repgjs(html) return rep(html) else: if hasattr(self,"_render"): print("**DEPRECATING** use of _render() ... use render() instead !") html = _caller(self, self.render, [path] ) html=repgjs(html) return rep(html) else: if html: if includeGuyJs: html=("""<script src="guy.js"></script>""")+ html html=repgjs(html) return rep(html) else: f=os.path.join(path,FOLDERSTATIC,"%s.html" % self._name) if os.path.isfile(f): html=readTextFile(f) html=repgjs(html) return rep(html) else: return "ERROR: can't find '%s'" % f class Guy(GuyBase): _wsock=None # when cloned and connected to a client/wsock (only the cloned instance set this) _instances={} # class variable handling all rendered instances size=None def __init__(self): self.parent=None self._log=False self._name = self.__class__.__name__ self._id=self._name+"_"+hex(id(self))[2:] # unique (readable) id to this instance self._callbackExit=None #public callback when "exit" if hasattr(sys, "_MEIPASS"): # when freezed with pyinstaller ;-) self._folder=sys._MEIPASS else: self._folder = os.path.dirname( inspect.getfile( self.__class__ ) ) # *ME* self._routes={} for n, v in inspect.getmembers(self, inspect.ismethod): if not v.__func__.__qualname__.startswith("GuyBase."): # only "Guy." and its subclass if not n.startswith("_") and n!="render" : #~ print("------------Route %s: %s" %(self._id,n)) self._routes[n]=v Guy._instances[self._id]=self # When render -> save the instance in the pool @property def cfg(self): class Proxy: def __init__(sself): if ISANDROID: exepath=os.path.abspath(os.path.realpath(sys.argv[0])) path=os.path.join( os.path.dirname(exepath), "..", "config.json" ) else: exepath=os.path.abspath(os.path.realpath(sys.argv[0])) # or os.path.abspath(__main__.__file__) classpath= os.path.abspath( os.path.realpath(inspect.getfile( self.__class__ )) ) if not exepath.endswith(".exe") and classpath!=exepath: # as module path=os.path.join( os.path.expanduser("~") , ".%s.json"%os.path.basename(exepath) ) else: # as exe path = os.path.join( os.path.dirname(exepath), "config.json" ) logger.debug("Use config: %s",path) sself.__o=JDict( path ) sself._file=path # new >0.5.3 def __setattr__(self,k,v): if k.startswith("_"): super(Proxy, self).__setattr__(k, v) else: self.__o.set(k,v) def __getattr__(self,k): if k.startswith("_"): return super(Proxy, self).__getattr__(k) else: return self.__o.get(k) return Proxy() def cfg_set(self, key, value): setattr(self.cfg,key,value) def cfg_get(self, key=None): return getattr(self.cfg,key) @property def js(self): class Proxy: def __getattr__(sself,jsmethod): async def _(*args): return await self._callMe(jsmethod,*args) return _ return Proxy() def exit(self,v=None): if self._callbackExit: self._callbackExit(v) else: self.parent._callbackExit(v) async def emit(self, event, *args): await emit(event, *args) async def emitMe(self,event, *args): logger.debug(">>> emitMe %s (%s)",event,args) await sockwrite(self._wsock,event=event,args=args) async def _callMe(self,jsmethod, *args): logger.debug(">>> callMe %s (%s)",jsmethod,args) key=uuid.uuid4().hex # send jsmethod await sockwrite(self._wsock,jsmethod=jsmethod,args=args,key=key) # wait the return (of the key) while 1: if key in WebSocketHandler.returns: response=WebSocketHandler.returns[key] del WebSocketHandler.returns[key] if "error" in response: raise JSException(response["error"]) else: return response.get("value") await asyncio.sleep(0.01) def _getRoutage(self,method): # or None return self._routes.get(method) #~ def __call__(self,theSock,method,*args): #~ #################################################################### #~ ## not the best (no concurrent client in servermode) #~ #################################################################### #~ self._wsock=theSock #~ for k, v in self._routes.items(): #~ setattr(self,k,v) #rebound ! (for init()) #~ function = self._getRoutage(method) #~ print("__CALL__",method,args) #~ return function(*args) def __call__(self,theSock,method,*args): #################################################################### ## create a context, contextual to the socket "theSock" -> context #################################################################### context = copy.copy(self) # important (not deepcopy!), to be able to share mutable for n, v in inspect.getmembers(context): if n in self._routes.keys(): if inspect.isfunction(v): v=types.MethodType( v, context ) setattr( context, n, v ) context._routes[n]=v context._wsock=theSock #################################################################### try: function = context._getRoutage(method) r=function(*args) finally: del context return r def runAndroid(ga): import kivy from kivy.app import App from kivy.utils import platform from kivy.uix.widget import Widget from kivy.clock import Clock from kivy.logger import Logger def run_on_ui_thread(arg): pass webView = None webViewClient = None #~ webChromeClient = None activity = None if platform == 'android': from jnius import autoclass from android.runnable import run_on_ui_thread webView = autoclass('android.webkit.WebView') webViewClient = autoclass('android.webkit.WebViewClient') #~ webChromeClient = autoclass('android.webkit.WebChromeClient') activity = autoclass('org.kivy.android.PythonActivity').mActivity class Wv(Widget): def __init__(self, guyWindow ): self.f2 = self.create_webview # important super(Wv, self).__init__() self.visible = False def exit(v): activity.finish() App.get_running_app().stop() os._exit(0) guyWindow._callbackExit = exit self.ws=WebServer( guyWindow ) self.ws.start() Clock.schedule_once(self.create_webview, 0) @run_on_ui_thread def create_webview(self, *args): webview = webView(activity) webview.getSettings().setJavaScriptEnabled(True) webview.getSettings().setDomStorageEnabled(True) webview.setWebViewClient(webViewClient()) #~ webview.setWebChromeClient(webChromeClient()) activity.setContentView(webview) webview.loadUrl(self.ws.startPage) class ServiceApp(App): def build(self): return Wv( ga ) ServiceApp().run() if __name__ == "__main__": #~ from testTordu import Tordu as GuyApp # from testPrompt import Win as GuyApp # GuyApp().run() pass