Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Mettre à jour à distance: écraser fichiers ?

+1 vote

Je reviens sur une question que j'ai posée il y a plus d'un an: ma question

J'ai toujours le même cahier des charges: je souhaite packager un programme que j'ai écrit, et le distribuer, sur/sous Linux, Mac OS et Windows. Jusqu'à maintenant, je faisais mon petit setup.py, je freezais le code avec cx_freeze (Linux, Windows) ou py2app (Mac OS), et avec Esky, je créais une sorte de package capable de s'auto-updater. Pour information, le programme est GUI et écrit avec PyQt: ChemBrows. Il aide les chercheurs en chimie à filtrer la littérature.

Le principe est simple: au démarrage mon programme vérifie sur mon serveur si une nouvelle version est présente, et si oui, il la télécharge et l'installe. Jusqu'à présent, c'était Esky qui se chargeait de l'update.

Le problème, c'est que Esky n'est plus maintenu. Je contribuais, mais je n'en ai plus le temps. Je cherche donc des solutions de remplacement, solides, simples et pérennes.

Déjà, je compte passer à pyinstaller pour le freezer. Il marche pour les trois OS à propri, et sans setup.py. De plus, il a l'air pérenne (je suis ouvert aux avis et suggestions).

Pour l'auto-update par contre, je n'ai pas d'idée. Comme je n'ai pas trouvé de lib qui a l'air durable et simple, j'ai décidé d'écrire ma propre feature d'update, basée sur la réponse de @eliotberriot de l'époque:

#!/usr/bin/python
# coding: utf-8
import os
import urllib.request
import json


API_URL = 'https://api.github.com/repos/Arzaroth/CelestiaSunrise/releases'
APP_EXE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'app.exe')
VERSION_INFO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION')


def check_new_version():
    """Check if there is new version, download it if necessary then launch the program"""

    current_version = get_current_version()
    latest_version, download_url = get_latest_version_data()

    if current_version is None:
        print('Current installation not found, please wait while downloading latest release...')
        install_new_version(latest_version, download_url)

    elif current_version != latest_version:
        print('A new version ({0}) is available. You are currently using {1}.'.format(latest_version, CURRENT_VERSION))
        install_new_version(latest_version, download_url)

    else:
        print('Current version is up to date !')

    print('Launching {0}'.format(APP_EXE))
    # os.system(APP_EXE)


def get_current_version():
    try:
        with open(VERSION_INFO_FILE) as f:
            return f.read()
    except FileNotFoundError:
        return None


def install_new_version(version_name, download_url):
    print('Updating to version {0}...'.format(version_name))
    download_new_version(download_url)
    update_current_version_name(version_name)


def download_new_version(download_url):
    print('Downloading new version ({0}...'.format(download_url))
    urllib.request.urlretrieve(download_url, APP_EXE)


def update_current_version_name(version_name):
    """Storing the new version name in a file so we can check against it for later updates"""

    with open(VERSION_INFO_FILE, 'w+') as f:
        return f.write(version_name) 


def get_latest_version_data(): 
    """Query the GitHub APi for the latest release and return the tag name and the exe download URL"""

    response = urllib.request.urlopen(API_URL)
    data = json.loads(response.read().decode('utf-8'))
    latest = data[0]
    return latest['tag_name'], latest['assets'][0]['browser_download_url']


if __name__ == '__main__':
    check_new_version()

J'ai juste un petit problème avec ce code, c'est la fonction qui installe la nouvelle version:

def install_new_version(version_name, download_url):
    print('Updating to version {0}...'.format(version_name))
    download_new_version(download_url)
    update_current_version_name(version_name)

Cette fonction "n'installe" en fait pas grand chose.

Supposons que le code ait réussi à télécharger la nouvelle version du programme. Qu'est ce que j'en fais ? Est-ce que je peux écraser les fichiers de mon programme, qui est en train de tourner ? Ou est-ce qu'il y a une approche plus safe ?

Vous constaterez que ma question est un peu longue. Je suis avant tout à la recherche d'avis sur la bonne technique à employer ici. Je veux pouvoir packager et updater mo programme sans trop galérer, maintenant et dans le futur. Écrire son propre updater me semble un bon choix ici, parce que cela m'évitera les mauvaises surprises du style "lib plus maintenue". Mais je suis ouvert aux suggestions.

