Skip to content

Commit c375cf2

Browse files
Oto Šťávaalesmrazek
Oto Šťáva
authored andcommitted
manager: use proper JSON values for socket communication
This commit adds a special JSON mode for control sockets. The mode is activated by issuing a special `__json` command to the socket, resulting in all Lua objects returned by all subsequent commands to be serialized into JSONs, prepended by a 32-bit unsigned integer byte-length value. This JSON mode is now exclusively utilized by Manager, removing the need to hackily strip single-quotes from the output and to read the output by lines. Instead, it can always just read the 32-bit length value and subsequently the whole JSON-formatted message, which is now automatically deserialized into a Python object.
1 parent a94d38a commit c375cf2

File tree

11 files changed

+84
-38
lines changed

11 files changed

+84
-38
lines changed

daemon/engine.c

+12-2
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,17 @@ int engine_pcall(lua_State *L, int argc)
642642
return lua_pcall(L, argc, LUA_MULTRET, 0);
643643
}
644644

645-
int engine_cmd(lua_State *L, const char *str, bool raw)
645+
const char *engine_eval_mode_str(enum engine_eval_mode mode)
646+
{
647+
switch (mode) {
648+
#define XX(cid) case ENGINE_EVAL_MODE_##cid: return #cid;
649+
ENGINE_EVAL_MODE_MAP(XX)
650+
#undef XX
651+
}
652+
return "(invalid)";
653+
}
654+
655+
int engine_cmd(struct lua_State *L, const char *str, enum engine_eval_mode mode)
646656
{
647657
if (L == NULL) {
648658
return kr_error(ENOEXEC);
@@ -651,7 +661,7 @@ int engine_cmd(lua_State *L, const char *str, bool raw)
651661
/* Evaluate results */
652662
lua_getglobal(L, "eval_cmd");
653663
lua_pushstring(L, str);
654-
lua_pushboolean(L, raw);
664+
lua_pushstring(L, engine_eval_mode_str(mode));
655665

656666
/* Check result. */
657667
return engine_pcall(L, 2);

daemon/engine.h

+15-1
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,26 @@ int engine_init(void);
3131
* this and before `network_deinit`. */
3232
void engine_deinit(void);
3333

34+
#define ENGINE_EVAL_MODE_MAP(XX) \
35+
XX(LUA_TABLE) \
36+
XX(RAW) \
37+
XX(JSON) \
38+
//
39+
40+
enum engine_eval_mode {
41+
#define XX(cid) ENGINE_EVAL_MODE_##cid,
42+
ENGINE_EVAL_MODE_MAP(XX)
43+
#undef XX
44+
};
45+
46+
const char *engine_eval_mode_str(enum engine_eval_mode mode);
47+
3448
/** Perform a lua command within the sandbox.
3549
*
3650
* @return zero on success.
3751
* The result will be returned on the lua stack - an error message in case of failure.
3852
* http://www.lua.org/manual/5.1/manual.html#lua_pcall */
39-
int engine_cmd(struct lua_State *L, const char *str, bool raw);
53+
int engine_cmd(struct lua_State *L, const char *str, enum engine_eval_mode mode);
4054

4155
/** Execute current chunk in the sandbox */
4256
int engine_pcall(struct lua_State *L, int argc);

daemon/io.c

+21-9
Original file line numberDiff line numberDiff line change
@@ -647,8 +647,9 @@ int io_listen_tcp(uv_loop_t *loop, uv_tcp_t *handle, int fd, int tcp_backlog, bo
647647

648648

649649
enum io_stream_mode {
650-
io_mode_text = 0,
651-
io_mode_binary = 1,
650+
IO_MODE_TEXT = 0,
651+
IO_MODE_BINARY = 1,
652+
IO_MODE_JSON = 2,
652653
};
653654

654655
struct io_stream_data {
@@ -753,20 +754,28 @@ void io_tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t *bu
753754

754755
/* Pseudo-command for switching to "binary output"; */
755756
if (strcmp(cmd, "__binary") == 0) {
756-
data->mode = io_mode_binary;
757+
data->mode = IO_MODE_BINARY;
758+
goto next_iter;
759+
}
760+
if (strcmp(cmd, "__json") == 0) {
761+
data->mode = IO_MODE_JSON;
757762
goto next_iter;
758763
}
759764

760-
const bool cmd_failed = engine_cmd(L, cmd, false);
765+
const bool cmd_failed = engine_cmd(L, cmd,
766+
(data->mode == IO_MODE_JSON)
767+
? ENGINE_EVAL_MODE_JSON
768+
: ENGINE_EVAL_MODE_LUA_TABLE);
761769
const char *message = NULL;
762770
size_t len_s;
763771
if (lua_gettop(L) > 0) {
764772
message = lua_tolstring(L, -1, &len_s);
765773
}
766774

767-
/* Send back the output, either in "binary" or normal mode. */
768-
if (data->mode == io_mode_binary) {
769-
/* Leader expects length field in all cases */
775+
switch (data->mode) {
776+
case IO_MODE_BINARY:
777+
case IO_MODE_JSON:
778+
/* Length-field-prepended mode */
770779
if (!message || len_s > UINT32_MAX) {
771780
kr_log_error(IO, "unrepresentable response on control socket, "
772781
"sending back empty block (command '%s')\n", cmd);
@@ -776,13 +785,16 @@ void io_tty_process_input(uv_stream_t *stream, ssize_t nread, const uv_buf_t *bu
776785
fwrite(&len_n, sizeof(len_n), 1, out);
777786
if (len_s > 0)
778787
fwrite(message, len_s, 1, out);
779-
} else {
788+
break;
789+
case IO_MODE_TEXT:
790+
/* Human-readable and console-printable mode */
780791
if (message)
781792
fprintf(out, "%s", message);
782793
if (message || !args->quiet)
783794
fprintf(out, "\n");
784795
if (!args->quiet)
785796
fprintf(out, "> ");
797+
break;
786798
}
787799

788800
/* Duplicate command and output to logs */
@@ -826,7 +838,7 @@ struct io_stream_data *io_tty_alloc_data(void) {
826838
struct io_stream_data *data = mm_alloc(pool, sizeof(struct io_stream_data));
827839

828840
data->buf = mp_start(pool->ctx, 512);
829-
data->mode = io_mode_text;
841+
data->mode = IO_MODE_TEXT;
830842
data->blen = 0;
831843
data->pool = pool;
832844

daemon/lua/sandbox.lua.in

+18-7
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ modules.load('extended_error')
514514
-- Load keyfile_default
515515
trust_anchors.add_file('@keyfile_default@', @unmanaged@)
516516

517-
local function eval_cmd_compile(line, raw)
517+
local function eval_cmd_compile(line, mode)
518518
-- Compatibility sandbox code loading
519519
local function load_code(code)
520520
if getfenv then -- Lua 5.1
@@ -523,17 +523,28 @@ local function eval_cmd_compile(line, raw)
523523
return load(code, nil, 't', _ENV)
524524
end
525525
end
526+
527+
-- See `ENGINE_EVAL_MODE_MAP(XX)` C-macro for possible values
526528
local err, chunk
527-
chunk, err = load_code(raw and 'return '..line or 'return table_print('..line..')')
529+
if mode == "LUA_TABLE" then
530+
chunk, err = load_code('return table_print(('..line..'))')
531+
elseif mode == "RAW" then
532+
chunk, err = load_code('return ('..line..')')
533+
elseif mode == "JSON" then
534+
chunk, err = load_code('return tojson(('..line..'))')
535+
else
536+
return nil, "invalid mode"
537+
end
538+
528539
if err then
529540
chunk, err = load_code(line)
530541
end
531542
return chunk, err
532543
end
533544

534545
-- Interactive command evaluation
535-
function eval_cmd(line, raw)
536-
local chunk, err = eval_cmd_compile(line, raw)
546+
function eval_cmd(line, mode)
547+
local chunk, err = eval_cmd_compile(line, mode)
537548
if not err then
538549
return chunk()
539550
else
@@ -642,7 +653,7 @@ end
642653
-- must be public because it is called from eval_cmd()
643654
-- when map() commands are read from control socket
644655
function _map_luaobj_call_wrapper(cmd)
645-
local func = eval_cmd_compile(cmd, true)
656+
local func = eval_cmd_compile(cmd, "RAW")
646657
local ret = kluautil.kr_table_pack(xpcall(func, debug.traceback))
647658
local ok, serial = pcall(krprint.serialize_lua, ret, 'error')
648659
if not ok then
@@ -747,7 +758,7 @@ function map(cmd, format)
747758
if (#cmd <= 0) then
748759
panic('map() command must be non-empty') end
749760
-- syntax check on input command to detect typos early
750-
local chunk, err = eval_cmd_compile(cmd, false)
761+
local chunk, err = eval_cmd_compile(cmd, "LUA_TABLE")
751762
if not chunk then
752763
panic('failure when compiling map() command: %s', err)
753764
end
@@ -785,7 +796,7 @@ function map(cmd, format)
785796
log_info(ffi.C.LOG_GRP_SYSTEM, 'executing map() on %s: command %s', path_name, cmd)
786797
local ret
787798
if local_exec then
788-
ret = eval_cmd(cmd)
799+
ret = eval_cmd(cmd, "LUA_TABLE")
789800
else
790801
ret = map_send_recv(cmd, path)
791802
-- skip dead sockets (leftovers from dead instances)

manager/knot_resolver_manager/datamodel/cache_schema.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
from knot_resolver_manager.utils.modeling.base_schema import lazy_default
2020

2121
_CACHE_CLEAR_TEMPLATE = template_from_str(
22-
"{% from 'macros/common_macros.lua.j2' import tojson %}"
23-
"{% from 'macros/cache_macros.lua.j2' import cache_clear %}"
24-
"{{ tojson(cache_clear(params)) }}"
22+
"{% from 'macros/cache_macros.lua.j2' import cache_clear %} {{ cache_clear(params) }}"
2523
)
2624

2725

manager/knot_resolver_manager/datamodel/templates/macros/common_macros.lua.j2

-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
{% macro tojson(object) -%}
2-
tojson({{ object }})
3-
{%- endmacro %}
4-
51
{% macro quotes(string) -%}
62
'{{ string }}'
73
{%- endmacro %}

manager/knot_resolver_manager/datamodel/templates/monitoring.lua.j2

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ function collect_lazy_statistics()
2424
modules.load('stats')
2525
end
2626

27-
return tojson(stats.list())
27+
return stats.list()
2828
end
2929

3030
--- function used for statistics collection
3131
function collect_statistics()
32-
return tojson(stats.list())
32+
return stats.list()
3333
end

manager/knot_resolver_manager/kresd_controller/interface.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import asyncio
22
import itertools
3+
import json
34
import logging
5+
import struct
46
import sys
57
from abc import ABC, abstractmethod # pylint: disable=no-name-in-module
68
from enum import Enum, auto
@@ -165,7 +167,7 @@ def type(self) -> SubprocessType:
165167
def id(self) -> KresID:
166168
return self._id
167169

168-
async def command(self, cmd: str) -> str:
170+
async def command(self, cmd: str) -> object:
169171
reader: asyncio.StreamReader
170172
writer: Optional[asyncio.StreamWriter] = None
171173
try:
@@ -174,14 +176,18 @@ async def command(self, cmd: str) -> str:
174176
# drop prompt
175177
_ = await reader.read(2)
176178

179+
# switch to JSON mode
180+
writer.write("__json\n".encode("utf8"))
181+
177182
# write command
178183
writer.write(cmd.encode("utf8"))
179184
writer.write(b"\n")
180185
await writer.drain()
181186

182187
# read result
183-
result_bytes = await reader.readline()
184-
return result_bytes.decode("utf8")[:-1] # strip trailing newline
188+
(msg_len,) = struct.unpack(">I", await reader.read(4))
189+
result_bytes = await reader.readexactly(msg_len)
190+
return json.loads(result_bytes.decode("utf8"))
185191

186192
finally:
187193
if writer is not None:

manager/knot_resolver_manager/kresd_controller/registered_workers.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def get_registered_workers_kresids() -> "List[KresID]":
1818
return list(_REGISTERED_WORKERS.keys())
1919

2020

21-
async def command_single_registered_worker(cmd: str) -> "Tuple[KresID, str]":
21+
async def command_single_registered_worker(cmd: str) -> "Tuple[KresID, object]":
2222
for sub in _REGISTERED_WORKERS.values():
2323
return sub.id, await sub.command(cmd)
2424
raise SubprocessControllerException(
@@ -27,8 +27,8 @@ async def command_single_registered_worker(cmd: str) -> "Tuple[KresID, str]":
2727
)
2828

2929

30-
async def command_registered_workers(cmd: str) -> "Dict[KresID, str]":
31-
async def single_pair(sub: "Subprocess") -> "Tuple[KresID, str]":
30+
async def command_registered_workers(cmd: str) -> "Dict[KresID, object]":
31+
async def single_pair(sub: "Subprocess") -> "Tuple[KresID, object]":
3232
return sub.id, await sub.command(cmd)
3333

3434
pairs = await asyncio.gather(*(single_pair(inst) for inst in _REGISTERED_WORKERS.values()))

manager/knot_resolver_manager/server.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ async def _handler_cache_clear(self, request: web.Request) -> web.Response:
260260

261261
_, result = await command_single_registered_worker(config.render_lua())
262262
return web.Response(
263-
body=result,
263+
body=json.dumps(result),
264264
content_type="application/json",
265265
charset="utf8",
266266
)

manager/knot_resolver_manager/statistics.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def _histogram(
6969

7070
class ResolverCollector:
7171
def __init__(self, config_store: ConfigStore) -> None:
72-
self._stats_raw: "Optional[Dict[KresID, str]]" = None
72+
self._stats_raw: "Optional[Dict[KresID, object]]" = None
7373
self._config_store: ConfigStore = config_store
7474
self._collection_task: "Optional[asyncio.Task[None]]" = None
7575
self._skip_immediate_collection: bool = False
@@ -148,8 +148,7 @@ def collect(self) -> Generator[Metric, None, None]:
148148
success = False
149149
try:
150150
if kresid in self._stats_raw:
151-
raw = self._stats_raw[kresid]
152-
metrics: Dict[str, int] = json.loads(raw[1:-1])
151+
metrics = self._stats_raw[kresid]
153152
yield from self._parse_resolver_metrics(kresid, metrics)
154153
success = True
155154
except json.JSONDecodeError:

0 commit comments

Comments
 (0)