summaryrefslogtreecommitdiff
path: root/script/service.lua
diff options
context:
space:
mode:
Diffstat (limited to 'script/service.lua')
-rw-r--r--script/service.lua1023
1 files changed, 1023 insertions, 0 deletions
diff --git a/script/service.lua b/script/service.lua
new file mode 100644
index 00000000..2d8a3e64
--- /dev/null
+++ b/script/service.lua
@@ -0,0 +1,1023 @@
+local subprocess = require 'bee.subprocess'
+local method = require 'method'
+local thread = require 'bee.thread'
+local async = require 'async'
+local rpc = require 'rpc'
+local parser = require 'parser'
+local core = require 'core'
+local lang = require 'language'
+local updateTimer= require 'timer'
+local buildVM = require 'vm'
+local sourceMgr = require 'vm.source'
+local localMgr = require 'vm.local'
+local valueMgr = require 'vm.value'
+local chainMgr = require 'vm.chain'
+local functionMgr= require 'vm.function'
+local listMgr = require 'vm.list'
+local emmyMgr = require 'emmy.manager'
+local config = require 'config'
+local task = require 'task'
+local files = require 'files'
+local uric = require 'uri'
+local capability = require 'capability'
+local plugin = require 'plugin'
+
+local ErrorCodes = {
+ -- Defined by JSON RPC
+ ParseError = -32700,
+ InvalidRequest = -32600,
+ MethodNotFound = -32601,
+ InvalidParams = -32602,
+ InternalError = -32603,
+ serverErrorStart = -32099,
+ serverErrorEnd = -32000,
+ ServerNotInitialized = -32002,
+ UnknownErrorCode = -32001,
+
+ -- Defined by the protocol.
+ RequestCancelled = -32800,
+}
+
+local CachedVM = setmetatable({}, {__mode = 'kv'})
+
+---@class LSP
+local mt = {}
+mt.__index = mt
+---@type files
+mt._files = nil
+
+function mt:_callMethod(name, params)
+ local optional
+ if name:sub(1, 2) == '$/' then
+ name = name:sub(3)
+ optional = true
+ end
+ local f = method[name]
+ if f then
+ local clock = os.clock()
+ local suc, res = xpcall(f, debug.traceback, self, params)
+ local passed = os.clock() - clock
+ if passed > 0.2 then
+ log.debug(('Task [%s] takes [%.3f]sec.'):format(name, passed))
+ end
+ if suc then
+ return res
+ else
+ local ok, r = pcall(table.dump, params)
+ local dump = ok and r or '<Cyclic table>'
+ log.debug(('Task [%s] failed, params: %s'):format(
+ name, dump
+ ))
+ log.error(res)
+ if res:find 'not enough memory' then
+ self:restartDueToMemoryLeak()
+ end
+ return nil, {
+ code = ErrorCodes.InternalError,
+ message = r .. '\n' .. res,
+ }
+ end
+ end
+ if optional then
+ return nil
+ else
+ return nil, {
+ code = ErrorCodes.MethodNotFound,
+ message = 'MethodNotFound',
+ }
+ end
+end
+
+function mt:responseProto(id, response, err)
+ local container = table.container()
+ if err then
+ container.error = err
+ else
+ container.result = response
+ end
+ rpc:response(id, container)
+end
+
+function mt:_doProto(proto)
+ local id = proto.id
+ local name = proto.method
+ local params = proto.params
+ local response, err = self:_callMethod(name, params)
+ if not id then
+ return
+ end
+ if type(response) == 'function' then
+ response(function (final)
+ self:responseProto(id, final)
+ end)
+ else
+ self:responseProto(id, response, err)
+ end
+end
+
+function mt:clearDiagnostics(uri)
+ rpc:notify('textDocument/publishDiagnostics', {
+ uri = uri,
+ diagnostics = {},
+ })
+ self._needDiagnostics[uri] = nil
+end
+
+---@param uri uri
+---@param compiled table
+---@param mode string
+---@return boolean
+function mt:needCompile(uri, compiled, mode)
+ self._needDiagnostics[uri] = true
+ if self._needCompile[uri] then
+ return false
+ end
+ if not compiled then
+ compiled = {}
+ end
+ if compiled[uri] then
+ return false
+ end
+ self._needCompile[uri] = compiled
+ if mode == 'child' then
+ table.insert(self._needCompile, uri)
+ else
+ table.insert(self._needCompile, 1, uri)
+ end
+ return true
+end
+
+function mt:isNeedCompile(uri)
+ return self._needCompile[uri]
+end
+
+function mt:isWaitingCompile()
+ if self._needCompile[1] then
+ return true
+ else
+ return false
+ end
+end
+
+---@param uri uri
+---@param version integer
+---@param text string
+function mt:saveText(uri, version, text)
+ self._lastLoadedVM = uri
+ self._files:save(uri, text, version)
+ self:needCompile(uri)
+end
+
+---@param uri uri
+function mt:isDeadText(uri)
+ return self._files:isDead(uri)
+end
+
+---@param uri uri
+---@return boolean
+function mt:isLua(uri)
+ if not self.workspace then
+ return true
+ end
+ local path = self.workspace:absolutePathByUri(uri)
+ if not path then
+ return false
+ end
+ if self.workspace:isLuaFile(path) then
+ return true
+ end
+ return false
+end
+
+function mt:isIgnored(uri)
+ if not self.workspace then
+ return true
+ end
+ if not self.workspace.gitignore then
+ return true
+ end
+ local path = self.workspace:relativePathByUri(uri)
+ if not path then
+ return true
+ end
+ if self.workspace.gitignore(path:string()) then
+ return true
+ end
+ return false
+end
+
+---@param uri uri
+---@param version integer
+---@param text string
+function mt:open(uri, version, text)
+ if not self:isLua(uri) then
+ return
+ end
+ self:saveText(uri, version, text)
+ self._files:open(uri, text)
+end
+
+---@param uri uri
+function mt:close(uri)
+ self._files:close(uri)
+ if self._files:isLibrary(uri) then
+ return
+ end
+ if not self:isLua(uri) or self:isIgnored(uri) then
+ self:removeText(uri)
+ end
+end
+
+---@param uri uri
+---@return boolean
+function mt:isOpen(uri)
+ return self._files:isOpen(uri)
+end
+
+---@param uri uri
+---@param path path
+---@param text string
+function mt:checkReadFile(uri, path, text)
+ if not text then
+ log.debug('No file: ', path)
+ return false
+ end
+ local size = #text / 1000.0
+ if size > config.config.workspace.preloadFileSize then
+ log.info(('Skip large file, size: %.3f KB: %s'):format(size, uri))
+ return false
+ end
+ if self:getCachedFileCount() >= config.config.workspace.maxPreload then
+ if not self._hasShowHitMaxPreload then
+ self._hasShowHitMaxPreload = true
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script('MWS_MAX_PRELOAD', config.config.workspace.maxPreload),
+ })
+ end
+ return false
+ end
+ return true
+end
+
+---@param uri uri
+---@param path path
+---@param buf string
+---@param compiled table
+function mt:readText(uri, path, buf, compiled)
+ if self._files:get(uri) then
+ log.debug('Read failed due to duplicate:', uri)
+ return
+ end
+ if not self:isLua(uri) then
+ log.debug('Read failed due to not lua:', uri)
+ return
+ end
+ if not self._files:isOpen() and self:isIgnored(uri) then
+ log.debug('Read failed due to ignored:', uri)
+ return
+ end
+ local text = buf or io.load(path)
+ if not self._files:isOpen() and not self:checkReadFile(uri, path, text) then
+ log.debug('Read failed due to check failed:', uri)
+ return
+ end
+ self._files:save(uri, text, 0)
+ self:needCompile(uri, compiled)
+end
+
+---@param uri uri
+---@param path path
+---@param buf string
+---@param compiled table
+function mt:readLibrary(uri, path, buf, compiled)
+ if not self:isLua(uri) then
+ return
+ end
+ if not self:checkReadFile(uri, path, buf) then
+ return
+ end
+ self._files:save(uri, buf, 0)
+ self._files:setLibrary(uri)
+ self:needCompile(uri, compiled)
+ self:clearDiagnostics(uri)
+end
+
+---@param uri uri
+function mt:removeText(uri)
+ self._files:remove(uri)
+ self:compileVM(uri)
+end
+
+function mt:getCachedFileCount()
+ return self._files:count()
+end
+
+function mt:reCompile()
+ if self.global then
+ self.global:remove()
+ end
+ if self.chain then
+ self.chain:remove()
+ end
+ if self.emmy then
+ self.emmy:remove()
+ end
+
+ local compiled = {}
+ self._files:clearVM()
+
+ for _, obj in pairs(listMgr.list) do
+ if obj.type == 'source' or obj.type == 'function' then
+ obj:kill()
+ end
+ end
+
+ self.global = core.global(self)
+ self.chain = chainMgr()
+ self.emmy = emmyMgr()
+ self.globalValue = nil
+ self._compileTask:remove()
+ self._needCompile = {}
+ local n = 0
+ for uri in self._files:eachFile() do
+ self:needCompile(uri, compiled)
+ n = n + 1
+ end
+ log.debug('reCompile:', n, self._files:count())
+
+ self:_testMemory('skip')
+end
+
+function mt:reDiagnostic()
+ for uri in self._files:eachFile() do
+ self:clearDiagnostics(uri)
+ self._needDiagnostics[uri] = true
+ end
+end
+
+function mt:clearAllFiles()
+ for uri in self._files:eachFile() do
+ self:clearDiagnostics(uri)
+ end
+ self._files:clear()
+end
+
+---@param uri uri
+function mt:loadVM(uri)
+ local file = self._files:get(uri)
+ if not file then
+ return nil
+ end
+ if uri ~= self._lastLoadedVM then
+ self:needCompile(uri)
+ end
+ if self._compileTask
+ and not self._compileTask:isRemoved()
+ and self._compileTask:get 'uri' == uri
+ then
+ self._compileTask:fastForward()
+ else
+ self:compileVM(uri)
+ end
+ if file:getVM() then
+ self._lastLoadedVM = uri
+ end
+ return file:getVM(), file:getLines()
+end
+
+function mt:_markCompiled(uri, compiled)
+ local newCompiled = self._needCompile[uri]
+ if newCompiled then
+ newCompiled[uri] = true
+ self._needCompile[uri] = nil
+ end
+ for i, u in ipairs(self._needCompile) do
+ if u == uri then
+ table.remove(self._needCompile, i)
+ break
+ end
+ end
+ if newCompiled == compiled then
+ return compiled
+ end
+ if not compiled then
+ compiled = {}
+ end
+ for k, v in pairs(newCompiled) do
+ compiled[k] = v
+ end
+ return compiled
+end
+
+---@param file file
+---@return table
+function mt:compileAst(file)
+ local ast, err, comments = parser:parse(file:getText(), 'lua', config.config.runtime.version)
+ file.comments = comments
+ if ast then
+ file:setAstErr(err)
+ else
+ if type(err) == 'string' then
+ local message = lang.script('PARSER_CRASH', err)
+ log.debug(message)
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script('PARSER_CRASH', err:match '%.lua%:%d+%:(.+)' or err),
+ })
+ if message:find 'not enough memory' then
+ self:restartDueToMemoryLeak()
+ end
+ end
+ end
+ return ast
+end
+
+---@param file file
+---@param uri uri
+function mt:_clearChainNode(file, uri)
+ for pUri in file:eachParent() do
+ local parent = self._files:get(pUri)
+ if parent then
+ parent:removeChild(uri)
+ end
+ end
+end
+
+---@param file file
+---@param compiled table
+function mt:_compileChain(file, compiled)
+ if not compiled then
+ compiled = {}
+ end
+ for uri in file:eachChild() do
+ self:needCompile(uri, compiled, 'child')
+ end
+ for uri in file:eachParent() do
+ self:needCompile(uri, compiled, 'parent')
+ end
+end
+
+function mt:_compileGlobal(compiled)
+ local uris = self.global:getAllUris()
+ for _, uri in ipairs(uris) do
+ self:needCompile(uri, compiled, 'global')
+ end
+end
+
+function mt:_clearGlobal(uri)
+ self.global:clearGlobal(uri)
+end
+
+function mt:_hasSetGlobal(uri)
+ return self.global:hasSetGlobal(uri)
+end
+
+---@param uri uri
+function mt:compileVM(uri)
+ local file = self._files:get(uri)
+ if not file then
+ self:_markCompiled(uri)
+ return nil
+ end
+ local compiled = self._needCompile[uri]
+ if not compiled then
+ return nil
+ end
+ file:removeVM()
+
+ local clock = os.clock()
+ local ast = self:compileAst(file)
+ local version = file:getVersion()
+ local astCost = os.clock() - clock
+ if astCost > 0.1 then
+ log.warn(('Compile Ast[%s] takes [%.3f] sec, size [%.3f]kb'):format(uri, astCost, #file:getText() / 1000))
+ end
+ file:clearOldText()
+
+ self:_clearChainNode(file, uri)
+ self:_clearGlobal(uri)
+
+ local clock = os.clock()
+ local vm, err = buildVM(ast, self, uri, file:getText())
+ if vm then
+ CachedVM[vm] = true
+ end
+ if self:isDeadText(uri)
+ or file:isRemoved()
+ or version ~= file:getVersion()
+ then
+ if vm then
+ vm:remove()
+ end
+ return nil
+ end
+ if self._needCompile[uri] then
+ self:_markCompiled(uri, compiled)
+ self._needDiagnostics[uri] = true
+ else
+ if vm then
+ vm:remove()
+ end
+ return nil
+ end
+ file:saveVM(vm, version, os.clock() - clock)
+
+ local clock = os.clock()
+ local lines = parser:lines(file:getText(), 'utf8')
+ local lineCost = os.clock() - clock
+ file:saveLines(lines, lineCost)
+
+ if file:getVMCost() > 0.2 then
+ log.debug(('Compile VM[%s] takes: %.3f sec'):format(uri, file:getVMCost()))
+ end
+ if not vm then
+ error(err)
+ end
+
+ self:_compileChain(file, compiled)
+ if self:_hasSetGlobal(uri) then
+ self:_compileGlobal(compiled)
+ end
+
+ return file
+end
+
+---@param uri uri
+function mt:doDiagnostics(uri)
+ if not config.config.diagnostics.enable then
+ self._needDiagnostics[uri] = nil
+ return
+ end
+ if not self._needDiagnostics[uri] then
+ return
+ end
+ local name = 'textDocument/publishDiagnostics'
+ local file = self._files:get(uri)
+ if not file
+ or file:isRemoved()
+ or not file:getVM()
+ or file:getVM():isRemoved()
+ or self._files:isLibrary(uri)
+ then
+ self._needDiagnostics[uri] = nil
+ self:clearDiagnostics(uri)
+ return
+ end
+ local data = {
+ uri = uri,
+ vm = file:getVM(),
+ lines = file:getLines(),
+ version = file:getVM():getVersion(),
+ }
+ local res = self:_callMethod(name, data)
+ if self:isDeadText(uri) then
+ return
+ end
+ if file:getVM():getVersion() ~= data.version then
+ return
+ end
+ if self._needDiagnostics[uri] then
+ self._needDiagnostics[uri] = nil
+ else
+ return
+ end
+ if res then
+ rpc:notify(name, {
+ uri = uri,
+ diagnostics = res,
+ })
+ else
+ self:clearDiagnostics(uri)
+ end
+end
+
+---@param uri uri
+---@return file
+function mt:getFile(uri)
+ return self._files:get(uri)
+end
+
+---@param uri uri
+---@return VM
+---@return table
+---@return string
+function mt:getVM(uri)
+ local file = self._files:get(uri)
+ if not file then
+ return nil
+ end
+ return file:getVM(), file:getLines(), file:getText()
+end
+
+---@param uri uri
+---@return string
+---@return string
+function mt:getText(uri)
+ local file = self._files:get(uri)
+ if not file then
+ return nil
+ end
+ return file:getText(), file:getOldText()
+end
+
+function mt:getComments(uri)
+ local file = self._files:get(uri)
+ if not file then
+ return nil
+ end
+ return file:getComments()
+end
+
+---@param uri uri
+---@return table
+function mt:getAstErrors(uri)
+ local file = self._files:get(uri)
+ if not file then
+ return nil
+ end
+ return file:getAstErr()
+end
+
+---@param child uri
+---@param parent uri
+function mt:compileChain(child, parent)
+ local parentFile = self._files:get(parent)
+ local childFile = self._files:get(child)
+
+ if not parentFile or not childFile then
+ return
+ end
+ if parentFile == childFile then
+ return
+ end
+
+ parentFile:addChild(child)
+ childFile:addParent(parent)
+end
+
+function mt:checkWorkSpaceComplete()
+ if self._hasCheckedWorkSpaceComplete then
+ return
+ end
+ self._hasCheckedWorkSpaceComplete = true
+ if self.workspace:isComplete() then
+ return
+ end
+ self._needShowComplete = true
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script.MWS_NOT_COMPLETE,
+ })
+end
+
+function mt:_createCompileTask()
+ if not self:isWaitingCompile() and not next(self._needDiagnostics) then
+ if self._needShowComplete then
+ self._needShowComplete = nil
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script.MWS_COMPLETE,
+ })
+ end
+ end
+ self._compileTask = task(function ()
+ self:doDiagnostics(self._lastLoadedVM)
+ local uri = self._needCompile[1]
+ if uri then
+ self._compileTask:set('uri', uri)
+ pcall(function () self:compileVM(uri) end)
+ else
+ uri = next(self._needDiagnostics)
+ if uri then
+ self:doDiagnostics(uri)
+ end
+ end
+ end)
+end
+
+function mt:_doCompileTask()
+ if not self._compileTask or self._compileTask:isRemoved() then
+ self:_createCompileTask()
+ end
+ while true do
+ local res = self._compileTask:step()
+ if res == 'stop' then
+ self._compileTask:remove()
+ break
+ end
+ if self._compileTask:isRemoved() then
+ break
+ end
+ end
+ self:_loadProto()
+end
+
+function mt:_loadProto()
+ while true do
+ local ok, proto = self._proto:pop()
+ if not ok then
+ break
+ end
+ if proto.method then
+ self:_doProto(proto)
+ else
+ rpc:recieve(proto)
+ end
+ end
+end
+
+function mt:restartDueToMemoryLeak()
+ rpc:requestWait('window/showMessageRequest', {
+ type = 3,
+ message = lang.script('DEBUG_MEMORY_LEAK', '[Lua]'),
+ actions = {
+ {
+ title = lang.script.DEBUG_RESTART_NOW,
+ }
+ }
+ }, function ()
+ os.exit(true)
+ end)
+ ac.wait(5, function ()
+ os.exit(true)
+ end)
+end
+
+function mt:reScanFiles()
+ if not self.workspace then
+ return
+ end
+ log.debug('reScanFiles')
+ self:clearAllFiles()
+ self.workspace:scanFiles()
+end
+
+function mt:onUpdateConfig(updated, other)
+ local oldConfig = table.deepCopy(config.config)
+ local oldOther = table.deepCopy(config.other)
+ config:setConfig(updated, other)
+ local newConfig = config.config
+ local newOther = config.other
+ if not table.equal(oldConfig.runtime, newConfig.runtime) then
+ local library = require 'core.library'
+ library.reload()
+ self:reCompile()
+ end
+ if not table.equal(oldConfig.diagnostics, newConfig.diagnostics) then
+ log.debug('reDiagnostic')
+ self:reDiagnostic()
+ end
+ if newConfig.completion.enable then
+ capability.completion.enable()
+ else
+ capability.completion.disable()
+ end
+ if not table.equal(oldConfig.plugin, newConfig.plugin) then
+ plugin.load(self.workspace)
+ end
+ if not table.equal(oldConfig.workspace, newConfig.workspace)
+ or not table.equal(oldConfig.plugin, newConfig.plugin)
+ or not table.equal(oldOther.associations, newOther.associations)
+ or not table.equal(oldOther.exclude, newOther.exclude)
+ then
+ self:reScanFiles()
+ end
+end
+
+function mt:_testMemory(skipDead)
+ local clock = os.clock()
+ collectgarbage()
+ log.debug('collectgarbage: ', ('%.3f'):format(os.clock() - clock))
+
+ local clock = os.clock()
+ local cachedVM = 0
+ local cachedSource = 0
+ local cachedFunction = 0
+ for _, file in self._files:eachFile() do
+ local vm = file:getVM()
+ if vm and not vm:isRemoved() then
+ cachedVM = cachedVM + 1
+ cachedSource = cachedSource + #vm.sources
+ cachedFunction = cachedFunction + #vm.funcs
+ end
+ end
+ local aliveVM = 0
+ local deadVM = 0
+ for vm in pairs(CachedVM) do
+ if vm:isRemoved() then
+ deadVM = deadVM + 1
+ else
+ aliveVM = aliveVM + 1
+ end
+ end
+
+ local alivedSource = 0
+ local deadSource = 0
+ for _, id in pairs(sourceMgr.watch) do
+ if listMgr.get(id) then
+ alivedSource = alivedSource + 1
+ else
+ deadSource = deadSource + 1
+ end
+ end
+
+ local alivedFunction = 0
+ local deadFunction = 0
+ for _, id in pairs(functionMgr.watch) do
+ if listMgr.get(id) then
+ alivedFunction = alivedFunction + 1
+ else
+ deadFunction = deadFunction + 1
+ end
+ end
+
+ local totalLocal = 0
+ for _ in pairs(localMgr.watch) do
+ totalLocal = totalLocal + 1
+ end
+
+ local totalValue = 0
+ local deadValue = 0
+ for value in pairs(valueMgr.watch) do
+ totalValue = totalValue + 1
+ if not value:getSource() then
+ deadValue = deadValue + 1
+ end
+ end
+
+ local totalEmmy = self.emmy:count()
+
+ local mem = collectgarbage 'count'
+ local threadInfo = async.info
+ local threadBuf = {}
+ for i, count in ipairs(threadInfo) do
+ if count then
+ threadBuf[i] = ('#%03d Mem: [%.3f]kb'):format(i, count)
+ else
+ threadBuf[i] = ('#%03d Mem: <Unknown>'):format(i)
+ end
+ end
+
+ log.debug(('\n\z
+ State\n\z
+ Main Mem: [%.3f]kb\n\z
+ %s\n\z
+-------------------\n\z
+ CachedVM: [%d]\n\z
+ AlivedVM: [%d]\n\z
+ DeadVM: [%d]\n\z
+-------------------\n\z
+ CachedSrc: [%d]\n\z
+ AlivedSrc: [%d]\n\z
+ DeadSrc: [%d]\n\z
+-------------------\n\z
+ CachedFunc:[%d]\n\z
+ AlivedFunc:[%d]\n\z
+ DeadFunc: [%d]\n\z
+-------------------\n\z
+ TotalVal: [%d]\n\z
+ DeadVal: [%d]\n\z
+-------------------\n\z
+ TotalLoc: [%d]\n\z
+ TotalEmmy: [%d]\n\z'):format(
+ mem,
+ table.concat(threadBuf, '\n'),
+
+ cachedVM,
+ aliveVM,
+ deadVM,
+
+ cachedSource,
+ alivedSource,
+ deadSource,
+
+ cachedFunction,
+ alivedFunction,
+ deadFunction,
+
+ totalValue,
+ deadValue,
+ totalLocal,
+ totalEmmy
+ ))
+ log.debug('test memory: ', ('%.3f'):format(os.clock() - clock))
+
+ if deadValue / totalValue >= 0.5 and not skipDead then
+ self:_testFindDeadValues()
+ end
+end
+
+function mt:_testFindDeadValues()
+ if self._testHasFoundDeadValues then
+ return
+ end
+ self._testHasFoundDeadValues = true
+
+ log.debug('Start find dead values, may takes few seconds...')
+
+ local mark = {}
+ local stack = {}
+ local count = 0
+ local clock = os.clock()
+ local function push(info)
+ stack[#stack+1] = info
+ end
+ local function pop()
+ stack[#stack] = nil
+ end
+ local function showStack(uri)
+ count = count + 1
+ log.debug(uri, table.concat(stack, '->'))
+ end
+ local function scan(name, tbl)
+ if count > 100 or os.clock() - clock > 5.0 then
+ return
+ end
+ if type(tbl) ~= 'table' then
+ return
+ end
+ if mark[tbl] then
+ return
+ end
+ mark[tbl] = true
+ if tbl.type then
+ push(('%s<%s>'):format(name, tbl.type))
+ else
+ push(name)
+ end
+ if tbl.type == 'value' then
+ if not tbl:getSource() then
+ showStack(tbl.uri)
+ end
+ elseif tbl.type == 'files' then
+ for k, v in tbl:eachFile() do
+ scan(k, v)
+ end
+ else
+ for k, v in pairs(tbl) do
+ scan(k, v)
+ end
+ end
+ pop()
+ end
+ scan('root', self._files)
+ log.debug('Finish...')
+end
+
+function mt:onTick()
+ self:_loadProto()
+ self:_doCompileTask()
+ if (os.clock() - self._clock >= 60 and not self:isWaitingCompile())
+ or (os.clock() - self._clock >= 300)
+ then
+ self._clock = os.clock()
+ self:_testMemory()
+ end
+end
+
+function mt:listen()
+ subprocess.filemode(io.stdin, 'b')
+ subprocess.filemode(io.stdout, 'b')
+ io.stdin:setvbuf 'no'
+ io.stdout:setvbuf 'no'
+
+ local _, out = async.run 'proto'
+ self._proto = out
+
+ local timerClock = 0.0
+ while true do
+ local startClock = os.clock()
+ async.onTick()
+ self:onTick()
+
+ local delta = os.clock() - timerClock
+ local suc, err = xpcall(updateTimer, log.error, delta)
+ if not suc then
+ io.stderr:write(err)
+ io.stderr:flush()
+ end
+ timerClock = os.clock()
+
+ local passedClock = os.clock() - startClock
+ if passedClock > 0.1 then
+ thread.sleep(0.0)
+ else
+ thread.sleep(0.001)
+ end
+ end
+end
+
+return function ()
+ local session = setmetatable({
+ _needCompile = {},
+ _needDiagnostics = {},
+ _clock = -100,
+ _version = 0,
+ _files = files(),
+ }, mt)
+ session.global = core.global(session)
+ session.chain = chainMgr()
+ session.emmy = emmyMgr()
+ return session
+end