Self Updating Package Script

Share your scripts and packages with other Mudlet users.
Jor'Mox
Posts: 1142
Joined: Wed Apr 03, 2013 2:19 am

Self Updating Package Script

Post by Jor'Mox »

So, someone requested a copy of my self updating package script, so here it is. Please keep in mind that it is still a work in progress, and this isn't stripped down to be generic (so it is still structured to be part of the larger script package that I built and maintain for a game I play). Basically, it grabs a version check file from a remote server, compares the versions listed there with the versions stored locally, and then downloads any files that are out of date. Once all files are downloaded, all of the files are loaded in sequence, using doFile. Comments and suggestions are welcome.
Code: [show] | [select all] lua
-- DSL PNP 4.0 Main Script
-- By: Zachary Hiland
-- 2/09/2014
-- Not actually hosted, shown as included in testing.xml

dslpnp = dslpnp or {}
dslpnp.update = dslpnp.update or {}
dslpnp.config = dslpnp.config or {}

local defaults = {
	general_scripts = {
		"Gauges",
		"Window Manager"
	},
	basic_scripts = {
		"growl",
		"triggers",
		"aliases",
		"timers",
		"fileIO",
		"extensions",
		"data",
	},
	scripts_list = {
		"borders",
		"statusbar",
		"support",
		"filesend",
		"character",
	},
	initialize = {
		"borders",
		"statusbar",
		"support",
		"filesend",
		"character",
	},
	connect_events = {
		"resetTimers",
		"resetTriggers",
		"resetAliases",
	},
	file_path = getMudletHomeDir() .. "/PNP/",
	download_path = "http://shatteredcloud.com/scripts/",
	package_name = "testing",
	version_check_download = "versions.txt",
	version_check_save = "version_check.txt",
	versions_file_name = "versions.txt",
}

local download_queue = download_queue or {}
local downloading = false
local available = {}
local unavailable = {}
local scripts_list = {}
local init_list = {}
local event_list = {}

-- Creates a duplicate of the given table
-- This is a local copy of table.copy used by table.update
local function table_copy(tbl)
	local new = {}
	for k,v in pairs(tbl) do
		if type(v) == "table" then
			new[k] = table_copy(v)
		else
			new[k] = v
		end
	end
	return new
end

-- Updates the first table with values from the second table
-- This is a local copy of table.update as it will function in the next Mudlet release
local function table_update(t1, t2)
	local tbl = table_copy(t1)
	for k,v in pairs(t2) do
		if type(v) == "table" then
			tbl[k] = table_update(tbl[k] or {}, v)
		else
			tbl[k] = v
		end
	end
	return tbl
end

local function fileOpen(filename,mode)
	local errors
	mode = mode or "read"
	assert(table.contains({"read","write","append","modify"},mode),"Invalid mode: must be 'read', 'write', 'append', or 'modify'.")
	if mode ~= "write" then
		local info = lfs.attributes(filename)
		if not info then
			errors = "Invalid filename: no such file."
			return nil, errors
		end
		if info.mode ~= "file" then
			errors = "Invalid filename: path points to a directory."
			return nil, errors
		end
	end
	local file = {}
	file.name = filename
	file.mode = mode
	file.type = "fileIO_file"
	file.contents = {}
	if file.mode == "read" or file.mode == "modify" then
		local tmp = io.open(file.name,"r")
		local linenum = 1
		for line in tmp:lines() do
			file.contents[linenum] = line
			linenum = linenum + 1
		end
		tmp:close()
	end
	setmetatable(file,dslpnp.fileIO)
	return file, nil
end

local function fileClose(file)
	assert(file.type == "fileIO_file", "Invalid file: must be file returned by fileIO.open.")
	local tmp
	if file.mode == "write" then
		tmp = io.open(file.name,"w")
	elseif file.mode == "append" then
		tmp = io.open(file.name,"a")
	elseif file.mode == "modify" then
		tmp = io.open(file.name,"w+")
	end
	if tmp then
		for k,v in ipairs(file.contents) do
			tmp:write(v .. "\n")
		end
		tmp:flush()
		tmp:close()
		tmp = nil
	end
	return true
