MikaylaF

CCMSI Utility v2.2 (with self-update)

Aug 22nd, 2023 (edited)
18,007
1
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 25.06 KB | None | 1 0
  1. --[[
  2. CC-MEK-SCADA Installer Utility
  3.  
  4. Copyright (c) 2023 - 2026 Mikayla Fischler
  5.  
  6. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
  7. associated documentation files (the "Software"), to deal in the Software without restriction,
  8. including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
  9. and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
  10.  
  11. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
  12. LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
  13. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  14. WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  15. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  16. ]]--
  17.  
  18. local ccs = require("cc.strings")
  19.  
  20. local CCMSI_VERSION = "2.2"
  21.  
  22. local IS_PKT = pocket ~= nil -- luacheck: ignore pocket
  23.  
  24. local INSTALL_DIR = "/.install-cache"
  25. local DEPLOY_DIR = "https://mikaylafischler.github.io/cc-mek-scada/"
  26. local MANIFEST_DIR = DEPLOY_DIR.."manifests/"
  27. local BUILD_DIR = DEPLOY_DIR.."builds/"
  28.  
  29. local OPTS = { ... }
  30.  
  31. local mode, app, target, build_url, manifest_url
  32.  
  33. local out_w, out_h = term.getSize()
  34.  
  35. local function tsc(c) term.setTextColor(c) end
  36. local function tbc(c) term.setBackgroundColor(c) end
  37.  
  38. local function red() tsc(colors.red) end
  39. local function orange() tsc(colors.orange) end
  40. local function yellow() tsc(colors.yellow) end
  41. local function green() tsc(colors.green) end
  42. local function cyan() tsc(colors.cyan) end
  43. local function blue() tsc(colors.blue) end
  44. local function purple() tsc(colors.purple) end
  45. local function white() tsc(colors.white) end
  46. local function lgray() tsc(colors.lightGray) end
  47.  
  48. local function pln(msg) print(tostring(msg)) end
  49.  
  50. -- stripped down & modified copy of log.dmesg
  51. local function print(msg)
  52.     msg = tostring(msg)
  53.  
  54.     local cur_x, cur_y = term.getCursorPos()
  55.  
  56.     if cur_x == out_w then
  57.         -- jump to next line
  58.         cur_x = 1
  59.         if cur_y == out_h then
  60.             term.scroll(1)
  61.             term.setCursorPos(1, cur_y)
  62.         else
  63.             term.setCursorPos(1, cur_y + 1)
  64.         end
  65.     end
  66.  
  67.     local lines, remaining, s_start, s_end, ln = {}, true, 1, out_w + 1 - cur_x, 1
  68.     while remaining do
  69.         local line = string.sub(msg, s_start, s_end)
  70.  
  71.         if line == "" then
  72.             remaining = false
  73.         else
  74.             lines[ln] = line
  75.             s_start = s_end + 1
  76.             s_end = s_end + out_w
  77.             ln = ln + 1
  78.         end
  79.     end
  80.  
  81.     for i = 1, #lines do
  82.         cur_x, cur_y = term.getCursorPos()
  83.         if i > 1 and cur_x > 1 then
  84.             if cur_y == out_h then
  85.                 term.scroll(1)
  86.                 term.setCursorPos(1, cur_y)
  87.             else term.setCursorPos(1, cur_y + 1) end
  88.         end
  89.         term.write(lines[i])
  90.     end
  91. end
  92.  
  93. -- reset cursor to before a print of s occurred
  94. local function print_reset(s)
  95.     local _, y = term.getCursorPos()
  96.     for i = 1, #ccs.wrap(s, out_w) do
  97.         term.setCursorPos(1, y - i);term.clearLine()
  98.     end
  99. end
  100.  
  101. -- show progress and percentage at the bottom of the screen
  102. local function show_progress(p)
  103.     local _, y = term.getCursorPos()
  104.     term.setCursorPos(1, out_h)
  105.  
  106.     print(string.format("%3.0f%% ", p*100))
  107.     local pb = math.floor(p*(out_w-6))
  108.  
  109.     tbc(colors.lightBlue);tsc(colors.black)
  110.     print(string.rep("\x80", pb))
  111.     tbc(colors.black)
  112.  
  113.     term.setCursorPos(1, y);lgray()
  114. end
  115.  
  116. -- get command line option in list
  117. local function get_opt(opt, options)
  118.     for _, v in pairs(options) do if opt == v then return v end end
  119.     return nil
  120. end
  121.  
  122. -- wait for any key press
  123. local function any_key() os.pullEvent("key_up") end
  124.  
  125. -- ask the user yes or no
  126. local function ask_y_n(question, default)
  127.     print(question)
  128.     if default == true then print(" (Y/n)? ") else print(" (y/N)? ") end
  129.     white()
  130.     local r = read();any_key()
  131.     if r == "" then return default
  132.     elseif r == "Y" or r == "y" then return true
  133.     elseif r == "N" or r == "n" then return false
  134.     else return nil end
  135. end
  136.  
  137. -- get major, minor, patch version numbers
  138. local function v_nums(v)
  139.     local a, b, c = v:match("^[vV]?(%d+)%.(%d+)%.?(%d*)$")
  140.     return tonumber(a), tonumber(b), tonumber(c) or 0
  141. end
  142.  
  143. -- 1 = update, 0 = same, -1 = downgrade
  144. local function is_update(v)
  145.     local l1, l2, l3 = v_nums(v.v_local)
  146.     local r1, r2, r3 = v_nums(v.v_remote)
  147.  
  148.     if r1 ~= l1 then return (r1 > l1 and 1) or -1 end
  149.     if r2 ~= l2 then return (r2 > l2 and 1) or -1 end
  150.     if r3 ~= l3 then return (r3 > l3 and 1) or -1 end
  151.     return 0
  152. end
  153.  
  154. -- package version message
  155. local function pkg_v_msg(n, m, va, vb)
  156.     purple();print("["..n.."] ");white();print(m.." ");blue()
  157.     if vb then print(va);white();print(" \x1a ");blue();pln(vb) else pln(va) end
  158.     white()
  159. end
  160.  
  161. -- package info/warn message
  162. local function pkg_msg(n, m, w)
  163.     purple();print("["..n.."] ")
  164.     if w then yellow() else white() end
  165.     pln(m.." ")
  166. end
  167.  
  168. -- indicate actions to be taken based on package differences for installs/updates
  169. local function show_pkg_change(name, v)
  170.     if v.v_local then
  171.         local is_up = is_update(v)
  172.         if is_up ~= 0 then
  173.             local updn = (is_up > 0) and "updating" or "downgrading"
  174.             pkg_v_msg(name, updn, v.v_local, v.v_remote)
  175.         elseif mode == "install" then
  176.             pkg_v_msg(name, "reinstalling", v.v_local)
  177.         end
  178.     else pkg_v_msg(name, "new install of", v.v_remote) end
  179.  
  180.     return v.v_local ~= v.v_remote
  181. end
  182.  
  183. -- read the local manifest file
  184. local function read_local_manifest()
  185.     local ok, manifest, f = false, {}, fs.open("install_manifest.json", "r")
  186.     if f ~= nil then
  187.         ok, manifest = pcall(function () return textutils.unserializeJSON(f.readAll()) end)
  188.         f.close()
  189.     end
  190.     return ok, manifest
  191. end
  192.  
  193. -- read the manifest from GitHub
  194. local function read_remote_manifest()
  195.     local resp, err = http.get(manifest_url)
  196.     if resp == nil then
  197.         orange();pln("Failed to read installation manifest from GitHub, cannot update or install.")
  198.         red();pln("HTTP error: "..err);white()
  199.         return false, {}
  200.     end
  201.  
  202.     local ok, manifest = pcall(function () return textutils.unserializeJSON(resp.readAll()) end)
  203.     if not ok then red();pln("error parsing remote installation manifest");white() end
  204.  
  205.     return ok, manifest
  206. end
  207.  
  208. -- record the local installation manifest
  209. local function write_install_manifest(manifest, deps)
  210.     local versions = {}
  211.     for k, v in pairs(manifest.versions) do
  212.         local is_dep = false
  213.         for _, dep in pairs(deps) do
  214.             if (k == "bootloader" and dep == "system") or k == dep then
  215.                 is_dep = true;break
  216.             end
  217.         end
  218.         if k == app or k == "comms" or is_dep then versions[k] = v end
  219.     end
  220.  
  221.     manifest.versions = versions
  222.  
  223.     local f = fs.open("install_manifest.json", "w")
  224.     f.write(textutils.serializeJSON(manifest));f.close()
  225. end
  226.  
  227. -- try at most 3 times to download a file from the repository and write into w_path base directory
  228. ---@return 0|1|2|3 success 0: ok, 1: download fail, 2: file open fail, 3: out of space
  229. local function http_get_file(file, w_path)
  230.     for i = 1, 3 do
  231.         local dl, err = http.get(build_url..file)
  232.         if dl then
  233.             if i > 1 then green();pln("success!");lgray() end
  234.  
  235.             local f = fs.open(w_path..file, "w")
  236.             if not f then return 2 end
  237.  
  238.             local ok, msg = pcall(function() f.write(dl.readAll()) end)
  239.             f.close()
  240.             if not ok then
  241.                 if string.find(msg or "", "Out of space") ~= nil then
  242.                     red();pln("[out of space]");lgray()
  243.                     return 3
  244.                 else return 2 end
  245.             end
  246.             break
  247.         else
  248.             red();pln("HTTP Error: "..err)
  249.             if i < 3 then
  250.                 lgray();print("> retrying...")
  251.                 os.sleep(i/3)
  252.             else return 1 end
  253.         end
  254.     end
  255.     return 0
  256. end
  257.  
  258. -- recursively build a tree out of the file manifest
  259. local function gen_tree(manifest, log)
  260.     local function _tree_add(tree, split)
  261.         if #split > 1 then
  262.             local name = table.remove(split, 1)
  263.             if tree[name] == nil then tree[name] = {} end
  264.             table.insert(tree[name], _tree_add(tree[name], split))
  265.         else return split[1] end
  266.         return nil
  267.     end
  268.  
  269.     local list, tree = { log }, {}
  270.  
  271.     for _, files in pairs(manifest.files) do for i = 1, #files do table.insert(list, files[i]) end end
  272.  
  273.     for i = 1, #list do
  274.         local split = {}
  275.         string.gsub(list[i], "([^/]+)", function(c) split[#split + 1] = c end)
  276.         if #split == 1 then table.insert(tree, list[i])
  277.         else table.insert(tree, _tree_add(tree, split)) end
  278.     end
  279.  
  280.     return tree
  281. end
  282.  
  283. local function _in_array(val, array)
  284.     for _, v in pairs(array) do if v == val then return true end end
  285.     return false
  286. end
  287.  
  288. local function _clean_dir(dir, tree)
  289.     if tree == nil then tree = {} end
  290.     local ls = fs.list(dir)
  291.     for _, l in pairs(ls) do
  292.         local path = dir.."/"..l
  293.         if fs.isDir(path) then
  294.             _clean_dir(path, tree[l])
  295.             if #fs.list(path) == 0 then fs.delete(path);pln("deleted "..path) end
  296.         elseif (not _in_array(l, tree)) and l ~= "config.lua" then
  297.             fs.delete(path);pln("deleted "..path)
  298.         end
  299.     end
  300. end
  301.  
  302. -- go through app/common directories to delete unused files
  303. local function clean(manifest)
  304.     local log, cfg = nil, app..".settings"
  305.     if fs.exists(cfg) and settings.load(cfg) then
  306.         log = settings.get("LogPath")
  307.         if log:sub(1, 1) == "/" then log = log:sub(2) end
  308.     end
  309.  
  310.     local tree = gen_tree(manifest, log)
  311.  
  312.     table.insert(tree, "install_manifest.json")
  313.     table.insert(tree, "ccmsi.lua")
  314.  
  315.     local ls = fs.list("/")
  316.     for _, val in pairs(ls) do
  317.         if fs.isDriveRoot(val) then
  318.             yellow();pln("skipped mount '"..val.."'")
  319.         elseif fs.isDir(val) then
  320.             if tree[val] ~= nil then lgray();_clean_dir("/"..val, tree[val])
  321.             else white(); if ask_y_n("delete the unused directory '"..val.."'") then lgray();_clean_dir("/"..val) end end
  322.             if #fs.list(val) == 0 then fs.delete(val);lgray();pln("deleted empty directory '"..val.."'") end
  323.         elseif not _in_array(val, tree) and (string.find(val, ".settings") == nil) then
  324.             white();if ask_y_n("delete the unused file '"..val.."'") then fs.delete(val);lgray();pln("deleted "..val) end
  325.         end
  326.     end
  327.  
  328.     white()
  329. end
  330.  
  331. -- startup header
  332.  
  333. tsc(colors.magenta)
  334. if IS_PKT then pln("- SCADA Installer v"..CCMSI_VERSION.." -")
  335. else pln("-- ComputerCraft Mekanism SCADA Installer v"..CCMSI_VERSION.." --") end
  336. white()
  337.  
  338. -- handle command line options
  339.  
  340. if #OPTS == 0 or OPTS[1] == "help" then
  341.     pln("usage: ccmsi <mode> <app> <branch>")
  342.  
  343.     blue();pln("<mode>")
  344.     if IS_PKT then
  345.         lgray();pln(" check - check latest\n install - fresh install\n update - update app\n uninstall - remove app")
  346.         blue();pln("<app>")
  347.         lgray();pln(" pocket\n installer (update only)")
  348.         blue();pln("<branch>")
  349.     else
  350.         lgray();pln(" check       - check latest versions available")
  351.         yellow();pln("               ccmsi check <branch> (skip <app>)")
  352.         lgray();pln(" install     - fresh install\n update      - update files\n uninstall   - delete files INCLUDING config/logs")
  353.         blue();print("<app>");cyan();pln(" omit to auto-detect installed app")
  354.         lgray();pln(" reactor-plc - Fission Reactor PLC firmware\n rtu         - RTU Gateway firmware\n supervisor  - Supervisor server application\n coordinator - Coordinator application\n pocket      - Pocket application\n installer   - CCMSI installer (update only)")
  355.         blue();print("<branch>");cyan();pln(" omit for 'main'")
  356.     end
  357.     lgray();pln(" main (default) | devel");white()
  358.  
  359.     return
  360. else
  361.     mode = get_opt(OPTS[1], { "check", "install", "update", "uninstall" })
  362.     if mode == nil then
  363.         red();pln("Invalid mode.");white()
  364.         return
  365.     end
  366.  
  367.     local next_opt = 3
  368.     local apps = { "reactor-plc", "rtu", "supervisor", "coordinator", "pocket", "installer" }
  369.     app = get_opt(OPTS[2], apps)
  370.     if app == nil then
  371.         for _, a in pairs(apps) do
  372.             if fs.isDir(a) then app, next_opt = a, 2 end
  373.         end
  374.     end
  375.  
  376.     if app == nil and mode ~= "check" then
  377.         red();pln("Invalid application.");white()
  378.         return
  379.     elseif mode == "check" then
  380.         next_opt = 2
  381.     elseif app == "installer" and mode ~= "update" then
  382.         red();pln("Installer only supports 'update'.");white()
  383.         return
  384.     end
  385.  
  386.     target = OPTS[next_opt] or "main"
  387.     if target ~= "main" and target ~= "devel" then
  388.         red();pln("Invalid branch target.");white()
  389.         return
  390.     end
  391.  
  392.     manifest_url = MANIFEST_DIR..target.."/install_manifest.json"
  393.     build_url = BUILD_DIR..target.."/"
  394. end
  395.  
  396. -- main operation
  397.  
  398. local ok, r_manifest, l_manifest
  399.  
  400. if mode == "check" then
  401.     ok, r_manifest = read_remote_manifest()
  402.     if not ok then return end
  403.  
  404.     ok, l_manifest = read_local_manifest()
  405.     if not ok then
  406.         yellow();pln("failed to load local installation information");white()
  407.         l_manifest = { versions = { installer = CCMSI_VERSION } }
  408.     else
  409.         l_manifest.versions.installer = CCMSI_VERSION
  410.     end
  411.  
  412.     if not IS_PKT then pln("") end
  413.  
  414.     -- list all versions
  415.     for k, v in pairs(r_manifest.versions) do
  416.         purple()
  417.         local tag = string.format("%-14s", "["..k.."]")
  418.         if not IS_PKT then print(tag) end
  419.         if k == "installer" or (ok and (l_manifest.versions[k] ~= nil)) then
  420.             if IS_PKT then pln(tag) end
  421.             blue();print(l_manifest.versions[k])
  422.             if v ~= l_manifest.versions[k] then
  423.                 white();print(" (");cyan();print(v);white();pln(" available)")
  424.             else green();pln(" (up to date)") end
  425.         elseif not IS_PKT then
  426.             lgray();print("not installed");white();print(" (latest ");cyan();print(v);white();pln(")")
  427.         end
  428.     end
  429.  
  430.     if r_manifest.versions.installer ~= l_manifest.versions.installer and not IS_PKT then
  431.         yellow();pln("\nA different version of the installer is available, it is STRONGLY recommended to update (use 'ccmsi update installer').");white()
  432.     end
  433. elseif mode == "install" or mode == "update" then
  434.     local update_installer = app == "installer"
  435.  
  436.     ok, r_manifest = read_remote_manifest()
  437.     if not ok then return end
  438.  
  439.     local ver = {
  440.         app = { v_local = nil, v_remote = nil, changed = false },
  441.         boot = { v_local = nil, v_remote = nil, changed = false },
  442.         comms = { v_local = nil, v_remote = nil, changed = false },
  443.         common = { v_local = nil, v_remote = nil, changed = false },
  444.         graphics = { v_local = nil, v_remote = nil, changed = false },
  445.         lockbox = { v_local = nil, v_remote = nil, changed = false }
  446.     }
  447.  
  448.     -- try to load local versions
  449.     ok, l_manifest = read_local_manifest()
  450.     if ok then
  451.         ver.boot.v_local = l_manifest.versions.bootloader
  452.         ver.app.v_local = l_manifest.versions[app]
  453.         ver.comms.v_local = l_manifest.versions.comms
  454.         ver.common.v_local = l_manifest.versions.common
  455.         ver.graphics.v_local = l_manifest.versions.graphics
  456.         ver.lockbox.v_local = l_manifest.versions.lockbox
  457.  
  458.         if l_manifest.versions[app] == nil then
  459.             red();pln("Another application is already installed, please uninstall it before installing a new application.");white()
  460.             return
  461.         end
  462.     elseif mode == "update" and not update_installer then
  463.         red();pln("Failed to load local installation information, cannot update.");white()
  464.         return
  465.     end
  466.  
  467.     -- installer update handling
  468.     if r_manifest.versions.installer ~= CCMSI_VERSION then
  469.         if not update_installer then yellow();pln("A different version of the installer is available, it is STRONGLY recommended to update to it.");white() end
  470.         if update_installer or ask_y_n("Would you like to update it now", true) then
  471.             lgray();pln("GET ccmsi.lua")
  472.             local dl, err = http.get(build_url.."ccmsi.lua")
  473.  
  474.             if dl == nil then
  475.                 red();pln("HTTP Error: "..err)
  476.                 pln("Installer download failed.")
  477.             else
  478.                 local f = fs.open(debug.getinfo(1, "S").source:sub(2), "w") -- this file
  479.                 f.write(dl.readAll());f.close()
  480.                 green();pln("Installer updated successfully.")
  481.             end
  482.  
  483.             white()
  484.             return
  485.         end
  486.     elseif update_installer then
  487.         green();pln("Installer already up-to-date.");white()
  488.         return
  489.     end
  490.  
  491.     ver.boot.v_remote = r_manifest.versions.bootloader
  492.     ver.app.v_remote = r_manifest.versions[app]
  493.     ver.comms.v_remote = r_manifest.versions.comms
  494.     ver.common.v_remote = r_manifest.versions.common
  495.     ver.graphics.v_remote = r_manifest.versions.graphics
  496.     ver.lockbox.v_remote = r_manifest.versions.lockbox
  497.  
  498.     green()
  499.     if mode == "install" then print("Installing ") else print("Updating ") end
  500.     pln(app.." files...");white()
  501.  
  502.     ver.boot.changed = show_pkg_change("bootldr", ver.boot)
  503.     ver.common.changed = show_pkg_change("common", ver.common)
  504.     ver.comms.changed = show_pkg_change("comms", ver.comms)
  505.     if ver.comms.changed and ver.comms.v_local ~= nil then
  506.         pkg_msg("comms", "all networked devices must be updated", true)
  507.     end
  508.     ver.app.changed = show_pkg_change(app, ver.app)
  509.     ver.graphics.changed = show_pkg_change("graphics", ver.graphics)
  510.     ver.lockbox.changed = show_pkg_change("lockbox", ver.lockbox)
  511.  
  512.     -- start install/update
  513.  
  514.     local space_req = r_manifest.sizes.manifest
  515.     local space_avail = fs.getFreeSpace("/")
  516.  
  517.     local file_list, size_list = r_manifest.files, r_manifest.sizes
  518.     local deps, sf_deps = r_manifest.depends[app], {}
  519.  
  520.     table.insert(deps, app)
  521.  
  522.     -- helper function to check if a dependency is unchanged
  523.     local function unchanged(dep)
  524.         if dep == "system" then return not ver.boot.changed
  525.         elseif dep == "graphics" then return not ver.graphics.changed
  526.         elseif dep == "lockbox" then return not ver.lockbox.changed
  527.         elseif dep == "common" then return not (ver.common.changed or ver.comms.changed)
  528.         elseif dep == app then return not ver.app.changed
  529.         else return true end
  530.     end
  531.  
  532.     local any_change = false
  533.  
  534.     for _, dep in pairs(deps) do
  535.         table.insert(sf_deps, dep)
  536.         local size = size_list[dep]
  537.         space_req = space_req + size
  538.         any_change = any_change or not unchanged(dep)
  539.     end
  540.  
  541.     if mode == "update" and not any_change then
  542.         yellow();pln("Nothing to do, everything is already up-to-date!");white()
  543.         return
  544.     end
  545.  
  546.     -- ask for confirmation
  547.     if not ask_y_n("Continue", false) then return end
  548.  
  549.     local single_file_mode = space_avail < space_req
  550.  
  551.     local success = true
  552.  
  553.     -- delete a file if the capitalization changes so that things work on Windows
  554.     ---@param path string
  555.     local function mitigate_case(path)
  556.         local dir, file = fs.getDir(path), fs.getName(path)
  557.         if not fs.isDir(dir) then return end
  558.         for _, p in ipairs(fs.list(dir)) do
  559.             if string.lower(p) == string.lower(file) then
  560.                 if p ~= file then fs.delete(path) end
  561.                 return
  562.             end
  563.         end
  564.     end
  565.  
  566.     ---@param dl_stat 1|2|3 download status
  567.     ---@param file string file name
  568.     ---@param attempt integer recursive attempt #
  569.     ---@param sf_install function installer function for recursion
  570.     local function handle_dl_fail(dl_stat, file, attempt, sf_install)
  571.         red()
  572.         if dl_stat == 1 then
  573.             pln("failed to download "..file)
  574.         elseif dl_stat > 1 then
  575.             if dl_stat == 2 then pln("filesystem error with "..file) else pln("not enough space for "..file) end
  576.             if attempt == 1 then
  577.                 orange();pln("re-attempting operation...");white()
  578.                 sf_install(2)
  579.             elseif attempt == 2 then
  580.                 yellow()
  581.                 if dl_stat == 2 then pln("There was an error writing to a file.") else pln("Insufficient space available.") end
  582.                 lgray()
  583.                 if dl_stat == 2 then
  584.                     pln("This may be due to insufficent space available or file permission issues. The installer can now attempt to delete files not used by the SCADA system.")
  585.                 else pln("The installer can now attempt to delete files not used by the SCADA system.") end
  586.                 white()
  587.                 if not ask_y_n("Continue", false) then
  588.                     success = false
  589.                     return
  590.                 end
  591.                 clean(r_manifest)
  592.                 sf_install(3)
  593.             elseif attempt == 3 then
  594.                 yellow()
  595.                 if dl_stat == 2 then pln("There again was an error writing to a file.") else pln("Insufficient space available.") end
  596.                 lgray()
  597.                 if dl_stat == 2 then
  598.                     pln("This may be due to insufficent space available or file permission issues. Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.")
  599.                 else pln("Please delete any unused files you have on this computer then try again. Do not delete the "..app..".settings file unless you want to re-configure.") end
  600.                 white()
  601.                 success = false
  602.             end
  603.         end
  604.     end
  605.  
  606.     -- single file update routine: go through all files and replace one by one
  607.     ---@param attempt integer recursive attempt #
  608.     local function sf_install(attempt)
  609.         if attempt > 1 then os.sleep(2.0) end
  610.  
  611.         local abort_attempt = false
  612.         success = true
  613.  
  614.         for k, dep in pairs(sf_deps) do
  615.             if mode == "update" and unchanged(dep) then
  616.                 pkg_msg(dep, "skipping install of unchanged package", true)
  617.                 sf_deps[k] = nil
  618.             else
  619.                 pkg_msg(dep, "installing package...")
  620.                 lgray()
  621.  
  622.                 -- beginning on the second try, delete the directory before starting
  623.                 if attempt >= 2 then
  624.                     if dep == "common" then
  625.                         if fs.exists("/scada-common") then
  626.                             fs.delete("/scada-common");pln("deleted /scada-common")
  627.                         end
  628.                     elseif dep ~= "system" then
  629.                         if fs.exists("/"..dep) then
  630.                             fs.delete("/"..dep);pln("deleted /"..dep)
  631.                         end
  632.                     end
  633.                 end
  634.  
  635.                 local files, n = file_list[dep], 1
  636.                 for _, file in pairs(files) do
  637.                     local s = "GET "..file
  638.                     pln(s)
  639.                     mitigate_case(file)
  640.                     local dl_stat = http_get_file(file, "/")
  641.                     if dl_stat ~= 0 then
  642.                         abort_attempt = true
  643. ---@diagnostic disable-next-line: param-type-mismatch
  644.                         handle_dl_fail(dl_stat, file, attempt, sf_install)
  645.                         break
  646.                     end
  647.                     show_progress(n/#files)
  648.                     n = n + 1
  649.                     print_reset(s)
  650.                 end
  651.  
  652.                 if not abort_attempt then pkg_msg(dep, "installed!");sf_deps[k] = nil end
  653.                 term.clearLine()
  654.             end
  655.             if abort_attempt or not success then break end
  656.         end
  657.     end
  658.  
  659.     -- handle update/install
  660.     if single_file_mode then sf_install(1)
  661.     else
  662.         if fs.exists(INSTALL_DIR) then fs.delete(INSTALL_DIR);fs.makeDir(INSTALL_DIR) end
  663.  
  664.         -- download all dependencies
  665.         for _, dep in pairs(deps) do
  666.             if mode == "update" and unchanged(dep) then
  667.                 pkg_msg(dep, "skipping download of unchanged package", true)
  668.             else
  669.                 pkg_msg(dep, "downloading package...")
  670.                 lgray()
  671.  
  672.                 local files, n = file_list[dep], 1
  673.                 for _, file in pairs(files) do
  674.                     local s = "GET "..file
  675.                     pln(s)
  676.                     local dl_stat = http_get_file(file, INSTALL_DIR.."/")
  677.                     success = dl_stat == 0
  678.                     if dl_stat == 1 then
  679.                         red();pln("failed to download "..file)
  680.                         break
  681.                     elseif dl_stat == 2 then
  682.                         red();pln("filesystem error with "..file)
  683.                         break
  684.                     elseif dl_stat == 3 then
  685.                         -- this shouldn't occur in this mode
  686.                         red();pln("not enough space for "..file)
  687.                         break
  688.                     end
  689.                     show_progress(n/#files)
  690.                     n = n + 1
  691.                     print_reset(s)
  692.                 end
  693.  
  694.                 if success then pkg_msg(dep, "download complete!") end
  695.                 term.clearLine()
  696.             end
  697.             if not success then break end
  698.         end
  699.  
  700.         -- copy in downloaded files (installation)
  701.         if success then
  702.             for _, dep in pairs(deps) do
  703.                 if mode == "update" and unchanged(dep) then
  704.                     pkg_msg(dep, "skipping install of unchanged package", true)
  705.                 else
  706.                     pkg_msg(dep, "installing package...")
  707.                     lgray()
  708.  
  709.                     local files = file_list[dep]
  710.                     for _, file in pairs(files) do
  711.                         local temp_file = INSTALL_DIR.."/"..file
  712.                         mitigate_case(file)
  713.                         if fs.exists(file) then fs.delete(file) end
  714.                         fs.move(temp_file, file)
  715.                     end
  716.  
  717.                     pkg_msg(dep, "installed!")
  718.                 end
  719.             end
  720.         end
  721.  
  722.         fs.delete(INSTALL_DIR)
  723.     end
  724.  
  725.     if success then
  726.         write_install_manifest(r_manifest, deps)
  727.         green()
  728.         if mode == "install" then print("Installation") else print("Update") end
  729.         pln(" completed successfully.")
  730.         white();pln("Ready to clean up unused files, press any key to continue...")
  731.         any_key();clean(r_manifest)
  732.         white();pln("Done.")
  733.     else
  734.         red()
  735.         if single_file_mode then
  736.             if mode == "install" then print("Installation") else print("Update") end
  737.             pln(" failed, files may have been skipped.")
  738.         else
  739.             if mode == "install" then pln("Installation failed.")
  740.             else orange();pln("Update failed, existing files unmodified.") end
  741.         end
  742.     end
  743. elseif mode == "uninstall" then
  744.     ok, l_manifest = read_local_manifest()
  745.     if not ok then
  746.         red();pln("Error parsing local installation manifest.");white()
  747.         return
  748.     end
  749.  
  750.     if l_manifest.versions[app] == nil then
  751.         red();pln("Error: '"..app.."' is not installed.")
  752.         return
  753.     end
  754.  
  755.     orange();pln("Uninstalling all "..app.." files...");white()
  756.  
  757.     -- ask for confirmation
  758.     if not ask_y_n("Continue", false) then return end
  759.  
  760.     -- delete unused files first
  761.     clean(l_manifest)
  762.  
  763.     local file_list = l_manifest.files
  764.     local deps = l_manifest.depends[app]
  765.  
  766.     table.insert(deps, app)
  767.  
  768.     -- delete all installed files
  769.     lgray()
  770.     for _, dep in pairs(deps) do
  771.         local files = file_list[dep]
  772.         for _, file in pairs(files) do
  773.             if fs.exists(file) then fs.delete(file);pln("deleted "..file) end
  774.         end
  775.  
  776.         local folder = files[1]
  777.         while true do
  778.             local dir = fs.getDir(folder)
  779.             if dir == "" or dir == ".." then break else folder = dir end
  780.         end
  781.  
  782.         if fs.isDir(folder) then
  783.             fs.delete(folder);pln("deleted directory "..folder)
  784.         end
  785.     end
  786.  
  787.     -- delete log file
  788.     local log_deleted, cfg = false, app..".settings"
  789.     if fs.exists(cfg) and settings.load(cfg) then
  790.         local log = settings.get("LogPath")
  791.         if log ~= nil then
  792.             log_deleted = true
  793.             if fs.exists(log) then
  794.                 fs.delete(log);pln("deleted log file "..log)
  795.             end
  796.         end
  797.     end
  798.  
  799.     if not log_deleted then
  800.         red();pln("Failed to delete log file (it may not exist).");lgray()
  801.     end
  802.  
  803.     if fs.exists(cfg) then
  804.         fs.delete(cfg);pln("deleted "..cfg)
  805.     end
  806.  
  807.     fs.delete("install_manifest.json")
  808.     pln("deleted install_manifest.json")
  809.  
  810.     green();pln("Done!")
  811. end
  812.  
  813. white()
Advertisement
Add Comment
Please, Sign In to add comment