demandé 27-Aou-2016 par Rififi (482 points)

2 Réponses

0 votes

Je suis plus développeur web, donc il est possible que je dise des bêtises.

Je ne pense pas que tu puisse/doive updater pendant que ton programme tourne. Dans la plupart des apps / outils que j'utilise, il me semble que les updates se font lorsque le programme ne tourne pas.

Le fait que l'app ne tourne pas lors d'un upgrade permet de gérer proprement la migration des données. Par exemple, imaginons que dans ta version 1, tu stocke les données en XML et que dans ta version 2, tu les stocke en JSON. Lors de ton upgrade, il va falloir que tu convertisse les données d'un format à l'autre. Si l'app tourne, l'utilisateur peut potentiellement mettre à jour les données pendant la migration, ce qui peut causer de la perte / corruption de données.

Peut-être qu'un approche safe pourrait être celle décrite ici, apparemment utilisée par Chrome: chaque nouvelle version est téléchargée et stockée dans un dossier dédié. Le launcher se content de lancer l'application avec le numéro de version le plus élevé.

Dans la pratique, je pense que ta structure de fichier de base peut ressembler à ça:

.
├── data
├── launcher.exe
└── programme
    ├── 1.1
        ├── programme.exe
        └── updater.exe

Lorsque tu lance launcher, le dossier programme est scanné, ordonné par version et le dossier correspondant à la dernière version est lancée.

Ensuite, dans ton programme proprement dit, tu peux faire appel à l'updater. Si il y a une nouvelle version disponible, tu la télécharge et tu la place dans un dossier dédié et nommé d'après son numéro de version (le dossier 1.2, par exemple)

Lorsque la nouvelle version est téléchargée, ton programme se ferme et relance le launcher, qui va lancer la nouvelle version.

Une fois que la nouvelle version est installée, ta structure de fichier ressemble à ça:

.
├── data
├── launcher.exe
└── programme
    ├── 1.1
    │   ├── programme.exe
    │   └── updater.exe
    └── 1.2
        ├── programme.exe
        └── updater.exe

Dans ton launcher, tu peux également supprimer les dossiers devenus inutiles, s'il y en a.

Les avantages de ce système sont multiples:

  • C'est propre: a aucun moment on écrit sur des fichiers existants, on se content d'en télécharger de nouveaux
  • Ça permet de versionner l'updater avec le programme lui-même et du coup d'ajouter si nécessaire des scripts de migration dans l'updater
  • Tu peux rollbacker facilement sur une ancienne version, tant qu'elle n'est pas supprimée, bien sûr

Par contre, il faut garder à l'esprit qu'avec cette approche, il y a un fichier qui ne s'update pas: le launcher. En théorie ce n'est pas à problème car c'est un fichier vraiment très simple, qui ne devrait pas changer.

Il est peut être possible de l'updater malgré tout en bundlant le launcher dans chaque version et en le copiant lors de l'update. Je ne sais pas si ça peut marcher.

répondu 28-Aou-2016 par eliotberriot (678 points)
0 votes

Pour éviter d'avoir un launcher "jamais mis à jour", tu pourrais faire un truc de ce style :

from multiprocessing import Process
import sys

def restart(f):
    p = Process(target=f)
    p.start()

def perform_update(updater_path):
    #dirty...
    sys.path.insert(0, updater_path)
    import updater
    restart(updater.update)

def application():
    #check if update available
    if update:
        update_folder = download_update()
        perform_update(update_folder)
    else:
        pass
        #normal stuff

Et dans ton update script:

def update():
    #Update stuff
    sys.path[0] = application_path
    import application
    application.restart(application.application)

C'est pas hyper propre mais c'est simple à maintenir et ça garde une certaine séparation des tâches : l'application lance l'update, l'update fait ce qu'elle a à faire (ajout de nouveaux fichiers, remplacement... blabla) et l'update relance la nouvelle application.

J'ai pas testé le code, c'est théorique tout ça

répondu 29-Aou-2016 par Fab
...