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): |
|
|
|
|
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: |
|
|
|
|
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) ) |
|
|
|
|
95
|
|
|
ofh = open(filename, "wb") |
96
|
|
|
# download and report progress |
97
|
|
|
downloaded = 0 |
98
|
|
|
while True: |
99
|
|
|
data = ifh.read(100000) |
|
|
|
|
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): |
|
|
|
|
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
|
|
|
|