end

-- THIS FUNCTION INITIALIZES SCRIPTS, GETS CALLED ON CONNECT/RECONNECT

function dslpnp.initialize()
	init_list = table.n_union(defaults.initialize, dslpnp.config.initialize or {})
	event_list = table.n_union(defaults.connect_events, dslpnp.config.connect_events or {})
	-- run required events
	for k,v in ipairs(event_list) do
		raiseEvent(v)
	end
	-- initialize scripts
	for k,v in ipairs(init_list) do
		raiseEvent("onConfig",v)
	end
end

local function load_package()
	-- uninstall old package
	uninstallPackage(defaults.package_name)
	-- install new package
	installPackage(defaults.file_path .. defaults.package_name .. ".xml")
end

local function check_available()
	for k, v in ipairs(scripts_list) do
		-- check if script is listed in available or unavailable table
		if (not table.contains(available, v)) and (not table.contains(unavailable,v)) then
			-- if script not accounted for, not all scripts are loaded, return false
			return false
		end
	end
	-- if all scripts accounted for, return true
	return true
end

local function load_scripts()
	local path, name
	if check_available() then
		for k, v in ipairs(available) do
			if table.contains(unavailable,v) then
				table.remove(unavailable,table.index_of(unavailable, v))
			end
			path = defaults.file_path .. v .. ".lua"
			name = string.gsub(v,"DSL_PNP_", "")
			if io.exists(path) then
				dofile(path)
				print("Script: " .. name .. " loaded successfully.")
			else
				print("Script: " .. name .. " does not exist.")
			end
		end
		echo("\n")
		for k,v in ipairs(unavailable) do
			name = string.gsub(v,"DSL_PNP_", "")
			print("Script: " .. name .. " not loaded.")
		end
		echo("\n")
		dslpnp.initialize()
	end
end

local function start_download()
	-- get info from queue
	local info = download_queue[1]
	if info then
		local path, address = info[1], info[2]
		-- remove current item from queue
		table.remove(download_queue,1)
		-- begin download
		downloadFile(path,address)
		downloading = true
	else
		load_scripts()
	end
end

local function queue_download(path, address)
	-- add item to queue
	table.insert(download_queue, {path, address})
	if not downloading then
		-- start new download if none in progress
		start_download()
	end
end

local function get_version_check()
	-- create PNP folder in Mudlet home directory if necessary
	lfs.mkdir(getMudletHomeDir() .. "/PNP")
	-- download current version info
	queue_download(defaults.file_path .. defaults.version_check_save, defaults.download_path .. defaults.version_check_download)
end

local function check_versions()
	local version_path = defaults.file_path .. defaults.versions_file_name
	local check_path = defaults.file_path .. defaults.version_check_save
	local new_version, old_version
	local check_file, version_file
	local found
	
	-- read in check file
	check_file = fileOpen(check_path,"read")
	if io.exists(version_path) then
		-- read in version file
		version_file = fileOpen(version_path,"modify")
	else
		-- create new version file
		version_file = fileOpen(version_path,"write")
	end
	
	-- check versions for all loaded scripts
	for k, v in ipairs(scripts_list) do
		new_version = nil
		found = false
		-- find new version info for current script
		for k2, v2 in ipairs(check_file.contents) do
			if string.find(v2,v) then
				found = true
				new_version = string.match(v2,"[%w%.]+ : ([%w%.]+)")
				old_version = nil
				-- find old version info for current script
				for k3, v3 in ipairs(version_file.contents) do
					if string.find(v3,v) then
						old_version = string.match(v3,"[%w%.]+ : ([%w%.]+)")
						-- update old version info
						version_file.contents[k3] = v2
						break
					end
				end
				
				-- compare version info
				if new_version ~= old_version then
					-- download new version of old script
					queue_download(defaults.file_path .. v .. ".lua", defaults.download_path .. v .. ".lua")
					if not old_version then
						-- insert missing version info
						table.insert(version_file.contents, v2)
					end
				else
					table.insert(available, v)
				end
				break
			end
		end
		-- scripts not found are listed as unavailable
		if not found then table.insert(unavailable, v) end
	end
	-- write new version info to file
	fileClose(version_file)
	-- close version check file
	fileClose(check_file)
	-- try to load scripts
	load_scripts()
