Issues (229)

client/osci/dialog/UpdateDlg.py (5 issues)

1
#
2
#  Copyright 2001 - 2016 Ludek Smid [http://www.ospace.net/]
3
#
4
#  This file is part of Outer Space.
5
#
6
#  Outer Space is free software; you can redistribute it and/or modify
7
#  it under the terms of the GNU General Public License as published by
8
#  the Free Software Foundation; either version 2 of the License, or
9
#  (at your option) any later version.
10
#
11
#  Outer Space is distributed in the hope that it will be useful,
12
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
13
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
#  GNU General Public License for more details.
15
#
16
#  You should have received a copy of the GNU General Public License
17
#  along with Outer Space; if not, write to the Free Software
18
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19
#
20
21
from ige import log
22
from ige.version import version as clientVersion
23
import os
24
from osci import client, gdata, res
25
import pygame
26
import pygameui as ui
27
import re
28
import shutil
29
import sys
30
import urllib2
31
import tarfile
32
33
class UpdateDlg:
34
35
    def __init__(self, app):
36
        self.app = app
37
        self.createUI()
38
        self.checkedForUpdate = False
39
40
    def display(self, caller = None, options = None):
41
        self.caller = caller
42
        self.options = options
43
        if self.checkedForUpdate:
44
            log.debug("Update already checked this session, skipping it")
45
            self.onCancel(None, None, '')
46
            return
47
        update = self.isUpdateAvailable()
48
        # check for new version only once per session
49
        self.checkedForUpdate = True
50
        if update is False:
51
            self.onCancel(None, None, _("Client is up-to-date"))
52
            return
53
        self.win.show()
54
        self.win.vProgress.visible = 0
55
        if update is True:
56
            self.setUpdateAction()
57
58
    def hide(self):
59
        self.win.hide()
60
61
    def onConfirm(self, widget, action, data):
62
        self.win.vStatusBar.text = _("Updating client...")
63
        # self.win.hide()
64
65
    def performDownload(self, updateDirectory):
66
        """Download zip with new version"""
67
        log.debug('Downloading new version')
68
        self.setProgress('Preparing download...', 0, 1)
69
        # setup proxies
70
        proxies = {}
71
        if gdata.config.proxy.http != None:
72
            proxies['http'] = gdata.config.proxy.http
73
        log.debug('Using proxies', proxies)
74
        # get file
75
        try:
76
            # open URL
77
            opener = urllib2.build_opener(urllib2.ProxyHandler(proxies))
78
            # it unfortunately is not completely reliable
79
            for i in xrange(1,5):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable xrange does not seem to be defined.
Loading history...
80
                try:
81
                    ifh = opener.open(self.url)
82
                    log.debug("Retrieving URL", ifh.geturl())
83
                    # download file
84
                    total = int(ifh.info()["content-length"])
85
                    basename = re.search('(?<=filename=).*', ifh.info()["content-disposition"]).group(0)
86
                    break
87
                except KeyError:
88
                    pygame.time.wait(1)
89
            if not basename:
0 ignored issues
show
The variable basename does not seem to be defined in case the for loop on line 79 is not entered. Are you sure this can never be the case?
Loading history...
90
                log.message("URL is not a file")
91
                self.reportFailure(_("Error: URL does not point to a file."))
92
                return
93
            filename = os.path.join(updateDirectory, basename)
94
            log.debug("Downloading file %s of size %d" % (filename, total) )
0 ignored issues
show
The variable total does not seem to be defined in case the for loop on line 79 is not entered. Are you sure this can never be the case?
Loading history...
95
            ofh = open(filename, "wb")
96
            # download and report progress
97
            downloaded = 0
98
            while True:
99
                data = ifh.read(100000)
0 ignored issues
show
The variable ifh does not seem to be defined in case the for loop on line 79 is not entered. Are you sure this can never be the case?
Loading history...
100
                if not data:
101
                    break
102
                ofh.write(data)
103
                downloaded += len(data)
104
                log.debug("Download progress", downloaded, total)
105
                self.setProgress("Downloading update...", downloaded, total)
106
            ifh.close()
107
            ofh.close()
108
            return filename
109
        except urllib2.URLError, e:
110
            log.warning("Cannot download file")
111
            self.reportFailure(_("Cannot finish download: %(s)") % str(e.reason))
