def test_extract_no_file(self, client): update = client.update_check(client.app_name, '0.0.1') assert update is not None assert update.download() is True with ChDir(update.update_folder): files = os.listdir(os.getcwd()) for f in files: remove_any(f) if get_system() != 'win': assert update.extract() is False
def main(use_custom_dir, port, windowed): scripts = [('app_restart_onedir.py', '4.1', '--windowed' if windowed else ''), ('app_restart_02.py', '4.2', '--windowed' if windowed else '')] # We use this flag to untar & move our binary to the # current working directory first = True # patch config_file for custom port number config_file = open('client_config.py', 'rt').read() config_file = re.sub( 'localhost:\d+', 'localhost:%s' % port, config_file) # patch config_file for use_custom_dir if use_custom_dir: config_file += '\n USE_CUSTOM_DIR = True\n' open('client_config.py', 'wt').write(config_file) for s in scripts: build(s) if first: if sys.platform == 'win32': ext = '.zip' else: ext = '.tar.gz' # Build path to archive archive_path = os.path.join('pyu-data', 'new', 'Acme-{}-4.1{}'.format(get_system(), ext)) if not os.path.exists(archive_path): print("Archive did not build!") sys.exit(1) # We extract the Acme binary here. When we call pyupdater pkg -P # the Acme binary will be moved to the deploy folder. In our test # (test_pyupdater.TestExecution.test_execution_update_*) we # move all of the files from the deploy directory to the cwd # of the test runner. extract(archive_path) first = False os.system(cmd1) os.system(cmd2)
def _restart(self): log.debug('Restarting') current_app = os.path.join(self._current_app_dir, self.name) if get_system() == 'mac': # Must be dealing with Mac .app application if not os.path.exists(current_app): log.debug('Must be a .app bundle') current_app += '.app' mac_app_binary_dir = os.path.join(current_app, 'Contents', 'MacOS') _file = os.listdir(mac_app_binary_dir) # We are making an assumption here that only 1 # executable will be in the MacOS folder. current_app = os.path.join(mac_app_binary_dir, sys.executable) r = Restarter(current_app, name=self.name) r.process()
def extract(self): """Will extract the update from its archive to the update folder. If updating a lib you can take over from there. If updating an app this call should be followed by method "restart" to complete update. ######Returns: (bool) True - Extract successful. False - Extract failed. """ if get_system() == 'win': # Tested elsewhere log.debug('Only supported on Unix like systems') return False try: self._extract_update() except ClientError as err: log.debug(err, exc_info=True) return False return True
def main(use_custom_dir, port, windowed): scripts = [('app_restart_01.py', '4.1', '--windowed' if windowed else ''), ('app_restart_02.py', '4.2', '--windowed' if windowed else '')] # We use this flag to untar & move our binary to the # current working directory first = True # patch config_file for custom port number config_file = open('client_config.py', 'rt').read() config_file = re.sub('localhost:\d+', 'localhost:%s' % port, config_file) # patch config_file for use_custom_dir if use_custom_dir: config_file += '\n USE_CUSTOM_DIR = True\n' open('client_config.py', 'wt').write(config_file) for s in scripts: build(s) if first: if sys.platform == 'win32': ext = '.zip' else: ext = '.tar.gz' # Build path to archive archive_path = os.path.join( 'pyu-data', 'new', 'Acme-{}-4.1{}'.format(get_system(), ext)) if not os.path.exists(archive_path): print("Archive did not build!") sys.exit(1) # We extract the Acme binary here. When we call pyupdater pkg -P # the Acme binary will be moved to the deploy folder. In our test # (test_pyupdater.TestExecution.test_execution_update_*) we # move all of the files from the deploy directory to the cwd # of the test runner. extract(archive_path) first = False os.system(cmd1) os.system(cmd2)
def build(self): start = time.time() temp_name = get_system() # Check for spec file or python script self._setup() spec_file_path = os.path.join(self.spec_dir, temp_name + '.spec') # Spec file used instead of python script if self.app_info['type'] == 'spec': spec_file_path = self.app_info['name'] else: # Creating spec file from script self._make_spec(self.pyi_args, temp_name, self.app_info) # Build executable self._build(self.args, spec_file_path) # Archive executable self._archive(self.args, temp_name) finished = time.time() - start msg = 'Build finished in {:.2f} seconds.'.format(finished) log.info(msg)
def _overwrite(self): # Unix: Overwrites the running applications binary if get_system() == 'mac': if self._current_app_dir.endswith('MacOS') is True: log.debug('Looks like we\'re dealing with a Mac Gui') temp_dir = get_mac_dot_app_dir(self._current_app_dir) self._current_app_dir = temp_dir app_update = os.path.join(self.update_folder, self.name) # Must be dealing with Mac .app application if not os.path.exists(app_update) and sys.platform == "darwin": app_update += '.app' log.debug('Update Location:\n%s', os.path.dirname(app_update)) log.debug('Update Name: %s', os.path.basename(app_update)) current_app = os.path.join(self._current_app_dir, self.name) # Must be dealing with Mac .app application if not os.path.exists(current_app): current_app += '.app' log.debug('Current App location:\n\n%s', current_app) # Remove current app to prevent errors when moving # update to new location # if update_app is a directory, then we are updating a directory if os.path.isdir(app_update): if (os.path.isdir(current_app)): shutil.rmtree(current_app) else: shutil.rmtree(os.path.dirname(current_app)) if os.path.exists(current_app): remove_any(current_app) log.debug('Moving app to new location:\n\n%s', self._current_app_dir) shutil.move(app_update, self._current_app_dir)
def build(self): start = time.time() # Check for spec file or python script self._setup() temp_name = get_system() spec_file_path = os.path.join(self.spec_dir, temp_name + ".spec") # Spec file used instead of python script if self.app_info["type"] == "spec": spec_file_path = self.app_info["name"] else: # Creating spec file from script self._make_spec(temp_name) # Build executable self._build(spec_file_path) # Archive executable self._archive(temp_name) finished = time.time() - start log.info("Build finished in {:.2f} seconds.".format(finished))
def extract(self): """Will extract the update from its archive to the update folder. If updating a lib you can take over from there. If updating an app this call should be followed by method "restart" to complete update. Returns: (bool) Meanings: True - Extract successful False - Extract failed """ if get_system() == 'win': # Tested elsewhere log.debug('Only supported on Unix like systems') return False try: self._extract_update() except ClientError as err: log.debug(err, exc_info=True) return False return True
def test_archive(self): with io.open('test', 'w', encoding='utf-8') as f: f.write('this is a test') ex = ExternalLib('test', '0.1') ex.archive() assert os.path.exists('test-{}-0.1{}'.format(get_system(), EXT))
def make_spec(self): temp_name = get_system() self._make_spec(temp_name, spec_only=True)
def test_extract(self, client): update = client.update_check(client.app_name, '0.0.1') assert update is not None assert update.download() is True if get_system() != 'win': assert update.extract() is True
import os import tempfile import bsdiff4 from dsdev_utils.crypto import get_package_hashes from dsdev_utils.helpers import EasyAccessDict, Version from dsdev_utils.paths import ChDir, remove_any from dsdev_utils.system import get_system from pyupdater.client.downloader import FileDownloader from pyupdater import settings from pyupdater.utils.exceptions import PatcherError log = logging.getLogger(__name__) _PLATFORM = get_system() class Patcher(object): """Downloads, verifies, and patches binaries Kwargs: name (str): Name of binary to patch json_data (dict): Info dict with all package meta data current_version (str): Version number of currently installed binary latest_version (str): Newest version available
def test_archive(self): with io.open("test", "w", encoding="utf-8") as f: f.write("this is a test") ex = ExternalLib("test", "0.1") ex.archive() assert os.path.exists("test-{}-0.1{}".format(get_system(), EXT))
def test_freeze_update_available(self): """ Test ability to freeze and run app with an update available. """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # PyUpdater uses PyInstaller under the hood. We will customize # the command-line arguments PyUpdater sends to PyInstaller. # The SocketServer module (used by werkzeug, which is used by Flask) # doesn't seem to get detected automatically by PyInstaller (observed # on Windows), so we add this as a hidden import. pyiArgs = ['--hidden-import=SocketServer', 'run.py'] if get_system() == 'mac': # On Mac, we need to use PyInstaller's --windowed option to create # an app bundle, otherwise attempting to run the frozen application # gives this error: # # This program needs access to the screen. # Please run with a Framework build of python, and only when # you are logged in on the main display of your Mac. # # On other platforms, we will build a console application for the # purposes of testing, so that we can easily interact with its # STDOUT and STDERR: pyiArgs = ['--windowed'] + pyiArgs else: pyiArgs = ['--console'] + pyiArgs wxupdatedemo.__version__ = CURRENT_VERSION args = Namespace(app_version=CURRENT_VERSION, clean=False, command='build', distpath=None, keep=False, name=None, onedir=False, onefile=False, specpath=None, workpath=None) builder = Builder(args, pyiArgs) builder.build() if get_system() == 'win': ext = '.zip' else: ext = '.tar.gz' buildFilename = \ '%s-%s-%s%s' % (APP_NAME, get_system(), CURRENT_VERSION, ext) newDir = os.path.join(settings.USER_DATA_FOLDER, 'new') self.assertEqual(os.listdir(newDir), [buildFilename]) os.chdir(newDir) if get_system() == 'win': with zipfile.ZipFile(buildFilename, 'r') as zipFile: zipFile.extractall() pathToExe = '%s.exe' % APP_NAME self.assertEqual(sorted(os.listdir('.')), [buildFilename, pathToExe]) elif get_system() == 'mac': tar = tarfile.open(buildFilename, "r:gz") tar.extractall() tar.close() appBundleName = '%s.app' % APP_NAME self.assertEqual(sorted(os.listdir('.')), [buildFilename, appBundleName]) pathToExe = os.path.join(newDir, '%s.app' % APP_NAME, 'Contents', 'MacOS', APP_NAME) else: # Linux / Unix tar = tarfile.open(buildFilename, "r:gz") tar.extractall() tar.close() pathToExe = os.path.join(newDir, APP_NAME) sys.stderr.write("\n\nTesting ability to apply patch update...\n") cmdList = [pathToExe, '--debug'] runExeProc = subprocess.Popen(cmdList, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ.copy()) runExeStdout, _ = runExeProc.communicate() logger.debug(runExeStdout) self.assertEqual(runExeProc.returncode, 0) appliedPatchSuccessfully = False statusPrefix = "Exiting with status: " for line in runExeStdout.splitlines(): if "Applied patch successfully" in line: sys.stderr.write("\t%s\n" % line) appliedPatchSuccessfully = True if line.startswith("Exiting with status: "): sys.stderr.write("\t%s\n" % line) status = line.split(statusPrefix)[1] self.assertEqual(status, "Extracting update and restarting.") self.assertTrue(appliedPatchSuccessfully) # Remove all local data from previous PyUpdater downloads: if os.path.exists(appdirs.user_data_dir(APP_NAME, COMPANY_NAME)): shutil.rmtree(appdirs.user_data_dir(APP_NAME, COMPANY_NAME)) # Now we can't patch because there's no base binary to patch from: sys.stderr.write("\nTesting ability to download full update...\n") runExeProc = subprocess.Popen(cmdList, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ.copy()) runExeStdout, _ = runExeProc.communicate() logger.debug(runExeStdout) self.assertEqual(runExeProc.returncode, 0) fullDownloadSuccessful = False statusPrefix = "Exiting with status: " for line in runExeStdout.splitlines(): if "Full download successful" in line: sys.stderr.write("\t%s\n" % line) fullDownloadSuccessful = True if line.startswith("Exiting with status: "): sys.stderr.write("\t%s\n" % line) status = line.split(statusPrefix)[1] self.assertEqual(status, "Extracting update and restarting.") self.assertTrue(fullDownloadSuccessful) # Remove all local data from previous PyUpdater downloads: if os.path.exists(appdirs.user_data_dir(APP_NAME, COMPANY_NAME)): shutil.rmtree(appdirs.user_data_dir(APP_NAME, COMPANY_NAME)) # Remove update archive from file server: os.remove(os.path.join(self.fileServerDir, self.updateFilename)) # Now attempting to update should fail - can't download update. sys.stderr.write( "\nTesting ability to report failed download of update...\n") runExeProc = subprocess.Popen(cmdList, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=os.environ.copy()) runExeStdout, _ = runExeProc.communicate() logger.debug(runExeStdout) self.assertEqual(runExeProc.returncode, 0) statusPrefix = "Exiting with status: " fullDownloadFailed = False for line in runExeStdout.splitlines(): if "Full download failed" in line: sys.stderr.write("\t%s\n" % line) fullDownloadFailed = True if line.startswith("Exiting with status: "): sys.stderr.write("\t%s\n" % line) status = line.split(statusPrefix)[1] self.assertEqual(status, "Update download failed.") self.assertTrue(fullDownloadFailed)
def make_spec(self): temp_name = get_system() self._make_spec(self.pyi_args, temp_name, self.app_info, spec_only=True)
import os import pytest from dsdev_utils.system import get_system from pyupdater import PyUpdater from pyupdater.utils.config import Config from tconfig import TConfig def create_build_cmd(version): cmd = ['build', '--app-name', 'myapp', '--app-version', '0.1.{}'.format(version), 'app.py', '-F'] return cmd if get_system() == 'win': ext = '.zip' else: ext = '.tar.gz' @pytest.mark.usefixtures('cleandir', 'create_keypack', 'pyu') class TestUtils(object): def test_setup(self): data_dir = os.getcwd() pyu_data_dir = os.path.join(data_dir, 'pyu-data') t_config = TConfig() t_config.DATA_DIR = data_dir pyu = PyUpdater(t_config) pyu.setup()
def __init__(self, data): self._is_win = get_system() == 'win' super(AppUpdate, self).__init__(data)
def gen_archive_name(version): archive_name = 'myapp-{}-0.1.{}{}'.format(get_system(), version, ext) return archive_name
__version__ = "0.0.3" __app__ = "PyQt-Updater" __author__ = "liuzixuan" __develop__ = True __asset_update_log__ = "updatelog" __download_folder_win__ = "/Users/chihuen/PycharmProjects/PyQt-Updater/download" __download_folder_mac__ = "/Users/chihuen/PycharmProjects/PyQt-Updater/download" __download_folder__ = '' __develop_server_host__ = '127.0.0.1' __develop_update_url__ = ['http://127.0.0.1:2664/Update/'] __develop_server_root__ = 'root' __develop_server_password__ = '' __release_server_host__ = '127.0.0.1' __release_update_url__ = ['http://127.0.0.1:2664/Update/'] __release_server_root__ = 'root' __release_server_password__ = '' from dsdev_utils.system import get_system if get_system() == 'win': __download_folder__ = __download_folder_win__ else: __download_folder__ = __download_folder_mac__
CONFIG_DB_KEY_APP_CONFIG = "app_config" CONFIG_DB_KEY_KEYPACK = "keypack" CONFIG_DB_KEY_VERSION_META = "version_meta" CONFIG_DB_KEY_PY_REPO_CONFIG = "py_repo_config" DEFAULT_CLIENT_CONFIG = ["client_config.py"] GENERIC_APP_NAME = "PyUpdater App" GENERIC_COMPANY_NAME = "PyUpdater" # Log filename LOG_FILENAME_DEBUG = "pyu-debug.log" # KeyFile KEYPACK_FILENAME = "keypack.pyu" # Main user visible data folder USER_DATA_FOLDER = "pyu-data" # Key in version file where value are update meta data UPDATES_KEY = "updates" # Folder on client system where updates are stored UPDATE_FOLDER = "update" # Name of version file in online repo VERSION_FILE_FILENAME = "versions-{}.gz".format(system.get_system()) VERSION_FILE_FILENAME_COMPAT = "versions.gz" KEY_FILE_FILENAME = "keys.gz"
def make_archive(name, target, version, archive_format): """Used to make archives of file or dir. Zip on windows and tar.gz on all other platforms Args: name - Name to rename binary. version - Version of app. Used to create archive filename target - Name of file to archive. Returns: (str) - name of archive """ log.debug('starting archive') ext = os.path.splitext(target)[1] temp_file = name + ext log.debug('Temp file: %s', temp_file) # Remove file if it exists. Found during testing... if os.path.exists(temp_file): paths.remove_any(temp_file) if os.path.isfile(target): shutil.copy(target, temp_file) else: shutil.copytree(target, temp_file, symlinks=True) # renames the entry-point executable file_ext = '.exe' if system.get_system() == 'win' else '' src_executable = temp_file + os.sep + target + file_ext dst_executable = temp_file + os.sep + name + file_ext # is an osx bundle app so does not need to fix the executable name if ext != '.app': shutil.move(src_executable, dst_executable) # is a win folder so the manifest need to be renamed too if system.get_system() == 'win': src_manifest = src_executable + '.manifest' dst_manifest = dst_executable + '.manifest' shutil.move(src_manifest, dst_manifest) file_dir = os.path.dirname(os.path.abspath(target)) filename = '{}-{}-{}'.format( os.path.splitext(name)[0], system.get_system(), version) # Only use zip on windows. # Zip does not preserve file permissions on nix & mac # tar.gz creates full file path with paths.ChDir(file_dir): ext = 'gztar' if archive_format == 'default': if system.get_system() == 'win': ext = 'zip' else: ext = archive_format output_filename = shutil.make_archive(filename, ext, file_dir, temp_file) if os.path.exists(temp_file): paths.remove_any(temp_file) log.debug('Archive output filename: %s', output_filename) return output_filename
def setUp(self): # pylint: disable=too-many-statements # pylint: disable=too-many-locals self.initialWorkingDir = os.getcwd() userDataDir = appdirs.user_data_dir(APP_NAME, COMPANY_NAME) if os.path.exists(userDataDir): shutil.rmtree(userDataDir) os.makedirs(userDataDir) versionsUserDataFilePath = os.path.join(userDataDir, 'versions.gz') userDataUpdateDir = os.path.join(userDataDir, "update") os.mkdir(userDataUpdateDir) system = get_system() self.currentFilename = \ VERSIONS['updates'][APP_NAME][CURRENT_VERSION_PYU_FORMAT]\ [system]['filename'] currentFilePath = os.path.join(userDataUpdateDir, self.currentFilename) with open(currentFilePath, "wb") as currentFile: currentFile.write("%s" % CURRENT_VERSION) currentFile.seek(FILE_SIZE - 1) currentFile.write("\0") fileHash = get_package_hashes(currentFilePath) VERSIONS['updates'][APP_NAME][CURRENT_VERSION_PYU_FORMAT]\ [system]['file_hash'] = fileHash tempFile = tempfile.NamedTemporaryFile() self.fileServerDir = tempFile.name tempFile.close() os.mkdir(self.fileServerDir) os.chdir(self.fileServerDir) self.updateFilename = \ VERSIONS['updates'][APP_NAME][UPDATE_VERSION_PYU_FORMAT]\ [system]['filename'] with open(self.updateFilename, "wb") as updateFile: updateFile.write("%s" % UPDATE_VERSION) updateFile.seek(FILE_SIZE - 1) updateFile.write("\0") os.chdir(self.fileServerDir) fileHash = get_package_hashes(self.updateFilename) VERSIONS['updates'][APP_NAME][UPDATE_VERSION_PYU_FORMAT]\ [system]['file_hash'] = fileHash self.patchFilename = \ VERSIONS['updates'][APP_NAME][UPDATE_VERSION_PYU_FORMAT]\ [system]['patch_name'] bsdiff4.file_diff(currentFilePath, self.updateFilename, self.patchFilename) os.chdir(self.fileServerDir) fileHash = get_package_hashes(self.patchFilename) VERSIONS['updates'][APP_NAME][UPDATE_VERSION_PYU_FORMAT]\ [system]['patch_hash'] = fileHash os.chdir(self.initialWorkingDir) privateKey = ed25519.SigningKey(PRIVATE_KEY.encode('utf-8'), encoding='base64') signature = privateKey.sign(six.b(json.dumps(VERSIONS, sort_keys=True)), encoding='base64').decode() VERSIONS['signature'] = signature keysFilePath = os.path.join(self.fileServerDir, 'keys.gz') with gzip.open(keysFilePath, 'wb') as keysFile: keysFile.write(json.dumps(KEYS, sort_keys=True)) versionsFilePath = os.path.join(self.fileServerDir, 'versions.gz') with gzip.open(versionsFilePath, 'wb') as versionsFile: versionsFile.write(json.dumps(VERSIONS, sort_keys=True)) with gzip.open(versionsUserDataFilePath, 'wb') as versionsFile: versionsFile.write(json.dumps(VERSIONS, sort_keys=True)) tempFile = tempfile.NamedTemporaryFile() self.tempDir = tempFile.name tempFile.close() settings.CONFIG_DATA_FOLDER = os.path.join(self.tempDir, '.pyupdater') settings.USER_DATA_FOLDER = os.path.join(self.tempDir, 'pyu-data') os.mkdir(self.tempDir) os.mkdir(settings.USER_DATA_FOLDER) os.mkdir(settings.CONFIG_DATA_FOLDER) # The way we set the App name below avoids having to # create .pyupdater/config.pyu: settings.GENERIC_APP_NAME = APP_NAME settings.GENERIC_COMPANY_NAME = COMPANY_NAME os.environ['PYUPDATER_FILESERVER_DIR'] = self.fileServerDir os.environ['WXUPDATEDEMO_TESTING'] = 'True' os.environ['WXUPDATEDEMO_TESTING_FROZEN'] = 'True' os.environ['WXUPDATEDEMO_TESTING_APP_NAME'] = APP_NAME os.environ['WXUPDATEDEMO_TESTING_COMPANY_NAME'] = COMPANY_NAME os.environ['WXUPDATEDEMO_TESTING_APP_VERSION'] = CURRENT_VERSION os.environ['WXUPDATEDEMO_TESTING_PUBLIC_KEY'] = PUBLIC_KEY