{"id":1576,"url":"https://patchwork.libcamera.org/api/patches/1576/?format=json","web_url":"https://patchwork.libcamera.org/patch/1576/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20190701220612.6342-3-niklas.soderlund@ragnatech.se>","date":"2019-07-01T22:06:12","name":"[libcamera-devel,v3,2/2] test: ipc: unix: Add test for IPCUnixSocket","commit_ref":null,"pull_url":null,"state":"accepted","archived":false,"hash":"7e55d6ff77864dc24ec4390ea3bad502e766aacd","submitter":{"id":5,"url":"https://patchwork.libcamera.org/api/people/5/?format=json","name":"Niklas Söderlund","email":"niklas.soderlund@ragnatech.se"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/1576/mbox/","series":[{"id":388,"url":"https://patchwork.libcamera.org/api/series/388/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=388","date":"2019-07-01T22:06:10","name":"libcamera: ipc: unix: Add a IPC mechanism based on Unix sockets","version":3,"mbox":"https://patchwork.libcamera.org/series/388/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/1576/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/1576/checks/","tags":{},"headers":{"Return-Path":"<niklas.soderlund@ragnatech.se>","Received":["from bin-mail-out-05.binero.net (bin-mail-out-05.binero.net\n\t[195.74.38.228])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 822D961E16\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue,  2 Jul 2019 00:06:59 +0200 (CEST)","from bismarck.berto.se (unknown [145.14.112.32])\n\tby bin-vsp-out-01.atm.binero.net (Halon) with ESMTPA\n\tid 7e5cf0c6-9c4c-11e9-8ab4-005056917a89;\n\tTue, 02 Jul 2019 00:06:37 +0200 (CEST)"],"X-Halon-ID":"7e5cf0c6-9c4c-11e9-8ab4-005056917a89","Authorized-sender":"niklas@soderlund.pp.se","From":"=?utf-8?q?Niklas_S=C3=B6derlund?= <niklas.soderlund@ragnatech.se>","To":"libcamera-devel@lists.libcamera.org","Date":"Tue,  2 Jul 2019 00:06:12 +0200","Message-Id":"<20190701220612.6342-3-niklas.soderlund@ragnatech.se>","X-Mailer":"git-send-email 2.21.0","In-Reply-To":"<20190701220612.6342-1-niklas.soderlund@ragnatech.se>","References":"<20190701220612.6342-1-niklas.soderlund@ragnatech.se>","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Subject":"[libcamera-devel] [PATCH v3 2/2] test: ipc: unix: Add test for\n\tIPCUnixSocket","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.23","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","X-List-Received-Date":"Mon, 01 Jul 2019 22:06:59 -0000"},"content":"Test that the IPC supports sending data and file descriptors over the\nIPC medium. To be able to execute the test two parts are needed, one\nto drive the test and act as the libcamera (master) and a one to act as\nthe IPA (slave).\n\nThe master drives the testing posting requests to the slave to process\nand sometimes respond to. A few different tests are performed.\n\n- Master sends an array to the slave which responds with a reversed copy\n  of the array. The master verifies that a reversed array is returned.\n\n- Master ties to sends an empty message making sure that the send call\n  fails.\n\n- Master sends a list of file descriptors and ask the slave to calculate\n  and respond with the sum of the size of the files. The master verifies\n  that the calculated size is correct.\n\n- Master sends a pre-computed size and a list of file descriptors and\n  asks the slave to verify that the pre-computed size matches the sum of\n  the size of the file descriptors.\n\n- Master sends two file descriptors and asks the salve to join the file\n  contents in a new file and respond with its file descriptor. The\n  master then verifies that the content of the returned file descriptor\n  matches the order of the original two files.\n\nSigned-off-by: Niklas Söderlund <niklas.soderlund@ragnatech.se>\n---\n test/ipc/meson.build    |  17 ++\n test/ipc/unixsocket.cpp | 510 ++++++++++++++++++++++++++++++++++++++++\n test/meson.build        |   1 +\n 3 files changed, 528 insertions(+)\n create mode 100644 test/ipc/meson.build\n create mode 100644 test/ipc/unixsocket.cpp","diff":"diff --git a/test/ipc/meson.build b/test/ipc/meson.build\nnew file mode 100644\nindex 0000000000000000..b557698d4b264ca5\n--- /dev/null\n+++ b/test/ipc/meson.build\n@@ -0,0 +1,17 @@\n+ipc_tests = [\n+    [ 'unixsocket',  'unixsocket.cpp' ],\n+]\n+\n+ipc_tests_dep = [\n+  cc.find_library('rt'),\n+  libcamera_dep,\n+]\n+\n+foreach t : ipc_tests\n+    exe = executable(t[0], t[1],\n+                     dependencies : ipc_tests_dep,\n+                     link_with : test_libraries,\n+                     include_directories : test_includes_internal)\n+\n+    test(t[0], exe, suite : 'ipc', is_parallel : false)\n+endforeach\ndiff --git a/test/ipc/unixsocket.cpp b/test/ipc/unixsocket.cpp\nnew file mode 100644\nindex 0000000000000000..d3e1d9227c537e66\n--- /dev/null\n+++ b/test/ipc/unixsocket.cpp\n@@ -0,0 +1,510 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/*\n+ * Copyright (C) 2019, Google Inc.\n+ *\n+ * unixsocket.cpp - Unix socket IPC test\n+ */\n+\n+#include <algorithm>\n+#include <atomic>\n+#include <fcntl.h>\n+#include <fcntl.h>\n+#include <iostream>\n+#include <string.h>\n+#include <sys/mman.h>\n+#include <sys/stat.h>\n+#include <sys/wait.h>\n+#include <unistd.h>\n+#include <string.h>\n+\n+#include <libcamera/camera_manager.h>\n+#include <libcamera/event_dispatcher.h>\n+#include <libcamera/timer.h>\n+\n+#include \"ipc_unixsocket.h\"\n+#include \"test.h\"\n+\n+#define CMD_CLOSE\t0\n+#define CMD_REVERSE\t1\n+#define CMD_LEN_CALC\t2\n+#define CMD_LEN_CMP\t3\n+#define CMD_JOIN\t4\n+\n+using namespace std;\n+using namespace libcamera;\n+\n+int calculateLength(int fd)\n+{\n+\tlseek(fd, 0, 0);\n+\tint size = lseek(fd, 0, SEEK_END);\n+\tlseek(fd, 0, 0);\n+\n+\treturn size;\n+}\n+\n+class UnixSocketTestSlave\n+{\n+public:\n+\tUnixSocketTestSlave()\n+\t\t: exitCode_(EXIT_FAILURE), exit_(false)\n+\t{\n+\t\tdispatcher_ = CameraManager::instance()->eventDispatcher();\n+\t\tipc_.readyRead.connect(this, &UnixSocketTestSlave::readyRead);\n+\t}\n+\n+\tint run(int fd)\n+\t{\n+\t\tif (ipc_.bind(fd)) {\n+\t\t\tcerr << \"Failed to connect to IPC channel\" << endl;\n+\t\t\treturn EXIT_FAILURE;\n+\t\t}\n+\n+\t\twhile (!exit_)\n+\t\t\tdispatcher_->processEvents();\n+\n+\t\tipc_.close();\n+\n+\t\treturn exitCode_;\n+\t}\n+\n+private:\n+\tvoid readyRead(IPCUnixSocket *ipc)\n+\t{\n+\t\tIPCUnixSocket::Payload message, response;\n+\t\tint ret;\n+\n+\t\tif (ipc->receive(&message)) {\n+\t\t\tcerr << \"Receive message failed\" << endl;\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tconst uint8_t cmd = message.data[0];\n+\n+\t\tswitch (cmd) {\n+\t\tcase CMD_CLOSE:\n+\t\t\tstop(0);\n+\t\t\tbreak;\n+\n+\t\tcase CMD_REVERSE: {\n+\t\t\tresponse.data = message.data;\n+\t\t\tstd::reverse(response.data.begin() + 1, response.data.end());\n+\n+\t\t\tret = ipc_.send(response);\n+\t\t\tif (ret < 0) {\n+\t\t\t\tcerr << \"Reverse fail\" << endl;\n+\t\t\t\tstop(ret);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase CMD_LEN_CALC: {\n+\t\t\tint size = 0;\n+\t\t\tfor (int fd : message.fds)\n+\t\t\t\tsize += calculateLength(fd);\n+\n+\t\t\tresponse.data.resize(1 + sizeof(size));\n+\t\t\tresponse.data[0] = cmd;\n+\t\t\tmemcpy(response.data.data() + 1, &size, sizeof(size));\n+\n+\t\t\tret = ipc_.send(response);\n+\t\t\tif (ret < 0) {\n+\t\t\t\tcerr << \"Calc fail\" << endl;\n+\t\t\t\tstop(ret);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase CMD_LEN_CMP: {\n+\t\t\tint size = 0;\n+\t\t\tfor (int fd : message.fds)\n+\t\t\t\tsize += calculateLength(fd);\n+\n+\t\t\tint cmp;\n+\t\t\tmemcpy(&cmp, message.data.data() + 1, sizeof(cmp));\n+\n+\t\t\tif (cmp != size) {\n+\t\t\t\tcerr << \"Compare fail\" << endl;\n+\t\t\t\tstop(-ERANGE);\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tcase CMD_JOIN: {\n+\t\t\tint outfd = shm_open(\"/libcamera.ipc.unixsocket.fdorder.out\",\n+\t\t\t\t\t     O_RDWR | O_CREAT | O_TRUNC,\n+\t\t\t\t\t     S_IRUSR | S_IWUSR);\n+\n+\t\t\tif (outfd < 0) {\n+\t\t\t\tcerr << \"Create out file fail\" << endl;\n+\t\t\t\tstop(outfd);\n+\t\t\t\treturn;\n+\t\t\t}\n+\n+\t\t\tfor (int fd : message.fds) {\n+\t\t\t\twhile (true) {\n+\t\t\t\t\tchar buf[32];\n+\t\t\t\t\tint num = read(fd, &buf, sizeof(buf));\n+\n+\t\t\t\t\tif (num < 0) {\n+\t\t\t\t\t\tcerr << \"Read fail\" << endl;\n+\t\t\t\t\t\tstop(-EIO);\n+\t\t\t\t\t\treturn;\n+\t\t\t\t\t} else if (!num)\n+\t\t\t\t\t\tbreak;\n+\n+\t\t\t\t\tif (write(outfd, buf, num) < 0) {\n+\t\t\t\t\t\tcerr << \"Write fail\" << endl;\n+\t\t\t\t\t\tstop(-EIO);\n+\t\t\t\t\t\treturn;\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\n+\t\t\t\tclose(fd);\n+\t\t\t}\n+\n+\t\t\tlseek(outfd, 0, 0);\n+\t\t\tresponse.data.push_back(CMD_JOIN);\n+\t\t\tresponse.fds.push_back(outfd);\n+\n+\t\t\tret = ipc_.send(response);\n+\t\t\tif (ret < 0) {\n+\t\t\t\tcerr << \"Join fail\" << endl;\n+\t\t\t\tstop(ret);\n+\t\t\t}\n+\n+\t\t\tclose(outfd);\n+\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tdefault:\n+\t\t\tcerr << \"Unknown command \" << cmd << endl;\n+\t\t\tstop(-EINVAL);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tvoid stop(int code)\n+\t{\n+\t\texitCode_ = code;\n+\t\texit_ = true;\n+\t}\n+\n+\tIPCUnixSocket ipc_;\n+\tEventDispatcher *dispatcher_;\n+\tint exitCode_;\n+\tbool exit_;\n+};\n+\n+class UnixSocketTest : public Test\n+{\n+protected:\n+\tint slaveStart(int fd)\n+\t{\n+\t\tpid_ = fork();\n+\n+\t\tif (pid_ == -1)\n+\t\t\treturn TestFail;\n+\n+\t\tif (!pid_) {\n+\t\t\tstd::string arg = std::to_string(fd);\n+\t\t\texecl(\"/proc/self/exe\", \"/proc/self/exe\",\n+\t\t\t      arg.c_str(), nullptr);\n+\n+\t\t\t/* Only get here if exec fails. */\n+\t\t\texit(TestFail);\n+\t\t}\n+\n+\t\treturn TestPass;\n+\t}\n+\n+\tint slaveStop()\n+\t{\n+\t\tint status;\n+\n+\t\tif (pid_ < 0)\n+\t\t\treturn TestFail;\n+\n+\t\tif (waitpid(pid_, &status, 0) < 0)\n+\t\t\treturn TestFail;\n+\n+\t\tif (!WIFEXITED(status) || WEXITSTATUS(status))\n+\t\t\treturn TestFail;\n+\n+\t\treturn TestPass;\n+\t}\n+\n+\tint testReverse()\n+\t{\n+\t\tIPCUnixSocket::Payload message, response;\n+\t\tint ret;\n+\n+\t\tmessage.data = { CMD_REVERSE, 1, 2, 3, 4, 5 };\n+\n+\t\tret = call(message, &response);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\tstd::reverse(response.data.begin() + 1, response.data.end());\n+\t\tif (message.data != response.data)\n+\t\t\treturn TestFail;\n+\n+\t\treturn 0;\n+\t}\n+\n+\tint testEmptyFail()\n+\t{\n+\t\tIPCUnixSocket::Payload message;\n+\n+\t\treturn ipc_.send(message) != -EINVAL;\n+\t}\n+\n+\tint testCalc()\n+\t{\n+\t\tIPCUnixSocket::Payload message, response;\n+\t\tint sizeOut, sizeIn, ret;\n+\n+\t\tsizeOut = prepareFDs(&message, 2);\n+\t\tif (sizeOut < 0)\n+\t\t\treturn sizeOut;\n+\n+\t\tmessage.data.push_back(CMD_LEN_CALC);\n+\n+\t\tret = call(message, &response);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\tmemcpy(&sizeIn, response.data.data() + 1, sizeof(sizeIn));\n+\t\tif (sizeOut != sizeIn)\n+\t\t\treturn TestFail;\n+\n+\t\treturn 0;\n+\t}\n+\n+\tint testCmp()\n+\t{\n+\t\tIPCUnixSocket::Payload message;\n+\t\tint size;\n+\n+\t\tsize = prepareFDs(&message, 7);\n+\t\tif (size < 0)\n+\t\t\treturn size;\n+\n+\t\tmessage.data.resize(1 + sizeof(size));\n+\t\tmessage.data[0] = CMD_LEN_CMP;\n+\t\tmemcpy(message.data.data() + 1, &size, sizeof(size));\n+\n+\t\tif (ipc_.send(message))\n+\t\t\treturn TestFail;\n+\n+\t\treturn 0;\n+\t}\n+\n+\tint testFdOrder()\n+\t{\n+\t\tIPCUnixSocket::Payload message, response;\n+\t\tint ret;\n+\n+\t\tstruct {\n+\t\t\tconst char *name;\n+\t\t\tconst char *string;\n+\t\t\tint fd;\n+\t\t} data[] = {\n+\t\t\t{ .name = \"/libcamera.ipc.unixsocket.fdorder.0\", .string = \"Foo\", .fd = -1 },\n+\t\t\t{ .name = \"/libcamera.ipc.unixsocket.fdorder.1\", .string = \"Bar\", .fd = -1 },\n+\t\t\t{ .name = nullptr, .string = nullptr, .fd = -1 },\n+\t\t};\n+\n+\t\tfor (unsigned int i = 0; data[i].name && data[i].string; i++) {\n+\t\t\tunsigned int len = strlen(data[i].string);\n+\n+\t\t\tdata[i].fd = shm_open(data[i].name,\n+\t\t\t\t\t      O_RDWR | O_CREAT | O_TRUNC,\n+\t\t\t\t\t      S_IRUSR | S_IWUSR);\n+\n+\t\t\tif (data[i].fd < 0)\n+\t\t\t\treturn TestFail;\n+\n+\t\t\tret = write(data[i].fd, data[i].string, len);\n+\t\t\tif (ret < 0)\n+\t\t\t\treturn TestFail;\n+\n+\t\t\tlseek(data[i].fd, 0, 0);\n+\t\t\tmessage.fds.push_back(data[i].fd);\n+\t\t}\n+\n+\t\tmessage.data.push_back(CMD_JOIN);\n+\n+\t\tret = call(message, &response);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\tfor (unsigned int i = 0; data[i].name && data[i].string; i++) {\n+\t\t\tunsigned int len = strlen(data[i].string);\n+\t\t\tchar buf[len];\n+\n+\t\t\tclose(data[i].fd);\n+\n+\t\t\tif (read(response.fds[0], &buf, len) <= 0)\n+\t\t\t\treturn TestFail;\n+\n+\t\t\tif (strncmp(buf, data[i].string, len))\n+\t\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\tclose(response.fds[0]);\n+\n+\t\treturn 0;\n+\t}\n+\n+\tint init()\n+\t{\n+\t\tcallResponse_ = nullptr;\n+\t\treturn 0;\n+\t}\n+\n+\tint run()\n+\t{\n+\t\tint slavefd = ipc_.create();\n+\t\tif (slavefd < 0)\n+\t\t\treturn TestFail;\n+\n+\t\tif (slaveStart(slavefd)) {\n+\t\t\tcerr << \"Failed to start slave\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\tipc_.readyRead.connect(this, &UnixSocketTest::readyRead);\n+\n+\t\t/* Test reversing a string, this test sending only data. */\n+\t\tif (testReverse()) {\n+\t\t\tcerr << \"Reveres array test failed\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test empty message fails. */\n+\t\tif (testEmptyFail()) {\n+\t\t\tcerr << \"empty test failed\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test offloading a calculation, this test sending only FDs. */\n+\t\tif (testCalc()) {\n+\t\t\tcerr << \"Calc test failed\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test fire and forget, this tests sending data and FDs. */\n+\t\tif (testCmp()) {\n+\t\t\tcerr << \"Cmp test failed\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Test order of file descriptors. */\n+\t\tif (testFdOrder()) {\n+\t\t\tcerr << \"fd order test failed\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\t/* Close slave connection. */\n+\t\tIPCUnixSocket::Payload close;\n+\t\tclose.data.push_back(CMD_CLOSE);\n+\t\tif (ipc_.send(close)) {\n+\t\t\tcerr << \"Closing IPC channel failed\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\tipc_.close();\n+\t\tif (slaveStop()) {\n+\t\t\tcerr << \"Failed to stop slave\" << endl;\n+\t\t\treturn TestFail;\n+\t\t}\n+\n+\t\treturn TestPass;\n+\t}\n+\n+private:\n+\tint call(const IPCUnixSocket::Payload &message, IPCUnixSocket::Payload *response)\n+\t{\n+\t\tTimer timeout;\n+\t\tint ret;\n+\n+\t\tcallDone_ = false;\n+\t\tcallResponse_ = response;\n+\n+\t\tret = ipc_.send(message);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\n+\t\ttimeout.start(200);\n+\t\twhile (!callDone_) {\n+\t\t\tif (!timeout.isRunning()) {\n+\t\t\t\tcerr << \"Call timeout!\" << endl;\n+\t\t\t\tcallResponse_ = nullptr;\n+\t\t\t\treturn -ETIMEDOUT;\n+\t\t\t}\n+\n+\t\t\tCameraManager::instance()->eventDispatcher()->processEvents();\n+\t\t}\n+\n+\t\tcallResponse_ = nullptr;\n+\n+\t\treturn 0;\n+\t}\n+\n+\tvoid readyRead(IPCUnixSocket *ipc)\n+\t{\n+\t\tif (!callResponse_) {\n+\t\t\tcerr << \"Read ready without expecting data, fail.\" << endl;\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tif (ipc->receive(callResponse_)) {\n+\t\t\tcerr << \"Receive message failed\" << endl;\n+\t\t\treturn;\n+\t\t}\n+\n+\t\tcallDone_ = true;\n+\t}\n+\n+\tint prepareFDs(IPCUnixSocket::Payload *message, unsigned int num)\n+\t{\n+\t\tint fd = open(\"/proc/self/exe\", O_RDONLY);\n+\t\tif (fd < 0)\n+\t\t\treturn fd;\n+\n+\t\tint size = 0;\n+\t\tfor (unsigned int i = 0; i < num; i++) {\n+\t\t\tint clone = dup(fd);\n+\t\t\tif (clone < 0)\n+\t\t\t\treturn clone;\n+\n+\t\t\tsize += calculateLength(clone);\n+\t\t\tmessage->fds.push_back(clone);\n+\t\t}\n+\n+\t\tclose(fd);\n+\n+\t\treturn size;\n+\t}\n+\n+\tpid_t pid_;\n+\tIPCUnixSocket ipc_;\n+\tbool callDone_;\n+\tIPCUnixSocket::Payload *callResponse_;\n+};\n+\n+/*\n+ * Can't use TEST_REGISTER() as single binary needs to act as both proxy\n+ * master and slave.\n+ */\n+int main(int argc, char **argv)\n+{\n+\tif (argc == 2) {\n+\t\tint ipcfd = std::stoi(argv[1]);\n+\t\tUnixSocketTestSlave slave;\n+\t\treturn slave.run(ipcfd);\n+\t}\n+\n+\treturn UnixSocketTest().execute();\n+}\ndiff --git a/test/meson.build b/test/meson.build\nindex c36ac24796367501..3666f6b2385bd4ca 100644\n--- a/test/meson.build\n+++ b/test/meson.build\n@@ -2,6 +2,7 @@ subdir('libtest')\n \n subdir('camera')\n subdir('ipa')\n+subdir('ipc')\n subdir('media_device')\n subdir('pipeline')\n subdir('stream')\n","prefixes":["libcamera-devel","v3","2/2"]}