--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file server_session.lua -- -- imports import("core.base.pipe") import("core.base.bytes") import("core.base.object") import("core.base.global") import("core.base.option") import("core.base.hashset") import("core.base.scheduler") import("private.service.server_config", {alias = "config"}) import("private.service.message") import("private.service.remote_build.filesync", {alias = "new_filesync"}) -- define module local server_session = server_session or object() -- init server session function server_session:init(server, session_id) self._ID = session_id self._SERVER = server local filesync = new_filesync(self:sourcedir(), path.join(self:workdir(), "manifest.txt")) filesync:ignorefiles_add(".git/**") filesync:ignorefiles_add(".xmake/**") self._FILESYNC = filesync end -- get server session id function server_session:id() return self._ID end -- get server function server_session:server() return self._SERVER end -- open server session function server_session:open() if self:is_connected() then return end -- ensure source directory self:_ensure_sourcedir() -- update status local status = self:status() status.connected = true status.session_id = self:id() self:status_save() end -- close server session function server_session:close() if not self:is_connected() then return end -- update status local status = self:status() status.connected = false status.session_id = self:id() self:status_save() end -- set stream function server_session:stream_set(stream) self._STREAM = stream end -- get stream function server_session:stream() return self._STREAM end -- diff files function server_session:diff(respmsg) local body = respmsg:body() -- ensure sourcedir self:_ensure_sourcedir() -- do snapshot local filesync = body.xmakesrc and self:_xmake_filesync() or self:_filesync() local manifest_server = assert(filesync:snapshot(), "server manifest not found!") local manifest_client = assert(body.manifest, "client manifest not found!") vprint("%s: diff files in %s ..", self, filesync:rootdir()) -- get all files local fileitems = hashset.new() for fileitem, _ in pairs(manifest_client) do fileitems:insert(fileitem) end for fileitem, _ in pairs(manifest_server) do fileitems:insert(fileitem) end -- do diff local removed = {} local modified = {} local inserted = {} local changed = false for _, fileitem in fileitems:keys() do local manifest_info_client = manifest_client[fileitem] local manifest_info_server = manifest_server[fileitem] if manifest_info_client and manifest_info_server and manifest_info_client.sha256 ~= manifest_info_server.sha256 then table.insert(modified, fileitem) changed = true vprint("[*]: %s", fileitem) elseif not manifest_info_server and manifest_info_client then table.insert(inserted, fileitem) changed = true vprint("[+]: %s", fileitem) elseif manifest_info_server and not manifest_info_client then table.insert(removed, fileitem) changed = true vprint("[-]: %s", fileitem) end end body.manifest = {changed = changed, removed = removed, inserted = inserted, modified = modified} vprint("%s: diff files ok", self) end -- sync files function server_session:sync(respmsg) local body = respmsg:body() local stream = self:stream() local manifest = assert(body.manifest, "manifest not found!") local filesync = body.xmakesrc and self:_xmake_filesync() or self:_filesync() local sourcedir = body.xmakesrc and self:xmake_sourcedir() or self:sourcedir() local archivedir = os.tmpfile() .. ".dir" vprint("%s: sync files in %s ..", self, sourcedir) if self:_recv_syncfiles(manifest, archivedir) then -- do sync for _, fileitem in ipairs(manifest.inserted) do vprint("[+]: %s", fileitem) local filepath_server = path.join(sourcedir, fileitem) local filepath_client = path.join(archivedir, fileitem) os.cp(filepath_client, filepath_server) filesync:update(fileitem, filepath_server) end for _, fileitem in ipairs(manifest.modified) do vprint("[*]: %s", fileitem) local filepath_server = path.join(sourcedir, fileitem) local filepath_client = path.join(archivedir, fileitem) os.cp(filepath_client, filepath_server) filesync:update(fileitem, filepath_server) end for _, fileitem in ipairs(manifest.removed) do vprint("[-]: %s", fileitem) local filepath_server = path.join(sourcedir, fileitem) os.rm(filepath_server) filesync:remove(fileitem) end filesync:manifest_save() else raise("receive files failed!") end os.tryrm(archivedir) vprint("%s: sync files ok", self) end -- pull file function server_session:pull(respmsg) local body = respmsg:body() local stream = self:stream() local filepattern = body.filename vprint("pull %s ..", filepattern) -- get files local filepaths = os.files(path.join(self:sourcedir(), filepattern)) local fileitems = {} for _, filepath in ipairs(filepaths) do local fileitem = path.relative(filepath, self:sourcedir()) if is_host("windows") then fileitem = fileitem:gsub("\\", "/") end if fileitem:startswith("./") then fileitem = fileitem:sub(3) end table.insert(fileitems, fileitem) end vprint(fileitems) -- send files body.fileitems = fileitems respmsg:status_set(true) if stream:send_msg(respmsg) then for _, fileitem in ipairs(fileitems) do local filepath = path.join(self:sourcedir(), fileitem) vprint("sending %s ..", filepath) if not stream:send_file(filepath, {compress = os.filesize(filepath) > 4096}) then raise("send %s failed!", filepath) end end end end -- clean files function server_session:clean(respmsg) local body = respmsg:body() vprint("%s: clean files in %s ..", self, self:workdir()) os.tryrm(self:sourcedir()) os.tryrm(path.join(self:workdir(), "manifest.txt")) if body.all then for _, sessiondir in ipairs(os.dirs(path.join(self:server():workdir(), "sessions", "*"))) do os.tryrm(path.join(sessiondir, "source")) os.tryrm(path.join(sessiondir, "manifest.txt")) end os.tryrm(self:xmake_sourcedir()) os.tryrm(path.join(self:server():workdir(), "xmakesrc_manifest.txt")) end vprint("%s: clean files ok", self) end -- run command function server_session:runcmd(respmsg) local body = respmsg:body() local program = body.program local argv = body.argv vprint("%s: run command(%s) ..", self, os.args(table.join(program, argv))) -- init pipes local stdin_rpipe, stdin_wpipe = pipe.openpair("BA") -- rpipe (block) local stdin_wpipeopt = {wpipe = stdin_wpipe, stop = false} local stdout_rpipe, stdout_wpipe = pipe.openpair() local stdout_rpipeopt = {rpipe = stdout_rpipe, stop = false} -- read and write pipe local group_name = "remote_build/runcmd" scheduler.co_group_begin(group_name, function (co_group) scheduler.co_start(self._write_pipe, self, stdin_wpipeopt) scheduler.co_start(self._read_pipe, self, stdout_rpipeopt) end) -- run program local xmakesrc if os.isfile(path.join(self:xmake_sourcedir(), "core", "main.lua")) then xmakesrc = self:xmake_sourcedir() end try { function () os.execv(program, argv, {curdir = self:sourcedir(), stdout = stdout_wpipe, stderr = stdout_wpipe, stdin = stdin_rpipe, envs = {XMAKE_IN_SERVICE = "true", XMAKE_PROGRAM_DIR = xmakesrc}}) end} stdin_rpipe:close() stdout_wpipe:close() -- stop it stdin_wpipeopt.stop = true stdout_rpipeopt.stop = true -- wait pipes exits scheduler.co_group_wait(group_name) vprint("%s: run command ok", self) end -- get work directory function server_session:workdir() return path.join(self:server():workdir(), "sessions", self:id()) end -- is connected? function server_session:is_connected() return self:status().connected end -- get the status function server_session:status() local status = self._STATUS local statusfile = self:statusfile() if not status then if os.isfile(statusfile) then status = io.load(statusfile) end status = status or {} self._STATUS = status end return status end -- save status function server_session:status_save() io.save(self:statusfile(), self:status()) end -- get status file function server_session:statusfile() return path.join(self:workdir(), "status.txt") end -- get sourcedir directory function server_session:sourcedir() return path.join(self:workdir(), "source") end -- get xmake sourcedir directory function server_session:xmake_sourcedir() return self:server():xmake_sourcedir() end -- get filesync function server_session:_filesync() return self._FILESYNC end -- get xmake filesync function server_session:_xmake_filesync() return self:server():_xmake_filesync() end -- ensure source directory function server_session:_ensure_sourcedir() local sourcedir = self:sourcedir() if not os.isdir(sourcedir) then os.mkdir(sourcedir) end local xmake_sourcedir = self:xmake_sourcedir() if not os.isdir(xmake_sourcedir) then os.mkdir(xmake_sourcedir) end end -- write data to pipe function server_session:_write_pipe(opt) local buff = bytes(256) local wpipe = opt.wpipe vprint("%s: %s: writing data ..", self, wpipe) while not opt.stop do local data = self:_recv_data(buff) if data then local real = wpipe:write(data, {block = true}) vprint("%s: %s: write bytes(%d)", self, wpipe, real) if real < 0 then break end else break end end wpipe:close() vprint("%s: %s: write data end", self, wpipe) end -- read data from pipe function server_session:_read_pipe(opt) local buff = bytes(256) local rpipe = opt.rpipe local verbose = option.get("verbose") vprint("%s: %s: reading data ..", self, rpipe) local leftstr = "" while not opt.stop do local real, data = rpipe:read(buff) if real > 0 then if verbose then leftstr = leftstr .. data:str() local pos = leftstr:lastof("\n", true) if pos then cprint(leftstr:sub(1, pos - 1)) leftstr = leftstr:sub(pos + 1) end end if not self:_send_data(data) then break end elseif real == 0 then if rpipe:wait(pipe.EV_READ, -1) < 0 then break end else break end end rpipe:close() if #leftstr > 0 then cprint(leftstr) end -- say end to client self:_send_end() vprint("%s: %s: read data end", self, rpipe) end -- recv data from stream function server_session:_recv_data(buff) local stream = self:stream() local msg = stream:recv_msg({timeout = -1}) if msg and msg:is_data() then return stream:recv(buff, msg:body().size) end end -- send data to stream function server_session:_send_data(data) local stream = self:stream() if stream:send_msg(message.new_data(self:id(), data:size())) then if stream:send(data) then return stream:flush() end end end -- send end to stream function server_session:_send_end() local stream = self:stream() if stream:send_msg(message.new_end(self:id())) then return stream:flush() end end -- recv syncfiles function server_session:_recv_syncfiles(manifest, outputdir) local stream = self:stream() for _, fileitem in ipairs(manifest.inserted) do local filepath = path.join(outputdir, fileitem) if not stream:recv_file(filepath) then dprint("%s: recv %s failed!", self, filepath) return false end end for _, fileitem in ipairs(manifest.modified) do local filepath = path.join(outputdir, fileitem) if not stream:recv_file(filepath) then dprint("%s: recv %s failed!", self, filepath) return false end end return true end function server_session:__tostring() return string.format("", self:id()) end function main(server, session_id) local instance = server_session() instance:init(server, session_id) return instance end