diff --git a/.gitignore b/.gitignore index cb23c9e..007206a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ screenshots/* *.log .DS_Store sys.py/.* +sys.py/*.db diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9a1f2ab --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "sys.py/pyaria2_rpc"] + path = sys.py/pyaria2_rpc + url = https://github.com/cuu/pyaria2_rpc.git diff --git a/.xinitrc b/.xinitrc index 30c771d..ed27456 100644 --- a/.xinitrc +++ b/.xinitrc @@ -2,14 +2,18 @@ session=${1:-gameshell} case $session in hdmi ) + exec ~/launcher/aria2c --conf-path=/home/cpi/launcher/aria2.conf & feh --bg-center ~/launcher/sys.py/gameshell/wallpaper/desktopbg.jpg + cd ~/launcher/sys.py/ ; python appinstaller.py > /tmp/appinstaller.log & cd ~/ exec ~/launcher/load.sh & exec ~/launcher/sys.py/gsnotify/gsnotify-arm daemon & #exec /usr/bin/twm -f ~/launcher/.twmrc exec ~/launcher/dwm-mod ;; -gameshell ) +gameshell ) + exec ~/launcher/aria2c --conf-path=/home/cpi/launcher/aria2.conf & feh --bg-center ~/launcher/sys.py/gameshell/wallpaper/loading.png + cd ~/launcher/sys.py/ ; python appinstaller.py > /tmp/appinstaller.log & cd ~/ exec ~/launcher/load.sh & exec ~/launcher/sys.py/gsnotify/gsnotify-arm & #exec awesome -c ~/launcher/awesome/rc.lua diff --git a/Menu/GameShell/10_Settings/Lima/__init__.py b/Menu/GameShell/10_Settings/Lima/__init__.py index f193785..ca198f6 100644 --- a/Menu/GameShell/10_Settings/Lima/__init__.py +++ b/Menu/GameShell/10_Settings/Lima/__init__.py @@ -152,7 +152,7 @@ class GPUDriverPage(Page): self._Scroller.SetCanvasHWND(self._HWND) def Click(self): - if len(self._MyList) == 0: + if self._PsIndex > len(self._MyList) -1: return cur_li = self._MyList[self._PsIndex] diff --git a/Menu/GameShell/21_Warehouse/__init__.py b/Menu/GameShell/21_Warehouse/__init__.py new file mode 100644 index 0000000..956b89f --- /dev/null +++ b/Menu/GameShell/21_Warehouse/__init__.py @@ -0,0 +1,1153 @@ +# -*- coding: utf-8 -*- +import os +import pygame +import platform +#import commands +import glob +import json +import gobject +import sqlite3 +#from beeprint import pp +from libs.roundrects import aa_round_rect +from shutil import copyfile,rmtree + +## local UI import +from UI.constants import Width,Height,ICON_TYPES,RESTARTUI +from UI.page import Page,PageSelector +from UI.label import Label +from UI.util_funcs import midRect,FileExists,ArmSystem +from UI.keys_def import CurKeys, IsKeyStartOrA, IsKeyMenuOrB +from UI.scroller import ListScroller +from UI.icon_pool import MyIconPool +from UI.icon_item import IconItem +from UI.multilabel import MultiLabel +from UI.skin_manager import MySkinManager +from UI.lang_manager import MyLangManager +from UI.info_page_list_item import InfoPageListItem +from UI.info_page_selector import InfoPageSelector +from UI.yes_cancel_confirm_page import YesCancelConfirmPage +from UI.keyboard import Keyboard + +from UI.download import Download + +import config + +class RPCStack: + def __init__(self): + self.stack = list() + + def Push(self,data): + if data not in self.stack: + self.stack.append(data) + return True + return False + + def Pop(self): + if len(self.stack)<=0: + return None,False + return self.stack.pop(),True + + def Last(self): + idx = len(self.stack) -1 + if idx < 0: + return None + else: + return self.stack[ idx ] + + def Length(self): + return len(self.stack) + +class LoadHousePage(Page): + _FootMsg = ["Nav.","","","Back","Cancel"] + _DownloaderTimer = -1 + _Value = 0 + _URL = None + _ListFontObj = MyLangManager.TrFont("varela18") + _URLColor = MySkinManager.GiveColor('URL') + _TextColor = MySkinManager.GiveColor('Text') + _Caller=None + _img = None + _Downloader=None + _DownloaderTimer=-1 + def __init__(self): + Page.__init__(self) + self._Icons = {} + self._CanvasHWND = None + + def Init(self): + self._PosX = self._Index * self._Screen._Width + self._Width = self._Screen._Width + self._Height = self._Screen._Height + + self._CanvasHWND = self._Screen._CanvasHWND + self._LoadingLabel = Label() + self._LoadingLabel.SetCanvasHWND(self._CanvasHWND) + self._LoadingLabel.Init("Loading",self._ListFontObj) + self._LoadingLabel.SetColor(self._TextColor ) + + def OnLoadCb(self): + if self._URL is None: + return + self._img = None + self.ClearCanvas() + self._Screen.Draw() + self._Screen.SwapAndShow() + + filename = self._URL.split("/")[-1].strip() + local_dir = self._URL.split("raw.githubusercontent.com") + + if len(local_dir) >1: + menu_file = local_dir[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + + if FileExists(local_menu_file): + #load json + with open(local_menu_file) as json_file: + try: + local_menu_json = json.load(json_file) + self._Caller._MyStack.Push(local_menu_json["list"]) + except: + pass + + self.Leave() + + else: + self._Downloader = Download(self._URL,"/tmp",None) + self._Downloader.start() + self._DownloaderTimer = gobject.timeout_add(400, self.GObjectUpdateProcessInterval) + + + def GObjectUpdateProcessInterval(self): + ret = True + if self._Screen.CurPage() == self: + if self._Downloader._stop == True: + ret = False + + dst_filename = self._Downloader.get_dest() + if self._Downloader.isFinished(): + if self._Downloader.isSuccessful(): + filename = self._URL.split("/")[-1].strip() + local_dir = self._URL.split("raw.githubusercontent.com") + menu_file = local_dir[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + + dl_file = os.path.join("/tmp",filename) + if not os.path.exists(os.path.dirname(local_menu_file)): + os.makedirs(os.path.dirname(local_menu_file)) + + copyfile(dl_file, local_menu_file) + with open(local_menu_file) as json_file: + try: + local_menu_json = json.load(json_file) + self._Caller._MyStack.Push(local_menu_json["list"]) + except: + pass + + ret = False + + self.Leave() + else: + self._Screen._MsgBox.SetText("Fetch house failed") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + ret = False + return ret + else: + return False + + def Leave(self): + if self._DownloaderTimer != -1: + gobject.source_remove(self._DownloaderTimer) + self._DownloaderTimer = -1 + + if self._Downloader != None: + try: + self._Downloader.stop() + except: + print("user canceled ") + + self.ReturnToUpLevelPage() + self._Screen.Draw() + self._Screen.SwapAndShow() + self._URL = None + + def KeyDown(self,event): + if IsKeyMenuOrB(event.key): + self.Leave() + + def Draw(self): + self.ClearCanvas() + self._LoadingLabel.NewCoord( (Width-self._LoadingLabel._Width)/2,(Height-44)/2) + self._LoadingLabel.Draw() + + if self._img is not None: + self._CanvasHWND.blit(self._img,midRect(Width/2, + (Height-44)/2, + pygame.Surface.get_width(self._img),pygame.Surface.get_height(self._img),Width,Height-44)) + + +class ImageDownloadProcessPage(Page): + _FootMsg = ["Nav.","","","Back",""] + _DownloaderTimer = -1 + _Value = 0 + _URL = None + _ListFontObj = MyLangManager.TrFont("varela13") + _URLColor = MySkinManager.GiveColor('URL') + _TextColor = MySkinManager.GiveColor('Text') + _img = None + _Downloader=None + _DownloaderTimer=-1 + def __init__(self): + Page.__init__(self) + self._Icons = {} + self._CanvasHWND = None + + def Init(self): + self._PosX = self._Index * self._Screen._Width + self._Width = self._Screen._Width + self._Height = self._Screen._Height + + self._CanvasHWND = self._Screen._CanvasHWND + self._LoadingLabel = Label() + self._LoadingLabel.SetCanvasHWND(self._CanvasHWND) + self._LoadingLabel.Init("Loading",self._ListFontObj) + self._LoadingLabel.SetColor(self._TextColor ) + + def OnLoadCb(self): + if self._URL is None: + return + self._img = None + self.ClearCanvas() + self._Screen.Draw() + self._Screen.SwapAndShow() + + filename = self._URL.split("/")[-1].strip() + local_dir = self._URL.split("raw.githubusercontent.com") + + if len(local_dir) >1: + menu_file = local_dir[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + + if FileExists(local_menu_file): + self._img = pygame.image.load(local_menu_file).convert_alpha() + self._Screen.Draw() + self._Screen.SwapAndShow() + else: + self._Downloader = Download(self._URL,"/tmp",None) + self._Downloader.start() + self._DownloaderTimer = gobject.timeout_add(300, self.GObjectUpdateProcessInterval) + + + def GObjectUpdateProcessInterval(self): + ret = True + if self._Screen.CurPage() == self: + if self._Downloader._stop == True: + ret = False + + dst_filename = self._Downloader.get_dest() + if self._Downloader.isFinished(): + if self._Downloader.isSuccessful(): + filename = self._URL.split("/")[-1].strip() + local_dir = self._URL.split("raw.githubusercontent.com") + menu_file = local_dir[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + + dl_file = os.path.join("/tmp",filename) + if not os.path.exists(os.path.dirname(local_menu_file)): + os.makedirs(os.path.dirname(local_menu_file)) + + copyfile(dl_file, local_menu_file) + ret = False + + #print("dest ",dst_filename) + if FileExists(dst_filename): + try: + #print("load and draw") + self._img = pygame.image.load(dst_filename).convert_alpha() + self._Screen.Draw() + self._Screen.SwapAndShow() + except Exception as ex: + print(ex) + + return ret + else: + return False + + + def KeyDown(self,event): + if IsKeyMenuOrB(event.key): + gobject.source_remove(self._DownloaderTimer) + self._DownloaderTimer = -1 + + if self._Downloader != None: + try: + self._Downloader.stop() + except: + print("user canceled ") + + self.ReturnToUpLevelPage() + self._Screen.Draw() + self._Screen.SwapAndShow() + self._URL = None + + def Draw(self): + self.ClearCanvas() + self._LoadingLabel.NewCoord( (Width-self._LoadingLabel._Width)/2,(Height-44)/2) + self._LoadingLabel.Draw() + + if self._img is not None: + self._CanvasHWND.blit(self._img,midRect(Width/2, + (Height-44)/2, + pygame.Surface.get_width(self._img),pygame.Surface.get_height(self._img),Width,Height-44)) + + +class Aria2DownloadProcessPage(Page): + _FootMsg = ["Nav.","","Pause","Back","Cancel"] + _DownloaderTimer = -1 + _Value = 0 + _GID = None + + _PngSize = {} + + _FileNameLabel = None + _SizeLabel = None + + _URLColor = MySkinManager.GiveColor('URL') + _TextColor = MySkinManager.GiveColor('Text') + + def __init__(self): + Page.__init__(self) + self._Icons = {} + self._CanvasHWND = None + + def Init(self): + self._PosX = self._Index * self._Screen._Width + self._Width = self._Screen._Width + self._Height = self._Screen._Height + + self._CanvasHWND = self._Screen._CanvasHWND + + bgpng = IconItem() + bgpng._ImgSurf = MyIconPool.GiveIconSurface("rom_download") + bgpng._MyType = ICON_TYPES["STAT"] + bgpng._Parent = self + bgpng.Adjust(0,0,self._PngSize["bg"][0],self._PngSize["bg"][1],0) + self._Icons["bg"] = bgpng + + + self._FileNameLabel = Label() + self._FileNameLabel.SetCanvasHWND(self._CanvasHWND) + self._FileNameLabel.Init("", MyLangManager.TrFont("varela12")) + + self._SizeLabel = Label() + self._SizeLabel.SetCanvasHWND(self._CanvasHWND) + self._SizeLabel.Init("0/0Kb",MyLangManager.TrFont("varela12")) + self._SizeLabel.SetColor( self._URLColor ) + + def GObjectUpdateProcessInterval(self): + if self._Screen.CurPage() == self and self._GID is not None: + self._Value = config.RPC.tellStatus(self._GID) + + downloaded = 0 + total = 0 + + if self._Value["status"] == "waiting": + self._FileNameLabel.SetText( "waiting to download..." ) + if self._Value["status"] == "paused": + self._FileNameLabel.SetText( "download paused..." ) + if self._Value["status"] == "error": + self._FileNameLabel.SetText("download errors,cancel it please") + + if self._Value["status"] == "active": + downloaded = self._Value["completedLength"] + total = self._Value["totalLength"] + + downloaded = downloaded/1000.0/1000.0 + total = total/1000.0/1000.0 + + self._SizeLabel.SetText( "%.2f" % downloaded+"/"+ "%.2f" % total +"Mb") + + print("Progress: %d%%" % (self._Value)) + self._Screen.Draw() + self._Screen.SwapAndShow() + return True + else: + return False + + def CheckDownload(self,aria2_gid): + self._GID = aria2_gid + self._DownloaderTimer = gobject.timeout_add(234, self.GObjectUpdateProcessInterval) + + def KeyDown(self,event): + if IsKeyMenuOrB(event.key): + gobject.source_remove(self._DownloaderTimer) + self._DownloaderTimer = -1 + + #if self._Downloader != None: + # try: + # self._Downloader.stop() + # except: + # print("user canceled ") + + self.ReturnToUpLevelPage() + self._Screen.Draw() + self._Screen.SwapAndShow() + + def Draw(self): + self.ClearCanvas() + + self._Icons["bg"].NewCoord(self._Width/2,self._Height/2-20) + self._Icons["bg"].Draw() + + percent = self._Value + if percent < 10: + percent = 10 + + + rect_ = midRect(self._Width/2,self._Height/2+33,170,17, Width,Height) + aa_round_rect(self._CanvasHWND,rect_,MySkinManager.GiveColor('TitleBg'),5,0,MySkinManager.GiveColor('TitleBg')) + + rect2 = midRect(self._Width/2,self._Height/2+33,int(170*(percent/100.0)),17, Width,Height) + rect2.left = rect_.left + rect2.top = rect_.top + aa_round_rect(self._CanvasHWND,rect2,MySkinManager.GiveColor('Front'),5,0,MySkinManager.GiveColor('Front')) + + rect3 = midRect(self._Width/2,self._Height/2+53,self._FileNameLabel._Width, self._FileNameLabel._Height,Width,Height) + + rect4 = midRect(self._Width/2,self._Height/2+70,self._SizeLabel._Width, self._SizeLabel._Height,Width,Height) + + self._FileNameLabel.NewCoord(rect3.left,rect3.top) + self._SizeLabel.NewCoord(rect4.left, rect4.top) + + self._FileNameLabel.Draw() + self._SizeLabel.Draw() + + +def dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + +class GameStoreListItem(InfoPageListItem): + _Type = None #source,dir,launcher,pico8 + _CanvasHWND = None + def Init(self,text): + + #self._Fonts["normal"] = fonts["veramono12"] + + l = Label() + l._PosX = 10 + l.SetCanvasHWND(self._Parent._CanvasHWND) + + l.Init(text,self._Fonts["normal"]) + self._Labels["Text"] = l + + add_icon = IconItem() + add_icon._ImgSurf = MyIconPool.GiveIconSurface("add") + add_icon._CanvasHWND = self._CanvasHWND + add_icon._Parent = self + add_icon.Init(0,0,MyIconPool.Width("add"),MyIconPool.Height("add"),0) + + ware_icon = IconItem() + ware_icon._ImgSurf = MyIconPool.GiveIconSurface("ware") + ware_icon._CanvasHWND = self._CanvasHWND + ware_icon._Parent = self + ware_icon.Init(0,0,MyIconPool.Width("ware"),MyIconPool.Height("ware"),0) + + app_icon = IconItem() + app_icon._ImgSurf = MyIconPool.GiveIconSurface("app") + app_icon._CanvasHWND = self._CanvasHWND + app_icon._Parent = self + app_icon.Init(0,0,MyIconPool.Width("app"),MyIconPool.Height("app"),0) + + appdling_icon = IconItem() + appdling_icon._ImgSurf = MyIconPool.GiveIconSurface("appdling") + appdling_icon._CanvasHWND = self._CanvasHWND + appdling_icon._Parent = self + appdling_icon.Init(0,0,MyIconPool.Width("appdling"),MyIconPool.Height("appdling"),0) + + blackheart_icon = IconItem() + blackheart_icon._ImgSurf = MyIconPool.GiveIconSurface("blackheart") + blackheart_icon._Width = MyIconPool.Width("blackheart") + blackheart_icon._Height = MyIconPool.Height("blackheart") + blackheart_icon._CanvasHWND = self._CanvasHWND + blackheart_icon._Parent = self + + self._Icons["add"] = add_icon + self._Icons["ware"] = ware_icon + self._Icons["app"] = app_icon + self._Icons["appdling"] = appdling_icon + self._Icons["blackheart"] = blackheart_icon + + + def Draw(self): + if self._ReadOnly == True: + self._Labels["Text"].SetColor(MySkinManager.GiveColor("ReadOnlyText")) + else: + self._Labels["Text"].SetColor(MySkinManager.GiveColor("Text")) + + padding = 17 + + if self._Type == None: + padding = 0 + + + if self._Type == "source" or self._Type == "dir": + self._Icons["ware"].NewCoord( 4, (self._Height - self._Icons["ware"]._Height)/2 ) + self._Icons["ware"].DrawTopLeft() + + if self._Type == "launcher" or self._Type == "pico8": + _icon = "app" + if self._ReadOnly == True: + _icon = "appdling" + + self._Icons[_icon].NewCoord( 4, (self._Height - self._Icons[_icon]._Height)/2) + self._Icons[_icon].DrawTopLeft() + + if self._Type == "add_house": + self._Icons["add"].NewCoord( 4, (self._Height - self._Icons["add"]._Height)/2) + self._Icons["add"].DrawTopLeft() + + + self._Labels["Text"]._PosX = self._Labels["Text"]._PosX + self._PosX + padding + self._Labels["Text"]._PosY = self._PosY + (self._Height - self._Labels["Text"]._Height)/2 + self._Labels["Text"].Draw() + self._Labels["Text"]._PosX = self._Labels["Text"]._PosX - self._PosX - padding + + if "Small" in self._Labels: + self._Labels["Small"]._PosX = self._Width - self._Labels["Small"]._Width-5 + + self._Labels["Small"]._PosY = self._PosY + (self._Height - self._Labels["Small"]._Height)/2 + self._Labels["Small"].Draw() + + + pygame.draw.line(self._Parent._CanvasHWND,MySkinManager.GiveColor('Line'),(self._PosX,self._PosY+self._Height-1),(self._PosX+self._Width,self._PosY+self._Height-1),1) + + +class GameStorePage(Page): + _FootMsg = ["Nav","Update","Up","Back","Select"] + _MyList = [] + _ListFont12 = MyLangManager.TrFont("notosanscjk12") + _ListFont15 = MyLangManager.TrFont("varela15") + + _AList = {} + + _Scrolled = 0 + + _BGwidth = 320 + _BGheight = 240-24-20 + + _DrawOnce = False + _Scroller = None + _InfoPage = None + _Downloading = None + _aria2_db = "aria2tasks.db" + _warehouse_db = "warehouse.db" + _GobjTimer = -1 + + def __init__(self): + Page.__init__(self) + self._Icons = {} + self._MyStack = RPCStack() + #title file type + ## Two level url , only github.com + + repos = [ + {"title":"github.com/clockworkpi/warehouse","file":"https://raw.githubusercontent.com/clockworkpi/warehouse/master/index.json","type":"source"} + ] + self._MyStack.Push(repos) + + def GObjectUpdateProcessInterval(self): + ret = True + dirty = False + for x in self._MyList: + if x._Type == "launcher" or x._Type == "pico8" or x._Type == "tic80": + percent = config.RPC.getPercent(x._Value["file"]) + if percent is not None: + x.SetSmallText(str(percent)+"%") + dirty = True + else: + x.SetSmallText("") + + if self._Screen.CurPage() == self and dirty == True: + self._Screen.Draw() + self._Screen.SwapAndShow() + + return ret + + def SyncWarehouse(self): + try: + conn = sqlite3.connect(self._warehouse_db) + conn.row_factory = dict_factory + c = conn.cursor() + ret = c.execute("SELECT * FROM warehouse").fetchall() + conn.close() + return ret + except Exception as ex: + print(ex) + return None + return None + + def SyncTasks(self): + try: + conn = sqlite3.connect(self._aria2_db) + conn.row_factory = dict_factory + c = conn.cursor() + ret = c.execute("SELECT * FROM tasks").fetchall() + conn.close() + return ret + except Exception as ex: + print(ex) + return None + return None + + def SyncList(self): + + self._MyList = [] + + start_x = 0 + start_y = 0 + last_height = 0 + + repos = [] + stk = self._MyStack.Last() + stk_lev = self._MyStack.Length() + repos.extend(stk) + add_new_house = [ + {"title":"Add new warehouse...","file":"master/index.json","type":"add_house","status":"complete"} + ] + + if stk_lev == 1: # on top + ware_menu= self.SyncWarehouse() + if ware_menu != None and len(ware_menu) > 0: + #print(ware_menu) + repos.extend(ware_menu ) + + tasks_menu= self.SyncTasks() + if tasks_menu != None and len(tasks_menu) > 0: + #print(tasks_menu) + repos.extend(tasks_menu ) + + #print(repos) + repos.extend(add_new_house) + + for i,u in enumerate( repos ): + #print(i,u) + li = GameStoreListItem() + li._CanvasHWND = self._CanvasHWND + li._Parent = self + li._PosX = start_x + li._PosY = start_y + last_height + li._Width = Width + li._Fonts["normal"] = self._ListFont15 + li._Fonts["small"] = self._ListFont12 + li._Active = False + li._ReadOnly = True + li._Value = u + li._Type = u["type"] + li.Init( u["title"] ) + + if stk_lev >1: + remote_file_url = u["file"] + menu_file = remote_file_url.split("raw.githubusercontent.com")[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + + if FileExists(local_menu_file): + li._ReadOnly = False + else: + li._ReadOnly = True + elif stk_lev == 1: + if "status" in u: + if u["status"] == "complete": + li._ReadOnly = False + + if u["type"]=="source": + li._ReadOnly = False + + last_height += li._Height + + if li._Type == "launcher" or li._Type == "pico8" or li._Type == "tic80": + li.SetSmallText("") + + self._MyList.append(li) + + if self._PsIndex > len(self._MyList) - 1: + self._PsIndex = len(self._MyList) - 1 + if self._PsIndex < 0: + self._PsIndex = 0 + + + def Init(self): + if self._Screen != None: + if self._Screen._CanvasHWND != None and self._CanvasHWND == None: + self._HWND = self._Screen._CanvasHWND + self._CanvasHWND = pygame.Surface( (self._Screen._Width,self._BGheight) ) + + self._PosX = self._Index*self._Screen._Width + self._Width = self._Screen._Width ## equal to screen width + self._Height = self._Screen._Height + + done = IconItem() + done._ImgSurf = MyIconPool.GiveIconSurface("done") + done._MyType = ICON_TYPES["STAT"] + done._Parent = self + self._Icons["done"] = done + + ps = InfoPageSelector() + ps._Parent = self + self._Ps = ps + self._PsIndex = 0 + + self.SyncList() + + self._Scroller = ListScroller() + self._Scroller._Parent = self + self._Scroller._PosX = self._Width - 10 + self._Scroller._PosY = 2 + self._Scroller.Init() + self._Scroller.SetCanvasHWND(self._CanvasHWND) + + self._remove_page = YesCancelConfirmPage() + self._remove_page._Screen = self._Screen + self._remove_page._StartOrA_Event = self.RemoveGame + + self._remove_page._Name ="Are you sure?" + self._remove_page.Init() + + self._Keyboard = Keyboard() + self._Keyboard._Name = "Enter warehouse addr" + self._Keyboard._FootMsg = ["Nav.","Add","ABC","Backspace","Enter"] + self._Keyboard._Screen = self._Screen + self._Keyboard.Init() + self._Keyboard.SetPassword("github.com/clockworkpi/warehouse") + self._Keyboard._Caller = self + + self._PreviewPage = ImageDownloadProcessPage() + self._PreviewPage._Screen = self._Screen + self._PreviewPage._Name = "preview" + self._PreviewPage.Init() + + self._LoadHousePage = LoadHousePage() + self._LoadHousePage._Screen = self._Screen + self._LoadHousePage._Name = "Warehouse" + self._LoadHousePage._Caller = self + self._LoadHousePage.Init() + + + def ResetHouse(self): + if self._PsIndex > len(self._MyList) -1: + return + cur_li = self._MyList[self._PsIndex] + if cur_li._Value["type"] == "source": + remote_file_url = cur_li._Value["file"] + menu_file = remote_file_url.split("raw.githubusercontent.com")[1] #assume master branch + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + local_menu_file_path = os.path.dirname(local_menu_file) + print(local_menu_file_path) + local_jsons = glob.glob(local_menu_file_path+"/**/*.json") + try: + if os.path.exists(local_menu_file): + os.remove(local_menu_file) + if os.path.exists(local_menu_file+".aria2"): + os.remove(local_menu_file+".aria2") + + for x in local_jsons: + os.remove(x) + + except Exception as ex: + print(ex) + + self._Screen._MsgBox.SetText("Done") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + + def LoadHouse(self): + if self._PsIndex > len(self._MyList) -1: + return + cur_li = self._MyList[self._PsIndex] + if cur_li._Value["type"] == "source" or cur_li._Value["type"] == "dir": + + self._LoadHousePage._URL = cur_li._Value["file"] + self._Screen.PushPage(self._LoadHousePage) + self._Screen.Draw() + self._Screen.SwapAndShow() + + + def PreviewGame(self): + if self._PsIndex > len(self._MyList) -1: + return + cur_li = self._MyList[self._PsIndex] + if cur_li._Value["type"] == "launcher" or cur_li._Value["type"] == "pico8" or cur_li._Value["type"] == "tic80": + if "shots" in cur_li._Value: + print(cur_li._Value["shots"]) + self._PreviewPage._URL = cur_li._Value["shots"] + self._Screen.PushPage(self._PreviewPage) + self._Screen.Draw() + self._Screen.SwapAndShow() + + + def RemoveGame(self): + if self._PsIndex > len(self._MyList) -1: + return + + cur_li = self._MyList[self._PsIndex] + #if cur_li._Active == True: + # return + print("Remove cur_li._Value",cur_li._Value) + + if cur_li._Value["type"] == "source": + print("remove a source") + try: + conn = sqlite3.connect(self._warehouse_db) + conn.row_factory = dict_factory + c = conn.cursor() + c.execute("DELETE FROM warehouse WHERE file = '%s'" % cur_li._Value["file"] ) + conn.commit() + conn.close() + except Exception as ex: + print(ex) + elif cur_li._Value["type"] == "launcher" or cur_li._Value["type"] == "pico8" or cur_li._Value["type"] == "tic80": + remote_file_url = cur_li._Value["file"] + menu_file = remote_file_url.split("raw.githubusercontent.com")[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + local_menu_file_path = os.path.dirname(local_menu_file) + + gid,ret = config.RPC.urlDownloading(remote_file_url) + if ret == True: + config.RPC.remove(gid) + + try: + if os.path.exists(local_menu_file): + os.remove(local_menu_file) + if os.path.exists(local_menu_file+".aria2"): + os.remove(local_menu_file+".aria2") + if os.path.exists( os.path.join(local_menu_file_path,cur_li._Value["title"])): + rmtree(os.path.join(local_menu_file_path,cur_li._Value["title"]) ) + + except Exception as ex: + print(ex) + + def Click(self): + if self._PsIndex > len(self._MyList) -1: + return + + cur_li = self._MyList[self._PsIndex] + #if cur_li._Active == True: + # return + + print("cur_li._Value",cur_li._Value) + + if cur_li._Value["type"] == "source" or cur_li._Value["type"] == "dir": + remote_file_url = cur_li._Value["file"] + menu_file = remote_file_url.split("raw.githubusercontent.com")[1] #assume master branch + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + print(local_menu_file) + if FileExists( local_menu_file ) == False: + self.LoadHouse() + else: + #read the local_menu_file, push into stack,display menu + self._Downloading = None + try: + with open(local_menu_file) as json_file: + local_menu_json = json.load(json_file) + print(local_menu_json) + self._MyStack.Push(local_menu_json["list"]) + + self.SyncList() + self._Screen.Draw() + self._Screen.SwapAndShow() + except Exception as ex: + print(ex) + self._Screen._MsgBox.SetText("Open house failed ") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + + elif cur_li._Value["type"] == "add_house": + print("show keyboard to add ware house") + self._Screen.PushCurPage() + self._Screen.SetCurPage( self._Keyboard ) + + else: + #download the game probably + remote_file_url = cur_li._Value["file"] + menu_file = remote_file_url.split("raw.githubusercontent.com")[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + + if FileExists( local_menu_file ) == False: + gid,ret = config.RPC.urlDownloading(remote_file_url) + if ret == False: + gid = config.RPC.addUri( remote_file_url, options={"out": menu_file}) + self._Downloading = gid + print("stack length ",self._MyStack.Length()) + """ + if self._MyStack.Length() > 1:## not on the top list page + try: + conn = sqlite3.connect(self._aria2_db) + c = conn.cursor() + c.execute("INSERT INTO tasks(gid,title,file,type,status,fav) VALUES ('"+gid+"','"+cur_li._Value["title"]+"','"+cur_li._Value["file"]+"','"+cur_li._Value["type"]+"','active','0')") + + conn.commit() + conn.close() + except Exception as ex: + print("SQLITE3 ",ex) + """ + else: + print(config.RPC.tellStatus(gid,["status","totalLength","completedLength"])) + + self._Screen._MsgBox.SetText("Getting the game now") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + + pygame.time.delay(800) + self._Screen._TitleBar.Redraw() + + + else: + print("file downloaded")# maybe check it if is installed,then execute it + if cur_li._Value["type"]=="launcher" and cur_li._ReadOnly == False: + local_menu_file_path = os.path.dirname(local_menu_file) + game_sh = os.path.join( local_menu_file_path, cur_li._Value["title"],cur_li._Value["title"]+".sh") + #game_sh = reconstruct_broken_string( game_sh) + print("run game: ",game_sh, os.path.exists( game_sh)) + self._Screen.RunEXE(game_sh) + + if cur_li._Value["type"]=="pico8" and cur_li._ReadOnly == False: + if os.path.exists("/home/cpi/games/PICO-8/pico-8/pico8") == True: + game_sh = "/home/cpi/launcher/Menu/GameShell/50_PICO-8/PICO-8.sh" + self._Screen.RunEXE(game_sh) + else: + self._Screen._MsgBox.SetText("Purchase pico8") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + + if cur_li._Value["type"]=="tic80" and cur_li._ReadOnly == False: + game_sh = "/home/cpi/apps/Menu/51_TIC-80/TIC-80.sh" + self._Screen.RunEXE(game_sh) + + def OnAria2CompleteCb(self,gid): + print("OnAria2CompleteCb ", gid) + self.SyncList() + self._Screen.Draw() + self._Screen.SwapAndShow() + + def raw_github_com(self,_url):#eg: github.com/clockworkpi/warehouse + if _url.startswith("github.com")== False: + return False + parts = _url.split("/") + if len(parts) != 3: + return False + return "/".join(["https://raw.githubusercontent.com",parts[1],parts[2],"master/index.json"]) + + def OnKbdReturnBackCb(self): + inputed = "".join(self._Keyboard._Textarea._MyWords).strip() + inputed = inputed.replace("http://","") + inputed = inputed.replace("https://","") + + if inputed.endswith(".git"): + inputed = inputed[:len(inputed)-4] + if inputed.endswith("/"): + inputed = inputed[:len(inputed)-1] + + print("last: ",inputed) + try: + conn = sqlite3.connect(self._warehouse_db) + conn.row_factory = dict_factory + c = conn.cursor() + ret = c.execute("SELECT * FROM warehouse WHERE title='%s'" % inputed ).fetchone() + if ret != None: + self._Screen._MsgBox.SetText("Warehouse existed!") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + else: + valid_url= self.raw_github_com(inputed) + + if valid_url == False: + self._Screen._MsgBox.SetText("Warehouse existed!") + self._Screen._MsgBox.Draw() + self._Screen.SwapAndShow() + else: + sql_insert = """ INSERT INTO warehouse(title,file,type) VALUES( + '%s', + '%s', + 'source');""" % (inputed,valid_url) + + c.execute(sql_insert) + conn.commit() + self.SyncList() + self._Screen.Draw() + self._Screen.SwapAndShow() + conn.close() + except Exception as ex: + print(ex) + + def OnLoadCb(self): + self._Scrolled = 0 + self._PosY = 0 + self._DrawOnce = False + #sync + print("OnLoadCb") + if self._MyStack.Length() == 1: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Update" + else: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Preview" + + self._GobjTimer = gobject.timeout_add(500, self.GObjectUpdateProcessInterval) + + self.SyncList() + + def OnReturnBackCb(self): + + if self._MyStack.Length() == 1: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Update" + else: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Preview" + + self.SyncList() + self._Screen.Draw() + self._Screen.SwapAndShow() + + """ + self.ReturnToUpLevelPage() + self._Screen.Draw() + self._Screen.SwapAndShow() + """ + def KeyDown(self,event): + if IsKeyMenuOrB(event.key): + + if self._MyStack.Length() > 1: + self._MyStack.Pop() + if self._MyStack.Length() == 1: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Update" + else: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Preview" + if self._MyStack.Length() == 2: + self._FootMsg[2] = "" + self._FootMsg[1] = "" + + self.SyncList() + self._Screen.Draw() + self._Screen.SwapAndShow() + + elif self._MyStack.Length() == 1: + self.ReturnToUpLevelPage() + self._Screen.Draw() + self._Screen.SwapAndShow() + gobject.source_remove(self._GobjTimer) + + if IsKeyStartOrA(event.key): + self.Click() + + if self._MyStack.Length() == 1: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Update" + else: + self._FootMsg[2] = "Remove" + self._FootMsg[1] = "Preview" + if self._MyStack.Length() == 2: + self._FootMsg[2] = "" + self._FootMsg[1] = "" + + + self._Screen.Draw() + self._Screen.SwapAndShow() + + + if event.key == CurKeys["X"]: + #print(self._MyStack.Length() ) + if self._PsIndex <= len(self._MyList) -1: + cur_li = self._MyList[self._PsIndex] + if cur_li._Type != "dir": + if self._MyStack.Length() == 1 and self._PsIndex == 0: + pass + #predefined source + else: + self._Screen.PushPage(self._remove_page) + self._remove_page._StartOrA_Event = self.RemoveGame + self._Screen.Draw() + self._Screen.SwapAndShow() + return + + self.SyncList() + self._Screen.Draw() + self._Screen.SwapAndShow() + + if event.key == CurKeys["Y"]: + if self._MyStack.Length() == 1: + self.ResetHouse() + else: + self.PreviewGame() + + if event.key == CurKeys["Up"]: + self.ScrollUp() + self._Screen.Draw() + self._Screen.SwapAndShow() + if event.key == CurKeys["Down"]: + self.ScrollDown() + self._Screen.Draw() + self._Screen.SwapAndShow() + + + def Draw(self): + + self.ClearCanvas() + if len(self._MyList) == 0: + return + + else: + if len(self._MyList) * self._MyList[0]._Height > self._Height: + self._Ps._Width = self._Width - 11 + self._Ps.Draw() + for i in self._MyList: + if i._PosY > self._Height + self._Height/2: + break + if i._PosY < 0: + continue + i.Draw() + self._Scroller.UpdateSize( len(self._MyList)*self._MyList[0]._Height, self._PsIndex*self._MyList[0]._Height) + self._Scroller.Draw() + + else: + self._Ps._Width = self._Width + self._Ps.Draw() + for i in self._MyList: + if i._PosY > self._Height + self._Height/2: + break + if i._PosY < 0: + continue + i.Draw() + + if self._HWND != None: + self._HWND.fill(MySkinManager.GiveColor("White")) + + self._HWND.blit(self._CanvasHWND,(self._PosX,self._PosY,self._Width, self._Height ) ) + +class APIOBJ(object): + + _Page = None + def __init__(self): + pass + def Init(self,main_screen): + self._Page = GameStorePage() + self._Page._Screen = main_screen + self._Page._Name ="Warehouse" + self._Page.Init() + + def API(self,main_screen): + if main_screen !=None: + main_screen.PushPage(self._Page) + main_screen.Draw() + main_screen.SwapAndShow() + +OBJ = APIOBJ() +def Init(main_screen): + OBJ.Init(main_screen) +def API(main_screen): + OBJ.API(main_screen) + diff --git a/aria2.conf b/aria2.conf new file mode 100644 index 0000000..602592e --- /dev/null +++ b/aria2.conf @@ -0,0 +1,18 @@ +max-connection-per-server=5 +enable-rpc=true +rpc-allow-origin-all=true +rpc-listen-all=true +log-level=error +log=/tmp/aria.log +dir=/home/cpi/aria2download +daemon=true +allow-overwrite=true +split=1 +max-concurrent-downloads=100 +disk-cache=15M +timeout=600 +retry-wait=30 +max-tries=50 +save-session-interval=10 +disable-ipv6=true +save-session=/home/cpi/aria2download/aria.session.txt diff --git a/aria2c b/aria2c new file mode 100755 index 0000000..39c0d6a Binary files /dev/null and b/aria2c differ diff --git a/skin/default/Menu/GameShell/Warehouse.png b/skin/default/Menu/GameShell/Warehouse.png new file mode 100644 index 0000000..f658b28 Binary files /dev/null and b/skin/default/Menu/GameShell/Warehouse.png differ diff --git a/skin/default/sys.py/gameshell/icons/add.png b/skin/default/sys.py/gameshell/icons/add.png new file mode 100644 index 0000000..4ffcda7 Binary files /dev/null and b/skin/default/sys.py/gameshell/icons/add.png differ diff --git a/skin/default/sys.py/gameshell/icons/app.png b/skin/default/sys.py/gameshell/icons/app.png new file mode 100644 index 0000000..3d3c2db Binary files /dev/null and b/skin/default/sys.py/gameshell/icons/app.png differ diff --git a/skin/default/sys.py/gameshell/icons/appdling.png b/skin/default/sys.py/gameshell/icons/appdling.png new file mode 100644 index 0000000..c4ab447 Binary files /dev/null and b/skin/default/sys.py/gameshell/icons/appdling.png differ diff --git a/skin/default/sys.py/gameshell/icons/blackheart.png b/skin/default/sys.py/gameshell/icons/blackheart.png new file mode 100644 index 0000000..2540295 Binary files /dev/null and b/skin/default/sys.py/gameshell/icons/blackheart.png differ diff --git a/skin/default/sys.py/gameshell/icons/ware.png b/skin/default/sys.py/gameshell/icons/ware.png new file mode 100644 index 0000000..fbc955c Binary files /dev/null and b/skin/default/sys.py/gameshell/icons/ware.png differ diff --git a/skin/default/sys.py/gameshell/titlebar_icons/dlstatus18.png b/skin/default/sys.py/gameshell/titlebar_icons/dlstatus18.png new file mode 100644 index 0000000..0b3eab4 Binary files /dev/null and b/skin/default/sys.py/gameshell/titlebar_icons/dlstatus18.png differ diff --git a/sys.py/UI/confirm_page.py b/sys.py/UI/confirm_page.py index 56e2ba0..db36a66 100644 --- a/sys.py/UI/confirm_page.py +++ b/sys.py/UI/confirm_page.py @@ -62,13 +62,13 @@ class ConfirmPage(Page): _BGWidth = 0 _BGHeight = 0 _Parent = None - + def __init__(self): Page.__init__(self) self._Icons = {} self._CanvasHWND = None self._MyList = [] - + def Reset(self): self._MyList[0].SetText(self._ConfirmText) self._MyList[0]._PosX = (self._Width - self._MyList[0]._Width)/2 diff --git a/sys.py/UI/download.py b/sys.py/UI/download.py index cb97470..014373f 100644 --- a/sys.py/UI/download.py +++ b/sys.py/UI/download.py @@ -6,6 +6,7 @@ import urllib2 import hashlib from threading import Thread +from StringIO import StringIO class Download(Thread): _dst_path = "" @@ -24,7 +25,7 @@ class Download(Thread): self.downloaded = 0 self.progress = { 'downloaded': 0, 'total': 0, 'percent': 0,'stopped':False } - self.stop = False + self._stop = False self.filename = "" def isFinished(self): @@ -40,14 +41,15 @@ class Download(Thread): return self._errors def run(self): - c = pycurl.Curl() c.setopt(pycurl.URL, self.url) - c.setopt(pycurl.FOLLOWLOCATION, 1) - c.setopt(pycurl.MAXREDIRS, 5) - c.setopt(pycurl.NOBODY, 1) + c.setopt(pycurl.FOLLOWLOCATION, True) + c.setopt(pycurl.MAXREDIRS, 4) + #c.setopt(pycurl.NOBODY, 1) - c.setopt(pycurl.CONNECTTIMEOUT, 10) + c.setopt(c.VERBOSE, True) + + #c.setopt(pycurl.CONNECTTIMEOUT, 20) if self.useragent: c.setopt(pycurl.USERAGENT, self.useragent) @@ -55,39 +57,39 @@ class Download(Thread): # add cookies, if available if self.cookies: c.setopt(pycurl.COOKIE, self.cookies) - c.perform() - realurl = c.getinfo(pycurl.EFFECTIVE_URL) + #realurl = c.getinfo(pycurl.EFFECTIVE_URL) + realurl = self.url + print("realurl",realurl) self.filename = realurl.split("/")[-1].strip() - - c = pycurl.Curl() - c.setopt(pycurl.CONNECTTIMEOUT, 10) c.setopt(pycurl.URL, realurl) - c.setopt(pycurl.FOLLOWLOCATION, 0) + c.setopt(pycurl.FOLLOWLOCATION, True) c.setopt(pycurl.NOPROGRESS, False) c.setopt(pycurl.XFERINFOFUNCTION, self.getProgress) - if self.useragent: - c.setopt(pycurl.USERAGENT, self.useragent) + + c.setopt(pycurl.SSL_VERIFYPEER, False) + c.setopt(pycurl.SSL_VERIFYHOST, False) # configure pycurl output file if self.path == False: self.path = os.getcwd() filepath = os.path.join(self.path, self.filename) - + + if os.path.exists(filepath):## remove old file,restart download os.system("rm -rf " + filepath) - f = open(filepath, "wb") - else: - f = open(filepath, "wb") - c.setopt(pycurl.WRITEDATA, f) + buffer = StringIO() + c.setopt(pycurl.WRITEDATA, buffer) self._dst_path = filepath # add cookies, if available if self.cookies: c.setopt(pycurl.COOKIE, self.cookies) - + + self._stop = False + self.progress["stopped"] = False # download file try: c.perform() @@ -95,14 +97,18 @@ class Download(Thread): errno,errstr = error print("curl error: %s" % errstr) self._errors.append(errstr) - self.stop = True + self._stop = True self.progress["stopped"] = True + self.stop() finally: code = c.getinfo( c.RESPONSE_CODE ) c.close() self._is_finished = True - + + with open(filepath, mode='w') as f: + f.write(buffer.getvalue()) + if self.progress["percent"] < 100: self._is_successful = False else: @@ -127,7 +133,7 @@ class Download(Thread): self.progress['total'] = download_t + self.downloaded self.progress['percent'] = ( float(self.progress['downloaded']) / float(self.progress['total'])) * 100.0 self.progress["stopped"] = False - if self.stop: + if self._stop: self.progress["stopped"] = True return 1 @@ -151,11 +157,11 @@ class Download(Thread): return self.progress["percent"] def stop(self): - self.stop = True + self._stop = True def cancel(self): # sets the boolean to stop the thread. - self.stop = True + self._stop = True def main(): from optparse import OptionParser diff --git a/sys.py/UI/download_process_page.py b/sys.py/UI/download_process_page.py index a5bb569..060eea7 100644 --- a/sys.py/UI/download_process_page.py +++ b/sys.py/UI/download_process_page.py @@ -187,7 +187,7 @@ class DownloadProcessPage(Page): self._Downloader = Download(url,dst_dir,None) self._Downloader.start() - self._DownloaderTimer = gobject.timeout_add(100, self.GObjectUpdateProcessInterval) + self._DownloaderTimer = gobject.timeout_add(200, self.GObjectUpdateProcessInterval) def KeyDown(self,event): if IsKeyMenuOrB(event.key): diff --git a/sys.py/UI/title_bar.py b/sys.py/UI/title_bar.py index f87e859..6d5bf3d 100644 --- a/sys.py/UI/title_bar.py +++ b/sys.py/UI/title_bar.py @@ -20,7 +20,7 @@ from lang_manager import MyLangManager from util_funcs import midRect,SwapAndShow from skin_manager import MySkinManager from widget import Widget -from config import Battery +from config import Battery,RPC from libs.roundrects import aa_round_rect @@ -50,6 +50,13 @@ class TitleBar(Widget): def __init__(self): self._Icons = {} + def Redraw(self): + #self.CheckBatteryStat() + #self.SyncSoundVolume() + #self.CheckBluetooth() + #self.UpdateWifiStrength() + self.UpdateDownloadStatus() + SwapAndShow() def GObjectRoundRobin(self): if self._InLowBackLight < 0: @@ -57,7 +64,8 @@ class TitleBar(Widget): self.SyncSoundVolume() self.CheckBluetooth() self.UpdateWifiStrength() - + self.UpdateDownloadStatus() + SwapAndShow() # print("TitleBar Gobjectroundrobin") elif self._InLowBackLight >= 0: @@ -67,11 +75,20 @@ class TitleBar(Widget): self.SyncSoundVolume() self.CheckBluetooth() self.UpdateWifiStrength() + self.UpdateDownloadStatus() SwapAndShow() self._InLowBackLight = 0 return True - + + def UpdateDownloadStatus(self): + resp = RPC.getGlobalStat() + + if( int(resp["numActive"]) > 0): + self._Icons["dlstatus"]._IconIndex = 1 + elif( int(resp["numActive"]) == 0): + self._Icons["dlstatus"]._IconIndex = 0 + def UpdateWifiStrength(self): self.Draw(self._Title) @@ -166,11 +183,11 @@ class TitleBar(Widget): self._Icons["battery_charging"]._IconIndex = cap_ge self._Icons["battery"] = self._Icons["battery_charging"] - print("Charging %d" % cap_ge) + #print("Charging %d" % cap_ge) else: self._Icons["battery_discharging"]._IconIndex = cap_ge self._Icons["battery"] = self._Icons["battery_discharging"] - print("Discharging %d" % cap_ge) + #print("Discharging %d" % cap_ge) return True @@ -264,6 +281,15 @@ class TitleBar(Widget): self._Icons["round_corners"] = round_corners + dlstatus = MultiIconItem() + dlstatus._MyType = ICON_TYPES["STAT"] + dlstatus._Parent = self + dlstatus._ImageName = icon_base_path+"dlstatus18.png" + dlstatus.Adjust(start_x+self._icon_width+self._icon_width+8,self._icon_height/2+(self._BarHeight-self._icon_height)/2,self._icon_width,self._icon_height,0) + + self._Icons["dlstatus"] = dlstatus + self.UpdateDownloadStatus() + if is_wifi_connected_now(): print("wifi is connected") print( wifi_strength()) @@ -312,6 +338,8 @@ class TitleBar(Widget): start_x = Width-time_text_size[0]-self._ROffset-self._icon_width*3 # near by the time_text + self._Icons["dlstatus"].NewCoord(start_x - self._icon_width*2,self._icon_height/2+(self._BarHeight-self._icon_height)/2) + self._Icons["bluetooth"].NewCoord(start_x - self._icon_width,self._icon_height/2+(self._BarHeight-self._icon_height)/2) self._Icons["sound"].NewCoord(start_x, self._icon_height/2+(self._BarHeight-self._icon_height)/2) @@ -345,7 +373,10 @@ class TitleBar(Widget): self._Icons["battery"].Draw() self._Icons["bluetooth"].Draw() - + + #self._Icons["dlstatus"].Draw() + + pygame.draw.line(self._CanvasHWND,self._SkinManager.GiveColor("Line"),(0,self._BarHeight),(self._Width,self._BarHeight),self._BorderWidth) if self._HWND != None: diff --git a/sys.py/UI/yes_cancel_confirm_page.py b/sys.py/UI/yes_cancel_confirm_page.py new file mode 100644 index 0000000..4d44998 --- /dev/null +++ b/sys.py/UI/yes_cancel_confirm_page.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +import pygame + +#UI lib +from UI.constants import RUNSYS +from UI.keys_def import CurKeys, IsKeyStartOrA, IsKeyMenuOrB +from UI.confirm_page import ConfirmPage +from UI.lang_manager import MyLangManager +from UI.skin_manager import MySkinManager + +class YesCancelConfirmPage(ConfirmPage): + + _ConfirmText = MyLangManager.Tr("Awaiting Input") + _FootMsg = ["Nav","","","Cancel","Yes"] + _StartOrA_Event = None + _Key_X_Event = None + _Key_Y_Event = None + + def KeyDown(self,event): + + if IsKeyMenuOrB(event.key): + self.ReturnToUpLevelPage() + self._Screen.Draw() + self._Screen.SwapAndShow() + + if IsKeyStartOrA(event.key): + if self._StartOrA_Event != None: + if callable( self._StartOrA_Event): + self._StartOrA_Event() + self.ReturnToUpLevelPage() + + if event.key == CurKeys["X"]: + if self._Key_X_Event != None: + if callable( self._Key_X_Event): + self._Key_X_Event() + self.ReturnToUpLevelPage() + + if event.key == CurKeys["Y"]: + if self._Key_Y_Event != None: + if callable( self._Key_Y_Event): + self._Key_Y_Event() + self.ReturnToUpLevelPage() + diff --git a/sys.py/appinstaller.py b/sys.py/appinstaller.py new file mode 100644 index 0000000..1739716 --- /dev/null +++ b/sys.py/appinstaller.py @@ -0,0 +1,190 @@ +import os +import platform +import sqlite3 +import json + +from wicd import misc +from pyaria2_rpc.pyaria2 import Wsrpc + +import libs.websocket as websocket + +aria2_ws = "ws://localhost:6800/jsonrpc" +aria2_db = "aria2tasks.db" +warehouse_db = "warehouse.db" + +rpc = Wsrpc('localhost',6800) + +def dict_factory(cursor, row): + d = {} + for idx, col in enumerate(cursor.description): + d[col[0]] = row[idx] + return d + +@misc.threaded +def game_install_thread(aria2_result): + try: + #print("game_install_thread ",aria2_result) + if "files" in aria2_result: + if len(aria2_result["files"]) <= 0: + return + if "arm" not in platform.machine(): + return + + ret = aria2_result["files"][0]['uris'] + + remote_file_url = ret[0]['uri'] + menu_file = remote_file_url.split("raw.githubusercontent.com")[1] + local_menu_file = "%s/aria2download%s" % (os.path.expanduser('~'),menu_file ) + local_menu_file_path = os.path.dirname(local_menu_file) + + + if os.path.exists(local_menu_file) == True: + + gametype = "launcher" + + if local_menu_file.endswith(".tar.gz"): + gametype = "launcher" + if local_menu_file.endswith(".p8.png"): + gametype = "pico8" + if local_menu_file.endswith(".tic"): + gametype = "tic80" + + if gametype == "launcher": + #tar zxvf + _cmd = "tar zxvf '%s' -C %s" % (local_menu_file, local_menu_file_path) + print(_cmd) + os.system(_cmd) + if gametype == "pico8": + _cmd="cp -rf '%s' ~/.lexaloffle/pico-8/carts/" % local_menu_file + print(_cmd) + os.system(_cmd) + if gametype == "tic80": + _cmd = "cp -rf '%s' ~/games/TIC-80/" % local_menu_file + print(_cmd) + os.system(_cmd) + + + except Exception as ex: + print("app install error: ",ex) + + +def on_message(ws, message): + global rpc + print("got message ",message) + #decode json + #lookup in the sqlite db ,update the status[error,complete], + #uncompress the game into destnation folder in the game_install_thread + aria2_noti = json.loads(message) + if "method" in aria2_noti and aria2_noti["method"] == "aria2.onDownloadError": + gid = aria2_noti["params"][0]["gid"] + msg = rpc.tellStatus(gid) + ws.send(msg) + + if "method" in aria2_noti and aria2_noti["method"] == "aria2.onDownloadComplete": + gid = aria2_noti["params"][0]["gid"] + msg = rpc.tellStatus(gid) + ws.send(msg) + #game_install_thread(gid) + + if "method" not in aria2_noti and "result" in aria2_noti: + result = aria2_noti["result"] + if "status" in result: + if result["status"] == "error": + try: + print(result["errorMessage"]) + for x in result["files"]: + if os.path.exists(x["path"]): + os.remove(x["path"]) + if os.path.exists(x["path"]+".aria2"): + os.remove(x["path"]+".aria2") + + except Exception as ex: + print(ex) + if result["status"] == "complete": + game_install_thread(result) + + +def on_error(ws, error): + print(error) + +def on_close(ws): + print("### closed ###") + +def on_open(ws): + print "on open" + +def create_connection(db_file): + conn = None + try: + conn = sqlite3.connect(db_file) + return conn + except Error as e: + print(e) + + return conn + + +def create_table(conn, create_table_sql): + try: + c = conn.cursor() + c.execute(create_table_sql) + except Error as e: + print(e) + + + +def init_sqlite3(): + global aria2_db + global warehouse_db + + database = aria2_db + + sql_create_tasks_table = """ CREATE TABLE IF NOT EXISTS tasks ( + id integer PRIMARY KEY, + gid text NOT NULL, + title text NOT NULL, + file text NOT NULL, + type text NOT NULL, + status text, + totalLength text, + completedLength text, + fav text + ); """ + + sql_create_warehouse_table = """ CREATE TABLE IF NOT EXISTS warehouse ( + id integer PRIMARY KEY, + title text NOT NULL, + file text NOT NULL, + type text NOT NULL + ); """ + + conn = create_connection(database) + + if conn is not None: + create_table(conn, sql_create_tasks_table) + conn.close() + else: + print("Error! cannot create the database connection.") + exit() + + database = warehouse_db + conn = create_connection(database) + + if conn is not None: + create_table(conn, sql_create_warehouse_table) + conn.close() + else: + print("Error! cannot create the database connection.") + exit() + + + +if __name__ == "__main__": + init_sqlite3() + websocket.enableTrace(True) + ws = websocket.WebSocketApp(aria2_ws, + on_message = on_message, + on_error = on_error, + on_close = on_close) +# ws.on_open = on_open + ws.run_forever() diff --git a/sys.py/config.py b/sys.py/config.py index 5b12eb1..8a64a36 100644 --- a/sys.py/config.py +++ b/sys.py/config.py @@ -2,6 +2,7 @@ import os import platform from UI.util_funcs import FileExists,ArmSystem +from pyaria2_rpc.pyaria2 import Xmlrpc CurKeySet = "GameShell" ## >>> PC or GameShell <<< @@ -10,7 +11,6 @@ DontLeave = False BackLight = "/proc/driver/backlight" Battery = "/sys/class/power_supply/axp20x-battery/uevent" - MPD_socket = "/tmp/mpd.socket" UPDATE_URL="https://raw.githubusercontent.com/clockworkpi/CPI/master/launcher_ver0.4.json" @@ -21,6 +21,7 @@ SKIN=None ButtonsLayout="xbox" +RPC = None ## three timer values in seconds: dim screen, close screen,PowerOff ## zero means no action PowerLevels = {} @@ -34,7 +35,7 @@ PowerLevel = "balance_saving" def PreparationInAdv(): global SKIN,ButtonsLayout global PowerLevel - + global RPC if SKIN != None: return @@ -71,7 +72,8 @@ def PreparationInAdv(): ArmSystem("sudo iw wlan0 set power_save on > /dev/null") else: ArmSystem("sudo iw wlan0 set power_save off >/dev/null") - + + RPC = Xmlrpc('localhost', 6800) PreparationInAdv() ##sys.py/.powerlevel diff --git a/sys.py/libs/websocket/__init__.py b/sys.py/libs/websocket/__init__.py new file mode 100644 index 0000000..15c74ce --- /dev/null +++ b/sys.py/libs/websocket/__init__.py @@ -0,0 +1,29 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +from ._abnf import * +from ._app import WebSocketApp +from ._core import * +from ._exceptions import * +from ._logging import * +from ._socket import * + +__version__ = "0.56.0" diff --git a/sys.py/libs/websocket/_abnf.py b/sys.py/libs/websocket/_abnf.py new file mode 100644 index 0000000..a0000fa --- /dev/null +++ b/sys.py/libs/websocket/_abnf.py @@ -0,0 +1,447 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import array +import os +import struct + +import six + +from ._exceptions import * +from ._utils import validate_utf8 +from threading import Lock + +try: + if six.PY3: + import numpy + else: + numpy = None +except ImportError: + numpy = None + +try: + # If wsaccel is available we use compiled routines to mask data. + if not numpy: + from wsaccel.xormask import XorMaskerSimple + + def _mask(_m, _d): + return XorMaskerSimple(_m).process(_d) +except ImportError: + # wsaccel is not available, we rely on python implementations. + def _mask(_m, _d): + for i in range(len(_d)): + _d[i] ^= _m[i % 4] + + if six.PY3: + return _d.tobytes() + else: + return _d.tostring() + + +__all__ = [ + 'ABNF', 'continuous_frame', 'frame_buffer', + 'STATUS_NORMAL', + 'STATUS_GOING_AWAY', + 'STATUS_PROTOCOL_ERROR', + 'STATUS_UNSUPPORTED_DATA_TYPE', + 'STATUS_STATUS_NOT_AVAILABLE', + 'STATUS_ABNORMAL_CLOSED', + 'STATUS_INVALID_PAYLOAD', + 'STATUS_POLICY_VIOLATION', + 'STATUS_MESSAGE_TOO_BIG', + 'STATUS_INVALID_EXTENSION', + 'STATUS_UNEXPECTED_CONDITION', + 'STATUS_BAD_GATEWAY', + 'STATUS_TLS_HANDSHAKE_ERROR', +] + +# closing frame status codes. +STATUS_NORMAL = 1000 +STATUS_GOING_AWAY = 1001 +STATUS_PROTOCOL_ERROR = 1002 +STATUS_UNSUPPORTED_DATA_TYPE = 1003 +STATUS_STATUS_NOT_AVAILABLE = 1005 +STATUS_ABNORMAL_CLOSED = 1006 +STATUS_INVALID_PAYLOAD = 1007 +STATUS_POLICY_VIOLATION = 1008 +STATUS_MESSAGE_TOO_BIG = 1009 +STATUS_INVALID_EXTENSION = 1010 +STATUS_UNEXPECTED_CONDITION = 1011 +STATUS_BAD_GATEWAY = 1014 +STATUS_TLS_HANDSHAKE_ERROR = 1015 + +VALID_CLOSE_STATUS = ( + STATUS_NORMAL, + STATUS_GOING_AWAY, + STATUS_PROTOCOL_ERROR, + STATUS_UNSUPPORTED_DATA_TYPE, + STATUS_INVALID_PAYLOAD, + STATUS_POLICY_VIOLATION, + STATUS_MESSAGE_TOO_BIG, + STATUS_INVALID_EXTENSION, + STATUS_UNEXPECTED_CONDITION, + STATUS_BAD_GATEWAY, +) + + +class ABNF(object): + """ + ABNF frame class. + see http://tools.ietf.org/html/rfc5234 + and http://tools.ietf.org/html/rfc6455#section-5.2 + """ + + # operation code values. + OPCODE_CONT = 0x0 + OPCODE_TEXT = 0x1 + OPCODE_BINARY = 0x2 + OPCODE_CLOSE = 0x8 + OPCODE_PING = 0x9 + OPCODE_PONG = 0xa + + # available operation code value tuple + OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE, + OPCODE_PING, OPCODE_PONG) + + # opcode human readable string + OPCODE_MAP = { + OPCODE_CONT: "cont", + OPCODE_TEXT: "text", + OPCODE_BINARY: "binary", + OPCODE_CLOSE: "close", + OPCODE_PING: "ping", + OPCODE_PONG: "pong" + } + + # data length threshold. + LENGTH_7 = 0x7e + LENGTH_16 = 1 << 16 + LENGTH_63 = 1 << 63 + + def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0, + opcode=OPCODE_TEXT, mask=1, data=""): + """ + Constructor for ABNF. + please check RFC for arguments. + """ + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.opcode = opcode + self.mask = mask + if data is None: + data = "" + self.data = data + self.get_mask_key = os.urandom + + def validate(self, skip_utf8_validation=False): + """ + validate the ABNF frame. + skip_utf8_validation: skip utf8 validation. + """ + if self.rsv1 or self.rsv2 or self.rsv3: + raise WebSocketProtocolException("rsv is not implemented, yet") + + if self.opcode not in ABNF.OPCODES: + raise WebSocketProtocolException("Invalid opcode %r", self.opcode) + + if self.opcode == ABNF.OPCODE_PING and not self.fin: + raise WebSocketProtocolException("Invalid ping frame.") + + if self.opcode == ABNF.OPCODE_CLOSE: + l = len(self.data) + if not l: + return + if l == 1 or l >= 126: + raise WebSocketProtocolException("Invalid close frame.") + if l > 2 and not skip_utf8_validation and not validate_utf8(self.data[2:]): + raise WebSocketProtocolException("Invalid close frame.") + + code = 256 * \ + six.byte2int(self.data[0:1]) + six.byte2int(self.data[1:2]) + if not self._is_valid_close_status(code): + raise WebSocketProtocolException("Invalid close opcode.") + + @staticmethod + def _is_valid_close_status(code): + return code in VALID_CLOSE_STATUS or (3000 <= code < 5000) + + def __str__(self): + return "fin=" + str(self.fin) \ + + " opcode=" + str(self.opcode) \ + + " data=" + str(self.data) + + @staticmethod + def create_frame(data, opcode, fin=1): + """ + create frame to send text, binary and other data. + + data: data to send. This is string value(byte array). + if opcode is OPCODE_TEXT and this value is unicode, + data value is converted into unicode string, automatically. + + opcode: operation code. please see OPCODE_XXX. + + fin: fin flag. if set to 0, create continue fragmentation. + """ + if opcode == ABNF.OPCODE_TEXT and isinstance(data, six.text_type): + data = data.encode("utf-8") + # mask must be set if send data from client + return ABNF(fin, 0, 0, 0, opcode, 1, data) + + def format(self): + """ + format this object to string(byte array) to send data to server. + """ + if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]): + raise ValueError("not 0 or 1") + if self.opcode not in ABNF.OPCODES: + raise ValueError("Invalid OPCODE") + length = len(self.data) + if length >= ABNF.LENGTH_63: + raise ValueError("data is too long") + + frame_header = chr(self.fin << 7 + | self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4 + | self.opcode) + if length < ABNF.LENGTH_7: + frame_header += chr(self.mask << 7 | length) + frame_header = six.b(frame_header) + elif length < ABNF.LENGTH_16: + frame_header += chr(self.mask << 7 | 0x7e) + frame_header = six.b(frame_header) + frame_header += struct.pack("!H", length) + else: + frame_header += chr(self.mask << 7 | 0x7f) + frame_header = six.b(frame_header) + frame_header += struct.pack("!Q", length) + + if not self.mask: + return frame_header + self.data + else: + mask_key = self.get_mask_key(4) + return frame_header + self._get_masked(mask_key) + + def _get_masked(self, mask_key): + s = ABNF.mask(mask_key, self.data) + + if isinstance(mask_key, six.text_type): + mask_key = mask_key.encode('utf-8') + + return mask_key + s + + @staticmethod + def mask(mask_key, data): + """ + mask or unmask data. Just do xor for each byte + + mask_key: 4 byte string(byte). + + data: data to mask/unmask. + """ + if data is None: + data = "" + + if isinstance(mask_key, six.text_type): + mask_key = six.b(mask_key) + + if isinstance(data, six.text_type): + data = six.b(data) + + if numpy: + origlen = len(data) + _mask_key = mask_key[3] << 24 | mask_key[2] << 16 | mask_key[1] << 8 | mask_key[0] + + # We need data to be a multiple of four... + data += bytes(" " * (4 - (len(data) % 4)), "us-ascii") + a = numpy.frombuffer(data, dtype="uint32") + masked = numpy.bitwise_xor(a, [_mask_key]).astype("uint32") + if len(data) > origlen: + return masked.tobytes()[:origlen] + return masked.tobytes() + else: + _m = array.array("B", mask_key) + _d = array.array("B", data) + return _mask(_m, _d) + + +class frame_buffer(object): + _HEADER_MASK_INDEX = 5 + _HEADER_LENGTH_INDEX = 6 + + def __init__(self, recv_fn, skip_utf8_validation): + self.recv = recv_fn + self.skip_utf8_validation = skip_utf8_validation + # Buffers over the packets from the layer beneath until desired amount + # bytes of bytes are received. + self.recv_buffer = [] + self.clear() + self.lock = Lock() + + def clear(self): + self.header = None + self.length = None + self.mask = None + + def has_received_header(self): + return self.header is None + + def recv_header(self): + header = self.recv_strict(2) + b1 = header[0] + + if six.PY2: + b1 = ord(b1) + + fin = b1 >> 7 & 1 + rsv1 = b1 >> 6 & 1 + rsv2 = b1 >> 5 & 1 + rsv3 = b1 >> 4 & 1 + opcode = b1 & 0xf + b2 = header[1] + + if six.PY2: + b2 = ord(b2) + + has_mask = b2 >> 7 & 1 + length_bits = b2 & 0x7f + + self.header = (fin, rsv1, rsv2, rsv3, opcode, has_mask, length_bits) + + def has_mask(self): + if not self.header: + return False + return self.header[frame_buffer._HEADER_MASK_INDEX] + + def has_received_length(self): + return self.length is None + + def recv_length(self): + bits = self.header[frame_buffer._HEADER_LENGTH_INDEX] + length_bits = bits & 0x7f + if length_bits == 0x7e: + v = self.recv_strict(2) + self.length = struct.unpack("!H", v)[0] + elif length_bits == 0x7f: + v = self.recv_strict(8) + self.length = struct.unpack("!Q", v)[0] + else: + self.length = length_bits + + def has_received_mask(self): + return self.mask is None + + def recv_mask(self): + self.mask = self.recv_strict(4) if self.has_mask() else "" + + def recv_frame(self): + + with self.lock: + # Header + if self.has_received_header(): + self.recv_header() + (fin, rsv1, rsv2, rsv3, opcode, has_mask, _) = self.header + + # Frame length + if self.has_received_length(): + self.recv_length() + length = self.length + + # Mask + if self.has_received_mask(): + self.recv_mask() + mask = self.mask + + # Payload + payload = self.recv_strict(length) + if has_mask: + payload = ABNF.mask(mask, payload) + + # Reset for next frame + self.clear() + + frame = ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload) + frame.validate(self.skip_utf8_validation) + + return frame + + def recv_strict(self, bufsize): + shortage = bufsize - sum(len(x) for x in self.recv_buffer) + while shortage > 0: + # Limit buffer size that we pass to socket.recv() to avoid + # fragmenting the heap -- the number of bytes recv() actually + # reads is limited by socket buffer and is relatively small, + # yet passing large numbers repeatedly causes lots of large + # buffers allocated and then shrunk, which results in + # fragmentation. + bytes_ = self.recv(min(16384, shortage)) + self.recv_buffer.append(bytes_) + shortage -= len(bytes_) + + unified = six.b("").join(self.recv_buffer) + + if shortage == 0: + self.recv_buffer = [] + return unified + else: + self.recv_buffer = [unified[bufsize:]] + return unified[:bufsize] + + +class continuous_frame(object): + + def __init__(self, fire_cont_frame, skip_utf8_validation): + self.fire_cont_frame = fire_cont_frame + self.skip_utf8_validation = skip_utf8_validation + self.cont_data = None + self.recving_frames = None + + def validate(self, frame): + if not self.recving_frames and frame.opcode == ABNF.OPCODE_CONT: + raise WebSocketProtocolException("Illegal frame") + if self.recving_frames and \ + frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + raise WebSocketProtocolException("Illegal frame") + + def add(self, frame): + if self.cont_data: + self.cont_data[1] += frame.data + else: + if frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY): + self.recving_frames = frame.opcode + self.cont_data = [frame.opcode, frame.data] + + if frame.fin: + self.recving_frames = None + + def is_fire(self, frame): + return frame.fin or self.fire_cont_frame + + def extract(self, frame): + data = self.cont_data + self.cont_data = None + frame.data = data[1] + if not self.fire_cont_frame and data[0] == ABNF.OPCODE_TEXT and not self.skip_utf8_validation and not validate_utf8(frame.data): + raise WebSocketPayloadException( + "cannot decode: " + repr(frame.data)) + + return [data[0], frame] diff --git a/sys.py/libs/websocket/_app.py b/sys.py/libs/websocket/_app.py new file mode 100644 index 0000000..81aa1fc --- /dev/null +++ b/sys.py/libs/websocket/_app.py @@ -0,0 +1,351 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + +""" +WebSocketApp provides higher level APIs. +""" +import inspect +import select +import sys +import threading +import time +import traceback + +import six + +from ._abnf import ABNF +from ._core import WebSocket, getdefaulttimeout +from ._exceptions import * +from . import _logging + + +__all__ = ["WebSocketApp"] + +class Dispatcher: + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, read_callback, check_callback): + while self.app.sock.connected: + r, w, e = select.select( + (self.app.sock.sock, ), (), (), self.ping_timeout) + if r: + if not read_callback(): + break + check_callback() + +class SSLDispacther: + def __init__(self, app, ping_timeout): + self.app = app + self.ping_timeout = ping_timeout + + def read(self, sock, read_callback, check_callback): + while self.app.sock.connected: + r = self.select() + if r: + if not read_callback(): + break + check_callback() + + def select(self): + sock = self.app.sock.sock + if sock.pending(): + return [sock,] + + r, w, e = select.select((sock, ), (), (), self.ping_timeout) + return r + +class WebSocketApp(object): + """ + Higher level of APIs are provided. + The interface is like JavaScript WebSocket object. + """ + + def __init__(self, url, header=None, + on_open=None, on_message=None, on_error=None, + on_close=None, on_ping=None, on_pong=None, + on_cont_message=None, + keep_running=True, get_mask_key=None, cookie=None, + subprotocols=None, + on_data=None): + """ + url: websocket url. + header: custom header for websocket handshake. + on_open: callable object which is called at opening websocket. + this function has one argument. The argument is this class object. + on_message: callable object which is called when received data. + on_message has 2 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + on_error: callable object which is called when we get error. + on_error has 2 arguments. + The 1st argument is this class object. + The 2nd argument is exception object. + on_close: callable object which is called when closed the connection. + this function has one argument. The argument is this class object. + on_cont_message: callback object which is called when receive continued + frame data. + on_cont_message has 3 arguments. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is continue flag. if 0, the data continue + to next frame data + on_data: callback object which is called when a message received. + This is called before on_message or on_cont_message, + and then on_message or on_cont_message is called. + on_data has 4 argument. + The 1st argument is this class object. + The 2nd argument is utf-8 string which we get from the server. + The 3rd argument is data type. ABNF.OPCODE_TEXT or ABNF.OPCODE_BINARY will be came. + The 4th argument is continue flag. if 0, the data continue + keep_running: this parameter is obsolete and ignored. + get_mask_key: a callable to produce new mask keys, + see the WebSocket.set_mask_key's docstring for more information + subprotocols: array of available sub protocols. default is None. + """ + self.url = url + self.header = header if header is not None else [] + self.cookie = cookie + + self.on_open = on_open + self.on_message = on_message + self.on_data = on_data + self.on_error = on_error + self.on_close = on_close + self.on_ping = on_ping + self.on_pong = on_pong + self.on_cont_message = on_cont_message + self.keep_running = False + self.get_mask_key = get_mask_key + self.sock = None + self.last_ping_tm = 0 + self.last_pong_tm = 0 + self.subprotocols = subprotocols + + def send(self, data, opcode=ABNF.OPCODE_TEXT): + """ + send message. + data: message to send. If you set opcode to OPCODE_TEXT, + data must be utf-8 string or unicode. + opcode: operation code of data. default is OPCODE_TEXT. + """ + + if not self.sock or self.sock.send(data, opcode) == 0: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + def close(self, **kwargs): + """ + close websocket connection. + """ + self.keep_running = False + if self.sock: + self.sock.close(**kwargs) + self.sock = None + + def _send_ping(self, interval, event): + while not event.wait(interval): + self.last_ping_tm = time.time() + if self.sock: + try: + self.sock.ping() + except Exception as ex: + _logging.warning("send_ping routine terminated: {}".format(ex)) + break + + def run_forever(self, sockopt=None, sslopt=None, + ping_interval=0, ping_timeout=None, + http_proxy_host=None, http_proxy_port=None, + http_no_proxy=None, http_proxy_auth=None, + skip_utf8_validation=False, + host=None, origin=None, dispatcher=None, + suppress_origin = False, proxy_type=None): + """ + run event loop for WebSocket framework. + This loop is infinite loop and is alive during websocket is available. + sockopt: values for socket.setsockopt. + sockopt must be tuple + and each element is argument of sock.setsockopt. + sslopt: ssl socket optional dict. + ping_interval: automatically send "ping" command + every specified period(second) + if set to 0, not send automatically. + ping_timeout: timeout(second) if the pong message is not received. + http_proxy_host: http proxy host name. + http_proxy_port: http proxy port. If not set, set to 80. + http_no_proxy: host names, which doesn't use proxy. + skip_utf8_validation: skip utf8 validation. + host: update host header. + origin: update origin header. + dispatcher: customize reading data from socket. + suppress_origin: suppress outputting origin header. + + Returns + ------- + False if caught KeyboardInterrupt + True if other exception was raised during a loop + """ + + if ping_timeout is not None and ping_timeout <= 0: + ping_timeout = None + if ping_timeout and ping_interval and ping_interval <= ping_timeout: + raise WebSocketException("Ensure ping_interval > ping_timeout") + if not sockopt: + sockopt = [] + if not sslopt: + sslopt = {} + if self.sock: + raise WebSocketException("socket is already opened") + thread = None + self.keep_running = True + self.last_ping_tm = 0 + self.last_pong_tm = 0 + + def teardown(close_frame=None): + """ + Tears down the connection. + If close_frame is set, we will invoke the on_close handler with the + statusCode and reason from there. + """ + if thread and thread.isAlive(): + event.set() + thread.join() + self.keep_running = False + if self.sock: + self.sock.close() + close_args = self._get_close_args( + close_frame.data if close_frame else None) + self._callback(self.on_close, *close_args) + self.sock = None + + try: + self.sock = WebSocket( + self.get_mask_key, sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=self.on_cont_message is not None, + skip_utf8_validation=skip_utf8_validation, + enable_multithread=True if ping_interval else False) + self.sock.settimeout(getdefaulttimeout()) + self.sock.connect( + self.url, header=self.header, cookie=self.cookie, + http_proxy_host=http_proxy_host, + http_proxy_port=http_proxy_port, http_no_proxy=http_no_proxy, + http_proxy_auth=http_proxy_auth, subprotocols=self.subprotocols, + host=host, origin=origin, suppress_origin=suppress_origin, + proxy_type=proxy_type) + if not dispatcher: + dispatcher = self.create_dispatcher(ping_timeout) + + self._callback(self.on_open) + + if ping_interval: + event = threading.Event() + thread = threading.Thread( + target=self._send_ping, args=(ping_interval, event)) + thread.setDaemon(True) + thread.start() + + def read(): + if not self.keep_running: + return teardown() + + op_code, frame = self.sock.recv_data_frame(True) + if op_code == ABNF.OPCODE_CLOSE: + return teardown(frame) + elif op_code == ABNF.OPCODE_PING: + self._callback(self.on_ping, frame.data) + elif op_code == ABNF.OPCODE_PONG: + self.last_pong_tm = time.time() + self._callback(self.on_pong, frame.data) + elif op_code == ABNF.OPCODE_CONT and self.on_cont_message: + self._callback(self.on_data, frame.data, + frame.opcode, frame.fin) + self._callback(self.on_cont_message, + frame.data, frame.fin) + else: + data = frame.data + if six.PY3 and op_code == ABNF.OPCODE_TEXT: + data = data.decode("utf-8") + self._callback(self.on_data, data, frame.opcode, True) + self._callback(self.on_message, data) + + return True + + def check(): + if (ping_timeout): + has_timeout_expired = time.time() - self.last_ping_tm > ping_timeout + has_pong_not_arrived_after_last_ping = self.last_pong_tm - self.last_ping_tm < 0 + has_pong_arrived_too_late = self.last_pong_tm - self.last_ping_tm > ping_timeout + + if (self.last_ping_tm + and has_timeout_expired + and (has_pong_not_arrived_after_last_ping or has_pong_arrived_too_late)): + raise WebSocketTimeoutException("ping/pong timed out") + return True + + dispatcher.read(self.sock.sock, read, check) + except (Exception, KeyboardInterrupt, SystemExit) as e: + self._callback(self.on_error, e) + if isinstance(e, SystemExit): + # propagate SystemExit further + raise + teardown() + return not isinstance(e, KeyboardInterrupt) + + def create_dispatcher(self, ping_timeout): + timeout = ping_timeout or 10 + if self.sock.is_ssl(): + return SSLDispacther(self, timeout) + + return Dispatcher(self, timeout) + + def _get_close_args(self, data): + """ this functions extracts the code, reason from the close body + if they exists, and if the self.on_close except three arguments """ + # if the on_close callback is "old", just return empty list + if sys.version_info < (3, 0): + if not self.on_close or len(inspect.getargspec(self.on_close).args) != 3: + return [] + else: + if not self.on_close or len(inspect.getfullargspec(self.on_close).args) != 3: + return [] + + if data and len(data) >= 2: + code = 256 * six.byte2int(data[0:1]) + six.byte2int(data[1:2]) + reason = data[2:].decode('utf-8') + return [code, reason] + + return [None, None] + + def _callback(self, callback, *args): + if callback: + try: + if inspect.ismethod(callback): + callback(*args) + else: + callback(self, *args) + + except Exception as e: + _logging.error("error from callback {}: {}".format(callback, e)) + if _logging.isEnabledForDebug(): + _, _, tb = sys.exc_info() + traceback.print_tb(tb) diff --git a/sys.py/libs/websocket/_cookiejar.py b/sys.py/libs/websocket/_cookiejar.py new file mode 100644 index 0000000..3efeb0f --- /dev/null +++ b/sys.py/libs/websocket/_cookiejar.py @@ -0,0 +1,52 @@ +try: + import Cookie +except: + import http.cookies as Cookie + + +class SimpleCookieJar(object): + def __init__(self): + self.jar = dict() + + def add(self, set_cookie): + if set_cookie: + try: + simpleCookie = Cookie.SimpleCookie(set_cookie) + except: + simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + cookie = self.jar.get(domain) if self.jar.get(domain) else Cookie.SimpleCookie() + cookie.update(simpleCookie) + self.jar[domain.lower()] = cookie + + def set(self, set_cookie): + if set_cookie: + try: + simpleCookie = Cookie.SimpleCookie(set_cookie) + except: + simpleCookie = Cookie.SimpleCookie(set_cookie.encode('ascii', 'ignore')) + + for k, v in simpleCookie.items(): + domain = v.get("domain") + if domain: + if not domain.startswith("."): + domain = "." + domain + self.jar[domain.lower()] = simpleCookie + + def get(self, host): + if not host: + return "" + + cookies = [] + for domain, simpleCookie in self.jar.items(): + host = host.lower() + if host.endswith(domain) or host == domain[1:]: + cookies.append(self.jar.get(domain)) + + return "; ".join(filter(None, ["%s=%s" % (k, v.value) for cookie in filter(None, sorted(cookies)) for k, v in + sorted(cookie.items())])) diff --git a/sys.py/libs/websocket/_core.py b/sys.py/libs/websocket/_core.py new file mode 100644 index 0000000..0f914c2 --- /dev/null +++ b/sys.py/libs/websocket/_core.py @@ -0,0 +1,515 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +from __future__ import print_function + +import socket +import struct +import threading +import time + +import six + +# websocket modules +from ._abnf import * +from ._exceptions import * +from ._handshake import * +from ._http import * +from ._logging import * +from ._socket import * +from ._ssl_compat import * +from ._utils import * + +__all__ = ['WebSocket', 'create_connection'] + +""" +websocket python client. +========================= + +This version support only hybi-13. +Please see http://tools.ietf.org/html/rfc6455 for protocol. +""" + + +class WebSocket(object): + """ + Low level WebSocket interface. + This class is based on + The WebSocket protocol draft-hixie-thewebsocketprotocol-76 + http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 + + We can connect to the websocket server and send/receive data. + The following example is an echo client. + + >>> import websocket + >>> ws = websocket.WebSocket() + >>> ws.connect("ws://echo.websocket.org") + >>> ws.send("Hello, Server") + >>> ws.recv() + 'Hello, Server' + >>> ws.close() + + get_mask_key: a callable to produce new mask keys, see the set_mask_key + function's docstring for more details + sockopt: values for socket.setsockopt. + sockopt must be tuple and each element is argument of sock.setsockopt. + sslopt: dict object for ssl socket option. + fire_cont_frame: fire recv event for each cont frame. default is False + enable_multithread: if set to True, lock send method. + skip_utf8_validation: skip utf8 validation. + """ + + def __init__(self, get_mask_key=None, sockopt=None, sslopt=None, + fire_cont_frame=False, enable_multithread=False, + skip_utf8_validation=False, **_): + """ + Initialize WebSocket object. + """ + self.sock_opt = sock_opt(sockopt, sslopt) + self.handshake_response = None + self.sock = None + + self.connected = False + self.get_mask_key = get_mask_key + # These buffer over the build-up of a single frame. + self.frame_buffer = frame_buffer(self._recv, skip_utf8_validation) + self.cont_frame = continuous_frame( + fire_cont_frame, skip_utf8_validation) + + if enable_multithread: + self.lock = threading.Lock() + self.readlock = threading.Lock() + else: + self.lock = NoLock() + self.readlock = NoLock() + + def __iter__(self): + """ + Allow iteration over websocket, implying sequential `recv` executions. + """ + while True: + yield self.recv() + + def __next__(self): + return self.recv() + + def next(self): + return self.__next__() + + def fileno(self): + return self.sock.fileno() + + def set_mask_key(self, func): + """ + set function to create musk key. You can customize mask key generator. + Mainly, this is for testing purpose. + + func: callable object. the func takes 1 argument as integer. + The argument means length of mask key. + This func must return string(byte array), + which length is argument specified. + """ + self.get_mask_key = func + + def gettimeout(self): + """ + Get the websocket timeout(second). + """ + return self.sock_opt.timeout + + def settimeout(self, timeout): + """ + Set the timeout to the websocket. + + timeout: timeout time(second). + """ + self.sock_opt.timeout = timeout + if self.sock: + self.sock.settimeout(timeout) + + timeout = property(gettimeout, settimeout) + + def getsubprotocol(self): + """ + get subprotocol + """ + if self.handshake_response: + return self.handshake_response.subprotocol + else: + return None + + subprotocol = property(getsubprotocol) + + def getstatus(self): + """ + get handshake status + """ + if self.handshake_response: + return self.handshake_response.status + else: + return None + + status = property(getstatus) + + def getheaders(self): + """ + get handshake response header + """ + if self.handshake_response: + return self.handshake_response.headers + else: + return None + + def is_ssl(self): + return isinstance(self.sock, ssl.SSLSocket) + + headers = property(getheaders) + + def connect(self, url, **options): + """ + Connect to url. url is websocket url scheme. + ie. ws://host:port/resource + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> ws = WebSocket() + >>> ws.connect("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + options: "header" -> custom http header list or dict. + "cookie" -> cookie value. + "origin" -> custom origin url. + "suppress_origin" -> suppress outputting origin header. + "host" -> custom host header string. + "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. If not set, set to 80. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "redirect_limit" -> number of redirects to follow. + "subprotocols" - array of available sub protocols. + default is None. + "socket" - pre-initialized stream socket. + + """ + # FIXME: "subprotocols" are getting lost, not passed down + # FIXME: "header", "cookie", "origin" and "host" too + self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout) + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + + try: + self.handshake_response = handshake(self.sock, *addrs, **options) + for attempt in range(options.pop('redirect_limit', 3)): + if self.handshake_response.status in SUPPORTED_REDIRECT_STATUSES: + url = self.handshake_response.headers['location'] + self.sock.close() + self.sock, addrs = connect(url, self.sock_opt, proxy_info(**options), + options.pop('socket', None)) + self.handshake_response = handshake(self.sock, *addrs, **options) + self.connected = True + except: + if self.sock: + self.sock.close() + self.sock = None + raise + + def send(self, payload, opcode=ABNF.OPCODE_TEXT): + """ + Send the data as string. + + payload: Payload must be utf-8 string or unicode, + if the opcode is OPCODE_TEXT. + Otherwise, it must be string(byte array) + + opcode: operation code to send. Please see OPCODE_XXX. + """ + + frame = ABNF.create_frame(payload, opcode) + return self.send_frame(frame) + + def send_frame(self, frame): + """ + Send the data frame. + + frame: frame data created by ABNF.create_frame + + >>> ws = create_connection("ws://echo.websocket.org/") + >>> frame = ABNF.create_frame("Hello", ABNF.OPCODE_TEXT) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("My name is ", ABNF.OPCODE_CONT, 0) + >>> ws.send_frame(frame) + >>> cont_frame = ABNF.create_frame("Foo Bar", ABNF.OPCODE_CONT, 1) + >>> ws.send_frame(frame) + + """ + if self.get_mask_key: + frame.get_mask_key = self.get_mask_key + data = frame.format() + length = len(data) + trace("send: " + repr(data)) + + with self.lock: + while data: + l = self._send(data) + data = data[l:] + + return length + + def send_binary(self, payload): + return self.send(payload, ABNF.OPCODE_BINARY) + + def ping(self, payload=""): + """ + send ping data. + + payload: data payload to send server. + """ + if isinstance(payload, six.text_type): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PING) + + def pong(self, payload): + """ + send pong data. + + payload: data payload to send server. + """ + if isinstance(payload, six.text_type): + payload = payload.encode("utf-8") + self.send(payload, ABNF.OPCODE_PONG) + + def recv(self): + """ + Receive string data(byte array) from the server. + + return value: string(byte array) value. + """ + with self.readlock: + opcode, data = self.recv_data() + if six.PY3 and opcode == ABNF.OPCODE_TEXT: + return data.decode("utf-8") + elif opcode == ABNF.OPCODE_TEXT or opcode == ABNF.OPCODE_BINARY: + return data + else: + return '' + + def recv_data(self, control_frame=False): + """ + Receive data with operation code. + + control_frame: a boolean flag indicating whether to return control frame + data, defaults to False + + return value: tuple of operation code and string(byte array) value. + """ + opcode, frame = self.recv_data_frame(control_frame) + return opcode, frame.data + + def recv_data_frame(self, control_frame=False): + """ + Receive data with operation code. + + control_frame: a boolean flag indicating whether to return control frame + data, defaults to False + + return value: tuple of operation code and string(byte array) value. + """ + while True: + frame = self.recv_frame() + if not frame: + # handle error: + # 'NoneType' object has no attribute 'opcode' + raise WebSocketProtocolException( + "Not a valid frame %s" % frame) + elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT): + self.cont_frame.validate(frame) + self.cont_frame.add(frame) + + if self.cont_frame.is_fire(frame): + return self.cont_frame.extract(frame) + + elif frame.opcode == ABNF.OPCODE_CLOSE: + self.send_close() + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PING: + if len(frame.data) < 126: + self.pong(frame.data) + else: + raise WebSocketProtocolException( + "Ping message is too long") + if control_frame: + return frame.opcode, frame + elif frame.opcode == ABNF.OPCODE_PONG: + if control_frame: + return frame.opcode, frame + + def recv_frame(self): + """ + receive data as frame from server. + + return value: ABNF frame object. + """ + return self.frame_buffer.recv_frame() + + def send_close(self, status=STATUS_NORMAL, reason=six.b("")): + """ + send close data to the server. + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string or bytes. + """ + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + self.connected = False + self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE) + + def close(self, status=STATUS_NORMAL, reason=six.b(""), timeout=3): + """ + Close Websocket object + + status: status code to send. see STATUS_XXX. + + reason: the reason to close. This must be string. + + timeout: timeout until receive a close frame. + If None, it will wait forever until receive a close frame. + """ + if self.connected: + if status < 0 or status >= ABNF.LENGTH_16: + raise ValueError("code is invalid range") + + try: + self.connected = False + self.send(struct.pack('!H', status) + + reason, ABNF.OPCODE_CLOSE) + sock_timeout = self.sock.gettimeout() + self.sock.settimeout(timeout) + start_time = time.time() + while timeout is None or time.time() - start_time < timeout: + try: + frame = self.recv_frame() + if frame.opcode != ABNF.OPCODE_CLOSE: + continue + if isEnabledForError(): + recv_status = struct.unpack("!H", frame.data[0:2])[0] + if recv_status != STATUS_NORMAL: + error("close status: " + repr(recv_status)) + break + except: + break + self.sock.settimeout(sock_timeout) + self.sock.shutdown(socket.SHUT_RDWR) + except: + pass + + self.shutdown() + + def abort(self): + """ + Low-level asynchronous abort, wakes up other threads that are waiting in recv_* + """ + if self.connected: + self.sock.shutdown(socket.SHUT_RDWR) + + def shutdown(self): + """close socket, immediately.""" + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + + def _send(self, data): + return send(self.sock, data) + + def _recv(self, bufsize): + try: + return recv(self.sock, bufsize) + except WebSocketConnectionClosedException: + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + raise + + +def create_connection(url, timeout=None, class_=WebSocket, **options): + """ + connect to url and return websocket object. + + Connect to url and return the WebSocket object. + Passing optional timeout parameter will set the timeout on the socket. + If no timeout is supplied, + the global default timeout setting returned by getdefauttimeout() is used. + You can customize using 'options'. + If you set "header" list object, you can set your own custom header. + + >>> conn = create_connection("ws://echo.websocket.org/", + ... header=["User-Agent: MyProgram", + ... "x-custom: header"]) + + + timeout: socket timeout time. This value is integer. + if you set None for this value, + it means "use default_timeout value" + + class_: class to instantiate when creating the connection. It has to implement + settimeout and connect. It's __init__ should be compatible with + WebSocket.__init__, i.e. accept all of it's kwargs. + options: "header" -> custom http header list or dict. + "cookie" -> cookie value. + "origin" -> custom origin url. + "suppress_origin" -> suppress outputting origin header. + "host" -> custom host header string. + "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. If not set, set to 80. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "enable_multithread" -> enable lock for multithread. + "redirect_limit" -> number of redirects to follow. + "sockopt" -> socket options + "sslopt" -> ssl option + "subprotocols" - array of available sub protocols. + default is None. + "skip_utf8_validation" - skip utf8 validation. + "socket" - pre-initialized stream socket. + """ + sockopt = options.pop("sockopt", []) + sslopt = options.pop("sslopt", {}) + fire_cont_frame = options.pop("fire_cont_frame", False) + enable_multithread = options.pop("enable_multithread", False) + skip_utf8_validation = options.pop("skip_utf8_validation", False) + websock = class_(sockopt=sockopt, sslopt=sslopt, + fire_cont_frame=fire_cont_frame, + enable_multithread=enable_multithread, + skip_utf8_validation=skip_utf8_validation, **options) + websock.settimeout(timeout if timeout is not None else getdefaulttimeout()) + websock.connect(url, **options) + return websock diff --git a/sys.py/libs/websocket/_exceptions.py b/sys.py/libs/websocket/_exceptions.py new file mode 100644 index 0000000..b7a61d3 --- /dev/null +++ b/sys.py/libs/websocket/_exceptions.py @@ -0,0 +1,87 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + + +""" +define websocket exceptions +""" + + +class WebSocketException(Exception): + """ + websocket exception class. + """ + pass + + +class WebSocketProtocolException(WebSocketException): + """ + If the websocket protocol is invalid, this exception will be raised. + """ + pass + + +class WebSocketPayloadException(WebSocketException): + """ + If the websocket payload is invalid, this exception will be raised. + """ + pass + + +class WebSocketConnectionClosedException(WebSocketException): + """ + If remote host closed the connection or some network error happened, + this exception will be raised. + """ + pass + + +class WebSocketTimeoutException(WebSocketException): + """ + WebSocketTimeoutException will be raised at socket timeout during read/write data. + """ + pass + + +class WebSocketProxyException(WebSocketException): + """ + WebSocketProxyException will be raised when proxy error occurred. + """ + pass + + +class WebSocketBadStatusException(WebSocketException): + """ + WebSocketBadStatusException will be raised when we get bad handshake status code. + """ + + def __init__(self, message, status_code, status_message=None, resp_headers=None): + msg = message % (status_code, status_message) + super(WebSocketBadStatusException, self).__init__(msg) + self.status_code = status_code + self.resp_headers = resp_headers + +class WebSocketAddressException(WebSocketException): + """ + If the websocket address info cannot be found, this exception will be raised. + """ + pass diff --git a/sys.py/libs/websocket/_handshake.py b/sys.py/libs/websocket/_handshake.py new file mode 100644 index 0000000..809a8c9 --- /dev/null +++ b/sys.py/libs/websocket/_handshake.py @@ -0,0 +1,205 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import hashlib +import hmac +import os + +import six + +from ._cookiejar import SimpleCookieJar +from ._exceptions import * +from ._http import * +from ._logging import * +from ._socket import * + +if six.PY3: + from base64 import encodebytes as base64encode +else: + from base64 import encodestring as base64encode + +if six.PY3: + if six.PY34: + from http import client as HTTPStatus + else: + from http import HTTPStatus +else: + import httplib as HTTPStatus + +__all__ = ["handshake_response", "handshake", "SUPPORTED_REDIRECT_STATUSES"] + +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + def compare_digest(s1, s2): + return s1 == s2 + +# websocket supported version. +VERSION = 13 + +SUPPORTED_REDIRECT_STATUSES = [HTTPStatus.MOVED_PERMANENTLY, HTTPStatus.FOUND, HTTPStatus.SEE_OTHER] + +CookieJar = SimpleCookieJar() + + +class handshake_response(object): + + def __init__(self, status, headers, subprotocol): + self.status = status + self.headers = headers + self.subprotocol = subprotocol + CookieJar.add(headers.get("set-cookie")) + + +def handshake(sock, hostname, port, resource, **options): + headers, key = _get_handshake_headers(resource, hostname, port, options) + + header_str = "\r\n".join(headers) + send(sock, header_str) + dump("request header", header_str) + + status, resp = _get_resp_headers(sock) + if status in SUPPORTED_REDIRECT_STATUSES: + return handshake_response(status, resp, None) + success, subproto = _validate(resp, key, options.get("subprotocols")) + if not success: + raise WebSocketException("Invalid WebSocket Header") + + return handshake_response(status, resp, subproto) + +def _pack_hostname(hostname): + # IPv6 address + if ':' in hostname: + return '[' + hostname + ']' + + return hostname + +def _get_handshake_headers(resource, host, port, options): + headers = [ + "GET %s HTTP/1.1" % resource, + "Upgrade: websocket", + "Connection: Upgrade" + ] + if port == 80 or port == 443: + hostport = _pack_hostname(host) + else: + hostport = "%s:%d" % (_pack_hostname(host), port) + + if "host" in options and options["host"] is not None: + headers.append("Host: %s" % options["host"]) + else: + headers.append("Host: %s" % hostport) + + if "suppress_origin" not in options or not options["suppress_origin"]: + if "origin" in options and options["origin"] is not None: + headers.append("Origin: %s" % options["origin"]) + else: + headers.append("Origin: http://%s" % hostport) + + key = _create_sec_websocket_key() + + # Append Sec-WebSocket-Key & Sec-WebSocket-Version if not manually specified + if not 'header' in options or 'Sec-WebSocket-Key' not in options['header']: + key = _create_sec_websocket_key() + headers.append("Sec-WebSocket-Key: %s" % key) + else: + key = options['header']['Sec-WebSocket-Key'] + + if not 'header' in options or 'Sec-WebSocket-Version' not in options['header']: + headers.append("Sec-WebSocket-Version: %s" % VERSION) + + subprotocols = options.get("subprotocols") + if subprotocols: + headers.append("Sec-WebSocket-Protocol: %s" % ",".join(subprotocols)) + + if "header" in options: + header = options["header"] + if isinstance(header, dict): + header = [ + ": ".join([k, v]) + for k, v in header.items() + if v is not None + ] + headers.extend(header) + + server_cookie = CookieJar.get(host) + client_cookie = options.get("cookie", None) + + cookie = "; ".join(filter(None, [server_cookie, client_cookie])) + + if cookie: + headers.append("Cookie: %s" % cookie) + + headers.append("") + headers.append("") + + return headers, key + + +def _get_resp_headers(sock, success_statuses=(101, 301, 302, 303)): + status, resp_headers, status_message = read_headers(sock) + if status not in success_statuses: + raise WebSocketBadStatusException("Handshake status %d %s", status, status_message, resp_headers) + return status, resp_headers + +_HEADERS_TO_CHECK = { + "upgrade": "websocket", + "connection": "upgrade", +} + + +def _validate(headers, key, subprotocols): + subproto = None + for k, v in _HEADERS_TO_CHECK.items(): + r = headers.get(k, None) + if not r: + return False, None + r = r.lower() + if v != r: + return False, None + + if subprotocols: + subproto = headers.get("sec-websocket-protocol", None).lower() + if not subproto or subproto not in [s.lower() for s in subprotocols]: + error("Invalid subprotocol: " + str(subprotocols)) + return False, None + + result = headers.get("sec-websocket-accept", None) + if not result: + return False, None + result = result.lower() + + if isinstance(result, six.text_type): + result = result.encode('utf-8') + + value = (key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode('utf-8') + hashed = base64encode(hashlib.sha1(value).digest()).strip().lower() + success = compare_digest(hashed, result) + + if success: + return True, subproto + else: + return False, None + + +def _create_sec_websocket_key(): + randomness = os.urandom(16) + return base64encode(randomness).decode('utf-8').strip() diff --git a/sys.py/libs/websocket/_http.py b/sys.py/libs/websocket/_http.py new file mode 100644 index 0000000..5b9a26d --- /dev/null +++ b/sys.py/libs/websocket/_http.py @@ -0,0 +1,326 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import errno +import os +import socket +import sys + +import six + +from ._exceptions import * +from ._logging import * +from ._socket import* +from ._ssl_compat import * +from ._url import * + +if six.PY3: + from base64 import encodebytes as base64encode +else: + from base64 import encodestring as base64encode + +__all__ = ["proxy_info", "connect", "read_headers"] + +try: + import socks + ProxyConnectionError = socks.ProxyConnectionError + HAS_PYSOCKS = True +except: + class ProxyConnectionError(BaseException): + pass + HAS_PYSOCKS = False + +class proxy_info(object): + + def __init__(self, **options): + self.type = options.get("proxy_type") or "http" + if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']): + raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'") + self.host = options.get("http_proxy_host", None) + if self.host: + self.port = options.get("http_proxy_port", 0) + self.auth = options.get("http_proxy_auth", None) + self.no_proxy = options.get("http_no_proxy", None) + else: + self.port = 0 + self.auth = None + self.no_proxy = None + +def _open_proxied_socket(url, options, proxy): + hostname, port, resource, is_secure = parse_url(url) + + if not HAS_PYSOCKS: + raise WebSocketException("PySocks module not found.") + + ptype = socks.SOCKS5 + rdns = False + if proxy.type == "socks4": + ptype = socks.SOCKS4 + if proxy.type == "http": + ptype = socks.HTTP + if proxy.type[-1] == "h": + rdns = True + + sock = socks.create_connection( + (hostname, port), + proxy_type = ptype, + proxy_addr = proxy.host, + proxy_port = proxy.port, + proxy_rdns = rdns, + proxy_username = proxy.auth[0] if proxy.auth else None, + proxy_password = proxy.auth[1] if proxy.auth else None, + timeout = options.timeout, + socket_options = DEFAULT_SOCKET_OPTION + options.sockopt + ) + + if is_secure: + if HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + else: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + + +def connect(url, options, proxy, socket): + if proxy.host and not socket and not (proxy.type == 'http'): + return _open_proxied_socket(url, options, proxy) + + hostname, port, resource, is_secure = parse_url(url) + + if socket: + return socket, (hostname, port, resource) + + addrinfo_list, need_tunnel, auth = _get_addrinfo_list( + hostname, port, is_secure, proxy) + if not addrinfo_list: + raise WebSocketException( + "Host not found.: " + hostname + ":" + str(port)) + + sock = None + try: + sock = _open_socket(addrinfo_list, options.sockopt, options.timeout) + if need_tunnel: + sock = _tunnel(sock, hostname, port, auth) + + if is_secure: + if HAVE_SSL: + sock = _ssl_socket(sock, options.sslopt, hostname) + else: + raise WebSocketException("SSL not available.") + + return sock, (hostname, port, resource) + except: + if sock: + sock.close() + raise + + +def _get_addrinfo_list(hostname, port, is_secure, proxy): + phost, pport, pauth = get_proxy_info( + hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy) + try: + if not phost: + addrinfo_list = socket.getaddrinfo( + hostname, port, 0, 0, socket.SOL_TCP) + return addrinfo_list, False, None + else: + pport = pport and pport or 80 + # when running on windows 10, the getaddrinfo used above + # returns a socktype 0. This generates an error exception: + #_on_error: exception Socket type must be stream or datagram, not 0 + # Force the socket type to SOCK_STREAM + addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP) + return addrinfo_list, True, pauth + except socket.gaierror as e: + raise WebSocketAddressException(e) + + +def _open_socket(addrinfo_list, sockopt, timeout): + err = None + for addrinfo in addrinfo_list: + family, socktype, proto = addrinfo[:3] + sock = socket.socket(family, socktype, proto) + sock.settimeout(timeout) + for opts in DEFAULT_SOCKET_OPTION: + sock.setsockopt(*opts) + for opts in sockopt: + sock.setsockopt(*opts) + + address = addrinfo[4] + err = None + while not err: + try: + sock.connect(address) + except ProxyConnectionError as error: + err = WebSocketProxyException(str(error)) + err.remote_ip = str(address[0]) + continue + except socket.error as error: + error.remote_ip = str(address[0]) + try: + eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED) + except: + eConnRefused = (errno.ECONNREFUSED, ) + if error.errno == errno.EINTR: + continue + elif error.errno in eConnRefused: + err = error + continue + else: + raise error + else: + break + else: + continue + break + else: + if err: + raise err + + return sock + + +def _can_use_sni(): + return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2) + + +def _wrap_sni_socket(sock, sslopt, hostname, check_hostname): + context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23)) + + if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE: + cafile = sslopt.get('ca_certs', None) + capath = sslopt.get('ca_cert_path', None) + if cafile or capath: + context.load_verify_locations(cafile=cafile, capath=capath) + elif hasattr(context, 'load_default_certs'): + context.load_default_certs(ssl.Purpose.SERVER_AUTH) + if sslopt.get('certfile', None): + context.load_cert_chain( + sslopt['certfile'], + sslopt.get('keyfile', None), + sslopt.get('password', None), + ) + # see + # https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153 + context.verify_mode = sslopt['cert_reqs'] + if HAVE_CONTEXT_CHECK_HOSTNAME: + context.check_hostname = check_hostname + if 'ciphers' in sslopt: + context.set_ciphers(sslopt['ciphers']) + if 'cert_chain' in sslopt: + certfile, keyfile, password = sslopt['cert_chain'] + context.load_cert_chain(certfile, keyfile, password) + if 'ecdh_curve' in sslopt: + context.set_ecdh_curve(sslopt['ecdh_curve']) + + return context.wrap_socket( + sock, + do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True), + suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True), + server_hostname=hostname, + ) + + +def _ssl_socket(sock, user_sslopt, hostname): + sslopt = dict(cert_reqs=ssl.CERT_REQUIRED) + sslopt.update(user_sslopt) + + certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE') + if certPath and os.path.isfile(certPath) \ + and user_sslopt.get('ca_certs', None) is None \ + and user_sslopt.get('ca_cert', None) is None: + sslopt['ca_certs'] = certPath + elif certPath and os.path.isdir(certPath) \ + and user_sslopt.get('ca_cert_path', None) is None: + sslopt['ca_cert_path'] = certPath + + check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop( + 'check_hostname', True) + + if _can_use_sni(): + sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) + else: + sslopt.pop('check_hostname', True) + sock = ssl.wrap_socket(sock, **sslopt) + + if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname: + match_hostname(sock.getpeercert(), hostname) + + return sock + + +def _tunnel(sock, host, port, auth): + debug("Connecting proxy...") + connect_header = "CONNECT %s:%d HTTP/1.0\r\n" % (host, port) + # TODO: support digest auth. + if auth and auth[0]: + auth_str = auth[0] + if auth[1]: + auth_str += ":" + auth[1] + encoded_str = base64encode(auth_str.encode()).strip().decode() + connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str + connect_header += "\r\n" + dump("request header", connect_header) + + send(sock, connect_header) + + try: + status, resp_headers, status_message = read_headers(sock) + except Exception as e: + raise WebSocketProxyException(str(e)) + + if status != 200: + raise WebSocketProxyException( + "failed CONNECT via proxy status: %r" % status) + + return sock + + +def read_headers(sock): + status = None + status_message = None + headers = {} + trace("--- response header ---") + + while True: + line = recv_line(sock) + line = line.decode('utf-8').strip() + if not line: + break + trace(line) + if not status: + + status_info = line.split(" ", 2) + status = int(status_info[1]) + if len(status_info) > 2: + status_message = status_info[2] + else: + kv = line.split(":", 1) + if len(kv) == 2: + key, value = kv + headers[key.lower()] = value.strip() + else: + raise WebSocketException("Invalid header") + + trace("-----------------------") + + return status, headers, status_message diff --git a/sys.py/libs/websocket/_logging.py b/sys.py/libs/websocket/_logging.py new file mode 100644 index 0000000..70a6271 --- /dev/null +++ b/sys.py/libs/websocket/_logging.py @@ -0,0 +1,82 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import logging + +_logger = logging.getLogger('websocket') +try: + from logging import NullHandler +except ImportError: + class NullHandler(logging.Handler): + def emit(self, record): + pass + +_logger.addHandler(NullHandler()) + +_traceEnabled = False + +__all__ = ["enableTrace", "dump", "error", "warning", "debug", "trace", + "isEnabledForError", "isEnabledForDebug"] + + +def enableTrace(traceable, handler = logging.StreamHandler()): + """ + turn on/off the traceability. + + traceable: boolean value. if set True, traceability is enabled. + """ + global _traceEnabled + _traceEnabled = traceable + if traceable: + _logger.addHandler(handler) + _logger.setLevel(logging.DEBUG) + + +def dump(title, message): + if _traceEnabled: + _logger.debug("--- " + title + " ---") + _logger.debug(message) + _logger.debug("-----------------------") + + +def error(msg): + _logger.error(msg) + + +def warning(msg): + _logger.warning(msg) + + +def debug(msg): + _logger.debug(msg) + + +def trace(msg): + if _traceEnabled: + _logger.debug(msg) + + +def isEnabledForError(): + return _logger.isEnabledFor(logging.ERROR) + + +def isEnabledForDebug(): + return _logger.isEnabledFor(logging.DEBUG) diff --git a/sys.py/libs/websocket/_socket.py b/sys.py/libs/websocket/_socket.py new file mode 100644 index 0000000..7be3913 --- /dev/null +++ b/sys.py/libs/websocket/_socket.py @@ -0,0 +1,166 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import errno +import select +import socket + +import six +import sys + +from ._exceptions import * +from ._ssl_compat import * +from ._utils import * + +DEFAULT_SOCKET_OPTION = [(socket.SOL_TCP, socket.TCP_NODELAY, 1)] +if hasattr(socket, "SO_KEEPALIVE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)) +if hasattr(socket, "TCP_KEEPIDLE"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPIDLE, 30)) +if hasattr(socket, "TCP_KEEPINTVL"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)) +if hasattr(socket, "TCP_KEEPCNT"): + DEFAULT_SOCKET_OPTION.append((socket.SOL_TCP, socket.TCP_KEEPCNT, 3)) + +_default_timeout = None + +__all__ = ["DEFAULT_SOCKET_OPTION", "sock_opt", "setdefaulttimeout", "getdefaulttimeout", + "recv", "recv_line", "send"] + + +class sock_opt(object): + + def __init__(self, sockopt, sslopt): + if sockopt is None: + sockopt = [] + if sslopt is None: + sslopt = {} + self.sockopt = sockopt + self.sslopt = sslopt + self.timeout = None + + +def setdefaulttimeout(timeout): + """ + Set the global timeout setting to connect. + + timeout: default socket timeout time. This value is second. + """ + global _default_timeout + _default_timeout = timeout + + +def getdefaulttimeout(): + """ + Return the global timeout setting(second) to connect. + """ + return _default_timeout + + +def recv(sock, bufsize): + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + def _recv(): + try: + return sock.recv(bufsize) + except SSLWantReadError: + pass + except socket.error as exc: + error_code = extract_error_code(exc) + if error_code is None: + raise + if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: + raise + + r, w, e = select.select((sock, ), (), (), sock.gettimeout()) + if r: + return sock.recv(bufsize) + + try: + if sock.gettimeout() == 0: + bytes_ = sock.recv(bufsize) + else: + bytes_ = _recv() + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except SSLError as e: + message = extract_err_message(e) + if isinstance(message, str) and 'timed out' in message: + raise WebSocketTimeoutException(message) + else: + raise + + if not bytes_: + raise WebSocketConnectionClosedException( + "Connection is already closed.") + + return bytes_ + + +def recv_line(sock): + line = [] + while True: + c = recv(sock, 1) + line.append(c) + if c == six.b("\n"): + break + return six.b("").join(line) + + +def send(sock, data): + if isinstance(data, six.text_type): + data = data.encode('utf-8') + + if not sock: + raise WebSocketConnectionClosedException("socket is already closed.") + + def _send(): + try: + return sock.send(data) + except SSLWantWriteError: + pass + except socket.error as exc: + error_code = extract_error_code(exc) + if error_code is None: + raise + if error_code != errno.EAGAIN or error_code != errno.EWOULDBLOCK: + raise + + r, w, e = select.select((), (sock, ), (), sock.gettimeout()) + if w: + return sock.send(data) + + try: + if sock.gettimeout() == 0: + return sock.send(data) + else: + return _send() + except socket.timeout as e: + message = extract_err_message(e) + raise WebSocketTimeoutException(message) + except Exception as e: + message = extract_err_message(e) + if isinstance(message, str) and "timed out" in message: + raise WebSocketTimeoutException(message) + else: + raise diff --git a/sys.py/libs/websocket/_ssl_compat.py b/sys.py/libs/websocket/_ssl_compat.py new file mode 100644 index 0000000..5b3c413 --- /dev/null +++ b/sys.py/libs/websocket/_ssl_compat.py @@ -0,0 +1,52 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +__all__ = ["HAVE_SSL", "ssl", "SSLError", "SSLWantReadError", "SSLWantWriteError"] + +try: + import ssl + from ssl import SSLError + from ssl import SSLWantReadError + from ssl import SSLWantWriteError + if hasattr(ssl, 'SSLContext') and hasattr(ssl.SSLContext, 'check_hostname'): + HAVE_CONTEXT_CHECK_HOSTNAME = True + else: + HAVE_CONTEXT_CHECK_HOSTNAME = False + if hasattr(ssl, "match_hostname"): + from ssl import match_hostname + else: + from backports.ssl_match_hostname import match_hostname + __all__.append("match_hostname") + __all__.append("HAVE_CONTEXT_CHECK_HOSTNAME") + + HAVE_SSL = True +except ImportError: + # dummy class of SSLError for ssl none-support environment. + class SSLError(Exception): + pass + + class SSLWantReadError(Exception): + pass + + class SSLWantWriteError(Exception): + pass + + HAVE_SSL = False diff --git a/sys.py/libs/websocket/_url.py b/sys.py/libs/websocket/_url.py new file mode 100644 index 0000000..ae46d6c --- /dev/null +++ b/sys.py/libs/websocket/_url.py @@ -0,0 +1,163 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" + +import os +import socket +import struct + +from six.moves.urllib.parse import urlparse + + +__all__ = ["parse_url", "get_proxy_info"] + + +def parse_url(url): + """ + parse url and the result is tuple of + (hostname, port, resource path and the flag of secure mode) + + url: url string. + """ + if ":" not in url: + raise ValueError("url is invalid") + + scheme, url = url.split(":", 1) + + parsed = urlparse(url, scheme="ws") + if parsed.hostname: + hostname = parsed.hostname + else: + raise ValueError("hostname is invalid") + port = 0 + if parsed.port: + port = parsed.port + + is_secure = False + if scheme == "ws": + if not port: + port = 80 + elif scheme == "wss": + is_secure = True + if not port: + port = 443 + else: + raise ValueError("scheme %s is invalid" % scheme) + + if parsed.path: + resource = parsed.path + else: + resource = "/" + + if parsed.query: + resource += "?" + parsed.query + + return hostname, port, resource, is_secure + + +DEFAULT_NO_PROXY_HOST = ["localhost", "127.0.0.1"] + + +def _is_ip_address(addr): + try: + socket.inet_aton(addr) + except socket.error: + return False + else: + return True + + +def _is_subnet_address(hostname): + try: + addr, netmask = hostname.split("/") + return _is_ip_address(addr) and 0 <= int(netmask) < 32 + except ValueError: + return False + + +def _is_address_in_network(ip, net): + ipaddr = struct.unpack('I', socket.inet_aton(ip))[0] + netaddr, bits = net.split('/') + netmask = struct.unpack('I', socket.inet_aton(netaddr))[0] & ((2 << int(bits) - 1) - 1) + return ipaddr & netmask == netmask + + +def _is_no_proxy_host(hostname, no_proxy): + if not no_proxy: + v = os.environ.get("no_proxy", "").replace(" ", "") + no_proxy = v.split(",") + if not no_proxy: + no_proxy = DEFAULT_NO_PROXY_HOST + + if hostname in no_proxy: + return True + elif _is_ip_address(hostname): + return any([_is_address_in_network(hostname, subnet) for subnet in no_proxy if _is_subnet_address(subnet)]) + + return False + + +def get_proxy_info( + hostname, is_secure, proxy_host=None, proxy_port=0, proxy_auth=None, + no_proxy=None, proxy_type='http'): + """ + try to retrieve proxy host and port from environment + if not provided in options. + result is (proxy_host, proxy_port, proxy_auth). + proxy_auth is tuple of username and password + of proxy authentication information. + + hostname: websocket server name. + + is_secure: is the connection secure? (wss) + looks for "https_proxy" in env + before falling back to "http_proxy" + + options: "http_proxy_host" - http proxy host name. + "http_proxy_port" - http proxy port. + "http_no_proxy" - host names, which doesn't use proxy. + "http_proxy_auth" - http proxy auth information. + tuple of username and password. + default is None + "proxy_type" - if set to "socks5" PySocks wrapper + will be used in place of a http proxy. + default is "http" + """ + if _is_no_proxy_host(hostname, no_proxy): + return None, 0, None + + if proxy_host: + port = proxy_port + auth = proxy_auth + return proxy_host, port, auth + + env_keys = ["http_proxy"] + if is_secure: + env_keys.insert(0, "https_proxy") + + for key in env_keys: + value = os.environ.get(key, None) + if value: + proxy = urlparse(value) + auth = (proxy.username, proxy.password) if proxy.username else None + return proxy.hostname, proxy.port, auth + + return None, 0, None diff --git a/sys.py/libs/websocket/_utils.py b/sys.py/libs/websocket/_utils.py new file mode 100644 index 0000000..8eddabf --- /dev/null +++ b/sys.py/libs/websocket/_utils.py @@ -0,0 +1,110 @@ +""" +websocket - WebSocket client library for Python + +Copyright (C) 2010 Hiroki Ohtani(liris) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1335 USA + +""" +import six + +__all__ = ["NoLock", "validate_utf8", "extract_err_message", "extract_error_code"] + + +class NoLock(object): + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + pass + +try: + # If wsaccel is available we use compiled routines to validate UTF-8 + # strings. + from wsaccel.utf8validator import Utf8Validator + + def _validate_utf8(utfbytes): + return Utf8Validator().validate(utfbytes)[0] + +except ImportError: + # UTF-8 validator + # python implementation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + + _UTF8_ACCEPT = 0 + _UTF8_REJECT = 12 + + _UTF8D = [ + # The first part of the table maps bytes to character classes that + # to reduce the size of the transition table and create bitmasks. + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8, + + # The second part is a transition table that maps a combination + # of a state of the automaton and a character class to a state. + 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12, + 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12, + 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12, + 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12, + 12,36,12,12,12,12,12,12,12,12,12,12, ] + + def _decode(state, codep, ch): + tp = _UTF8D[ch] + + codep = (ch & 0x3f) | (codep << 6) if ( + state != _UTF8_ACCEPT) else (0xff >> tp) & ch + state = _UTF8D[256 + state + tp] + + return state, codep + + def _validate_utf8(utfbytes): + state = _UTF8_ACCEPT + codep = 0 + for i in utfbytes: + if six.PY2: + i = ord(i) + state, codep = _decode(state, codep, i) + if state == _UTF8_REJECT: + return False + + return True + + +def validate_utf8(utfbytes): + """ + validate utf8 byte string. + utfbytes: utf byte string to check. + return value: if valid utf8 string, return true. Otherwise, return false. + """ + return _validate_utf8(utfbytes) + + +def extract_err_message(exception): + if exception.args: + return exception.args[0] + else: + return None + + +def extract_error_code(exception): + if exception.args and len(exception.args) > 1: + return exception.args[0] if isinstance(exception.args[0], int) else None diff --git a/sys.py/pyaria2_rpc b/sys.py/pyaria2_rpc new file mode 160000 index 0000000..b554bce --- /dev/null +++ b/sys.py/pyaria2_rpc @@ -0,0 +1 @@ +Subproject commit b554bce52720629420238c3813f72a4c6554917a diff --git a/sys.py/run.py b/sys.py/run.py index ef66844..c1470e5 100644 --- a/sys.py/run.py +++ b/sys.py/run.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import os import dbus import dbus.service import sys @@ -7,7 +7,8 @@ import commands import logging import errno -from wicd import misc +from wicd import misc +import libs.websocket as websocket ##misc.to_bool ##misc.misc.noneToString ##misc.to_unicode @@ -22,7 +23,7 @@ import gobject import socket import pygame from sys import exit -import os +import json #from beeprint import pp ######## @@ -163,11 +164,9 @@ def RestoreLastBackLightBrightness(main_screen): f.seek(0) f.write(str( last_brt )) f.truncate() - f.close() - last_brt = -1 - else: - f.close() + f.close() + last_brt = -1 try: f = open("/proc/driver/led1","w") except IOError: @@ -198,7 +197,6 @@ def InspectionTeam(main_screen): if cur_time - everytime_keydown > time_1 and passout_time_stage == 0: print("timeout, dim screen %d" % int(cur_time - everytime_keydown)) - try: f = open(config.BackLight,"r+") except IOError: @@ -226,7 +224,6 @@ def InspectionTeam(main_screen): elif cur_time - everytime_keydown > time_2 and passout_time_stage == 1: print("timeout, close screen %d" % int(cur_time - everytime_keydown)) - try: f = open(config.BackLight,"r+") except IOError: @@ -482,6 +479,43 @@ def gobject_pygame_event_timer(main_screen): return True +@misc.threaded +def aria2_ws(main_screen): + def on_message(ws, message): + print("run.py aria2_ws on_message: ",message) + try: + aria2_noti = json.loads(message) + if "method" in aria2_noti and aria2_noti["method"] == "aria2.onDownloadError": + gid = aria2_noti["params"][0]["gid"] + + if "method" in aria2_noti and aria2_noti["method"] == "aria2.onDownloadComplete": + gid = aria2_noti["params"][0]["gid"] + on_comp_cb = getattr(main_screen._CurrentPage,"OnAria2CompleteCb",None) + if on_comp_cb != None: + if callable( on_comp_cb ): + main_screen._CurrentPage.OnAria2CompleteCb(gid) + #game_install_thread(gid) + except Exception as ex: + print(ex) + + def on_error(ws, error): + print(error) + + def on_close(ws): + print("### closed ###") + + + #websocket.enableTrace(True) + try: + ws = websocket.WebSocketApp("ws://localhost:6800/jsonrpc", + on_message = on_message, + on_error = on_error, + on_close = on_close) +# ws.on_open = on_open + ws.run_forever() + except: + return + @misc.threaded def socket_thread(main_screen): @@ -540,8 +574,17 @@ def socket_thread(main_screen): api_cb = getattr(i._CmdPath,"API",None) if api_cb != None: if callable(api_cb): - i._CmdPath.API(main_screen) - + i._CmdPath.API(main_screen) + + if tokens[0].lower() == "redraw": #echo "redraw titlebar" | socat - UNIX-CONNECT:/tmp/gameshell + if len(tokens) > 1: + area = tokens[1].lower() + if area == "titlebar": + if hasattr(main_screen._TitleBar,'Redraw'): + if main_screen._TitleBar.Redraw != None and callable(main_screen._TitleBar.Redraw): + main_screen._TitleBar.Redraw() + + def big_loop(): global sound_patch,gobject_flash_led1 @@ -583,6 +626,7 @@ def big_loop(): socket_thread(main_screen) + aria2_ws(main_screen) gobject_loop()