--------------------------------------------------------------------------------------------------------------------- ----- Dr.Web Server update script. Version 12.0 2019/11/11 --------------------------------------------------------------------------------------------------------------------- -- log levels ll_info = "[SCRIPT INF]" ll_warn = "[SCRIPT WRN]" ll_err = "[SCRIPT ERR]" ll_dbg = "[SCRIPT DBG]" -- print adapter for forms table function log(level, msg) print(level .. msg) return true end function pcall( ... ) local pack = table.pack local pars = pack( ... ) return xpcall( pars[1], function(...) log( ll_err, "Error <" .. table.concat( pack( ... ), ' ' ) .. "> stack:" ) local getinfo = debug.getinfo for i = 2,999999 do local info = getinfo( i, "nSl" ) if not info then break end local msg = i .. ". " if info.namewhat and info.namewhat ~= '' then msg = msg .. '"' .. info.namewhat .. '" ' end msg = msg .. info.what .. "-function" if info.name and info.name ~= '' then msg = msg .. ' (' .. info.name .. ')' end if info.what ~= "C" then if info.currentline == -1 then info.currentline = 0 end if info.linedefined == -1 then info.linedefined = 0 end if info.lastlinedefined == -1 then info.lastlinedefined = 0 end msg = msg .. ", line " .. info.currentline .. " (" .. info.linedefined .. "-" .. info.lastlinedefined .. ') "' .. info.source .. '"' end log( ll_err, msg ) end return ... end, table.unpack( pars, 2 ) ) end -- checks function result which returns -- either (true) or (false, error_message) -- in @res and @err and displays message from -- @fail_msg and reason from @err in case of -- error; function check(fail_msg, res, msg) if not res then if not msg then msg = "unknown error" end log(ll_err, fail_msg .. " because of " .. msg) return res, msg else return res end end -- change delimiters to native, drop duplicates, drop trailing delimiter function adjust_path(path, delimiter) local ret = '' local prev_slash = false path:gsub('.', function(char) if ( char == '/' or char == '\\' ) then if not prev_slash then prev_slash = true ret = ret .. delimiter end else prev_slash = false ret = ret .. char end end ) return ( prev_slash and #ret ~= 1 ) and ret:sub(1, #ret-1) or ret end function restore_configs( path ) local res = false local msg = 'unknown error' local function walk_cb(root, _, files) for _, file in pairs( files ) do local distr_ext = file:find( '%.distr$' ) if distr_ext ~= nil then local basename = file:sub( 1, distr_ext-1 ) local distr = root .. '/' .. file local work = root .. '/' .. basename if not dwupd.file_exists( work ) then log( ll_info, string.format( "Restore `%s' configuration file from `%s.distr'", basename, basename ) ) res, msg = check( "Unable to restore `" .. basename .. "'", dwupd.copy_file( distr, work ) ) if not res then return res end end end end return true end local res = check( "Unable to walk directory `" .. path .. "'", dwupd.walk_dir( path, walk_cb ) ) return check( "Unable to restore configs in `" .. path .. "'", res, msg) end local function is_excluded( path, excludes ) if not excludes then return false end local path = adjust_path( path ) for _, pattern in pairs( excludes ) do if path:match( pattern ) then return true end end return false end function webmin_symlink_clean( webmin, what ) log( ll_info, "Clean symlink stuff related to `" .. what .. "'" ) local pattern = "^" .. what .. "%.%d+$" dwupd.walk_dir( webmin, function( _, dirs, _ ) for _,dir in pairs(dirs) do if dir == what or dir:match( pattern ) then log( ll_dbg, "Delete found `" .. dir .. "'" ) delete( adjust_path( webmin .. '/' .. dir ) ) end end return false end ) return true end local function webmin_symlink( webmin, backup, what, rescue ) local path = adjust_path( webmin .. "/" .. what ) local backup_path = adjust_path( backup .. "/" .. what ) if dwupd.is_symlink( path ) then log( ll_dbg, "`" .. path .. "' is already symlink, skip" ) return true end local res local path_0 = path .. ".0" if dwupd.is_dir( path ) then log( ll_info, "`" .. path .. "' is a directory, backup, rename and create symlink" ) res = copy( path, backup_path ); if not res then return res end res = check( "Unable to rename `" .. path .. "' to `" .. path_0 .. "'", dwupd.rename( path, path_0 ) ) if not res then return res end rescue( copy, backup_path, path ) elseif not dwupd.file_exists( path ) then log( ll_info, "`" .. path .. "' not exists, make empty directory and create symlink" ) res = check( "Unable to mkdir `" .. path .. "'", dwupd.mkdir( path_0 ) ) if not res then return res end else return false, "unexpected file found at `" .. path .. "'" end rescue( webmin_symlink_clean, webmin, what ) res = check( "Unable to link `" .. path .. "' -> `" .. path_0 .. "'", dwupd.create_symlink( path, path_0, false, true ) ) return res end local function fill_registry( func_registry ) func_registry[ webmin_symlink_clean ] = "webmin_symlink_clean" end ------------------------------------------------------------------------------------------------------------- windows ES_SERVICE_NAME = "DrWebES" params = dwupd.cmd_params -- execution mode master (default), update, revert mode = params.mode or "master" -- more logging verbose = params.verbose == "true" local unpack = table.unpack local insert = table.insert if mode ~= "help" then -- service executable, like "C:\Program Files\DrWeb Server\bin\drwcsd.exe" service = params.service -- repository revisions @from which @to update, like "20000101000000000" from_rev = params.from to_rev = params.to adm_login = params.admlogin or '' adm_addr = params.admaddr or '' -- current platform, like "windows-nt-x64" platform = params.platform -- server's home directory, like "C:\Program Files\DrWeb Server" home = params.home -- server's var directory, like "C:\Program Files\DrWeb Server" var = params.var or home -- server's etc directory, like "C:\Program Files\DrWeb Server\etc" etc = params.etc or var .. "\\etc" -- server's bin-root directory, like "C:\Program Files\DrWeb Server" bin_root = params.bin or home -- server's bin directory, like "C:\Program Files\DrWeb Server\bin" bin = bin_root .. "\\bin" -- server's tmp directory, like "C:\Program Files\DrWeb Server\tmp" tmp = params.tmp or home .. "\\tmp" -- where desired revision of server is stored revision = var .. "\\repository\\20-drwcs\\" .. to_rev -- where platform specific components are situated repo = revision .. "\\" .. platform -- where common components are situated common = revision .. "\\common" -- where old components will be placed (or taken in case of revert mode) update_root = var .. "\\update" backup_root = update_root .. "\\backup" backup = backup_root .. "\\" .. from_rev .. "-" .. to_rev -- path to this updater updater = params.updater or ".\\drwupsrv.exe" -- path to this script script = params.script or "update.lua" -- path to a file which shall exist during upgrade lock = params.lock -- log rotate params rotate = params.rotate or "10,10M" end -- translate process return code -- into a (ok, err) return format function proc_ret(what, rc) if rc == 0 then return true else return false, "unable to " .. what .. ", process exit code is " .. format_execute_rc(rc, err) end end local adjust_path_orig = adjust_path adjust_path = function(path) return adjust_path_orig(path, '\\') end function common_srv_cmdline(action) return "silent -home=\"" .. home .. "\" -etc=\"" .. etc .. "\" -bin=\"" .. bin_root .. "\" -var-root=\"" .. var .. "\" -verb=ALL" .. " -rotate=" .. rotate .. " -log=\"" .. var .. "\\updater-" .. action .. ".log\"" end -- command server @service to start function start_server(service) log(ll_info, "Starting server through SCM") local scm = dwupd.srv_open_manager() local es = dwupd.srv_open(scm, service) if not es then return false, "unable to open service `" .. service .. "'" end local state = dwupd.srv_query_state(es) if state == "running" or state == "start_pending" then log(ll_info, "Server is already in state `" .. state .. "'" ) return true end local res = dwupd.srv_start(es, 3600) if not res then return true else return false, res end end -- command server @service to stop function stop_server(service) log(ll_info, "Stopping server through SCM") local scm = dwupd.srv_open_manager() local es = dwupd.srv_open(scm, service) if not es then return false, "unable to open service `" .. service .. "'" end local state = dwupd.srv_query_state(es) if state == "stopped" or state == "stop_pending" then return true end local res = dwupd.srv_stop(es, 3600000) if not res then return true else return false, res end end -- command server through @service_script to -- export database to @dbexport file function export_db(service_script, dbexport) log(ll_info, "Exporting database to `" .. dbexport .. "'") local rc, err = dwupd.execute(service_script, common_srv_cmdline("update") .. " xmlexportdb \"" .. dbexport .. "\"") log(ll_info, "Exporting database process exit code is " .. format_execute_rc(rc, err)) if rc ~= 0 and dwupd.file_exists(dbexport) then delete(dbexport) end return proc_ret("export database", rc) end -- command server through @service_script to -- import database from @dbexport file function import_db(service_script, dbexport) log(ll_info, "Importing database from `" .. dbexport .. "'") local rc, err = dwupd.execute(service_script, common_srv_cmdline("revert") .. " xmlimportdb \"" .. dbexport .. "\"") log(ll_info, "Importing database process exit code is " .. format_execute_rc(rc, err)) return proc_ret("import database", rc) end -- command server through @service_script to -- update database with @sql script function update_db(service_script, sql, action) log(ll_info, "Updating database with `" .. sql .. "'") local rc, err = dwupd.execute(service_script, common_srv_cmdline(action) .. " updatedb \"" .. sql .. "\"") log(ll_info, "Updating database process exit code is " .. format_execute_rc(rc, err)) return proc_ret("update database", rc) end -- command server through @service_script to -- initialize database with @agent_key key and -- @sql script function init_db(service_script, agent_key) log(ll_info, "Initializing database with `" .. agent_key .. "' key") local rc, err = dwupd.execute(service_script, common_srv_cmdline("revert") .. " initdb") log(ll_info, "Initializing database process exit code is " .. format_execute_rc(rc, err)) return proc_ret("initialize database", rc) end -- command server through @service_script to -- to clean database function clean_db(service_script) log(ll_info, "Cleaning database") local rc, err = dwupd.execute( service_script, common_srv_cmdline("revert") .. " cleandb" ) log(ll_info, "Cleaning database process exit code is " .. format_execute_rc(rc, err)) return proc_ret("clean database", rc) end -- storage for rescue forms local rescue_forms = {} -- store a form in a LIFO manner, so that we -- could revert actions caused by upgrade. function rescue(...) insert(rescue_forms, 1, {...}) return true end -- command server through @service_script to upgrade database function upgrade_db(service_script) log(ll_info, "Check database schema actuality") local rc, err = dwupd.execute(service_script, common_srv_cmdline("update") .. " upgradedb -- -") if rc == 0 then log(ll_info, "Database schema is already actual, skip upgrading") return true end log(ll_info, "Need to stop server for database upgrading") local res, msg = stop_server( ES_SERVICE_NAME ) if not res then return false, msg end log( ll_info, "Backing up DB to `" .. backup .. "\\dbexport.gz'" ) res, msg = export_db( service, backup .. "\\dbexport.gz" ) if not res then return false, msg end log(ll_info, "Upgrading database") local rc, err = dwupd.execute(service_script, common_srv_cmdline("update") .. " upgradedb \"" .. home .. "\\update-db\"") log(ll_info, "Upgrading database process exit code is " .. format_execute_rc(rc, err)) -- revert database before server starts on rollback local rescue_form = { progn, { { clean_db, service }, { init_db, service, etc .. "\\agent.key" }, { import_db, service, backup .. "\\dbexport.gz" } } } insert(rescue_forms, #rescue_forms, rescue_form) if verbose then log(ll_info, "Added following revert forms: \n" .. join(rescue_form)) end return proc_ret("upgrade database", rc) end -- copies a file from path @from to path @to function copy_file(from, to, excludes) if is_excluded( from, excludes ) then log(ll_info, "Do not copy file `" .. from .."' due to excludes") elseif not dwupd.file_exists(to) then log(ll_info, "Destination file `" .. to .. "' doesn't exist, copy from `" .. from .. "'") local res, msg = dwupd.copy_file(from, to) if not res then return res, msg end elseif dwupd.is_file(to) then if not dwupd.are_files_equal(from, to) then log(ll_info, "Destination file `" .. to .. "' is not the same as a source one, copy from `" .. from .. "'") local res, msg = dwupd.remove_or_rename(to) if not res then return res, msg end res, msg = dwupd.copy_file(from, to, true) if not res then return res, msg end else log(ll_info, "Destination file `" .. to .. "' is the same as a source one `" .. from .. "', won't copy") end else return false, "Destination for file `" .. from .. "' is not a file `" .. to .. "'" end return true end -- copies a directory from path @from to path @to function copy_dir(from, to, excludes) local from_len = string.len(from) local res = false local msg = "unknown error" local function walk_cb(root, _, files) local to_root = to .. "\\" .. string.sub(root, from_len + 1) .. "\\" if is_excluded( root, excludes ) then log(ll_info, "Do not copy directory `" .. to_root .."' due to excludes") return true elseif not dwupd.file_exists(to_root) then log(ll_info, "Destination directory `" .. to_root .. "' doesn't exist, creating...") res, msg = check("Unable to create directory `" .. to_root .. "'", dwupd.mkdir(to_root)) if not res then return false end elseif dwupd.is_file(to_root) then res = false msg = "Destination for directory `" .. root .. "' is not a directory `" .. to_root .. "'" return false end local root = root .. "\\" local from_file, to_file for _,file in pairs(files) do from_file = root .. file to_file = to_root .. file res, msg = copy_file(from_file, to_file, excludes) if not res then return false end end return true end res = check("Unable to walk directory `" .. from .. "'", dwupd.walk_dir(from, walk_cb)) return check("Unable to copy directory `" .. from .. "' to `" .. to .. "'", res, msg) end -- removes files which are present in -- @target but not in @master directory -- unless file name is in @excludes function clean(target, master, excludes) local target_len = string.len(target) local res = false local msg = "unknown error" local excludes = excludes or {} local function walk_cb(root, _, files) if is_excluded( root, excludes ) then log(ll_info, "Do not clean directory `" .. root .."' due to excludes") return true end local master_root = master .. "\\" .. string.sub(root, target_len + 1) .. "\\" if not dwupd.file_exists(master_root) then res, msg = check("Unable to delete directory `" .. root .. "'", delete(root)) if not res then return false end else local root = root .. "\\" local root_file, master_file for _,file in pairs(files) do root_file = root .. file if not is_excluded( root_file, excludes ) then master_file = master_root .. file if not dwupd.file_exists(master_file) then res, msg = check("Unable to delete file `" .. root_file .. "'", dwupd.remove_or_rename(root_file)) if not res then return false end end end end end return true end log(ll_info, string.format( "Cleaning `%s' comparing to `%s' with%s%s", target, master, excludes and " excludes: " or "out excludes", excludes and join(excludes) or "")) res = check("Unable to walk directory `" .. target .. "'", dwupd.walk_dir(target, walk_cb)) return check("Unable to clean directory `" .. target .. "' comparing to master directory `" .. master .. "'", res, msg) end -- copies directory or file -- from path @from to path @to function copy(from, to, excludes) log(ll_info, string.format( "Copying `%s' to `%s' with%s%s", from, to, excludes and " excludes: " or "out excludes", excludes and join(excludes) or "")) if dwupd.is_file(from) then return copy_file(from, to, excludes) else return copy_dir(from, to, excludes) end end -- deletes directory or file at @path function delete(path) log(ll_info, "Deleting `" .. path .. "'") if dwupd.is_file(path) then return check("Failed to remove file `" .. path .. "'", dwupd.remove_or_rename(path)) else return check("Failed to remove directory `" .. path .. "'", dwupd.remove_dir(path)) end end -- creates directory at @path function mkdir(path) log(ll_info, "Making directory `" .. path .. "'") return check("Failed to make directory `" .. path .. "'", dwupd.mkdir(path)) end -- if @path exist calls @continuation -- with the rest of arguments function ifexist(path, continuation, ...) if dwupd.file_exists(path) then return continuation(...) end return true end -- takes a list @lst and returns -- it without a first element local function rest(lst) local nl = { } local i = 2 local curr = lst[i] while curr ~= nil do insert(nl, curr) i = i + 1 curr = lst[i] end return nl end -- executes @forms sequentially until -- no more left or error occured function progn(forms) local func, args local ret, msg for _, form in pairs(forms) do func = form[1] args = rest(form) ret, msg = func(unpack(args)) if not ret then return ret, msg end end return true end -- writes specified string -- @what to a file with @path function spit(what, path) return dwupd.spit(what, path) end -- generates SQL script to save -- success status into a database function success_script(from_rev, to_rev) local rec_uuid = dwupd.uuid() local timestamp = dwupd.now_timestamp() local subsys, admlogin local admaddr = adm_addr:gsub("'", "''") local operation = "10605" local finish_status = "1" if #adm_login ~= 0 then subsys = "1" admlogin = adm_login:gsub("'", "''") else subsys = "3" admlogin = '' end return "INSERT INTO admin_activity(record,subsys,login,address,oper,status,createtime)" .. " VALUES('" .. rec_uuid .. "'," .. subsys .. ",'" .. admlogin .. "','" .. admaddr .. "'," .. operation .. "," .. finish_status .. "," .. timestamp .. "); " .. "INSERT INTO activity_data(record,item,value,createtime)" .. " VALUES('" .. rec_uuid .. "','FromRev','" .. from_rev .. "'," .. timestamp .. "); " .. "INSERT INTO activity_data(record,item,value,createtime)" .. " VALUES('" .. rec_uuid .. "','ToRev','" .. to_rev .. "'," .. timestamp .. ");" end -- generates SQL script to save -- fail status into a database function fail_script(msg) local rec_uuid = dwupd.uuid() local timestamp = dwupd.now_timestamp() local subsys, admlogin local admaddr = adm_addr:gsub("'", "''") local operation = "10605" local fail_status = "2" msg = msg:gsub("'", "''") -- so sql apostrophes are quoted if #adm_login ~= 0 then subsys = "1" admlogin = adm_login:gsub("'", "''") else subsys = "3" admlogin = '' end return "INSERT INTO admin_activity(record,subsys,login,address,oper,status,createtime)" .. " VALUES('" .. rec_uuid .. "'," .. subsys .. ",'" .. admlogin .. "','" .. admaddr .. "'," .. operation .. "," .. fail_status .. "," .. timestamp .. "); " .. "INSERT INTO activity_data(record,item,value,createtime)" .. " VALUES('" .. rec_uuid .. "','Error','" .. msg .. "'," .. timestamp .. ");" end -- calls @func and always reports success function ignore(func, ...) local ok, msg = func(...) if not ok then log(ll_err, "ignore: function `" .. func_name(func) .. "' failed because of " .. msg) end return true end -- decorate with hook dispatching by first argument local function hook_path(f, hooks) return function(path, ...) path = adjust_path(path) local hook = hooks[path] if not hook then return f(path, ...) else return hook(path, ...) end end end -- remember before hooking local copy_file_orig = dwupd.copy_file local server_stop_hook_called = false local upgrade_forms = {} local server_stopped = false -- command server to stop through @service name -- and remember to start it on success or rollback function safe_stop_server() if server_stopped then return true end local res, msg = stop_server(ES_SERVICE_NAME) if not res then return res, msg end -- checks if @forms already contain `start_server' -- form at the end local function needs_start_server_form(forms) local last_form = forms[#forms] if last_form then local func = last_form[1] return func ~= start_server else return true end end local start_server_form = { start_server, ES_SERVICE_NAME } if needs_start_server_form(rescue_forms) then -- server is stopped, need to start in case of rollback insert(rescue_forms, start_server_form) end if needs_start_server_form(upgrade_forms) then -- start server at the end insert(upgrade_forms, start_server_form) if verbose then log(ll_info, "Added following forms to execute: \n" .. join(start_server_form)) end end server_stopped = true return true end -- called when server needs to be stopped local function server_stop_hook(from, to, overwrite) -- call once if server_stop_hook_called then return copy_file_orig(from, to, overwrite) end server_stop_hook_called = true log(ll_info, "Need to stop server, because `" .. to .. "' is going to be replaced by `" .. from .. "'") local res, msg = safe_stop_server() if not res then return res, msg end return copy_file_orig(from, to, overwrite) end local where_bin = mode == "revert" and backup or repo -- setup hooks for copy_file local copy_hooks = { [adjust_path(where_bin .. "\\bin\\drwcsd.exe")] = server_stop_hook, } local bin_dir = where_bin .. "\\bin" -- generate hooks dynamically check("Unable to walk directory `" .. bin_dir .. "'", dwupd.walk_dir(bin_dir, function(root, _, files) for _,file in pairs(files) do if string.match(file, ".dll$") then copy_hooks[adjust_path(root .. "\\" .. file)] = server_stop_hook end end return true end)) -- used by func_name see below; -- register any function You use -- in upgrade_forms, or else =) func_registry = { [rescue] = "rescue", [copy] = "copy", [delete] = "delete", [mkdir] = "mkdir", [upgrade_db] = "upgrade_db", [update_db] = "update_db", [init_db] = "init_db", [clean_db] = "clean_db", [import_db] = "import_db", [export_db] = "export_db", [start_server] = "start_server", [stop_server] = "stop_server", [log] = "log", [ifexist] = "ifexist", [spit] = "spit", [ignore] = "ignore", [clean] = "clean", [progn] = "progn", [safe_stop_server] = "safe_stop_server", [restore_configs] = "restore_configs" } fill_registry( func_registry ) -- translates function's @func address into -- a name through func_registry table function func_name(func) return func_registry[func] or tostring(func) end if mode == "upgrade" or mode == "debug" then local ucrt_excludes if not dwupd.file_exists(bin .. "\\ucrtbase.dll") then ucrt_excludes = { ".+\\ucrtbase%.dll$", ".+\\api%-ms%-.*%.dll$" } end local webmin_dont_clean = { -- this directories are modified by the `drwextra' and `drwagent' products ".+\\webmin\\install$", ".+\\webmin\\install%.%d+$", ".+\\webmin\\install\\", ".+\\webmin\\install%.%d+\\", ".+\\webmin\\splitted%-install$", ".+\\webmin\\splitted%-install%.%d+$", ".+\\webmin\\splitted%-install\\", ".+\\webmin\\splitted%-install%.%d+\\", ".+\\webmin\\utilities$", ".+\\webmin\\utilities%.%d+$", ".+\\webmin\\utilities\\", ".+\\webmin\\utilities%.%d+\\" } -- the table of forms interpreted -- by `run' when server upgrade is run upgrade_forms = { { ifexist, backup, delete, backup }, { log, ll_info, "Creating backup directory `" .. backup .. "'" }, { mkdir, backup }, { mkdir, backup .. "\\.clean" }, { rescue, ifexist, backup .. "\\.clean", progn, { { log, ll_info, "Deleting backup directory after fail" }, { delete, backup } } }, { log, ll_info, "Backing up `" .. home .. "' to `" .. backup .. "'" }, { copy, bin , backup .. "\\bin" }, { copy, etc, backup .. "\\etc" }, { copy, home .. "\\ds-modules", backup .. "\\ds-modules" }, { copy, home .. "\\fonts", backup .. "\\fonts" }, { copy, home .. "\\vfs", backup .. "\\vfs" }, { copy, home .. "\\webmin", backup .. "\\webmin", webmin_dont_clean }, { copy, home .. "\\websockets", backup .. "\\websockets" }, { delete, backup .. "\\.clean" }, { log, ll_info, "Checking webmin symlinks" }, { webmin_symlink, home .. "\\webmin", backup .. "\\webmin", "install", rescue }, { webmin_symlink, home .. "\\webmin", backup .. "\\webmin", "utilities", rescue }, { log, ll_info, "Upgrading `" .. bin .. "'" }, { rescue, progn, { { copy, backup .. "\\bin", bin }, { clean, bin , backup .. "\\bin" } } }, { copy, repo .. "\\bin", bin, ucrt_excludes }, { clean, bin, repo .. "\\bin" }, { log, ll_info, "Upgrading `" .. home .. "\\ds-modules'" }, { rescue, progn, { { copy, backup .. "\\ds-modules", home .. "\\ds-modules" }, { clean, home .. "\\ds-modules", backup .. "\\ds-modules" } } }, { copy, common .. "\\ds-modules", home .. "\\ds-modules" }, { clean, home .. "\\ds-modules", common .. "\\ds-modules" }, { log, ll_info, "Upgrading `" .. home .. "\\fonts'" }, { rescue, progn, { { copy, backup .. "\\fonts", home .. "\\fonts" }, { clean, home .. "\\fonts", backup .. "\\fonts" } } }, { copy, common .. "\\fonts", home .. "\\fonts" }, { clean, home .. "\\fonts", common .. "\\fonts" }, { log, ll_info, "Upgrading `" .. home .. "\\vfs'" }, { rescue, progn, { { copy, backup .. "\\vfs", home .. "\\vfs" }, { clean, home .. "\\vfs", backup .. "\\vfs" } } }, { copy, common .. "\\vfs", home .. "\\vfs" }, { clean, home .. "\\vfs", common .. "\\vfs" }, { log, ll_info, "Upgrading `" .. home .. "\\webmin'" }, { rescue, progn, { { copy, backup .. "\\webmin", home .. "\\webmin", webmin_dont_clean }, { clean, home .. "\\webmin", backup .. "\\webmin", webmin_dont_clean } } }, { copy, common .. "\\webmin", home .. "\\webmin", webmin_dont_clean }, { clean, home .. "\\webmin", common .. "\\webmin", webmin_dont_clean }, { log, ll_info, "Upgrading `" .. home .. "\\websockets'" }, { rescue, progn, { { copy, backup .. "\\websockets", home .. "\\websockets" }, { clean, home .. "\\websockets", backup .. "\\websockets" } } }, { copy, common .. "\\websockets", home .. "\\websockets" }, { clean, home .. "\\websockets", common .. "\\websockets" }, { log, ll_info, "Upgrading `" .. etc .. "'" }, { rescue, progn, { { copy, backup .. "\\etc", etc }, { clean, etc, backup .. "\\etc" } } }, { copy, common .. "\\etc", etc }, { restore_configs, etc }, { update_db, service, etc .. "\\upgrade-conf.lua", "update" }, { spit, success_script(from_rev, to_rev), backup .. "\\postupgrade.sql" }, { update_db, service, backup .. "\\postupgrade.sql", "update" } } local upgrade_db_hook_called = false -- called when server needs to upgrade database local function upgrade_db_hook(from, to, overwrite) -- call once if upgrade_db_hook_called then return copy_file_orig(from, to, overwrite) end upgrade_db_hook_called = true -- need to stop server before upgrading database local res, msg = safe_stop_server() if not res then return res, msg end -- prepare forms for database upgrade local upgrade_db_forms = { { upgrade_db, service } } -- we need to upgrade database before server -- starts by form inserted in server_stop_hook local where = #upgrade_forms - 1 for i,form in pairs(upgrade_db_forms) do insert(upgrade_forms, where + i - 1, form) end if verbose then log(ll_info, "Added following forms to execute: \n" .. join(upgrade_db_forms)) end return copy_file_orig(from, to, overwrite) end copy_hooks[adjust_path(where_bin .. "\\bin\\database\\drwdatabase-manager.dll")] = upgrade_db_hook copy_hooks[adjust_path(where_bin .. "\\bin\\drwupsrv.exe")] = function(from, to) log(ll_info, "Skipping copy `" .. from .. "' to `" .. to .. "' because updater replaces itself independently") return true end end -- hook dwupd.copy_file, dispatch by first arg, -- that is look at source files (from repo) to -- determine hook to call dwupd.copy_file = hook_path(dwupd.copy_file, copy_hooks) local remove_file_orig = dwupd.remove_or_rename local remove_hook_called = false -- called when server needs to be stopped local function remove_hook(what) -- call once if remove_hook_called then return remove_file_orig(what) end remove_hook_called = true log(ll_info, "Need to stop server, because `" .. what .. "' is going to be deleted") local res, msg = safe_stop_server() if not res then return res, msg end return remove_file_orig(what) end local remove_hooks = {} -- generate hooks for delettion operations for _,dir in pairs({ bin }) do check("Unable to walk directory `" .. dir .. "'", dwupd.walk_dir(dir, function(root, _, files) for _,file in pairs(files) do if file ~= "drwupsrv.exe" then remove_hooks[adjust_path(root .. "\\" .. file)] = remove_hook end end return true end)) end -- hook dwupd.remove_or_rename, dispatch by first arg dwupd.remove_or_rename = hook_path(dwupd.remove_or_rename, remove_hooks) -- prints updater script's parameters function print_params() log(ll_info, "Script parameters: ") for name, value in pairs(params) do log(ll_info, " " .. name .. " = " .. value) end end -- translates table @tbl into a textual -- representation parsable by Lua interpreter function join(tbl, sep) local res = "" local x_type sep = sep or ", " for _, x in pairs(tbl) do if string.len(res) ~= 0 then res = res .. sep end x_type = type(x) if x_type == "string" then res = res .. '[=[' .. string.gsub(x, "([%[%]])=%1", "\"") .. ']=]' elseif x_type == "function" then res = res .. func_name(x) elseif x_type == "table" then -- NB: may be too mutch for stack res = res .. "{ " .. join(x, sep) .. " }\n" else res = res .. tostring(x) end end return res end -- saves table @forms into a file at @path -- by creating function get_forms, so that -- we could `load_forms' it later (see -- load_forms) local function dump_forms(forms, path) local contents = "function get_forms()\n" .. "return {\n" .. join(forms) .. "}\nend\n" return dwupd.spit(contents, path) end -- loads forms from a @path relative to -- backup directory (see dump_forms) local function load_forms(path) local script, err = dwupd.slurp(path) if not script then error(err) end assert(load(script))() return get_forms() -- get_forms is in a script at @path end -- converts function reference @func and its -- arguments @args into a string representation local function signature(func, args) return func_name(func) .. "(" .. join(args) .. ")" end -- traverses a @forms table and interprets -- each item (form) as -- { callable, arg1, arg2, ..., argN }, then -- calls `callable' with specified arguments; -- stops then forms all are exhausted or then -- `callable' returns false function run(forms) if verbose then log(ll_info, "Will execute following forms:") log(ll_info, join(forms)) end local func, args local ret, msg for _, form in pairs(forms) do func = form[1] args = rest(form) log( ll_dbg, string.format( "Execute %s from form %s", signature(func, args), join(form) ) ) ret, msg = func(unpack(args)) if not ret then msg = "Failed to run `" .. signature(func, args) .. "', details: " .. msg -- insert failure auditing into a position before -- last element, since last element is a server -- starting routine insert(rescue_forms, #rescue_forms+1, { ignore, spit, fail_script(msg), backup .. "\\rollback.sql" }) insert(rescue_forms, #rescue_forms+1, { ifexist, backup .. "\\rollback.sql", ignore, update_db, service, backup .. "\\rollback.sql", "revert" }) log(ll_err, msg) return false end end return true end -- updates updater from repository %) function selfupdate(from) log(ll_info, "Trying to update `" .. updater .. "' from `" .. from .. "'") if not dwupd.are_files_equal(from, updater) then return dwupd.remove_or_rename(updater) and copy_file(from, updater) else log(ll_info, "Won't update, since `" .. updater .. "' is the same as `" .. from .. "") return true end end -- starts operation @what in a separate process function isolate(what) return dwupd.execute("\"" .. updater .. "\"", " --enable-runcopy" .. " --tmp-dir \"" .. tmp .. "\" --log-dir \"" .. var .. "\" --log-file dwupdater.log" .. " --verbosity " .. (verbose and "debug" or "info") .. " -c exec" .. " -s \"" .. script .. "\" -p " .. " from=" .. from_rev .. " to=" .. to_rev .. " home=\"" .. home .. "\" bin=\"" .. bin_root .. "\" var=\"" .. var .. "\" etc=\"" .. etc .. "\" tmp=\"" .. tmp .. "\" platform=\"" .. platform .. "\" updater=\"" .. updater .. "\" script=\"" .. script .. "\" service=\"" .. service .. "\" rotate=\"" .. rotate .. "\" verbose=" .. tostring(verbose) .. " mode=" .. what .. " admlogin=\"" .. adm_login .. "\" admaddr=\"" .. adm_addr .. "\"") end -- upgrades server by consequently -- running upgrade forms (see upgrade_forms) function do_upgrade() log(ll_info, "Upgrading...") local success, res = pcall(run, upgrade_forms) local dump_res = check("Failed to dump rescue forms to `rescue.lua'", dump_forms(rescue_forms, backup .. "\\rescue.lua")) if not success then error(res) end return res and dump_res end -- reverts changes caused by upgrade -- as recorded in a rescue.lua (see -- rescue & dump_forms & load_forms) function do_revert() log(ll_info, "Reverting changes...") -- set globally to modify dynamically when needed (see safe_stop_server) rescue_forms = load_forms(backup .. "\\rescue.lua") local success, res = pcall(run, rescue_forms) if not success then error(res) end return res end -- controls uprgading and reverting -- in a separate processes function do_master() local msg local osinfo = dwupd.OSinfoex() if osinfo.major < 6 or ( osinfo.major == 6 and osinfo.minor < 1 ) then msg = string.format( "Update is not possible for Windows %d.%d", osinfo.major, osinfo.minor ) end if dwupd.is_fs_compatible and not dwupd.is_fs_compatible( home, var, etc, bin_root, bin ) then msg = string.format( "Update is not possible for non-compatible file system" ) end if msg then local forms = { { log, ll_err, msg }, { ifexist, backup, delete, backup }, { log, ll_info, "Creating backup directory `" .. backup .. "'" }, { mkdir, backup }, { ignore, spit, fail_script( msg ), backup .. "\\rollback.sql" }, { ifexist, backup .. "\\rollback.sql", ignore, update_db, service, backup .. "\\rollback.sql", "revert" } } return check( "Unable to write audit because of ", progn( forms ) ) end if not selfupdate(repo .. "\\bin\\drwupsrv.exe") then return false end log(ll_info, "Upgrading (master -> upgrade)...") local rc, err = isolate("upgrade") log(ll_info, "Upgrade process exit code (upgrade -> master) is " .. format_execute_rc(rc, err)) if rc ~= 0 then log(ll_warn, "Reverting changes (master -> revert)...") rc, err = isolate("revert") log(ll_info, "Revert process exit code (revert -> master) is " .. format_execute_rc(rc, err)) if rc ~= 0 then log(ll_err, "Failed to revert changes, please contact technical support (http://support.drweb.com)") end return false end return true end -- print help command switches function do_help() local help = [[ Dr.Web Server update script. Basically script is invoked like `drwupsrv.exe --log-dir --verbosity -c exec -s -p Command line options (after `-p' drwupsrv switch): service - service executable, like `C:\Program Files\DrWeb Server\bin\drwcsd.exe' from, to - repository revisions `from' which `to' update, like `20000101000000000' platform - current platform, like `windows-nt-x86' home - server's home directory, like `C:\Program Files\DrWeb Server' var - server's var directory, like `C:\Program Files\DrWeb Server\var' etc - server's etc directory, like `C:\Program Files\DrWeb Server\etc' tmp - server's tmp directory, like `C:\Program Files\DrWeb Server\tmp' updater - path to this updater (.\drwupsrv.exe by default) script - path to this script (update.lua by default) mode - execution mode: master - default, try to update safely from revision `from' to `to', revert changes in case of error, update - one-way update from revision `from' to `to', no automatic revert, revert - revert changes caused by update from revision `from' to `to', debug - Lua debug REPL, help - this help message. verbose - more verbose logging when set to `true' lock - path to a file which shall exist during upgrade `' ]] print(help) return true end -------------------------------------------------------------------------------------------------------------- common function format_execute_rc(rc, err) return rc == nil and "undefined because of " .. err or rc end -- start Lua debug REPL local function do_debug() verbose = true debug.debug() return true end -- creates maintenance lock file by -- @path, hooks os.exit to remove it local function create_lock(path) if check("Failed to create lock file `" .. path .. "'", dwupd.spit("", path)) then local exit = os.exit os.exit = function(code) if dwupd.file_exists(path) and dwupd.is_file(path) then check("Failed to remove lock file `" .. path .. "'", dwupd.remove_file(path)) end return exit(code) end else os.exit(1) end end local exit_status = { mode=mode, rc=1, res='' } local function main() if verbose then print_params() end local todo = { master = do_master, upgrade = do_upgrade, revert = do_revert, help = do_help, debug = do_debug } local run = todo[ mode ] if run ~= nil then if lock then create_lock( lock ) end local success, res = pcall(run) if success then if res then log( ll_info, "Successfully completed `" .. mode .. "'" ) exit_status.rc = 0 else log( ll_err, "Failed on `" .. mode .. "'" ) end else log( ll_err, "Failed on `" .. mode .. "', Lua error: " .. res ) exit_status.res = res end else log( ll_err, "Unknown mode `" .. mode .. "' aborting" ) end os.exit( exit_status.rc ) end do local function store_update_status() local dir = update_root .. '/history/' .. from_rev .. "-" .. to_rev if not dwupd.file_exists( dir ) then local rc, msg = dwupd.mkdir( dir ) if not rc then log( ll_err, "Unable to create directory '"..dir.."' "..msg ) return end end local file = io.open( dir..'/exit_status.ds', 'w' ) if not file then log( ll_err, "Unable to create file '"..dir.."/exit_status.ds'" ) return end file:write( "return { mode='" .. tostring( exit_status.mode ) .. "', rc=" .. tostring( exit_status.rc ) .. ", res=[[" .. tostring( exit_status.res ) .. "]] }" ) file:close() local last_rev if exit_status.rc == 0 then if exit_status.mode == "upgrade" then last_rev = to_rev elseif exit_status.mode == "revert" then last_rev = from_rev end end if last_rev then file = io.open( update_root .. '/history/last.ds', 'w' ) if not file then log( ll_err, "Unable to create file '"..update_root.."/history/last.ds'" ) return end file:write( "return { revision='" .. tostring( last_rev ) .. "' }" ) file:close() end end local function drop_obsolete_backups() local maxbackup = tonumber( dwupd.cmd_params.maxbackup ) or 3 if 0 == maxbackup then log( ll_info, 'Store all backups in .. "' .. backup_root .. '"' ) return else log( ll_info, 'Store ' .. maxbackup .. ' last backups in .. "' .. backup_root .. '"' ) end local dirs = {} local function walk_cb(_, files) for _, v in pairs(files) do if v:match( "^%d*%d%d%d%d%d%d%d%d%d%d%d%d%d%d%-%d*%d%d%d%d%d%d%d%d%d%d%d%d%d%d$" ) and dwupd.is_dir( backup_root .. '/' .. v ) then log( ll_dbg, 'Found backup "' .. v .. '"' ) table.insert(dirs, v) end end return false end dwupd.walk_dir(backup_root, walk_cb) if #dirs <= maxbackup then return end table.sort(dirs, function( lhs, rhs ) local lfrom, lto = lhs:match( "^(%d+)%-(%d+)$" ) local rfrom, rto = rhs:match( "^(%d+)%-(%d+)$" ) if lto == nil then error( "unable to match `" .. lhs .. "'" ) end if rto == nil then error( "unable to match `" .. rhs .. "'" ) end if #lto < #rto then return true elseif #lto > #rto then return false elseif lto < rto then return true elseif lto > rto then return false elseif #lfrom < #rfrom then return true elseif #lfrom > #rfrom then return false elseif lfrom < rfrom then return true elseif lfrom > rfrom then return false end return false end ) for i = 1, #dirs - maxbackup do log( ll_info, "Drop obsolete backup '"..dirs[i].."'" ) local fn = backup_root .. '/' .. dirs[i] local res, msg = dwupd.remove_dir( fn ) if not res then log( ll_err, "Unable to remove directory '"..fn.."' "..msg ) end end return true end local exit = os.exit os.exit = function( rc ) if rc == 0 and mode == "master" then local rc, msg = pcall( drop_obsolete_backups ) if not rc then log( ll_err, "Unable to cleanup backup directory:"..tostring( msg ) ) end end store_update_status() return exit( rc ) end end return main() --sign=E0FE71897A0D9F4996F5C7ECE0DB29E912429A1B6D9AE02154DE44FE289F363FE6F210F44DED39CD6EECACDAB741286CB7DEC8CE3B40BD166840ADC78A7A564F