github / src / autoupdate.py @ 0ce7e35f
1 |
# Copyright (c) 2016, LE GOFF Vincent
|
---|---|
2 |
# Copyright (c) 2016, LE GOFF Vincent
|
3 |
# All rights reserved.
|
4 |
|
5 |
# Redistribution and use in source and binary forms, with or without
|
6 |
# modification, are permitted provided that the following conditions are met:
|
7 |
|
8 |
# * Redistributions of source code must retain the above copyright notice, this
|
9 |
# list of conditions and the following disclaimer.
|
10 |
|
11 |
# * Redistributions in binary form must reproduce the above copyright notice,
|
12 |
# this list of conditions and the following disclaimer in the documentation
|
13 |
# and/or other materials provided with the distribution.
|
14 |
|
15 |
# * Neither the name of ytranslate nor the names of its
|
16 |
# contributors may be used to endorse or promote products derived from
|
17 |
# this software without specific prior written permission.
|
18 |
|
19 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
22 |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
23 |
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24 |
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
25 |
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
26 |
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
27 |
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28 |
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
29 |
|
30 |
"""This file contains the AutoUpdate class."""
|
31 |
|
32 |
import json |
33 |
import os |
34 |
import shutil |
35 |
from subprocess import Popen |
36 |
import sys |
37 |
from threading import Thread |
38 |
import urllib |
39 |
from zipfile import ZipFile, BadZipfile |
40 |
|
41 |
class AutoUpdate(Thread): |
42 |
|
43 |
"""Class to perform an automatic update.
|
44 |
|
45 |
An object of this class is used to perform an automatic update.
|
46 |
This can be triggered by a standalone program (required when the
|
47 |
update actually performs), but some methods of its object can
|
48 |
be used to check for updates and do not leave the application.
|
49 |
|
50 |
"""
|
51 |
|
52 |
def __init__(self, current_version, object=None, just_checking=False): |
53 |
Thread.__init__(self)
|
54 |
if isinstance(current_version, str): |
55 |
if not current_version.isdigit(): |
56 |
raise ValueError("the current version {} isn't an int".format( |
57 |
repr(current_version)))
|
58 |
else:
|
59 |
current_version = int(current_version)
|
60 |
|
61 |
self.current_version = current_version
|
62 |
self.location = None |
63 |
self.path_archive = None |
64 |
self.object = object |
65 |
self.just_checking = just_checking
|
66 |
|
67 |
def run(self): |
68 |
"""Run the thread."""
|
69 |
build = self.check()
|
70 |
if self.object: |
71 |
self.object.ResponseUpdate(build)
|
72 |
|
73 |
if build is not None: |
74 |
if not self.just_checking: |
75 |
self.download()
|
76 |
self.update()
|
77 |
|
78 |
def check(self): |
79 |
"""Check for updates."""
|
80 |
if self.object: |
81 |
self.object.UpdateText("ui.message.update.checking") |
82 |
self.object.UpdateGauge(0) |
83 |
|
84 |
url = "https://cocomud.plan.io/projects/cocomud-client.json"
|
85 |
try:
|
86 |
response = urllib.request.urlopen(url) |
87 |
except urllib.error.URLError:
|
88 |
return
|
89 |
|
90 |
try:
|
91 |
info = json.loads(response.read()) |
92 |
except ValueError: |
93 |
raise UpdateDecodeError
|
94 |
|
95 |
# The latest builds are in a custom field
|
96 |
customs = info.get("project", {}).get("custom_fields") |
97 |
recent_build = None
|
98 |
for field in customs: |
99 |
if field['name'] == "build": |
100 |
recent_build = field['value']
|
101 |
|
102 |
# If a recent build has been found, try to read it
|
103 |
if recent_build:
|
104 |
try:
|
105 |
recent_build = json.loads(recent_build) |
106 |
except ValueError: |
107 |
raise UpdateDecodeError
|
108 |
else:
|
109 |
raise UnavailableUpdateError
|
110 |
|
111 |
# If everything went according to plan, recent_build is a dictionary
|
112 |
# with build: {locations}
|
113 |
new_build = list(recent_build.keys())[0] |
114 |
if new_build.isdigit():
|
115 |
new_build = int(new_build)
|
116 |
else:
|
117 |
raise InvalidSyntaxUpdateError
|
118 |
|
119 |
# If the recent build is greated than the current one
|
120 |
platform = ""
|
121 |
if sys.platform == "win32": |
122 |
platform = "windows"
|
123 |
|
124 |
location = recent_build[str(new_build)].get(platform)
|
125 |
if location:
|
126 |
self.location = location
|
127 |
if new_build > self.current_version: |
128 |
if self.location: |
129 |
return new_build
|
130 |
else:
|
131 |
raise UknownPlatformUpdateError
|
132 |
|
133 |
def download(self, stdout=False): |
134 |
"""Download the build."""
|
135 |
if self.object: |
136 |
self.object.UpdateText("ui.message.update.downloading") |
137 |
self.object.UpdateGauge(0) |
138 |
|
139 |
# Creates a new folder for updates
|
140 |
if os.path.exists("updating"): |
141 |
shutil.rmtree("updating")
|
142 |
|
143 |
os.mkdir("updating")
|
144 |
if stdout:
|
145 |
print("Downloading the build at", self.location) |
146 |
|
147 |
# Get the build
|
148 |
response = urlopen(self.location)
|
149 |
meta = response.info() |
150 |
size = int(meta.getheaders("Content-Length")[0]) |
151 |
chunk_size = 4096
|
152 |
path_archive = os.path.join("updating", "build.zip") |
153 |
with open(path_archive, "wb") as file: |
154 |
keep = True
|
155 |
progress = 0.0
|
156 |
percent = 0
|
157 |
if stdout:
|
158 |
sys.stdout.write(" Downloading... 0.0%")
|
159 |
sys.stdout.flush() |
160 |
|
161 |
while keep:
|
162 |
old_percent = percent |
163 |
progress += chunk_size |
164 |
percent = round((progress / size) * 100, 1) |
165 |
if self.object and int(percent) != int(old_percent): |
166 |
self.object.UpdateGauge(int(percent)) |
167 |
elif old_percent != percent and stdout: |
168 |
sys.stdout.write("\r Downloading... {:>5}%".format(
|
169 |
percent)) |
170 |
sys.stdout.flush() |
171 |
|
172 |
chunk = response.read(chunk_size) |
173 |
if not chunk: |
174 |
keep = False
|
175 |
|
176 |
file.write(chunk)
|
177 |
|
178 |
if stdout:
|
179 |
print("\r Downloading... 100%")
|
180 |
|
181 |
self.path_archive = path_archive
|
182 |
|
183 |
def update(self, stdout=False): |
184 |
"""Update the archive.
|
185 |
|
186 |
This method must be called after downloading the new archive.
|
187 |
Since it cannot perform all updates by itself (it would delete
|
188 |
the updater and a couple of libraries), it needs to pass a batch
|
189 |
file at the end that will delete itself.
|
190 |
|
191 |
"""
|
192 |
if self.path_archive is None: |
193 |
raise ValueError("the updated archive hasn't been downloaded") |
194 |
|
195 |
if self.object: |
196 |
self.object.UpdateText("ui.message.update.extracting") |
197 |
self.object.UpdateGauge(0) |
198 |
|
199 |
# Analyze the zip file
|
200 |
try:
|
201 |
with ZipFile(self.path_archive, "r") as archive: |
202 |
infos = archive.infolist() |
203 |
extract = [] |
204 |
total = 0
|
205 |
for info in infos: |
206 |
name = info.filename |
207 |
names = name.split("/")
|
208 |
if len(names) > 1 and names[1] in ("settings", "worlds"): |
209 |
continue
|
210 |
|
211 |
total += info.file_size |
212 |
extract.append(info) |
213 |
|
214 |
# Extract these files
|
215 |
if stdout:
|
216 |
print("Extracting {}o".format(total))
|
217 |
|
218 |
extracted = 0.0
|
219 |
if stdout:
|
220 |
sys.stdout.write(" Extracting files... 0%")
|
221 |
sys.stdout.flush() |
222 |
|
223 |
percent = 0.0
|
224 |
for info in extract: |
225 |
old_percent = percent |
226 |
percent = round(extracted / total * 100, 1) |
227 |
if self.object and int(percent) != int(old_percent): |
228 |
self.object.UpdateGauge(int(percent)) |
229 |
elif old_percent != percent and stdout: |
230 |
sys.stdout.write("\r Extracting files..." \
|
231 |
" {:>5}%".format(percent))
|
232 |
sys.stdout.flush() |
233 |
|
234 |
archive.extract(info.filename, "updating")
|
235 |
extracted += info.file_size |
236 |
|
237 |
if stdout:
|
238 |
print("\r Extracting files... 100%")
|
239 |
except BadZipfile:
|
240 |
raise InvalidSyntaxUpdateError
|
241 |
|
242 |
# Delete the archive
|
243 |
os.remove(self.path_archive)
|
244 |
|
245 |
# Move the content of the archive
|
246 |
batch = "timeout 1 /NOBREAK"
|
247 |
for name in os.listdir(os.path.join("updating", "CocoMUD")): |
248 |
path = os.path.join("updating", "CocoMUD", name) |
249 |
if os.path.isfile(name):
|
250 |
batch += "\ndel /F /Q " + name
|
251 |
batch += "\ncopy /V " + path + " " + name |
252 |
elif os.path.isdir(name):
|
253 |
batch += "\nrmdir /Q /S " + name
|
254 |
batch += "\nmd " + name
|
255 |
batch += "\nxcopy /S " + path + " " + name |
256 |
elif not os.path.exists(name): |
257 |
if os.path.isfile(path):
|
258 |
batch += "\ncopy /V " + path + " " + name |
259 |
elif os.path.isdir(path):
|
260 |
batch += "\nmd " + name
|
261 |
batch += "\nxcopy /S " + path + " " + name |
262 |
|
263 |
|
264 |
# Add instructions to delete the clean update
|
265 |
batch += "\nrmdir /S /Q updating"
|
266 |
batch += "\nstart /B \"\" cocomud.exe"
|
267 |
batch += "\nexit"
|
268 |
|
269 |
# Write the batch file
|
270 |
with open("updating.bat", "w") as file: |
271 |
file.write(batch)
|
272 |
with open("bgupdating.bat", "w") as file: |
273 |
cmd = "cmd /C updating.bat >> update.log 2>&1"
|
274 |
cmd += "exit"
|
275 |
file.write(cmd)
|
276 |
|
277 |
if self.object: |
278 |
self.object.AskDestroy()
|
279 |
#os.startfile("bgupdating.bat")
|
280 |
hidden = 0x08000000 # Windows only |
281 |
Popen(["bgupdating.bat"], bufsize=-1, creationflags=hidden) |
282 |
sys.exit(0)
|
283 |
|
284 |
|
285 |
# Exceptions
|
286 |
class UpdateError(RuntimeError): |
287 |
|
288 |
"""An error occured dduring the update."""
|
289 |
|
290 |
pass
|
291 |
|
292 |
|
293 |
class UpdateDecodeError(UpdateError): |
294 |
|
295 |
"""The update couldn't be decoded."""
|
296 |
|
297 |
pass
|
298 |
|
299 |
|
300 |
class UnavailableUpdateError(UpdateError): |
301 |
|
302 |
"""The update cannot be reached."""
|
303 |
|
304 |
pass
|
305 |
|
306 |
|
307 |
class InvalidSyntaxUpdateError(UpdateError): |
308 |
|
309 |
"""Something went wrong in the syntax of the build."""
|
310 |
|
311 |
pass
|
312 |
|
313 |
|
314 |
class UknownPlatformUpdateError(UpdateError): |
315 |
|
316 |
"""No platform could be found for this build."""
|
317 |
|
318 |
pass
|