网络寻租

Programmer, Gamer, Hacker

程序如何作在线更新

| Comments

问题

image

我们的客户端程序庞大, 笨重, 更为严重的是, 当我们的程序发布给用户之后, 如果没有问题还好, 遇到了bug, 修改很成问题. 如何把更新后的程序发布给用户呢? 很多项目在做架构的时候, 选择采用web方式来绕过这样的问题. 但是很多时候还是非用客户端程序不可. 如何让客户端程序实现在线更新功能? 这里提供我的一个非常简单的解决方案.

思路

更新的业务逻辑很简单. 只需要下面几步:

  • 去某个服务器, 下载一个记录有版本信息的文档.
  • 与本地程序的版本号做比较, 如果不是最新的, 提示用户更新程序.
  • 更新程序的方法: 去服务器下载一个压缩包, 解压覆盖本地程序.

然后是具体实现上面的逻辑.

要点

  • 服务器必须能够被客户访问到.

    如果是在外网的话, 必须有一个存放更新文件的公共服务器. 我采用 google code 存放文件. 一个项目上传文件的限额是2G, 足够用了. 你也可以选择自己搭建一个http/ftp服务器, 或者用dropbox(被墙掉了), everbox(现在还不支持共享文件).

  • 更新程序. 下载程序后, 需要覆盖当前程序目录, windows下面, 开启后的程序会加锁执行文件, 所以需要运行另外的一个程序来做更新. 下面是具体的逻辑.

更新程序解决方案

比如旧程序在dir_a目录下面, 可执行文件是a.exe. 星号括起来的是当前正在执行的文件.

dir_a: **a.exe**

我们把dir_a拷贝出一份, 到dir_backup, 最新的程序保存到dir_backup里面, 保存为new.tar.gz. 下载完毕后, 我们在dir_b里面建立一个文件, 叫need_update, 作为标识.

dir_a: **a.exe**
dir_backup: a.exe, new.tar.gz, need_update

execl (c/c++/python/都可以用类似的函数调用), 关闭当前程序, 跳到dir_backup环境里面, 执行dir_backup/a.exe.

dir_a: a.exe
dir_backup: **a.exe**, new.tar.gz, need_update

dir_backup/a.exe启动的时候检查当前目录下面是否有need_update, 有的话, 就解压new.tar.gz, 覆盖dir_a. 这个时候程序已经更新完成了.

dir_a: new.exe
dir_backup: **a.exe**, new.tar.gz, need_update

然后再用 `execl`, 放弃当前程序, 执行dir_a/new.exe.

dir_a: **new.exe**
dir_backup: a.exe, new.tar.gz, need_update

步骤虽然有点复杂, 但是顶用, 也不需要另外加一个更新程序. 对于python发布出去的代码来说, 一个可执行文件就上M了, 少一个是一个.

下面是实际代码, 因为是从实际项目中挖出来的, 保证不能用. 都是update.py这一个源文件的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from qtlib import *
from qtlib.download import download
from config import add_config, conf
from tools.progresser import Progresser

import tarfile
from distutils.dir_util import copy_tree
SERVER = "http://project.googlecode.com/files/"

def update(program, version):
    """具体更新的方法"""
    if not confirm(None, '开始准备更新, 是否继续?'):
        return

    # 检查是否有更新
    if not check_update(program, version):
        return

    # 复制program
    temp_path = '../%s.backup' % program
    shutil.rmtree(temp_path, True)
    shutil.copytree(os.path.join('..', program),
                    temp_path)

    # 下载程序
    tar_file = program + '.tar.gz'
    local_tar_file = os.path.join(temp_path, tar_file)

    with Progresser("下载中, 比较忙, 你可以离开位置休息一下..","退出",0,100, None) as p:
        result = download(SERVER + tar_file,
                          local_tar_file,
                          stepper = p)
        if result == False:
            showMsg("无法连接到服务器, 请检查是否连上外部网络!")
            return
        elif result == None:
            showMsg('取消下载, 不好意思, 下次还得从头开始下载..')
            return

    showMsg('下载完成, 现在安装新程序.')
    os.chdir(temp_path)
    # 解压
    unzip(tar_file)
    # 标示一下是新程序
    with open('need_update', 'w+') as f:
        f.write(program)
    # 执行新程序
    program_name = program+'.exe'
    os.execl(program_name, program_name)

def check_update(program, version):
    """检查是否有更新"""
    # 下载versions文件
    if download(SERVER+'versions', 'temp/versions') == False:
        showMsg("无法连接到服务器, 请检查是否连上外部网络!")
        return False

    # 读取里面的信息
    data = open('temp/versions').read()
    # 格式是: program + 空格 + 版本号
    for line in data.split('\n'):
        if line.startswith(program):
            new_version = line.split(' ')[1]
            # 把版本号当作浮点来检查
            if float(version) < float(new_version):
                return True
            else:
                showMsg("程序已经是最新版本: %s, %s" % (program, version))


def check_finish_update():
    # 检查当前是否是需要更新的临时文件
    if not os.path.isfile('need_update'):
        return
    # 读取程序名称
    with open('need_update') as f:
        program = f.read()
    os.remove('need_update')
    # 然后把新程序复制回去
    old_path = '../%s'%program
    copy_tree(program, old_path)
    # 执行程序
    os.chdir(old_path)
    program_name = program + '.exe'
    os.execl(program_name, program_name)

# 加载update.py模块的时候, 判断当前是否为在backup过程中执行的..
check_finish_update()

其他

基于上面的更新逻辑, 我们还可以根据需求, 补充一些功能:

  • 启动的时候检查更新. 只需要用threading.Thread(target=check_update, …)来用一个线程跑更新检查就可以了.
  • 后台更新. 也可以用一个线程跑下载, 下载完毕后, 再通知主线程.
  • 增量更新, 减少下载时间. 可以考虑用diff之类工具.

如果有人觉得这样的更新脚本有价值的话, 可以联系我, 让我整理出一份不依赖其他模块的代码出来.

Comments