112
            return None
113
114
    def performUpdate(self, updateDirectory, filename):
115
        log.debug('Updating game to the new version')
116
        """Extract new version, and replace current directory with it"""
117
        self.setProgress('Preparing update...', 0, 4)
118
        # we expect archive contains one common prefix
119
        version = "%(major)s.%(minor)s.%(revision)s%(status)s" % self.serverVersion
120
        expectedDir = 'outerspace-' + version
121
        # now extraction!
122
        archive = tarfile.open(filename, 'r:gz')
123
        for member in archive.getnames():
124
            if not re.match('^{0}'.format(expectedDir), member):
125
                log.error("That archive is suspicious, because of file {0}".format(member))
126
                log.debug("Expected prefix directory is {0}".format(expectedDir))
127
                sys.exit(1)
128
        log.debug('Archive has expected directory structure')
129
        self.setProgress('Extracting new version...', 1, 4)
130
        archive.extractall(updateDirectory)
131
        log.debug('Update extracted to temporary directory')
132
133
        self.setProgress('Backing up old version...', 2, 4)
134
        # move current directory to temporary location
135
        actualDir = os.path.dirname(os.path.abspath(sys.argv[0]))
136
        actualDirTrgt = os.path.join(updateDirectory, os.path.basename(actualDir))
137
        if os.path.exists(actualDirTrgt):
138
            shutil.rmtree(actualDirTrgt)
139
        # we have to clear out of CWD, as it might get deleted
140
        # and python does not like that situation
141
        savedCWD = os.getcwd()
142
        os.chdir(updateDirectory)  # this is ensured to exist
143
        shutil.copytree(actualDir, actualDirTrgt)
144
        # ignore_errors is set because of windows
145
        # they prohibit removing of directory, if user browse it.
146
        # result is empty actualDir
147
        shutil.rmtree(actualDir, ignore_errors=True)
148
        log.debug('Old version backuped to {0}'.format(actualDirTrgt))
149
150
        self.setProgress('Applying new version...', 3, 4)
151
152
        # move newly extracted directory to original location
153
        extractedDir = os.path.join(updateDirectory, expectedDir)
154
        if os.path.exists(actualDir):
155
            # most likely due to Windows issue described above
156
            # as normally this directory should have been removed
157
            log.debug('Moving new version item by item')
158
            for item in os.listdir(extractedDir):
159
                shutil.move(os.path.join(extractedDir, item), os.path.join(actualDir, item))
160
            os.rmdir(extractedDir)
161
        else:
162
            log.debug('Moving new version in bulk')
163
            # simple version, non-windows
164
            shutil.move(extractedDir, actualDir)
165
        os.chdir(savedCWD)
166
        self.setProgress('Update complete', 4, 4)
167
        log.debug('Game prepared for restart')
168
169
170
    def performRestart(self, widget, action, data):
171
        text = [
172
            _("Game will now restart itself.")
173
        ]
174
        self.win.vConfirm.action = "onRestart"
175
        self.win.vConfirm.text = _("Restart")
176
        self.win.vCancel.text = ""
177
        self.win.vCancel.enabled = False
178
        self.win.vText.text = text
179
        self.win.title = _("Outer Space Update Complete")
180
181
    def onRestart(self, widget, action, data):
182
        if os.name == 'nt':
183
            quoted = map(lambda x: '"' + str(x) + '"', sys.argv)
184
            os.execl(sys.executable, sys.executable, *quoted)
185
        else:
186
            os.execl(sys.executable, sys.executable, *sys.argv)
187
188
189
    def onDownloadAndInstall(self, widget, action, data):
190
191
        updateDirectory = os.path.join(self.options.configDir, 'Update')
192
        if not os.path.isdir(updateDirectory):
193
            log.debug("Creating update directory")
194
            os.mkdir(updateDirectory)
195
        filename = self.performDownload(updateDirectory)
196
        if filename is None:
197
            self.onQuit(widget, action, data)
198
        self.performUpdate(updateDirectory, filename)
199
        self.performRestart(widget, action, data)
200
201
202
    def reportFailure(self, reason):
203
        self.win.vProgress.visible = 0
204
        self.win.vText.text = [reason]
205
        self.win.vCancel.text = ""
206
        self.win.vConfirm.text = _("OK")