end

local function finish_download(path)
	-- start next download in queue
	if download_queue[1] then
		start_download()
	else
		downloading = false
	end
	-- run version checking once file downloaded
	if string.find(path,defaults.version_check_save) then
		check_versions()
	elseif string.find(path,".xml") then
		load_package()
	else
		for k, v in ipairs(scripts_list) do
			if string.find(path,v) then
				table.insert(available,v)
				break
			end
		end
		if not downloading then
			load_scripts()
		end
	end
end

local function fail_download(...)
	-- begin next download
	start_download()
	-- add failed download to list of unavailable scripts
	table.insert(unavailable, arg[1])
end

function dslpnp.update.update_package()
	-- create PNP folder in Mudlet home directory if necessary
	lfs.mkdir(getMudletHomeDir() .. "/PNP")
	-- download newest package
	queue_download(defaults.file_path .. defaults.package_name .. ".xml", defaults.download_path .. defaults.package_name .. ".xml")
end

function dslpnp.update.update_scripts()
	available = {}
	unavailable = {}
	dslpnp.config.basic_scripts = table_update(defaults.basic_scripts, dslpnp.config.basic_scripts or {})
	dslpnp.config.scripts_list = dslpnp.config.scripts_list or defaults.scripts_list or {}
	dslpnp.config.general_scripts = table_update(defaults.general_scripts, dslpnp.config.general_scripts or {})
	scripts_list = table.n_union(dslpnp.config.basic_scripts, dslpnp.config.scripts_list)
	for k,v in ipairs(scripts_list) do
		scripts_list[k] = "DSL_PNP_" .. string.title(v)
	end
	scripts_list = table.n_union(scripts_list, dslpnp.config.general_scripts)
	get_version_check()
end

function dslpnp.update.clear_versions()
	local version_path = defaults.file_path .. defaults.versions_file_name

	if io.exists(version_path) then
		-- read in version file
		version_file = fileOpen(version_path,"modify")
		version_file.contents = {}
		fileClose(version_file)
	end
end

function dslpnp.update.eventHandler(event, ...)
	if event == "sysConnectionEvent" then
		dslpnp.update.update_scripts()
	elseif event == "sysDownloadDone" then
		finish_download(...)
	elseif event == "sysDownloadError" then
		fail_download(...)
	end
end

registerAnonymousEventHandler("sysConnectionEvent","dslpnp.update.eventHandler")
registerAnonymousEventHandler("sysDownloadDone", "dslpnp.update.eventHandler")
registerAnonymousEventHandler("sysDownloadError", "dslpnp.update.eventHandler")

icesteruk
Posts: 287
Joined: Sun Jan 20, 2013 9:16 pm

Re: Self Updating Package Script

Post by icesteruk »

Sorry to ask but im a little confused, what does this represent?



local defaults = {
general_scripts = {
"Gauges",
"Window Manager"
},
basic_scripts = {
"growl",
"triggers",
"aliases",
"timers",
"fileIO",
"extensions",
"data",
},
scripts_list = {
"borders",
"statusbar",
"support",
"filesend",
"character",
},
initialize = {
"borders",
"statusbar",
"support",
"filesend",
"character",
},
connect_events = {
"resetTimers",
"resetTriggers",
"resetAliases",
},
file_path = getMudletHomeDir() .. "/PNP/",


version_check_download = "version.txt",
version_check_save = "version_check.txt",
versions_file_name = "versions.txt",
}


the things I deleted I understand what that does its the others which Im unsure on :/

Jor'Mox
Posts: 1142
Joined: Wed Apr 03, 2013 2:19 am

