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
![]() |
|||
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
|
|||
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
|
|||
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
|
|||
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
|
|||
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 |