2016-02-25 05:49:18 -06:00
from logging import getLogger
logger = getLogger ( ' update ' )
2019-01-07 15:35:28 -06:00
import sys
2016-02-25 05:49:18 -06:00
import contextlib
import io
import os
import platform
import requests
import tempfile
import widgetUtils
import webbrowser
try :
2021-09-22 09:17:12 -05:00
import czipfile as zipfile
2016-02-25 05:49:18 -06:00
except ImportError :
2021-09-22 09:17:12 -05:00
import zipfile
2016-02-25 05:49:18 -06:00
from platform_utils import paths
2018-12-26 12:27:44 -06:00
def perform_update ( endpoint , current_version , update_type = " stable " , app_name = ' ' , password = None , update_available_callback = None , progress_callback = None , update_complete_callback = None ) :
2021-09-22 09:17:12 -05:00
requests_session = create_requests_session ( app_name = app_name , version = current_version )
available_update = find_update ( endpoint , requests_session = requests_session )
if not available_update :
logger . debug ( " No update available " )
return False
available_version , available_description , update_url = find_version_data ( update_type , current_version , available_update )
if available_version == False :
return False
logger . info ( " A new update is available. Version %s " % available_version )
if callable ( update_available_callback ) and not update_available_callback ( version = available_version , description = available_description ) : #update_available_callback should return a falsy value to stop the process
logger . info ( " User canceled update. " )
return
base_path = tempfile . mkdtemp ( )
download_path = os . path . join ( base_path , ' update.zip ' )
update_path = os . path . join ( base_path , ' update ' )
downloaded = download_update ( update_url , download_path , requests_session = requests_session , progress_callback = progress_callback )
extracted = extract_update ( downloaded , update_path , password = password )
bootstrap_path = move_bootstrap ( extracted )
execute_bootstrap ( bootstrap_path , extracted )
logger . info ( " Update prepared for installation. " )
if callable ( update_complete_callback ) :
update_complete_callback ( )
2016-02-25 05:49:18 -06:00
def create_requests_session ( app_name = None , version = None ) :
2021-09-22 09:17:12 -05:00
user_agent = ' '
session = requests . session ( )
if app_name :
user_agent = ' %s / %r ' % ( app_name , version )
session . headers [ ' User-Agent ' ] = session . headers [ ' User-Agent ' ] + user_agent
return session
2016-02-25 05:49:18 -06:00
def find_update ( endpoint , requests_session ) :
2021-09-22 09:17:12 -05:00
response = requests_session . get ( endpoint )
response . raise_for_status ( )
content = response . json ( )
return content
2016-02-25 05:49:18 -06:00
2018-12-26 12:27:44 -06:00
def find_version_data ( update_type , current_version , available_update ) :
2021-09-22 09:17:12 -05:00
if update_type == " stable " :
available_version = float ( available_update [ ' current_version ' ] )
if not float ( available_version ) > float ( current_version ) or platform . system ( ) + platform . architecture ( ) [ 0 ] [ : 2 ] not in available_update [ ' downloads ' ] :
logger . debug ( " No update for this architecture " )
return ( False , False , False )
available_description = available_update . get ( ' description ' , None )
update_url = available_update [ ' downloads ' ] [ platform . system ( ) + platform . architecture ( ) [ 0 ] [ : 2 ] ]
return ( available_version , available_description , update_url )
else : # Unstable versions, based in commits instead of version numbers.
available_version = available_update [ " current_version " ]
if available_version == current_version :
return ( False , False , False )
available_description = available_update [ " description " ]
update_url = available_update [ ' downloads ' ] [ platform . system ( ) + platform . architecture ( ) [ 0 ] [ : 2 ] ]
return ( available_version , available_description , update_url )
2018-12-26 12:27:44 -06:00
2016-02-25 05:49:18 -06:00
def download_update ( update_url , update_destination , requests_session , progress_callback = None , chunk_size = io . DEFAULT_BUFFER_SIZE ) :
2021-09-22 09:17:12 -05:00
total_downloaded = total_size = 0
with io . open ( update_destination , ' w+b ' ) as outfile :
download = requests_session . get ( update_url , stream = True )
total_size = int ( download . headers . get ( ' content-length ' , 0 ) )
logger . debug ( " Total update size: %d " % total_size )
download . raise_for_status ( )
for chunk in download . iter_content ( chunk_size ) :
outfile . write ( chunk )
total_downloaded + = len ( chunk )
if callable ( progress_callback ) :
call_callback ( progress_callback , total_downloaded , total_size )
logger . debug ( " Update downloaded " )
return update_destination
2016-02-25 05:49:18 -06:00
def extract_update ( update_archive , destination , password = None ) :
2021-09-22 09:17:12 -05:00
""" Given an update archive, extracts it. Returns the directory to which it has been extracted """
with contextlib . closing ( zipfile . ZipFile ( update_archive ) ) as archive :
if password :
archive . setpassword ( password )
archive . extractall ( path = destination )
logger . debug ( " Update extracted " )
return destination
2016-02-25 05:49:18 -06:00
def move_bootstrap ( extracted_path ) :
2021-09-22 09:17:12 -05:00
working_path = os . path . abspath ( os . path . join ( extracted_path , ' .. ' ) )
if platform . system ( ) == ' Darwin ' :
extracted_path = os . path . join ( extracted_path , ' Contents ' , ' Resources ' )
downloaded_bootstrap = os . path . join ( extracted_path , bootstrap_name ( ) )
new_bootstrap_path = os . path . join ( working_path , bootstrap_name ( ) )
os . rename ( downloaded_bootstrap , new_bootstrap_path )
return new_bootstrap_path
2016-02-25 05:49:18 -06:00
def execute_bootstrap ( bootstrap_path , source_path ) :
2021-09-22 09:17:12 -05:00
arguments = r ' " %s " " %s " " %s " " %s " ' % ( os . getpid ( ) , source_path , paths . app_path ( ) , paths . get_executable ( ) )
if platform . system ( ) == ' Windows ' :
import win32api
win32api . ShellExecute ( 0 , ' open ' , bootstrap_path , arguments , ' ' , 5 )
else :
import subprocess
make_executable ( bootstrap_path )
subprocess . Popen ( [ ' %s %s ' % ( bootstrap_path , arguments ) ] , shell = True )
logger . info ( " Bootstrap executed " )
2016-02-25 05:49:18 -06:00
def bootstrap_name ( ) :
2021-09-22 09:17:12 -05:00
if platform . system ( ) == ' Windows ' : return ' bootstrap.exe '
if platform . system ( ) == ' Darwin ' : return ' bootstrap-mac.sh '
return ' bootstrap-lin.sh '
2016-02-25 05:49:18 -06:00
def make_executable ( path ) :
2021-09-22 09:17:12 -05:00
import stat
st = os . stat ( path )
os . chmod ( path , st . st_mode | stat . S_IEXEC )
2016-02-25 05:49:18 -06:00
def call_callback ( callback , * args , * * kwargs ) :
# try:
2021-09-22 09:17:12 -05:00
callback ( * args , * * kwargs )
2016-02-25 05:49:18 -06:00
# except:
# logger.exception("Failed calling callback %r with args %r and kwargs %r" % (callback, args, kwargs))