Overview
| Comment: | enable remote control of running instances |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
f8816b0ab5148803709d93515359d376 |
| User & Date: | lexi on 2020-12-29 15:48:34 |
| Other Links: | manifest | tags |
Context
|
2020-12-30
| ||
| 00:43 | continued iteration check-in: 0324d62546 user: lexi tags: trunk | |
|
2020-12-29
| ||
| 15:48 | enable remote control of running instances check-in: f8816b0ab5 user: lexi tags: trunk | |
| 14:35 | check in missing file check-in: 5a4f99fb55 user: lexi tags: trunk | |
Changes
Modified ipc.t from [5ea8386d8f] to [ff639e2a51].
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 ... 246 247 248 249 250 251 252 253 254 255 256 257 258 259 ... 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
cmd.cmd = m.cmd.none
end
terra m.emperor:mallack() return lib.mem.heapa(m.ack, self:countpeers()) end
terra m.emperor.methods.decree :: {
&m.emperor, uint64, rawstring, m.cmd.t, uint64, &m.ack
} -> {}
terra m.emperor:decree(
tgtclid: uint64, tgtname: rawstring,
cmd: m.cmd.t, operand: uint64,
result: &m.ack
): {}
if self.client then lib.bail('client attempted to issue IPC decree') end
var dem = m.demand {
cmd = cmd;
operand = operand;
empq = self.msqid; -- register to receive replies
}
var npeers = self:countpeers()
if npeers == 0 then lib.bail('no processes connected to control bus') end
if tgtclid == 0 and tgtname == nil then
lib.dbg('sending to all instances, waiting for edict to become writable')
self.edict:sem(0) -- wait for all locks on edict to resolve
lib.dbg('locking edict')
self.edict:sem(npeers) -- place a read lock for each peer
self.edict.demand = dem
lib.dbg('sending edict')
................................................................................
found = true
tgt = acks(i).cliq
end
end
acks:free()
if not found then
lib.warn('no such instance is currently online and responding to IPC calls')
else
lib.dbg('located instance, sending command')
if mq.msgsnd(tgt, &dem, sizeof(m.demand), 0) == -1 then
lib.bail('could not send command to target process')
end
while true do
var ack: m.ack
................................................................................
lib.dbg('got response, writing out and returning')
@result = ack
break
else lib.warn('got spurious response, ignoring') end
end
end
end
end
terra m.demand:ack(emp: &m.emperor, a: &m.ack)
a.clid = emp.clid
a.cliq = emp.msqid
mq.msgsnd(self.empq, a, sizeof(m.ack), 0)
end
|
| | | > > |
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 ... 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 ... 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 |
cmd.cmd = m.cmd.none
end
terra m.emperor:mallack() return lib.mem.heapa(m.ack, self:countpeers()) end
terra m.emperor.methods.decree :: {
&m.emperor, uint64, rawstring, m.cmd.t, uint64, &m.ack
} -> bool
terra m.emperor:decree(
tgtclid: uint64, tgtname: rawstring,
cmd: m.cmd.t, operand: uint64,
result: &m.ack
): bool
if self.client then lib.bail('client attempted to issue IPC decree') end
var dem = m.demand {
cmd = cmd;
operand = operand;
empq = self.msqid; -- register to receive replies
}
var npeers = self:countpeers()
if npeers == 0 then lib.warn('no processes connected to control bus') return false end
if tgtclid == 0 and tgtname == nil then
lib.dbg('sending to all instances, waiting for edict to become writable')
self.edict:sem(0) -- wait for all locks on edict to resolve
lib.dbg('locking edict')
self.edict:sem(npeers) -- place a read lock for each peer
self.edict.demand = dem
lib.dbg('sending edict')
................................................................................
found = true
tgt = acks(i).cliq
end
end
acks:free()
if not found then
lib.warn('no such instance is currently online and responding to IPC calls')
return false
else
lib.dbg('located instance, sending command')
if mq.msgsnd(tgt, &dem, sizeof(m.demand), 0) == -1 then
lib.bail('could not send command to target process')
end
while true do
var ack: m.ack
................................................................................
lib.dbg('got response, writing out and returning')
@result = ack
break
else lib.warn('got spurious response, ignoring') end
end
end
end
return true
end
terra m.demand:ack(emp: &m.emperor, a: &m.ack)
a.clid = emp.clid
a.cliq = emp.msqid
mq.msgsnd(self.empq, a, sizeof(m.ack), 0)
end
|
Modified mem.t from [a177326f1c] to [1f9397ac82].
60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
t.methods = {
free = terra(self: &t): bool
[recurse and quote
self.ptr:free()
end or {}]
if self.ct > 0 then
m.heapf(self.ptr)
self.ct = 0
return true
end
return false
end;
init = terra(self: &t, newct: intptr): bool
if newct == 0 then self.ct = 0 self.ptr = nil return false end
|
> |
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
t.methods = {
free = terra(self: &t): bool
[recurse and quote
self.ptr:free()
end or {}]
if self.ct > 0 then
m.heapf(self.ptr)
self.ptr = nil
self.ct = 0
return true
end
return false
end;
init = terra(self: &t, newct: intptr): bool
if newct == 0 then self.ct = 0 self.ptr = nil return false end
|
Modified mgtool.t from [c954b701da] to [c623f2c8c5].
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 .. 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 .. 67 68 69 70 71 72 73 74 75 76 77 78 79 80 .. 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 ... 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 ... 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
local ctloptions = lib.cmdparse({
version = {'V', 'display information about the binary build and exit'};
verbose = {'v', 'increase logging verbosity', inc=1};
quiet = {'q', 'do not print to standard out'};
help = {'h', 'display this list'};
backend_file = {'B', 'init from specified backend file', consume=1};
backend = {'b', 'operate on only the selected backend'};
instance = {'i', 'specify the instance to control by name', consume=1};
all = {'A', 'affect all running instances'};
}, { subcmd = 1 })
local pbasic = lib.cmdparse {
help = {'h', 'display this list'}
}
local subcmds = {
}
local ctlcmds = {
{ 'start', 'start a new instance of the server' };
{ 'stop', 'stop a running instance' };
{ 'attach', 'capture log output from a running instance' };
{ 'db', 'set up and manage the database' };
{ 'user', 'manage users, privileges, and credentials'};
{ 'mkroot <handle>', 'establish a new root user with the given handle' };
{ 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
{ 'actor <xid> create', 'instantiate a new actor' };
{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
................................................................................
v[2]
)
end
return str
end
local struct idelegate {
all: bool
src: &lib.store.source
srv: &lib.srv.overlord
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
local expr = {...}
local rt
for _,f in pairs(lib.store.backend.entries) do
local fn = f.field or f[1]
local ft = f.type or f[2]
................................................................................
if self.all
then r=self.srv:[meth]([expr])
elseif self.src ~= nil then r=self.src:[meth]([expr])
else lib.bail('no data source specified')
end
in r end
end)
local terra gensec(sdest: rawstring)
var dest = [&uint8](sdest)
lib.crypt.spray(dest,64)
for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end
dest[64] = 0
end
................................................................................
else tmppw[i] = tmppw[i] + 0x30 end
end
lib.dbg('assigning temporary password')
dlg:auth_create_pw(uid, reset, pstr {
ptr = [rawstring](tmppw), ct = 32
})
end
local emp = lib.ipc.global_emperor
local terra entry_mgtool(argc: int, argv: &rawstring): int
if argc < 1 then lib.bail('bad invocation!') end
lib.noise.init(2)
[lib.init]
var srv: lib.srv.overlord
var dlg = idelegate { srv = &srv, src = nil }
var mode: ctloptions
mode:parse(argc,argv) defer mode:free()
if mode.version then version() return 0 end
if mode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, cmdhelp(ctlcmds)) ]
return 0
................................................................................
var cnf: rawstring
if mode.backend_file ~= nil
then cnf = @mode.backend_file
else cnf = lib.proc.getenv('parsav_backend_file')
end
if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end
if mode.all then dlg.all = true else
-- iterate through and pick the right backend
end
if mode.arglist.ct == 0 then lib.bail('no command') return 1 end
if lib.str.cmp(mode.arglist(0),'start') ~= 0 then
-- hack to save us some pain around forking
emp = lib.ipc.emperor.mk(false)
end
defer emp:release()
if lib.str.cmp(mode.arglist(0),'attach') == 0 then
elseif lib.str.cmp(mode.arglist(0),'start') == 0 then
mode.arglist(0) = "-";
var chargv = mode.arglist.ptr
var lsr = lib.ipc.listener.mk()
var chpid = lib.proc.fork()
if chpid == 0 then
lsr:release()
--lib.proc.daemonize(1,0)
lib.io.close(0) lib.io.close(1) lib.io.close(2)
lib.proc.exec([config.prefix_bin .. '/parsavd'], chargv)
lib.proc.execp([config.prefix_bin .. '/parsavd'], chargv)
lib.ipc.notify_parent(lib.ipc.signals.state_fail_find)
lib.bail('cannot find parsav program')
else
lib.report('starting parsav daemon')
while true do
var sig = lsr:block()
if sig.system then lib.dbg('got system signal') end
if sig.from == chpid then
if sig.system and sig.sig == lib.ipc.signals.sys_child then
................................................................................
else lib.warn('got unrecognized signal, ignoring')
end
end
end
lsr:release() -- just because i feel distinctly uncomfortable leaving it out
end
elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then
var acks = emp:mallack()
emp:decree(0,nil, lib.ipc.cmd.stop, 0, &acks(0)) -- TODO targeting
for i=0,acks.ct do
if acks(i).success then
lib.io.fmt('instance %llu successfully stepped down\n', acks(i).clid)
else
lib.io.fmt('instance %llu reports failure to halt\n', acks(i).clid)
end
end
acks:free()
else
if lib.str.cmp(mode.arglist(0),'db') == 0 then
var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0))
if dbmode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts, cmdhelp {
{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
{ 'db vacuum', 'delete old remote content from the database' };
|
| > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > | > > > > > > > > > > | > > > > > > | > > > > | > > > > > > | > | | > | | | | | < < | > > > > > > > > > | | > |
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 .. 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 .. 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 ... 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 ... 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 ... 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
local ctloptions = lib.cmdparse({
version = {'V', 'display information about the binary build and exit'};
verbose = {'v', 'increase logging verbosity', inc=1};
quiet = {'q', 'do not print to standard out'};
help = {'h', 'display this list'};
backend_file = {'B', 'init from specified backend file', consume=1};
backend = {'b', 'operate on only the selected backend'};
instance_id = {'i', 'specify the instance to control by name', consume=1};
instance_serial = {'I', 'specify the instance to control by serial', consume=1};
all = {'A', 'affect all running instances/backends'};
}, { subcmd = 1 })
local pbasic = lib.cmdparse {
help = {'h', 'display this list'}
}
local subcmds = {
}
local ctlcmds = {
{ 'start', 'start a new instance of the server' };
{ 'stop', 'stop a running instance' };
{ 'ls', 'list all running instances' };
{ 'attach', 'capture log output from a running instance' };
{ 'db', 'set up and manage the database' };
{ 'user', 'manage users, privileges, and credentials'};
{ 'mkroot <handle>', 'establish a new root user with the given handle' };
{ 'actor <xid> purge-all', 'remove all traces of a user from the database (except local user credentials -- use \27[1mauth all purge\27[m to prevent a user from accessing the instance)' };
{ 'actor <xid> create', 'instantiate a new actor' };
{ 'actor <xid> bestow <epithet>', 'bestow an epithet upon an actor' };
................................................................................
v[2]
)
end
return str
end
local struct idelegate {
emperor: &lib.ipc.emperor
all: bool
src: &lib.store.source
srv: &lib.srv.overlord
sid: uint64
iname: rawstring
}
idelegate.metamethods.__methodmissing = macro(function(meth, self, ...)
local expr = {...}
local rt
for _,f in pairs(lib.store.backend.entries) do
local fn = f.field or f[1]
local ft = f.type or f[2]
................................................................................
if self.all
then r=self.srv:[meth]([expr])
elseif self.src ~= nil then r=self.src:[meth]([expr])
else lib.bail('no data source specified')
end
in r end
end)
terra idelegate:ipc_send(cmd: lib.ipc.cmd.t, operand: uint64)
var emp = self.emperor
var acks: lib.mem.ptr(lib.ipc.ack)
if self.sid == 0 and self.iname == nil then
if not self.all and emp:countpeers() > 1 then
lib.bail('either specify the instance to control or pass --all to control all instances')
end
acks = emp:mallack()
emp:decree(0,nil, cmd, operand, &acks(0)) -- TODO targeting
else
acks = lib.mem.heapa(lib.ipc.ack, 1)
if not emp:decree(self.sid, self.iname, cmd, operand, &acks(0)) then
acks:free()
end
end
return acks
end
local terra gensec(sdest: rawstring)
var dest = [&uint8](sdest)
lib.crypt.spray(dest,64)
for i=0,64 do dest[i] = dest[i] % (0x7e - 0x20) + 0x20 end
dest[64] = 0
end
................................................................................
else tmppw[i] = tmppw[i] + 0x30 end
end
lib.dbg('assigning temporary password')
dlg:auth_create_pw(uid, reset, pstr {
ptr = [rawstring](tmppw), ct = 32
})
end
local terra ipc_report(acks: lib.mem.ptr(lib.ipc.ack), rep: rawstring)
var decbuf: int8[21]
for i=0,acks.ct do
var num = lib.math.decstr(acks(i).clid, &decbuf[20])
if acks(i).success then
lib.report('instance #',num,' reports successful ',rep)
else
lib.report('instance #',num,' reports failed ',rep)
end
end
end
local emp = lib.ipc.global_emperor
local terra entry_mgtool(argc: int, argv: &rawstring): int
if argc < 1 then lib.bail('bad invocation!') end
lib.noise.init(2)
[lib.init]
var srv: lib.srv.overlord
var dlg = idelegate {
emperor = &emp, srv = &srv;
src = nil, sid = 0, iname = nil, all = false;
}
var mode: ctloptions
mode:parse(argc,argv) defer mode:free()
if mode.version then version() return 0 end
if mode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' ', ctloptions.helptxt.flags, ' <cmd> [<args>…]', ctloptions.helptxt.opts, cmdhelp(ctlcmds)) ]
return 0
................................................................................
var cnf: rawstring
if mode.backend_file ~= nil
then cnf = @mode.backend_file
else cnf = lib.proc.getenv('parsav_backend_file')
end
if cnf == nil then cnf = [config.prefix_conf .. "/backend.conf"] end
if mode.all then dlg.all = true else
-- iterate through and pick the right backend, if one is indicated
end
if mode.instance_id ~= nil and mode.instance_serial ~= nil then
lib.bail('conflicting flags passed')
end
if mode.instance_id ~= nil then
dlg.iname = @mode.instance_id
elseif mode.instance_serial ~= nil then
-- decode serial
end
if mode.arglist.ct == 0 then lib.bail('no command') return 1 end
if lib.str.cmp(mode.arglist(0),'start') ~= 0 then
-- hack to save us some pain around forking
emp = lib.ipc.emperor.mk(false)
end
defer emp:release()
if lib.str.cmp(mode.arglist(0),'attach') == 0 then
elseif lib.str.cmp(mode.arglist(0),'start') == 0 then
var smode: lib.cmdparse {
help = {'h', 'display this list'};
log = {'l', 'send server\'s logging output to a file', consume=1};
pid = {'p', 'report PID of launched process'};
instance_serial = {'s', 'report the instance serial of the launched process'};
keep_attached = {'k', 'don\'t detach from the calling terminal until a successful startup (or ever, if passed twice)', inc=1};
}
smode:parse(mode.arglist.ct,mode.arglist.ptr)
if smode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' start ', smode.type.helptxt.flags, ' [<instance args>…] [-- <instance flags>…]', smode.type.helptxt.opts) ]
return 1
end
var lsr = lib.ipc.listener.mk()
var chpid = lib.proc.fork()
if chpid == 0 then
lsr:release()
var chargv = lib.mem.heapa(rawstring, smode.arglist.ct + 2)
chargv(0) = '-'
for i = 1, chargv.ct - 1 do
chargv(i) = smode.arglist(i-1)
end
chargv(chargv.ct-1) = nil
if smode.keep_attached == 0 then
lib.io.close(0) lib.io.close(1) lib.io.close(2)
end
lib.proc.exec([config.prefix_bin .. '/parsavd'], chargv.ptr)
lib.proc.execp([config.prefix_bin .. '/parsavd'], chargv.ptr)
lib.ipc.notify_parent(lib.ipc.signals.state_fail_find)
lib.bail('cannot find parsav program')
-- chargv:free()
else
lib.report('starting parsav daemon')
while true do
var sig = lsr:block()
if sig.system then lib.dbg('got system signal') end
if sig.from == chpid then
if sig.system and sig.sig == lib.ipc.signals.sys_child then
................................................................................
else lib.warn('got unrecognized signal, ignoring')
end
end
end
lsr:release() -- just because i feel distinctly uncomfortable leaving it out
end
elseif lib.str.cmp(mode.arglist(0),'stop') == 0 then
if mode.arglist.ct ~= 1 then goto cmderr end
var acks = dlg:ipc_send(lib.ipc.cmd.stop, 0)
if acks:ref() then
ipc_report(acks, 'step-down')
acks:free()
end
elseif lib.str.cmp(mode.arglist(0),'ls') == 0 then
if mode.arglist.ct ~= 1 then goto cmderr end
if dlg.sid == 0 and dlg.iname == nil then dlg.all = true end
var acks = dlg:ipc_send(lib.ipc.cmd.enumerate, 0)
if acks:ref() then
for i=0,acks.ct do
var decbuf: int8[21]
var num = lib.math.decstr(acks(i).clid, &decbuf[20])
[ lib.emit(true,1, '\27[1m(', `num, ')\27[m ',`&acks(i).iname[0],' \27[1;32mactive\27[m') ]
end
acks:free()
else lib.bail('no active instances') end
else
if lib.str.cmp(mode.arglist(0),'db') == 0 then
var dbmode: pbasic dbmode:parse(mode.arglist.ct, &mode.arglist(0))
if dbmode.help then
[ lib.emit(false, 1, 'usage: ', `argv[0], ' db ', dbmode.type.helptxt.flags, ' <cmd> [<args>…]', dbmode.type.helptxt.opts, cmdhelp {
{ 'db init <domain>', 'initialize backend databases (or a single specified database) with the necessary schema and structures for the given FQDN' };
{ 'db vacuum', 'delete old remote content from the database' };
|
Modified parsav.t from [1565f096a3] to [b85f6f7647].
429 430 431 432 433 434 435 436 437 438 439 440 441 442 |
quiet = {'q', 'do not print to standard out'};
help = {'h', 'display this list'};
backend_file = {'B', 'init from specified backend file', consume=1};
static_dir = {'S', 'directory with overrides for static content', consume=1};
builtin_data = {'D', 'do not load static content overrides at runtime under any circumstances'};
instance = {'i', 'set an instance name to make it easier to control multiple daemons', consume = 1};
no_ipc = {'I', 'disable IPC'};
}
local static_setup = quote end
local mapin = quote end
local odir = symbol(rawstring)
local pathbuf = symbol(lib.str.acc)
|
> > |
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
quiet = {'q', 'do not print to standard out'};
help = {'h', 'display this list'};
backend_file = {'B', 'init from specified backend file', consume=1};
static_dir = {'S', 'directory with overrides for static content', consume=1};
builtin_data = {'D', 'do not load static content overrides at runtime under any circumstances'};
instance = {'i', 'set an instance name to make it easier to control multiple daemons', consume = 1};
no_ipc = {'I', 'disable IPC'};
chroot = {'C', 'chroot to the specified directory after starting and connecting to backends', consume=1};
no_lockdown = {'L', 'don\'t drop privileges or apply other security measures that could cause compatibility or communication problems'}
}
local static_setup = quote end
local mapin = quote end
local odir = symbol(rawstring)
local pathbuf = symbol(lib.str.acc)
|
Modified srv.t from [7c204f248d] to [7727a773dd].
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 |
lib.net.mg_mgr_init(&self.webmgr)
self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)
if dbbind.ptr ~= nil then dbbind:free() end
end
terra srv:poll()
lib.net.mg_mgr_poll(&self.webmgr,1000)
end
terra srv:shutdown()
lib.net.mg_mgr_free(&self.webmgr)
for i=0,self.sources.ct do var src = self.sources.ptr + i
lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
src:close()
|
| |
619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 |
lib.net.mg_mgr_init(&self.webmgr)
self.webcon = lib.net.mg_http_listen(&self.webmgr, bind, handle.http, self)
if dbbind.ptr ~= nil then dbbind:free() end
end
terra srv:poll()
lib.net.mg_mgr_poll(&self.webmgr,300)
end
terra srv:shutdown()
lib.net.mg_mgr_free(&self.webmgr)
for i=0,self.sources.ct do var src = self.sources.ptr + i
lib.report('closing data source ', src.id.ptr, '(', src.backend.id, ')')
src:close()
|