Re: Self Updating Package Script

Post by Jor'Mox »

As stated, that is my script as is, not turned into some generalized format. So that is telling it what scripts to download, read in, and then later initialize for my script package. It also includes some events that it fires once everything is loaded up (because some of my scripts want those events), as well as the location to save files, and the file names of the file to download, what to save it as, and the local file to read to compare the downloaded version file to.

For most purposes, you can just use getMudletHomeDir instead of the file_path variable I have, most people won't need the connect_events or initialize tables at all, and the three different lists of script names can just be condensed into one list, because you don't need to have them broken down by category the way that I have them.

User avatar
keneanung
Site Admin
Posts: 94
Joined: Mon Mar 21, 2011 9:36 am
Discord: keneanung#2803

Re: Self Updating Package Script

Post by keneanung »

I've been working on something similar, though different as well lately: a package manager fo Mudlet.

The basic idea is to build something like apt on Ubuntu, wich is able to resolve dependencies, look for updates and find new packages hosted in one place. That would have the advantage, that not every package needs the update code and existing packages are easily integrated into the system. The disadvantage is, that a dedicated server instance is needed for my idea.

I have both components on my github, though they are not quite finished yet.

ETA: it might help to actually add my github, right? https://github.com/keneanung

Jor'Mox
Posts: 1142
Joined: Wed Apr 03, 2013 2:19 am

Re: Self Updating Package Script

Post by Jor'Mox »

So, I'm not going to lie, I find reading code in XML format rather difficult. But, from scanning what you have, do you basically just have it redownload all of the managed packages every time? Or do you have some sort of version management going on that I missed?

User avatar
keneanung
Site Admin
Posts: 94
Joined: Mon Mar 21, 2011 9:36 am
Discord: keneanung#2803

Re: Self Updating Package Script

Post by keneanung »

Of course not. The client stores the current version it has installed in a database (like you have in a plain text file) and is able to retrieve the version strings and dependencies as well as some other things of all available packages via JSON from the server(s). If a newer version is found on one of the servers, it downloads the package and installs it.