207
        self.win.vConfirm.action = "onCancel"
208
209
    def setProgress(self, text, current = None, max = None):
210
        self.win.vProgress.visible = 1
211
        if text:
212
            self.win.vText.text = [text]
213
        if max != None:
214
            self.win.vProgress.min = 0
215
            self.win.vProgress.max = max
216
        if current != None:
217
            self.win.vProgress.value = current
218
        self.app.update()
219
220
    def onCancel(self, widget, action, data):
221
        self.win.hide()
222
        if self.caller:
223
            self.caller.display(message = data or _("Update skipped."))
224
225
    def onQuit(self, widget, action, data):
226
        self.win.hide()
227
        self.app.exit()
228
229
    def isUpdateAvailable(self):
230
        """Check if client version matches server version and update client
231
        if neccessary"""
232
        log.message("Checking for update...")
233
        updateMode = gdata.config.client.updatemode or "normal"
234
        # quit if update is disabled
235
        if updateMode == 'never':
236
            return False
237
        # compare server and client versions
238
        log.message("Retrieving server version")
239
        self.serverVersion = client.cmdProxy.getVersion()
240
        log.debug("Comparing server and client versions", self.serverVersion, clientVersion)
241
        matches = True
242
        for i in ("major", "minor", "revision", "status"):
243
            if clientVersion[i] != self.serverVersion[i]:
244
                matches = False
245
        if matches:
246
            log.message("Versions match, no need to update")
247
            return False
248
        log.message("Version do not match, update is needed")
249
        return True
250
251
    def setUpdateAction(self):
252
        response = self.serverVersion["clientURLs"].get(sys.platform, self.serverVersion["clientURLs"]["*"])
253
        self.url = response
254
        # if the game resides in git repository, leave it on user, otherwise volunteer to perform update
255
        gameDirectory = os.path.realpath(os.path.dirname(sys.argv[0]))
256
        gitDir = os.path.join(gameDirectory, '.git')
257
        if os.path.isdir(gitDir):
258
            version = "%(major)s.%(minor)s.%(revision)s%(status)s" % self.serverVersion
259
            text = [
260
                _("Server requires client version %s. It is recommended to update your client.") % version,
261
                "",
262
                _('Please update your git repo to tag "%s"') % version
263
            ]
264
            self.win.vConfirm.action = "onQuit"
265
            self.win.vConfirm.text = _("OK")
266
            self.win.vStatusBar.layout = (0, 5, 17,1)
267
            self.win.vCancel.visible = 0
268
        else:
269
            version = "%(major)s.%(minor)s.%(revision)s%(status)s" % self.serverVersion
270
            text = [
271
                _("Server requires client version %s. It is necessary to update your client.") % version,
272
                "",
273
                _("Do you want Outer Space to perform update?")
274
            ]
275
            self.win.vConfirm.action = "onDownloadAndInstall"
276
            self.win.vCancel.action = "onQuit"
277
            self.win.vCancel.text = _("Quit")
278
        self.win.vText.text = text
279
280 View Code Duplication
    def createUI(self):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
281
        w, h = gdata.scrnSize
282
        self.win = ui.Window(self.app,
283
            modal = 1,
284
            movable = 0,
285
            title = _('Outer Space Update Available'),
286
            rect = ui.Rect((w - 424) / 2, (h - 144) / 2, 424, 144),
287
            layoutManager = ui.SimpleGridLM(),
288
        )
289
        self.win.subscribeAction('*', self)
290
        ui.Text(self.win, layout = (5, 0, 16, 4), id = 'vText', background = self.win.app.theme.themeBackground, editable = 0)
291
        ui.ProgressBar(self.win, layout = (0, 4, 21, 1), id = 'vProgress')
292
        ui.Label(self.win, layout = (0, 0, 5, 4), icons = ((res.loginLogoImg, ui.ALIGN_W),))
293
        ui.Title(self.win, layout = (0, 5, 13, 1), id = 'vStatusBar', align = ui.ALIGN_W)
294
        ui.TitleButton(self.win, layout = (13, 5, 4, 1), id = 'vCancel', text = _("No"), action = 'onCancel')
295
        ui.TitleButton(self.win, layout = (17, 5, 4, 1), id = 'vConfirm', text = _("Yes"), action = 'onConfirm')
296