· 5 years ago · Feb 27, 2021, 08:14 AM
1--[[
2 BS - Better Startup - 2013 Sangar
3
4 This program is licensed under the MIT license.
5 http://opensource.org/licenses/mit-license.php
6
7 This startup script allows for multiple startup scripts. Just drop them in
8 the folder as configured in the startupPath variable (default: /autorun).
9 All scripts in that folder will be run in sequence, in order of their names
10 when private.run() is called. Alternatively use the API to modifiy scripts.
11 Startup scripts can be disabled without having to delete/move them by
12 simply adding an extension to the filename as configured in the
13 disabledPostfix variable (default: disabled). This can also be done via
14 the API.
15]]
16
17-------------------------------------------------------------------------------
18-- Config --
19-------------------------------------------------------------------------------
20
21-- The path to the folder containing all startup scripts.
22local startupPath = "/autorun"
23
24-- The extension startup and daemon scripts have that should be ignored.
25local disabledPostfix = "disabled"
26
27-- The time in seconds to wait for a user to chose whether to cancel startup or
28-- not, before automatically resuming. It set to zero startup will resume
29-- instantly, if set to a negative value startup will always be aborted and if
30-- set to math.huge will wait indefinitely for input (and not show a timer).
31local errorTimeout = 5
32
33-------------------------------------------------------------------------------
34-------------------------------------------------------------------------------
35-- Startup API --
36-------------------------------------------------------------------------------
37-------------------------------------------------------------------------------
38
39-- If this API was loaded before, reuse it to avoid unnecessary garbage.
40if startup then
41 local env = getfenv()
42 for k, v in pairs(startup) do
43 env[k] = v
44 end
45 return
46end
47
48-- Internal forward declarations. They have to be declared here so that
49-- functions can access them.
50local private
51
52-------------------------------------------------------------------------------
53-- Public API --
54-------------------------------------------------------------------------------
55
56-- The current version of the API.
57version = "1.3"
58
59--[[
60 Adds a new startup script with the specified priority by copying the
61 specified file into the startup folder.
62
63 @param name the name for the script.
64 @param priority the startup priority of the script, an integer in [0, 99].
65 @param path the path to the file to copy.
66 @return true if the script was installed successfully; (false, reason) if
67 there is no such file or a script with that name already exists.
68]]
69function addFile(name, priority, path)
70 assert(type(name) == "string" and name ~= "",
71 "'name' must be a non-empty string")
72 assert(type(path) == "string" and path ~= "",
73 "'path' must be a non-empty string")
74 priority = private.formatPriority(priority)
75
76 if not fs.exists(path) or fs.isDir(path) then
77 return false, "no such file"
78 end
79 if exists(name) then
80 return false, "script with that name already exists"
81 end
82
83 local scriptPath = fs.combine(startupPath, priority .. "_" .. name)
84 local file = assert(fs.open(scriptPath, "w"),
85 "Could not open startup script file for writing")
86 file.write(string.format("shell.run(%q)", path))
87 file.close()
88 return true
89end
90
91--[[
92 Adds a new startup script with the specified priority by creating a script
93 with the specified code in the startup folder.
94
95 @param name the name for the script.
96 @param priority the startup priority of the script, an integer in [0, 99].
97 @param code the code of the startup script.
98 @return true if the script was installed successfully; (false, reason) if a
99 script with that name already exists.
100]]
101function addString(name, priority, code)
102 assert(type(name) == "string" and name ~= "",
103 "'name' must be a non-empty string")
104 assert(type(code) == "string" and code ~= "",
105 "'code' must be a non-empty string")
106 priority = private.formatPriority(priority)
107
108 if exists(name) then
109 return false, "script with that name already exists"
110 end
111
112 local path = fs.combine(startupPath, priority .. "_" .. name)
113 local file = assert(fs.open(path, "w"),
114 "Could not open file '" .. path .. "' for writing.")
115 file.write(code)
116 file.close()
117 return true
118end
119
120--[[
121 Checks whether a script with the specified name exists.
122
123 @param name the name of the script to check for.
124 @return true if such a script exists, enabled or disabled; false otherwise.
125]]
126function exists(name)
127 assert(type(name) == "string" and name ~= "",
128 "'name' must be a non-empty string")
129 return select(1, private.find(name))
130end
131
132--[[
133 Remove the startup script with the specified name.
134
135 IMPORTANT: this deletes the actual file from the system.
136
137 @param name the name of the script to remove.
138 @return true if the script was removed; false otherwise.
139]]
140function remove(name)
141 if not exists(name) then
142 return false
143 end
144 fs.delete(scriptPath(name))
145 return true
146end
147
148--[[
149 Checks whether startup scripts can be disabled without being deleted.
150
151 @return true if scripts can be disabled/enabled; false otherwise.
152]]
153function canDisableScripts()
154 return disabledPostfix and disabledPostfix ~= ""
155end
156
157--[[
158 Enables the startup script with the specified name.
159
160 @param name the name of the startup script.
161 @return true if the script is or was enabled; false otherwise.
162]]
163function enable(name)
164 if not canDisableScripts() or not exists(name) then
165 return false
166 end
167 if not isEnabled(name) then
168 local priority = getPriority(name)
169 local pathEnabled = fs.combine(startupPath, priority .. "_" .. name)
170 local pathDisabled = pathEnabled .. "." .. disabledPostfix
171 if fs.exists(pathEnabled) then
172 fs.delete(pathEnabled)
173 end
174 private.checkedMove(pathDisabled, pathEnabled)
175 end
176 return true
177end
178
179--[[
180 Disables the startup script with the specified name.
181
182 @param name the name of the startup script.
183 @return true if the script is or was disabled; false otherwise.
184]]
185function disable(name)
186 if not canDisableScripts() or not exists(name) then
187 return false
188 end
189 if isEnabled(name) then
190 local priority = getPriority(name)
191 local pathEnabled = fs.combine(startupPath, priority .. "_" .. name)
192 local pathDisabled = pathEnabled .. "." .. disabledPostfix
193 if fs.exists(pathDisabled) then
194 fs.delete(pathDisabled)
195 end
196 private.checkedMove(pathEnabled, pathDisabled)
197 end
198 return true
199end
200
201--[[
202 Tests whether the startup script with the specified name is enabled.
203
204 @param name the name of the script to check.
205 @return true if the script is enabled; false otherwise.
206]]
207function isEnabled(name)
208 assert(type(name) == "string" and name ~= "",
209 "'name' must be a non-empty string")
210 local success, _, enabled = private.find(name)
211 return success and enabled
212end
213
214--[[
215 Returns the current priority of the script with the specified name.
216
217 @param name the name of the script.
218 @return the priority of the script; math.huge if there is no such script.
219]]
220function getPriority(name)
221 if not exists(name) then
222 return math.huge
223 end
224 local _, priority, _ = private.find(name)
225 return tonumber(priority)
226end
227
228--[[
229 Sets the new priority for the script with the specified name.
230
231 @param name the name of the script.
232 @param priority the new priority, an integer in [0, 99]
233 @return true if the priority is or was applied; false otherwise.
234]]
235function setPriority(name, priority)
236 priority = private.formatPriority(priority)
237 if not exists(name) then
238 return false
239 end
240 local currentPriority = getPriority(name)
241 local oldPath = fs.combine(startupPath, currentPriority .. "_" .. name)
242 local newPath = fs.combine(startupPath, priority .. "_" .. name)
243 if not isEnabled(name) then
244 oldPath = oldPath .. "." .. disabledPostfix
245 newPath = newPath .. "." .. disabledPostfix
246 end
247 private.checkedMove(oldPath, newPath)
248 return true
249end
250
251--[[
252 Returns an iterator function over all startup scripts.
253
254 This will include any known scripts, whether they're enabled or not does
255 not make a difference.
256
257 Usage: for name in startup.iter() do ... end
258
259 @return an iterator over all known scripts.
260]]
261function iter()
262 local list = fs.list(startupPath)
263 table.sort(list)
264 local index
265 return function()
266 local name
267 index, name = next(list, index)
268 if name and name:match("^%d%d_.+$") then
269 name = name:sub(4)
270 if canDisableScripts() then
271 return select(1, name:gsub("%." .. disabledPostfix .. "$", ""))
272 else
273 return name
274 end
275 end
276 end
277end
278
279--[[
280 Returns the path to the startup script with the specified alias.
281
282 This can be useful for backups and automatic installer scripts (for
283 example, JAM uses this for replicating a system environment onto a floppy
284 disk and back again).
285
286 @param name the name of the script to get the path to.
287 @return the path to the script file.
288]]
289function scriptPath(name)
290 assert(exists(name), "no such script")
291 local _, priority, enabled = private.find(name)
292 local path = fs.combine(startupPath, priority .. "_" .. name)
293 if not enabled then
294 path = path .. "." .. disabledPostfix
295 end
296 return path
297end
298
299--[[
300 Runs all startup scripts in the startup scripts folder in alphabetic order.
301
302 Note that this will print status and error messages to the screen.
303
304 @return true if all scripts ran successfully; false otherwise (this
305 includes the case where remaining scripts ran either because we're
306 configured to continue regardless, or because the user said it's OK).
307]]
308function run()
309 -- Get list of startup scripts.
310 local list = fs.list(startupPath)
311 -- Make sure the list is sorted.
312 table.sort(list)
313 -- Run all startup scripts.
314 local success = true
315 for _, name in ipairs(list) do
316 local path = fs.combine(startupPath, name)
317 if private.shouldRun(path) then
318 print("> " .. path)
319 -- Try to load the script.
320 local result, message = loadfile(path)
321 if result then
322 -- Give each script its own environment so they do not
323 -- interfere with each other, then do a protected call.
324 setfenv(result, setmetatable({}, {__index = getfenv(2)}))
325 result, message = pcall(result)
326 end
327 -- Check if we had an error, if so, depending on our configuration,
328 -- give the user chance to decide whether to go on or not, or
329 -- decide for ourselves what to do.
330 if not result then
331 success = false
332 printError(message)
333 if errorTimeout < 0 then
334 print("Skipping remaining scripts.")
335 break
336 elseif errorTimeout ~= 0 then
337 print("Resume startup?")
338 if not private.prompt() then
339 break
340 end
341 end
342 end
343 end
344 end
345 return success
346end
347
348-------------------------------------------------------------------------------
349-------------------------------------------------------------------------------
350-- Internals --
351-------------------------------------------------------------------------------
352-------------------------------------------------------------------------------
353
354-- Private namespace.
355private = {}
356
357--[[
358 Utility function for validating and formatting priorities.
359
360 @param priority the input priority.
361 @return the priority as a formatted string.
362]]
363function private.formatPriority(priority)
364 assert(type(priority) == "number", "'priority' must be a number")
365 assert(priority == math.floor(priority), "'priority' must be integral")
366 assert(priority >= 0 and priority <= 99,
367 "'priority' must be in the interval [0, 99]")
368 return string.format("%2d", priority)
369end
370
371--[[
372 Utility function for finding an existing startup script.
373
374 @param name the name of the script.
375 @return (true, priority, enabled) on success; false otherwise.
376]]
377function private.find(name)
378 local pattern = "^(%d%d)_(.+)$"
379 -- Check all files. Make sure the list is sorted, just in case someone got
380 -- the crazy idea to put in two scripts with the same "name", so that we
381 -- have deterministic behavior.
382 local list = fs.exists(startupPath) and fs.list(startupPath) or {}
383 table.sort(list)
384 for _, fileName in ipairs(list) do
385 local path = fs.combine(startupPath, fileName)
386 if not fs.isDir(path) then
387 -- If we allow disabling scripts, check for disabled scripts first
388 -- because the "pattern" is more selective (we cut off the postfix).
389 if canDisableScripts() then
390 -- Determine the end of the string without the extension.
391 local length = fileName:len() - (disabledPostfix:len() + 1)
392 -- Make sure it's not negative, because that has a special
393 -- meaning for the string.sub() function (start from the back).
394 length = math.max(0, length)
395 local priority, scriptName =
396 fileName:sub(1, length):match(pattern)
397 if scriptName == name then
398 return true, priority, false
399 end
400 end
401 do
402 local priority, scriptName = fileName:match(pattern)
403 if scriptName == name then
404 return true, priority, true
405 end
406 end
407 end
408 end
409 return false
410end
411
412--[[
413 Utility function for checking whether a startup script should be run.
414
415 @param the path to the script.
416 @return true if the script is a file and not disabled; false otherwise.
417]]
418function private.shouldRun(path)
419 if fs.isDir(path) then
420 return false
421 end
422 if not fs.getName(path):match("^%d%d_.+$") then
423 return false
424 end
425 if disabledPostfix and disabledPostfix ~= "" then
426 return path:sub(-disabledPostfix:len()) ~= disabledPostfix
427 end
428 return true
429end
430
431--[[
432 Performs a fs.move() but checks if there's enough disk space first and
433 throws a more meaningful error than "could not copy file" if not.
434
435 @param from the path to move the file from.
436 @param to the path to move the file to.
437]]
438function private.checkedMove(from, to)
439 local toSize = math.max(512, fs.getSize(from) + fs.getName(to):len())
440 assert(fs.getFreeSpace(to) >= toSize, "Out of disk space!")
441 fs.move(from, to)
442end
443
444--[[
445 Utility function waiting for the user to press a key or a timeout.
446
447 @return true if the prompt was positively confirmed; false otherwise.
448]]
449function private.prompt()
450 write("> [Y/n] ")
451 term.setCursorBlink(true)
452 local countdown = errorTimeout
453 local update = nil
454 while true do
455 if countdown ~= math.huge then
456 update = update or os.startTimer(1)
457 write(countdown)
458 end
459 local type, arg = os.pullEvent()
460 if countdown ~= math.huge then
461 local x, y = term.getCursorPos()
462 term.setCursorPos(x - 1, y)
463 end
464 if type == "key" and arg == keys.y or arg == keys.enter then
465 -- Return success if the prompt was confirmed.
466 os.sleep(0.1)
467 print("y")
468 term.setCursorBlink(false)
469 return true
470 elseif type == "key" and arg == keys.n then
471 -- Return failure if the prompt was denied.
472 os.sleep(0.1)
473 print("n")
474 term.setCursorBlink(false)
475 return false
476 elseif type == "key" then
477 -- Stop the countdown if any other key was pressed.
478 if countdown ~= math.huge then
479 -- Erase the countdown number.
480 local x, y = term.getCursorPos()
481 write((" "):rep(tostring(countdown):len()))
482 term.setCursorPos(x, y)
483 end
484 countdown = math.huge
485 elseif type == "timer" and arg == update then
486 -- Our regular timer update to update the displayed countdown.
487 countdown = countdown - 1
488 update = nil
489 if countdown == 0 then
490 print("y")
491 term.setCursorBlink(false)
492 return true
493 end
494 end
495 end
496end
497
498-------------------------------------------------------------------------------
499-- Environment checking --
500-------------------------------------------------------------------------------
501
502-- Ensure we have a path set for our startup files.
503assert(type(startupPath) == "string" and startupPath ~= "",
504 "The setting 'startupPath' must be a non-empty string.")
505
506-- Ensure the folder with startup files exists.
507assert(not fs.exists(startupPath) or fs.isDir(startupPath),
508 "Folder for startup scripts cannot be created because a file " ..
509 "with that name already exists ('" .. startupPath .. "').")
510
511-- Ensure our timeout is a number.
512assert(type(errorTimeout) == "number",
513 "The setting 'errorTimeout' must be a number.")
514
515-------------------------------------------------------------------------------
516-- Initialization --
517-------------------------------------------------------------------------------
518
519-- Create folder if it doesn't exist.
520fs.makeDir(startupPath)