├── .travis.yml ├── LICENSE ├── README.md ├── bench ├── bench-ev.lua ├── bench-hash.lua ├── bench-http.lua ├── bench-http2.lua ├── bench-lfs.lua ├── bench-raw.lua ├── bench-require.lua ├── n.lua └── toload.lua ├── rockspec ├── hotswap-ev-master-1.rockspec ├── hotswap-hash-master-1.rockspec ├── hotswap-http-master-1.rockspec ├── hotswap-lfs-master-1.rockspec └── hotswap-master-1.rockspec ├── src └── hotswap │ ├── ev.lua │ ├── hash.lua │ ├── http.lua │ ├── init.lua │ └── lfs.lua └── test ├── nginx.conf ├── test-ev.lua ├── test-hash.lua ├── test-hotswap.lua ├── test-http.lua └── test-lfs.lua /.travis.yml: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/kikito/middleclass/blob/master/.travis.yml 2 | os: linux 3 | language: python 4 | sudo: false 5 | 6 | addons: 7 | apt: 8 | packages: 9 | - libev-dev 10 | - nginx-extras 11 | 12 | env: 13 | - LUA="lua=5.1" 14 | - LUA="lua=5.2" 15 | - LUA="lua=5.3" 16 | - LUA="luajit=2.0" 17 | - LUA="luajit=2.1" 18 | 19 | matrix: 20 | allow_failures: 21 | - env: LUA="lua=5.3" 22 | 23 | before_install: 24 | - pip install hererocks 25 | - hererocks lua_nginx -r^ --lua=5.1 26 | - hererocks lua_install -r^ --$LUA 27 | - export PATH=$PATH:$PWD/lua_install/bin 28 | 29 | install: 30 | - $PWD/lua_nginx/bin/luarocks --tree=$PWD/lua_install install compat52 31 | - $PWD/lua_nginx/bin/luarocks --tree=$PWD/lua_install install lua-cjson 32 | - luarocks install luacheck 33 | - luarocks install busted 34 | - luarocks install luacov 35 | - luarocks install luacov-coveralls 36 | - luarocks install coronest 37 | - luarocks make rockspec/hotswap-master-1.rockspec 38 | - for file in rockspec/hotswap-*-master-1.rockspec; do luarocks make $file; done 39 | 40 | script: 41 | - luacheck --std max+busted src/hotswap/*.lua 42 | - busted --verbose --coverage --pattern=test test 43 | 44 | after_success: 45 | - luacov-coveralls --exclude "test" --exclude "lua_install" 46 | 47 | notifications: 48 | recipients: 49 | - alban@linard.fr 50 | email: 51 | on_success: change 52 | on_failure: always 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alban Linard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/saucisson/lua-hotswap.svg?branch=master)](https://travis-ci.org/saucisson/lua-hotswap) 2 | [![Coverage Status](https://coveralls.io/repos/saucisson/lua-hotswap/badge.svg?branch=master&service=github)](https://coveralls.io/github/saucisson/lua-hotswap?branch=master) 3 | [![Join the chat at https://gitter.im/saucisson/lua-hotswap](https://badges.gitter.im/saucisson/lua-hotswap.svg)](https://gitter.im/saucisson/lua-hotswap?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | # Require with hotswapping 6 | 7 | Sometimes, we would like to reload automatically an updated module 8 | within a long-running program. The `hotswap` module provides such 9 | functionality, using various backends for change detection. 10 | 11 | See [Wikipedia](https://en.wikipedia.org/wiki/Hot_swapping#Software) 12 | 13 | ## Install 14 | 15 | This module is available in [luarocks](https://luarocks.org): 16 | 17 | ````sh 18 | luarocks install hotswap 19 | ```` 20 | 21 | ## Example 22 | 23 | The easiest way to use this library is as below: 24 | 25 | ````lua 26 | local hotswap = require "hotswap".new () 27 | local mymodule = hotswap.require "mymodule" 28 | ... 29 | ```` 30 | 31 | Note that this is useless, as there is not hotswapping in the default 32 | behavior. Note also that the `.new ()` can be omitted if you need only one 33 | instance of the `hotswap` module. 34 | 35 | An easy way to use hotswapping is to require the updated module within 36 | a loop. The following code reloads the module only when its file hash hash 37 | changed: 38 | 39 | ````lua 40 | local hotswap = require "hotswap.hash" 41 | while true do 42 | local mymodule = hotswap.require "mymodule" 43 | ... 44 | end 45 | ```` 46 | 47 | The same applies with file modification date given by `lfs`: 48 | 49 | ````lua 50 | local hotswap = require "hotswap.lfs" 51 | while true do 52 | local mymodule = hotswap.require "mymodule" 53 | ... 54 | end 55 | ```` 56 | 57 | A more advanced use is for instance with `lua-ev` in a idle loop: 58 | 59 | ````lua 60 | local ev = require "ev" 61 | local hotswap = require "hotswap.ev" 62 | ev.Idle.new (function () 63 | local mymodule = hotswap.require "mymodule" 64 | ... 65 | end):start (ev.Loop.default) 66 | ev.Loop.default:loop () 67 | ```` 68 | 69 | The `hotswap` can even replace the `require` function easily: 70 | 71 | ````lua 72 | require = require "hotswap.xxx".require 73 | ```` 74 | 75 | ## Backends 76 | 77 | Currently, the following backends are supported: 78 | 79 | * `hotswap`: the default backend does not perform any change detection, 80 | see [this example](bench/bench-raw.lua); 81 | * `hotswap.ev`: this backend detects module changes using `lua-ev`, 82 | see [this example](bench/bench-ev.lua); 83 | * `hotswap.hash`: this backend detects module changes by checking file hashes 84 | using `xxhah`, 85 | see [this example](bench/bench-hash.lua); 86 | * `hotswap.http`: this backend loads modules from a HTTP server, 87 | and avoids useless downloads, 88 | see [this example](bench/bench-http.lua); 89 | * `hotswap.lfs`: this backend detects module changes by observing file 90 | modification date using `luafilesystem`, 91 | see [this example](bench/bench-lfs.lua). 92 | 93 | Notice that the dependencies for each backend are not listed in the rockspec. 94 | Make sure to install them! 95 | 96 | ## Compatibility 97 | 98 | This module makes use of `package.searchers`, available from Lua 5.2. If you 99 | are running under Lua 5.1 or LuaJIT, a fake `package.searchers` will be 100 | automatically created. 101 | 102 | ## Benchmarks 103 | 104 | The [bench](bench/) directory contains benchmarks for the backends. They 105 | can be run using: 106 | 107 | ````sh 108 | cd bench/ 109 | for bench in bench-*.lua 110 | do 111 | echo ${bench} 112 | luajit ${bench} 113 | done 114 | cd .. 115 | ```` 116 | 117 | 118 | # Test 119 | 120 | Tests are written for [busted](http://olivinelabs.com/busted). 121 | ```bash 122 | busted test/*.lua 123 | ``` 124 | -------------------------------------------------------------------------------- /bench/bench-ev.lua: -------------------------------------------------------------------------------- 1 | if not package.searchers then 2 | require "compat52" 3 | end 4 | 5 | local gettime = require "socket".gettime 6 | local hotswap = require "hotswap.ev" 7 | local ev = require "ev" 8 | local n = require "n" 9 | 10 | local start = gettime () 11 | local i = 1 12 | ev.Idle.new (function (loop, idle, _) 13 | local _ = hotswap.require "toload" 14 | if i == n then 15 | idle:stop (loop) 16 | loop:unloop () 17 | end 18 | i = i+1 19 | end):start (ev.Loop.default) 20 | ev.Loop.default:loop () 21 | local finish = gettime () 22 | 23 | print (math.floor (n / (finish - start)), "requires/second") 24 | -------------------------------------------------------------------------------- /bench/bench-hash.lua: -------------------------------------------------------------------------------- 1 | if not package.searchers then 2 | require "compat52" 3 | end 4 | 5 | local gettime = require "socket".gettime 6 | local hotswap = require "hotswap.hash" 7 | local n = require "n" 8 | 9 | local start = gettime () 10 | for _ = 1, n do 11 | local _ = hotswap.require "toload" 12 | end 13 | local finish = gettime () 14 | 15 | print (math.floor (n / (finish - start)), "requires/second") 16 | -------------------------------------------------------------------------------- /bench/bench-http.lua: -------------------------------------------------------------------------------- 1 | local gettime = require "socket".gettime 2 | local n = require "n" 3 | local hotswap = require "hotswap.http" { 4 | encode = function (t) 5 | if next (t) == nil 6 | or next (t, next (t)) ~= nil then 7 | return 8 | end 9 | local k, v = next (t) 10 | return { 11 | url = "http://127.0.0.1:8080/lua/" .. k, 12 | method = "GET", 13 | headers = { 14 | ["If-None-Match"] = type (v) == "table" and v.etag or nil, 15 | ["Lua-Module" ] = k, 16 | }, 17 | } 18 | end, 19 | decode = function (t) 20 | local module = t.request.headers ["Lua-Module"] 21 | if t.code == 200 then 22 | return { 23 | [module] = { 24 | lua = t.body, 25 | etag = t.headers.etag, 26 | }, 27 | } 28 | elseif t.code == 304 then 29 | return { 30 | [module] = {}, 31 | } 32 | elseif t.code == 404 then 33 | return {} 34 | else 35 | assert (false) 36 | end 37 | end, 38 | } 39 | 40 | local tmp = os.tmpname () 41 | do 42 | local conf_file = io.open ("nginx.conf", "r") 43 | local conf = conf_file:read "*all" 44 | conf_file:close () 45 | conf = conf:gsub ("{{{TMP}}}", tmp) 46 | conf_file = io.open (tmp, "w") 47 | conf_file:write (conf .. "\n") 48 | conf_file:close () 49 | local command = ([[ 50 | mv {{{TMP}}} {{{TMP}}}.back 51 | mkdir -p {{{TMP}}} 52 | mv {{{TMP}}}.back {{{TMP}}}/nginx.conf 53 | nginx -s stop 54 | nginx -p {{{TMP}}} -c {{{TMP}}}/nginx.conf 55 | ]]):gsub ("{{{TMP}}}", tmp) 56 | assert (os.execute (command)) 57 | end 58 | 59 | local start = gettime () 60 | for _ = 1, n do 61 | local _ = hotswap.require "toload" 62 | end 63 | local finish = gettime () 64 | 65 | print (math.floor (n / (finish - start)), "requires/second") 66 | do 67 | local command = ([[ 68 | kill -QUIT $(cat {{{TMP}}}/nginx.pid) 69 | rm -rf {{{TMP}}} 70 | ]]):gsub ("{{{TMP}}}", tmp) 71 | assert (os.execute (command)) 72 | end 73 | -------------------------------------------------------------------------------- /bench/bench-http2.lua: -------------------------------------------------------------------------------- 1 | local json = require "cjson" 2 | local ltn12 = require "ltn12" 3 | local gettime = require "socket".gettime 4 | local n = require "n" 5 | local hotswap = require "hotswap.http" { 6 | encode = function (t) 7 | local data = json.encode (t) 8 | return { 9 | url = "http://127.0.0.1:8080/luaset", 10 | method = "POST", 11 | headers = { 12 | ["If-None-Match" ] = type (v) == "table" and v.etag or nil, 13 | ["Content-Length"] = #data, 14 | }, 15 | source = ltn12.source.string (data), 16 | } 17 | end, 18 | decode = function (t) 19 | if t.code == 200 then 20 | return json.decode (t.body) 21 | end 22 | end, 23 | } 24 | 25 | assert (os.execute [[ 26 | rm -rf ./nginx/*.log ./nginx/*.pid 27 | /usr/sbin/nginx -p ./nginx/ -c nginx.conf 28 | ]]) 29 | 30 | local start = gettime () 31 | for _ = 1, n do 32 | local _ = hotswap.require "cosy.string" 33 | end 34 | local finish = gettime () 35 | 36 | print (math.floor (n / (finish - start)), "requires/second") 37 | assert (os.execute [[ 38 | kill -QUIT $(cat ./nginx/nginx.pid) 39 | ]]) 40 | -------------------------------------------------------------------------------- /bench/bench-lfs.lua: -------------------------------------------------------------------------------- 1 | if not package.searchers then 2 | require "compat52" 3 | end 4 | 5 | local gettime = require "socket".gettime 6 | local hotswap = require "hotswap.lfs" 7 | local n = require "n" 8 | 9 | local start = gettime () 10 | for _ = 1, n do 11 | local _ = hotswap.require "toload" 12 | end 13 | local finish = gettime () 14 | 15 | print (math.floor (n / (finish - start)), "requires/second") 16 | -------------------------------------------------------------------------------- /bench/bench-raw.lua: -------------------------------------------------------------------------------- 1 | if not package.searchers then 2 | require "compat52" 3 | end 4 | 5 | local gettime = require "socket".gettime 6 | local hotswap = require "hotswap" 7 | local n = require "n" 8 | 9 | local start = gettime () 10 | for _ = 1, n do 11 | local _ = hotswap.require "toload" 12 | end 13 | local finish = gettime () 14 | 15 | print (math.floor (n / (finish - start)), "requires/second") 16 | -------------------------------------------------------------------------------- /bench/bench-require.lua: -------------------------------------------------------------------------------- 1 | if not package.searchers then 2 | require "compat52" 3 | end 4 | 5 | local gettime = require "socket".gettime 6 | local n = require "n" 7 | 8 | local start = gettime () 9 | for _ = 1, n do 10 | local _ = require "toload" 11 | end 12 | local finish = gettime () 13 | 14 | print (math.floor (n / (finish - start)), "requires/second") 15 | -------------------------------------------------------------------------------- /bench/n.lua: -------------------------------------------------------------------------------- 1 | if type (_G.jit) == "table" then 2 | return 1000000 3 | else 4 | return 10000 5 | end -------------------------------------------------------------------------------- /bench/toload.lua: -------------------------------------------------------------------------------- 1 | return {} 2 | -------------------------------------------------------------------------------- /rockspec/hotswap-ev-master-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotswap-ev" 2 | version = "master-1" 3 | 4 | source = { 5 | url = "git://github.com/saucisson/lua-hotswap", 6 | } 7 | 8 | description = { 9 | summary = "Hotswap backend using lua-ev", 10 | detailed = [[]], 11 | license = "MIT/X11", 12 | homepage = "https://github.com/saucisson/lua-hotswap", 13 | maintainer = "Alban Linard ", 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | "hotswap >= 1", 19 | "lua-ev >= v1", 20 | "luaposix >= 33", 21 | "xxhash >= v1", 22 | } 23 | 24 | build = { 25 | type = "builtin", 26 | modules = { 27 | ["hotswap.ev"] = "src/hotswap/ev.lua", 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /rockspec/hotswap-hash-master-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotswap-hash" 2 | version = "master-1" 3 | 4 | source = { 5 | url = "git://github.com/saucisson/lua-hotswap", 6 | } 7 | 8 | description = { 9 | summary = "Hotswap backend using file hashes", 10 | detailed = [[]], 11 | license = "MIT/X11", 12 | homepage = "https://github.com/saucisson/lua-hotswap", 13 | maintainer = "Alban Linard ", 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | "hotswap >= 1", 19 | "xxhash >= v1", 20 | } 21 | 22 | build = { 23 | type = "builtin", 24 | modules = { 25 | ["hotswap.hash"] = "src/hotswap/hash.lua", 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /rockspec/hotswap-http-master-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotswap-http" 2 | version = "master-1" 3 | 4 | source = { 5 | url = "git://github.com/saucisson/lua-hotswap", 6 | } 7 | 8 | description = { 9 | summary = "Hotswap backend using http", 10 | detailed = [[]], 11 | license = "MIT/X11", 12 | homepage = "https://github.com/saucisson/lua-hotswap", 13 | maintainer = "Alban Linard ", 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | "hotswap >= 1", 19 | "luafilesystem >= 1", 20 | "luasocket >= 2", 21 | "serpent >= 0", 22 | } 23 | 24 | build = { 25 | type = "builtin", 26 | modules = { 27 | ["hotswap.http"] = "src/hotswap/http.lua", 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /rockspec/hotswap-lfs-master-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotswap-lfs" 2 | version = "master-1" 3 | 4 | source = { 5 | url = "git://github.com/saucisson/lua-hotswap", 6 | } 7 | 8 | description = { 9 | summary = "Hotswap backend using file modification timestamps", 10 | detailed = [[]], 11 | license = "MIT/X11", 12 | homepage = "https://github.com/saucisson/lua-hotswap", 13 | maintainer = "Alban Linard ", 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | "hotswap >= 1", 19 | "luafilesystem >= 1", 20 | } 21 | 22 | build = { 23 | type = "builtin", 24 | modules = { 25 | ["hotswap.lfs"] = "src/hotswap/lfs.lua", 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /rockspec/hotswap-master-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "hotswap" 2 | version = "master-1" 3 | 4 | source = { 5 | url = "git://github.com/saucisson/lua-hotswap", 6 | } 7 | 8 | description = { 9 | summary = "Replacement for 'require' that allows hotswapping", 10 | detailed = [[]], 11 | license = "MIT/X11", 12 | homepage = "https://github.com/saucisson/lua-hotswap", 13 | maintainer = "Alban Linard ", 14 | } 15 | 16 | dependencies = { 17 | "lua >= 5.1", 18 | } 19 | 20 | build = { 21 | type = "builtin", 22 | modules = { 23 | ["hotswap"] = "src/hotswap/init.lua", 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/hotswap/ev.lua: -------------------------------------------------------------------------------- 1 | local posix = require "posix" 2 | local ev = require "ev" 3 | local xxhash = require "xxhash" 4 | local Hotswap = getmetatable (require "hotswap") 5 | local Ev = {} 6 | 7 | function Ev.new (t) 8 | return Hotswap.new { 9 | new = Ev.new, 10 | observe = Ev.observe, 11 | loop = t and t.loop or ev.Loop.default, 12 | observed = {}, 13 | hashes = {}, 14 | seed = 0x5bd1e995, 15 | } 16 | end 17 | 18 | function Ev:observe (name, filename) 19 | if self.observed [name] then 20 | return 21 | end 22 | do 23 | local file = assert (io.open (filename, "r")) 24 | self.hashes [name] = xxhash.xxh32 (file:read "*all", self.seed) 25 | file:close () 26 | end 27 | local hotswap = self 28 | local current = filename 29 | repeat 30 | local stat = ev.Stat.new (function () 31 | local file = assert (io.open (filename, "r")) 32 | local hash = xxhash.xxh32 (file:read "*all", self.seed) 33 | file:close () 34 | if hash ~= self.hashes [name] then 35 | hotswap.loaded [name] = nil 36 | hotswap.try_require (name) 37 | end 38 | end, current) 39 | self.observed [current] = stat 40 | stat:start (hotswap.loop) 41 | current = posix.readlink (current) 42 | until not current 43 | 44 | end 45 | 46 | return Ev.new () 47 | -------------------------------------------------------------------------------- /src/hotswap/hash.lua: -------------------------------------------------------------------------------- 1 | local xxhash = require "xxhash" 2 | local Hotswap = getmetatable (require "hotswap") 3 | local Hash = {} 4 | 5 | function Hash.new () 6 | return Hotswap.new { 7 | new = Hash.new, 8 | access = Hash.access, 9 | observe = Hash.observe, 10 | hashes = {}, 11 | seed = 0x5bd1e995, 12 | } 13 | end 14 | 15 | function Hash:access (name, filename) 16 | local file = io.open (filename, "r") 17 | if not file then 18 | self.hashes [name] = nil 19 | self.loaded [name] = nil 20 | return 21 | end 22 | local hash = xxhash.xxh32 (file:read "*all", self.seed) 23 | if hash ~= self.hashes [name] then 24 | self.loaded [name] = nil 25 | end 26 | end 27 | 28 | function Hash:observe (name, filename) 29 | local file = io.open (filename, "r") 30 | local hash = xxhash.xxh32 (file:read "*all", self.seed) 31 | self.hashes [name] = hash 32 | end 33 | 34 | return Hash.new () -------------------------------------------------------------------------------- /src/hotswap/http.lua: -------------------------------------------------------------------------------- 1 | local http = require "socket.http" 2 | local https = pcall (require, "ssl.https") and require "ssl.https" or nil 3 | local lfs = require "lfs" 4 | local ltn12 = require "ltn12" 5 | local Hotswap = getmetatable (require "hotswap") 6 | local Http = {} 7 | 8 | 9 | --[[ 10 | 11 | Send: { 12 | name = { 13 | etag = "...", -- optional 14 | }, 15 | } 16 | 17 | Receive: { 18 | name = {}, -- no change 19 | name = { -- updated 20 | etag = "...", 21 | lua = "...", 22 | }, 23 | name = nil, -- not found 24 | } 25 | --]] 26 | 27 | local function request (t) 28 | local result = {} 29 | if type (t) == "string" then 30 | t = { 31 | url = t, 32 | } 33 | end 34 | assert (type (t) == "table") 35 | t.sink = ltn12.sink.table (result) 36 | local _, code, headers, status 37 | if t.url:match "^http://" then 38 | _, code, headers, status = http .request (t) 39 | elseif t.url:match "https://" then 40 | _, code, headers, status = https.request (t) 41 | else 42 | assert (false) 43 | end 44 | local ret = { 45 | body = table.concat (result), 46 | code = code, 47 | headers = headers, 48 | status = status, 49 | request = t, 50 | } 51 | return ret 52 | end 53 | 54 | function Http.new (t) 55 | local instance = Hotswap.new { 56 | new = Http.new, 57 | init = Http.init, 58 | data = {}, 59 | load = Http.load, 60 | save = Http.save, 61 | encode = t and t.encode or assert (false), 62 | decode = t and t.decode or assert (false), 63 | storage = t and t.storage or os.tmpname (), 64 | } 65 | os.remove (instance.storage) 66 | lfs.mkdir (instance.storage) 67 | instance.downloaded = instance.storage .. "/_list" 68 | pcall (function () 69 | for line in io.lines (instance.downloaded) do 70 | local module, etag = line:match "^([^:]+):(%S+)" 71 | instance.data [module] = { 72 | etag = etag, 73 | } 74 | end 75 | end) 76 | local function from_storage (name) 77 | return loadfile (instance.storage .. "/" .. name) 78 | end 79 | local function from_http (name) 80 | local result = instance.decode (request (instance.encode { 81 | [name] = instance.data [name] or true, 82 | })) 83 | if not result then 84 | return 85 | end 86 | instance:load (name, result [name]) 87 | instance:save () 88 | return from_storage (name) 89 | end 90 | table.insert (instance.searchers, 2, from_storage) 91 | table.insert (instance.searchers, 3, from_http ) 92 | instance:init () 93 | return instance 94 | end 95 | 96 | function Http:init () 97 | if not next (self.data) then 98 | return 99 | end 100 | local encoded = self.encode (self.data) 101 | local result 102 | if encoded then 103 | result = self.decode (request (encoded)) 104 | else 105 | result = {} 106 | for key, t in pairs (self.data) do 107 | local subresult = self.decode (request (self.encode { 108 | key = t, 109 | })) 110 | result [key] = subresult [key] 111 | end 112 | end 113 | if type (result) == "table" then 114 | for key, t in pairs (result) do 115 | self:load (key, t) 116 | end 117 | self:save () 118 | end 119 | end 120 | 121 | function Http:load (key, t) 122 | assert (type (key) == "string") 123 | if not t then 124 | self.data [key] = nil 125 | os.remove (self.storage .. "/" .. key) 126 | return 127 | end 128 | assert (type (t) == "table") 129 | if not t.lua then 130 | return 131 | end 132 | local file = io.open (self.storage .. "/" .. key, "w") 133 | if file then 134 | file:write (t.lua) 135 | file:close () 136 | end 137 | self.data [key] = { 138 | etag = t.etag, 139 | } 140 | end 141 | 142 | function Http:save () 143 | local file = io.open (self.downloaded, "w") 144 | if file then 145 | for module, t in pairs (self.data) do 146 | file:write (module .. ":" .. tostring (t.etag) .. "\n") 147 | end 148 | file:close () 149 | end 150 | end 151 | 152 | return Http.new 153 | -------------------------------------------------------------------------------- /src/hotswap/init.lua: -------------------------------------------------------------------------------- 1 | local function make_searchers (package) 2 | package.searchers = {} 3 | package.searchers [1] = function (name) 4 | return package.preload [name] 5 | end 6 | package.searchers [2] = function (name) 7 | local path, err = package.searchpath (name, package.path) 8 | if not path then 9 | return nil, err 10 | end 11 | return loadfile (path), path 12 | end 13 | package.searchers [3] = function (name) 14 | local path, err = package.searchpath (name, package.cpath) 15 | if not path then 16 | return nil, err 17 | end 18 | name = name:gsub ("%.", "_") 19 | name = name:gsub ("[^%-]+%-", "") 20 | return package.loadlib (path, "luaopen_" .. name), path 21 | end 22 | package.searchers [4] = function (name) 23 | local prefix = name:match "^[^%.]+" 24 | local path, err = package.searchpath (prefix, package.cpath) 25 | if not path then 26 | return nil, err 27 | end 28 | name = name:gsub ("%.", "_") 29 | name = name:gsub ("[^%-]+%-", "") 30 | return package.loadlib (path, "luaopen_" .. name), path 31 | end 32 | end 33 | 34 | if not package.searchers then 35 | make_searchers (package) 36 | end 37 | 38 | local Hotswap = {} 39 | 40 | function Hotswap.new (t) 41 | assert (t == nil or type (t) == "table") 42 | local result = t or {} 43 | for k, v in pairs (package) do 44 | result [k] = v 45 | end 46 | result.new = result.new or Hotswap.new 47 | result.access = result.access or function () end 48 | result.observe = result.observe or function () end 49 | result.sources = {} 50 | result.modules = {} 51 | result.on_change = {} 52 | result.loaded = {} 53 | result.preload = {} 54 | for k, v in pairs (package.loaded) do 55 | local wrapper = Hotswap.wrap (result, v, k) 56 | result.modules [k] = v 57 | result.loaded [k] = wrapper 58 | end 59 | for k, v in pairs (package.preload) do 60 | local wrapper = Hotswap.wrap (result, v, k) 61 | result.modules [k] = v 62 | result.preload [k] = wrapper 63 | end 64 | make_searchers (result) 65 | result.require = function (name) 66 | return Hotswap.require (result, name, false) 67 | end 68 | result.try_require = function (name) 69 | return Hotswap.require (result, name, true ) 70 | end 71 | return setmetatable (result, Hotswap) 72 | end 73 | 74 | function Hotswap:require (name, no_error) 75 | if self.sources [name] then 76 | self:access (name, self.sources [name]) 77 | end 78 | local loaded = self.loaded [name] 79 | if loaded then 80 | return loaded 81 | end 82 | local global = _G or _ENV 83 | local back = { 84 | require = global.require, 85 | package = global.package, 86 | } 87 | local function exit (...) 88 | global.require = back.require 89 | global.package = back.package 90 | return ... 91 | end 92 | global.require = function (...) 93 | return self.require (...) 94 | end 95 | global.package = self 96 | local errors = { 97 | "module '" .. tostring (name) .. "' not found:", 98 | } 99 | for i = 1, #self.searchers do 100 | local searcher = self.searchers [i] 101 | local factory, path = searcher (name) 102 | if type (factory) == "function" then 103 | local result 104 | if no_error then 105 | local ok 106 | ok, result = pcall (factory, name) 107 | if not ok then 108 | return exit (nil, result) 109 | end 110 | else 111 | result = factory (name) 112 | end 113 | self.modules [name] = result 114 | self.sources [name] = path 115 | if path then 116 | self:observe (name, path) 117 | end 118 | local wrapper = Hotswap.wrap (self, result, name) 119 | self.loaded [name] = wrapper 120 | for _, f in pairs (self.on_change) do 121 | f (name, wrapper) 122 | end 123 | return exit (wrapper) 124 | else 125 | errors [#errors+1] = path 126 | end 127 | end 128 | errors = table.concat (errors, "\n") 129 | if no_error then 130 | return exit (nil, errors) 131 | else 132 | error (exit (errors)) 133 | end 134 | end 135 | 136 | function Hotswap:wrap (result, name) 137 | if type (result) == "function" then 138 | return function (...) 139 | return self.modules [name] (...) 140 | end 141 | elseif type (result) == "table" then 142 | return setmetatable ({}, { 143 | __index = function (_, key) 144 | return self.modules [name] [key] 145 | end, 146 | __newindex = function (_, key, value) 147 | self.modules [name] [key] = value 148 | end, 149 | __mode = nil, 150 | __call = function (_, ...) 151 | return self.modules [name] (...) 152 | end, 153 | __metatable = getmetatable (result), 154 | __tostring = function (_) 155 | return tostring (self.modules [name]) 156 | end, 157 | __len = function (_) 158 | return # (self.modules [name]) 159 | end, 160 | __gc = nil, 161 | __unm = function (_) 162 | return - (self.modules [name]) 163 | end, 164 | __add = function (_, rhs) 165 | return self.modules [name] + rhs 166 | end, 167 | __mul= function (_, rhs) 168 | return self.modules [name] * rhs 169 | end, 170 | __div = function (_, rhs) 171 | return self.modules [name] / rhs 172 | end, 173 | __mod = function (_, rhs) 174 | return self.modules [name] % rhs 175 | end, 176 | __pow = function (_, rhs) 177 | return self.modules [name] ^ rhs 178 | end, 179 | __concat = function (_, rhs) 180 | return self.modules [name] .. rhs 181 | end, 182 | __eq = function (_, rhs) 183 | return self.modules [name] == rhs 184 | end, 185 | __lt = function (_, rhs) 186 | return self.modules [name] < rhs 187 | end, 188 | __le = function (_, rhs) 189 | return self.modules [name] <= rhs 190 | end, 191 | __pairs = function (_) 192 | return pairs (self.modules [name]) 193 | end, 194 | __ipairs = function (_) 195 | return ipairs (self.modules [name]) 196 | end, 197 | }) 198 | else 199 | return result 200 | end 201 | end 202 | 203 | -- > hotswap = require "hotswap.hash" 204 | 205 | -- > local file = io.open ("example.lua", "w") 206 | -- > file:write [[ return 1 ]] 207 | -- > file:close () 208 | -- > = hotswap "example" 209 | -- 1 210 | 211 | -- > local file = io.open ("example.lua", "w") 212 | -- > file:write [[ return 2 ]] 213 | -- > file:close () 214 | -- > = hotswap "example" 215 | -- 2 216 | 217 | -- > os.remove "example.lua" 218 | -- > = hotswap "example" 219 | -- error: "module 'example' not found" 220 | 221 | -- > os.remove "example.lua" 222 | -- > = hotswap ("example", true) 223 | -- nil 224 | 225 | return Hotswap.new () 226 | -------------------------------------------------------------------------------- /src/hotswap/lfs.lua: -------------------------------------------------------------------------------- 1 | local lfs = require "lfs" 2 | local Hotswap = getmetatable (require "hotswap") 3 | local Lfs = {} 4 | 5 | function Lfs.new () 6 | return Hotswap.new { 7 | new = Lfs.new, 8 | access = Lfs.access, 9 | observe = Lfs.observe, 10 | dates = {}, 11 | } 12 | end 13 | 14 | function Lfs:access (name, filename) 15 | if self.dates [name] ~= lfs.attributes (filename, "modification") then 16 | self.loaded [name] = nil 17 | end 18 | end 19 | 20 | function Lfs:observe (name, filename) 21 | self.dates [name] = lfs.attributes (filename, "modification") 22 | end 23 | 24 | return Lfs.new () -------------------------------------------------------------------------------- /test/nginx.conf: -------------------------------------------------------------------------------- 1 | error_log {{{TMP}}}/error.log; 2 | pid {{{TMP}}}/nginx.pid; 3 | 4 | worker_processes 1; 5 | events { 6 | worker_connections 1024; 7 | } 8 | 9 | http { 10 | tcp_nopush on; 11 | tcp_nodelay on; 12 | keepalive_timeout 65; 13 | types_hash_max_size 2048; 14 | 15 | client_body_temp_path {{{TMP}}}client_body; 16 | fastcgi_temp_path {{{TMP}}}/fastcgi_temp; 17 | proxy_temp_path {{{TMP}}}/proxy_temp; 18 | scgi_temp_path {{{TMP}}}/scgi_temp; 19 | uwsgi_temp_path {{{TMP}}}/uwsgi_temp; 20 | lua_package_path {{{PATH}}}; 21 | lua_package_cpath {{{CPATH}}}; 22 | 23 | server { 24 | listen localhost:{{{PORT}}}; 25 | listen *:{{{PORT}}}; 26 | server_name "lua-server"; 27 | charset utf-8; 28 | default_type application/octet-stream; 29 | access_log {{{TMP}}}/access.log; 30 | 31 | location /lua { 32 | default_type application/lua; 33 | root /; 34 | set $target ""; 35 | access_by_lua ' 36 | coroutine = { 37 | create = function () return true end, 38 | } 39 | require "compat52" 40 | local name = ngx.var.uri:match "/lua/(.*)" 41 | local filename = package.searchpath (name, package.path) 42 | if filename then 43 | ngx.var.target = filename 44 | else 45 | ngx.log (ngx.ERR, "failed to locate lua module: " .. name) 46 | return ngx.exit (404) 47 | end 48 | '; 49 | try_files $target =404; 50 | } 51 | 52 | location /luaset { 53 | default_type application/json; 54 | access_by_lua ' 55 | coroutine = { 56 | create = function () return true end, 57 | } 58 | require "compat52" 59 | ngx.req.read_body () 60 | local body = ngx.req.get_body_data () 61 | local json = require "cjson" 62 | local http = require "resty.http" 63 | local data = json.decode (body) 64 | local result = {} 65 | for k, t in pairs (data) do 66 | local hc = http:new () 67 | local url = "http://127.0.0.1:8080/lua/" .. k 68 | local res, err = hc:request_uri (url, { 69 | method = "GET", 70 | headers = { 71 | ["If-None-Match"] = type (t) == "table" and t.etag, 72 | }, 73 | }) 74 | if not res then 75 | ngx.log (ngx.ERR, "failed to request: " .. err) 76 | return 77 | end 78 | if res.status == 200 then 79 | result [k] = { 80 | lua = res.body, 81 | etag = res.headers.etag:match [[^"([^"]+)"$]], 82 | } 83 | elseif res.status == 304 then 84 | result [k] = {} 85 | elseif res.status == 404 then 86 | result [k] = nil 87 | end 88 | end 89 | ngx.say (json.encode (result)) 90 | '; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/test-ev.lua: -------------------------------------------------------------------------------- 1 | require "busted.runner" () 2 | 3 | local assert = require "luassert" 4 | local ev = require "ev" 5 | 6 | if not package.searchers then 7 | require "compat52" 8 | end 9 | 10 | describe ("the hotswap.ev module", function () 11 | 12 | it ("can be required", function () 13 | assert.has.no.error (function () 14 | require "hotswap.ev" 15 | end) 16 | end) 17 | 18 | it ("requires preloaded modules", function () 19 | assert.has.no.error (function () 20 | local Hotswap = require "hotswap.ev" 21 | ev.Idle.new (function (loop, idle, _) 22 | Hotswap.require "string" 23 | idle:stop (loop) 24 | loop:unloop () 25 | end):start (ev.Loop.default) 26 | ev.Loop.default:loop () 27 | end) 28 | end) 29 | 30 | it ("requires lua modules", function () 31 | assert.has.no.error (function () 32 | local Hotswap = require "hotswap.ev" 33 | ev.Idle.new (function (loop, idle, _) 34 | Hotswap.require "busted" 35 | idle:stop (loop) 36 | loop:unloop () 37 | end):start (ev.Loop.default) 38 | ev.Loop.default:loop () 39 | end) 40 | end) 41 | 42 | it ("requires binary modules", function () 43 | assert.has.no.error (function () 44 | local Hotswap = require "hotswap.ev" 45 | ev.Idle.new (function (loop, idle, _) 46 | Hotswap.require "socket" 47 | idle:stop (loop) 48 | loop:unloop () 49 | end):start (ev.Loop.default) 50 | ev.Loop.default:loop () 51 | end) 52 | end) 53 | 54 | it ("allows to test require", function () 55 | assert.has.no.error (function () 56 | local Hotswap = require "hotswap.ev" 57 | ev.Idle.new (function (loop, idle, _) 58 | assert.is_truthy (Hotswap.try_require "busted") 59 | assert.is_falsy (Hotswap.try_require "nonexisting") 60 | idle:stop (loop) 61 | loop:unloop () 62 | end):start (ev.Loop.default) 63 | ev.Loop.default:loop () 64 | end) 65 | end) 66 | 67 | end) 68 | -------------------------------------------------------------------------------- /test/test-hash.lua: -------------------------------------------------------------------------------- 1 | require "busted.runner" () 2 | 3 | local assert = require "luassert" 4 | 5 | if not package.searchers then 6 | require "compat52" 7 | end 8 | 9 | describe ("the hotswap.hash module", function () 10 | 11 | it ("can be required", function () 12 | assert.has.no.error (function () 13 | require "hotswap.hash" 14 | end) 15 | end) 16 | 17 | it ("requires preloaded modules", function () 18 | assert.has.no.error (function () 19 | local Hotswap = require "hotswap.hash" 20 | Hotswap.require "string" 21 | end) 22 | end) 23 | 24 | it ("requires lua modules", function () 25 | assert.has.no.error (function () 26 | local Hotswap = require "hotswap.hash" 27 | Hotswap.require "busted" 28 | end) 29 | end) 30 | 31 | it ("requires binary modules", function () 32 | assert.has.no.error (function () 33 | local Hotswap = require "hotswap.hash" 34 | Hotswap.require "socket" 35 | end) 36 | end) 37 | 38 | it ("allows to test require", function () 39 | assert.has.no.error (function () 40 | local Hotswap = require "hotswap.hash" 41 | assert.is_truthy (Hotswap.try_require "busted") 42 | assert.is_falsy (Hotswap.try_require "nonexisting") 43 | end) 44 | end) 45 | 46 | end) 47 | -------------------------------------------------------------------------------- /test/test-hotswap.lua: -------------------------------------------------------------------------------- 1 | require "busted.runner" () 2 | 3 | local assert = require "luassert" 4 | 5 | if not package.searchers then 6 | require "compat52" 7 | end 8 | 9 | describe ("the hotswap module", function () 10 | 11 | it ("can be required", function () 12 | assert.has.no.error (function () 13 | require "hotswap" 14 | end) 15 | end) 16 | 17 | it ("requires preloaded modules", function () 18 | assert.has.no.error (function () 19 | local Hotswap = require "hotswap" 20 | Hotswap.require "string" 21 | end) 22 | end) 23 | 24 | it ("requires lua modules", function () 25 | assert.has.no.error (function () 26 | local Hotswap = require "hotswap" 27 | Hotswap.require "busted" 28 | end) 29 | end) 30 | 31 | it ("requires binary modules", function () 32 | assert.has.no.error (function () 33 | local Hotswap = require "hotswap" 34 | Hotswap.require "socket" 35 | end) 36 | end) 37 | 38 | it ("allows to test require", function () 39 | assert.has.no.error (function () 40 | local Hotswap = require "hotswap" 41 | assert.is_truthy (Hotswap.try_require "busted") 42 | assert.is_falsy (Hotswap.try_require "nonexisting") 43 | end) 44 | end) 45 | 46 | end) 47 | -------------------------------------------------------------------------------- /test/test-http.lua: -------------------------------------------------------------------------------- 1 | require "busted.runner" () 2 | 3 | local assert = require "luassert" 4 | local socket = require "socket" 5 | 6 | if not package.searchers then 7 | require "compat52" 8 | end 9 | 10 | describe ("the hotswap.http module", function () 11 | 12 | local _package_path = package.path 13 | local _package_cpath = package.cpath 14 | local tmp 15 | local port 16 | local options 17 | 18 | setup (function () 19 | local server = socket.bind ("*", 0) 20 | local _, p = server:getsockname () 21 | server:close () 22 | port = p 23 | tmp = os.tmpname () 24 | local conf_file = io.open ("test/nginx.conf", "r") 25 | local conf = conf_file:read "*all" 26 | conf_file:close () 27 | conf = conf 28 | :gsub ("{{{TMP}}}" , tmp) 29 | :gsub ("{{{PORT}}}" , tostring (port)) 30 | :gsub ("{{{PATH}}}" , string.format ("%q", package.path .. ";" .. package.path :gsub ("5%.%d", "5.1"))) 31 | :gsub ("{{{CPATH}}}", string.format ("%q", package.cpath .. ";" .. package.cpath:gsub ("5%.%d", "5.1"))) 32 | conf_file = io.open (tmp, "w") 33 | conf_file:write (conf .. "\n") 34 | conf_file:close () 35 | local command = ([[ 36 | mv {{{TMP}}} {{{TMP}}}.back 37 | mkdir -p {{{TMP}}} 38 | mv {{{TMP}}}.back {{{TMP}}}/nginx.conf 39 | nginx -p {{{TMP}}} -c {{{TMP}}}/nginx.conf 40 | ]]):gsub ("{{{TMP}}}", tmp) 41 | assert (os.execute (command)) 42 | options = { 43 | encode = function (t) 44 | if next (t) == nil 45 | or next (t, next (t)) ~= nil then 46 | return 47 | end 48 | local k, v = next (t) 49 | return { 50 | url = "http://127.0.0.1:" .. tostring (port) .. "/lua/" .. k, 51 | method = "GET", 52 | headers = { 53 | ["If-None-Match"] = type (v) == "table" and v.etag or nil, 54 | ["Lua-Module" ] = k, 55 | }, 56 | } 57 | end, 58 | decode = function (t) 59 | local module = t.request.headers ["Lua-Module"] 60 | if t.code == 200 then 61 | return { 62 | [module] = { 63 | lua = t.body, 64 | etag = t.headers.etag, 65 | }, 66 | } 67 | elseif t.code == 304 then 68 | return { 69 | [module] = {}, 70 | } 71 | elseif t.code == 404 then 72 | return {} 73 | else 74 | return nil 75 | end 76 | end, 77 | } 78 | end) 79 | 80 | teardown (function () 81 | local command = ([[ 82 | kill -QUIT $(cat {{{TMP}}}/nginx.pid) 83 | rm -rf {{{TMP}}} 84 | ]]):gsub ("{{{TMP}}}", tmp) 85 | assert (os.execute (command)) 86 | package.path = _package_path 87 | package.cpath = _package_cpath 88 | end) 89 | 90 | it ("can be required", function () 91 | assert.has.no.error (function () 92 | require "hotswap.http" 93 | end) 94 | end) 95 | 96 | it ("can be instantiated", function () 97 | assert.has.no.error (function () 98 | require "hotswap.http" (options) 99 | end) 100 | end) 101 | 102 | it ("fails to require preloaded modules through HTTP", function () 103 | assert.has.error (function () 104 | local Hotswap = require "hotswap.http" (options) 105 | Hotswap.loaded = {} 106 | Hotswap.path = "" 107 | Hotswap.cpath = "" 108 | Hotswap.require "string" 109 | end) 110 | end) 111 | 112 | it ("requires preloaded modules locally", function () 113 | assert.has.no.error (function () 114 | local Hotswap = require "hotswap.http" (options) 115 | Hotswap.require "string" 116 | end) 117 | end) 118 | 119 | it ("requires lua modules through HTTP", function () 120 | assert.has.no.error (function () 121 | local Hotswap = require "hotswap.http" (options) 122 | Hotswap.loaded = {} 123 | Hotswap.path = "" 124 | Hotswap.cpath = "" 125 | Hotswap.require "dkjson" 126 | end) 127 | end) 128 | 129 | it ("requires lua modules locally", function () 130 | assert.has.no.error (function () 131 | local Hotswap = require "hotswap.http" (options) 132 | Hotswap.loaded = {} 133 | Hotswap.require "dkjson" 134 | end) 135 | end) 136 | 137 | it ("fails to require binary modules through HTTP", function () 138 | assert.has.error (function () 139 | local Hotswap = require "hotswap.http" (options) 140 | Hotswap.loaded = {} 141 | Hotswap.path = "" 142 | Hotswap.cpath = "" 143 | Hotswap.require "xxhash" 144 | end) 145 | end) 146 | 147 | it ("requires binary modules locally", function () 148 | assert.has.no.error (function () 149 | local Hotswap = require "hotswap.http" (options) 150 | Hotswap.loaded = {} 151 | Hotswap.require "xxhash" 152 | end) 153 | end) 154 | 155 | it ("fails to require non existing modules through HTTP", function () 156 | assert.has.error (function () 157 | local Hotswap = require "hotswap.http" (options) 158 | Hotswap.loaded = {} 159 | Hotswap.path = "" 160 | Hotswap.cpath = "" 161 | Hotswap.require "nonexisting" 162 | end) 163 | end) 164 | 165 | it ("requires non existing modules locally", function () 166 | assert.has.error (function () 167 | local Hotswap = require "hotswap.http" (options) 168 | Hotswap.loaded = {} 169 | Hotswap.require "nonexisting" 170 | end) 171 | end) 172 | 173 | it ("allows to test require", function () 174 | assert.has.no.error (function () 175 | local Hotswap = require "hotswap.http" (options) 176 | assert.is_truthy (Hotswap.try_require "dkjson") 177 | assert.is_falsy (Hotswap.try_require "nonexisting") 178 | end) 179 | end) 180 | 181 | end) 182 | -------------------------------------------------------------------------------- /test/test-lfs.lua: -------------------------------------------------------------------------------- 1 | require "busted.runner" () 2 | 3 | local assert = require "luassert" 4 | 5 | if not package.searchers then 6 | require "compat52" 7 | end 8 | 9 | describe ("the hotswap.lfs module", function () 10 | 11 | it ("can be required", function () 12 | assert.has.no.error (function () 13 | require "hotswap.lfs" 14 | end) 15 | end) 16 | 17 | it ("requires preloaded modules", function () 18 | assert.has.no.error (function () 19 | local Hotswap = require "hotswap.lfs" 20 | Hotswap.require "string" 21 | end) 22 | end) 23 | 24 | it ("requires lua modules", function () 25 | assert.has.no.error (function () 26 | local Hotswap = require "hotswap.lfs" 27 | Hotswap.require "busted" 28 | end) 29 | end) 30 | 31 | it ("requires binary modules", function () 32 | assert.has.no.error (function () 33 | local Hotswap = require "hotswap.lfs" 34 | Hotswap.require "socket" 35 | end) 36 | end) 37 | 38 | it ("allows to test require", function () 39 | assert.has.no.error (function () 40 | local Hotswap = require "hotswap.lfs" 41 | assert.is_truthy (Hotswap.try_require "busted") 42 | assert.is_falsy (Hotswap.try_require "nonexisting") 43 | end) 44 | end) 45 | 46 | end) 47 | --------------------------------------------------------------------------------