From c9ca672ce2708fe7c71951a6899f20e65c8dba1f Mon Sep 17 00:00:00 2001 From: Xenofon Foukas Date: Wed, 13 Nov 2024 01:04:00 +0000 Subject: [PATCH 1/8] Added first version of repo --- .gitmodules | 6 + 3p/nanopb | 1 + README.md | 84 +++--- deploy/Dockerfile | 26 ++ examples/first_example_ipc/.gitignore | 7 + examples/first_example_ipc/Makefile | 41 +++ examples/first_example_ipc/README.md | 114 ++++++++ .../codeletset_load_request.yaml | 21 ++ .../codeletset_unload_request.yaml | 1 + examples/first_example_ipc/example_app.cpp | 107 +++++++ examples/first_example_ipc/example_codelet.c | 75 +++++ .../example_collect_control.cpp | 223 ++++++++++++++ examples/first_example_ipc/load.sh | 7 + examples/first_example_ipc/run_app.sh | 3 + .../first_example_ipc/run_collect_control.sh | 3 + examples/first_example_ipc/run_decoder.sh | 3 + examples/first_example_ipc/schema.options | 1 + examples/first_example_ipc/schema.proto | 9 + examples/first_example_ipc/send_control.sh | 5 + examples/first_example_ipc/unload.sh | 5 + examples/first_example_standalone/.gitignore | 6 + examples/first_example_standalone/Makefile | 36 +++ examples/first_example_standalone/README.md | 88 ++++++ .../codeletset_load_request.yaml | 21 ++ .../codeletset_unload_request.yaml | 1 + .../first_example_standalone/example_app.cpp | 275 ++++++++++++++++++ .../example_codelet.c | 75 +++++ examples/first_example_standalone/load.sh | 7 + examples/first_example_standalone/run_app.sh | 3 + .../first_example_standalone/run_decoder.sh | 3 + .../first_example_standalone/schema.options | 1 + .../first_example_standalone/schema.proto | 9 + .../first_example_standalone/send_control.sh | 5 + examples/first_example_standalone/unload.sh | 5 + init_submodules.sh | 7 + jbpf | 1 + pkg/.gitignore | 2 + pkg/.golangci.yml | 21 ++ pkg/Makefile | 32 ++ pkg/README.md | 20 ++ pkg/__snapshots__/example1/example.pb | Bin 0 -> 421 bytes pkg/__snapshots__/example1/example.pb.c | 26 ++ pkg/__snapshots__/example1/example.pb.h | 155 ++++++++++ .../example1/example:req_resp_serializer.c | 26 ++ .../example1/example:status_serializer.c | 26 ++ pkg/__snapshots__/example2/example2.pb | 6 + pkg/__snapshots__/example2/example2.pb.c | 12 + pkg/__snapshots__/example2/example2.pb.h | 52 ++++ .../example2/example2:item_serializer.c | 26 ++ pkg/__snapshots__/example3/example3.pb | 31 ++ pkg/__snapshots__/example3/example3.pb.c | 20 ++ pkg/__snapshots__/example3/example3.pb.h | 136 +++++++++ .../example3/example3:obj_serializer.c | 26 ++ pkg/cmd/decoder/control/control.go | 111 +++++++ pkg/cmd/decoder/decoder.go | 29 ++ pkg/cmd/decoder/load/load.go | 109 +++++++ pkg/cmd/decoder/load/load_config.go | 113 +++++++ pkg/cmd/decoder/run/run.go | 82 ++++++ pkg/cmd/decoder/unload/unload.go | 91 ++++++ pkg/cmd/decoder/unload/unload_config.go | 78 +++++ pkg/cmd/serde/serde.go | 157 ++++++++++ pkg/cmd/serde/serde_test.go | 161 ++++++++++ pkg/common/file.go | 95 ++++++ pkg/common/options.go | 60 ++++ pkg/common/subproc.go | 30 ++ pkg/data/server.go | 115 ++++++++ pkg/data/server_options.go | 46 +++ pkg/generator/nanopb/files.go | 54 ++++ pkg/generator/nanopb/nanopb.go | 54 ++++ pkg/generator/schema/schema.go | 112 +++++++ pkg/generator/stream/_serializer.c.tpl | 26 ++ pkg/generator/stream/stream.go | 88 ++++++ pkg/generator/stream/templates.go | 28 ++ pkg/go.mod | 21 ++ pkg/go.sum | 36 +++ pkg/jbpf/client.go | 91 ++++++ pkg/jbpf/options.go | 51 ++++ pkg/main.go | 32 ++ pkg/schema/client.go | 170 +++++++++++ pkg/schema/client_options.go | 24 ++ pkg/schema/model.go | 113 +++++++ pkg/schema/options.go | 38 +++ pkg/schema/serve.go | 134 +++++++++ pkg/schema/server.go | 178 ++++++++++++ pkg/schema/server_options.go | 40 +++ pkg/schema/store.go | 75 +++++ setup_jbpfp_env.sh | 5 + testdata/example1/example.options | 3 + testdata/example1/example.proto | 35 +++ testdata/example2/example2.options | 1 + testdata/example2/example2.proto | 6 + testdata/example3/example3.options | 14 + testdata/example3/example3.proto | 31 ++ 93 files changed, 4606 insertions(+), 33 deletions(-) create mode 100644 .gitmodules create mode 160000 3p/nanopb create mode 100644 deploy/Dockerfile create mode 100644 examples/first_example_ipc/.gitignore create mode 100644 examples/first_example_ipc/Makefile create mode 100644 examples/first_example_ipc/README.md create mode 100644 examples/first_example_ipc/codeletset_load_request.yaml create mode 100644 examples/first_example_ipc/codeletset_unload_request.yaml create mode 100644 examples/first_example_ipc/example_app.cpp create mode 100644 examples/first_example_ipc/example_codelet.c create mode 100644 examples/first_example_ipc/example_collect_control.cpp create mode 100755 examples/first_example_ipc/load.sh create mode 100755 examples/first_example_ipc/run_app.sh create mode 100755 examples/first_example_ipc/run_collect_control.sh create mode 100755 examples/first_example_ipc/run_decoder.sh create mode 100644 examples/first_example_ipc/schema.options create mode 100644 examples/first_example_ipc/schema.proto create mode 100755 examples/first_example_ipc/send_control.sh create mode 100755 examples/first_example_ipc/unload.sh create mode 100644 examples/first_example_standalone/.gitignore create mode 100644 examples/first_example_standalone/Makefile create mode 100644 examples/first_example_standalone/README.md create mode 100644 examples/first_example_standalone/codeletset_load_request.yaml create mode 100644 examples/first_example_standalone/codeletset_unload_request.yaml create mode 100644 examples/first_example_standalone/example_app.cpp create mode 100644 examples/first_example_standalone/example_codelet.c create mode 100755 examples/first_example_standalone/load.sh create mode 100755 examples/first_example_standalone/run_app.sh create mode 100755 examples/first_example_standalone/run_decoder.sh create mode 100644 examples/first_example_standalone/schema.options create mode 100644 examples/first_example_standalone/schema.proto create mode 100755 examples/first_example_standalone/send_control.sh create mode 100755 examples/first_example_standalone/unload.sh create mode 100755 init_submodules.sh create mode 160000 jbpf create mode 100644 pkg/.gitignore create mode 100644 pkg/.golangci.yml create mode 100644 pkg/Makefile create mode 100644 pkg/README.md create mode 100644 pkg/__snapshots__/example1/example.pb create mode 100644 pkg/__snapshots__/example1/example.pb.c create mode 100644 pkg/__snapshots__/example1/example.pb.h create mode 100644 pkg/__snapshots__/example1/example:req_resp_serializer.c create mode 100644 pkg/__snapshots__/example1/example:status_serializer.c create mode 100644 pkg/__snapshots__/example2/example2.pb create mode 100644 pkg/__snapshots__/example2/example2.pb.c create mode 100644 pkg/__snapshots__/example2/example2.pb.h create mode 100644 pkg/__snapshots__/example2/example2:item_serializer.c create mode 100644 pkg/__snapshots__/example3/example3.pb create mode 100644 pkg/__snapshots__/example3/example3.pb.c create mode 100644 pkg/__snapshots__/example3/example3.pb.h create mode 100644 pkg/__snapshots__/example3/example3:obj_serializer.c create mode 100644 pkg/cmd/decoder/control/control.go create mode 100644 pkg/cmd/decoder/decoder.go create mode 100644 pkg/cmd/decoder/load/load.go create mode 100644 pkg/cmd/decoder/load/load_config.go create mode 100644 pkg/cmd/decoder/run/run.go create mode 100644 pkg/cmd/decoder/unload/unload.go create mode 100644 pkg/cmd/decoder/unload/unload_config.go create mode 100644 pkg/cmd/serde/serde.go create mode 100644 pkg/cmd/serde/serde_test.go create mode 100644 pkg/common/file.go create mode 100644 pkg/common/options.go create mode 100644 pkg/common/subproc.go create mode 100644 pkg/data/server.go create mode 100644 pkg/data/server_options.go create mode 100644 pkg/generator/nanopb/files.go create mode 100644 pkg/generator/nanopb/nanopb.go create mode 100644 pkg/generator/schema/schema.go create mode 100644 pkg/generator/stream/_serializer.c.tpl create mode 100644 pkg/generator/stream/stream.go create mode 100644 pkg/generator/stream/templates.go create mode 100644 pkg/go.mod create mode 100644 pkg/go.sum create mode 100644 pkg/jbpf/client.go create mode 100644 pkg/jbpf/options.go create mode 100644 pkg/main.go create mode 100644 pkg/schema/client.go create mode 100644 pkg/schema/client_options.go create mode 100644 pkg/schema/model.go create mode 100644 pkg/schema/options.go create mode 100644 pkg/schema/serve.go create mode 100644 pkg/schema/server.go create mode 100644 pkg/schema/server_options.go create mode 100644 pkg/schema/store.go create mode 100755 setup_jbpfp_env.sh create mode 100644 testdata/example1/example.options create mode 100644 testdata/example1/example.proto create mode 100644 testdata/example2/example2.options create mode 100644 testdata/example2/example2.proto create mode 100644 testdata/example3/example3.options create mode 100644 testdata/example3/example3.proto diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b0154c9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "jbpf"] + path = jbpf + url = https://github.com/microsoft/jbpf.git +[submodule "3p/nanopb"] + path = 3p/nanopb + url = https://github.com/nanopb/nanopb.git diff --git a/3p/nanopb b/3p/nanopb new file mode 160000 index 0000000..b36a089 --- /dev/null +++ b/3p/nanopb @@ -0,0 +1 @@ +Subproject commit b36a089ae41284bf4af5230c423750dfbadd649f diff --git a/README.md b/README.md index 5cd7cec..80e45e9 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,51 @@ -# Project - -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. - -As the maintainer of this project, please make a few updates: - -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README - -## Contributing - -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. - -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. - -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. - -## Trademarks - -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +# jbpf-protobuf + +This repository is a extension for [jbpf](https://github.com/microsoft/jbpf/) demonstrating how to utilize protobuf serialization as part of jbpf. + +Prerequisites: +* C compiler +* Go v1.23.2+ +* Make +* Pip +* Python + +The project utilizes [Nanopb](https://github.com/nanopb/nanopb) to generate C structures for given protobuf specs that use contiguous memory. It also generates serializer libraries that can be provided to jbpf, to encode output and decode input data to seamlessly integrate external data processing systems. + +# Getting started + +```sh +# init submodules: +./init_submodules.sh + +# source environment variables +source ./setup_jbpfp_env.sh + +# build jbpf_protobuf_cli +make -C pkg +``` + +Alternatively, build using a container: +```sh +# init submodules: +./init_submodules.sh + +docker build -t jbpf_protobuf_builder:latest -f deploy/Dockerfile . +``` + +## Running the examples + +In order to run any of the samples, you'll need to build Janus. + +```sh +mkdir -p jbpf/build +cd jbpf/build +cmake .. -DJBPF_EXPERIMENTAL_FEATURES=on +make -j +cd ../.. +``` + +Then follow [these](./examples/first_example_standalone/README.md) steps to run a simple example. + +# License + +The jbpf framework is licensed under the [MIT license](LICENSE.md). diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..84b57d9 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,26 @@ +FROM mcr.microsoft.com/oss/go/microsoft/golang:1.23.2-1-azurelinux3.0 AS builder + +RUN tdnf upgrade tdnf --refresh -y && tdnf -y update +RUN tdnf install -y make python3-pip awk jq +RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /root/go/bin v1.60.3 +ENV PATH="$PATH:/root/go/bin" + +COPY pkg /workspace/pkg +COPY 3p /workspace/3p +RUN python3 -m pip install -r /workspace/3p/nanopb/requirements.txt +COPY testdata /workspace/testdata +ENV NANO_PB=/workspace/3p/nanopb + +RUN make -C /workspace/pkg +RUN make -C /workspace/pkg test lint -j + +FROM mcr.microsoft.com/azurelinux/base/core:3.0 +RUN tdnf upgrade tdnf --refresh -y && tdnf -y update +RUN tdnf install -y build-essential make python3-pip + +COPY --from=builder /workspace/3p/nanopb /nanopb +RUN python3 -m pip install -r /nanopb/requirements.txt +COPY --from=builder /workspace/pkg/jbpf_protobuf_cli /usr/local/bin/jbpf_protobuf_cli +ENV NANO_PB=/nanopb + +ENTRYPOINT [ "jbpf_protobuf_cli" ] diff --git a/examples/first_example_ipc/.gitignore b/examples/first_example_ipc/.gitignore new file mode 100644 index 0000000..cf2fdbe --- /dev/null +++ b/examples/first_example_ipc/.gitignore @@ -0,0 +1,7 @@ +*.pb +*.pb.c +*.pb.h +*.so +example_app +example_codelet.o +example_collect_control diff --git a/examples/first_example_ipc/Makefile b/examples/first_example_ipc/Makefile new file mode 100644 index 0000000..ccaa66e --- /dev/null +++ b/examples/first_example_ipc/Makefile @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +ifeq ($(BUILD_TYPE),Debug) + DEBUG_CFLAGS = -g + DEBUG_LDFLAGS = -lgcov +else ifeq ($(BUILD_TYPE),AddressSanitizer) + DEBUG_CFLAGS = -fsanitize=address +endif + +AGENT_NAME := example_app +PRIMARY_NAME := example_collect_control +CODELET_NAME := example_codelet.o +INCLUDES := -I${JBPF_OUT_DIR}/inc -I${JBPF_PATH}/src/common -I${NANO_PB} -DJBPF_EXPERIMENTAL_FEATURES=on +AGENT_LDFLAGS := -L${JBPF_OUT_DIR}/lib -ljbpf -lck -lubpf -lmimalloc -lpthread -ldl -lrt ${DEBUG_LDFLAGS} +PRIMARY_LDFLAGS := -L${JBPF_OUT_DIR}/lib -ljbpf_io -lck -lmimalloc -lpthread -ldl -lrt ${DEBUG_LDFLAGS} +AGENT_FILE := example_app.cpp +PRIMARY_FILE := example_collect_control.cpp +CODELET_FILE := example_codelet.c +CODELET_CC := clang +JBPF_PROTOBUF_CLI := ${JBPFP_PATH}/pkg/jbpf_protobuf_cli + +CODELET_CFLAGS := -O2 -target bpf -Wall -DJBPF_DEBUG_ENABLED -D__x86_64__ + +.PHONY: all clean + +all: clean schema codelet agent primary + +codelet: ${CODELET_FILE} + ${CODELET_CC} ${CODELET_CFLAGS} ${INCLUDES} -c ${CODELET_FILE} -o ${CODELET_NAME} + +schema: + ${JBPF_PROTOBUF_CLI} serde -s schema:packet,manual_ctrl_event; \ + rm -f *_serializer.c + +agent: + g++ -std=c++17 $(INCLUDES) -o ${AGENT_NAME} $(AGENT_FILE) ${DEBUG_CFLAGS} ${AGENT_LDFLAGS} + +primary: + g++ -std=c++17 $(INCLUDES) -o ${PRIMARY_NAME} $(PRIMARY_FILE) ${DEBUG_CFLAGS} ${PRIMARY_LDFLAGS} + +clean: + rm -f ${AGENT_NAME} ${PRIMARY_NAME} ${CODELET_NAME} *.pb.h *.pb.c *.pb *.so diff --git a/examples/first_example_ipc/README.md b/examples/first_example_ipc/README.md new file mode 100644 index 0000000..82b4f56 --- /dev/null +++ b/examples/first_example_ipc/README.md @@ -0,0 +1,114 @@ +# Basic example of standalone *jbpf* operation + +This example showcases a basic *jbpf-protobuf* usage scenario, when using in IPC mode. It provides a C++ application (`example_collect_control`) + that initializes *jbpf* in IPC primary mode, a dummy C++ application (`example_app`), that initializes +*jbpf* in IPC secondary mode, and an example codelet (`example_codelet.o`). +The example demonstrates the following: +1. How to declare and call hooks in the *jbpf* secondary process. +2. How to collect data sent by the codelet from the *jbpf* primary process. +3. How to forward data sent by the codelet onwards to a local decoder using a UDP socket. +4. How to receive data sent by the decoder using a TCP socket onwards to the primary process. +5. How to load and unload codeletsets using the LCM CLI tool (via a Unix socket API). + +For more details of the exact behavior of the application and the codelet, you can check the inline comments in [example_collect_control.cpp](./example_collect_control.cpp), +[example_app.cpp](./example_app.cpp) and [example_codelet.c](./example_codelet.c) + +## Usage + +This example expects *jbpf* to be built (see [README.md](../../README.md)). + +To build the example from scratch, we run the following commands: +```sh +$ source ../../setup_jbpfp_env.sh +$ make +``` + +This should produce these artifacts: +* `example_collect_control` +* `example_app` +* `example_codelet.o` +* `schema:manual_ctrl_event_serializer.so` - serializer library for `manual_ctrl_event` protobuf struct. +* `schema:packet_serializer.so` - serializer library for `packet` protobuf struct. +* `schema.pb` - compiled protobuf of [schema.proto](./schema.proto). +* `schema.pb.c` - nanopb generated C file. +* `schema.pb.h` - nanopb generated H file. + +To bring the primary application up, we run the following commands: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./run_collect_control.sh +``` + +To start the local decoder: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./run_decoder.sh +``` + +If successful, we should see the following line printed: +``` +[JBPF_INFO]: Allocated size is 1107296256 +``` + +To bring the primary application up, we run the following commands on a second terminal: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./run_app.sh +``` + +If successful, we should see the following printed in the log of the secondary: +``` +[JBPF_INFO]: Agent thread initialization finished +[JBPF_INFO]: Setting the name of thread 1035986496 to jbpf_lcm_ipc +[JBPF_INFO]: Registered thread id 1 +[JBPF_INFO]: Started LCM IPC thread at /var/run/jbpf/jbpf_lcm_ipc +[JBPF_DEBUG]: jbpf_lcm_ipc thread ready +[JBPF_INFO]: Registered thread id 2 +[JBPF_INFO]: Started LCM IPC server +``` + +and on the primary: +``` +[JBPF_INFO]: Negotiation was successful +[JBPF_INFO]: Allocation worked for size 1073741824 +[JBPF_INFO]: Allocated size is 1073741824 +[JBPF_INFO]: Heap was created successfully +``` + +To load the codeletset, we run the following commands on a third terminal window: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./load.sh +``` + +If the codeletset was loaded successfully, we should see the following output in the `example_app` window: +``` +[JBPF_INFO]: VM created and loaded successfully: example_codelet +``` + +After that, the primary `example_collect_control` should start printing periodical messages (once per second): +``` +INFO[0008] {"seqNo":5, "value":-5, "name":"instance 5"} streamUUID=00112233-4455-6677-8899-aabbccddeeff +INFO[0009] {"seqNo":6, "value":-6, "name":"instance 6"} streamUUID=00112233-4455-6677-8899-aabbccddeeff +INFO[0010] {"seqNo":7, "value":-7, "name":"instance 7"} streamUUID=00112233-4455-6677-8899-aabbccddeeff +``` + +To send a manual control message to the `example_app`, we run the command: +```sh +$ ./send_control.sh 101 +``` + +This should trigger a message in the `example_app`: +``` +[JBPF_DEBUG]: Called 2 times so far and received manual_ctrl_event with value 101 +``` + +To unload the codeletset, we run the command: +```sh +$ ./unload.sh +``` + +The `example_app` should stop printing the periodical messages and should give the following output: +``` +[JBPF_INFO]: VM with vmfd 0 (i = 0) destroyed successfully +``` \ No newline at end of file diff --git a/examples/first_example_ipc/codeletset_load_request.yaml b/examples/first_example_ipc/codeletset_load_request.yaml new file mode 100644 index 0000000..7976255 --- /dev/null +++ b/examples/first_example_ipc/codeletset_load_request.yaml @@ -0,0 +1,21 @@ +codelet_descriptor: + - codelet_name: example_codelet + codelet_path: ${JBPFP_PATH}/examples/first_example_ipc/example_codelet.o + hook_name: example + in_io_channel: + - name: inmap + stream_id: "11111111111111111111111111111111" + serde: + file_path: ${JBPFP_PATH}/examples/first_example_ipc/schema:manual_ctrl_event_serializer.so + protobuf: + package_path: ${JBPFP_PATH}/examples/first_example_ipc/schema.pb + msg_name: manual_ctrl_event + out_io_channel: + - name: outmap + stream_id: 00112233445566778899AABBCCDDEEFF + serde: + file_path: ${JBPFP_PATH}/examples/first_example_ipc/schema:packet_serializer.so + protobuf: + package_path: ${JBPFP_PATH}/examples/first_example_ipc/schema.pb + msg_name: packet +codeletset_id: example_codeletset diff --git a/examples/first_example_ipc/codeletset_unload_request.yaml b/examples/first_example_ipc/codeletset_unload_request.yaml new file mode 100644 index 0000000..f24189a --- /dev/null +++ b/examples/first_example_ipc/codeletset_unload_request.yaml @@ -0,0 +1 @@ +codeletset_id: example_codeletset diff --git a/examples/first_example_ipc/example_app.cpp b/examples/first_example_ipc/example_app.cpp new file mode 100644 index 0000000..a27db3c --- /dev/null +++ b/examples/first_example_ipc/example_app.cpp @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +#include +#include +#include +#include +#include + +#include "schema.pb.h" + +#include "jbpf.h" +#include "jbpf_hook.h" +#include "jbpf_defs.h" + +using namespace std; + +#define SHM_NAME "example_ipc_app" + +// Hook declaration and definition. +DECLARE_JBPF_HOOK( + example, + struct jbpf_generic_ctx ctx, + ctx, + HOOK_PROTO(packet *p, int ctx_id), + HOOK_ASSIGN(ctx.ctx_id = ctx_id; ctx.data = (uint64_t)(void *)p; ctx.data_end = (uint64_t)(void *)(p + 1);)) + +DEFINE_JBPF_HOOK(example) + +bool done = false; + +void sig_handler(int signo) +{ + done = true; +} + +int handle_signal() +{ + if (signal(SIGINT, sig_handler) == SIG_ERR) + { + return 0; + } + if (signal(SIGTERM, sig_handler) == SIG_ERR) + { + return 0; + } + return -1; +} + +int main(int argc, char **argv) +{ + + struct jbpf_config jbpf_config = {0}; + jbpf_set_default_config_options(&jbpf_config); + + // Instruct libjbpf to use an external IPC interface + jbpf_config.io_config.io_type = JBPF_IO_IPC_CONFIG; + // Configure memory size for the IO buffer + jbpf_config.io_config.io_ipc_config.ipc_mem_size = JBPF_HUGEPAGE_SIZE_1GB; + strncpy(jbpf_config.io_config.io_ipc_config.ipc_name, SHM_NAME, JBPF_IO_IPC_MAX_NAMELEN); + + // Enable LCM IPC interface using UNIX socket at the default socket path (the default is through C API) + jbpf_config.lcm_ipc_config.has_lcm_ipc_thread = true; + snprintf( + jbpf_config.lcm_ipc_config.lcm_ipc_name, + sizeof(jbpf_config.lcm_ipc_config.lcm_ipc_name) - 1, + "%s", + JBPF_DEFAULT_LCM_SOCKET); + + if (!handle_signal()) + { + std::cout << "Could not register signal handler" << std::endl; + return -1; + } + + // Initialize jbpf + if (jbpf_init(&jbpf_config) < 0) + { + return -1; + } + + // Any thread that calls a hook must be registered + jbpf_register_thread(); + + int i = 0; + + // Sample application code calling a hook every second + while (!done) + { + packet p; + p.seq_no = i; + p.value = -i; + + std::stringstream ss; + ss << "instance " << i; + + std::strcpy(p.name, ss.str().c_str()); + + // Call hook and pass packet + hook_example(&p, 1); + sleep(1); + i++; + } + + jbpf_stop(); + exit(EXIT_SUCCESS); + + return 0; +} diff --git a/examples/first_example_ipc/example_codelet.c b/examples/first_example_ipc/example_codelet.c new file mode 100644 index 0000000..c609fa2 --- /dev/null +++ b/examples/first_example_ipc/example_codelet.c @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#include + +#include "jbpf_defs.h" +#include "jbpf_helper.h" +#include "schema.pb.h" + +// Output map of type JBPF_MAP_TYPE_RINGBUF. +// The map is used to send out data of type packet. +// It holds a ringbuffer with a total of 3 elements. +jbpf_ringbuf_map(outmap, packet, 3); + +// Input map of type JBPF_MAP_TYPE_CONTROL_INPUT. +// The map is used to receive data of type manual_ctrl_event. +// It uses a ringbuffer, that can store a total of 3 elements. +jbpf_control_input_map(inmap, manual_ctrl_event, 3); + +// A map of type JBPF_MAP_TYPE_ARRAY, which is used +// to store internal codelet state. +struct jbpf_load_map_def SEC("maps") counter = { + .type = JBPF_MAP_TYPE_ARRAY, + .key_size = sizeof(int), + .value_size = sizeof(int), + .max_entries = 1, +}; + +SEC("jbpf_generic") +uint64_t +jbpf_main(void *state) +{ + + void *c; + int cnt; + struct jbpf_generic_ctx *ctx; + packet *p, *p_end; + packet echo; + manual_ctrl_event resp = {0}; + uint64_t index = 0; + + ctx = state; + + c = jbpf_map_lookup_elem(&counter, &index); + if (!c) + return 1; + + cnt = *(int *)c; + cnt++; + *(uint32_t *)c = cnt; + + p = (packet *)ctx->data; + p_end = (packet *)ctx->data_end; + + if (p + 1 > p_end) + return 1; + + echo = *p; + + // Copy the data that was passed to the codelet to the outmap ringbuffer + // and send them out. + if (jbpf_ringbuf_output(&outmap, &echo, sizeof(echo)) < 0) + { + return 1; + } + + if (jbpf_control_input_receive(&inmap, &resp, sizeof(resp)) == 1) + { + // Print a debug message. This helper function should NOT be used in production environments, due to + // its performance overhead. The helper function will be ignored, if *jbpf* has been built with the + // USE_JBPF_PRINTF_HELPER option set to OFF. + jbpf_printf_debug(" Called %d times so far and received manual_ctrl_event with value %d\n\n", cnt, resp.value); + } + + return 0; +} diff --git a/examples/first_example_ipc/example_collect_control.cpp b/examples/first_example_ipc/example_collect_control.cpp new file mode 100644 index 0000000..06a8422 --- /dev/null +++ b/examples/first_example_ipc/example_collect_control.cpp @@ -0,0 +1,223 @@ +#include +#include + +#include "jbpf.h" +#include "jbpf_defs.h" +#include "jbpf_io.h" +#include "jbpf_io_channel.h" +#include + +#include "schema.pb.h" + +#define SHM_NAME "example_ipc_app" + +#define MAX_SERIALIZED_SIZE 1024 + +int sockfd; +struct sockaddr_in servaddr; +static volatile int done = 0; + +static void +handle_channel_bufs( + struct jbpf_io_channel *io_channel, struct jbpf_io_stream_id *stream_id, void **bufs, int num_bufs, void *ctx) +{ + struct jbpf_io_ctx *io_ctx = static_cast(ctx); + char serialized[MAX_SERIALIZED_SIZE]; + int serialized_size; + + if (stream_id && num_bufs > 0) + { + // Fetch the data and send to local decoder + for (auto i = 0; i < num_bufs; i++) + { + serialized_size = jbpf_io_channel_pack_msg(io_ctx, bufs[i], serialized, sizeof(serialized)); + if (serialized_size > 0) + { + sendto(sockfd, serialized, serialized_size, + MSG_CONFIRM, (const struct sockaddr *)&servaddr, + sizeof(servaddr)); + std::cout << "Message sent, size: " << serialized_size << std::endl; + } + else + { + std::cerr << "Failed to serialize message. Got return code: " << serialized_size << std::endl; + } + } + } +} + +void *fwd_socket_to_channel_in(void *arg) +{ + struct jbpf_io_ctx *io_ctx = static_cast(arg); + + jbpf_io_register_thread(); + + int sockfd, connfd; + socklen_t len; + struct sockaddr_in servaddr, cli; + + // socket create and verification + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) + { + printf("socket creation failed...\n"); + exit(0); + } + else + printf("Socket successfully created..\n"); + bzero(&servaddr, sizeof(servaddr)); + + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = htonl(INADDR_ANY); + servaddr.sin_port = htons(20787); + + if ((bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) != 0) + { + printf("socket bind failed...\n"); + exit(0); + } + else + printf("Socket successfully binded..\n"); + + if ((listen(sockfd, 5)) != 0) + { + printf("Listen failed...\n"); + exit(0); + } + else + printf("Server listening..\n"); + len = sizeof(cli); + + for (;;) + { + connfd = accept(sockfd, (struct sockaddr *)&cli, &len); + if (connfd < 0) + { + printf("server accept failed...\n"); + exit(0); + } + else + printf("server accept the client...\n"); + + char buff[MAX_SERIALIZED_SIZE]; + int n; + struct jbpf_io_stream_id stream_id = {0}; + + for (;;) + { + auto n_diff = read(connfd, &buff[n], sizeof(buff) - n); + n += n_diff; + if (n_diff == 0) + { + printf("Client disconnected\n"); + break; + } + else if (n >= 18) + { + uint16_t payload_size = buff[1] * 256 + buff[0]; + if (n < payload_size + 2) + { + continue; + } + else if (n > payload_size + 2) + { + std::cerr << "Unexpected number of bytes in buffer, expected: " << payload_size << ", got: " << n - 2 << std::endl; + break; + } + + jbpf_channel_buf_ptr deserialized = jbpf_io_channel_unpack_msg(io_ctx, &buff[2], payload_size, &stream_id); + if (deserialized == NULL) + { + std::cerr << "Failed to deserialize message. Got NULL" << std::endl; + } + else + { + auto io_channel = jbpf_io_find_channel(io_ctx, stream_id, false); + if (io_channel) + { + auto ret = jbpf_io_channel_submit_buf(io_channel); + if (ret != 0) + { + std::cerr << "Failed to send message to channel. Got return code: " << ret << std::endl; + } + else + { + std::cout << "Dispatched msg of size: " << payload_size << std::endl; + } + } + else + { + std::cerr << "Failed to find io channel. Got NULL" << std::endl; + } + } + bzero(buff, MAX_SERIALIZED_SIZE); + n = 0; + } + } + } + + close(sockfd); + + // exit the current thread + pthread_exit(NULL); +} + +void handle_ctrl_c(int signum) +{ + printf("\nCaught Ctrl+C! Exiting gracefully...\n"); + done = 1; +} + +int main(int argc, char **argv) +{ + signal(SIGINT, handle_ctrl_c); + + // Creating socket file descriptor + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + { + perror("socket creation failed"); + exit(EXIT_FAILURE); + } + + memset(&servaddr, 0, sizeof(servaddr)); + + // Filling server information + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(20788); + servaddr.sin_addr.s_addr = INADDR_ANY; + + struct jbpf_io_config io_config = {0}; + struct jbpf_io_ctx *io_ctx; + + // Designate the data collection framework as a primary for the IPC + io_config.type = JBPF_IO_IPC_PRIMARY; + + strncpy(io_config.ipc_config.addr.jbpf_io_ipc_name, SHM_NAME, JBPF_IO_IPC_MAX_NAMELEN); + + // Configure memory size for the IO buffer + io_config.ipc_config.mem_cfg.memory_size = JBPF_HUGEPAGE_SIZE_1GB; + + // Configure the jbpf agent to operate in shared memory mode + io_ctx = jbpf_io_init(&io_config); + + if (!io_ctx) + { + return -1; + } + + pthread_t ptid; + pthread_create(&ptid, NULL, &fwd_socket_to_channel_in, io_ctx); + + // Every thread that sends or receives jbpf data needs to be registered using this call + jbpf_io_register_thread(); + + while (!done) + { + // Continuously poll IPC output buffers + jbpf_io_channel_handle_out_bufs(io_ctx, handle_channel_bufs, io_ctx); + sleep(1); + } + + pthread_cancel(ptid); + return 0; +} diff --git a/examples/first_example_ipc/load.sh b/examples/first_example_ipc/load.sh new file mode 100755 index 0000000..f0be6c8 --- /dev/null +++ b/examples/first_example_ipc/load.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-control-ip 0.0.0.0 + +sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_ipc/run_app.sh b/examples/first_example_ipc/run_app.sh new file mode 100755 index 0000000..1a91cd4 --- /dev/null +++ b/examples/first_example_ipc/run_app.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app diff --git a/examples/first_example_ipc/run_collect_control.sh b/examples/first_example_ipc/run_collect_control.sh new file mode 100755 index 0000000..5f6b69b --- /dev/null +++ b/examples/first_example_ipc/run_collect_control.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_collect_control diff --git a/examples/first_example_ipc/run_decoder.sh b/examples/first_example_ipc/run_decoder.sh new file mode 100755 index 0000000..48ccb33 --- /dev/null +++ b/examples/first_example_ipc/run_decoder.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder run --jbpf-enable diff --git a/examples/first_example_ipc/schema.options b/examples/first_example_ipc/schema.options new file mode 100644 index 0000000..618bb99 --- /dev/null +++ b/examples/first_example_ipc/schema.options @@ -0,0 +1 @@ +packet.name max_size:32 diff --git a/examples/first_example_ipc/schema.proto b/examples/first_example_ipc/schema.proto new file mode 100644 index 0000000..b3db3d3 --- /dev/null +++ b/examples/first_example_ipc/schema.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +message packet { + required int32 seq_no = 1; + required int32 value = 2; + required string name = 3; +} + +message manual_ctrl_event { required int32 value = 1; } diff --git a/examples/first_example_ipc/send_control.sh b/examples/first_example_ipc/send_control.sh new file mode 100755 index 0000000..05cff25 --- /dev/null +++ b/examples/first_example_ipc/send_control.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder control \ + --stream-id 11111111-1111-1111-1111-111111111111 \ + --inline-json "{\"value\": $1}" diff --git a/examples/first_example_ipc/unload.sh b/examples/first_example_ipc/unload.sh new file mode 100755 index 0000000..e519f7d --- /dev/null +++ b/examples/first_example_ipc/unload.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder unload -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/.gitignore b/examples/first_example_standalone/.gitignore new file mode 100644 index 0000000..6b7a62a --- /dev/null +++ b/examples/first_example_standalone/.gitignore @@ -0,0 +1,6 @@ +*.pb +*.pb.c +*.pb.h +*.so +example_app +example_codelet.o diff --git a/examples/first_example_standalone/Makefile b/examples/first_example_standalone/Makefile new file mode 100644 index 0000000..e54640b --- /dev/null +++ b/examples/first_example_standalone/Makefile @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +ifeq ($(BUILD_TYPE),Debug) + DEBUG_CFLAGS = -g + DEBUG_LDFLAGS = -lgcov +else ifeq ($(BUILD_TYPE),AddressSanitizer) + DEBUG_CFLAGS = -fsanitize=address +endif + +AGENT_NAME := example_app +CODELET_NAME := example_codelet.o +INCLUDES := -I${JBPF_OUT_DIR}/inc -I${NANO_PB} -DJBPF_EXPERIMENTAL_FEATURES=on +LDFLAGS := -L${JBPF_OUT_DIR}/lib -ljbpf -lck -lubpf -lmimalloc -lpthread -ldl -lrt ${DEBUG_LDFLAGS} +AGENT_FILE := example_app.cpp +CODELET_FILE := example_codelet.c +CODELET_CC := clang +JBPF_PROTOBUF_CLI := ${JBPFP_PATH}/pkg/jbpf_protobuf_cli + +CODELET_CFLAGS := -O2 -target bpf -Wall -DJBPF_DEBUG_ENABLED -D__x86_64__ + +.PHONY: all clean + +all: clean schema codelet agent + +codelet: ${CODELET_FILE} + ${CODELET_CC} ${CODELET_CFLAGS} ${INCLUDES} -c ${CODELET_FILE} -o ${CODELET_NAME} + +schema: + ${JBPF_PROTOBUF_CLI} serde -s schema:packet,manual_ctrl_event; \ + rm -f *_serializer.c + +agent: + g++ -std=c++17 $(INCLUDES) -o ${AGENT_NAME} $(AGENT_FILE) ${DEBUG_CFLAGS} ${LDFLAGS} + +clean: + rm -f ${AGENT_NAME} ${CODELET_NAME} *.pb.h *.pb.c *.pb *.so + diff --git a/examples/first_example_standalone/README.md b/examples/first_example_standalone/README.md new file mode 100644 index 0000000..dbab1b5 --- /dev/null +++ b/examples/first_example_standalone/README.md @@ -0,0 +1,88 @@ +# Basic example of standalone *jbpf* operation + +This example showcases a basic *jbpf* usage scenario. It provides a dummy C++ application (`example_app`), that initializes +*jbpf* in standalone mode, and an example codelet (`example_codelet.o`). +The example demonstrates the following: +1. How to declare and call hooks. +2. How to register handler functions for capturing output data from codelets in standalone mode. +3. How to load and unload codeletsets using the LCM CLI tool (via a Unix socket API). +4. How to send data back to running codelets. + +For more details of the exact behavior of the application and the codelet, check [here](../../docs/understand_first_codelet.md). +You can also check the inline comments in [example_app.cpp](./example_app.cpp) +and [example_codelet.c](./example_codelet.c) + + +## Usage + +This example expects *jbpf* to be built (see [README.md](../../README.md)). + +To build the example from scratch, we run the following commands: +```sh +$ source ../../setup_jbpfp_env.sh +$ make +``` + +This should produce these artifacts: +* `example_app` +* `example_codelet.o` +* `schema:manual_ctrl_event_serializer.so` - serializer library for `manual_ctrl_event` protobuf struct. +* `schema:packet_serializer.so` - serializer library for `packet` protobuf struct. +* `schema.pb` - compiled protobuf of [schema.proto](./schema.proto). +* `schema.pb.c` - nanopb generated C file. +* `schema.pb.h` - nanopb generated H file. + +To bring up the application, we run the following commands: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./run_app.sh +``` + +To start the local decoder: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./run_decoder.sh +``` + +If successful, we shoud see the following line printed: +``` +[JBPF_INFO]: Started LCM IPC server +``` + +To load the codeletset, we run the following commands on a second terminal window: +```sh +$ source ../../setup_jbpfp_env.sh +$ ./load.sh +``` + +If the codeletset was loaded successfully, we should see the following output in the `example_app` window: +``` +[JBPF_INFO]: VM created and loaded successfully: example_codelet +``` + +After that, the agent should start printing periodical messages (once per second): +``` +INFO[0008] {"seqNo":5, "value":-5, "name":"instance 5"} streamUUID=00112233-4455-6677-8899-aabbccddeeff +INFO[0009] {"seqNo":6, "value":-6, "name":"instance 6"} streamUUID=00112233-4455-6677-8899-aabbccddeeff +INFO[0010] {"seqNo":7, "value":-7, "name":"instance 7"} streamUUID=00112233-4455-6677-8899-aabbccddeeff +``` + +To send a manual control message to the `example_app`, we run the command: +```sh +$ ./send_control.sh 101 +``` + +This should trigger a message in the `example_app`: +``` +[JBPF_DEBUG]: Called 2 times so far and received manual_ctrl_event with value 101 +``` + +To unload the codeletset, we run the command: +```sh +$ ./unload.sh +``` + +The `example_app` should stop printing the periodical messages and should give the following output: +``` +[JBPF_INFO]: VM with vmfd 0 (i = 0) destroyed successfully +``` \ No newline at end of file diff --git a/examples/first_example_standalone/codeletset_load_request.yaml b/examples/first_example_standalone/codeletset_load_request.yaml new file mode 100644 index 0000000..617bbdd --- /dev/null +++ b/examples/first_example_standalone/codeletset_load_request.yaml @@ -0,0 +1,21 @@ +codelet_descriptor: + - codelet_name: example_codelet + codelet_path: ${JBPFP_PATH}/examples/first_example_standalone/example_codelet.o + hook_name: example + in_io_channel: + - name: inmap + stream_id: "11111111111111111111111111111111" + serde: + file_path: ${JBPFP_PATH}/examples/first_example_standalone/schema:manual_ctrl_event_serializer.so + protobuf: + package_path: ${JBPFP_PATH}/examples/first_example_standalone/schema.pb + msg_name: manual_ctrl_event + out_io_channel: + - name: outmap + stream_id: 00112233445566778899AABBCCDDEEFF + serde: + file_path: ${JBPFP_PATH}/examples/first_example_standalone/schema:packet_serializer.so + protobuf: + package_path: ${JBPFP_PATH}/examples/first_example_standalone/schema.pb + msg_name: packet +codeletset_id: example_codeletset diff --git a/examples/first_example_standalone/codeletset_unload_request.yaml b/examples/first_example_standalone/codeletset_unload_request.yaml new file mode 100644 index 0000000..f24189a --- /dev/null +++ b/examples/first_example_standalone/codeletset_unload_request.yaml @@ -0,0 +1 @@ +codeletset_id: example_codeletset diff --git a/examples/first_example_standalone/example_app.cpp b/examples/first_example_standalone/example_app.cpp new file mode 100644 index 0000000..3793044 --- /dev/null +++ b/examples/first_example_standalone/example_app.cpp @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +#define BOOST_BIND_GLOBAL_PLACEHOLDERS + +#include +#include +#include +#include +#include +#include + +#include "schema.pb.h" + +#include "jbpf.h" +#include "jbpf_hook.h" +#include "jbpf_defs.h" + +using namespace std; + +#define MAX_SERIALIZED_SIZE 1024 + +int sockfd; +struct sockaddr_in servaddr; + +// Hook declaration and definition. +DECLARE_JBPF_HOOK( + example, + struct jbpf_generic_ctx ctx, + ctx, + HOOK_PROTO(packet *p, int ctx_id), + HOOK_ASSIGN(ctx.ctx_id = ctx_id; ctx.data = (uint64_t)(void *)p; ctx.data_end = (uint64_t)(void *)(p + 1);)) + +DEFINE_JBPF_HOOK(example) + +// Handler function that is invoked every time that jbpf receives one or more buffers of data from a codelet +static void +io_channel_forward_output(jbpf_io_stream_id_t *stream_id, void **bufs, int num_bufs, void *ctx) +{ + auto io_ctx = jbpf_get_io_ctx(); + if (io_ctx == NULL) + { + std::cerr << "Failed to get IO context. Got NULL" << std::endl; + return; + } + + char serialized[MAX_SERIALIZED_SIZE]; + int serialized_size; + + if (stream_id && num_bufs > 0) + { + // Fetch the data and print in JSON format + for (auto i = 0; i < num_bufs; i++) + { + serialized_size = jbpf_io_channel_pack_msg(io_ctx, bufs[i], serialized, sizeof(serialized)); + if (serialized_size > 0) + { + sendto(sockfd, serialized, serialized_size, + MSG_CONFIRM, (const struct sockaddr *)&servaddr, + sizeof(servaddr)); + std::cout << "Message sent, size: " << serialized_size << std::endl; + } + else + { + std::cerr << "Failed to serialize message. Got return code: " << serialized_size << std::endl; + } + } + } +} + +bool done = false; + +void sig_handler(int signo) +{ + done = true; +} + +int handle_signal() +{ + if (signal(SIGINT, sig_handler) == SIG_ERR) + { + return 0; + } + if (signal(SIGTERM, sig_handler) == SIG_ERR) + { + return 0; + } + return -1; +} + +void *fwd_socket_to_channel_in(void *arg) +{ + jbpf_register_thread(); + + auto io_ctx = jbpf_get_io_ctx(); + + if (io_ctx == NULL) + { + std::cerr << "Failed to get IO context. Got NULL" << std::endl; + exit(0); + } + + int sockfd, connfd; + socklen_t len; + struct sockaddr_in servaddr, cli; + // socket create and verification + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd == -1) + { + printf("socket creation failed...\n"); + exit(0); + } + else + printf("Socket successfully created..\n"); + bzero(&servaddr, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = htonl(INADDR_ANY); + servaddr.sin_port = htons(20787); + if ((bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) != 0) + { + printf("socket bind failed...\n"); + exit(0); + } + else + printf("Socket successfully binded..\n"); + if ((listen(sockfd, 5)) != 0) + { + printf("Listen failed...\n"); + exit(0); + } + else + printf("Server listening..\n"); + len = sizeof(cli); + for (;;) + { + connfd = accept(sockfd, (struct sockaddr *)&cli, &len); + if (connfd < 0) + { + printf("server accept failed...\n"); + exit(0); + } + else + printf("server accept the client...\n"); + char buff[MAX_SERIALIZED_SIZE]; + int n; + struct jbpf_io_stream_id stream_id = {0}; + for (;;) + { + auto n_diff = read(connfd, &buff[n], sizeof(buff) - n); + n += n_diff; + if (n_diff == 0) + { + printf("Client disconnected\n"); + break; + } + else if (n >= 18) + { + uint16_t payload_size = buff[1] * 256 + buff[0]; + if (n < payload_size + 2) + { + continue; + } + else if (n > payload_size + 2) + { + std::cerr << "Unexpected number of bytes in buffer, expected: " << payload_size << ", got: " << n - 2 << std::endl; + break; + } + + jbpf_channel_buf_ptr deserialized = jbpf_io_channel_unpack_msg(io_ctx, &buff[2], payload_size, &stream_id); + if (deserialized == NULL) + { + std::cerr << "Failed to deserialize message. Got NULL" << std::endl; + } + else + { + auto io_channel = jbpf_io_find_channel(io_ctx, stream_id, false); + if (io_channel) + { + auto ret = jbpf_io_channel_submit_buf(io_channel); + if (ret != 0) + { + std::cerr << "Failed to send message to channel. Got return code: " << ret << std::endl; + } + else + { + std::cout << "Dispatched msg of size: " << payload_size << std::endl; + } + } + else + { + std::cerr << "Failed to find io channel. Got NULL" << std::endl; + } + } + bzero(buff, MAX_SERIALIZED_SIZE); + n = 0; + } + } + } + close(sockfd); + // exit the current thread + pthread_exit(NULL); +} + +int main(int argc, char **argv) +{ + // Creating socket file descriptor + if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + { + perror("socket creation failed"); + exit(EXIT_FAILURE); + } + + memset(&servaddr, 0, sizeof(servaddr)); + + // Filling server information + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(20788); + servaddr.sin_addr.s_addr = INADDR_ANY; + + struct jbpf_config jbpf_config = {0}; + jbpf_set_default_config_options(&jbpf_config); + + // Enable LCM IPC interface using UNIX socket at the default socket path (the default is through C API) + jbpf_config.lcm_ipc_config.has_lcm_ipc_thread = true; + snprintf( + jbpf_config.lcm_ipc_config.lcm_ipc_name, + sizeof(jbpf_config.lcm_ipc_config.lcm_ipc_name) - 1, + "%s", + JBPF_DEFAULT_LCM_SOCKET); + + if (!handle_signal()) + { + std::cout << "Could not register signal handler" << std::endl; + return -1; + } + + // Initialize jbpf + if (jbpf_init(&jbpf_config) < 0) + { + return -1; + } + + pthread_t ptid; + pthread_create(&ptid, NULL, &fwd_socket_to_channel_in, NULL); + + // Any thread that calls a hook must be registered + jbpf_register_thread(); + + // Register the callback to handle output messages from codelets + jbpf_register_io_output_cb(io_channel_forward_output); + + int i = 0; + + // Sample application code calling a hook every second + while (!done) + { + packet p; + p.seq_no = i; + p.value = -i; + + std::stringstream ss; + ss << "instance " << i; + + std::strcpy(p.name, ss.str().c_str()); + + // Call hook and pass packet + hook_example(&p, 1); + sleep(1); + i++; + } + + jbpf_stop(); + pthread_cancel(ptid); + exit(EXIT_SUCCESS); + + return 0; +} diff --git a/examples/first_example_standalone/example_codelet.c b/examples/first_example_standalone/example_codelet.c new file mode 100644 index 0000000..cfb04f0 --- /dev/null +++ b/examples/first_example_standalone/example_codelet.c @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#include + +#include "jbpf_defs.h" +#include "jbpf_helper.h" +#include "schema.pb.h" + +// Output map of type JBPF_MAP_TYPE_RINGBUF. +// The map is used to send out data of type packet. +// It holds a ringbuffer with a total of 3 elements. +jbpf_ringbuf_map(outmap, packet, 3); + +// Input map of type JBPF_MAP_TYPE_CONTROL_INPUT. +// The map is used to receive data of type manual_ctrl_event. +// It uses a ringbuffer, that can store a total of 3 elements. +jbpf_control_input_map(inmap, manual_ctrl_event, 3); + +// A map of type JBPF_MAP_TYPE_ARRAY, which is used +// to store internal codelet state. +struct jbpf_load_map_def SEC("maps") counter = { + .type = JBPF_MAP_TYPE_ARRAY, + .key_size = sizeof(int), + .value_size = sizeof(int), + .max_entries = 1, +}; + +SEC("jbpf_generic") +uint64_t +jbpf_main(void *state) +{ + + void *c; + int cnt; + struct jbpf_generic_ctx *ctx; + packet *p, *p_end; + packet echo; + manual_ctrl_event resp = {0}; + uint64_t index = 0; + + ctx = state; + + c = jbpf_map_lookup_elem(&counter, &index); + if (!c) + return 1; + + cnt = *(int *)c; + cnt++; + *(uint32_t *)c = cnt; + + p = (packet *)ctx->data; + p_end = (packet *)ctx->data_end; + + if (p + 1 > p_end) + return 1; + + echo = *p; + + // Copy the data that was passed to the codelet to the outmap ringbuffer + // and send them out. + if (jbpf_ringbuf_output(&outmap, &echo, sizeof(echo)) < 0) + { + return 1; + } + + if (jbpf_control_input_receive(&inmap, &resp, sizeof(resp)) == 1) + { + // Print a debug message. This helper function should NOT be used in production environemtns, due to + // its performance overhead. The helper function will be ignored, if *jbpf* has been built with the + // USE_JBPF_PRINTF_HELPER option set to OFF. + jbpf_printf_debug(" Called %d times so far and received manual_ctrl_event with value %d\n\n", cnt, resp.value); + } + + return 0; +} diff --git a/examples/first_example_standalone/load.sh b/examples/first_example_standalone/load.sh new file mode 100755 index 0000000..f0be6c8 --- /dev/null +++ b/examples/first_example_standalone/load.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-control-ip 0.0.0.0 + +sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/run_app.sh b/examples/first_example_standalone/run_app.sh new file mode 100755 index 0000000..1a91cd4 --- /dev/null +++ b/examples/first_example_standalone/run_app.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app diff --git a/examples/first_example_standalone/run_decoder.sh b/examples/first_example_standalone/run_decoder.sh new file mode 100755 index 0000000..48ccb33 --- /dev/null +++ b/examples/first_example_standalone/run_decoder.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder run --jbpf-enable diff --git a/examples/first_example_standalone/schema.options b/examples/first_example_standalone/schema.options new file mode 100644 index 0000000..618bb99 --- /dev/null +++ b/examples/first_example_standalone/schema.options @@ -0,0 +1 @@ +packet.name max_size:32 diff --git a/examples/first_example_standalone/schema.proto b/examples/first_example_standalone/schema.proto new file mode 100644 index 0000000..b3db3d3 --- /dev/null +++ b/examples/first_example_standalone/schema.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +message packet { + required int32 seq_no = 1; + required int32 value = 2; + required string name = 3; +} + +message manual_ctrl_event { required int32 value = 1; } diff --git a/examples/first_example_standalone/send_control.sh b/examples/first_example_standalone/send_control.sh new file mode 100755 index 0000000..05cff25 --- /dev/null +++ b/examples/first_example_standalone/send_control.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder control \ + --stream-id 11111111-1111-1111-1111-111111111111 \ + --inline-json "{\"value\": $1}" diff --git a/examples/first_example_standalone/unload.sh b/examples/first_example_standalone/unload.sh new file mode 100755 index 0000000..e519f7d --- /dev/null +++ b/examples/first_example_standalone/unload.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml + +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder unload -c codeletset_load_request.yaml diff --git a/init_submodules.sh b/init_submodules.sh new file mode 100755 index 0000000..e181355 --- /dev/null +++ b/init_submodules.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +rm -rf 3p/nanopb jbpf +git submodule update --init --recursive +cd jbpf +./init_and_patch_submodules.sh +cd .. \ No newline at end of file diff --git a/jbpf b/jbpf new file mode 160000 index 0000000..5709617 --- /dev/null +++ b/jbpf @@ -0,0 +1 @@ +Subproject commit 5709617993d3fd719f62f12893c6cfa50556509f diff --git a/pkg/.gitignore b/pkg/.gitignore new file mode 100644 index 0000000..899d913 --- /dev/null +++ b/pkg/.gitignore @@ -0,0 +1,2 @@ +*.pb.go +jbpf_protobuf_cli diff --git a/pkg/.golangci.yml b/pkg/.golangci.yml new file mode 100644 index 0000000..6b06203 --- /dev/null +++ b/pkg/.golangci.yml @@ -0,0 +1,21 @@ +# Refer to golangci-lint's example config file for more options and information: +# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml + +run: + timeout: 5m + modules-download-mode: readonly + +linters: + enable: + - errcheck + - goimports + - govet + - revive + - staticcheck + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 + exclude: + - "package-comments" diff --git a/pkg/Makefile b/pkg/Makefile new file mode 100644 index 0000000..c383f28 --- /dev/null +++ b/pkg/Makefile @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +BINARY_NAME := jbpf_protobuf_cli +CURRENT_DIR = $(shell pwd) +NANO_PB ?= $(shell dirname $(shell pwd))/3p/nanopb +TEST_WORKDIR ?= $(shell dirname $(shell pwd))/testdata +REGENERATE_SNAPSHOT ?= false +OUT_DIR ?= . + +.PHONY : mod clean lint test testclean + +${BINARY_NAME}: clean mod + CGO_ENABLED=0 go build --trimpath -o ${OUT_DIR}/${BINARY_NAME} main.go + +mod: + go mod tidy + +clean: + rm -f ${OUT_DIR}/${BINARY_NAME} + +lint: + golangci-lint run + +test: + TEST_WORKDIR=${TEST_WORKDIR} \ + NANO_PB=${NANO_PB} \ + SNAPSHOT_DIR=${CURRENT_DIR}/__snapshots__ \ + REGENERATE_SNAPSHOT=${REGENERATE_SNAPSHOT} \ + go test -v ./... + +testclean: + rm -r ${CURRENT_DIR}/__snapshots__/*; \ + go clean -testcache diff --git a/pkg/README.md b/pkg/README.md new file mode 100644 index 0000000..a54fc9c --- /dev/null +++ b/pkg/README.md @@ -0,0 +1,20 @@ +# Introduction + +`jbpf_protobuf_cli` is a cli tool to build serialization assets to use protobuf as a content encoding mechanism to send data to and from jbpf. + +To build locally, run the following command: +```sh +make +``` + +To lint and test locally, run the following command: +```sh +make lint test -j +``` + +# Usage + +For detailed usage, build then run: +```sh +./jbpf_protobuf_cli --help +``` diff --git a/pkg/__snapshots__/example1/example.pb b/pkg/__snapshots__/example1/example.pb new file mode 100644 index 0000000000000000000000000000000000000000..16170bb92d9681b058ef040f417c1f84fa4e9724 GIT binary patch literal 421 zcmZWl!AiqG6l}7o*}RYvR*)PNOF$?hIrI-&3VLcGJ=9xRbWy=g(%lW{@A##@-E2<1 z4Kwe}%zN-hAQL}a_TQrFRcE?#1Bv`?v{A#GVb?I{O*_aUSca>tj`)6%>>g6ycIHzk zCxy!ZCGZpulzWJkI1fS_wt(>K-ESje45F4xQH1E1)DENHTpMmgLC|TjPAZp=L_PB5 z6Hp~|-?ds?80J8v{_uVlmalgD+}I6v3j{Id2di{VE8ps6R$Cf_BRhr5R!&eJLr{0Z zHiOh!b;`MwC%Cdj2Yu1vW>S2SY3>0i_rJvJLQH}2I{V!^=12GD$2*|Y&5T0yd^qg# M8H<5zH@iIe2j$Lex&QzG literal 0 HcmV?d00001 diff --git a/pkg/__snapshots__/example1/example.pb.c b/pkg/__snapshots__/example1/example.pb.c new file mode 100644 index 0000000..c20248f --- /dev/null +++ b/pkg/__snapshots__/example1/example.pb.c @@ -0,0 +1,26 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-1.0.0-dev */ + +#include "example.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(my_struct, my_struct, AUTO) + + +PB_BIND(request, request, AUTO) + + +PB_BIND(response, response, AUTO) + + +PB_BIND(req_resp, req_resp, AUTO) + + +PB_BIND(status, status, AUTO) + + + + + diff --git a/pkg/__snapshots__/example1/example.pb.h b/pkg/__snapshots__/example1/example.pb.h new file mode 100644 index 0000000..96d4a5c --- /dev/null +++ b/pkg/__snapshots__/example1/example.pb.h @@ -0,0 +1,155 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-1.0.0-dev */ + +#ifndef PB_EXAMPLE_PB_H_INCLUDED +#define PB_EXAMPLE_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Enum definitions */ +typedef enum _my_state { + my_state_GOOD = 0, + my_state_BAD = 1 +} my_state; + +/* Struct definitions */ +typedef struct _my_struct { + uint32_t a_num; + bool has_another_num; + uint32_t another_num; +} my_struct; + +typedef struct _request { + uint32_t id; + char name[32]; + bool has_state; + my_state state; +} request; + +typedef struct _response { + uint32_t id; + char msg[100]; +} response; + +typedef struct _req_resp { + pb_size_t which_req_or_resp; + union _req_resp_req_or_resp { + request req; + response resp; + } req_or_resp; +} req_resp; + +typedef struct _status { + uint32_t id; + char status[100]; + my_struct a_struct; +} status; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Helper constants for enums */ +#define _my_state_MIN my_state_GOOD +#define _my_state_MAX my_state_BAD +#define _my_state_ARRAYSIZE ((my_state)(my_state_BAD+1)) + + +#define request_state_ENUMTYPE my_state + + + + + +/* Initializer values for message structs */ +#define my_struct_init_default {0, false, 0} +#define request_init_default {0, "", false, _my_state_MIN} +#define response_init_default {0, ""} +#define req_resp_init_default {0, {request_init_default}} +#define status_init_default {0, "", my_struct_init_default} +#define my_struct_init_zero {0, false, 0} +#define request_init_zero {0, "", false, _my_state_MIN} +#define response_init_zero {0, ""} +#define req_resp_init_zero {0, {request_init_zero}} +#define status_init_zero {0, "", my_struct_init_zero} + +/* Field tags (for use in manual encoding/decoding) */ +#define my_struct_a_num_tag 1 +#define my_struct_another_num_tag 2 +#define request_id_tag 1 +#define request_name_tag 2 +#define request_state_tag 3 +#define response_id_tag 1 +#define response_msg_tag 2 +#define req_resp_req_tag 1 +#define req_resp_resp_tag 2 +#define status_id_tag 1 +#define status_status_tag 2 +#define status_a_struct_tag 3 + +/* Struct field encoding specification for nanopb */ +#define my_struct_FIELDLIST(X, a) \ +X(a, STATIC, REQUIRED, UINT32, a_num, 1) \ +X(a, STATIC, OPTIONAL, UINT32, another_num, 2) +#define my_struct_CALLBACK NULL +#define my_struct_DEFAULT NULL + +#define request_FIELDLIST(X, a) \ +X(a, STATIC, REQUIRED, UINT32, id, 1) \ +X(a, STATIC, REQUIRED, STRING, name, 2) \ +X(a, STATIC, OPTIONAL, UENUM, state, 3) +#define request_CALLBACK NULL +#define request_DEFAULT NULL + +#define response_FIELDLIST(X, a) \ +X(a, STATIC, REQUIRED, UINT32, id, 1) \ +X(a, STATIC, REQUIRED, STRING, msg, 2) +#define response_CALLBACK NULL +#define response_DEFAULT NULL + +#define req_resp_FIELDLIST(X, a) \ +X(a, STATIC, ONEOF, MESSAGE, (req_or_resp,req,req_or_resp.req), 1) \ +X(a, STATIC, ONEOF, MESSAGE, (req_or_resp,resp,req_or_resp.resp), 2) +#define req_resp_CALLBACK NULL +#define req_resp_DEFAULT NULL +#define req_resp_req_or_resp_req_MSGTYPE request +#define req_resp_req_or_resp_resp_MSGTYPE response + +#define status_FIELDLIST(X, a) \ +X(a, STATIC, REQUIRED, UINT32, id, 1) \ +X(a, STATIC, REQUIRED, STRING, status, 2) \ +X(a, STATIC, REQUIRED, MESSAGE, a_struct, 3) +#define status_CALLBACK NULL +#define status_DEFAULT NULL +#define status_a_struct_MSGTYPE my_struct + +extern const pb_msgdesc_t my_struct_msg; +extern const pb_msgdesc_t request_msg; +extern const pb_msgdesc_t response_msg; +extern const pb_msgdesc_t req_resp_msg; +extern const pb_msgdesc_t status_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define my_struct_fields &my_struct_msg +#define request_fields &request_msg +#define response_fields &response_msg +#define req_resp_fields &req_resp_msg +#define status_fields &status_msg + +/* Maximum encoded size of messages (where known) */ +#define EXAMPLE_PB_H_MAX_SIZE status_size +#define my_struct_size 12 +#define req_resp_size 109 +#define request_size 41 +#define response_size 107 +#define status_size 121 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/pkg/__snapshots__/example1/example:req_resp_serializer.c b/pkg/__snapshots__/example1/example:req_resp_serializer.c new file mode 100644 index 0000000..00b1c1e --- /dev/null +++ b/pkg/__snapshots__/example1/example:req_resp_serializer.c @@ -0,0 +1,26 @@ +#define PB_FIELD_32BIT 1 +#include +#include +#include +#include "example.pb.h" + +const uint32_t proto_message_size = sizeof(req_resp); + +int jbpf_io_serialize(void* input_msg_buf, size_t input_msg_buf_size, char* serialized_data_buf, size_t serialized_data_buf_size) { + if (input_msg_buf_size != proto_message_size) + return -1; + + pb_ostream_t ostream = pb_ostream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + if (!pb_encode(&ostream, req_resp_fields, input_msg_buf)) + return -1; + + return ostream.bytes_written; +} + +int jbpf_io_deserialize(char* serialized_data_buf, size_t serialized_data_buf_size, void* output_msg_buf, size_t output_msg_buf_size) { + if (output_msg_buf_size != proto_message_size) + return 0; + + pb_istream_t istream = pb_istream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + return pb_decode(&istream, req_resp_fields, output_msg_buf); +} diff --git a/pkg/__snapshots__/example1/example:status_serializer.c b/pkg/__snapshots__/example1/example:status_serializer.c new file mode 100644 index 0000000..f9d74a1 --- /dev/null +++ b/pkg/__snapshots__/example1/example:status_serializer.c @@ -0,0 +1,26 @@ +#define PB_FIELD_32BIT 1 +#include +#include +#include +#include "example.pb.h" + +const uint32_t proto_message_size = sizeof(status); + +int jbpf_io_serialize(void* input_msg_buf, size_t input_msg_buf_size, char* serialized_data_buf, size_t serialized_data_buf_size) { + if (input_msg_buf_size != proto_message_size) + return -1; + + pb_ostream_t ostream = pb_ostream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + if (!pb_encode(&ostream, status_fields, input_msg_buf)) + return -1; + + return ostream.bytes_written; +} + +int jbpf_io_deserialize(char* serialized_data_buf, size_t serialized_data_buf_size, void* output_msg_buf, size_t output_msg_buf_size) { + if (output_msg_buf_size != proto_message_size) + return 0; + + pb_istream_t istream = pb_istream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + return pb_decode(&istream, status_fields, output_msg_buf); +} diff --git a/pkg/__snapshots__/example2/example2.pb b/pkg/__snapshots__/example2/example2.pb new file mode 100644 index 0000000..130042b --- /dev/null +++ b/pkg/__snapshots__/example2/example2.pb @@ -0,0 +1,6 @@ + +> +example2.proto", +item +name ( Rname +val ( Rval \ No newline at end of file diff --git a/pkg/__snapshots__/example2/example2.pb.c b/pkg/__snapshots__/example2/example2.pb.c new file mode 100644 index 0000000..481214a --- /dev/null +++ b/pkg/__snapshots__/example2/example2.pb.c @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-1.0.0-dev */ + +#include "example2.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(item, item, AUTO) + + + diff --git a/pkg/__snapshots__/example2/example2.pb.h b/pkg/__snapshots__/example2/example2.pb.h new file mode 100644 index 0000000..62bea39 --- /dev/null +++ b/pkg/__snapshots__/example2/example2.pb.h @@ -0,0 +1,52 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-1.0.0-dev */ + +#ifndef PB_EXAMPLE2_PB_H_INCLUDED +#define PB_EXAMPLE2_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef struct _item { + char name[30]; + bool has_val; + uint32_t val; +} item; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define item_init_default {"", false, 0} +#define item_init_zero {"", false, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define item_name_tag 1 +#define item_val_tag 2 + +/* Struct field encoding specification for nanopb */ +#define item_FIELDLIST(X, a) \ +X(a, STATIC, REQUIRED, STRING, name, 1) \ +X(a, STATIC, OPTIONAL, UINT32, val, 2) +#define item_CALLBACK NULL +#define item_DEFAULT NULL + +extern const pb_msgdesc_t item_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define item_fields &item_msg + +/* Maximum encoded size of messages (where known) */ +#define EXAMPLE2_PB_H_MAX_SIZE item_size +#define item_size 37 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/pkg/__snapshots__/example2/example2:item_serializer.c b/pkg/__snapshots__/example2/example2:item_serializer.c new file mode 100644 index 0000000..24702e3 --- /dev/null +++ b/pkg/__snapshots__/example2/example2:item_serializer.c @@ -0,0 +1,26 @@ +#define PB_FIELD_32BIT 1 +#include +#include +#include +#include "example2.pb.h" + +const uint32_t proto_message_size = sizeof(item); + +int jbpf_io_serialize(void* input_msg_buf, size_t input_msg_buf_size, char* serialized_data_buf, size_t serialized_data_buf_size) { + if (input_msg_buf_size != proto_message_size) + return -1; + + pb_ostream_t ostream = pb_ostream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + if (!pb_encode(&ostream, item_fields, input_msg_buf)) + return -1; + + return ostream.bytes_written; +} + +int jbpf_io_deserialize(char* serialized_data_buf, size_t serialized_data_buf_size, void* output_msg_buf, size_t output_msg_buf_size) { + if (output_msg_buf_size != proto_message_size) + return 0; + + pb_istream_t istream = pb_istream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + return pb_decode(&istream, item_fields, output_msg_buf); +} diff --git a/pkg/__snapshots__/example3/example3.pb b/pkg/__snapshots__/example3/example3.pb new file mode 100644 index 0000000..e5e3166 --- /dev/null +++ b/pkg/__snapshots__/example3/example3.pb @@ -0,0 +1,31 @@ + + +example3.proto"ý +obj +bval (Rbval +bytesval ( Rbytesval +dval (Rdval +f32val (Rf32val +f64val (Rf64val +i32val (Ri32val +i64val (Ri64val +sf32val (Rsf32val +sf64val (Rsf64val +si32val + (Rsi32val +si64val (Rsi64val +sval ( Rsval +ui32val ( Rui32val +ui64val (Rui64val +barr (Rbarr +darr (Rdarr +f32arr (Rf32arr +f64arr (Rf64arr +i32arr (Ri32arr +i64arr (Ri64arr +sf32arr (Rsf32arr +sf64arr (Rsf64arr +si32arr (Rsi32arr +si64arr (Rsi64arr +ui32arr ( Rui32arr +ui64arr (Rui64arr \ No newline at end of file diff --git a/pkg/__snapshots__/example3/example3.pb.c b/pkg/__snapshots__/example3/example3.pb.c new file mode 100644 index 0000000..35209fe --- /dev/null +++ b/pkg/__snapshots__/example3/example3.pb.c @@ -0,0 +1,20 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-1.0.0-dev */ + +#include "example3.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(obj, obj, 2) + + + +#ifndef PB_CONVERT_DOUBLE_FLOAT +/* On some platforms (such as AVR), double is really float. + * To be able to encode/decode double on these platforms, you need. + * to define PB_CONVERT_DOUBLE_FLOAT in pb.h or compiler command line. + */ +PB_STATIC_ASSERT(sizeof(double) == 8, DOUBLE_MUST_BE_8_BYTES) +#endif + diff --git a/pkg/__snapshots__/example3/example3.pb.h b/pkg/__snapshots__/example3/example3.pb.h new file mode 100644 index 0000000..5428ff1 --- /dev/null +++ b/pkg/__snapshots__/example3/example3.pb.h @@ -0,0 +1,136 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-1.0.0-dev */ + +#ifndef PB_EXAMPLE3_PB_H_INCLUDED +#define PB_EXAMPLE3_PB_H_INCLUDED +#include + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef PB_BYTES_ARRAY_T(20) obj_bytesval_t; +typedef struct _obj { + bool bval; + obj_bytesval_t bytesval; + double dval; + uint32_t f32val; + uint64_t f64val; + int32_t i32val; + int64_t i64val; + int32_t sf32val; + int64_t sf64val; + int32_t si32val; + int64_t si64val; + char sval[20]; + uint32_t ui32val; + uint64_t ui64val; + pb_size_t barr_count; + bool barr[10]; + pb_size_t darr_count; + double darr[10]; + pb_size_t f32arr_count; + uint32_t f32arr[10]; + pb_size_t f64arr_count; + uint64_t f64arr[10]; + pb_size_t i32arr_count; + int32_t i32arr[10]; + pb_size_t i64arr_count; + int64_t i64arr[10]; + pb_size_t sf32arr_count; + int32_t sf32arr[10]; + pb_size_t sf64arr_count; + int64_t sf64arr[10]; + pb_size_t si32arr_count; + int32_t si32arr[10]; + pb_size_t si64arr_count; + int64_t si64arr[10]; + pb_size_t ui32arr_count; + uint32_t ui32arr[10]; + pb_size_t ui64arr_count; + uint64_t ui64arr[10]; +} obj; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define obj_init_default {0, {0, {0}}, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}} +#define obj_init_zero {0, {0, {0}}, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", 0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}} + +/* Field tags (for use in manual encoding/decoding) */ +#define obj_bval_tag 1 +#define obj_bytesval_tag 2 +#define obj_dval_tag 3 +#define obj_f32val_tag 4 +#define obj_f64val_tag 5 +#define obj_i32val_tag 6 +#define obj_i64val_tag 7 +#define obj_sf32val_tag 8 +#define obj_sf64val_tag 9 +#define obj_si32val_tag 10 +#define obj_si64val_tag 11 +#define obj_sval_tag 12 +#define obj_ui32val_tag 13 +#define obj_ui64val_tag 14 +#define obj_barr_tag 15 +#define obj_darr_tag 16 +#define obj_f32arr_tag 17 +#define obj_f64arr_tag 18 +#define obj_i32arr_tag 19 +#define obj_i64arr_tag 20 +#define obj_sf32arr_tag 21 +#define obj_sf64arr_tag 22 +#define obj_si32arr_tag 23 +#define obj_si64arr_tag 24 +#define obj_ui32arr_tag 25 +#define obj_ui64arr_tag 26 + +/* Struct field encoding specification for nanopb */ +#define obj_FIELDLIST(X, a) \ +X(a, STATIC, REQUIRED, BOOL, bval, 1) \ +X(a, STATIC, REQUIRED, BYTES, bytesval, 2) \ +X(a, STATIC, REQUIRED, DOUBLE, dval, 3) \ +X(a, STATIC, REQUIRED, FIXED32, f32val, 4) \ +X(a, STATIC, REQUIRED, FIXED64, f64val, 5) \ +X(a, STATIC, REQUIRED, INT32, i32val, 6) \ +X(a, STATIC, REQUIRED, INT64, i64val, 7) \ +X(a, STATIC, REQUIRED, SFIXED32, sf32val, 8) \ +X(a, STATIC, REQUIRED, SFIXED64, sf64val, 9) \ +X(a, STATIC, REQUIRED, SINT32, si32val, 10) \ +X(a, STATIC, REQUIRED, SINT64, si64val, 11) \ +X(a, STATIC, REQUIRED, STRING, sval, 12) \ +X(a, STATIC, REQUIRED, UINT32, ui32val, 13) \ +X(a, STATIC, REQUIRED, UINT64, ui64val, 14) \ +X(a, STATIC, REPEATED, BOOL, barr, 15) \ +X(a, STATIC, REPEATED, DOUBLE, darr, 16) \ +X(a, STATIC, REPEATED, FIXED32, f32arr, 17) \ +X(a, STATIC, REPEATED, FIXED64, f64arr, 18) \ +X(a, STATIC, REPEATED, INT32, i32arr, 19) \ +X(a, STATIC, REPEATED, INT64, i64arr, 20) \ +X(a, STATIC, REPEATED, SFIXED32, sf32arr, 21) \ +X(a, STATIC, REPEATED, SFIXED64, sf64arr, 22) \ +X(a, STATIC, REPEATED, SINT32, si32arr, 23) \ +X(a, STATIC, REPEATED, SINT64, si64arr, 24) \ +X(a, STATIC, REPEATED, UINT32, ui32arr, 25) \ +X(a, STATIC, REPEATED, UINT64, ui64arr, 26) +#define obj_CALLBACK NULL +#define obj_DEFAULT NULL + +extern const pb_msgdesc_t obj_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define obj_fields &obj_msg + +/* Maximum encoded size of messages (where known) */ +#define EXAMPLE3_PB_H_MAX_SIZE obj_size +#define obj_size 1198 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/pkg/__snapshots__/example3/example3:obj_serializer.c b/pkg/__snapshots__/example3/example3:obj_serializer.c new file mode 100644 index 0000000..f66f508 --- /dev/null +++ b/pkg/__snapshots__/example3/example3:obj_serializer.c @@ -0,0 +1,26 @@ +#define PB_FIELD_32BIT 1 +#include +#include +#include +#include "example3.pb.h" + +const uint32_t proto_message_size = sizeof(obj); + +int jbpf_io_serialize(void* input_msg_buf, size_t input_msg_buf_size, char* serialized_data_buf, size_t serialized_data_buf_size) { + if (input_msg_buf_size != proto_message_size) + return -1; + + pb_ostream_t ostream = pb_ostream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + if (!pb_encode(&ostream, obj_fields, input_msg_buf)) + return -1; + + return ostream.bytes_written; +} + +int jbpf_io_deserialize(char* serialized_data_buf, size_t serialized_data_buf_size, void* output_msg_buf, size_t output_msg_buf_size) { + if (output_msg_buf_size != proto_message_size) + return 0; + + pb_istream_t istream = pb_istream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + return pb_decode(&istream, obj_fields, output_msg_buf); +} diff --git a/pkg/cmd/decoder/control/control.go b/pkg/cmd/decoder/control/control.go new file mode 100644 index 0000000..dd813e3 --- /dev/null +++ b/pkg/cmd/decoder/control/control.go @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package control + +import ( + "encoding/json" + "errors" + "fmt" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/schema" + "os" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type runOptions struct { + schema *schema.ClientOptions + general *common.GeneralOptions + + filePath string + inlineJSON string + payload string + streamID string + streamUUID uuid.UUID +} + +func addToFlags(flags *pflag.FlagSet, opts *runOptions) { + flags.StringVarP(&opts.filePath, "file", "f", "", "path to file containing payload in JSON format") + flags.StringVarP(&opts.inlineJSON, "inline-json", "j", "", "inline payload in JSON format") + flags.StringVar(&opts.streamID, "stream-id", "00000000-0000-0000-0000-000000000000", "stream ID") +} + +func (o *runOptions) parse() error { + if (len(o.inlineJSON) > 0 && len(o.filePath) > 0) || (len(o.inlineJSON) == 0 && len(o.filePath) == 0) { + return errors.New("exactly one of --file or --inline-json can be specified") + } + + if len(o.filePath) != 0 { + if fi, err := os.Stat(o.filePath); err != nil { + return err + } else if fi.IsDir() { + return fmt.Errorf(`expected "%s" to be a file, got a directory`, o.filePath) + } + payload, err := os.ReadFile(o.filePath) + if err != nil { + return err + } + var deserializedPayload interface{} + err = json.Unmarshal(payload, &deserializedPayload) + if err != nil { + return err + } + o.payload = string(payload) + } else { + var deserializedPayload interface{} + err := json.Unmarshal([]byte(o.inlineJSON), &deserializedPayload) + if err != nil { + return err + } + o.payload = o.inlineJSON + } + + var err error + o.streamUUID, err = uuid.Parse(o.streamID) + if err != nil { + return err + } + + return nil +} + +// Command Load a schema to a local decoder +func Command(opts *common.GeneralOptions) *cobra.Command { + runOptions := &runOptions{ + schema: &schema.ClientOptions{}, + general: opts, + } + cmd := &cobra.Command{ + Use: "control", + Short: "Load a control message via a local decoder", + Long: "Load a control message via a local decoder", + RunE: func(cmd *cobra.Command, _ []string) error { + return run(cmd, runOptions) + }, + SilenceUsage: true, + } + addToFlags(cmd.PersistentFlags(), runOptions) + schema.AddClientOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + return cmd +} + +func run(cmd *cobra.Command, opts *runOptions) error { + if err := errors.Join( + opts.general.Parse(), + opts.schema.Parse(), + opts.parse(), + ); err != nil { + return err + } + + logger := opts.general.Logger + + client, err := schema.NewClient(cmd.Context(), logger, opts.schema) + if err != nil { + return err + } + + return client.SendControl(opts.streamUUID, string(opts.payload)) +} diff --git a/pkg/cmd/decoder/decoder.go b/pkg/cmd/decoder/decoder.go new file mode 100644 index 0000000..6a2a1e7 --- /dev/null +++ b/pkg/cmd/decoder/decoder.go @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package decoder + +import ( + "jbpf_protobuf_cli/cmd/decoder/control" + "jbpf_protobuf_cli/cmd/decoder/load" + "jbpf_protobuf_cli/cmd/decoder/run" + "jbpf_protobuf_cli/cmd/decoder/unload" + "jbpf_protobuf_cli/common" + + "github.com/spf13/cobra" +) + +// Command returns the decoder commands +func Command(opts *common.GeneralOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "decoder", + Long: "Execute a decoder subcommand.", + Short: "Execute a decoder subcommand", + } + cmd.AddCommand( + control.Command(opts), + load.Command(opts), + unload.Command(opts), + run.Command(opts), + ) + return cmd +} diff --git a/pkg/cmd/decoder/load/load.go b/pkg/cmd/decoder/load/load.go new file mode 100644 index 0000000..d284d27 --- /dev/null +++ b/pkg/cmd/decoder/load/load.go @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package load + +import ( + "errors" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/schema" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type runOptions struct { + schema *schema.ClientOptions + general *common.GeneralOptions + + compiledProtos map[string]*common.File + configFiles []string + configs []DecoderLoadConfig +} + +func addToFlags(flags *pflag.FlagSet, opts *runOptions) { + flags.StringArrayVarP(&opts.configFiles, "config", "c", []string{}, "configuration files to load") +} + +func (o *runOptions) parse() error { + configs, compiledProtos, err := fromFiles(o.configFiles...) + if err != nil { + return err + } + o.configs = configs + o.compiledProtos = compiledProtos + + return nil +} + +// Command Load a schema to a local decoder +func Command(opts *common.GeneralOptions) *cobra.Command { + runOptions := &runOptions{ + schema: &schema.ClientOptions{}, + general: opts, + } + cmd := &cobra.Command{ + Use: "load", + Short: "Load a schema to a local decoder", + Long: "Load a schema to a local decoder", + RunE: func(cmd *cobra.Command, _ []string) error { + return run(cmd, runOptions) + }, + SilenceUsage: true, + } + addToFlags(cmd.PersistentFlags(), runOptions) + schema.AddClientOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + return cmd +} + +func run(cmd *cobra.Command, opts *runOptions) error { + if err := errors.Join( + opts.general.Parse(), + opts.schema.Parse(), + opts.parse(), + ); err != nil { + return err + } + + logger := opts.general.Logger + + client, err := schema.NewClient(cmd.Context(), logger, opts.schema) + if err != nil { + return err + } + + schemas := make(map[string]*schema.LoadRequest) + + for _, config := range opts.configs { + for _, desc := range config.CodeletDescriptor { + for _, io := range desc.InIOChannel { + if existing, ok := schemas[io.Serde.Protobuf.protoPackageName]; ok { + existing.Streams[io.streamUUID] = io.Serde.Protobuf.MsgName + } else { + compiledProto := opts.compiledProtos[io.Serde.Protobuf.absPackagePath] + schemas[io.Serde.Protobuf.protoPackageName] = &schema.LoadRequest{ + CompiledProto: compiledProto.Data, + Streams: map[uuid.UUID]string{ + io.streamUUID: io.Serde.Protobuf.MsgName, + }, + } + } + } + for _, io := range desc.OutIOChannel { + if existing, ok := schemas[io.Serde.Protobuf.protoPackageName]; ok { + existing.Streams[io.streamUUID] = io.Serde.Protobuf.MsgName + } else { + compiledProto := opts.compiledProtos[io.Serde.Protobuf.absPackagePath] + schemas[io.Serde.Protobuf.protoPackageName] = &schema.LoadRequest{ + CompiledProto: compiledProto.Data, + Streams: map[uuid.UUID]string{ + io.streamUUID: io.Serde.Protobuf.MsgName, + }, + } + } + } + } + } + + return client.Load(schemas) +} diff --git a/pkg/cmd/decoder/load/load_config.go b/pkg/cmd/decoder/load/load_config.go new file mode 100644 index 0000000..a2fcdec --- /dev/null +++ b/pkg/cmd/decoder/load/load_config.go @@ -0,0 +1,113 @@ +package load + +import ( + "errors" + "fmt" + "jbpf_protobuf_cli/common" + "os" + "path/filepath" + "strings" + + "github.com/google/uuid" + "gopkg.in/yaml.v3" +) + +// ProtobufConfig represents the configuration for a protobuf message +type ProtobufConfig struct { + MsgName string `yaml:"msg_name"` + PackagePath string `yaml:"package_path"` + + absPackagePath string + protoPackageName string +} + +// SerdeConfig represents the configuration for serialize/deserialize +type SerdeConfig struct { + Protobuf *ProtobufConfig `yaml:"protobuf"` +} + +// IOChannelConfig represents the configuration for an IO channel +type IOChannelConfig struct { + Serde *SerdeConfig `yaml:"serde"` + StreamID string `yaml:"stream_id"` + + streamUUID uuid.UUID +} + +// CodeletDescriptorConfig represents the configuration for a codelet descriptor +type CodeletDescriptorConfig struct { + InIOChannel []*IOChannelConfig `yaml:"in_io_channel"` + OutIOChannel []*IOChannelConfig `yaml:"out_io_channel"` +} + +// DecoderLoadConfig represents the configuration for loading a decoder +type DecoderLoadConfig struct { + CodeletDescriptor []*CodeletDescriptorConfig `yaml:"codelet_descriptor"` +} + +func (io *IOChannelConfig) verify(compiledProtos map[string]*common.File) error { + streamUUID, err := uuid.Parse(io.StreamID) + if err != nil { + return err + } + io.streamUUID = streamUUID + if io.Serde == nil || io.Serde.Protobuf == nil || io.Serde.Protobuf.PackagePath == "" { + return fmt.Errorf("missing required field package_path") + } + + io.Serde.Protobuf.absPackagePath = os.ExpandEnv(io.Serde.Protobuf.PackagePath) + basename := filepath.Base(io.Serde.Protobuf.absPackagePath) + io.Serde.Protobuf.protoPackageName = strings.TrimSuffix(basename, filepath.Ext(basename)) + + if _, ok := compiledProtos[io.Serde.Protobuf.absPackagePath]; !ok { + protoPkg, err := common.NewFile(io.Serde.Protobuf.absPackagePath) + if err != nil { + return err + } + compiledProtos[io.Serde.Protobuf.absPackagePath] = protoPkg + } + + return nil +} + +func fromFiles(configs ...string) ([]DecoderLoadConfig, map[string]*common.File, error) { + out := make([]DecoderLoadConfig, 0, len(configs)) + compiledProtos := make(map[string]*common.File) + errs := make([]error, 0, len(configs)) + +configLoad: + for _, c := range configs { + f, err := common.NewFile(c) + if err != nil { + errs = append(errs, fmt.Errorf("failed to read file %s: %w", c, err)) + continue + } + var config DecoderLoadConfig + if err := yaml.Unmarshal(f.Data, &config); err != nil { + errs = append(errs, fmt.Errorf("failed to unmarshal file %s: %w", c, err)) + continue + } + + for _, desc := range config.CodeletDescriptor { + for _, io := range desc.InIOChannel { + if err := io.verify(compiledProtos); err != nil { + errs = append(errs, fmt.Errorf("failed to verify in_io_channel in file %s: %w", c, err)) + continue configLoad + } + } + for _, io := range desc.OutIOChannel { + if err := io.verify(compiledProtos); err != nil { + errs = append(errs, fmt.Errorf("failed to verify out_io_channel in file %s: %w", c, err)) + continue configLoad + } + } + } + + out = append(out, config) + } + if err := errors.Join(errs...); err != nil { + return nil, nil, err + } + + return out, compiledProtos, nil +} diff --git a/pkg/cmd/decoder/run/run.go b/pkg/cmd/decoder/run/run.go new file mode 100644 index 0000000..df508e5 --- /dev/null +++ b/pkg/cmd/decoder/run/run.go @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package run + +import ( + "errors" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/data" + "jbpf_protobuf_cli/schema" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" +) + +type runOptions struct { + general *common.GeneralOptions + data *data.ServerOptions + schema *schema.ServerOptions +} + +// Command Run decoder to collect, decode and print jbpf output +func Command(opts *common.GeneralOptions) *cobra.Command { + runOptions := &runOptions{ + general: opts, + data: &data.ServerOptions{}, + schema: &schema.ServerOptions{}, + } + cmd := &cobra.Command{ + Use: "run", + Short: "Run decoder to collect, decode and print jbpf output", + Long: "Run dynamic protobuf decoder to collect, decode and print jbpf output.", + RunE: func(cmd *cobra.Command, _ []string) error { + return run(cmd, runOptions) + }, + SilenceUsage: true, + } + schema.AddServerOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + data.AddServerOptionsToFlags(cmd.PersistentFlags(), runOptions.data) + return cmd +} + +func run(cmd *cobra.Command, opts *runOptions) error { + if err := errors.Join( + opts.general.Parse(), + opts.data.Parse(), + opts.schema.Parse(), + ); err != nil { + return err + } + + logger := opts.general.Logger + + store := schema.NewStore() + + schemaServer, err := schema.NewServer(cmd.Context(), logger, opts.schema, store) + if err != nil { + return err + } + + dataServer, err := data.NewServer(cmd.Context(), logger, opts.data, store) + if err != nil { + return err + } + + g, _ := errgroup.WithContext(cmd.Context()) + + g.Go(func() error { + return dataServer.Listen(func(streamUUID uuid.UUID, data []byte) { + logger.WithFields(logrus.Fields{ + "streamUUID": streamUUID.String(), + }).Info(string(data)) + }) + }) + + g.Go(func() error { + return schemaServer.Serve() + }) + + return g.Wait() +} diff --git a/pkg/cmd/decoder/unload/unload.go b/pkg/cmd/decoder/unload/unload.go new file mode 100644 index 0000000..dfa4487 --- /dev/null +++ b/pkg/cmd/decoder/unload/unload.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package unload + +import ( + "errors" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/schema" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + maxStreamUUIDs = 1024 +) + +type runOptions struct { + schema *schema.ClientOptions + general *common.GeneralOptions + + configFiles []string + configs []DecoderUnloadConfig +} + +func addToFlags(flags *pflag.FlagSet, opts *runOptions) { + flags.StringArrayVarP(&opts.configFiles, "config", "c", []string{}, "configuration files to unload") +} + +func (o *runOptions) parse() error { + configs, err := fromFiles(o.configFiles...) + if err != nil { + return err + } + o.configs = configs + + return nil +} + +// Command Unload a schema from a local decoder +func Command(opts *common.GeneralOptions) *cobra.Command { + runOptions := &runOptions{ + schema: &schema.ClientOptions{}, + general: opts, + } + cmd := &cobra.Command{ + Use: "unload", + Short: "Unload a schema from a local decoder", + Long: "Unload a schema from a local decoder", + RunE: func(cmd *cobra.Command, _ []string) error { + return run(cmd, runOptions) + }, + SilenceUsage: true, + } + addToFlags(cmd.PersistentFlags(), runOptions) + schema.AddClientOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + return cmd +} + +func run(cmd *cobra.Command, opts *runOptions) error { + if err := errors.Join( + opts.general.Parse(), + opts.schema.Parse(), + opts.parse(), + ); err != nil { + return err + } + + logger := opts.general.Logger + + client, err := schema.NewClient(cmd.Context(), logger, opts.schema) + if err != nil { + return err + } + + streamUUIDs := make([]uuid.UUID, 0, maxStreamUUIDs) + + for _, config := range opts.configs { + for _, desc := range config.CodeletDescriptor { + for _, io := range desc.InIOChannel { + streamUUIDs = append(streamUUIDs, io.streamUUID) + } + for _, io := range desc.OutIOChannel { + streamUUIDs = append(streamUUIDs, io.streamUUID) + } + } + } + + return client.Unload(streamUUIDs) +} diff --git a/pkg/cmd/decoder/unload/unload_config.go b/pkg/cmd/decoder/unload/unload_config.go new file mode 100644 index 0000000..886e50b --- /dev/null +++ b/pkg/cmd/decoder/unload/unload_config.go @@ -0,0 +1,78 @@ +package unload + +import ( + "errors" + "fmt" + "jbpf_protobuf_cli/common" + + "github.com/google/uuid" + "gopkg.in/yaml.v3" +) + +// IOChannelConfig represents the configuration for an IO channel +type IOChannelConfig struct { + StreamID string `yaml:"stream_id"` + + streamUUID uuid.UUID +} + +// CodeletDescriptorConfig represents the configuration for a codelet descriptor +type CodeletDescriptorConfig struct { + InIOChannel []*IOChannelConfig `yaml:"in_io_channel"` + OutIOChannel []*IOChannelConfig `yaml:"out_io_channel"` +} + +// DecoderUnloadConfig represents the configuration for unloading a decoder +type DecoderUnloadConfig struct { + CodeletDescriptor []*CodeletDescriptorConfig `yaml:"codelet_descriptor"` +} + +func (io *IOChannelConfig) verify() error { + streamUUID, err := uuid.Parse(io.StreamID) + if err != nil { + return err + } + io.streamUUID = streamUUID + return nil +} + +func fromFiles(configs ...string) ([]DecoderUnloadConfig, error) { + out := make([]DecoderUnloadConfig, 0, len(configs)) + errs := make([]error, 0, len(configs)) + +configLoad: + for _, c := range configs { + f, err := common.NewFile(c) + if err != nil { + errs = append(errs, fmt.Errorf("failed to read file %s: %w", c, err)) + continue + } + var config DecoderUnloadConfig + if err := yaml.Unmarshal(f.Data, &config); err != nil { + errs = append(errs, fmt.Errorf("failed to unmarshal file %s: %w", c, err)) + continue + } + + for _, desc := range config.CodeletDescriptor { + for _, io := range desc.InIOChannel { + if err := io.verify(); err != nil { + errs = append(errs, fmt.Errorf("failed to verify in_io_channel in file %s: %w", c, err)) + continue configLoad + } + } + for _, io := range desc.OutIOChannel { + if err := io.verify(); err != nil { + errs = append(errs, fmt.Errorf("failed to verify out_io_channel in file %s: %w", c, err)) + continue configLoad + } + } + } + + out = append(out, config) + } + if err := errors.Join(errs...); err != nil { + return nil, err + } + + return out, nil +} diff --git a/pkg/cmd/serde/serde.go b/pkg/cmd/serde/serde.go new file mode 100644 index 0000000..789be30 --- /dev/null +++ b/pkg/cmd/serde/serde.go @@ -0,0 +1,157 @@ +package serde + +import ( + "errors" + "fmt" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/generator/nanopb" + "jbpf_protobuf_cli/generator/schema" + "log" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + relativeWorkingDir = "./" +) + +var ( + originalBaseDir string +) + +type parsedProtoConfig struct { + protoPackageName string + protoMessageNames []string +} + +type runOptions struct { + general *common.GeneralOptions + + absOutputDir string + absWorkingDir string + outputDir string + protoConfigs []string + schemas []*parsedProtoConfig + workingDir string +} + +func init() { + var err error + originalBaseDir, err = filepath.Abs(originalBaseDir) + if err != nil { + log.Fatal(err) + } +} + +func addToFlags(flags *pflag.FlagSet, opts *runOptions) { + flags.StringArrayVarP(&opts.protoConfigs, "schema", "s", []string{}, `source proto file(s), along with any message names. In the form "{proto package name}:{proto message names,}"`) + flags.StringVarP(&opts.outputDir, "output-dir", "o", relativeWorkingDir, "output directory, will default to the current directory") + flags.StringVarP(&opts.workingDir, "workdir", "w", relativeWorkingDir, "working directory, will default to the current directory") +} + +func validateDir(absPath string) error { + fi, err := os.Stat(absPath) + if err != nil { + return err + } else if !fi.IsDir() { + return fmt.Errorf(`Expected "%s" to be a directory`, absPath) + } + return nil +} + +func (o *runOptions) parse() error { + var err1, err2 error + o.absOutputDir, err1 = filepath.Abs(o.outputDir) + o.absWorkingDir, err2 = filepath.Abs(o.workingDir) + + if err := errors.Join(err1, err2); err != nil { + return err + } + + if err := errors.Join(validateDir(o.absOutputDir), validateDir(o.absWorkingDir)); err != nil { + return err + } + + o.schemas = make([]*parsedProtoConfig, len(o.protoConfigs)) + for i, s := range o.protoConfigs { + parts := strings.Split(s, ":") + if len(parts) != 2 { + return errors.New("invalid schema format") + } + protoPackageName := strings.TrimSpace(parts[0]) + if len(protoPackageName) == 0 { + return errors.New("invalid schema format") + } + protoMessageNames := make([]string, 0) + if len(parts[1]) > 0 { + protoMessageNames = strings.Split(parts[1], ",") + for i := range protoMessageNames { + protoMessageNames[i] = strings.TrimSpace(protoMessageNames[i]) + if len(protoMessageNames[i]) == 0 { + return errors.New("invalid schema format") + } + } + } + + o.schemas[i] = &parsedProtoConfig{ + protoPackageName: protoPackageName, + protoMessageNames: protoMessageNames, + } + } + + return nil +} + +// Command Generate serde assets for protobuf spec +func Command(opts *common.GeneralOptions) *cobra.Command { + runOptions := &runOptions{ + general: opts, + } + cmd := &cobra.Command{ + Use: "serde", + Short: "Generate serde assets for protobuf spec", + RunE: func(cmd *cobra.Command, _ []string) error { + return run(cmd, runOptions) + }, + SilenceUsage: true, + } + addToFlags(cmd.PersistentFlags(), runOptions) + return cmd +} + +func run(cmd *cobra.Command, opts *runOptions) error { + if err := errors.Join( + opts.general.Parse(), + opts.parse(), + ); err != nil { + return err + } + + logger := opts.general.Logger + + for _, cfg := range opts.schemas { + fileCfgs, err := nanopb.FindFiles(logger, opts.absWorkingDir) + if err != nil { + return err + } + + files, err := schema.Generate(cmd.Context(), logger, &schema.Config{ + Files: fileCfgs, + ProtoPackageName: cfg.protoPackageName, + ProtoMessageNames: cfg.protoMessageNames, + }) + if err != nil { + return err + } + + if err := common.WriteFilesToDirectory(logger, opts.absOutputDir, files); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/cmd/serde/serde_test.go b/pkg/cmd/serde/serde_test.go new file mode 100644 index 0000000..b16d4d4 --- /dev/null +++ b/pkg/cmd/serde/serde_test.go @@ -0,0 +1,161 @@ +package serde + +import ( + "errors" + "fmt" + "io/fs" + "jbpf_protobuf_cli/common" + "log" + "os" + "path/filepath" + "testing" + + _ "embed" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var workdir = os.Getenv("TEST_WORKDIR") +var snapshotdir = os.Getenv("SNAPSHOT_DIR") +var generateSnapshot = os.Getenv("REGENERATE_SNAPSHOT") == "true" +var generalOpts *common.GeneralOptions + +func init() { + logger := logrus.New() + logger.SetLevel(logrus.InfoLevel) + generalOpts = common.NewGeneralOptionsFromLogger(logger) + + if workdir == "" { + log.Fatal(`"TEST_WORKDIR" not set`) + } + if snapshotdir == "" { + log.Fatal(`"SNAPSHOT_DIR" not set`) + } + err := errors.Join( + verifyDirExists(workdir, false), + verifyDirExists(snapshotdir, true), + ) + if err != nil { + log.Fatal(err) + } + +} + +func verifyDirExists(dir string, createIfNotExists bool) error { + f, err := os.Stat(dir) + if err != nil && os.IsNotExist(err) && createIfNotExists { + if err := os.Mkdir(dir, 0755); err != nil { + return err + } + } else if err != nil { + return err + } else if !f.IsDir() { + return fmt.Errorf("%s is not a directory", dir) + } + return nil +} + +func snapshotTest(t *testing.T, snapshotDir, outDir string, cmd *cobra.Command) { + err := cmd.Execute() + require.NoError(t, err) + + cFiles, err := filepath.Glob(outDir + "/*.c") + require.NoError(t, err) + hFiles, err := filepath.Glob(outDir + "/*.h") + require.NoError(t, err) + pbFiles, err := filepath.Glob(outDir + "/*.pb") + require.NoError(t, err) + + outDirFiles := append(cFiles, hFiles...) + outDirFiles = append(outDirFiles, pbFiles...) + + for _, file := range outDirFiles { + baseName := filepath.Base(file) + snapshotFile := filepath.Join(snapshotDir, baseName) + + if generateSnapshot { + require.NoError(t, moveFile(file, snapshotFile)) + } else { + newFile, err := os.ReadFile(file) + require.NoError(t, err) + snapshotFile, err := os.ReadFile(snapshotFile) + require.NoError(t, err) + assert.Equal(t, snapshotFile, newFile, "file %s does not match snapshot", baseName) + } + } +} + +func moveFile(source, destination string) error { + fi, err := os.Stat(source) + if err != nil { + return err + } else if fi.IsDir() { + return fmt.Errorf("expected %s to be a file, got dir", source) + } + fileMod := fi.Mode() + data, err := os.ReadFile(source) + if err != nil { + return err + } + if err := os.Remove(source); err != nil { + return err + } + + newFi, err := os.Stat(destination) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } else if err == nil && newFi.IsDir() { + return fmt.Errorf("expected %s to be a file, got dir", destination) + } else if err == nil { + if err := os.Remove(destination); err != nil { + return err + } + } + destFile, err := os.Create(destination) + if err != nil { + return err + } + defer func() { + if err := destFile.Close(); err != nil { + fmt.Printf("failed to close destination file %s when moving\n", destination) + } + }() + + n, err := destFile.Write(data) + if n != len(data) { + return fmt.Errorf("failed to write entire file %s, wrote %d of %d bytes, with err %v", destination, n, len(data), err) + } else if err != nil { + return err + } + + return os.Chmod(destination, fileMod) +} + +func TestCases(t *testing.T) { + testArgs := map[string][]string{ + "example1": {"-s", "example:req_resp,status", "-w", filepath.Join(workdir, "example1")}, + "example2": {"-s", "example2:item", "-w", filepath.Join(workdir, "example2")}, + "example3": {"-s", "example3:obj", "-w", filepath.Join(workdir, "example3")}, + } + + for exampleName, testArgs := range testArgs { + t.Run(exampleName, func(t *testing.T) { + outDir, err := os.MkdirTemp("", exampleName) + require.NoError(t, err) + defer func() { + if err := os.RemoveAll(outDir); err != nil { + t.Logf("failed to remove outDir: %s", err) + } + }() + snapshotDir := filepath.Join(snapshotdir, exampleName) + err = verifyDirExists(snapshotDir, true) + require.NoError(t, err) + cmd := Command(generalOpts) + cmd.SetArgs(append(testArgs, "-o", outDir)) + snapshotTest(t, snapshotDir, outDir, cmd) + }) + } +} diff --git a/pkg/common/file.go b/pkg/common/file.go new file mode 100644 index 0000000..05e9374 --- /dev/null +++ b/pkg/common/file.go @@ -0,0 +1,95 @@ +package common + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/sirupsen/logrus" +) + +// File represents a generated file +type File struct { + Data []byte + Mode fs.FileMode + Name string +} + +// NewFile creates a new file from a file path +func NewFile(filePath string) (*File, error) { + filePath, err := filepath.Abs(filePath) + if err != nil { + return nil, err + } + + fi, err := os.Stat(filePath) + if err != nil { + return nil, err + } else if fi.IsDir() { + return nil, fmt.Errorf(`expected "%s" to be a file, got a directory`, filePath) + } + + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + return &File{ + Data: content, + Mode: fi.Mode(), + Name: filepath.Base(filePath), + }, nil +} + +// WriteFilesToDirectory writes files to a directory +func WriteFilesToDirectory(logger *logrus.Logger, outputDirectory string, files []*File) error { + for _, f := range files { + if err := WriteFileToDirectory(logger, outputDirectory, f); err != nil { + return err + } + } + return nil +} + +// WriteFileToDirectory writes a file to a directory +func WriteFileToDirectory(logger *logrus.Logger, outputDirectory string, file *File) error { + filePath := filepath.Join(outputDirectory, file.Name) + + l := logger.WithField("filename", filePath) + fi, err := os.Stat(filePath) + var f *os.File + if err == nil && fi.IsDir() { + return fmt.Errorf(`"%s" is a directory`, filePath) + } else if !os.IsNotExist(err) { + l.Debug("Overwriting existing file") + if err := os.Remove(filePath); err != nil { + return err + } + } else if err == nil { + l.Debug("Creating file") + } + f, err = os.Create(filePath) + if err != nil { + return err + } + + defer func() { + if err := f.Close(); err != nil { + l.WithError(err).Error("failed to close file") + } + }() + + n, err := f.Write(file.Data) + if err != nil { + return err + } else if n != len(file.Data) { + return fmt.Errorf("expected to write %d bytes, wrote %d", len(file.Data), n) + } + + if err := os.Chmod(filePath, file.Mode); err != nil { + return err + } + + return nil +} diff --git a/pkg/common/options.go b/pkg/common/options.go new file mode 100644 index 0000000..e4b3c3d --- /dev/null +++ b/pkg/common/options.go @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package common + +import ( + "errors" + + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" +) + +// NewGeneralOptions creates a new GeneralOptions with default values +func NewGeneralOptions(flags *pflag.FlagSet) *GeneralOptions { + opts := &GeneralOptions{} + opts.addToFlags(flags) + return opts +} + +// NewGeneralOptionsFromLogger creates a new GeneralOptions from a logger +func NewGeneralOptionsFromLogger(logger *logrus.Logger) *GeneralOptions { + opts := &GeneralOptions{ + Logger: logger, + logLevel: logger.Level.String(), + reportCaller: logger.ReportCaller, + } + return opts +} + +// GeneralOptions contains the general options for the jbpf cli +type GeneralOptions struct { + logLevel string + reportCaller bool + + Logger *logrus.Logger +} + +func (opts *GeneralOptions) addToFlags(flags *pflag.FlagSet) { + flags.BoolVar(&opts.reportCaller, "log-report-caller", false, "show report caller in logs") + flags.StringVar(&opts.logLevel, "log-level", "info", "log level, set to: panic, fatal, error, warn, info, debug or trace") +} + +// Parse will process and validate args +func (opts *GeneralOptions) Parse() error { + var err1, err2 error + opts.Logger, err1 = opts.getLogger() + return errors.Join(err1, err2) +} + +// GetLogger returns a logger based on the options +func (opts *GeneralOptions) getLogger() (*logrus.Logger, error) { + logger := logrus.New() + logLev, err := logrus.ParseLevel(opts.logLevel) + if err != nil { + return logger, err + } + + logger.SetReportCaller(opts.reportCaller) + logger.SetLevel(logLev) + return logger, nil +} diff --git a/pkg/common/subproc.go b/pkg/common/subproc.go new file mode 100644 index 0000000..4252f95 --- /dev/null +++ b/pkg/common/subproc.go @@ -0,0 +1,30 @@ +package common + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" +) + +// RunSubprocess runs a subprocess +func RunSubprocess(ctx context.Context, logger *logrus.Logger, name string, args ...string) error { + l := logger.WithFields(logrus.Fields{ + "cmd": strings.Join(append([]string{name}, args...), " "), + }) + l.Debug("Creating subprocess") + cmd := exec.CommandContext(ctx, name, args...) + cmd.Env = os.Environ() + l.Debug("Running subprocess") + cmd.Stderr = logger.WithField("channel", "stderr").WriterLevel(logrus.DebugLevel) + cmd.Stdout = logger.WithField("channel", "stdout").WriterLevel(logrus.DebugLevel) + if err := cmd.Run(); err != nil { + return errors.Join(err, fmt.Errorf("failed to run command")) + } + l.Debug("Complete subprocess") + return nil +} diff --git a/pkg/data/server.go b/pkg/data/server.go new file mode 100644 index 0000000..ec8f845 --- /dev/null +++ b/pkg/data/server.go @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package data + +import ( + context "context" + "errors" + "fmt" + "jbpf_protobuf_cli/schema" + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +const ( + dataReadDeadline = 1 * time.Second + decoderChanSize = 100 +) + +// Server is a server that implements the DynamicDecoderServer interface +type Server struct { + ctx context.Context + logger *logrus.Logger + opts *ServerOptions + store *schema.Store +} + +// NewServer returns a new Server +func NewServer(ctx context.Context, logger *logrus.Logger, opts *ServerOptions, store *schema.Store) (*Server, error) { + return &Server{ + ctx: ctx, + logger: logger, + opts: opts, + store: store, + }, nil +} + +// Listen starts the server +func (s *Server) Listen(onData func(uuid.UUID, []byte)) error { + data, err := net.ListenPacket(dataScheme, fmt.Sprintf("%s:%d", s.opts.dataIP, s.opts.dataPort)) + if err != nil { + return err + } + s.logger.WithField("addr", data.LocalAddr().Network()+"://"+data.LocalAddr().String()).Debug("starting data server") + defer func() { + s.logger.WithField("addr", data.LocalAddr().Network()+"://"+data.LocalAddr().String()).Debug("stopping data server") + if err := data.Close(); err != nil { + s.logger.WithError(err).Errorf("error closing data server") + } + }() + + stopper := make(chan os.Signal, 1) + signal.Notify(stopper, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + for { + select { + case <-stopper: + return nil + case <-s.ctx.Done(): + return nil + + default: + buffer := make([]byte, s.opts.dataBufferSize) + if err := data.SetReadDeadline(time.Now().Add(dataReadDeadline)); err != nil { + return err + } + n, _, err := data.ReadFrom(buffer) + if netErr, ok := err.(net.Error); ok && netErr.Timeout() { + continue + } + if err != nil { + return errors.Join(err, fmt.Errorf("error reading from UDP socket")) + } + + if n < 16 { + s.logger.Warnf("received data is less than %d bytes, skipping", 16) + continue + } + + streamUUID, err := uuid.FromBytes(buffer[:16]) + if err != nil { + s.logger.WithError(err).Error("error parsing stream UUID") + continue + } + + msg, err := s.store.GetProtoMsgInstance(streamUUID) + if err != nil { + s.logger.WithError(err).Error("error creating instance of proto message") + continue + } + + err = proto.Unmarshal(buffer[16:n], msg) + if err != nil { + s.logger.WithError(err).Error("error unmarshalling payload") + continue + } + + res, err := protojson.Marshal(msg) + if err != nil { + s.logger.WithError(err).Error("error marshalling message to JSON") + continue + } + + onData(streamUUID, res) + } + } + +} diff --git a/pkg/data/server_options.go b/pkg/data/server_options.go new file mode 100644 index 0000000..e129245 --- /dev/null +++ b/pkg/data/server_options.go @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package data + +import ( + "fmt" + "net/url" + + "github.com/spf13/pflag" +) + +const ( + dataPrefix = "decoder-data" + dataScheme = "udp" + defaultDataBufferSize = 1<<16 - 1 + defaultDataIP = "" + defaultDataPort = uint16(20788) +) + +// ServerOptions is the options for the decoder server +type ServerOptions struct { + dataBufferSize uint16 + dataIP string + dataPort uint16 +} + +// AddServerOptionsToFlags adds the server options to the flags +func AddServerOptionsToFlags(flags *pflag.FlagSet, opts *ServerOptions) { + if opts == nil { + return + } + + flags.StringVar(&opts.dataIP, dataPrefix+"-ip", defaultDataIP, "IP address of the data UDP server") + flags.Uint16Var(&opts.dataBufferSize, dataPrefix+"-buffer", defaultDataBufferSize, "buffer size for the data UDP server") + flags.Uint16Var(&opts.dataPort, dataPrefix+"-port", defaultDataPort, "port address of the data UDP server") +} + +// Parse parses the server options +func (o *ServerOptions) Parse() error { + _, err := url.ParseRequestURI(fmt.Sprintf("%s://%s:%d", dataScheme, o.dataIP, o.dataPort)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/generator/nanopb/files.go b/pkg/generator/nanopb/files.go new file mode 100644 index 0000000..0f7e6d6 --- /dev/null +++ b/pkg/generator/nanopb/files.go @@ -0,0 +1,54 @@ +package nanopb + +import ( + "errors" + "jbpf_protobuf_cli/common" + "path/filepath" + "strings" + + "github.com/sirupsen/logrus" +) + +const ( + optionsGlob = "*.options" + protosGlob = "*.proto" +) + +// FindFiles finds nanopb files (*.proto, *.options) in a directory +func FindFiles(logger *logrus.Logger, workingDir string) ([]*common.File, error) { + optionsFiles, err1 := filepath.Glob(filepath.Join(workingDir, optionsGlob)) + protoFiles, err2 := filepath.Glob(filepath.Join(workingDir, protosGlob)) + if err := errors.Join(err1, err2); err != nil { + return nil, err + } + + files := make([]*common.File, 0, len(optionsFiles)+len(protoFiles)) + fileNames := make([]string, 0, len(optionsFiles)+len(protoFiles)) + + for _, f := range optionsFiles { + file, err := common.NewFile(f) + if err != nil { + return nil, err + } + files = append(files, file) + fileNames = append(fileNames, filepath.Base(f)) + } + for _, f := range protoFiles { + file, err := common.NewFile(f) + if err != nil { + return nil, err + } + files = append(files, file) + fileNames = append(fileNames, filepath.Base(f)) + } + + if len(files) == 0 { + return nil, errors.New("no nanopb files found") + } else if len(protoFiles) == 0 { + return nil, errors.New("no proto file found") + } + + logger.WithField("files", strings.Join(fileNames, ", ")).Debug("found nanopb files") + + return files, nil +} diff --git a/pkg/generator/nanopb/nanopb.go b/pkg/generator/nanopb/nanopb.go new file mode 100644 index 0000000..33790f3 --- /dev/null +++ b/pkg/generator/nanopb/nanopb.go @@ -0,0 +1,54 @@ +package nanopb + +import ( + "fmt" + "log" + "os" +) + +const ( + nanoPbEnvVar = "NANO_PB" +) + +var ( + // GeneratorPath is $NANO_PB/generator/nanopb_generator + GeneratorPath string + // ProtocPath is $NANO_PB/generator/protoc + ProtocPath string + // Path is $NANO_PB + Path string + // PbCommonCPath is $NANO_PB/pb_common.c + PbCommonCPath string + // PbDecodeCPath is $NANO_PB/pb_decode.c + PbDecodeCPath string + // PbEncodeCPath is $NANO_PB/pb_encode.c + PbEncodeCPath string +) + +func init() { + Path = os.Getenv(nanoPbEnvVar) + + if err := validateDirPath(Path); err != nil { + log.Fatal(err) + } + + ProtocPath = fmt.Sprintf("%s/generator/protoc", Path) + GeneratorPath = fmt.Sprintf("%s/generator/nanopb_generator", Path) + PbCommonCPath = fmt.Sprintf("%s/pb_common.c", Path) + PbDecodeCPath = fmt.Sprintf("%s/pb_decode.c", Path) + PbEncodeCPath = fmt.Sprintf("%s/pb_encode.c", Path) +} + +func validateDirPath(path string) error { + if path == "" { + return nil + } + fi, err := os.Stat(path) + if err != nil { + return err + } + if !fi.IsDir() { + return fmt.Errorf(`Expected "%s" to be a directory`, path) + } + return nil +} diff --git a/pkg/generator/schema/schema.go b/pkg/generator/schema/schema.go new file mode 100644 index 0000000..4d73da7 --- /dev/null +++ b/pkg/generator/schema/schema.go @@ -0,0 +1,112 @@ +package schema + +import ( + "context" + "errors" + "fmt" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/generator/nanopb" + "jbpf_protobuf_cli/generator/stream" + "os" + + "github.com/sirupsen/logrus" +) + +const ( + pbTemplate = "%s.pb" +) + +var ( + generatedFileTemplate = []string{pbTemplate, "%s.pb.c", "%s.pb.h"} +) + +// Config for schema file generation +type Config struct { + Files []*common.File + ProtoMessageNames []string + ProtoPackageName string +} + +// Generate generates files for schema inside a temporary directory +func Generate(ctx context.Context, logger *logrus.Logger, cfg *Config) ([]*common.File, error) { + wd, err := os.MkdirTemp("", "temp*") + if err != nil { + return nil, err + } + defer func() { + if err = os.RemoveAll(wd); err != nil { + logger.WithField("directory", wd).WithError(err).Error("failed to remove working directory") + } + }() + + originalWd, err := os.Getwd() + if err != nil { + return nil, err + } + if err := os.Chdir(wd); err != nil { + return nil, err + } + defer func() { + if err := os.Chdir(originalWd); err != nil { + logger.WithField("directory", wd).WithError(err).Error("failed to change working directory") + } + }() + + for _, fileDetails := range cfg.Files { + logger.Debug("Writing file: ", fileDetails.Name) + f, err := os.Create(fileDetails.Name) + if err != nil { + return nil, err + } + n, err := f.Write(fileDetails.Data) + if n != len(fileDetails.Data) { + err = errors.Join(err, fmt.Errorf("expected to write %d bytes, wrote %d", len(fileDetails.Data), n)) + } + if err != nil { + return nil, errors.Join(err, f.Close()) + } + logger.Debug("Closed file: ", fileDetails.Name) + if err = f.Close(); err != nil { + return nil, err + } + } + + if err := errors.Join( + common.RunSubprocess( + ctx, + logger, + nanopb.GeneratorPath, + cfg.ProtoPackageName+".proto", + ), + common.RunSubprocess( + ctx, + logger, + nanopb.ProtocPath, + cfg.ProtoPackageName+".proto", + "-o", + fmt.Sprintf(pbTemplate, cfg.ProtoPackageName), + )); err != nil { + return nil, err + } + + generatedFiles := make([]*common.File, 0, len(cfg.ProtoMessageNames)*2+3) + + for _, fTemplate := range generatedFileTemplate { + f := fmt.Sprintf(fTemplate, cfg.ProtoPackageName) + fileData, err := common.NewFile(f) + if err != nil { + return nil, err + } + generatedFiles = append(generatedFiles, fileData) + } + + for _, protoMessageName := range cfg.ProtoMessageNames { + files, err := stream.Generate(ctx, logger, cfg.ProtoPackageName, protoMessageName) + if err != nil { + return nil, err + } + generatedFiles = append(generatedFiles, files...) + } + + return generatedFiles, nil +} diff --git a/pkg/generator/stream/_serializer.c.tpl b/pkg/generator/stream/_serializer.c.tpl new file mode 100644 index 0000000..b5c6183 --- /dev/null +++ b/pkg/generator/stream/_serializer.c.tpl @@ -0,0 +1,26 @@ +#define PB_FIELD_32BIT 1 +#include +#include +#include +#include "{{ .ProtoPackageName }}.pb.h" + +const uint32_t proto_message_size = sizeof({{ .ProtoMessageName }}); + +int jbpf_io_serialize(void* input_msg_buf, size_t input_msg_buf_size, char* serialized_data_buf, size_t serialized_data_buf_size) { + if (input_msg_buf_size != proto_message_size) + return -1; + + pb_ostream_t ostream = pb_ostream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + if (!pb_encode(&ostream, {{ .ProtoMessageName }}_fields, input_msg_buf)) + return -1; + + return ostream.bytes_written; +} + +int jbpf_io_deserialize(char* serialized_data_buf, size_t serialized_data_buf_size, void* output_msg_buf, size_t output_msg_buf_size) { + if (output_msg_buf_size != proto_message_size) + return 0; + + pb_istream_t istream = pb_istream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + return pb_decode(&istream, {{ .ProtoMessageName }}_fields, output_msg_buf); +} diff --git a/pkg/generator/stream/stream.go b/pkg/generator/stream/stream.go new file mode 100644 index 0000000..c026b12 --- /dev/null +++ b/pkg/generator/stream/stream.go @@ -0,0 +1,88 @@ +package stream + +import ( + "context" + "errors" + "fmt" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/generator/nanopb" + "os" + "text/template" + + "github.com/sirupsen/logrus" +) + +const ( + defaultPbField32Bit = "1" + envVarPbField32Bit = "PB_FIELD_32BIT" + envVarPbMaxRequiredFields = "PB_MAX_REQUIRED_FIELDS" + serializerC = "%s:%s_serializer.c" + serializerSO = "%s:%s_serializer.so" +) + +func createNewFileWithTmpl(logger *logrus.Logger, filename string, tmpl *template.Template, data SerializerTemplateData) error { + l := logger.WithField("filename", filename) + + f, err := os.Create(filename) + if err != nil { + return err + } + defer func() { + if err := f.Close(); err != nil { + l.WithError(err).Error("Failed to close file") + } + }() + l.Debug("Created file") + err = tmpl.Execute(f, data) + if err != nil { + l.WithError(err).Error("Failed to write to file") + return err + } + l.Debug("Successfully written to file") + return nil +} + +// Generate creates files for a stream +func Generate(ctx context.Context, logger *logrus.Logger, protoPackageName, protoMessageName string) ([]*common.File, error) { + cFile := fmt.Sprintf(serializerC, protoPackageName, protoMessageName) + soFile := fmt.Sprintf(serializerSO, protoPackageName, protoMessageName) + + if err := createNewFileWithTmpl(logger, + cFile, + serializerTemplate, + SerializerTemplateData{ProtoPackageName: protoPackageName, ProtoMessageName: protoMessageName}, + ); err != nil { + return nil, err + } + + pbField32Bit := os.Getenv(envVarPbField32Bit) + if pbField32Bit == "" { + pbField32Bit = defaultPbField32Bit + } + args := []string{ + "-I", + nanopb.Path, + cFile, + protoPackageName + ".pb.c", + nanopb.PbCommonCPath, + nanopb.PbDecodeCPath, + nanopb.PbEncodeCPath, + "-DPB_FIELD_32BIT=" + pbField32Bit, + } + if pbMaxRequiredFields := os.Getenv(envVarPbMaxRequiredFields); len(pbMaxRequiredFields) > 0 { + args = append(args, "-DPB_MAX_REQUIRED_FIELDS="+pbMaxRequiredFields) + } + args = append(args, "-shared", "-fPIC", "-o", soFile) + + if err := common.RunSubprocess(ctx, logger, "cc", args...); err != nil { + return nil, err + } + + cFileData, err1 := common.NewFile(cFile) + soFileData, err2 := common.NewFile(soFile) + if err := errors.Join(err1, err2); err != nil { + return nil, err + } + + return []*common.File{cFileData, soFileData}, nil +} diff --git a/pkg/generator/stream/templates.go b/pkg/generator/stream/templates.go new file mode 100644 index 0000000..982e763 --- /dev/null +++ b/pkg/generator/stream/templates.go @@ -0,0 +1,28 @@ +package stream + +import ( + _ "embed" + "log" + "text/template" +) + +//go:embed _serializer.c.tpl +var tpl string + +var serializerTemplate *template.Template + +// SerializerTemplateData is the data passed to the serializer template +type SerializerTemplateData struct { + ProtoMessageName string + ProtoPackageName string +} + +func init() { + var err error + + serializerTemplate, err = template.New("serializerTemplate").Funcs(nil).Parse(tpl) + + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/go.mod b/pkg/go.mod new file mode 100644 index 0000000..a2ccced --- /dev/null +++ b/pkg/go.mod @@ -0,0 +1,21 @@ +module jbpf_protobuf_cli + +go 1.23.2 + +require ( + github.com/google/uuid v1.6.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.9.0 + golang.org/x/sync v0.8.0 + google.golang.org/protobuf v1.35.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/pkg/go.sum b/pkg/go.sum new file mode 100644 index 0000000..9452956 --- /dev/null +++ b/pkg/go.sum @@ -0,0 +1,36 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/jbpf/client.go b/pkg/jbpf/client.go new file mode 100644 index 0000000..a9f2f94 --- /dev/null +++ b/pkg/jbpf/client.go @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package jbpf + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + + "github.com/sirupsen/logrus" +) + +// Client is a TCP socket client +type Client struct { + conn *net.TCPConn + logger *logrus.Logger + opts *Options +} + +// NewClient creates a new socket client +func NewClient(logger *logrus.Logger, opts *Options) (*Client, error) { + c := &Client{ + logger: logger, + opts: opts, + } + if err := c.connect(); err != nil { + return nil, err + } + return c, nil +} + +func (c *Client) connect() error { + conn, err := net.Dial(scheme, fmt.Sprintf("%s:%d", c.opts.ip, c.opts.port)) + if err != nil { + return err + } + + tcpc, ok := conn.(*net.TCPConn) + if !ok { + return fmt.Errorf("expected a tcp connection") + } + + if c.opts.keepAlivePeriod != 0 { + if err := tcpc.SetKeepAlive(true); err != nil { + return err + } + if err := tcpc.SetKeepAlivePeriod(c.opts.keepAlivePeriod); err != nil { + return err + } + } + + c.conn = tcpc + return nil +} + +// Write writes data to the socket +func (c *Client) Write(bs []byte) error { + if c.conn == nil { + if err := c.connect(); err != nil { + return err + } + } + + lengthField := make([]byte, 2) + binary.LittleEndian.PutUint16(lengthField, uint16(len(bs))) + + if _, err := c.conn.Write(append(lengthField, bs...)); err != nil { + var netErr net.Error + if errors.As(err, &netErr) { + if err := c.Close(); err != nil { + c.logger.WithError(err).Error("failed to close connection") + } + c.conn = nil + return fmt.Errorf("closing connection: %w", netErr) + } + return err + } + + return nil +} + +// Close closes the connection +func (c *Client) Close() error { + if c.conn == nil { + return nil + } + err := c.conn.Close() + c.conn = nil + return err +} diff --git a/pkg/jbpf/options.go b/pkg/jbpf/options.go new file mode 100644 index 0000000..31efcd9 --- /dev/null +++ b/pkg/jbpf/options.go @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package jbpf + +import ( + "fmt" + "net/url" + "time" + + "github.com/spf13/pflag" +) + +const ( + defaultIP = "0.0.0.0" + defaultPort = uint16(20787) + optionsPrefix = "jbpf" + scheme = "tcp" +) + +// Options is the options for the jbpf client +type Options struct { + Enable bool + ip string + keepAlivePeriod time.Duration + port uint16 +} + +// AddOptionsToFlags adds the options to the flags +func AddOptionsToFlags(flags *pflag.FlagSet, opts *Options) { + if opts == nil { + return + } + + flags.BoolVar(&opts.Enable, "jbpf-enable", false, "whether to allow sending control messages to the jbpf TCP server") + flags.DurationVar(&opts.keepAlivePeriod, optionsPrefix+"-keep-alive", 0, "time to keep alive the connection") + flags.StringVar(&opts.ip, optionsPrefix+"-ip", defaultIP, "IP address of the jbpf TCP server") + flags.Uint16Var(&opts.port, optionsPrefix+"-port", defaultPort, "port address of the jbpf TCP server") +} + +// Parse parses the options +func (o *Options) Parse() error { + if !o.Enable { + return nil + } + _, err := url.ParseRequestURI(fmt.Sprintf("%s://%s:%d", scheme, o.ip, o.port)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/main.go b/pkg/main.go new file mode 100644 index 0000000..9d3bc55 --- /dev/null +++ b/pkg/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "jbpf_protobuf_cli/cmd/decoder" + "jbpf_protobuf_cli/cmd/serde" + "jbpf_protobuf_cli/common" + "os" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func main() { + ctx := context.Background() + if err := cli().ExecuteContext(ctx); err != nil { + logrus.WithError(err).Fatal("Exiting") + } +} + +func cli() *cobra.Command { + cmd := &cobra.Command{ + Use: os.Args[0], + Long: "jbpf companion command line tool to generate protobuf assets and a local decoder to interact with a remote jbpf instance over sockets.", + } + opts := common.NewGeneralOptions(cmd.PersistentFlags()) + cmd.AddCommand( + decoder.Command(opts), + serde.Command(opts), + ) + return cmd +} diff --git a/pkg/schema/client.go b/pkg/schema/client.go new file mode 100644 index 0000000..b760d3a --- /dev/null +++ b/pkg/schema/client.go @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +// Client encapsulates the decoder client +type Client struct { + baseURL string + ctx context.Context + inner *http.Client + logger *logrus.Logger +} + +// NewClient creates a new Client +func NewClient(ctx context.Context, logger *logrus.Logger, opts *ClientOptions) (*Client, error) { + return &Client{ + baseURL: fmt.Sprintf("%s://%s:%d", controlScheme, opts.control.ip, opts.control.port), + ctx: ctx, + inner: &http.Client{}, + logger: logger, + }, nil +} + +func (c *Client) doPost(relativePath string, input interface{}) error { + jsonData, err := json.Marshal(input) + if err != nil { + return err + } + + var req *http.Request + req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.baseURL, relativePath), bytes.NewReader(jsonData)) + if err != nil { + return err + } + + resp, err := c.inner.Do(req) + if err != nil { + c.logger.WithError(err).Error("http request failed") + return err + } + + buf := new(strings.Builder) + _, err = io.Copy(buf, resp.Body) + if err != nil { + return err + } + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + err := fmt.Errorf("unexpected status code: %d", resp.StatusCode) + c.logger.WithField("body", buf.String()).WithError(err).Error("unexpected status code") + return err + } + + return nil +} + +func (c *Client) doDelete(relativePath string) error { + var req *http.Request + var err error + req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("%s%s", c.baseURL, relativePath), nil) + if err != nil { + return err + } + + resp, err := c.inner.Do(req) + if err != nil { + c.logger.WithError(err).Error("http request failed") + return err + } + + buf := new(strings.Builder) + _, err = io.Copy(buf, resp.Body) + if err != nil { + return err + } + + if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + err := fmt.Errorf("unexpected status code: %d", resp.StatusCode) + c.logger.WithField("body", buf.String()).WithError(err).Error("unexpected status code") + return err + } + + return nil +} + +// LoadRequest is a request to load a schema and stream +type LoadRequest struct { + CompiledProto []byte + Streams map[uuid.UUID]string +} + +// Load loads the schemas into the decoder +func (c *Client) Load(schemas map[string]*LoadRequest) error { + errs := make([]error, 0, len(schemas)) + + for protoPackageName, req := range schemas { + l := c.logger.WithFields(logrus.Fields{"pkg": protoPackageName}) + + if err := c.doPost("/schema", &UpsertSchemaRequest{ProtoDescriptor: req.CompiledProto}); err != nil { + err = fmt.Errorf("failed to upsert proto package %s: %w", protoPackageName, err) + errs = append(errs, err) + continue + } + + l.Info("successfully upserted proto package") + + for streamUUID, protoMsg := range req.Streams { + err := c.doPost("/stream", &AddSchemaAssociationRequest{StreamUUID: streamUUID, ProtoPackage: protoPackageName, ProtoMessage: protoMsg}) + if err != nil { + err = fmt.Errorf("failed to associate streamID %s to proto package %s and message %s: %w", streamUUID.String(), protoPackageName, protoMsg, err) + errs = append(errs, err) + continue + } + + l.WithFields(logrus.Fields{ + "protoMsg": protoMsg, + "protoPackageName": protoPackageName, + "streamId": streamUUID.String(), + }).Info("successfully associated stream ID with proto package") + } + } + + return errors.Join(errs...) +} + +// SendControl dispatches a control message to the decoder +func (c *Client) SendControl(streamUUID uuid.UUID, jdata string) error { + if err := c.doPost("/control", &SendControlRequest{StreamUUID: streamUUID, Payload: jdata}); err != nil { + return fmt.Errorf("failed to send control message %s: %w", streamUUID.String(), err) + } + + c.logger.WithFields(logrus.Fields{ + "streamId": streamUUID.String(), + }).Info("successfully sent control message") + + return nil +} + +// Unload removes the stream association from the decoder +func (c *Client) Unload(streamUUIDs []uuid.UUID) error { + errs := make([]error, 0, len(streamUUIDs)) + for _, streamUUID := range streamUUIDs { + streamIDStr := base64.StdEncoding.EncodeToString(streamUUID[:]) + if err := c.doDelete(fmt.Sprintf("/stream?stream_uuid=%s", streamIDStr)); err != nil { + err = fmt.Errorf("failed to delete stream ID association %s: %w", streamUUID.String(), err) + errs = append(errs, err) + continue + } + + c.logger.WithFields(logrus.Fields{ + "streamId": streamUUID.String(), + }).Info("successfully deleted stream ID association") + } + + return errors.Join(errs...) +} diff --git a/pkg/schema/client_options.go b/pkg/schema/client_options.go new file mode 100644 index 0000000..5310ed5 --- /dev/null +++ b/pkg/schema/client_options.go @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import "github.com/spf13/pflag" + +// ClientOptions is the options for the decoder client +type ClientOptions struct { + control *controlOptions +} + +// AddClientOptionsToFlags adds the client options to the flags +func AddClientOptionsToFlags(flags *pflag.FlagSet, opts *ClientOptions) { + if opts.control == nil { + opts.control = &controlOptions{} + } + + addControlOptionsToFlags(flags, opts.control) +} + +// Parse parses the client options +func (o *ClientOptions) Parse() error { + return o.control.parse() +} diff --git a/pkg/schema/model.go b/pkg/schema/model.go new file mode 100644 index 0000000..f0bd318 --- /dev/null +++ b/pkg/schema/model.go @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import ( + "encoding/base64" + "encoding/json" + + "github.com/google/uuid" +) + +// UpsertSchemaRequest is the request body for the /schema endpoint +type UpsertSchemaRequest struct { + ProtoDescriptor []byte +} + +// MarshalJSON marshals the UpsertSchemaRequest to JSON +func (u UpsertSchemaRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + ProtoDescriptor string + }{ + ProtoDescriptor: base64.StdEncoding.EncodeToString(u.ProtoDescriptor), + }) +} + +// UnmarshalJSON unmarshals the UpsertSchemaRequest from JSON +func (u *UpsertSchemaRequest) UnmarshalJSON(data []byte) error { + var intermediate struct{ ProtoDescriptor string } + if err := json.Unmarshal(data, &intermediate); err != nil { + return err + } + protoDesc, err := base64.StdEncoding.DecodeString(intermediate.ProtoDescriptor) + if err != nil { + return err + } + u.ProtoDescriptor = protoDesc + return nil +} + +// AddSchemaAssociationRequest is the request body for the /stream endpoint +type AddSchemaAssociationRequest struct { + StreamUUID uuid.UUID + ProtoPackage string + ProtoMessage string +} + +// MarshalJSON marshals the AddSchemaAssociationRequest to JSON +func (a AddSchemaAssociationRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + StreamUUID string + ProtoPackage string + ProtoMessage string + }{ + StreamUUID: a.StreamUUID.String(), + ProtoPackage: a.ProtoPackage, + ProtoMessage: a.ProtoMessage, + }) +} + +// UnmarshalJSON unmarshals the AddSchemaAssociationRequest from JSON +func (a *AddSchemaAssociationRequest) UnmarshalJSON(data []byte) error { + var intermediate struct { + StreamUUID string + ProtoPackage string + ProtoMessage string + } + if err := json.Unmarshal(data, &intermediate); err != nil { + return err + } + streamUUID, err := uuid.Parse(intermediate.StreamUUID) + if err != nil { + return err + } + a.StreamUUID = streamUUID + a.ProtoPackage = intermediate.ProtoPackage + a.ProtoMessage = intermediate.ProtoMessage + return nil +} + +// SendControlRequest is the request body for the /control endpoint +type SendControlRequest struct { + StreamUUID uuid.UUID + Payload string +} + +// MarshalJSON marshals the SendControlRequest to JSON +func (s SendControlRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + StreamUUID string + Payload string + }{ + StreamUUID: s.StreamUUID.String(), + Payload: s.Payload, + }) +} + +// UnmarshalJSON unmarshals the SendControlRequest from JSON +func (s *SendControlRequest) UnmarshalJSON(data []byte) error { + var intermediate struct { + StreamUUID string + Payload string + } + if err := json.Unmarshal(data, &intermediate); err != nil { + return err + } + streamUUID, err := uuid.Parse(intermediate.StreamUUID) + if err != nil { + return err + } + s.StreamUUID = streamUUID + s.Payload = intermediate.Payload + return nil +} diff --git a/pkg/schema/options.go b/pkg/schema/options.go new file mode 100644 index 0000000..75ef1f6 --- /dev/null +++ b/pkg/schema/options.go @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import ( + "fmt" + "net/url" + + "github.com/spf13/pflag" +) + +const ( + // DefaultControlPort is the default used for the local decoder server + DefaultControlPort = uint16(20789) + + controlPrefix = "decoder-control" + controlScheme = "http" + defaultControlIP = "" +) + +type controlOptions struct { + ip string + port uint16 +} + +func addControlOptionsToFlags(flags *pflag.FlagSet, opts *controlOptions) { + flags.StringVar(&opts.ip, controlPrefix+"-ip", defaultControlIP, "IP address of the control HTTP server") + flags.Uint16Var(&opts.port, controlPrefix+"-port", DefaultControlPort, "port address of the control HTTP server") +} + +func (o *controlOptions) parse() error { + _, err := url.ParseRequestURI(fmt.Sprintf("%s://%s:%d", controlScheme, o.ip, o.port)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/schema/serve.go b/pkg/schema/serve.go new file mode 100644 index 0000000..77f1fde --- /dev/null +++ b/pkg/schema/serve.go @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/google/uuid" +) + +func readBodyAs[T any](req *http.Request) (out T, err error) { + buf := new(strings.Builder) + _, err = io.Copy(buf, req.Body) + if err != nil { + return + } + err = json.Unmarshal([]byte(buf.String()), &out) + return +} + +func (s *Server) serveHTTP(ctx context.Context) error { + http.HandleFunc("/schema", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + body, err := readBodyAs[UpsertSchemaRequest](r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := s.UpsertProtoPackage(r.Context(), &body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusOK) + } + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + + http.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + body, err := readBodyAs[AddSchemaAssociationRequest](r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := s.AddStreamToSchemaAssociation(r.Context(), &body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusOK) + } + + case http.MethodDelete: + streamUUIDStr := r.URL.Query().Get("streamUUID") + bs, err := base64.StdEncoding.DecodeString(streamUUIDStr) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + streamUUID, err := uuid.FromBytes(bs) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := s.DeleteStreamToSchemaAssociation(r.Context(), streamUUID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusAccepted) + } + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + + if s.opts.jbpf.Enable { + http.HandleFunc("/control", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + body, err := readBodyAs[SendControlRequest](r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := s.SendControl(r.Context(), &body); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } else { + w.WriteHeader(http.StatusOK) + } + + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + } + + srv := &http.Server{ + Addr: fmt.Sprintf("%s:%d", s.opts.control.ip, s.opts.control.port), + Handler: nil, + } + + go func() { + stopper := make(chan os.Signal, 1) + signal.Notify(stopper, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + select { + case <-stopper: + case <-ctx.Done(): + } + if err := srv.Close(); err != nil { + s.logger.WithError(err).Error("failed stopping the server") + } + }() + + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return err + } + return nil +} diff --git a/pkg/schema/server.go b/pkg/schema/server.go new file mode 100644 index 0000000..9f54de1 --- /dev/null +++ b/pkg/schema/server.go @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import ( + context "context" + "crypto/sha1" + "encoding/base64" + "fmt" + "jbpf_protobuf_cli/jbpf" + "path/filepath" + "strings" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +// Server is a server that implements the DynamicDecoderServer interface +type Server struct { + ctx context.Context + jbpfClient *jbpf.Client + logger *logrus.Logger + opts *ServerOptions + store *Store +} + +// NewServer returns a new Server +func NewServer(ctx context.Context, logger *logrus.Logger, opts *ServerOptions, store *Store) (*Server, error) { + var jbpfClient *jbpf.Client + var err error + + if opts.jbpf.Enable { + jbpfClient, err = jbpf.NewClient(logger, opts.jbpf) + if err != nil { + return nil, err + } + } + + return &Server{ + ctx: ctx, + jbpfClient: jbpfClient, + logger: logger, + opts: opts, + store: store, + }, nil +} + +// Serve starts the server +func (s *Server) Serve() error { + return s.serveHTTP(s.ctx) +} + +// UpsertProtoPackage registers a proto package with the server +func (s *Server) UpsertProtoPackage(_ context.Context, req *UpsertSchemaRequest) error { + checksum := sha1.Sum(req.ProtoDescriptor) + checksumAsString := base64.StdEncoding.EncodeToString(checksum[:]) + + fds := &descriptorpb.FileDescriptorSet{} + if err := proto.Unmarshal(req.ProtoDescriptor, fds); err != nil { + s.logger.WithError(err).Error("unable to unmarshal proto descriptor") + return err + } + + protoPackageFile := fds.File[0].GetName() + protoPackageName := strings.TrimSuffix(protoPackageFile, filepath.Ext(protoPackageFile)) + l := s.logger.WithFields(logrus.Fields{ + "protoPackageName": protoPackageName, + "checksum": checksumAsString, + }) + + if len(fds.File) != 1 { + err := fmt.Errorf("expected exactly one file descriptor in the set, got %d", len(fds.File)) + l.WithError(err).Error("unable to interpret proto descriptor") + return err + } + + if current, ok := s.store.schemas[protoPackageName]; ok { + if current.checksum == checksum { + l.Info("checksum matches, skipping") + return nil + } + l.Warn("overwriting existing proto package") + } else { + l.Info("setting proto package") + } + + s.store.schemas[protoPackageName] = &RecordedProtoDescriptor{ + checksum: checksum, + ProtoDescriptor: req.ProtoDescriptor, + } + + return nil +} + +// AddStreamToSchemaAssociation associates a stream with a schema +func (s *Server) AddStreamToSchemaAssociation(_ context.Context, req *AddSchemaAssociationRequest) error { + l := s.logger.WithFields(logrus.Fields{ + "protoMsg": req.ProtoMessage, + "protoPackage": req.ProtoPackage, + "streamUUID": req.StreamUUID.String(), + }) + + if current, ok := s.store.streamToSchema[req.StreamUUID]; ok { + if current.ProtoMsg == req.ProtoMessage && current.ProtoPackage == req.ProtoPackage { + return nil + } + err := fmt.Errorf("stream already has a schema association") + l.WithError(err).Error("error adding stream to schema association") + return err + } + + if _, ok := s.store.schemas[req.ProtoPackage]; !ok { + err := fmt.Errorf("proto package %s not found", req.ProtoPackage) + l.WithError(err).Error("error adding stream to schema association") + return err + } + + s.store.streamToSchema[req.StreamUUID] = &RecordedStreamToSchema{ + ProtoMsg: req.ProtoMessage, + ProtoPackage: req.ProtoPackage, + } + + l.Info("association added") + + return nil +} + +// SendControl sends data to the jbpf agent +func (s *Server) SendControl(_ context.Context, req *SendControlRequest) error { + msg, err := s.store.GetProtoMsgInstance(req.StreamUUID) + if err != nil { + s.logger.WithError(err).Errorf("error creating instance of proto message %s", req.StreamUUID.String()) + return err + } + + err = protojson.Unmarshal([]byte(req.Payload), msg) + if err != nil { + s.logger.WithError(err).Error("error unmarshalling payload") + return err + } + + s.logger.WithFields(logrus.Fields{ + "msg": fmt.Sprintf("%T - \"%v\"", msg, msg), + }).Info("sending msg") + + payload, err := proto.Marshal(msg) + if err != nil { + return err + } + + out := append(req.StreamUUID[:], payload...) + if err := s.jbpfClient.Write(out); err != nil { + return err + } + + return nil +} + +// DeleteStreamToSchemaAssociation removes the association between a stream and a schema +func (s *Server) DeleteStreamToSchemaAssociation(_ context.Context, req uuid.UUID) error { + l := s.logger.WithField("streamUUID", req.String()) + + if current, ok := s.store.streamToSchema[req]; !ok { + l.Debug("no association found for stream UUID") + } else { + delete(s.store.streamToSchema, req) + l.WithFields(logrus.Fields{ + "protoMsg": current.ProtoMsg, + "protoPackage": current.ProtoPackage, + }).Info("association removed") + } + + return nil +} diff --git a/pkg/schema/server_options.go b/pkg/schema/server_options.go new file mode 100644 index 0000000..8d3d663 --- /dev/null +++ b/pkg/schema/server_options.go @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package schema + +import ( + "errors" + "jbpf_protobuf_cli/jbpf" + + "github.com/spf13/pflag" +) + +// ServerOptions is the options for the decoder server +type ServerOptions struct { + control *controlOptions + jbpf *jbpf.Options +} + +// AddServerOptionsToFlags adds the server options to the flags +func AddServerOptionsToFlags(flags *pflag.FlagSet, opts *ServerOptions) { + if opts == nil { + return + } + if opts.control == nil { + opts.control = &controlOptions{} + } + if opts.jbpf == nil { + opts.jbpf = &jbpf.Options{} + } + + addControlOptionsToFlags(flags, opts.control) + jbpf.AddOptionsToFlags(flags, opts.jbpf) +} + +// Parse parses the server options +func (o *ServerOptions) Parse() error { + return errors.Join( + o.control.parse(), + o.jbpf.Parse(), + ) +} diff --git a/pkg/schema/store.go b/pkg/schema/store.go new file mode 100644 index 0000000..7bb7542 --- /dev/null +++ b/pkg/schema/store.go @@ -0,0 +1,75 @@ +package schema + +import ( + "fmt" + + "github.com/google/uuid" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" +) + +// RecordedProtoDescriptor is a recorded proto descriptor +type RecordedProtoDescriptor struct { + checksum [20]byte + ProtoDescriptor []byte +} + +// RecordedStreamToSchema is a mapping of a stream to a schema +type RecordedStreamToSchema struct { + ProtoMsg string + ProtoPackage string +} + +// Store is an in memory store for protobuf schemas +type Store struct { + schemas map[string]*RecordedProtoDescriptor + streamToSchema map[uuid.UUID]*RecordedStreamToSchema +} + +// NewStore returns a new Store +func NewStore() *Store { + return &Store{ + schemas: make(map[string]*RecordedProtoDescriptor), + streamToSchema: make(map[uuid.UUID]*RecordedStreamToSchema), + } +} + +// GetProtoMsgInstance returns a new dynamic protobuf message instance +func (s *Store) GetProtoMsgInstance(streamUUID uuid.UUID) (*dynamicpb.Message, error) { + schema, ok := s.streamToSchema[streamUUID] + if !ok { + return nil, fmt.Errorf("no schema found for stream UUID %s", streamUUID.String()) + } + + sch, ok := s.schemas[schema.ProtoPackage] + if !ok { + return nil, fmt.Errorf("no schema found for proto package %s", schema.ProtoPackage) + } + + fds := &descriptorpb.FileDescriptorSet{} + if err := proto.Unmarshal(sch.ProtoDescriptor, fds); err != nil { + return nil, err + } + + pd, err := protodesc.NewFiles(fds) + if err != nil { + return nil, err + } + + msgName := protoreflect.FullName(schema.ProtoMsg) + var desc protoreflect.Descriptor + desc, err = pd.FindDescriptorByName(msgName) + if err != nil { + return nil, err + } + + md, ok := desc.(protoreflect.MessageDescriptor) + if !ok { + return nil, fmt.Errorf("failed to cast desc to protoreflect.MessageDescriptor, got %T", desc) + } + + return dynamicpb.NewMessage(md), nil +} diff --git a/setup_jbpfp_env.sh b/setup_jbpfp_env.sh new file mode 100755 index 0000000..540e785 --- /dev/null +++ b/setup_jbpfp_env.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +export JBPFP_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)" +export NANO_PB=$JBPFP_PATH/3p/nanopb +source $JBPFP_PATH/jbpf/setup_jbpf_env.sh diff --git a/testdata/example1/example.options b/testdata/example1/example.options new file mode 100644 index 0000000..c393729 --- /dev/null +++ b/testdata/example1/example.options @@ -0,0 +1,3 @@ +request.name max_size:32 +response.msg max_size:100 +status.status max_size:100 diff --git a/testdata/example1/example.proto b/testdata/example1/example.proto new file mode 100644 index 0000000..47597e7 --- /dev/null +++ b/testdata/example1/example.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; + +enum my_state { + GOOD=0 ; + BAD=1 ; +} + +message my_struct { + required uint32 a_num = 1; + optional uint32 another_num = 2; +} + +message request { + required uint32 id = 1; + required string name = 2; + optional my_state state = 3; +} + +message response { + required uint32 id = 1; + required string msg = 2; +} + +message req_resp { + oneof req_or_resp { + request req = 1; + response resp = 2; + } +} + +message status { + required uint32 id = 1 ; + required string status = 2; + required my_struct a_struct = 3; +} diff --git a/testdata/example2/example2.options b/testdata/example2/example2.options new file mode 100644 index 0000000..8888f25 --- /dev/null +++ b/testdata/example2/example2.options @@ -0,0 +1 @@ +item.name max_size:30 diff --git a/testdata/example2/example2.proto b/testdata/example2/example2.proto new file mode 100644 index 0000000..0169988 --- /dev/null +++ b/testdata/example2/example2.proto @@ -0,0 +1,6 @@ +syntax = "proto2"; + +message item { + required string name = 1; + optional uint32 val = 2; +} diff --git a/testdata/example3/example3.options b/testdata/example3/example3.options new file mode 100644 index 0000000..bcb7cd4 --- /dev/null +++ b/testdata/example3/example3.options @@ -0,0 +1,14 @@ +obj.bytesval max_size:20 +obj.sval max_size:20 +obj.barr max_count:10 +obj.darr max_count:10 +obj.f32arr max_count:10 +obj.f64arr max_count:10 +obj.i32arr max_count:10 +obj.i64arr max_count:10 +obj.sf32arr max_count:10 +obj.sf64arr max_count:10 +obj.si32arr max_count:10 +obj.si64arr max_count:10 +obj.ui32arr max_count:10 +obj.ui64arr max_count:10 diff --git a/testdata/example3/example3.proto b/testdata/example3/example3.proto new file mode 100644 index 0000000..b0c7e5a --- /dev/null +++ b/testdata/example3/example3.proto @@ -0,0 +1,31 @@ +syntax = "proto2"; + +message obj { + required bool bval = 1; + required bytes bytesval = 2; + required double dval = 3; + required fixed32 f32val = 4; + required fixed64 f64val = 5; + required int32 i32val = 6; + required int64 i64val = 7; + required sfixed32 sf32val = 8; + required sfixed64 sf64val = 9; + required sint32 si32val = 10; + required sint64 si64val = 11; + required string sval = 12; + required uint32 ui32val = 13; + required uint64 ui64val = 14; + + repeated bool barr = 15; + repeated double darr = 16; + repeated fixed32 f32arr = 17; + repeated fixed64 f64arr = 18; + repeated int32 i32arr = 19; + repeated int64 i64arr = 20; + repeated sfixed32 sf32arr = 21; + repeated sfixed64 sf64arr = 22; + repeated sint32 si32arr = 23; + repeated sint64 si64arr = 24; + repeated uint32 ui32arr = 25; + repeated uint64 ui64arr = 26; +} From a6b702c45bf619b5f2dc23476e373d6ffd5b2570 Mon Sep 17 00:00:00 2001 From: Xenofon Foukas Date: Wed, 13 Nov 2024 01:32:12 +0000 Subject: [PATCH 2/8] Removed sudo requirement --- examples/first_example_ipc/load.sh | 2 +- examples/first_example_ipc/run_app.sh | 2 +- examples/first_example_ipc/run_collect_control.sh | 2 +- examples/first_example_ipc/unload.sh | 2 +- examples/first_example_standalone/load.sh | 2 +- examples/first_example_standalone/run_app.sh | 2 +- examples/first_example_standalone/unload.sh | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/first_example_ipc/load.sh b/examples/first_example_ipc/load.sh index f0be6c8..b16b217 100755 --- a/examples/first_example_ipc/load.sh +++ b/examples/first_example_ipc/load.sh @@ -4,4 +4,4 @@ set -e $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-control-ip 0.0.0.0 -sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml +$JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_ipc/run_app.sh b/examples/first_example_ipc/run_app.sh index 1a91cd4..6b1b44e 100755 --- a/examples/first_example_ipc/run_app.sh +++ b/examples/first_example_ipc/run_app.sh @@ -1,3 +1,3 @@ #!/bin/sh -sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app diff --git a/examples/first_example_ipc/run_collect_control.sh b/examples/first_example_ipc/run_collect_control.sh index 5f6b69b..45970ed 100755 --- a/examples/first_example_ipc/run_collect_control.sh +++ b/examples/first_example_ipc/run_collect_control.sh @@ -1,3 +1,3 @@ #!/bin/sh -sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_collect_control +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_collect_control diff --git a/examples/first_example_ipc/unload.sh b/examples/first_example_ipc/unload.sh index e519f7d..1b5e2b7 100755 --- a/examples/first_example_ipc/unload.sh +++ b/examples/first_example_ipc/unload.sh @@ -1,5 +1,5 @@ #!/bin/sh -sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml +$JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder unload -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/load.sh b/examples/first_example_standalone/load.sh index f0be6c8..b16b217 100755 --- a/examples/first_example_standalone/load.sh +++ b/examples/first_example_standalone/load.sh @@ -4,4 +4,4 @@ set -e $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-control-ip 0.0.0.0 -sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml +$JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/run_app.sh b/examples/first_example_standalone/run_app.sh index 1a91cd4..6b1b44e 100755 --- a/examples/first_example_standalone/run_app.sh +++ b/examples/first_example_standalone/run_app.sh @@ -1,3 +1,3 @@ #!/bin/sh -sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app diff --git a/examples/first_example_standalone/unload.sh b/examples/first_example_standalone/unload.sh index e519f7d..1b5e2b7 100755 --- a/examples/first_example_standalone/unload.sh +++ b/examples/first_example_standalone/unload.sh @@ -1,5 +1,5 @@ #!/bin/sh -sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml +$JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder unload -c codeletset_load_request.yaml From 5789d1c14dfc5e8059d297183532291355e148c8 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Thu, 14 Nov 2024 17:19:50 +0000 Subject: [PATCH 3/8] Provide custom text formatter logger without colours --- jbpf | 2 +- pkg/common/options.go | 46 ++++- pkg/common/uncolored_text_formatter.go | 266 +++++++++++++++++++++++++ 3 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 pkg/common/uncolored_text_formatter.go diff --git a/jbpf b/jbpf index 5709617..9a25032 160000 --- a/jbpf +++ b/jbpf @@ -1 +1 @@ -Subproject commit 5709617993d3fd719f62f12893c6cfa50556509f +Subproject commit 9a25032f83b3d4038211d3042f9509a13438528a diff --git a/pkg/common/options.go b/pkg/common/options.go index e4b3c3d..2968e41 100644 --- a/pkg/common/options.go +++ b/pkg/common/options.go @@ -4,6 +4,10 @@ package common import ( "errors" + "fmt" + "io" + "os" + "strings" "github.com/sirupsen/logrus" "github.com/spf13/pflag" @@ -19,6 +23,8 @@ func NewGeneralOptions(flags *pflag.FlagSet) *GeneralOptions { // NewGeneralOptionsFromLogger creates a new GeneralOptions from a logger func NewGeneralOptionsFromLogger(logger *logrus.Logger) *GeneralOptions { opts := &GeneralOptions{ + file: "", + formatter: "TextFormatter", Logger: logger, logLevel: logger.Level.String(), reportCaller: logger.ReportCaller, @@ -28,6 +34,8 @@ func NewGeneralOptionsFromLogger(logger *logrus.Logger) *GeneralOptions { // GeneralOptions contains the general options for the jbpf cli type GeneralOptions struct { + file string + formatter string logLevel string reportCaller bool @@ -36,6 +44,8 @@ type GeneralOptions struct { func (opts *GeneralOptions) addToFlags(flags *pflag.FlagSet) { flags.BoolVar(&opts.reportCaller, "log-report-caller", false, "show report caller in logs") + flags.StringVar(&opts.file, "log-file", "", "if set, will write logs to file as well as terminal") + flags.StringVar(&opts.formatter, "log-formatter", "TextFormatter", "logger formatter, set to UncoloredTextFormatter, JSONFormatter or TextFormatter") flags.StringVar(&opts.logLevel, "log-level", "info", "log level, set to: panic, fatal, error, warn, info, debug or trace") } @@ -48,13 +58,39 @@ func (opts *GeneralOptions) Parse() error { // GetLogger returns a logger based on the options func (opts *GeneralOptions) getLogger() (*logrus.Logger, error) { - logger := logrus.New() logLev, err := logrus.ParseLevel(opts.logLevel) if err != nil { - return logger, err + return nil, err } - logger.SetReportCaller(opts.reportCaller) - logger.SetLevel(logLev) - return logger, nil + var formatter logrus.Formatter + switch strings.ToLower(opts.formatter) { + case "uncoloredtextformatter": + formatter = new(UncoloredTextFormatter) + case "jsonformatter": + formatter = new(logrus.JSONFormatter) + case "textformatter": + formatter = new(logrus.TextFormatter) + default: + return nil, fmt.Errorf("invalid log formatter: %v", opts.formatter) + } + + var out io.Writer = os.Stderr + + if opts.file != "" { + file, err := os.OpenFile(opts.file, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return nil, err + } + out = io.MultiWriter(os.Stderr, file) + } + + return &logrus.Logger{ + Out: out, + Formatter: formatter, + Hooks: make(logrus.LevelHooks), + Level: logLev, + ExitFunc: os.Exit, + ReportCaller: opts.reportCaller, + }, nil } diff --git a/pkg/common/uncolored_text_formatter.go b/pkg/common/uncolored_text_formatter.go new file mode 100644 index 0000000..9dbf8b9 --- /dev/null +++ b/pkg/common/uncolored_text_formatter.go @@ -0,0 +1,266 @@ +package common + +import ( + "bytes" + "fmt" + "runtime" + "sort" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/sirupsen/logrus" +) + +const ( + defaultTimestampFormat = time.RFC3339 +) + +var ( + baseTimestamp time.Time + levelTextMaxLength int +) + +func init() { + baseTimestamp = time.Now() + + for _, level := range logrus.AllLevels { + levelTextLength := utf8.RuneCount([]byte(level.String())) + if levelTextLength > levelTextMaxLength { + levelTextMaxLength = levelTextLength + } + } +} + +type fieldKey string + +// FieldMap allows customization of the key names for default fields. +type FieldMap map[fieldKey]string + +func (f FieldMap) resolve(key fieldKey) string { + if k, ok := f[key]; ok { + return k + } + + return string(key) +} + +// UncoloredTextFormatter formats logs into text +type UncoloredTextFormatter struct { + // Force quoting of all values + ForceQuote bool + + // DisableQuote disables quoting for all values. + // DisableQuote will have a lower priority than ForceQuote. + // If both of them are set to true, quote will be forced on all values. + DisableQuote bool + + // Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/ + EnvironmentOverrideColors bool + + // Disable timestamp logging. useful when output is redirected to logging + // system that already adds timestamps. + DisableTimestamp bool + + // Enable logging the full timestamp when a TTY is attached instead of just + // the time passed since beginning of execution. + FullTimestamp bool + + // TimestampFormat to use for display when a full timestamp is printed. + // The format to use is the same than for time.Format or time.Parse from the standard + // library. + // The standard Library already provides a set of predefined format. + TimestampFormat string + + // The fields are sorted by default for a consistent output. For applications + // that log extremely frequently and don't use the JSON formatter this may not + // be desired. + DisableSorting bool + + // The keys sorting function, when uninitialized it uses sort.Strings. + SortingFunc func([]string) + + // Disables the truncation of the level text to 4 characters. + DisableLevelTruncation bool + + // PadLevelText Adds padding the level text so that all the levels output at the same length + // PadLevelText is a superset of the DisableLevelTruncation option + PadLevelText bool + + // QuoteEmptyFields will wrap empty fields in quotes if true + QuoteEmptyFields bool + + // FieldMap allows users to customize the names of keys for default fields. + // As an example: + // formatter := &UncoloredTextFormatter{ + // FieldMap: FieldMap{ + // FieldKeyTime: "@timestamp", + // FieldKeyLevel: "@level", + // FieldKeyMsg: "@message"}} + FieldMap FieldMap + + // CallerPrettyfier can be set by the user to modify the content + // of the function and file keys in the data when ReportCaller is + // activated. If any of the returned value is the empty string the + // corresponding key will be removed from fields. + CallerPrettyfier func(*runtime.Frame) (function string, file string) +} + +// Format renders a single log entry +func (f *UncoloredTextFormatter) Format(entry *logrus.Entry) ([]byte, error) { + data := make(logrus.Fields) + for k, v := range entry.Data { + data[k] = v + } + prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) + keys := make([]string, 0, len(data)) + for k := range data { + keys = append(keys, k) + } + + if !f.DisableSorting { + if f.SortingFunc == nil { + sort.Strings(keys) + } + } + + var b *bytes.Buffer + if entry.Buffer != nil { + b = entry.Buffer + } else { + b = &bytes.Buffer{} + } + + timestampFormat := f.TimestampFormat + if timestampFormat == "" { + timestampFormat = defaultTimestampFormat + } + f.printToBuf(b, entry, keys, data, timestampFormat) + b.WriteByte('\n') + return b.Bytes(), nil +} + +func (f *UncoloredTextFormatter) printToBuf(b *bytes.Buffer, entry *logrus.Entry, keys []string, data logrus.Fields, timestampFormat string) { + levelText := strings.ToUpper(entry.Level.String()) + if !f.DisableLevelTruncation && !f.PadLevelText { + levelText = levelText[0:4] + } + if f.PadLevelText { + // Generates the format string used in the next line, for example "%-6s" or "%-7s". + // Based on the max level text length. + formatString := "%-" + strconv.Itoa(levelTextMaxLength) + "s" + // Formats the level text by appending spaces up to the max length, for example: + // - "INFO " + // - "WARNING" + levelText = fmt.Sprintf(formatString, levelText) + } + + // Remove a single newline if it already exists in the message to keep + // the behavior of logrus text_formatter the same as the stdlib log package + entry.Message = strings.TrimSuffix(entry.Message, "\n") + + caller := "" + if entry.HasCaller() { + funcVal := fmt.Sprintf("%s()", entry.Caller.Function) + fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line) + + if f.CallerPrettyfier != nil { + funcVal, fileVal = f.CallerPrettyfier(entry.Caller) + } + + if fileVal == "" { + caller = funcVal + } else if funcVal == "" { + caller = fileVal + } else { + caller = fileVal + " " + funcVal + } + } + + switch { + case f.DisableTimestamp: + fmt.Fprintf(b, "%s%s %-44s ", levelText, caller, entry.Message) + case !f.FullTimestamp: + fmt.Fprintf(b, "%s[%04d]%s %-44s ", levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message) + default: + fmt.Fprintf(b, "%s[%s]%s %-44s ", levelText, entry.Time.Format(timestampFormat), caller, entry.Message) + } + for _, k := range keys { + v := data[k] + fmt.Fprintf(b, " %s=", k) + f.appendValue(b, v) + } +} + +func (f *UncoloredTextFormatter) needsQuoting(text string) bool { + if f.ForceQuote { + return true + } + if f.QuoteEmptyFields && len(text) == 0 { + return true + } + if f.DisableQuote { + return false + } + for _, ch := range text { + if !((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '.' || ch == '_' || ch == '/' || ch == '@' || ch == '^' || ch == '+') { + return true + } + } + return false +} + +func (f *UncoloredTextFormatter) appendValue(b *bytes.Buffer, value interface{}) { + stringVal, ok := value.(string) + if !ok { + stringVal = fmt.Sprint(value) + } + + if !f.needsQuoting(stringVal) { + b.WriteString(stringVal) + } else { + b.WriteString(fmt.Sprintf("%q", stringVal)) + } +} + +func prefixFieldClashes(data logrus.Fields, fieldMap FieldMap, reportCaller bool) { + timeKey := fieldMap.resolve(logrus.FieldKeyTime) + if t, ok := data[timeKey]; ok { + data["fields."+timeKey] = t + delete(data, timeKey) + } + + msgKey := fieldMap.resolve(logrus.FieldKeyMsg) + if m, ok := data[msgKey]; ok { + data["fields."+msgKey] = m + delete(data, msgKey) + } + + levelKey := fieldMap.resolve(logrus.FieldKeyLevel) + if l, ok := data[levelKey]; ok { + data["fields."+levelKey] = l + delete(data, levelKey) + } + + logrusErrKey := fieldMap.resolve(logrus.FieldKeyLogrusError) + if l, ok := data[logrusErrKey]; ok { + data["fields."+logrusErrKey] = l + delete(data, logrusErrKey) + } + + // If reportCaller is not set, 'func' will not conflict. + if reportCaller { + funcKey := fieldMap.resolve(logrus.FieldKeyFunc) + if l, ok := data[funcKey]; ok { + data["fields."+funcKey] = l + } + fileKey := fieldMap.resolve(logrus.FieldKeyFile) + if l, ok := data[fileKey]; ok { + data["fields."+fileKey] = l + } + } +} From 97c44a8b4ad4f1ea3793aee640562d41ed2ab602 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Thu, 14 Nov 2024 19:45:31 +0000 Subject: [PATCH 4/8] Split input forwarder from the decoder --- README.md | 6 +- docs/design.md | 93 +++++++++ examples/first_example_ipc/README.md | 2 +- examples/first_example_ipc/load.sh | 2 +- .../{send_control.sh => send_input_msg.sh} | 3 +- examples/first_example_standalone/README.md | 2 +- examples/first_example_standalone/load.sh | 4 +- examples/first_example_standalone/run_app.sh | 2 +- .../first_example_standalone/run_decoder.sh | 2 +- .../{send_control.sh => send_input_msg.sh} | 3 +- examples/first_example_standalone/unload.sh | 2 +- pkg/cmd/decoder/control/control.go | 111 ---------- pkg/cmd/decoder/decoder.go | 2 - pkg/cmd/decoder/load/load.go | 41 ++-- pkg/cmd/decoder/run/run.go | 21 +- pkg/cmd/decoder/unload/unload.go | 23 +-- pkg/cmd/decoder/unload/unload_config.go | 78 ------- pkg/cmd/input/forward/forward.go | 190 ++++++++++++++++++ pkg/cmd/input/input.go | 23 +++ .../codeletset_config.go} | 50 +++-- pkg/jbpf/options.go | 5 - pkg/main.go | 4 +- pkg/schema/client.go | 4 +- pkg/schema/client_options.go | 24 --- pkg/schema/options.go | 15 +- pkg/schema/serve.go | 24 +-- pkg/schema/server.go | 65 +----- pkg/schema/server_options.go | 40 ---- 28 files changed, 404 insertions(+), 437 deletions(-) create mode 100644 docs/design.md rename examples/first_example_ipc/{send_control.sh => send_input_msg.sh} (53%) rename examples/first_example_standalone/{send_control.sh => send_input_msg.sh} (53%) delete mode 100644 pkg/cmd/decoder/control/control.go delete mode 100644 pkg/cmd/decoder/unload/unload_config.go create mode 100644 pkg/cmd/input/forward/forward.go create mode 100644 pkg/cmd/input/input.go rename pkg/{cmd/decoder/load/load_config.go => common/codeletset_config.go} (63%) delete mode 100644 pkg/schema/client_options.go delete mode 100644 pkg/schema/server_options.go diff --git a/README.md b/README.md index 80e45e9..7adb516 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # jbpf-protobuf +**NOTE: This project uses an experimental feature from jbpf. It is not meant to be used in production environments.** + This repository is a extension for [jbpf](https://github.com/microsoft/jbpf/) demonstrating how to utilize protobuf serialization as part of jbpf. Prerequisites: @@ -7,7 +9,7 @@ Prerequisites: * Go v1.23.2+ * Make * Pip -* Python +* Python3 The project utilizes [Nanopb](https://github.com/nanopb/nanopb) to generate C structures for given protobuf specs that use contiguous memory. It also generates serializer libraries that can be provided to jbpf, to encode output and decode input data to seamlessly integrate external data processing systems. @@ -34,7 +36,7 @@ docker build -t jbpf_protobuf_builder:latest -f deploy/Dockerfile . ## Running the examples -In order to run any of the samples, you'll need to build Janus. +In order to run any of the samples, you'll need to build jbpf. ```sh mkdir -p jbpf/build diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..772a327 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,93 @@ +# High level Architecture + +`jbpf_protobuf_cli` provides tooling to generate serialization assets for `jbpf` using protobuf. + +For complete details of each subcommand, see `./jbpf_protobuf_cli {SUBCOMMAND} --help`. + + +## Serde + +The `serde` subcommand generates assets from protobuf specs which can integrate with `jbpf`'s [serde functionality](../jbpf/docs/serde.md). + +Developers must write `.proto` file(s) defining the models that are to be serialized. Additionally they must provide [generator options](https://jpa.kapsi.fi/nanopb/docs/reference.html#generator-options) as defined by nanopb to ensure generated structs can be defined in C as contiguous memory structs. + + +### Simple example + +This example goes through generating serde assets for a simple protobuf schema. + +``` +// schema.proto +syntax = "proto2"; + +message my_struct { + required int32 value = 1; + required string name = 2; +} + +// schema.options +my_struct.name max_size:32 +``` + +```sh +# To see all flags and options available, see +./jbpf_protobuf_cli serde --help + +# Generate the jbpf serde assets for the above proto spec +./jbpf_protobuf_cli serde -s schema:my_struct +``` + +This will generate the following files: +* `schema:my_struct_serializer.c`: + ```c + #define PB_FIELD_32BIT 1 + #include + #include + #include + #include "schema.pb.h" + + const uint32_t proto_message_size = sizeof(my_struct); + + int jbpf_io_serialize(void* input_msg_buf, size_t input_msg_buf_size, char* serialized_data_buf, size_t serialized_data_buf_size) { + if (input_msg_buf_size != proto_message_size) + return -1; + + pb_ostream_t ostream = pb_ostream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + if (!pb_encode(&ostream, my_struct_fields, input_msg_buf)) + return -1; + + return ostream.bytes_written; + } + + int jbpf_io_deserialize(char* serialized_data_buf, size_t serialized_data_buf_size, void* output_msg_buf, size_t output_msg_buf_size) { + if (output_msg_buf_size != proto_message_size) + return 0; + + pb_istream_t istream = pb_istream_from_buffer((uint8_t*)serialized_data_buf, serialized_data_buf_size); + return pb_decode(&istream, my_struct_fields, output_msg_buf); + } + ``` +* `schema:my_struct_serializer.so` is the compiled shared object library of `schema:my_struct_serializer.c`. +* `schema.pb` is the complied protobuf spec. +* `schema.pb.c` is the generated nanopb constant definitions. +* `schema.pb.h` is the generated nanopb headers file. + +When loading the codelet description you can provide the generated `{schema}:{message_name}_serializer.so` as the io_channel `serde.file_path`. + +Additionally, you can provide the `{schema}.pb` to a decoder to be able to dynamically decode/encode the protobuf messages. + +To see detailed usage, run `jbpf_protobuf_cli serde --help`. + +## Decoder + +The cli tool also provides a `decoder` subcommand which can be run locally to receive and print protobuf messages sent over a UDP channel. The examples [example_collect_control](../examples/first_example_ipc/example_collect_control.cpp) and [first_example_standalone](../examples/first_example_standalone/example_app.cpp) bind to a UDP socket on port 20788 to send output data from jbpf which matches the default UDP socket for the decoder. + +This is useful for debugging output from jbpf and provide an example of how someone might dynamically decode output from jbpf by providing `.pb` schemas along with the associated stream identifier. + +To see detailed usage, run `jbpf_protobuf_cli decoder --help`. + +## Input Forwarder + +The tool also provides the ability to dynamically send protobuf input to jbpf from an external entity. It uses a TCP socket to send input channel messages to a jbpf instance. The examples [example_collect_control](../examples/first_example_ipc/example_collect_control.cpp) and [first_example_standalone](../examples/first_example_standalone/example_app.cpp) bind to a TCP socket on port 20787 to receive input data for jbpf which matches the default TCP socket for the input forwarder. + +To see detailed usage, run `jbpf_protobuf_cli input forward --help`. diff --git a/examples/first_example_ipc/README.md b/examples/first_example_ipc/README.md index 82b4f56..2e243e1 100644 --- a/examples/first_example_ipc/README.md +++ b/examples/first_example_ipc/README.md @@ -95,7 +95,7 @@ INFO[0010] {"seqNo":7, "value":-7, "name":"instance 7"} streamUUID=00112233-445 To send a manual control message to the `example_app`, we run the command: ```sh -$ ./send_control.sh 101 +$ ./send_input_msg.sh 101 ``` This should trigger a message in the `example_app`: diff --git a/examples/first_example_ipc/load.sh b/examples/first_example_ipc/load.sh index b16b217..ff38b1f 100755 --- a/examples/first_example_ipc/load.sh +++ b/examples/first_example_ipc/load.sh @@ -2,6 +2,6 @@ set -e -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-control-ip 0.0.0.0 +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-api-ip 0.0.0.0 $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_ipc/send_control.sh b/examples/first_example_ipc/send_input_msg.sh similarity index 53% rename from examples/first_example_ipc/send_control.sh rename to examples/first_example_ipc/send_input_msg.sh index 05cff25..b00c4df 100755 --- a/examples/first_example_ipc/send_control.sh +++ b/examples/first_example_ipc/send_input_msg.sh @@ -1,5 +1,6 @@ #!/bin/sh -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder control \ +$JBPFP_PATH/pkg/jbpf_protobuf_cli input forward \ + -c codeletset_load_request.yaml \ --stream-id 11111111-1111-1111-1111-111111111111 \ --inline-json "{\"value\": $1}" diff --git a/examples/first_example_standalone/README.md b/examples/first_example_standalone/README.md index dbab1b5..a72011f 100644 --- a/examples/first_example_standalone/README.md +++ b/examples/first_example_standalone/README.md @@ -69,7 +69,7 @@ INFO[0010] {"seqNo":7, "value":-7, "name":"instance 7"} streamUUID=00112233-445 To send a manual control message to the `example_app`, we run the command: ```sh -$ ./send_control.sh 101 +$ ./send_input_msg.sh 101 ``` This should trigger a message in the `example_app`: diff --git a/examples/first_example_standalone/load.sh b/examples/first_example_standalone/load.sh index b16b217..30d425b 100755 --- a/examples/first_example_standalone/load.sh +++ b/examples/first_example_standalone/load.sh @@ -2,6 +2,6 @@ set -e -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-control-ip 0.0.0.0 +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-api-ip 0.0.0.0 -$JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml +sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/run_app.sh b/examples/first_example_standalone/run_app.sh index 6b1b44e..1a91cd4 100755 --- a/examples/first_example_standalone/run_app.sh +++ b/examples/first_example_standalone/run_app.sh @@ -1,3 +1,3 @@ #!/bin/sh -LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app +sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app diff --git a/examples/first_example_standalone/run_decoder.sh b/examples/first_example_standalone/run_decoder.sh index 48ccb33..0458541 100755 --- a/examples/first_example_standalone/run_decoder.sh +++ b/examples/first_example_standalone/run_decoder.sh @@ -1,3 +1,3 @@ #!/bin/sh -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder run --jbpf-enable +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder run diff --git a/examples/first_example_standalone/send_control.sh b/examples/first_example_standalone/send_input_msg.sh similarity index 53% rename from examples/first_example_standalone/send_control.sh rename to examples/first_example_standalone/send_input_msg.sh index 05cff25..b00c4df 100755 --- a/examples/first_example_standalone/send_control.sh +++ b/examples/first_example_standalone/send_input_msg.sh @@ -1,5 +1,6 @@ #!/bin/sh -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder control \ +$JBPFP_PATH/pkg/jbpf_protobuf_cli input forward \ + -c codeletset_load_request.yaml \ --stream-id 11111111-1111-1111-1111-111111111111 \ --inline-json "{\"value\": $1}" diff --git a/examples/first_example_standalone/unload.sh b/examples/first_example_standalone/unload.sh index 1b5e2b7..e519f7d 100755 --- a/examples/first_example_standalone/unload.sh +++ b/examples/first_example_standalone/unload.sh @@ -1,5 +1,5 @@ #!/bin/sh -$JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml +sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder unload -c codeletset_load_request.yaml diff --git a/pkg/cmd/decoder/control/control.go b/pkg/cmd/decoder/control/control.go deleted file mode 100644 index dd813e3..0000000 --- a/pkg/cmd/decoder/control/control.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -package control - -import ( - "encoding/json" - "errors" - "fmt" - "jbpf_protobuf_cli/common" - "jbpf_protobuf_cli/schema" - "os" - - "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -type runOptions struct { - schema *schema.ClientOptions - general *common.GeneralOptions - - filePath string - inlineJSON string - payload string - streamID string - streamUUID uuid.UUID -} - -func addToFlags(flags *pflag.FlagSet, opts *runOptions) { - flags.StringVarP(&opts.filePath, "file", "f", "", "path to file containing payload in JSON format") - flags.StringVarP(&opts.inlineJSON, "inline-json", "j", "", "inline payload in JSON format") - flags.StringVar(&opts.streamID, "stream-id", "00000000-0000-0000-0000-000000000000", "stream ID") -} - -func (o *runOptions) parse() error { - if (len(o.inlineJSON) > 0 && len(o.filePath) > 0) || (len(o.inlineJSON) == 0 && len(o.filePath) == 0) { - return errors.New("exactly one of --file or --inline-json can be specified") - } - - if len(o.filePath) != 0 { - if fi, err := os.Stat(o.filePath); err != nil { - return err - } else if fi.IsDir() { - return fmt.Errorf(`expected "%s" to be a file, got a directory`, o.filePath) - } - payload, err := os.ReadFile(o.filePath) - if err != nil { - return err - } - var deserializedPayload interface{} - err = json.Unmarshal(payload, &deserializedPayload) - if err != nil { - return err - } - o.payload = string(payload) - } else { - var deserializedPayload interface{} - err := json.Unmarshal([]byte(o.inlineJSON), &deserializedPayload) - if err != nil { - return err - } - o.payload = o.inlineJSON - } - - var err error - o.streamUUID, err = uuid.Parse(o.streamID) - if err != nil { - return err - } - - return nil -} - -// Command Load a schema to a local decoder -func Command(opts *common.GeneralOptions) *cobra.Command { - runOptions := &runOptions{ - schema: &schema.ClientOptions{}, - general: opts, - } - cmd := &cobra.Command{ - Use: "control", - Short: "Load a control message via a local decoder", - Long: "Load a control message via a local decoder", - RunE: func(cmd *cobra.Command, _ []string) error { - return run(cmd, runOptions) - }, - SilenceUsage: true, - } - addToFlags(cmd.PersistentFlags(), runOptions) - schema.AddClientOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) - return cmd -} - -func run(cmd *cobra.Command, opts *runOptions) error { - if err := errors.Join( - opts.general.Parse(), - opts.schema.Parse(), - opts.parse(), - ); err != nil { - return err - } - - logger := opts.general.Logger - - client, err := schema.NewClient(cmd.Context(), logger, opts.schema) - if err != nil { - return err - } - - return client.SendControl(opts.streamUUID, string(opts.payload)) -} diff --git a/pkg/cmd/decoder/decoder.go b/pkg/cmd/decoder/decoder.go index 6a2a1e7..26ea3a3 100644 --- a/pkg/cmd/decoder/decoder.go +++ b/pkg/cmd/decoder/decoder.go @@ -3,7 +3,6 @@ package decoder import ( - "jbpf_protobuf_cli/cmd/decoder/control" "jbpf_protobuf_cli/cmd/decoder/load" "jbpf_protobuf_cli/cmd/decoder/run" "jbpf_protobuf_cli/cmd/decoder/unload" @@ -20,7 +19,6 @@ func Command(opts *common.GeneralOptions) *cobra.Command { Short: "Execute a decoder subcommand", } cmd.AddCommand( - control.Command(opts), load.Command(opts), unload.Command(opts), run.Command(opts), diff --git a/pkg/cmd/decoder/load/load.go b/pkg/cmd/decoder/load/load.go index d284d27..5074150 100644 --- a/pkg/cmd/decoder/load/load.go +++ b/pkg/cmd/decoder/load/load.go @@ -13,12 +13,12 @@ import ( ) type runOptions struct { - schema *schema.ClientOptions - general *common.GeneralOptions + decoderAPI *schema.Options + general *common.GeneralOptions compiledProtos map[string]*common.File configFiles []string - configs []DecoderLoadConfig + configs []common.CodeletsetConfig } func addToFlags(flags *pflag.FlagSet, opts *runOptions) { @@ -26,7 +26,7 @@ func addToFlags(flags *pflag.FlagSet, opts *runOptions) { } func (o *runOptions) parse() error { - configs, compiledProtos, err := fromFiles(o.configFiles...) + configs, compiledProtos, err := common.CodeletsetConfigFromFiles(o.configFiles...) if err != nil { return err } @@ -39,8 +39,8 @@ func (o *runOptions) parse() error { // Command Load a schema to a local decoder func Command(opts *common.GeneralOptions) *cobra.Command { runOptions := &runOptions{ - schema: &schema.ClientOptions{}, - general: opts, + decoderAPI: &schema.Options{}, + general: opts, } cmd := &cobra.Command{ Use: "load", @@ -52,14 +52,14 @@ func Command(opts *common.GeneralOptions) *cobra.Command { SilenceUsage: true, } addToFlags(cmd.PersistentFlags(), runOptions) - schema.AddClientOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + schema.AddOptionsToFlags(cmd.PersistentFlags(), runOptions.decoderAPI) return cmd } func run(cmd *cobra.Command, opts *runOptions) error { if err := errors.Join( opts.general.Parse(), - opts.schema.Parse(), + opts.decoderAPI.Parse(), opts.parse(), ); err != nil { return err @@ -67,7 +67,7 @@ func run(cmd *cobra.Command, opts *runOptions) error { logger := opts.general.Logger - client, err := schema.NewClient(cmd.Context(), logger, opts.schema) + client, err := schema.NewClient(cmd.Context(), logger, opts.decoderAPI) if err != nil { return err } @@ -76,28 +76,15 @@ func run(cmd *cobra.Command, opts *runOptions) error { for _, config := range opts.configs { for _, desc := range config.CodeletDescriptor { - for _, io := range desc.InIOChannel { - if existing, ok := schemas[io.Serde.Protobuf.protoPackageName]; ok { - existing.Streams[io.streamUUID] = io.Serde.Protobuf.MsgName - } else { - compiledProto := opts.compiledProtos[io.Serde.Protobuf.absPackagePath] - schemas[io.Serde.Protobuf.protoPackageName] = &schema.LoadRequest{ - CompiledProto: compiledProto.Data, - Streams: map[uuid.UUID]string{ - io.streamUUID: io.Serde.Protobuf.MsgName, - }, - } - } - } for _, io := range desc.OutIOChannel { - if existing, ok := schemas[io.Serde.Protobuf.protoPackageName]; ok { - existing.Streams[io.streamUUID] = io.Serde.Protobuf.MsgName + if existing, ok := schemas[io.Serde.Protobuf.ProtoPackageName]; ok { + existing.Streams[io.StreamUUID] = io.Serde.Protobuf.MsgName } else { - compiledProto := opts.compiledProtos[io.Serde.Protobuf.absPackagePath] - schemas[io.Serde.Protobuf.protoPackageName] = &schema.LoadRequest{ + compiledProto := opts.compiledProtos[io.Serde.Protobuf.AbsPackagePath] + schemas[io.Serde.Protobuf.ProtoPackageName] = &schema.LoadRequest{ CompiledProto: compiledProto.Data, Streams: map[uuid.UUID]string{ - io.streamUUID: io.Serde.Protobuf.MsgName, + io.StreamUUID: io.Serde.Protobuf.MsgName, }, } } diff --git a/pkg/cmd/decoder/run/run.go b/pkg/cmd/decoder/run/run.go index df508e5..1e96e0e 100644 --- a/pkg/cmd/decoder/run/run.go +++ b/pkg/cmd/decoder/run/run.go @@ -15,17 +15,17 @@ import ( ) type runOptions struct { - general *common.GeneralOptions - data *data.ServerOptions - schema *schema.ServerOptions + general *common.GeneralOptions + data *data.ServerOptions + decoderAPI *schema.Options } // Command Run decoder to collect, decode and print jbpf output func Command(opts *common.GeneralOptions) *cobra.Command { runOptions := &runOptions{ - general: opts, - data: &data.ServerOptions{}, - schema: &schema.ServerOptions{}, + general: opts, + data: &data.ServerOptions{}, + decoderAPI: &schema.Options{}, } cmd := &cobra.Command{ Use: "run", @@ -36,7 +36,7 @@ func Command(opts *common.GeneralOptions) *cobra.Command { }, SilenceUsage: true, } - schema.AddServerOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + schema.AddOptionsToFlags(cmd.PersistentFlags(), runOptions.decoderAPI) data.AddServerOptionsToFlags(cmd.PersistentFlags(), runOptions.data) return cmd } @@ -45,7 +45,7 @@ func run(cmd *cobra.Command, opts *runOptions) error { if err := errors.Join( opts.general.Parse(), opts.data.Parse(), - opts.schema.Parse(), + opts.decoderAPI.Parse(), ); err != nil { return err } @@ -54,10 +54,7 @@ func run(cmd *cobra.Command, opts *runOptions) error { store := schema.NewStore() - schemaServer, err := schema.NewServer(cmd.Context(), logger, opts.schema, store) - if err != nil { - return err - } + schemaServer := schema.NewServer(cmd.Context(), logger, opts.decoderAPI, store) dataServer, err := data.NewServer(cmd.Context(), logger, opts.data, store) if err != nil { diff --git a/pkg/cmd/decoder/unload/unload.go b/pkg/cmd/decoder/unload/unload.go index dfa4487..8abd819 100644 --- a/pkg/cmd/decoder/unload/unload.go +++ b/pkg/cmd/decoder/unload/unload.go @@ -17,11 +17,11 @@ const ( ) type runOptions struct { - schema *schema.ClientOptions - general *common.GeneralOptions + decoderAPI *schema.Options + general *common.GeneralOptions configFiles []string - configs []DecoderUnloadConfig + configs []common.CodeletsetConfig } func addToFlags(flags *pflag.FlagSet, opts *runOptions) { @@ -29,7 +29,7 @@ func addToFlags(flags *pflag.FlagSet, opts *runOptions) { } func (o *runOptions) parse() error { - configs, err := fromFiles(o.configFiles...) + configs, _, err := common.CodeletsetConfigFromFiles(o.configFiles...) if err != nil { return err } @@ -41,8 +41,8 @@ func (o *runOptions) parse() error { // Command Unload a schema from a local decoder func Command(opts *common.GeneralOptions) *cobra.Command { runOptions := &runOptions{ - schema: &schema.ClientOptions{}, - general: opts, + decoderAPI: &schema.Options{}, + general: opts, } cmd := &cobra.Command{ Use: "unload", @@ -54,14 +54,14 @@ func Command(opts *common.GeneralOptions) *cobra.Command { SilenceUsage: true, } addToFlags(cmd.PersistentFlags(), runOptions) - schema.AddClientOptionsToFlags(cmd.PersistentFlags(), runOptions.schema) + schema.AddOptionsToFlags(cmd.PersistentFlags(), runOptions.decoderAPI) return cmd } func run(cmd *cobra.Command, opts *runOptions) error { if err := errors.Join( opts.general.Parse(), - opts.schema.Parse(), + opts.decoderAPI.Parse(), opts.parse(), ); err != nil { return err @@ -69,7 +69,7 @@ func run(cmd *cobra.Command, opts *runOptions) error { logger := opts.general.Logger - client, err := schema.NewClient(cmd.Context(), logger, opts.schema) + client, err := schema.NewClient(cmd.Context(), logger, opts.decoderAPI) if err != nil { return err } @@ -78,11 +78,8 @@ func run(cmd *cobra.Command, opts *runOptions) error { for _, config := range opts.configs { for _, desc := range config.CodeletDescriptor { - for _, io := range desc.InIOChannel { - streamUUIDs = append(streamUUIDs, io.streamUUID) - } for _, io := range desc.OutIOChannel { - streamUUIDs = append(streamUUIDs, io.streamUUID) + streamUUIDs = append(streamUUIDs, io.StreamUUID) } } } diff --git a/pkg/cmd/decoder/unload/unload_config.go b/pkg/cmd/decoder/unload/unload_config.go deleted file mode 100644 index 886e50b..0000000 --- a/pkg/cmd/decoder/unload/unload_config.go +++ /dev/null @@ -1,78 +0,0 @@ -package unload - -import ( - "errors" - "fmt" - "jbpf_protobuf_cli/common" - - "github.com/google/uuid" - "gopkg.in/yaml.v3" -) - -// IOChannelConfig represents the configuration for an IO channel -type IOChannelConfig struct { - StreamID string `yaml:"stream_id"` - - streamUUID uuid.UUID -} - -// CodeletDescriptorConfig represents the configuration for a codelet descriptor -type CodeletDescriptorConfig struct { - InIOChannel []*IOChannelConfig `yaml:"in_io_channel"` - OutIOChannel []*IOChannelConfig `yaml:"out_io_channel"` -} - -// DecoderUnloadConfig represents the configuration for unloading a decoder -type DecoderUnloadConfig struct { - CodeletDescriptor []*CodeletDescriptorConfig `yaml:"codelet_descriptor"` -} - -func (io *IOChannelConfig) verify() error { - streamUUID, err := uuid.Parse(io.StreamID) - if err != nil { - return err - } - io.streamUUID = streamUUID - return nil -} - -func fromFiles(configs ...string) ([]DecoderUnloadConfig, error) { - out := make([]DecoderUnloadConfig, 0, len(configs)) - errs := make([]error, 0, len(configs)) - -configLoad: - for _, c := range configs { - f, err := common.NewFile(c) - if err != nil { - errs = append(errs, fmt.Errorf("failed to read file %s: %w", c, err)) - continue - } - var config DecoderUnloadConfig - if err := yaml.Unmarshal(f.Data, &config); err != nil { - errs = append(errs, fmt.Errorf("failed to unmarshal file %s: %w", c, err)) - continue - } - - for _, desc := range config.CodeletDescriptor { - for _, io := range desc.InIOChannel { - if err := io.verify(); err != nil { - errs = append(errs, fmt.Errorf("failed to verify in_io_channel in file %s: %w", c, err)) - continue configLoad - } - } - for _, io := range desc.OutIOChannel { - if err := io.verify(); err != nil { - errs = append(errs, fmt.Errorf("failed to verify out_io_channel in file %s: %w", c, err)) - continue configLoad - } - } - } - - out = append(out, config) - } - if err := errors.Join(errs...); err != nil { - return nil, err - } - - return out, nil -} diff --git a/pkg/cmd/input/forward/forward.go b/pkg/cmd/input/forward/forward.go new file mode 100644 index 0000000..16e9289 --- /dev/null +++ b/pkg/cmd/input/forward/forward.go @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package forward + +import ( + "encoding/json" + "errors" + "fmt" + "jbpf_protobuf_cli/common" + "jbpf_protobuf_cli/jbpf" + "os" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/dynamicpb" +) + +type runOptions struct { + jbpf *jbpf.Options + general *common.GeneralOptions + + compiledProtos map[string]*common.File + configFiles []string + configs []common.CodeletsetConfig + filePath string + inlineJSON string + payload string + streamID string + streamUUID uuid.UUID +} + +func addToFlags(flags *pflag.FlagSet, opts *runOptions) { + flags.StringArrayVarP(&opts.configFiles, "config", "c", []string{}, "configuration files to load") + flags.StringVar(&opts.streamID, "stream-id", "00000000-0000-0000-0000-000000000000", "stream ID") + flags.StringVarP(&opts.filePath, "file", "f", "", "path to file containing payload in JSON format") + flags.StringVarP(&opts.inlineJSON, "inline-json", "j", "", "inline payload in JSON format") +} + +func (o *runOptions) parse() error { + if (len(o.inlineJSON) > 0 && len(o.filePath) > 0) || (len(o.inlineJSON) == 0 && len(o.filePath) == 0) { + return errors.New("exactly one of --file or --inline-json can be specified") + } + + if len(o.filePath) != 0 { + if fi, err := os.Stat(o.filePath); err != nil { + return err + } else if fi.IsDir() { + return fmt.Errorf(`expected "%s" to be a file, got a directory`, o.filePath) + } + payload, err := os.ReadFile(o.filePath) + if err != nil { + return err + } + var deserializedPayload interface{} + err = json.Unmarshal(payload, &deserializedPayload) + if err != nil { + return err + } + o.payload = string(payload) + } else { + var deserializedPayload interface{} + err := json.Unmarshal([]byte(o.inlineJSON), &deserializedPayload) + if err != nil { + return err + } + o.payload = o.inlineJSON + } + + var err error + o.streamUUID, err = uuid.Parse(o.streamID) + if err != nil { + return err + } + + configs, compiledProtos, err := common.CodeletsetConfigFromFiles(o.configFiles...) + if err != nil { + return err + } + o.configs = configs + o.compiledProtos = compiledProtos + + return nil +} + +// Command Load a schema to a local decoder +func Command(opts *common.GeneralOptions) *cobra.Command { + runOptions := &runOptions{ + jbpf: &jbpf.Options{}, + general: opts, + } + cmd := &cobra.Command{ + Use: "forward", + Short: "Load a control message", + Long: "Load a control message", + RunE: func(cmd *cobra.Command, _ []string) error { + return run(cmd, runOptions) + }, + SilenceUsage: true, + } + addToFlags(cmd.PersistentFlags(), runOptions) + jbpf.AddOptionsToFlags(cmd.PersistentFlags(), runOptions.jbpf) + return cmd +} + +func run(_ *cobra.Command, opts *runOptions) error { + if err := errors.Join( + opts.general.Parse(), + opts.jbpf.Parse(), + opts.parse(), + ); err != nil { + return err + } + + logger := opts.general.Logger + + client, err := jbpf.NewClient(logger, opts.jbpf) + if err != nil { + return err + } + + msg, err := getMessageInstance(opts.configs, opts.compiledProtos, opts.streamUUID) + if err != nil { + return err + } + + err = protojson.Unmarshal([]byte(opts.payload), msg) + if err != nil { + logger.WithError(err).Error("error unmarshalling payload") + return err + } + + logger.WithFields(logrus.Fields{ + "msg": fmt.Sprintf("%T - \"%v\"", msg, msg), + }).Info("sending msg") + + payload, err := proto.Marshal(msg) + if err != nil { + return err + } + + out := append(opts.streamUUID[:], payload...) + + return client.Write(out) +} + +func getMessageInstance(configs []common.CodeletsetConfig, compiledProtos map[string]*common.File, streamUUID uuid.UUID) (*dynamicpb.Message, error) { + for _, config := range configs { + for _, desc := range config.CodeletDescriptor { + for _, io := range desc.InIOChannel { + fmt.Printf("%v == %v = %v\n", io.StreamUUID, streamUUID, io.StreamUUID == streamUUID) + if io.StreamUUID == streamUUID { + compiledProto := compiledProtos[io.Serde.Protobuf.AbsPackagePath] + + fds := &descriptorpb.FileDescriptorSet{} + if err := proto.Unmarshal(compiledProto.Data, fds); err != nil { + return nil, err + } + + pd, err := protodesc.NewFiles(fds) + if err != nil { + return nil, err + } + + msgName := protoreflect.FullName(io.Serde.Protobuf.MsgName) + var desc protoreflect.Descriptor + desc, err = pd.FindDescriptorByName(msgName) + if err != nil { + return nil, err + } + + md, ok := desc.(protoreflect.MessageDescriptor) + if !ok { + return nil, fmt.Errorf("failed to cast desc to protoreflect.MessageDescriptor, got %T", desc) + } + + return dynamicpb.NewMessage(md), nil + } + } + } + } + + return nil, fmt.Errorf("stream %s not found in any of the loaded schemas", streamUUID) +} diff --git a/pkg/cmd/input/input.go b/pkg/cmd/input/input.go new file mode 100644 index 0000000..01ff5f9 --- /dev/null +++ b/pkg/cmd/input/input.go @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package input + +import ( + "jbpf_protobuf_cli/cmd/input/forward" + "jbpf_protobuf_cli/common" + + "github.com/spf13/cobra" +) + +// Command returns the decoder commands +func Command(opts *common.GeneralOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "input", + Long: "Execute a jbpf input subcommand.", + Short: "Execute a jbpf input subcommand", + } + cmd.AddCommand( + forward.Command(opts), + ) + return cmd +} diff --git a/pkg/cmd/decoder/load/load_config.go b/pkg/common/codeletset_config.go similarity index 63% rename from pkg/cmd/decoder/load/load_config.go rename to pkg/common/codeletset_config.go index a2fcdec..0f83f6a 100644 --- a/pkg/cmd/decoder/load/load_config.go +++ b/pkg/common/codeletset_config.go @@ -1,9 +1,8 @@ -package load +package common import ( "errors" "fmt" - "jbpf_protobuf_cli/common" "os" "path/filepath" "strings" @@ -14,11 +13,10 @@ import ( // ProtobufConfig represents the configuration for a protobuf message type ProtobufConfig struct { - MsgName string `yaml:"msg_name"` - PackagePath string `yaml:"package_path"` - - absPackagePath string - protoPackageName string + AbsPackagePath string `yaml:"-"` + MsgName string `yaml:"msg_name"` + PackagePath string `yaml:"package_path"` + ProtoPackageName string `yaml:"-"` } // SerdeConfig represents the configuration for serialize/deserialize @@ -28,10 +26,9 @@ type SerdeConfig struct { // IOChannelConfig represents the configuration for an IO channel type IOChannelConfig struct { - Serde *SerdeConfig `yaml:"serde"` - StreamID string `yaml:"stream_id"` - - streamUUID uuid.UUID + Serde *SerdeConfig `yaml:"serde"` + StreamID string `yaml:"stream_id"` + StreamUUID uuid.UUID `yaml:"-"` } // CodeletDescriptorConfig represents the configuration for a codelet descriptor @@ -40,49 +37,50 @@ type CodeletDescriptorConfig struct { OutIOChannel []*IOChannelConfig `yaml:"out_io_channel"` } -// DecoderLoadConfig represents the configuration for loading a decoder -type DecoderLoadConfig struct { +// CodeletsetConfig represents the configuration for loading a decoder +type CodeletsetConfig struct { CodeletDescriptor []*CodeletDescriptorConfig `yaml:"codelet_descriptor"` } -func (io *IOChannelConfig) verify(compiledProtos map[string]*common.File) error { +func (io *IOChannelConfig) verify(compiledProtos map[string]*File) error { streamUUID, err := uuid.Parse(io.StreamID) if err != nil { return err } - io.streamUUID = streamUUID + io.StreamUUID = streamUUID if io.Serde == nil || io.Serde.Protobuf == nil || io.Serde.Protobuf.PackagePath == "" { return fmt.Errorf("missing required field package_path") } - io.Serde.Protobuf.absPackagePath = os.ExpandEnv(io.Serde.Protobuf.PackagePath) - basename := filepath.Base(io.Serde.Protobuf.absPackagePath) - io.Serde.Protobuf.protoPackageName = strings.TrimSuffix(basename, filepath.Ext(basename)) + io.Serde.Protobuf.AbsPackagePath = os.ExpandEnv(io.Serde.Protobuf.PackagePath) + basename := filepath.Base(io.Serde.Protobuf.AbsPackagePath) + io.Serde.Protobuf.ProtoPackageName = strings.TrimSuffix(basename, filepath.Ext(basename)) - if _, ok := compiledProtos[io.Serde.Protobuf.absPackagePath]; !ok { - protoPkg, err := common.NewFile(io.Serde.Protobuf.absPackagePath) + if _, ok := compiledProtos[io.Serde.Protobuf.AbsPackagePath]; !ok { + protoPkg, err := NewFile(io.Serde.Protobuf.AbsPackagePath) if err != nil { return err } - compiledProtos[io.Serde.Protobuf.absPackagePath] = protoPkg + compiledProtos[io.Serde.Protobuf.AbsPackagePath] = protoPkg } return nil } -func fromFiles(configs ...string) ([]DecoderLoadConfig, map[string]*common.File, error) { - out := make([]DecoderLoadConfig, 0, len(configs)) - compiledProtos := make(map[string]*common.File) +// CodeletsetConfigFromFiles reads and unmarshals the given files into a slice of CodeletsetConfig +func CodeletsetConfigFromFiles(configs ...string) ([]CodeletsetConfig, map[string]*File, error) { + out := make([]CodeletsetConfig, 0, len(configs)) + compiledProtos := make(map[string]*File) errs := make([]error, 0, len(configs)) configLoad: for _, c := range configs { - f, err := common.NewFile(c) + f, err := NewFile(c) if err != nil { errs = append(errs, fmt.Errorf("failed to read file %s: %w", c, err)) continue } - var config DecoderLoadConfig + var config CodeletsetConfig if err := yaml.Unmarshal(f.Data, &config); err != nil { errs = append(errs, fmt.Errorf("failed to unmarshal file %s: %w", c, err)) continue diff --git a/pkg/jbpf/options.go b/pkg/jbpf/options.go index 31efcd9..5a981f9 100644 --- a/pkg/jbpf/options.go +++ b/pkg/jbpf/options.go @@ -19,7 +19,6 @@ const ( // Options is the options for the jbpf client type Options struct { - Enable bool ip string keepAlivePeriod time.Duration port uint16 @@ -31,7 +30,6 @@ func AddOptionsToFlags(flags *pflag.FlagSet, opts *Options) { return } - flags.BoolVar(&opts.Enable, "jbpf-enable", false, "whether to allow sending control messages to the jbpf TCP server") flags.DurationVar(&opts.keepAlivePeriod, optionsPrefix+"-keep-alive", 0, "time to keep alive the connection") flags.StringVar(&opts.ip, optionsPrefix+"-ip", defaultIP, "IP address of the jbpf TCP server") flags.Uint16Var(&opts.port, optionsPrefix+"-port", defaultPort, "port address of the jbpf TCP server") @@ -39,9 +37,6 @@ func AddOptionsToFlags(flags *pflag.FlagSet, opts *Options) { // Parse parses the options func (o *Options) Parse() error { - if !o.Enable { - return nil - } _, err := url.ParseRequestURI(fmt.Sprintf("%s://%s:%d", scheme, o.ip, o.port)) if err != nil { return err diff --git a/pkg/main.go b/pkg/main.go index 9d3bc55..1314376 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -3,6 +3,7 @@ package main import ( "context" "jbpf_protobuf_cli/cmd/decoder" + "jbpf_protobuf_cli/cmd/input" "jbpf_protobuf_cli/cmd/serde" "jbpf_protobuf_cli/common" "os" @@ -21,11 +22,12 @@ func main() { func cli() *cobra.Command { cmd := &cobra.Command{ Use: os.Args[0], - Long: "jbpf companion command line tool to generate protobuf assets and a local decoder to interact with a remote jbpf instance over sockets.", + Long: "jbpf companion command line tool to generate protobuf assets. Includes a decoder to receive output data over a UDP socket from a jbpf instance. Messages are then decoded and print as json. Also provides a mechanism to dispatch input control messages to a jbpf instance via a TCP socket.", } opts := common.NewGeneralOptions(cmd.PersistentFlags()) cmd.AddCommand( decoder.Command(opts), + input.Command(opts), serde.Command(opts), ) return cmd diff --git a/pkg/schema/client.go b/pkg/schema/client.go index b760d3a..2aa7c92 100644 --- a/pkg/schema/client.go +++ b/pkg/schema/client.go @@ -26,9 +26,9 @@ type Client struct { } // NewClient creates a new Client -func NewClient(ctx context.Context, logger *logrus.Logger, opts *ClientOptions) (*Client, error) { +func NewClient(ctx context.Context, logger *logrus.Logger, opts *Options) (*Client, error) { return &Client{ - baseURL: fmt.Sprintf("%s://%s:%d", controlScheme, opts.control.ip, opts.control.port), + baseURL: fmt.Sprintf("%s://%s:%d", controlScheme, opts.ip, opts.port), ctx: ctx, inner: &http.Client{}, logger: logger, diff --git a/pkg/schema/client_options.go b/pkg/schema/client_options.go deleted file mode 100644 index 5310ed5..0000000 --- a/pkg/schema/client_options.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -package schema - -import "github.com/spf13/pflag" - -// ClientOptions is the options for the decoder client -type ClientOptions struct { - control *controlOptions -} - -// AddClientOptionsToFlags adds the client options to the flags -func AddClientOptionsToFlags(flags *pflag.FlagSet, opts *ClientOptions) { - if opts.control == nil { - opts.control = &controlOptions{} - } - - addControlOptionsToFlags(flags, opts.control) -} - -// Parse parses the client options -func (o *ClientOptions) Parse() error { - return o.control.parse() -} diff --git a/pkg/schema/options.go b/pkg/schema/options.go index 75ef1f6..bbef271 100644 --- a/pkg/schema/options.go +++ b/pkg/schema/options.go @@ -13,22 +13,25 @@ const ( // DefaultControlPort is the default used for the local decoder server DefaultControlPort = uint16(20789) - controlPrefix = "decoder-control" + controlPrefix = "decoder-api" controlScheme = "http" defaultControlIP = "" ) -type controlOptions struct { +// Options for internal communication with the decoder +type Options struct { ip string port uint16 } -func addControlOptionsToFlags(flags *pflag.FlagSet, opts *controlOptions) { - flags.StringVar(&opts.ip, controlPrefix+"-ip", defaultControlIP, "IP address of the control HTTP server") - flags.Uint16Var(&opts.port, controlPrefix+"-port", DefaultControlPort, "port address of the control HTTP server") +// AddOptionsToFlags adds the options to the provided flag set +func AddOptionsToFlags(flags *pflag.FlagSet, opts *Options) { + flags.StringVar(&opts.ip, controlPrefix+"-ip", defaultControlIP, "IP address of the decoder HTTP server") + flags.Uint16Var(&opts.port, controlPrefix+"-port", DefaultControlPort, "port address of the decoder HTTP server") } -func (o *controlOptions) parse() error { +// Parse the options +func (o *Options) Parse() error { _, err := url.ParseRequestURI(fmt.Sprintf("%s://%s:%d", controlScheme, o.ip, o.port)) if err != nil { return err diff --git a/pkg/schema/serve.go b/pkg/schema/serve.go index 77f1fde..7f2a37e 100644 --- a/pkg/schema/serve.go +++ b/pkg/schema/serve.go @@ -87,30 +87,8 @@ func (s *Server) serveHTTP(ctx context.Context) error { } }) - if s.opts.jbpf.Enable { - http.HandleFunc("/control", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodPost: - body, err := readBodyAs[SendControlRequest](r) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - if err := s.SendControl(r.Context(), &body); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } else { - w.WriteHeader(http.StatusOK) - } - - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } - }) - } - srv := &http.Server{ - Addr: fmt.Sprintf("%s:%d", s.opts.control.ip, s.opts.control.port), + Addr: fmt.Sprintf("%s:%d", s.opts.ip, s.opts.port), Handler: nil, } diff --git a/pkg/schema/server.go b/pkg/schema/server.go index 9f54de1..f02db4e 100644 --- a/pkg/schema/server.go +++ b/pkg/schema/server.go @@ -7,11 +7,9 @@ import ( "crypto/sha1" "encoding/base64" "fmt" - "jbpf_protobuf_cli/jbpf" "path/filepath" "strings" - "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/descriptorpb" @@ -21,32 +19,20 @@ import ( // Server is a server that implements the DynamicDecoderServer interface type Server struct { - ctx context.Context - jbpfClient *jbpf.Client - logger *logrus.Logger - opts *ServerOptions - store *Store + ctx context.Context + logger *logrus.Logger + opts *Options + store *Store } // NewServer returns a new Server -func NewServer(ctx context.Context, logger *logrus.Logger, opts *ServerOptions, store *Store) (*Server, error) { - var jbpfClient *jbpf.Client - var err error - - if opts.jbpf.Enable { - jbpfClient, err = jbpf.NewClient(logger, opts.jbpf) - if err != nil { - return nil, err - } - } - +func NewServer(ctx context.Context, logger *logrus.Logger, opts *Options, store *Store) *Server { return &Server{ - ctx: ctx, - jbpfClient: jbpfClient, - logger: logger, - opts: opts, - store: store, - }, nil + ctx: ctx, + logger: logger, + opts: opts, + store: store, + } } // Serve starts the server @@ -129,37 +115,6 @@ func (s *Server) AddStreamToSchemaAssociation(_ context.Context, req *AddSchemaA return nil } -// SendControl sends data to the jbpf agent -func (s *Server) SendControl(_ context.Context, req *SendControlRequest) error { - msg, err := s.store.GetProtoMsgInstance(req.StreamUUID) - if err != nil { - s.logger.WithError(err).Errorf("error creating instance of proto message %s", req.StreamUUID.String()) - return err - } - - err = protojson.Unmarshal([]byte(req.Payload), msg) - if err != nil { - s.logger.WithError(err).Error("error unmarshalling payload") - return err - } - - s.logger.WithFields(logrus.Fields{ - "msg": fmt.Sprintf("%T - \"%v\"", msg, msg), - }).Info("sending msg") - - payload, err := proto.Marshal(msg) - if err != nil { - return err - } - - out := append(req.StreamUUID[:], payload...) - if err := s.jbpfClient.Write(out); err != nil { - return err - } - - return nil -} - // DeleteStreamToSchemaAssociation removes the association between a stream and a schema func (s *Server) DeleteStreamToSchemaAssociation(_ context.Context, req uuid.UUID) error { l := s.logger.WithField("streamUUID", req.String()) diff --git a/pkg/schema/server_options.go b/pkg/schema/server_options.go deleted file mode 100644 index 8d3d663..0000000 --- a/pkg/schema/server_options.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. - -package schema - -import ( - "errors" - "jbpf_protobuf_cli/jbpf" - - "github.com/spf13/pflag" -) - -// ServerOptions is the options for the decoder server -type ServerOptions struct { - control *controlOptions - jbpf *jbpf.Options -} - -// AddServerOptionsToFlags adds the server options to the flags -func AddServerOptionsToFlags(flags *pflag.FlagSet, opts *ServerOptions) { - if opts == nil { - return - } - if opts.control == nil { - opts.control = &controlOptions{} - } - if opts.jbpf == nil { - opts.jbpf = &jbpf.Options{} - } - - addControlOptionsToFlags(flags, opts.control) - jbpf.AddOptionsToFlags(flags, opts.jbpf) -} - -// Parse parses the server options -func (o *ServerOptions) Parse() error { - return errors.Join( - o.control.parse(), - o.jbpf.Parse(), - ) -} From 5d746c43aefab6daef69b3f970b3cf1f9bb7e7d6 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Thu, 14 Nov 2024 21:17:33 +0000 Subject: [PATCH 5/8] Add basic arch diagram --- docs/design.md | 1 + docs/jbpf_arch.png | Bin 0 -> 30008 bytes examples/first_example_ipc/run_decoder.sh | 2 +- examples/first_example_standalone/load.sh | 2 +- examples/first_example_standalone/run_app.sh | 2 +- examples/first_example_standalone/unload.sh | 2 +- pkg/cmd/input/forward/forward.go | 73 ++++++++++--------- 7 files changed, 42 insertions(+), 40 deletions(-) create mode 100755 docs/jbpf_arch.png diff --git a/docs/design.md b/docs/design.md index 772a327..b938b29 100644 --- a/docs/design.md +++ b/docs/design.md @@ -4,6 +4,7 @@ For complete details of each subcommand, see `./jbpf_protobuf_cli {SUBCOMMAND} --help`. +![architecture](./jbpf_arch.png) ## Serde diff --git a/docs/jbpf_arch.png b/docs/jbpf_arch.png new file mode 100755 index 0000000000000000000000000000000000000000..72829c3ef35cd2d5bab7db75c1f0feca496b1909 GIT binary patch literal 30008 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 Lf0JRk@9e(;h8W#z literal 0 HcmV?d00001 diff --git a/examples/first_example_ipc/run_decoder.sh b/examples/first_example_ipc/run_decoder.sh index 48ccb33..d01e475 100755 --- a/examples/first_example_ipc/run_decoder.sh +++ b/examples/first_example_ipc/run_decoder.sh @@ -1,3 +1,3 @@ #!/bin/sh -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder run --jbpf-enable +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder diff --git a/examples/first_example_standalone/load.sh b/examples/first_example_standalone/load.sh index 30d425b..ff38b1f 100755 --- a/examples/first_example_standalone/load.sh +++ b/examples/first_example_standalone/load.sh @@ -4,4 +4,4 @@ set -e $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-api-ip 0.0.0.0 -sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml +$JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/run_app.sh b/examples/first_example_standalone/run_app.sh index 1a91cd4..6b1b44e 100755 --- a/examples/first_example_standalone/run_app.sh +++ b/examples/first_example_standalone/run_app.sh @@ -1,3 +1,3 @@ #!/bin/sh -sudo LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app +LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$JBPF_PATH/out/lib ./example_app diff --git a/examples/first_example_standalone/unload.sh b/examples/first_example_standalone/unload.sh index e519f7d..1b5e2b7 100755 --- a/examples/first_example_standalone/unload.sh +++ b/examples/first_example_standalone/unload.sh @@ -1,5 +1,5 @@ #!/bin/sh -sudo -E $JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml +$JBPF_PATH/out/bin/jbpf_lcm_cli -u -c codeletset_unload_request.yaml $JBPFP_PATH/pkg/jbpf_protobuf_cli decoder unload -c codeletset_load_request.yaml diff --git a/pkg/cmd/input/forward/forward.go b/pkg/cmd/input/forward/forward.go index 16e9289..ec559d3 100644 --- a/pkg/cmd/input/forward/forward.go +++ b/pkg/cmd/input/forward/forward.go @@ -43,50 +43,20 @@ func addToFlags(flags *pflag.FlagSet, opts *runOptions) { flags.StringVarP(&opts.inlineJSON, "inline-json", "j", "", "inline payload in JSON format") } -func (o *runOptions) parse() error { - if (len(o.inlineJSON) > 0 && len(o.filePath) > 0) || (len(o.inlineJSON) == 0 && len(o.filePath) == 0) { - return errors.New("exactly one of --file or --inline-json can be specified") - } - - if len(o.filePath) != 0 { - if fi, err := os.Stat(o.filePath); err != nil { - return err - } else if fi.IsDir() { - return fmt.Errorf(`expected "%s" to be a file, got a directory`, o.filePath) - } - payload, err := os.ReadFile(o.filePath) - if err != nil { - return err - } - var deserializedPayload interface{} - err = json.Unmarshal(payload, &deserializedPayload) - if err != nil { - return err - } - o.payload = string(payload) - } else { - var deserializedPayload interface{} - err := json.Unmarshal([]byte(o.inlineJSON), &deserializedPayload) - if err != nil { - return err - } - o.payload = o.inlineJSON - } - - var err error - o.streamUUID, err = uuid.Parse(o.streamID) +func (o *runOptions) parse() (err error) { + o.payload, err = loadInlineJSONOrFromFile(o.inlineJSON, o.filePath) if err != nil { return err } - configs, compiledProtos, err := common.CodeletsetConfigFromFiles(o.configFiles...) + o.streamUUID, err = uuid.Parse(o.streamID) if err != nil { return err } - o.configs = configs - o.compiledProtos = compiledProtos - return nil + o.configs, o.compiledProtos, err = common.CodeletsetConfigFromFiles(o.configFiles...) + + return err } // Command Load a schema to a local decoder @@ -150,6 +120,37 @@ func run(_ *cobra.Command, opts *runOptions) error { return client.Write(out) } +func loadInlineJSONOrFromFile(inlineJSON, filePath string) (string, error) { + if (len(inlineJSON) > 0 && len(filePath) > 0) || (len(inlineJSON) == 0 && len(filePath) == 0) { + return "", errors.New("exactly one of --file or --inline-json can be specified") + } + + if len(filePath) != 0 { + if fi, err := os.Stat(filePath); err != nil { + return "", err + } else if fi.IsDir() { + return "", fmt.Errorf(`expected "%s" to be a file, got a directory`, filePath) + } + payload, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + var deserializedPayload interface{} + err = json.Unmarshal(payload, &deserializedPayload) + if err != nil { + return "", err + } + return string(payload), nil + } + + var deserializedPayload interface{} + err := json.Unmarshal([]byte(inlineJSON), &deserializedPayload) + if err != nil { + return "", err + } + return inlineJSON, nil +} + func getMessageInstance(configs []common.CodeletsetConfig, compiledProtos map[string]*common.File, streamUUID uuid.UUID) (*dynamicpb.Message, error) { for _, config := range configs { for _, desc := range config.CodeletDescriptor { From 6b542d1094ea7825105e3775242b16af039b0d9c Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Thu, 14 Nov 2024 21:19:25 +0000 Subject: [PATCH 6/8] Fix run decoder script --- examples/first_example_ipc/run_decoder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/first_example_ipc/run_decoder.sh b/examples/first_example_ipc/run_decoder.sh index d01e475..0458541 100755 --- a/examples/first_example_ipc/run_decoder.sh +++ b/examples/first_example_ipc/run_decoder.sh @@ -1,3 +1,3 @@ #!/bin/sh -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder run From 58c0b75fc32eefaf0c9692b5bd99bb3452b44158 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Fri, 15 Nov 2024 11:04:14 +0000 Subject: [PATCH 7/8] Fix unload and update jbpf --- examples/first_example_ipc/load.sh | 2 +- examples/first_example_standalone/load.sh | 2 +- jbpf | 2 +- pkg/cmd/decoder/load/load.go | 23 +-- pkg/cmd/decoder/unload/unload.go | 13 +- pkg/cmd/input/forward/forward.go | 19 ++- pkg/cmd/input/input.go | 2 +- pkg/common/codeletset_config.go | 191 +++++++++++++++------- pkg/common/codeletset_raw_config.go | 49 ++++++ pkg/schema/client.go | 12 +- pkg/schema/serve.go | 21 ++- pkg/schema/server.go | 4 +- 12 files changed, 239 insertions(+), 101 deletions(-) create mode 100644 pkg/common/codeletset_raw_config.go diff --git a/examples/first_example_ipc/load.sh b/examples/first_example_ipc/load.sh index ff38b1f..0269ff0 100755 --- a/examples/first_example_ipc/load.sh +++ b/examples/first_example_ipc/load.sh @@ -2,6 +2,6 @@ set -e -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-api-ip 0.0.0.0 +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/examples/first_example_standalone/load.sh b/examples/first_example_standalone/load.sh index ff38b1f..0269ff0 100755 --- a/examples/first_example_standalone/load.sh +++ b/examples/first_example_standalone/load.sh @@ -2,6 +2,6 @@ set -e -$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml --decoder-api-ip 0.0.0.0 +$JBPFP_PATH/pkg/jbpf_protobuf_cli decoder load -c codeletset_load_request.yaml $JBPF_PATH/out/bin/jbpf_lcm_cli -l -c codeletset_load_request.yaml diff --git a/jbpf b/jbpf index 9a25032..d821e42 160000 --- a/jbpf +++ b/jbpf @@ -1 +1 @@ -Subproject commit 9a25032f83b3d4038211d3042f9509a13438528a +Subproject commit d821e42e56884ebdf5d66520ed2df30f136a90af diff --git a/pkg/cmd/decoder/load/load.go b/pkg/cmd/decoder/load/load.go index 5074150..03f88d5 100644 --- a/pkg/cmd/decoder/load/load.go +++ b/pkg/cmd/decoder/load/load.go @@ -18,22 +18,20 @@ type runOptions struct { compiledProtos map[string]*common.File configFiles []string - configs []common.CodeletsetConfig + configs []*common.CodeletsetConfig } func addToFlags(flags *pflag.FlagSet, opts *runOptions) { flags.StringArrayVarP(&opts.configFiles, "config", "c", []string{}, "configuration files to load") } -func (o *runOptions) parse() error { - configs, compiledProtos, err := common.CodeletsetConfigFromFiles(o.configFiles...) +func (o *runOptions) parse() (err error) { + o.configs, err = common.CodeletsetConfigFromFiles(o.configFiles...) if err != nil { - return err + return } - o.configs = configs - o.compiledProtos = compiledProtos - - return nil + o.compiledProtos, err = common.LoadCompiledProtos(o.configs, false, true) + return } // Command Load a schema to a local decoder @@ -77,11 +75,14 @@ func run(cmd *cobra.Command, opts *runOptions) error { for _, config := range opts.configs { for _, desc := range config.CodeletDescriptor { for _, io := range desc.OutIOChannel { - if existing, ok := schemas[io.Serde.Protobuf.ProtoPackageName]; ok { + if existing, ok := schemas[io.Serde.Protobuf.PackageName]; ok { existing.Streams[io.StreamUUID] = io.Serde.Protobuf.MsgName } else { - compiledProto := opts.compiledProtos[io.Serde.Protobuf.AbsPackagePath] - schemas[io.Serde.Protobuf.ProtoPackageName] = &schema.LoadRequest{ + compiledProto, ok := opts.compiledProtos[io.Serde.Protobuf.PackagePath] + if !ok { + return errors.New("compiled proto not found") + } + schemas[io.Serde.Protobuf.PackageName] = &schema.LoadRequest{ CompiledProto: compiledProto.Data, Streams: map[uuid.UUID]string{ io.StreamUUID: io.Serde.Protobuf.MsgName, diff --git a/pkg/cmd/decoder/unload/unload.go b/pkg/cmd/decoder/unload/unload.go index 8abd819..6bf82cf 100644 --- a/pkg/cmd/decoder/unload/unload.go +++ b/pkg/cmd/decoder/unload/unload.go @@ -21,21 +21,16 @@ type runOptions struct { general *common.GeneralOptions configFiles []string - configs []common.CodeletsetConfig + configs []*common.CodeletsetConfig } func addToFlags(flags *pflag.FlagSet, opts *runOptions) { flags.StringArrayVarP(&opts.configFiles, "config", "c", []string{}, "configuration files to unload") } -func (o *runOptions) parse() error { - configs, _, err := common.CodeletsetConfigFromFiles(o.configFiles...) - if err != nil { - return err - } - o.configs = configs - - return nil +func (o *runOptions) parse() (err error) { + o.configs, err = common.CodeletsetConfigFromFiles(o.configFiles...) + return } // Command Unload a schema from a local decoder diff --git a/pkg/cmd/input/forward/forward.go b/pkg/cmd/input/forward/forward.go index ec559d3..8b08e1d 100644 --- a/pkg/cmd/input/forward/forward.go +++ b/pkg/cmd/input/forward/forward.go @@ -28,7 +28,7 @@ type runOptions struct { compiledProtos map[string]*common.File configFiles []string - configs []common.CodeletsetConfig + configs []*common.CodeletsetConfig filePath string inlineJSON string payload string @@ -46,17 +46,21 @@ func addToFlags(flags *pflag.FlagSet, opts *runOptions) { func (o *runOptions) parse() (err error) { o.payload, err = loadInlineJSONOrFromFile(o.inlineJSON, o.filePath) if err != nil { - return err + return } o.streamUUID, err = uuid.Parse(o.streamID) if err != nil { - return err + return } - o.configs, o.compiledProtos, err = common.CodeletsetConfigFromFiles(o.configFiles...) + o.configs, err = common.CodeletsetConfigFromFiles(o.configFiles...) + if err != nil { + return + } - return err + o.compiledProtos, err = common.LoadCompiledProtos(o.configs, true, false) + return } // Command Load a schema to a local decoder @@ -151,13 +155,12 @@ func loadInlineJSONOrFromFile(inlineJSON, filePath string) (string, error) { return inlineJSON, nil } -func getMessageInstance(configs []common.CodeletsetConfig, compiledProtos map[string]*common.File, streamUUID uuid.UUID) (*dynamicpb.Message, error) { +func getMessageInstance(configs []*common.CodeletsetConfig, compiledProtos map[string]*common.File, streamUUID uuid.UUID) (*dynamicpb.Message, error) { for _, config := range configs { for _, desc := range config.CodeletDescriptor { for _, io := range desc.InIOChannel { - fmt.Printf("%v == %v = %v\n", io.StreamUUID, streamUUID, io.StreamUUID == streamUUID) if io.StreamUUID == streamUUID { - compiledProto := compiledProtos[io.Serde.Protobuf.AbsPackagePath] + compiledProto := compiledProtos[io.Serde.Protobuf.PackagePath] fds := &descriptorpb.FileDescriptorSet{} if err := proto.Unmarshal(compiledProto.Data, fds); err != nil { diff --git a/pkg/cmd/input/input.go b/pkg/cmd/input/input.go index 01ff5f9..1c3aa42 100644 --- a/pkg/cmd/input/input.go +++ b/pkg/cmd/input/input.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -// Command returns the decoder commands +// Command returns the input commands func Command(opts *common.GeneralOptions) *cobra.Command { cmd := &cobra.Command{ Use: "input", diff --git a/pkg/common/codeletset_config.go b/pkg/common/codeletset_config.go index 0f83f6a..fd0be50 100644 --- a/pkg/common/codeletset_config.go +++ b/pkg/common/codeletset_config.go @@ -8,104 +8,181 @@ import ( "strings" "github.com/google/uuid" - "gopkg.in/yaml.v3" ) // ProtobufConfig represents the configuration for a protobuf message type ProtobufConfig struct { - AbsPackagePath string `yaml:"-"` - MsgName string `yaml:"msg_name"` - PackagePath string `yaml:"package_path"` - ProtoPackageName string `yaml:"-"` + MsgName string + PackageName string + PackagePath string +} + +func newProtobufConfig(cfg *ProtobufRawConfig) (*ProtobufConfig, error) { + if len(cfg.MsgName) == 0 { + return nil, fmt.Errorf("missing required field serde.protobuf.msg_name") + } + if len(cfg.PackagePath) == 0 { + return nil, fmt.Errorf("missing required field serde.protobuf.package_path") + } + + packagePath := os.ExpandEnv(cfg.PackagePath) + basename := filepath.Base(packagePath) + + return &ProtobufConfig{ + MsgName: cfg.MsgName, + PackageName: strings.TrimSuffix(basename, filepath.Ext(basename)), + PackagePath: packagePath, + }, nil } // SerdeConfig represents the configuration for serialize/deserialize type SerdeConfig struct { - Protobuf *ProtobufConfig `yaml:"protobuf"` + Protobuf *ProtobufConfig +} + +func newSerdeConfig(cfg *SerdeRawConfig) (*SerdeConfig, error) { + if cfg.Protobuf == nil { + return nil, fmt.Errorf("missing required field serde.protobuf") + } + + protobuf, err := newProtobufConfig(cfg.Protobuf) + if err != nil { + return nil, err + } + + return &SerdeConfig{Protobuf: protobuf}, nil } // IOChannelConfig represents the configuration for an IO channel type IOChannelConfig struct { - Serde *SerdeConfig `yaml:"serde"` - StreamID string `yaml:"stream_id"` - StreamUUID uuid.UUID `yaml:"-"` + Serde *SerdeConfig + StreamUUID uuid.UUID +} + +func newIOChannelConfig(cfg *IOChannelRawConfig) (*IOChannelConfig, error) { + if cfg.Serde == nil { + return nil, fmt.Errorf("missing required field serde") + } + + serde, err := newSerdeConfig(cfg.Serde) + if err != nil { + return nil, err + } + + streamUUID, err := uuid.Parse(cfg.StreamID) + if err != nil { + return nil, err + } + + return &IOChannelConfig{ + Serde: serde, + StreamUUID: streamUUID, + }, nil } // CodeletDescriptorConfig represents the configuration for a codelet descriptor type CodeletDescriptorConfig struct { - InIOChannel []*IOChannelConfig `yaml:"in_io_channel"` - OutIOChannel []*IOChannelConfig `yaml:"out_io_channel"` + InIOChannel []*IOChannelConfig + OutIOChannel []*IOChannelConfig +} + +func newCodeletDescriptorConfig(cfg *CodeletDescriptorRawConfig) (*CodeletDescriptorConfig, error) { + inIOChannel := make([]*IOChannelConfig, 0, len(cfg.InIOChannel)) + for _, rawIO := range cfg.InIOChannel { + io, err := newIOChannelConfig(rawIO) + if err != nil { + return nil, err + } + inIOChannel = append(inIOChannel, io) + } + + outIOChannel := make([]*IOChannelConfig, 0, len(cfg.OutIOChannel)) + for _, rawIO := range cfg.OutIOChannel { + io, err := newIOChannelConfig(rawIO) + if err != nil { + return nil, err + } + outIOChannel = append(outIOChannel, io) + } + + return &CodeletDescriptorConfig{ + InIOChannel: inIOChannel, + OutIOChannel: outIOChannel, + }, nil } // CodeletsetConfig represents the configuration for loading a decoder type CodeletsetConfig struct { - CodeletDescriptor []*CodeletDescriptorConfig `yaml:"codelet_descriptor"` + CodeletDescriptor []*CodeletDescriptorConfig } -func (io *IOChannelConfig) verify(compiledProtos map[string]*File) error { - streamUUID, err := uuid.Parse(io.StreamID) - if err != nil { - return err - } - io.StreamUUID = streamUUID - if io.Serde == nil || io.Serde.Protobuf == nil || io.Serde.Protobuf.PackagePath == "" { - return fmt.Errorf("missing required field package_path") +func newCodeletSetConfig(cfg *CodeletsetRawConfig) (*CodeletsetConfig, error) { + codeletDescriptors := make([]*CodeletDescriptorConfig, 0, len(cfg.CodeletDescriptor)) + for _, rawDesc := range cfg.CodeletDescriptor { + desc, err := newCodeletDescriptorConfig(rawDesc) + if err != nil { + return nil, err + } + codeletDescriptors = append(codeletDescriptors, desc) } + return &CodeletsetConfig{CodeletDescriptor: codeletDescriptors}, nil +} - io.Serde.Protobuf.AbsPackagePath = os.ExpandEnv(io.Serde.Protobuf.PackagePath) - basename := filepath.Base(io.Serde.Protobuf.AbsPackagePath) - io.Serde.Protobuf.ProtoPackageName = strings.TrimSuffix(basename, filepath.Ext(basename)) +// LoadCompiledProtos loads the compiled protobuf files from the codeletset config +func LoadCompiledProtos(cfgs []*CodeletsetConfig, includeInIO, includeOutIO bool) (map[string]*File, error) { + compiledProtos := make(map[string]*File) - if _, ok := compiledProtos[io.Serde.Protobuf.AbsPackagePath]; !ok { - protoPkg, err := NewFile(io.Serde.Protobuf.AbsPackagePath) - if err != nil { - return err + for _, c := range cfgs { + for _, desc := range c.CodeletDescriptor { + if includeInIO { + for _, io := range desc.InIOChannel { + if _, ok := compiledProtos[io.Serde.Protobuf.PackagePath]; !ok { + protoPkg, err := NewFile(io.Serde.Protobuf.PackagePath) + if err != nil { + return nil, err + } + compiledProtos[io.Serde.Protobuf.PackagePath] = protoPkg + } + } + } + + if includeOutIO { + for _, io := range desc.OutIOChannel { + if _, ok := compiledProtos[io.Serde.Protobuf.PackagePath]; !ok { + protoPkg, err := NewFile(io.Serde.Protobuf.PackagePath) + if err != nil { + return nil, err + } + compiledProtos[io.Serde.Protobuf.PackagePath] = protoPkg + } + } + } } - compiledProtos[io.Serde.Protobuf.AbsPackagePath] = protoPkg } - return nil + return compiledProtos, nil } // CodeletsetConfigFromFiles reads and unmarshals the given files into a slice of CodeletsetConfig -func CodeletsetConfigFromFiles(configs ...string) ([]CodeletsetConfig, map[string]*File, error) { - out := make([]CodeletsetConfig, 0, len(configs)) - compiledProtos := make(map[string]*File) +func CodeletsetConfigFromFiles(configs ...string) ([]*CodeletsetConfig, error) { + out := make([]*CodeletsetConfig, 0, len(configs)) errs := make([]error, 0, len(configs)) -configLoad: for _, c := range configs { - f, err := NewFile(c) + rawConfig, err := newCodeletsetRawConfig(c) if err != nil { - errs = append(errs, fmt.Errorf("failed to read file %s: %w", c, err)) - continue - } - var config CodeletsetConfig - if err := yaml.Unmarshal(f.Data, &config); err != nil { - errs = append(errs, fmt.Errorf("failed to unmarshal file %s: %w", c, err)) + errs = append(errs, err) continue } - for _, desc := range config.CodeletDescriptor { - for _, io := range desc.InIOChannel { - if err := io.verify(compiledProtos); err != nil { - errs = append(errs, fmt.Errorf("failed to verify in_io_channel in file %s: %w", c, err)) - continue configLoad - } - } - for _, io := range desc.OutIOChannel { - if err := io.verify(compiledProtos); err != nil { - errs = append(errs, fmt.Errorf("failed to verify out_io_channel in file %s: %w", c, err)) - continue configLoad - } - } + config, err := newCodeletSetConfig(rawConfig) + if err != nil { + errs = append(errs, fmt.Errorf("failed to unpack file %s: %w", c, err)) + continue } out = append(out, config) } - if err := errors.Join(errs...); err != nil { - return nil, nil, err - } - return out, compiledProtos, nil + return out, errors.Join(errs...) } diff --git a/pkg/common/codeletset_raw_config.go b/pkg/common/codeletset_raw_config.go new file mode 100644 index 0000000..8a13823 --- /dev/null +++ b/pkg/common/codeletset_raw_config.go @@ -0,0 +1,49 @@ +package common + +import ( + "fmt" + + "gopkg.in/yaml.v3" +) + +// ProtobufRawConfig represents the configuration for a protobuf message as defined in the yaml config +type ProtobufRawConfig struct { + MsgName string `yaml:"msg_name"` + PackagePath string `yaml:"package_path"` +} + +// SerdeRawConfig represents the configuration for serialize/deserialize as defined in the yaml config +type SerdeRawConfig struct { + Protobuf *ProtobufRawConfig `yaml:"protobuf"` +} + +// IOChannelRawConfig represents the configuration for an IO channel as defined in the yaml config +type IOChannelRawConfig struct { + Serde *SerdeRawConfig `yaml:"serde"` + StreamID string `yaml:"stream_id"` +} + +// CodeletDescriptorRawConfig represents the configuration for a codelet descriptor as defined in the yaml config +type CodeletDescriptorRawConfig struct { + InIOChannel []*IOChannelRawConfig `yaml:"in_io_channel"` + OutIOChannel []*IOChannelRawConfig `yaml:"out_io_channel"` +} + +// CodeletsetRawConfig represents the configuration for loading a decoder as defined in the yaml config +type CodeletsetRawConfig struct { + CodeletDescriptor []*CodeletDescriptorRawConfig `yaml:"codelet_descriptor"` +} + +func newCodeletsetRawConfig(filePath string) (*CodeletsetRawConfig, error) { + f, err := NewFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + var rawConfig CodeletsetRawConfig + if err := yaml.Unmarshal(f.Data, &rawConfig); err != nil { + return nil, fmt.Errorf("failed to unmarshal file %s: %w", filePath, err) + } + + return &rawConfig, nil +} diff --git a/pkg/schema/client.go b/pkg/schema/client.go index 2aa7c92..be87c72 100644 --- a/pkg/schema/client.go +++ b/pkg/schema/client.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strings" "github.com/google/uuid" @@ -27,8 +28,13 @@ type Client struct { // NewClient creates a new Client func NewClient(ctx context.Context, logger *logrus.Logger, opts *Options) (*Client, error) { + ip := opts.ip + if len(ip) == 0 { + ip = "localhost" + } + return &Client{ - baseURL: fmt.Sprintf("%s://%s:%d", controlScheme, opts.ip, opts.port), + baseURL: fmt.Sprintf("%s://%s:%d", controlScheme, ip, opts.port), ctx: ctx, inner: &http.Client{}, logger: logger, @@ -88,7 +94,7 @@ func (c *Client) doDelete(relativePath string) error { return err } - if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices { + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { err := fmt.Errorf("unexpected status code: %d", resp.StatusCode) c.logger.WithField("body", buf.String()).WithError(err).Error("unexpected status code") return err @@ -155,7 +161,7 @@ func (c *Client) Unload(streamUUIDs []uuid.UUID) error { errs := make([]error, 0, len(streamUUIDs)) for _, streamUUID := range streamUUIDs { streamIDStr := base64.StdEncoding.EncodeToString(streamUUID[:]) - if err := c.doDelete(fmt.Sprintf("/stream?stream_uuid=%s", streamIDStr)); err != nil { + if err := c.doDelete(fmt.Sprintf("/stream?stream_uuid=%s", url.PathEscape(streamIDStr))); err != nil { err = fmt.Errorf("failed to delete stream ID association %s: %w", streamUUID.String(), err) errs = append(errs, err) continue diff --git a/pkg/schema/serve.go b/pkg/schema/serve.go index 7f2a37e..4747aba 100644 --- a/pkg/schema/serve.go +++ b/pkg/schema/serve.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/signal" "strings" @@ -64,24 +65,32 @@ func (s *Server) serveHTTP(ctx context.Context) error { } case http.MethodDelete: - streamUUIDStr := r.URL.Query().Get("streamUUID") - bs, err := base64.StdEncoding.DecodeString(streamUUIDStr) + streamUUIDStr := r.URL.Query().Get("stream_uuid") + unescapedStreamUUIDStr, err := url.PathUnescape(streamUUIDStr) + if err != nil { + s.logger.WithError(err).Error("failed to unescape stream_uuid") w.WriteHeader(http.StatusInternalServerError) return } - streamUUID, err := uuid.FromBytes(bs) + + bs, err := base64.StdEncoding.DecodeString(unescapedStreamUUIDStr) if err != nil { + s.logger.WithError(err).Errorf("failed to decode stream_uuid from %s", unescapedStreamUUIDStr) w.WriteHeader(http.StatusInternalServerError) return } - if err := s.DeleteStreamToSchemaAssociation(r.Context(), streamUUID); err != nil { + + streamUUID, err := uuid.FromBytes(bs) + if err != nil { + s.logger.WithError(err).Error("failed to parse stream id from bytes") w.WriteHeader(http.StatusInternalServerError) return - } else { - w.WriteHeader(http.StatusAccepted) } + s.DeleteStreamToSchemaAssociation(r.Context(), streamUUID) + w.WriteHeader(http.StatusAccepted) + default: w.WriteHeader(http.StatusMethodNotAllowed) } diff --git a/pkg/schema/server.go b/pkg/schema/server.go index f02db4e..25466d8 100644 --- a/pkg/schema/server.go +++ b/pkg/schema/server.go @@ -116,7 +116,7 @@ func (s *Server) AddStreamToSchemaAssociation(_ context.Context, req *AddSchemaA } // DeleteStreamToSchemaAssociation removes the association between a stream and a schema -func (s *Server) DeleteStreamToSchemaAssociation(_ context.Context, req uuid.UUID) error { +func (s *Server) DeleteStreamToSchemaAssociation(_ context.Context, req uuid.UUID) { l := s.logger.WithField("streamUUID", req.String()) if current, ok := s.store.streamToSchema[req]; !ok { @@ -128,6 +128,4 @@ func (s *Server) DeleteStreamToSchemaAssociation(_ context.Context, req uuid.UUI "protoPackage": current.ProtoPackage, }).Info("association removed") } - - return nil } From 7a93835bf94693afc5314a2f3c36e7295758aeb6 Mon Sep 17 00:00:00 2001 From: Connor Settle Date: Fri, 15 Nov 2024 12:29:35 +0000 Subject: [PATCH 8/8] PR comments --- README.md | 4 ++++ docs/design.md | 2 +- docs/jbpf_arch.png | Bin 30008 -> 24925 bytes 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7adb516..cf5327c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Prerequisites: * Make * Pip * Python3 +* Protocol Buffer Compiler (protoc) The project utilizes [Nanopb](https://github.com/nanopb/nanopb) to generate C structures for given protobuf specs that use contiguous memory. It also generates serializer libraries that can be provided to jbpf, to encode output and decode input data to seamlessly integrate external data processing systems. @@ -19,6 +20,9 @@ The project utilizes [Nanopb](https://github.com/nanopb/nanopb) to generate C st # init submodules: ./init_submodules.sh +# Install nanopb pip packages: +python3 -m pip install -r 3p/nanopb/requirements.txt + # source environment variables source ./setup_jbpfp_env.sh diff --git a/docs/design.md b/docs/design.md index b938b29..3fdef67 100644 --- a/docs/design.md +++ b/docs/design.md @@ -8,7 +8,7 @@ For complete details of each subcommand, see `./jbpf_protobuf_cli {SUBCOMMAND} - ## Serde -The `serde` subcommand generates assets from protobuf specs which can integrate with `jbpf`'s [serde functionality](../jbpf/docs/serde.md). +The `serde` subcommand generates assets from protobuf specs which can integrate with `jbpf`'s [serde functionality](https://github.com/microsoft/jbpf/blob/main/docs/serde.md). Developers must write `.proto` file(s) defining the models that are to be serialized. Additionally they must provide [generator options](https://jpa.kapsi.fi/nanopb/docs/reference.html#generator-options) as defined by nanopb to ensure generated structs can be defined in C as contiguous memory structs. diff --git a/docs/jbpf_arch.png b/docs/jbpf_arch.png index 72829c3ef35cd2d5bab7db75c1f0feca496b1909..345f44ec854a720565faec70dd382f0a2cdfbbc7 100755 GIT binary patch literal 24925 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 Lf0JRk@9e(;h8W#z