Last active
February 20, 2020 07:33
-
-
Save geekhunger/f5ec80010ca25de941f471bb42711996 to your computer and use it in GitHub Desktop.
hot-swap modules required by Lua (bonus: low-level system information and filesystem access via utilities.lua)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 2020 (c) [email protected] | |
-- IMPORTANT NOTE for Lua < 5.1 | |
-- When using this class to hotload other modules, be sure to count tables WITH table.getn and NOT with # !!! | |
-- Lua 5.1 and prior don't support overriding # via .__len metamethod - so our best bet here is to modify table.getn | |
-- and use table.getn whenever we have to check the number of entires in numerical tables | |
local _require = require | |
local _ipairs = ipairs | |
local _pairs = pairs | |
local _type = type | |
local _getn = table.getn or function(t) return #t end -- Lua > 5.1 | |
local INDEX = function(t, k) return getmetatable(t).__swap.value[k] end | |
local NEWINDEX = function(t, k, v) getmetatable(t).__swap.value[k] = v end | |
local CALL = function(t, ...) return getmetatable(t).__swap.value(...) end | |
local TYPE = function(t) return _type(getmetatable(t).__swap.value) end | |
local IPAIRS = function(t) return _ipairs(getmetatable(t).__swap.value) end | |
local PAIRS = function(t) return _pairs(getmetatable(t).__swap.value) end | |
local LEN = function(t) return _getn(getmetatable(t).__swap.value) end | |
local utilities = {} -- placeholder, see monkeypatch below | |
hotload = setmetatable( | |
{ | |
package_loaded = {}; | |
reload_interval = 3; | |
run = function(self) | |
for module, value in pairs(self.package_loaded) do | |
local proxy = getmetatable(value) | |
if type(proxy) == "table" then -- is a hot-swappable object | |
local this = getmetatable(self) | |
this:__update(proxy) | |
end | |
end | |
end; | |
onEnterFrame = function(self) | |
if not self.reload_timeout or self.reload_timeout < os.time() then | |
self.reload_timeout = os.time() + self.reload_interval | |
self:run() | |
end | |
end | |
}, | |
{ | |
__call = function(self, module) | |
assert( | |
not package.loaded[module], | |
"module '"..tostring(module).."' can't be registred for hot-reload as it has already been loaded traditionally via require()" | |
) | |
if self.package_loaded[module] then | |
local value = getmetatable(self.package_loaded[module]).__swap.value | |
if type(value) == "function" or type(value) == "table" then | |
return self.package_loaded[module] -- via proxy wrapper | |
end | |
return value | |
end | |
return getmetatable(self).__create(self, module) | |
end; | |
__create = function(self, module) | |
local mname, mpath, mvalue = getmetatable(self):__heap(module) | |
if not mname or not mpath then | |
error(string.format( | |
"module '%s' could neither be loaded nor registred (seems having errors) %s", | |
module, | |
type(mvalue) == "nil" and "because it returns nil" or "\n"..tostring(mvalue) | |
)) | |
return nil | |
end | |
self.package_loaded[mname] = setmetatable({}, { | |
__index = type(mvalue) == "table" and INDEX or nil, | |
__newindex = type(mvalue) == "table" and NEWINDEX or nil, | |
__call = (type(mvalue) == "function" or type(mvalue) == "table") and CALL or nil, | |
__ipairs = type(mvalue) == "table" and IPAIRS or nil, | |
__pairs = type(mvalue) == "table" and PAIRS or nil, | |
__len = type(mvalue) == "table" and LEN or nil, | |
__type = TYPE, | |
__swap = { | |
name = mname, | |
path = mpath, | |
value = mvalue, | |
timestamp = utilities.modifiedat(mpath) | |
} | |
}) | |
print(string.format("module '%s' has been loaded and registred for hot-reload", mname)) | |
return getmetatable(self).__call(self, module) | |
end; | |
__update = function(self, proxy) | |
local timestamp = utilities.modifiedat(proxy.__swap.path) | |
if proxy.__swap.timestamp ~= timestamp then | |
local mname, mpath, mvalue = self:__heap(proxy.__swap.name) | |
if mname and mpath and mvalue then | |
proxy.__swap.value = mvalue | |
proxy.__swap.timestamp = timestamp | |
print(string.format("module '%s' has been hot-reloaded", mname)) | |
-- TODO? preserve state of hotswappable objects | |
-- by providing :hotswap class method for transfering state | |
-- onto the swapped objects? | |
else | |
print(string.format("module '%s' could not be hot-re-loaded\n%s", module, mvalue)) | |
end | |
end | |
end; | |
__heap = function(self, module) | |
local path = self:__path(module) | |
assert(type(path) == "string", "can't find module '"..module.."'") | |
local ok, msg = pcall(dofile, path) | |
if ok == true and msg ~= nil then | |
return module, path, msg | |
end | |
return nil, nil, msg | |
end; | |
__path = function(self, resource) | |
local file_path = resource:gsub("%.", "/") | |
if file_path:sub(1, 1) == "/" then file_path = "."..file_path end | |
if file_path:sub(-4) ~= ".lua" then file_path = file_path..".lua" end | |
if not utilities.isfile(file_path) then | |
file_path = file_path:sub(1, -4).."init.lua" | |
if not utilities.isfile(file_path) then | |
file_path = nil | |
end | |
end | |
return file_path | |
end | |
} | |
) | |
do | |
-- monkeypatch to convert utilities module into hot-swappable object | |
-- 1. load the real, working methods from the utilities module | |
-- 2. at this point we can actually use hotload() to its full extent | |
-- 3. update the entire utilities module by aquiring it, which makes it hot-reload-able | |
utilities = dofile "utilities.lua" | |
utilities = hotload "utilities" | |
end | |
function require(module) | |
return (type(hotload) == "table" and hotload.package_loaded[module]) and hotload(module) or _require(module) | |
end | |
-- IMPORTANT NOTE the fallowing Lua overrides are only needed for Lua <= 5.1 backward compatibility because it doen't support __ipairs, __pairs, __len metamethods. Some of Lua's standard functions use facilities like #, for example in table.insert, when shifting entries. | |
function type(obj) | |
local proxy = getmetatable(obj) | |
if proxy and proxy.__type then | |
if _type(proxy.__type) == "string" then | |
return proxy.__type | |
elseif _type(proxy.__type) == "function" then | |
return proxy.__type(obj) | |
end | |
end | |
return _type(obj) | |
end | |
function ipairs(obj) | |
local proxy = getmetatable(obj) | |
if proxy and _type(proxy.__ipairs) == "function" then | |
return proxy.__ipairs(obj) | |
end | |
return _ipairs(obj) | |
end | |
function pairs(obj) | |
local proxy = getmetatable(obj) | |
if proxy and _type(proxy.__pairs) == "function" then | |
return proxy.__pairs(obj) | |
end | |
return _pairs(obj) | |
end | |
function table.getn(obj) | |
local proxy = getmetatable(obj) | |
if proxy and _type(proxy.__len) == "function" then | |
return proxy.__len(obj) | |
end | |
return _getn(obj) | |
end | |
function table.insert(t, p, v) | |
local pos, val | |
if v and type(p) == "number" then | |
assert(p >= 1 and p <= table.getn(t) + 1, "table.insert position index out of range") | |
pos = p | |
val = v | |
if t[pos] then | |
for i = table.getn(t) + 1, pos - 1, -1 do | |
t[i] = t[i - 1] | |
end | |
end | |
else | |
pos = table.getn(t) + 1 | |
val = p | |
end | |
t[pos] = val | |
end | |
function table.remove(t, p) | |
assert(p >= 1 and p <= table.getn(t) + 1, "table.remove position index out of range") | |
if t[p + 1] then | |
for i = p, table.getn(t) - 1 do | |
t[i] = t[i + 1] | |
end | |
t[table.getn(t)] = nil | |
end | |
t[p] = nil | |
end | |
return hotload |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 2019 (c) [email protected] | |
-- NOTE useful git command to setup when you want to count all lines of code in the current git repository | |
-- $ git config --local alias.count "! git ls-files | xargs wc -l" | |
-- $ git count | |
-- namespace unix plumbing utilities to access (file)system tools at low-level | |
local mimetypeguess = require "mimetype" | |
local filesystem = {shell = shell} | |
-- @str (string) the string to trim | |
-- returns (string) with removed whitespaces, tabs and line-break characters from beginning- and ending of the string | |
-- NOTE also useful to omit additional return values (only keep the first returned value) | |
local function trim(str) | |
if type(str) ~= "string" then str = tostring(str or "") end | |
local mask = "[ \t\r\n]*" | |
local output = str:gsub("^"..mask, ""):gsub(mask.."$", "") | |
return output | |
end | |
-- @val (any) the value to wrap in qoutes | |
-- returns (string) value converted to string and wrapped into quotation marks | |
local function quote(val) | |
return "\""..tostring(val or "").."\"" | |
end | |
-- @fragments (table) list of values | |
-- returns (string) concatenated string of all items similar to table.concat | |
local function toquery(fragments) | |
local query = "" | |
for _, frag in ipairs(fragments) do | |
local f = tostring(frag) | |
if (f == "" or f:match("%s+")) and not f:match("^[\"\']+.+[\"\']$") then f = quote(f) end -- but ignore already escaped frags | |
if not query:match("=$") then f = " "..f end | |
query = query..f | |
end | |
return trim(query) | |
end | |
-- @... (any) first argument should be the utility name fallowed by its list of parameters | |
-- returns (string or nil, boolean) return value of utility call or nil, and its status | |
local function cmd(...) | |
local tmpfile = "/tmp/shlua" | |
local exitcode = "; echo $? >>"..tmpfile | |
local command = os.execute(toquery{...}.." >>"..tmpfile..exitcode) | |
local console, report, status = io.open(tmpfile, "r") | |
if console then | |
report, status = console:read("*a"):match("(.*)(%d+)[\r\n]*$") -- response, exitcode | |
report = trim(report) | |
status = tonumber(status) == 0 | |
console:close() | |
end | |
os.remove(tmpfile) | |
return report ~= "" and report or nil, status | |
end | |
-- add api like shell[utility](arguments) or shell.utility(arguments) | |
local shell = setmetatable({cmd = cmd}, {__index = function(_, utility) | |
return function(...) | |
return cmd(utility, ...) | |
end | |
end}) | |
-- @platform (string) operating system to check against; returns (boolean) true on match | |
-- platform regex could be: linux*, windows* darwin*, cygwin*, mingw* (everything else might count as unknown) | |
-- returns (string) operating system identifier or (boolean) on match with @platform | |
-- NOTE love.system.getOS() is another way of retreving this, if the love2d framework is used in this context | |
function filesystem.os(platform) | |
filesystem.uname = filesystem.uname or shell.uname("-s") | |
if type(platform) == "string" then | |
if platform:lower():find("maco?s?") then platform = "darwin" end | |
return type(filesystem.uname:lower():match("^"..platform:lower())) ~= "nil" | |
end | |
return filesystem.uname | |
end | |
-- @path (string) relative- or absolute path to a file or folder | |
-- returns (boolean) | |
function filesystem.exists(path) | |
return select(2, shell.test("-e", path)) | |
end | |
-- @path (string) relative- or absolute path to a file or folder | |
-- returns (string) mime-type of the resource (file or folder) | |
-- NOTE for more predictable web-compilant results use the mime.lua module! | |
function filesystem.filetype(path) | |
if filesystem.exists(path) then | |
return trim(shell.file("--mime-type", "-b", path)) | |
end | |
return nil | |
end | |
-- @path (string) relative- or absolute path to a file | |
-- returns (string) mime-encoding of the resource | |
function filesystem.filecharset(path) | |
if filesystem.isfile(path) then | |
return trim(shell.file("--mime-encoding", "-b", path)) | |
end | |
return nil | |
end | |
-- @path (string) relative- or absolute path to a file | |
-- returns (string) mime-type; mime-encoding of the resource in one go | |
function filesystem.filemime(path) | |
if filesystem.isfile(path) then | |
if filesystem.os("darwin") then -- MacOS | |
return trim(shell.file("-bI", path)) | |
elseif filesystem.os("linux") then -- Linux | |
return trim(shell.file("-bi", path)) | |
end | |
end | |
return nil | |
end | |
-- @path (string) relative- or absolute path to a file | |
-- returns (boolean) | |
function filesystem.isfile(path) | |
return filesystem.exists(path) and select(2, shell.test("-f", path)) | |
end | |
-- @path (string) relative- or absolute path to a folder | |
-- returns (boolean) | |
function filesystem.isfolder(path) | |
return filesystem.exists(path) and select(2, shell.test("-d", path)) | |
end | |
-- returns (string) of the current location you are at | |
function filesystem.currentfolder() | |
return trim(shell.echo("$(pwd)")) | |
end | |
-- @path (string) relative- or absolute path to the (sub-)folder | |
-- @filter (string) filename to check against; or regex expression mask, see https://www.cyberciti.biz/faq/grep-regular-expressions | |
-- returns (boolen or table) nil if @path leads to a file instead of a folder; | |
-- true on a match with @filter + an array of files that match the @filter criteria; | |
-- otherwise an array of files inside that folder | |
function filesystem.infolder(path, filter) | |
-- TODO? include folders as well but append / to signal that its a folder? | |
if not filesystem.isfolder(path) then return nil end | |
local content, status = shell.cmd("ls", path, "|", "grep", filter or "") | |
local list = {} | |
for resource in content:gmatch("[^\r\n]*") do | |
if resource ~= "" then table.insert(list, resource) end | |
end | |
if filter then return content ~= "", list end | |
return list | |
end | |
-- @path (string) relative- or absolute path to the file or (sub-)folder | |
-- returns (string) birthtime of file as epoch/unix date timestamp | |
function filesystem.createdat(path) | |
if filesystem.os("darwin") then -- MacOS | |
return trim(shell.stat("-f", "%B", path)) | |
elseif filesystem.os("linux") then -- Linux | |
-- NOTE most Linux filesystems do not support this property and return 0 or - | |
-- see https://unix.stackexchange.com/questions/91197/how-to-find-creation-date-of-file | |
return trim(shell.stat("-c", "%W", path)) | |
end | |
end | |
-- @path (string) relative- or absolute path to the file or (sub-)folder | |
-- returns (string) epoch/ unix date timestamp | |
function filesystem.modifiedat(path) | |
-- NOTE a machine should first of all have the right timezone set in preferences, for Linux see https://askubuntu.com/questions/3375/how-to-change-time-zone-settings-from-the-command-line | |
-- Linux does always store modification time as UTC and converts these timestamps aleways back into the local timezone of your machine. However, if a device stores time as CET then Linux would assume that timestamp to be UTC and therefor (mistakenly) convert it back into the machines local timezone, see discussion https://unix.stackexchange.com/questions/440765/linux-showing-incorrect-file-modified-time-for-camera-video | |
-- In any case, you get different results for MacOS vs Linux! | |
return trim(shell.date("-r", path, "+%s")) | |
end | |
-- @path (string) relative- or absolute path to the file | |
-- returns (string) SHA1 checksum of file contents | |
function filesystem.checksum(path) | |
if filesystem.isfile(path) then | |
if filesystem.os("darwin") then -- MacOS | |
return trim(shell.cmd("shasum", "-a", 1, path, "|", "awk", "'{print $1}'")) | |
elseif filesystem.os("linux") then -- Linux | |
return trim(shell.cmd("sha1sum", path, "|", "awk", "'{print $1}'")) | |
end | |
end | |
return nil | |
end | |
-- @path (string) relative- or absolute path to the new, empty file | |
-- does not override existing file but updates its timestamp | |
-- returns (boolean) true on success | |
function filesystem.makefile(path) | |
if filesystem.isfolder(path) then return false end | |
return select(2, shell.touch(path)) | |
end | |
-- @path (string) relative- or absolute path to the file | |
-- skips non-existing file as well | |
-- returns (boolean) true on success | |
function filesystem.deletefile(path) | |
if filesystem.isfolder(path) then return false end | |
return select(2, shell.rm("-f", path)) | |
end | |
-- @path (string) relative- or absolute path to the file | |
-- returns (string) raw content of a file; or nil on failure | |
function filesystem.readfile(path, mode) | |
if type(mode) ~= "string" then mode = "rb" end | |
local file_pointer | |
if type(path) == "string" then | |
if not filesystem.isfile(path) then return nil end | |
file_pointer = io.open(path, mode) | |
else | |
file_pointer = path -- path is already a file handle | |
end | |
if not file_pointer then return nil end | |
local content = file_pointer:read("*a") | |
file_pointer:close() | |
return content | |
end | |
-- @path (string) relative- or absolute path to the file | |
-- returns (boolean) true on success, false on fail | |
function filesystem.writefile(path, data, mode) | |
if type(mode) ~= "string" then mode = "wb" end | |
local file_pointer | |
if type(path) == "string" then | |
if filesystem.isfolder(path) then return false end | |
if not filesystem.exists(path) then filesystem.makefile(path) end | |
file_pointer = io.open(path, mode) | |
else | |
file_pointer = path -- path is already a file handle | |
end | |
if not file_pointer then return false end | |
-- TODO? check permissions before write? | |
file_pointer:write(data) | |
file_pointer:close() | |
return true | |
end | |
-- @path (string) relative- or absolute path to the new (sub-)folder | |
-- folder name must not contain special characters, except: spaces, plus- & minus signs and underscores | |
-- does nothing to existing (sub-)folder or its contents | |
-- returns (boolean) true on success | |
function filesystem.makefolder(path) | |
if filesystem.isfile(path) then return false end | |
return select(2, shell.mkdir("-p", path)) | |
end | |
-- @path (string) relative- or absolute path to the (sub-)folder | |
-- deletes recursevly any sub-folder and its contents | |
-- skips non-existing folder | |
-- returns (boolean) true on success | |
function filesystem.deletefolder(path) | |
if filesystem.isfile(path) then return false end | |
return select(2, shell.rm("-rf", path)) | |
end | |
-- @path (string) relative- or absolute path to the file or (sub-)folder you want to copy | |
-- @location (string) is the new place of the copied resource, NOTE that this string can also contain a new name for the copied resource! | |
-- includes nested files and folders | |
-- returns (boolean) true on success | |
function filesystem.copy(path, location) | |
if not filesystem.exists(path) then return false end | |
return select(2, shell.cp("-a", path, location)) | |
end | |
-- @path (string) relative- or absolute path to the file or (sub-)folder you want to move to another location | |
-- @location (string) is the new place of the moved rosource, NOTE that this string can also contain a new name for the copied resource! | |
-- includes nested files and folders | |
-- returns (boolean) true on success | |
function filesystem.move(path, location) | |
if not filesystem.exists(path) then return false end | |
return select(2, shell.mv(path, location)) | |
end | |
-- @path (string) relative- or absolute path to folder or file | |
-- @rights (string or number) permission level, see http://permissions-calculator.org | |
-- fs.permissions(path) returns (string) an encoded 4 octal digit representing the permission level | |
-- fs.permissions(path, right) recursevly sets permission level and returns (boolean) true for successful assignment | |
function filesystem.permissions(path, right) | |
local fmt = "%03d" | |
if type(path) ~= "string" or not filesystem.exists(path) then return nil end | |
if type(right) == "number" then | |
-- NOTE seems you can not go below chmod 411 on MacOS | |
-- as the operating system resets it automatically to the next higher permission level | |
-- because the User (who created the file) at least holds a read access | |
-- thus trying to set rights to e.g. 044 would result in 644 | |
-- which means User group automatically gets full rights (7 bits instead of 0) | |
return select(2, shell.chmod("-R", string.format(fmt, right), path)) | |
end | |
if filesystem.os("darwin") then -- MacOS | |
return string.format(fmt, shell.cmd("stat", "-r", path, "|", "awk", "'{print $3}'", "|", "tail", "-c", "4")) | |
elseif filesystem.os("linux") then -- Linux | |
return shell.stat("-c", "'%a'", path) | |
end | |
return nil | |
end | |
-- @path (string) relative- or absolute path to a file or folder | |
-- returns directory path, filename, file extension and mime-type guessed by the file extension | |
-- NOTE .filetype is the operating system mime-type of the resource (file or folder), | |
-- while .mimetype is a web-compilant mime-type of the file judged by its file extension | |
function filesystem.fileinfo(path) | |
local t = {} | |
t.url = path | |
-- TODO remove mimetypeguess() in favor of .filemime() | |
t.mimetype, t.path, t.name, t.extension = mimetypeguess(t.url) -- same as .filemime but hardcoded | |
t.filemime = filesystem.filemime(t.url) -- .filetype + .filecharset | |
t.filetype = filesystem.filetype(t.url) | |
t.filecharset = filesystem.filecharset(t.url) | |
t.exists = filesystem.exists(t.url) | |
t.isfile = filesystem.isfile(t.url) | |
t.isfolder = filesystem.isfolder(t.url) | |
t.created = filesystem.createdat(t.url) | |
t.modified = filesystem.modifiedat(t.url) | |
t.checksum = filesystem.checksum(t.url) | |
t.permissions = filesystem.permissions(t.url) | |
return t | |
end | |
-- returns (string) current content of the system clipboard | |
function filesystem.readclipboard() | |
if filesystem.os("darwin") then -- MacOS | |
-- NOTE we could pass around specific formats | |
-- and by encode/decode these queries we could copy/paste application specific data | |
-- just like Adobe can transfer Photos from InDesign to Photoshop and back (or even settings) | |
return shell.pbpaste() --trim(sh.echo("`pbpaste`")) | |
elseif filesystem.os("linux") then-- TODO? Linux support via xclip | |
-- NOTE this makes no sense on a machine without a display, like is a webserver | |
-- see https://unix.stackexchange.com/questions/211817/copy-the-contents-of-a-file-into-the-clipboard-without-displaying-its-contents | |
end | |
return nil | |
end | |
-- @data (string) the content to insert into the clipboard | |
-- returns (boolean) true on success | |
function filesystem.writeclipboard(query) | |
if filesystem.os("darwin") then -- MacOS | |
return select(2, shell.cmd("echo", query, "|", "pbcopy")) | |
end | |
-- see NOTE above about Linux support | |
return false | |
end | |
-- @hyperthreading (optional boolean) to check against maximal resources instead of physically available once | |
-- returns (number) of cores this machine has (optionally counting the maximal utilization potential (@hyperthreading = true)) | |
function filesystem.cores(hyperthreading) | |
if filesystem.os("darwin") then -- MacOS | |
local pntr = hyperthreading and "hw.logicalcpu" or "hw.physicalcpu" | |
return trim(shell.sysctl(pntr, "|", "awk", "'{print $2}'")) | |
elseif filesystem.os("linux") then -- Linux | |
return trim(shell.nproc()) | |
end | |
end | |
-- returns (number) representing cpu workload in % percent | |
-- NOTE the workload could be grater than 100% if to much workload or not enough cores to handle it | |
function filesystem.cpu() | |
if filesystem.os("darwin") or filesystem.os("linux") then -- MacOS or Linux | |
-- NOTE @avgcpu can be grater than 100% if machine has multiple cores, e.g. up to 600% at 6 cores | |
-- it could also be larger than that, because of @hyperthreading (physical vs logical number of cores) | |
local avgcpu = trim(shell.ps("-A", "-o", "%cpu", "|", "awk", "'{s+=$1} END {print s}'")):gsub(",", ".") --% | |
local ncores = filesystem.cores() | |
local used = avgcpu * 100 / (ncores * 100) --% | |
local free = 100 - used --% | |
return used, free | |
end | |
end | |
-- returns (number) available ram space in kB | |
function filesystem.ram() | |
if filesystem.os("darwin") then -- MacOS | |
return trim(shell.sysctl("hw.memsize")) | |
elseif filesystem.os("linux") then -- Linux | |
return trim(shell.cat("/proc/meminfo", "|", "grep", "-i", "MemTotal", "|", "awk", "'{print $2}'")) | |
end | |
end | |
function filesystem.mem() | |
if filesystem.os("darwin") or filesystem.os("linux") then -- MacOS or Linux | |
local avgmem = trim(shell.ps("-A", "-o", "%mem", "|", "awk", "'{s+=$1} END {print s}'")):gsub(",", ".") --% | |
local rsize = filesystem.ram() --kB | |
local rfree = avgmem * rsize / 100 --kB | |
local rused = rsize - rfree --kB | |
local used = 100 - rused * 100 / rsize --% | |
local free = 100 - used --% | |
return used, free | |
end | |
end | |
-- returns (table) various information about the machine | |
function filesystem.sysinfo() | |
local t = {cpu = {}, mem = {}} | |
t.os = filesystem.os() | |
t.cores = filesystem.cores() | |
t.cpu.used, t.cpu.free = filesystem.cpu() -- in percent | |
t.ram = filesystem.ram() -- in kilobytes | |
t.mem.used, t.mem.free = filesystem.mem() -- in percent | |
return t | |
end | |
return filesystem |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment