-- -- ComputerCraft Mekanism SCADA System Installer Utility -- --[[ Copyright © 2023 Mikayla Fischler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]]-- local function println(message) print(tostring(message)) end local function print(message) term.write(tostring(message)) end local VERSION = "v0.3" local install_dir = "/.install-cache" local repo_path = "http://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/devel/" local install_manifest = repo_path .. "install_manifest.json" local opts = { ... } local mode = nil local app = nil -- -- get and validate command line options -- println("-- CC Mekanism SCADA Installer " .. VERSION .. " --") if #opts == 0 or opts[1] == "help" or #opts ~= 2 then println("note: only modifies files that are part of the device application") println("usage: installer ") println("") println(" install - fresh install, overwrites config") println(" update - update files EXCEPT for config/logs") println(" remove - delete files EXCEPT for config/logs") println(" purge - delete files INCLUDING config/logs") println("") println(" reactor-plc - reactor PLC firmware") println(" rtu - RTU firmware") println(" supervisor - supervisor server application") println(" coordinator - coordinator application") println(" pocket - pocket application") return else for _, v in pairs({ "install", "update", "remove", "purge" }) do if opts[1] == v then mode = v break end end if mode == nil then println("unrecognized mode") return end for _, v in pairs({ "reactor-plc", "rtu", "supervisor", "coordinator", "pocket" }) do if opts[2] == v then app = v break end end if app == nil then println("unrecognized application") return end end -- -- run selected mode -- if mode == "install" or mode == "update" then ------------------------- -- GET REMOTE MANIFEST -- ------------------------- local response, error = http.get(install_manifest) if response == nil then println("failed to get installation manifest from GitHub, cannot update or install") println("http error " .. error) return end local ok, manifest = pcall(function () return textutils.unserializeJSON(response.readAll()) end) if not ok then println("error parsing remote version manifest") return end ------------------------ -- GET LOCAL MANIFEST -- ------------------------ local imfile = fs.open("install_manifest.json", "r") local local_ok = false local local_manifest = {} if imfile ~= nil then local_ok, local_manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end) imfile.close() end local local_app_version = nil local local_comms_version = nil local local_boot_version = nil if not local_ok and mode == "update" then term.setTextColor(colors.yellow) println("warning: failed to load local installation information") term.setTextColor(colors.white) local_app_version = local_manifest.versions[app] local_comms_version = local_manifest.versions.comms local_boot_version = local_manifest.versions.bootloader end local remote_app_version = manifest.versions[app] local remote_comms_version = manifest.versions.comms local remote_boot_version = manifest.versions.bootloader term.setTextColor(colors.green) if mode == "install" then println("installing " .. app .. " files...") elseif mode == "update" then println("updating " .. app .. " files... (keeping old config.lua)") end term.setTextColor(colors.white) if local_boot_version ~= nil then if local_boot_version ~= remote_boot_version then print("[bootldr] updating ") term.setTextColor(colors.blue) print(local_boot_version) term.setTextColor(colors.white) print(" => ") term.setTextColor(colors.blue) println(remote_boot_version) term.setTextColor(colors.white) end else println("[bootldr] new install of ") term.setTextColor(colors.blue) println(remote_boot_version) term.setTextColor(colors.white) end if local_comms_version ~= nil then if local_comms_version ~= remote_comms_version then print("[comms] updating ") term.setTextColor(colors.blue) print(local_comms_version) term.setTextColor(colors.white) print(" => ") term.setTextColor(colors.blue) println(remote_comms_version) term.setTextColor(colors.white) print("[comms] ") term.setTextColor(colors.yellow) println("other devices on the network will require an update") term.setTextColor(colors.white) end else println("[comms] new install of ") term.setTextColor(colors.blue) println(remote_comms_version) term.setTextColor(colors.white) end if local_app_version ~= nil then if local_app_version ~= remote_app_version then println("[" .. app .. "] updating " .. local_app_version .. " => " .. remote_app_version) end else println("[" .. app .. "] fresh install of " .. remote_app_version) end -------------------------- -- START INSTALL/UPDATE -- -------------------------- local space_required = 0 local space_available = fs.getFreeSpace("/") local single_file_mode = false local file_list = manifest.files local size_list = manifest.sizes local dependencies = manifest.depends[app] local config_file = app .. "/config.lua" table.insert(dependencies, app) for _, dependency in pairs(dependencies) do local size = size_list[dependency] space_required = space_required + size end if space_available < space_required then single_file_mode = true term.setTextColor(colors.red) println("WARNING: Insuffienct space available for a full download!") term.setTextColor(colors.white) println("Files will 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)") local confirm = read() if confirm ~= "y" or confirm ~= "Y" then println("installation cancelled") return end end ---@diagnostic disable-next-line: undefined-field os.sleep(2) local success = true if not single_file_mode then if fs.exists(install_dir) then fs.delete(install_dir) fs.makeDir(install_dir) end term.setTextColor(colors.lightGray) for _, dependency in pairs(dependencies) do local files = file_list[dependency] for _, file in pairs(files) do println("get: " .. file) local dl, err_c = http.get(repo_path .. file) if dl == nil then term.setTextColor(colors.red) println("get: error " .. err_c) success = false break else local handle = fs.open(install_dir .. "/" .. file, "w") handle.write(dl.readAll()) handle.close() end end end if success then for _, dependency in pairs(dependencies) do local files = file_list[dependency] for _, file in pairs(files) do if mode == "install" or file ~= config_file then fs.move(install_dir .. "/" .. file, file) end end end end fs.delete(install_dir) if success then term.setTextColor(colors.green) -- if we made it here, then none of the file system functions threw exceptions -- that means everything is OK if mode == "install" then println("installation completed successfully") else println("update completed successfully") end else if mode == "install" then term.setTextColor(colors.red) println("installation failed") else term.setTextColor(colors.orange) println("update failed, existing files unmodified") end end else for _, dependency in pairs(dependencies) do local files = file_list[dependency] for _, file in pairs(files) do println("get: " .. file) local dl, err_c = http.get(repo_path .. file) if dl == nil then println("get: error " .. err_c) success = false break else local handle = fs.open("/" .. file, "w") handle.write(dl.readAll()) handle.close() end end end if success then -- if we made it here, then none of the file system functions threw exceptions -- that means everything is OK term.setTextColor(colors.green) if mode == "install" then println("installation completed successfully") else println("update completed successfully") end else term.setTextColor(colors.red) if mode == "install" then println("installation failed, files may have been skipped") else println("update failed, files may have been skipped") end end end elseif mode == "remove" or mode == "purge" then local imfile = fs.open("install_manifest.json") local ok, manifest = pcall(function () return textutils.unserializeJSON(imfile.readAll()) end) imfile.close() if not ok then term.setTextColor(colors.red) println("error parsing local version manifest") term.setTextColor(colors.white) return end term.setTextColor(colors.orange) if mode == "remove" then println("removing all " .. app .. " files except for config.lua and log.txt...") elseif mode == "purge" then println("purging all " .. app .. " files including config.lua and log.txt...") end ---@diagnostic disable-next-line: undefined-field os.sleep(2) local file_list = manifest.files local dependencies = manifest.depends[app] local config_file = app .. "/config.lua" table.insert(dependencies, app) term.setTextColor(colors.lightGray) -- delete all files except config unless purging for _, dependency in pairs(dependencies) do local files = file_list[dependency] for _, file in pairs(files) do if mode == "purge" or file ~= config_file then fs.delete(file) println("deleted " .. file) end end end -- delete log file if purging if mode == "purge" then println("deleting log file '" .. config_file .. "'...") local config = require(config_file) fs.delete(config.LOG_PATH) println("deleted " .. config.LOG_PATH) end term.setTextColor(colors.green) println("done!") end term.setTextColor(colors.white)