You can also install new packages via the package manager or uninstall existing packages (important, so it doesn't reinstall it on update).

User avatar
keneanung
Site Admin
Posts: 94
Joined: Mon Mar 21, 2011 9:36 am
Discord: keneanung#2803

Re: Self Updating Package Script

Post by keneanung »

Upon further analysis of your code we have a quite similar approach, although mine seems to be a bit less hardcoded.

I uploaded my current work in progress to github into https://github.com/keneanung/MudletPack ... ee/cleanup which contains some bugfixes but primarily localizing and creation of functions to have everything in one place.

phasma
Posts: 191
Joined: Sat Aug 03, 2013 7:00 pm
Discord: phasma#4694

Re: Self Updating Package Script

Post by phasma »

Yeah, I also do this, albeit differently. I'm loading my system via Lua modules which allows for quick and easy updating without the need to restart Mudlet.
Code: [show] | [select all] lua
-- Module loading and unloading

function load_modules(check)
	local path = package.path
	local cpath = package.cpath
	local home_dir = getMudletHomeDir()
	local lua_dir = string.format("%s/%s", home_dir, [[?.lua]])
	local init_dir = string.format("%s/%s", home_dir, [[?/init.lua]])
	local sysdir = string.format("%s/%s", getMudletHomeDir() .. sep .. "serenity" .. sep .. "system", [[?.lua]])
	package.path = string.format("%s;%s;%s;%s", path, lua_dir, init_dir, sysdir)
	package.cpath = string.format("%s;%s;%s;%s", cpath, lua_dir, init_dir, sysdir)

	local m = tmp.loaded and { "_gmcp", "queue", "core", "genrun", "settings", "ui" } or { "install" }
	for _, n in ipairs(m) do
		local s, c = pcall(require, n)
		if not s then display(c) e:error("Failed to load module: " .. n .. ".lua. Please contact support.") end
		_G[n] = c
	end

	package.path = path
	package.cpath = cpath
end

function reload_modules()
	local m = tmp.loaded and { "_gmcp", "queue", "core", "genrun", "settings", "ui" } or { "install" }
	e:echo("Reinitialising Lua package 'serenity'...")
	--resetProfile() - Todo. This function really should have the option to raise an event after successful execution.
	send("\n")
	package.loaded.serenity = nil
	e:echo("Performing live update.")
	e:echo("Unloading modules...")

	for _, n in ipairs(m) do
		package.loaded[n] = nil
	end

	e:echo("Reloading system now...")

	load_modules()
end

function get_modules()
	local exceptions = {
		"string",
		"package",
		"_G",
		"os",
		"table",
		"math",
		"coroutine",
		"luasql",
		"debug",
		"rex_pcre",
		"lfs",
		"io",
		"luasql.sqlite3",
		"gmod",
		"zip",
		"socket"
	}

	local modules = {}

	for m in pairs(package.loaded) do
		if not table.contains(exceptions, m) then
			table.insert(modules, m)
		end
	end

	return modules
end

function check_module(name)
	return package.loaded[name] and true or false
end

load_modules()
Then the Mudlet side of things:
Code: [show] | [select all] lua
function init()
	if not lfs.attributes(getMudletHomeDir() .. sep .. "serenity" .. sep .. "serenity.lua") then
		tmp.loaded = false
		raiseEvent("system installer")
	else
		raiseEvent("load serenity")
	end
end
Code: [show] | [select all] lua
function installer()
	e:echo("First installation detected. Please wait while we initialise the system...")

	lfs.mkdir(sysdir) -- Create main Serenity system directory

	local dirs = { "images", "updates", "settings", "logs", "api_data", "system" } -- Define and create system subdirectories

	for _, dir in ipairs(dirs) do
		lfs.mkdir(sysdir .. sep .. dir)
	end

	if lfs.attributes(sysdir .. sep .. "system") then
		e:echo("Initialisation complete! Proceeding to load system...")
		raiseEvent("load serenity")
	else
		e:error("An error has occurred during system initalisation. Please contact support.")
		return
	end
end
Code: [show] | [select all] lua
function loader()
	e:echo("System loading. Please wait...")

	local path = package.path
	local cpath = package.cpath
	local home_dir = getMudletHomeDir()
	local lua_dir = string.format("%s/%s", home_dir, [[?.lua]])
	local init_dir = string.format("%s/%s", home_dir, [[?/init.lua]])
	local sysdir = string.format("%s/%s", getMudletHomeDir() .. sep .. "serenity", [[?.lua]])
	package.path = string.format("%s;%s;%s;%s", path, lua_dir, init_dir, sysdir)
	package.cpath = string.format("%s;%s;%s;%s", cpath, lua_dir, init_dir, sysdir)

	local success, content = pcall(require, "serenity")
	package.path = path
	package.cpath = cpath
	if success then
		e:echo("System loaded! Sup?")
	else
		return e:error("Fatal error loading main system module: " .. content)
	end
end
Still a work in progress, but yet another way this can be accomplished.

icesteruk
Posts: 287
Joined: Sun Jan 20, 2013 9:16 pm

Re: Self Updating Package Script

Post by icesteruk »

I just noticed most of you guys have things like -

function get_modules()
local exceptions = {
"string",
"package",
"_G",
"os",
"table",
"math",
"coroutine",
"luasql",
"debug",
"rex_pcre",
"lfs",
"io",
"luasql.sqlite3",
"gmod",
"zip",
"socket"
}
-

How or what do you do to get that? Or what does that represent? As each time I export/package manger export it only gives me a .zip of the whole system...

phasma
Posts: 191
Joined: Sat Aug 03, 2013 7:00 pm
Discord: phasma#4694

Re: Self Updating Package Script

Post by phasma »

Just display the package.loaded table for that info.

It's less a Mudlet thing and more a Lua thing. Because of the way I load my modules and update, I had to add that blacklist as setting those particular modules to nil would be.... Bad, to say the least.

Post Reply