SHOW:
|
|
- or go back to the newest paste.
1 | tArgs = {...} | |
2 | ||
3 | if not term.isColor or not term.isColor() then | |
4 | error('OneOS Requires an Advanced (gold) Computer') | |
5 | end | |
6 | ||
7 | _jstr = [[ | |
8 | local base = _G | |
9 | ||
10 | ----------------------------------------------------------------------------- | |
11 | -- Module declaration | |
12 | ----------------------------------------------------------------------------- | |
13 | ||
14 | -- Public functions | |
15 | ||
16 | -- Private functions | |
17 | local decode_scanArray | |
18 | local decode_scanComment | |
19 | local decode_scanConstant | |
20 | local decode_scanNumber | |
21 | local decode_scanObject | |
22 | local decode_scanString | |
23 | local decode_scanWhitespace | |
24 | local encodeString | |
25 | local isArray | |
26 | local isEncodable | |
27 | ||
28 | ----------------------------------------------------------------------------- | |
29 | -- PUBLIC FUNCTIONS | |
30 | ----------------------------------------------------------------------------- | |
31 | --- Encodes an arbitrary Lua object / variable. | |
32 | -- @param v The Lua object / variable to be JSON encoded. | |
33 | -- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode) | |
34 | function encode (v) | |
35 | -- Handle nil values | |
36 | if v==nil then | |
37 | return "null" | |
38 | end | |
39 | ||
40 | local vtype = base.type(v) | |
41 | ||
42 | -- Handle strings | |
43 | if vtype=='string' then | |
44 | return '"' .. encodeString(v) .. '"' -- Need to handle encoding in string | |
45 | end | |
46 | ||
47 | -- Handle booleans | |
48 | if vtype=='number' or vtype=='boolean' then | |
49 | return base.tostring(v) | |
50 | end | |
51 | ||
52 | -- Handle tables | |
53 | if vtype=='table' then | |
54 | local rval = {} | |
55 | -- Consider arrays separately | |
56 | local bArray, maxCount = isArray(v) | |
57 | if bArray then | |
58 | for i = 1,maxCount do | |
59 | table.insert(rval, encode(v[i])) | |
60 | end | |
61 | else -- An object, not an array | |
62 | for i,j in base.pairs(v) do | |
63 | if isEncodable(i) and isEncodable(j) then | |
64 | table.insert(rval, '"' .. encodeString(i) .. '":' .. encode(j)) | |
65 | end | |
66 | end | |
67 | end | |
68 | if bArray then | |
69 | return '[' .. table.concat(rval,',') ..']' | |
70 | else | |
71 | return '{' .. table.concat(rval,',') .. '}' | |
72 | end | |
73 | end | |
74 | ||
75 | -- Handle null values | |
76 | if vtype=='function' and v==null then | |
77 | return 'null' | |
78 | end | |
79 | ||
80 | base.assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. base.tostring(v)) | |
81 | end | |
82 | ||
83 | ||
84 | --- Decodes a JSON string and returns the decoded value as a Lua data structure / value. | |
85 | -- @param s The string to scan. | |
86 | -- @param [startPos] Optional starting position where the JSON string is located. Defaults to 1. | |
87 | -- @param Lua object, number The object that was scanned, as a Lua table / string / number / boolean or nil, | |
88 | -- and the position of the first character after | |
89 | -- the scanned JSON object. | |
90 | function decode(s, startPos) | |
91 | startPos = startPos and startPos or 1 | |
92 | startPos = decode_scanWhitespace(s,startPos) | |
93 | base.assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']') | |
94 | local curChar = string.sub(s,startPos,startPos) | |
95 | -- Object | |
96 | if curChar=='{' then | |
97 | return decode_scanObject(s,startPos) | |
98 | end | |
99 | -- Array | |
100 | if curChar=='[' then | |
101 | return decode_scanArray(s,startPos) | |
102 | end | |
103 | -- Number | |
104 | if string.find("+-0123456789.e", curChar, 1, true) then | |
105 | return decode_scanNumber(s,startPos) | |
106 | end | |
107 | -- String | |
108 | if curChar=='"' or curChar=="'" then | |
109 | return decode_scanString(s,startPos) | |
110 | end | |
111 | if string.sub(s,startPos,startPos+1)=='/*' then | |
112 | return decode(s, decode_scanComment(s,startPos)) | |
113 | end | |
114 | -- Otherwise, it must be a constant | |
115 | return decode_scanConstant(s,startPos) | |
116 | end | |
117 | ||
118 | --- The null function allows one to specify a null value in an associative array (which is otherwise | |
119 | -- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null } | |
120 | function null() | |
121 | return null -- so json.null() will also return null ;-) | |
122 | end | |
123 | ----------------------------------------------------------------------------- | |
124 | -- Internal, PRIVATE functions. | |
125 | -- Following a Python-like convention, I have prefixed all these 'PRIVATE' | |
126 | -- functions with an underscore. | |
127 | ----------------------------------------------------------------------------- | |
128 | ||
129 | --- Scans an array from JSON into a Lua object | |
130 | -- startPos begins at the start of the array. | |
131 | -- Returns the array and the next starting position | |
132 | -- @param s The string being scanned. | |
133 | -- @param startPos The starting position for the scan. | |
134 | -- @return table, int The scanned array as a table, and the position of the next character to scan. | |
135 | function decode_scanArray(s,startPos) | |
136 | local array = {} -- The return value | |
137 | local stringLen = string.len(s) | |
138 | base.assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s ) | |
139 | startPos = startPos + 1 | |
140 | -- Infinite loop for array elements | |
141 | repeat | |
142 | startPos = decode_scanWhitespace(s,startPos) | |
143 | base.assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.') | |
144 | local curChar = string.sub(s,startPos,startPos) | |
145 | if (curChar==']') then | |
146 | return array, startPos+1 | |
147 | end | |
148 | if (curChar==',') then | |
149 | startPos = decode_scanWhitespace(s,startPos+1) | |
150 | end | |
151 | base.assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.') | |
152 | object, startPos = decode(s,startPos) | |
153 | table.insert(array,object) | |
154 | until false | |
155 | end | |
156 | ||
157 | --- Scans a comment and discards the comment. | |
158 | -- Returns the position of the next character following the comment. | |
159 | -- @param string s The JSON string to scan. | |
160 | -- @param int startPos The starting position of the comment | |
161 | function decode_scanComment(s, startPos) | |
162 | base.assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos) | |
163 | local endPos = string.find(s,'*/',startPos+2) | |
164 | base.assert(endPos~=nil, "Unterminated comment in string at " .. startPos) | |
165 | return endPos+2 | |
166 | end | |
167 | ||
168 | --- Scans for given constants: true, false or null | |
169 | -- Returns the appropriate Lua type, and the position of the next character to read. | |
170 | -- @param s The string being scanned. | |
171 | -- @param startPos The position in the string at which to start scanning. | |
172 | -- @return object, int The object (true, false or nil) and the position at which the next character should be | |
173 | -- scanned. | |
174 | function decode_scanConstant(s, startPos) | |
175 | local consts = { ["true"] = true, ["false"] = false, ["null"] = nil } | |
176 | local constNames = {"true","false","null"} | |
177 | ||
178 | for i,k in base.pairs(constNames) do | |
179 | --print ("[" .. string.sub(s,startPos, startPos + string.len(k) -1) .."]", k) | |
180 | if string.sub(s,startPos, startPos + string.len(k) -1 )==k then | |
181 | return consts[k], startPos + string.len(k) | |
182 | end | |
183 | end | |
184 | base.assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos) | |
185 | end | |
186 | ||
187 | --- Scans a number from the JSON encoded string. | |
188 | -- (in fact, also is able to scan numeric +- eqns, which is not | |
189 | -- in the JSON spec.) | |
190 | -- Returns the number, and the position of the next character | |
191 | -- after the number. | |
192 | -- @param s The string being scanned. | |
193 | -- @param startPos The position at which to start scanning. | |
194 | -- @return number, int The extracted number and the position of the next character to scan. | |
195 | function decode_scanNumber(s,startPos) | |
196 | local endPos = startPos+1 | |
197 | local stringLen = string.len(s) | |
198 | local acceptableChars = "+-0123456789.e" | |
199 | while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true) | |
200 | and endPos<=stringLen | |
201 | ) do | |
202 | endPos = endPos + 1 | |
203 | end | |
204 | local stringValue = 'return ' .. string.sub(s,startPos, endPos-1) | |
205 | local stringEval = base.loadstring(stringValue) | |
206 | base.assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos) | |
207 | return stringEval(), endPos | |
208 | end | |
209 | ||
210 | --- Scans a JSON object into a Lua object. | |
211 | -- startPos begins at the start of the object. | |
212 | -- Returns the object and the next starting position. | |
213 | -- @param s The string being scanned. | |
214 | -- @param startPos The starting position of the scan. | |
215 | -- @return table, int The scanned object as a table and the position of the next character to scan. | |
216 | function decode_scanObject(s,startPos) | |
217 | local object = {} | |
218 | local stringLen = string.len(s) | |
219 | local key, value | |
220 | base.assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s) | |
221 | startPos = startPos + 1 | |
222 | repeat | |
223 | startPos = decode_scanWhitespace(s,startPos) | |
224 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.') | |
225 | local curChar = string.sub(s,startPos,startPos) | |
226 | if (curChar=='}') then | |
227 | return object,startPos+1 | |
228 | end | |
229 | if (curChar==',') then | |
230 | startPos = decode_scanWhitespace(s,startPos+1) | |
231 | end | |
232 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.') | |
233 | -- Scan the key | |
234 | key, startPos = decode(s,startPos) | |
235 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
236 | startPos = decode_scanWhitespace(s,startPos) | |
237 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
238 | base.assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos) | |
239 | startPos = decode_scanWhitespace(s,startPos+1) | |
240 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
241 | value, startPos = decode(s,startPos) | |
242 | object[key]=value | |
243 | until false -- infinite loop while key-value pairs are found | |
244 | end | |
245 | ||
246 | --- Scans a JSON string from the opening inverted comma or single quote to the | |
247 | -- end of the string. | |
248 | -- Returns the string extracted as a Lua string, | |
249 | -- and the position of the next non-string character | |
250 | -- (after the closing inverted comma or single quote). | |
251 | -- @param s The string being scanned. | |
252 | -- @param startPos The starting position of the scan. | |
253 | -- @return string, int The extracted string as a Lua string, and the next character to parse. | |
254 | function decode_scanString(s,startPos) | |
255 | base.assert(startPos, 'decode_scanString(..) called without start position') | |
256 | local startChar = string.sub(s,startPos,startPos) | |
257 | base.assert(startChar=="'" or startChar=='"','decode_scanString called for a non-string') | |
258 | local escaped = false | |
259 | local endPos = startPos + 1 | |
260 | local bEnded = false | |
261 | local stringLen = string.len(s) | |
262 | repeat | |
263 | local curChar = string.sub(s,endPos,endPos) | |
264 | -- Character escaping is only used to escape the string delimiters | |
265 | if not escaped then | |
266 | if curChar=='\\' then | |
267 | escaped = true | |
268 | else | |
269 | bEnded = curChar==startChar | |
270 | end | |
271 | else | |
272 | -- If we're escaped, we accept the current character come what may | |
273 | escaped = false | |
274 | end | |
275 | endPos = endPos + 1 | |
276 | base.assert(endPos <= stringLen+1, "String decoding failed: unterminated string at position " .. endPos) | |
277 | until bEnded | |
278 | local stringValue = 'return ' .. string.sub(s, startPos, endPos-1) | |
279 | local stringEval = base.loadstring(stringValue) | |
280 | base.assert(stringEval, 'Failed to load string [ ' .. stringValue .. '] in JSON4Lua.decode_scanString at position ' .. startPos .. ' : ' .. endPos) | |
281 | return stringEval(), endPos | |
282 | end | |
283 | ||
284 | --- Scans a JSON string skipping all whitespace from the current start position. | |
285 | -- Returns the position of the first non-whitespace character, or nil if the whole end of string is reached. | |
286 | -- @param s The string being scanned | |
287 | -- @param startPos The starting position where we should begin removing whitespace. | |
288 | -- @return int The first position where non-whitespace was encountered, or string.len(s)+1 if the end of string | |
289 | -- was reached. | |
290 | function decode_scanWhitespace(s,startPos) | |
291 | local whitespace=" \n\r\t" | |
292 | local stringLen = string.len(s) | |
293 | while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do | |
294 | startPos = startPos + 1 | |
295 | end | |
296 | return startPos | |
297 | end | |
298 | ||
299 | --- Encodes a string to be JSON-compatible. | |
300 | -- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-) | |
301 | -- @param s The string to return as a JSON encoded (i.e. backquoted string) | |
302 | -- @return The string appropriately escaped. | |
303 | function encodeString(s) | |
304 | s = string.gsub(s,'\\','\\\\') | |
305 | s = string.gsub(s,'"','\\"') | |
306 | s = string.gsub(s,"'","\\'") | |
307 | s = string.gsub(s,'\n','\\n') | |
308 | s = string.gsub(s,'\t','\\t') | |
309 | return s | |
310 | end | |
311 | ||
312 | -- Determines whether the given Lua type is an array or a table / dictionary. | |
313 | -- We consider any table an array if it has indexes 1..n for its n items, and no | |
314 | -- other data in the table. | |
315 | -- I think this method is currently a little 'flaky', but can't think of a good way around it yet... | |
316 | -- @param t The table to evaluate as an array | |
317 | -- @return boolean, number True if the table can be represented as an array, false otherwise. If true, | |
318 | -- the second returned value is the maximum | |
319 | -- number of indexed elements in the array. | |
320 | function isArray(t) | |
321 | -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable | |
322 | -- (with the possible exception of 'n') | |
323 | local maxIndex = 0 | |
324 | for k,v in base.pairs(t) do | |
325 | if (base.type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair | |
326 | if (not isEncodable(v)) then return false end -- All array elements must be encodable | |
327 | maxIndex = math.max(maxIndex,k) | |
328 | else | |
329 | if (k=='n') then | |
330 | if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements | |
331 | else -- Else of (k=='n') | |
332 | if isEncodable(v) then return false end | |
333 | end -- End of (k~='n') | |
334 | end -- End of k,v not an indexed pair | |
335 | end -- End of loop across all pairs | |
336 | return true, maxIndex | |
337 | end | |
338 | ||
339 | --- Determines whether the given Lua object / table / variable can be JSON encoded. The only | |
340 | -- types that are JSON encodable are: string, boolean, number, nil, table and json.null. | |
341 | -- In this implementation, all other types are ignored. | |
342 | -- @param o The object to examine. | |
343 | -- @return boolean True if the object should be JSON encoded, false if it should be ignored. | |
344 | function isEncodable(o) | |
345 | local t = base.type(o) | |
346 | return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) | |
347 | end | |
348 | ]] | |
349 | ||
350 | function loadJSON() | |
351 | local sName = 'JSON' | |
352 | ||
353 | local tEnv = {} | |
354 | setmetatable( tEnv, { __index = _G } ) | |
355 | local fnAPI, err = loadstring(_jstr) | |
356 | if fnAPI then | |
357 | setfenv( fnAPI, tEnv ) | |
358 | fnAPI() | |
359 | else | |
360 | printError( err ) | |
361 | return false | |
362 | end | |
363 | ||
364 | local tAPI = {} | |
365 | for k,v in pairs( tEnv ) do | |
366 | tAPI[k] = v | |
367 | end | |
368 | ||
369 | _G[sName] = tAPI | |
370 | return true | |
371 | end | |
372 | ||
373 | local mainTitle = 'OneOS Installer' | |
374 | local subTitle = 'Please wait...' | |
375 | ||
376 | function Draw() | |
377 | sleep(0) | |
378 | term.setBackgroundColour(colours.white) | |
379 | term.clear() | |
380 | local w, h = term.getSize() | |
381 | term.setTextColour(colours.lightBlue) | |
382 | term.setCursorPos(math.ceil((w-#mainTitle)/2), 8) | |
383 | term.write(mainTitle) | |
384 | term.setTextColour(colours.blue) | |
385 | term.setCursorPos(math.ceil((w-#subTitle)/2), 10) | |
386 | term.write(subTitle) | |
387 | end | |
388 | ||
389 | tArgs = {...} | |
390 | ||
391 | Settings = { | |
392 | InstallPath = '/', --Where the program's installed, don't always asume root (if it's run under something like OneOS) | |
393 | Hidden = false, --Whether or not the update is hidden (doesn't write to the screen), useful for background updates | |
394 | GitHubUsername = 'oeed', --Your GitHub username as it appears in the URL | |
395 | GitHubRepoName = 'OneOS', --The repo name as it appears in the URL | |
396 | DownloadReleases = true, --If true it will download the latest release, otherwise it will download the files as they currently appear | |
397 | UpdateFunction = nil, --Sent when something happens (file downloaded etc.) | |
398 | TotalBytes = 0, --Do not change this value (especially programatically)! | |
399 | DownloadedBytes = 0, --Do not change this value (especially programatically)! | |
400 | Status = '', | |
401 | SecondaryStatus = '', | |
402 | } | |
403 | ||
404 | loadJSON() | |
405 | ||
406 | function downloadJSON(path) | |
407 | local _json = http.get(path) | |
408 | if not _json then | |
409 | error('Could not download: '..path..' Check your connection.') | |
410 | end | |
411 | return JSON.decode(_json.readAll()) | |
412 | end | |
413 | ||
414 | if http then | |
415 | subTitle = 'HTTP enabled, attempting update...' | |
416 | Draw() | |
417 | else | |
418 | subTitle = 'HTTP is required to update.' | |
419 | Draw() | |
420 | error('') | |
421 | end | |
422 | ||
423 | subTitle = 'Determining Latest Version' | |
424 | Draw() | |
425 | local releases = downloadJSON('https://api.github.com/repos/'..Settings.GitHubUsername..'/'..Settings.GitHubRepoName..'/releases') | |
426 | local latestReleaseTag = releases[1].tag_name | |
427 | if not tArgs or #tArgs ~= 1 and tArgs[1] ~= 'beta' then | |
428 | for i, v in ipairs(releases) do | |
429 | if not v.prerelease then | |
430 | latestReleaseTag = v.tag_name | |
431 | break | |
432 | end | |
433 | end | |
434 | end | |
435 | subTitle = 'Optaining Latest Version URL' | |
436 | Draw() | |
437 | local refs = downloadJSON('https://api.github.com/repos/'..Settings.GitHubUsername..'/'..Settings.GitHubRepoName..'/git/refs/tags/'..latestReleaseTag) | |
438 | local latestReleaseSha = refs.object.sha | |
439 | ||
440 | subTitle = 'Downloading File Listing' | |
441 | Draw() | |
442 | ||
443 | local tree = downloadJSON('https://api.github.com/repos/'..Settings.GitHubUsername..'/'..Settings.GitHubRepoName..'/git/trees/'..latestReleaseSha..'?recursive=1').tree | |
444 | ||
445 | local blacklist = { | |
446 | '/.gitignore', | |
447 | '/README.md', | |
448 | '/TODO', | |
449 | '/Desktop/.Desktop.settings', | |
450 | '/.version' | |
451 | } | |
452 | ||
453 | function isBlacklisted(path) | |
454 | for i, item in ipairs(blacklist) do | |
455 | if item == path then | |
456 | return true | |
457 | end | |
458 | end | |
459 | return false | |
460 | end | |
461 | ||
462 | Settings.TotalFiles = 0 | |
463 | Settings.TotalBytes = 0 | |
464 | for i, v in ipairs(tree) do | |
465 | if not isBlacklisted(Settings.InstallPath..v.path) and v.size then | |
466 | Settings.TotalBytes = Settings.TotalBytes + v.size | |
467 | Settings.TotalFiles = Settings.TotalFiles + 1 | |
468 | end | |
469 | end | |
470 | ||
471 | Settings.DownloadedBytes = 0 | |
472 | Settings.DownloadedFiles = 0 | |
473 | function downloadBlob(v) | |
474 | if isBlacklisted(Settings.InstallPath..v.path) then | |
475 | return | |
476 | end | |
477 | if v.type == 'tree' then | |
478 | -- subTitle = 'Making folder: '..'/'..Settings.InstallPath..v.path | |
479 | Draw() | |
480 | fs.makeDir('/'..Settings.InstallPath..v.path) | |
481 | else | |
482 | -- subTitle = 'Starting download for: '..Settings.InstallPath..v.path | |
483 | Draw() | |
484 | ||
485 | local tries, f = 0 | |
486 | - | repeat |
486 | + | repeat |
487 | - | f = http.get(('https://raw.github.com/'..Settings.GitHubUsername..'/'..Settings.GitHubRepoName..'/'..latestReleaseTag..Settings.InstallPath..v.path):gsub(' ','%%20')) |
487 | + | local url = 'https://raw.github.com/'..Settings.GitHubUsername..'/'..Settings.GitHubRepoName..'/'..latestReleaseTag..Settings.InstallPath..v.path |
488 | f = http.get(url:gsub(' ','%%20'), nil) | |
489 | if not f then sleep(5) end | |
490 | tries = tries + 1 | |
491 | until tries > 5 or f | |
492 | ||
493 | if not f then | |
494 | error('Downloading failed, try again. '..('https://raw.github.com/'..Settings.GitHubUsername..'/'..Settings.GitHubRepoName..'/'..latestReleaseTag..Settings.InstallPath..v.path):gsub(' ','%%20')) | |
495 | end | |
496 | ||
497 | local h = fs.open('/'..Settings.InstallPath..v.path, 'w') | |
498 | h.write(f.readAll()) | |
499 | h.close() | |
500 | -- subTitle = 'Downloading: ' .. math.floor(100*(Settings.DownloadedBytes/Settings.TotalBytes))..'%' | |
501 | subTitle = 'Downloading: ' .. math.floor(100*(Settings.DownloadedFiles/Settings.TotalFiles))..'%' -- using the number of files over the number of bytes actually appears to be more accurate, the connection takes longer than sending the data | |
502 | -- subTitle = '('..math.floor(100*(Settings.DownloadedBytes/Settings.TotalBytes))..'%) Downloaded: '..Settings.InstallPath..v.path | |
503 | Draw() | |
504 | if v.size then | |
505 | Settings.DownloadedBytes = Settings.DownloadedBytes + v.size | |
506 | Settings.DownloadedFiles = Settings.DownloadedFiles + 1 | |
507 | end | |
508 | end | |
509 | end | |
510 | ||
511 | local connectionLimit = 5 | |
512 | local downloads = {} | |
513 | for i, v in ipairs(tree) do | |
514 | local queueNumber = math.ceil(i / connectionLimit) | |
515 | if not downloads[queueNumber] then | |
516 | downloads[queueNumber] = {} | |
517 | end | |
518 | table.insert(downloads[queueNumber], function() | |
519 | downloadBlob(v) | |
520 | end) | |
521 | end | |
522 | ||
523 | for i, queue in ipairs(downloads) do | |
524 | parallel.waitForAll(unpack(queue)) | |
525 | end | |
526 | ||
527 | local h = fs.open('/System/.version', 'w') | |
528 | h.write(latestReleaseTag) | |
529 | h.close() | |
530 | ||
531 | mainTitle = 'Installation Complete!' | |
532 | subTitle = 'Rebooting in 1 second...' | |
533 | Draw() | |
534 | sleep(1) | |
535 | os.reboot() |