Merge branch 'devel' into 51-hmac-message-authentication

This commit is contained in:
Mikayla 2023-06-18 19:23:56 +00:00
commit 282c7db3eb
8 changed files with 167 additions and 160 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
_notes/ _notes/
program.sh /*program.sh

289
ccmsi.lua
View File

@ -20,7 +20,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
local function println(message) print(tostring(message)) end local function println(message) print(tostring(message)) end
local function print(message) term.write(tostring(message)) end local function print(message) term.write(tostring(message)) end
local CCMSI_VERSION = "v1.2" local CCMSI_VERSION = "v1.4f"
local install_dir = "/.install-cache" local install_dir = "/.install-cache"
local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/" local manifest_path = "https://mikaylafischler.github.io/cc-mek-scada/manifests/"
@ -29,6 +29,7 @@ local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada
local opts = { ... } local opts = { ... }
local mode = nil local mode = nil
local app = nil local app = nil
local target
-- record the local installation manifest -- record the local installation manifest
---@param manifest table ---@param manifest table
@ -38,7 +39,7 @@ local function write_install_manifest(manifest, dependencies)
for key, value in pairs(manifest.versions) do for key, value in pairs(manifest.versions) do
local is_dependency = false local is_dependency = false
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if key == "bootloader" and dependency == "system" then if (key == "bootloader" and dependency == "system") or key == dependency then
is_dependency = true is_dependency = true
break break
end end
@ -54,6 +55,68 @@ local function write_install_manifest(manifest, dependencies)
imfile.close() imfile.close()
end end
-- ask the user yes or no
---@nodiscard
---@param question string
---@param default boolean
---@return boolean|nil
local function ask_y_n(question, default)
print(question)
if default == true then
print(" (Y/n)? ")
else
print(" (y/N)? ")
end
local response = read(nil, nil)
if response == "" then
return default
elseif response == "Y" or response == "y" then
return true
elseif response == "N" or response == "n" then
return false
else
return nil
end
end
-- print out a white + blue text message<br>
-- automatically adds a space
---@param message string message
---@param package string dependency/package/version
local function pkg_message(message, package)
term.setTextColor(colors.white)
print(message .. " ")
term.setTextColor(colors.blue)
println(package)
term.setTextColor(colors.white)
end
-- indicate actions to be taken based on package differences for installs/updates
---@param name string package name
---@param v_local string|nil local version
---@param v_remote string remote version
local function show_pkg_change(name, v_local, v_remote)
if v_local ~= nil then
if v_local ~= v_remote then
print("[" .. name .. "] updating ")
term.setTextColor(colors.blue)
print(v_local)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(v_local)
term.setTextColor(colors.white)
elseif mode == "install" then
pkg_message("[" .. name .. "] reinstalling", v_local)
end
else
pkg_message("[" .. name .. "] new install of", v_remote)
end
end
-- --
-- get and validate command line options -- get and validate command line options
-- --
@ -61,12 +124,12 @@ end
println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --") println("-- CC Mekanism SCADA Installer " .. CCMSI_VERSION .. " --")
if #opts == 0 or opts[1] == "help" then if #opts == 0 or opts[1] == "help" then
println("usage: ccmsi <mode> <app> <tag/branch>") println("usage: ccmsi <mode> <app> <branch>")
println("<mode>") println("<mode>")
term.setTextColor(colors.lightGray) term.setTextColor(colors.lightGray)
println(" check - check latest versions avilable") println(" check - check latest versions avilable")
term.setTextColor(colors.yellow) term.setTextColor(colors.yellow)
println(" ccmsi check <tag/branch> for target") println(" ccmsi check <branch> for target")
term.setTextColor(colors.lightGray) term.setTextColor(colors.lightGray)
println(" install - fresh install, overwrites config") println(" install - fresh install, overwrites config")
println(" update - update files EXCEPT for config/logs") println(" update - update files EXCEPT for config/logs")
@ -81,12 +144,11 @@ if #opts == 0 or opts[1] == "help" then
println(" coordinator - coordinator application") println(" coordinator - coordinator application")
println(" pocket - pocket application") println(" pocket - pocket application")
term.setTextColor(colors.white) term.setTextColor(colors.white)
println("<tag/branch>") println("<branch>")
term.setTextColor(colors.yellow) term.setTextColor(colors.yellow)
println(" second parameter when used with check") println(" second parameter when used with check")
term.setTextColor(colors.lightGray) term.setTextColor(colors.lightGray)
println(" note: defaults to main") println(" main (default) | latest | devel")
println(" target GitHub tag or branch name")
return return
else else
for _, v in pairs({ "check", "install", "update", "remove", "purge" }) do for _, v in pairs({ "check", "install", "update", "remove", "purge" }) do
@ -112,6 +174,13 @@ else
println("unrecognized application") println("unrecognized application")
return return
end end
-- determine target
if mode == "check" then target = opts[2] else target = opts[3] end
if (target ~= "main") and (target ~= "latest") and (target ~= "devel") then
target = "main"
println("unknown target, defaulting to 'main'")
end
end end
-- --
@ -123,7 +192,7 @@ if mode == "check" then
-- GET REMOTE MANIFEST -- -- GET REMOTE MANIFEST --
------------------------- -------------------------
if opts[2] then manifest_path = manifest_path .. opts[2] .. "/" else manifest_path = manifest_path .. "main/" end manifest_path = manifest_path .. target .. "/"
local install_manifest = manifest_path .. "install_manifest.json" local install_manifest = manifest_path .. "install_manifest.json"
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)
@ -203,8 +272,8 @@ elseif mode == "install" or mode == "update" then
-- GET REMOTE MANIFEST -- -- GET REMOTE MANIFEST --
------------------------- -------------------------
if opts[3] then repo_path = repo_path .. opts[3] .. "/" else repo_path = repo_path .. "main/" end repo_path = repo_path .. target .. "/"
if opts[3] then manifest_path = manifest_path .. opts[3] .. "/" else manifest_path = manifest_path .. "main/" end manifest_path = manifest_path .. target .. "/"
local install_manifest = manifest_path .. "install_manifest.json" local install_manifest = manifest_path .. "install_manifest.json"
local response, error = http.get(install_manifest) local response, error = http.get(install_manifest)
@ -230,6 +299,13 @@ elseif mode == "install" or mode == "update" then
-- GET LOCAL MANIFEST -- -- GET LOCAL MANIFEST --
------------------------ ------------------------
local ver = {
app = { v_local = nil, v_remote = nil, changed = false },
boot = { v_local = nil, v_remote = nil, changed = false },
comms = { v_local = nil, v_remote = nil, changed = false },
graphics = { v_local = nil, v_remote = nil, changed = false }
}
local imfile = fs.open("install_manifest.json", "r") local imfile = fs.open("install_manifest.json", "r")
local local_ok = false local local_ok = false
local local_manifest = {} local local_manifest = {}
@ -239,10 +315,6 @@ elseif mode == "install" or mode == "update" then
imfile.close() imfile.close()
end end
local local_app_version = nil
local local_comms_version = nil
local local_boot_version = nil
-- try to find local versions -- try to find local versions
if not local_ok then if not local_ok then
if mode == "update" then if mode == "update" then
@ -252,9 +324,10 @@ elseif mode == "install" or mode == "update" then
return return
end end
else else
local_app_version = local_manifest.versions[app] ver.boot.v_local = local_manifest.versions.bootloader
local_comms_version = local_manifest.versions.comms ver.app.v_local = local_manifest.versions[app]
local_boot_version = local_manifest.versions.bootloader ver.comms.v_local = local_manifest.versions.comms
ver.graphics.v_local = local_manifest.versions.graphics
if local_manifest.versions[app] == nil then if local_manifest.versions[app] == nil then
term.setTextColor(colors.red) term.setTextColor(colors.red)
@ -271,94 +344,44 @@ elseif mode == "install" or mode == "update" then
end end
end end
local remote_app_version = manifest.versions[app] ver.boot.v_remote = manifest.versions.bootloader
local remote_comms_version = manifest.versions.comms ver.app.v_remote = manifest.versions[app]
local remote_boot_version = manifest.versions.bootloader ver.comms.v_remote = manifest.versions.comms
ver.graphics.v_remote = manifest.versions.graphics
term.setTextColor(colors.green) term.setTextColor(colors.green)
if mode == "install" then if mode == "install" then
println("installing " .. app .. " files...") println("Installing " .. app .. " files...")
elseif mode == "update" then elseif mode == "update" then
println("updating " .. app .. " files... (keeping old config.lua)") println("Updating " .. app .. " files... (keeping old config.lua)")
end end
term.setTextColor(colors.white) term.setTextColor(colors.white)
-- display bootloader version change information -- display bootloader version change information
if local_boot_version ~= nil then show_pkg_change("bootldr", ver.boot.v_local, ver.boot.v_remote)
if local_boot_version ~= remote_boot_version then ver.boot.changed = ver.boot.v_local ~= ver.boot.v_remote
print("[bootldr] updating ")
term.setTextColor(colors.blue)
print(local_boot_version)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(remote_boot_version)
term.setTextColor(colors.white)
elseif mode == "install" then
print("[bootldr] reinstalling ")
term.setTextColor(colors.blue)
println(local_boot_version)
term.setTextColor(colors.white)
end
else
print("[bootldr] new install of ")
term.setTextColor(colors.blue)
println(remote_boot_version)
term.setTextColor(colors.white)
end
-- display app version change information -- display app version change information
if local_app_version ~= nil then show_pkg_change(app, ver.app.v_local, ver.app.v_remote)
if local_app_version ~= remote_app_version then ver.app.changed = ver.app.v_local ~= ver.app.v_remote
print("[" .. app .. "] updating ")
term.setTextColor(colors.blue)
print(local_app_version)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(remote_app_version)
term.setTextColor(colors.white)
elseif mode == "install" then
print("[" .. app .. "] reinstalling ")
term.setTextColor(colors.blue)
println(local_app_version)
term.setTextColor(colors.white)
end
else
print("[" .. app .. "] new install of ")
term.setTextColor(colors.blue)
println(remote_app_version)
term.setTextColor(colors.white)
end
-- display comms version change information -- display comms version change information
if local_comms_version ~= nil then show_pkg_change("comms", ver.comms.v_local, ver.comms.v_remote)
if local_comms_version ~= remote_comms_version then ver.comms.changed = ver.comms.v_local ~= ver.comms.v_remote
print("[comms] updating ") if ver.comms.changed and ver.comms.v_local ~= nil then
term.setTextColor(colors.blue)
print(local_comms_version)
term.setTextColor(colors.white)
print(" \xbb ")
term.setTextColor(colors.blue)
println(remote_comms_version)
term.setTextColor(colors.white)
print("[comms] ") print("[comms] ")
term.setTextColor(colors.yellow) term.setTextColor(colors.yellow)
println("other devices on the network will require an update") println("other devices on the network will require an update")
term.setTextColor(colors.white) term.setTextColor(colors.white)
elseif mode == "install" then
print("[comms] reinstalling ")
term.setTextColor(colors.blue)
println(local_comms_version)
term.setTextColor(colors.white)
end
else
print("[comms] new install of ")
term.setTextColor(colors.blue)
println(remote_comms_version)
term.setTextColor(colors.white)
end end
-- display graphics version change information
show_pkg_change("graphics", ver.graphics.v_local, ver.graphics.v_remote)
ver.graphics.changed = ver.graphics.v_local ~= ver.graphics.v_remote
-- ask for confirmation
if not ask_y_n("Continue?", false) then return end
-------------------------- --------------------------
-- START INSTALL/UPDATE -- -- START INSTALL/UPDATE --
-------------------------- --------------------------
@ -386,20 +409,26 @@ elseif mode == "install" or mode == "update" then
println("WARNING: Insufficient space available for a full download!") println("WARNING: Insufficient space available for a full download!")
term.setTextColor(colors.white) term.setTextColor(colors.white)
println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.") println("Files can be downloaded one by one, so if you are replacing a current install this will not be a problem unless installation fails.")
println("Do you wish to continue? (y/N)") if mode == "update" then println("If installation still fails, delete this device's log file and try again.") end
if not ask_y_n("Do you wish to continue?", false) then
local confirm = read() println("Operation cancelled.")
if confirm ~= "y" and confirm ~= "Y" then
println("installation cancelled")
return return
end end
end end
---@diagnostic disable-next-line: undefined-field
os.sleep(2)
local success = true local success = true
-- helper function to check if a dependency is unchanged
---@nodiscard
---@param dependency string
---@return boolean
local function unchanged(dependency)
if dependency == "system" then return not ver.boot.changed
elseif dependency == "graphics" then return not ver.graphics.changed
elseif dependency == app then return not ver.app.changed
else return true end
end
if not single_file_mode then if not single_file_mode then
if fs.exists(install_dir) then if fs.exists(install_dir) then
fs.delete(install_dir) fs.delete(install_dir)
@ -408,20 +437,12 @@ elseif mode == "install" or mode == "update" then
-- download all dependencies -- download all dependencies
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if mode == "update" and ((dependency == "system" and local_boot_version == remote_boot_version) or (local_app_version == remote_app_version)) then if mode == "update" and unchanged(dependency) then
-- skip system package if unchanged, skip app package if not changed pkg_message("skipping download of unchanged package", dependency)
-- skip packages that have no version if app version didn't change
term.setTextColor(colors.white)
print("skipping download of unchanged package ")
term.setTextColor(colors.blue)
println(dependency)
else else
term.setTextColor(colors.white) pkg_message("downloading package", dependency)
print("downloading package ")
term.setTextColor(colors.blue)
println(dependency)
term.setTextColor(colors.lightGray) term.setTextColor(colors.lightGray)
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
println("GET " .. file) println("GET " .. file)
@ -444,20 +465,12 @@ elseif mode == "install" or mode == "update" then
-- copy in downloaded files (installation) -- copy in downloaded files (installation)
if success then if success then
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if mode == "update" and ((dependency == "system" and local_boot_version == remote_boot_version) or (local_app_version == remote_app_version)) then if mode == "update" and unchanged(dependency) then
-- skip system package if unchanged, skip app package if not changed pkg_message("skipping install of unchanged package", dependency)
-- skip packages that have no version if app version didn't change
term.setTextColor(colors.white)
print("skipping install of unchanged package ")
term.setTextColor(colors.blue)
println(dependency)
else else
term.setTextColor(colors.white) pkg_message("installing package", dependency)
print("installing package ")
term.setTextColor(colors.blue)
println(dependency)
term.setTextColor(colors.lightGray) term.setTextColor(colors.lightGray)
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "install" or file ~= config_file then if mode == "install" or file ~= config_file then
@ -478,36 +491,28 @@ elseif mode == "install" or mode == "update" then
write_install_manifest(manifest, dependencies) write_install_manifest(manifest, dependencies)
term.setTextColor(colors.green) term.setTextColor(colors.green)
if mode == "install" then if mode == "install" then
println("installation completed successfully") println("Installation completed successfully.")
else else
println("update completed successfully") println("Update completed successfully.")
end end
else else
if mode == "install" then if mode == "install" then
term.setTextColor(colors.red) term.setTextColor(colors.red)
println("installation failed") println("Installation failed.")
else else
term.setTextColor(colors.orange) term.setTextColor(colors.orange)
println("update failed, existing files unmodified") println("Update failed, existing files unmodified.")
end end
end end
else else
-- go through all files and replace one by one -- go through all files and replace one by one
for _, dependency in pairs(dependencies) do for _, dependency in pairs(dependencies) do
if mode == "update" and ((dependency == "system" and local_boot_version == remote_boot_version) or (local_app_version == remote_app_version)) then if mode == "update" and unchanged(dependency) then
-- skip system package if unchanged, skip app package if not changed pkg_message("skipping install of unchanged package", dependency)
-- skip packages that have no version if app version didn't change
term.setTextColor(colors.white)
print("skipping install of unchanged package ")
term.setTextColor(colors.blue)
println(dependency)
else else
term.setTextColor(colors.white) pkg_message("installing package", dependency)
print("installing package ")
term.setTextColor(colors.blue)
println(dependency)
term.setTextColor(colors.lightGray) term.setTextColor(colors.lightGray)
local files = file_list[dependency] local files = file_list[dependency]
for _, file in pairs(files) do for _, file in pairs(files) do
if mode == "install" or file ~= config_file then if mode == "install" or file ~= config_file then
@ -534,16 +539,16 @@ elseif mode == "install" or mode == "update" then
write_install_manifest(manifest, dependencies) write_install_manifest(manifest, dependencies)
term.setTextColor(colors.green) term.setTextColor(colors.green)
if mode == "install" then if mode == "install" then
println("installation completed successfully") println("Installation completed successfully.")
else else
println("update completed successfully") println("Update completed successfully.")
end end
else else
term.setTextColor(colors.red) term.setTextColor(colors.red)
if mode == "install" then if mode == "install" then
println("installation failed, files may have been skipped") println("Installation failed, files may have been skipped.")
else else
println("update failed, files may have been skipped") println("Update failed, files may have been skipped.")
end end
end end
end end
@ -576,8 +581,8 @@ elseif mode == "remove" or mode == "purge" then
println("purging all " .. app .. " files...") println("purging all " .. app .. " files...")
end end
---@diagnostic disable-next-line: undefined-field -- ask for confirmation
os.sleep(2) if not ask_y_n("Continue?", false) then return end
local file_list = manifest.files local file_list = manifest.files
local dependencies = manifest.depends[app] local dependencies = manifest.depends[app]
@ -666,7 +671,7 @@ elseif mode == "remove" or mode == "purge" then
end end
term.setTextColor(colors.green) term.setTextColor(colors.green)
println("done!") println("Done!")
end end
term.setTextColor(colors.white) term.setTextColor(colors.white)

View File

@ -7,6 +7,8 @@ local flasher = require("graphics.flasher")
local core = {} local core = {}
core.version = "1.0.0"
core.flasher = flasher core.flasher = flasher
core.events = events core.events = events

View File

@ -23,11 +23,11 @@ def dir_size(path):
return total return total
# get the version of an application at the provided path # get the version of an application at the provided path
def get_version(path, is_comms = False): def get_version(path, is_lib = False):
ver = "" ver = ""
string = "comms.version = \"" string = ".version = \""
if not is_comms: if not is_lib:
string = "_VERSION = \"" string = "_VERSION = \""
f = open(path, "r") f = open(path, "r")
@ -49,6 +49,7 @@ def make_manifest(size):
"installer" : get_version("./ccmsi.lua"), "installer" : get_version("./ccmsi.lua"),
"bootloader" : get_version("./startup.lua"), "bootloader" : get_version("./startup.lua"),
"comms" : get_version("./scada-common/comms.lua", True), "comms" : get_version("./scada-common/comms.lua", True),
"graphics" : get_version("./graphics/core.lua", True),
"reactor-plc" : get_version("./reactor-plc/startup.lua"), "reactor-plc" : get_version("./reactor-plc/startup.lua"),
"rtu" : get_version("./rtu/startup.lua"), "rtu" : get_version("./rtu/startup.lua"),
"supervisor" : get_version("./supervisor/startup.lua"), "supervisor" : get_version("./supervisor/startup.lua"),

File diff suppressed because one or more lines are too long

View File

@ -75,7 +75,7 @@ function facility.new(num_reactors, cooling_conf)
burn_target = 0.1, -- burn rate target for aggregate burn mode burn_target = 0.1, -- burn rate target for aggregate burn mode
charge_setpoint = 0, -- FE charge target setpoint charge_setpoint = 0, -- FE charge target setpoint
gen_rate_setpoint = 0, -- FE/t charge rate target setpoint gen_rate_setpoint = 0, -- FE/t charge rate target setpoint
group_map = { 0, 0, 0, 0 }, -- units -> group IDs group_map = {}, -- units -> group IDs
prio_defs = { {}, {}, {}, {} }, -- priority definitions (each level is a table of units) prio_defs = { {}, {}, {}, {} }, -- priority definitions (each level is a table of units)
at_max_burn = false, at_max_burn = false,
ascram = false, ascram = false,
@ -109,6 +109,7 @@ function facility.new(num_reactors, cooling_conf)
-- create units -- create units
for i = 1, num_reactors do for i = 1, num_reactors do
table.insert(self.units, unit.new(i, cooling_conf[i].BOILERS, cooling_conf[i].TURBINES)) table.insert(self.units, unit.new(i, cooling_conf[i].BOILERS, cooling_conf[i].TURBINES))
table.insert(self.group_map, 0)
end end
-- init redstone RTU I/O controller -- init redstone RTU I/O controller
@ -790,7 +791,7 @@ function facility.new(num_reactors, cooling_conf)
---@param unit_id integer unit ID ---@param unit_id integer unit ID
---@param group integer group ID or 0 for independent ---@param group integer group ID or 0 for independent
function public.set_group(unit_id, group) function public.set_group(unit_id, group)
if group >= 0 and group <= 4 and self.mode == PROCESS.INACTIVE then if (group >= 0 and group <= 4) and (unit_id > 0 and unit_id <= num_reactors) and self.mode == PROCESS.INACTIVE then
-- remove from old group if previously assigned -- remove from old group if previously assigned
local old_group = self.group_map[unit_id] local old_group = self.group_map[unit_id]
if old_group ~= 0 then if old_group ~= 0 then

View File

@ -120,7 +120,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
if u_type == false then if u_type == false then
-- validation fail -- validation fail
log.debug(log_header .. "advertisement unit validation failure") log.debug(log_header .. "_handle_advertisement(): advertisement unit validation failure")
else else
if unit_advert.reactor > 0 then if unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
@ -146,7 +146,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- skip virtual units -- skip virtual units
log.debug(util.c(log_header, "skipping virtual RTU unit #", i)) log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
else else
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string)) log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported reactor-specific RTU type ", type_string))
end end
else else
-- facility RTUs -- facility RTUs
@ -172,7 +172,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
-- skip virtual units -- skip virtual units
log.debug(util.c(log_header, "skipping virtual RTU unit #", i)) log.debug(util.c(log_header, "skipping virtual RTU unit #", i))
else else
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-independent RTU type ", type_string)) log.warning(util.c(log_header, "_handle_advertisement(): encountered unsupported facility RTU type ", type_string))
end end
end end
end end
@ -181,9 +181,7 @@ function rtu.new_session(id, s_addr, in_queue, out_queue, timeout, advertisement
self.units[i] = unit self.units[i] = unit
unit_count = unit_count + 1 unit_count = unit_count + 1
elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then elseif u_type ~= RTU_UNIT_TYPE.VIRTUAL then
_reset_config() log.warning(util.c(log_header, "_handle_advertisement(): problem occured while creating a unit (type is ", type_string, ")"))
log.error(util.c(log_header, "bad advertisement: error occured while creating a unit (type is ", type_string, ")"))
break
end end
end end

View File

@ -20,7 +20,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions") local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v0.17.7" local SUPERVISOR_VERSION = "v0.17.9"
local println = util.println local println = util.println
local println_ts = util.println_ts local println_ts = util.println_ts