[{"id":2229,"web_url":"https://patchwork.libcamera.org/comment/2229/","msgid":"<20190712063519.GD4831@pendragon.ideasonboard.com>","date":"2019-07-12T06:35:19","subject":"Re: [libcamera-devel] [PATCH v4 3/8] libcamera: Add Process and\n\tProcessManager classes","submitter":{"id":2,"url":"https://patchwork.libcamera.org/api/people/2/","name":"Laurent Pinchart","email":"laurent.pinchart@ideasonboard.com"},"content":"Hi Paul,\n\nThank you for the patch.\n\nOn Fri, Jul 12, 2019 at 03:50:42AM +0900, Paul Elder wrote:\n> Add a Process class to abstract a process, and a ProcessManager singleton\n> to monitor and manage the processes.\n> \n> Signed-off-by: Paul Elder <paul.elder@ideasonboard.com>\n> ---\n> Changes in v4:\n> - rename enum ExitStatus members\n> - use a running_ flag (instead of state_)\n> - fix sigaction registration (on Process construction)\n> - restore old signal handler (on Process destruction)\n> - replace closefrom_except() with closeAllFdsExcept()\n> - unsetenv LIBCAMERA_LOG_FILE on fork (before exec)\n> \n> Changes in v3:\n> - add Process test\n> - move ProcessManager header to process.cpp\n> - make Process final\n> - add a bunch of things for monitoring and signals on process\n>   termination\n> \n> New in v2\n> \n>  src/libcamera/include/process.h   |  55 +++++\n>  src/libcamera/meson.build         |   3 +\n>  src/libcamera/process.cpp         | 357 ++++++++++++++++++++++++++++++\n>  src/libcamera/process_manager.cpp |   0\n>  test/meson.build                  |   1 +\n>  test/process/meson.build          |  12 +\n>  test/process/process_test.cpp     | 100 +++++++++\n>  7 files changed, 528 insertions(+)\n>  create mode 100644 src/libcamera/include/process.h\n>  create mode 100644 src/libcamera/process.cpp\n>  create mode 100644 src/libcamera/process_manager.cpp\n>  create mode 100644 test/process/meson.build\n>  create mode 100644 test/process/process_test.cpp\n> \n> diff --git a/src/libcamera/include/process.h b/src/libcamera/include/process.h\n> new file mode 100644\n> index 0000000..d322fce\n> --- /dev/null\n> +++ b/src/libcamera/include/process.h\n> @@ -0,0 +1,55 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * process.h - Process object\n> + */\n> +#ifndef __LIBCAMERA_PROCESS_H__\n> +#define __LIBCAMERA_PROCESS_H__\n> +\n> +#include <string>\n> +#include <vector>\n> +\n> +#include <libcamera/event_notifier.h>\n> +\n> +namespace libcamera {\n> +\n> +class Process final\n> +{\n> +public:\n> +\tenum ExitStatus {\n> +\t\tNotExited,\n> +\t\tNormalExit,\n> +\t\tSignalExit,\n> +\t};\n> +\n> +\tProcess();\n> +\t~Process();\n> +\n> +\tint start(const std::string &path,\n> +\t\t  const std::vector<std::string> &args = std::vector<std::string>(),\n> +\t\t  const std::vector<int> &fds = std::vector<int>());\n> +\n> +\tExitStatus exitStatus() const { return exitStatus_; }\n> +\tint exitCode() const { return exitCode_; }\n> +\n> +\tvoid kill();\n> +\n> +\tSignal<Process *, enum ExitStatus, int> finished;\n> +\n> +private:\n> +\tvoid closeAllFdsExcept(const std::vector<int> &fds);\n> +\tint isolate();\n> +\tvoid died(int wstatus);\n> +\n> +\tpid_t pid_;\n> +\tbool running_;\n> +\tenum ExitStatus exitStatus_;\n> +\tint exitCode_;\n> +\n> +\tfriend class ProcessManager;\n> +};\n> +\n> +} /* namespace libcamera */\n> +\n> +#endif /* __LIBCAMERA_PROCESS_H__ */\n> diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build\n> index eda506b..01565c1 100644\n> --- a/src/libcamera/meson.build\n> +++ b/src/libcamera/meson.build\n> @@ -21,6 +21,8 @@ libcamera_sources = files([\n>      'message.cpp',\n>      'object.cpp',\n>      'pipeline_handler.cpp',\n> +    'process.cpp',\n> +    'process_manager.cpp',\n>      'request.cpp',\n>      'signal.cpp',\n>      'stream.cpp',\n> @@ -48,6 +50,7 @@ libcamera_headers = files([\n>      'include/media_object.h',\n>      'include/message.h',\n>      'include/pipeline_handler.h',\n> +    'include/process.h',\n>      'include/thread.h',\n>      'include/utils.h',\n>      'include/v4l2_device.h',\n> diff --git a/src/libcamera/process.cpp b/src/libcamera/process.cpp\n> new file mode 100644\n> index 0000000..736d59f\n> --- /dev/null\n> +++ b/src/libcamera/process.cpp\n> @@ -0,0 +1,357 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * process.cpp - Process object\n> + */\n> +\n> +#include \"process.h\"\n> +\n> +#include <algorithm>\n> +#include <dirent.h>\n> +#include <fcntl.h>\n> +#include <iostream>\n> +#include <list>\n> +#include <signal.h>\n> +#include <string.h>\n> +#include <sys/socket.h>\n> +#include <sys/types.h>\n> +#include <sys/wait.h>\n> +#include <unistd.h>\n> +#include <vector>\n> +\n> +#include <libcamera/event_notifier.h>\n> +\n> +#include \"log.h\"\n> +#include \"utils.h\"\n> +\n> +/**\n> + * \\file process.h\n> + * \\brief Process object\n> + */\n> +\n> +namespace libcamera {\n> +\n> +LOG_DEFINE_CATEGORY(Process)\n> +\n> +/**\n> + * \\class ProcessManager\n> + * \\brief Manager of processes\n> + *\n> + * The ProcessManager singleton keeps track of all created Process instances,\n> + * and manages the signal handling involved in terminating processes.\n> + */\n> +class ProcessManager\n> +{\n> +public:\n> +\tvoid registerProcess(Process *proc);\n> +\n> +\tstatic ProcessManager *instance();\n> +\n> +\tint writePipe() const;\n> +\n> +\tconst struct sigaction &oldsa() const;\n> +\n> +private:\n> +\tvoid sighandler(EventNotifier *notifier);\n> +\tProcessManager();\n> +\t~ProcessManager();\n> +\n> +\tstd::list<Process *> processes_;\n> +\n> +\tstruct sigaction oldsa_;\n> +\tEventNotifier *sigEvent_;\n> +\tint pipe_[2];\n> +};\n> +\n> +namespace {\n> +\n> +void sigact(int signal, siginfo_t *info, void *ucontext)\n> +{\n> +\tchar data = 0;\n> +\twrite(ProcessManager::instance()->writePipe(), &data, sizeof(data));\n> +\n> +\tconst struct sigaction &oldsa = ProcessManager::instance()->oldsa();\n> +\tif (oldsa.sa_flags & SA_SIGINFO) {\n> +\t\toldsa.sa_sigaction(signal, info, ucontext);\n> +\t} else {\n> +\t\tif (oldsa.sa_handler != SIG_IGN && oldsa.sa_handler != SIG_DFL)\n> +\t\t\toldsa.sa_handler(signal);\n> +\t}\n> +}\n> +\n> +} /* namespace */\n> +\n> +void ProcessManager::sighandler(EventNotifier *notifier)\n> +{\n> +\tchar data;\n> +\tread(pipe_[0], &data, sizeof(data));\n> +\n> +\tfor (auto it = processes_.begin(); it != processes_.end(); ) {\n> +\t\tProcess *process = *it;\n> +\n> +\t\tint wstatus;\n> +\t\tpid_t pid = waitpid(process->pid_, &wstatus, WNOHANG);\n> +\t\tif (process->pid_ != pid) {\n> +\t\t\t++it;\n> +\t\t\tcontinue;\n> +\t\t}\n> +\n> +\t\tit = processes_.erase(it);\n> +\t\tprocess->died(wstatus);\n> +\t}\n> +}\n> +\n> +/**\n> + * \\brief Register process with process manager\n> + * \\param[in] proc Process to register\n> + *\n> + * This method registers the \\a proc with the process manager. It\n> + * shall be called by the parent process after successfully forking, in\n> + * order to let the parent signal process termination.\n> + */\n> +void ProcessManager::registerProcess(Process *proc)\n> +{\n> +\tprocesses_.push_back(proc);\n> +}\n> +\n> +ProcessManager::ProcessManager()\n> +{\n> +\tsigaction(SIGCHLD, NULL, &oldsa_);\n> +\n> +\tstruct sigaction sa;\n> +\tmemset(&sa, 0, sizeof(sa));\n> +\tsa.sa_sigaction = &sigact;\n> +\tmemcpy(&sa.sa_mask, &oldsa_.sa_mask, sizeof(sa.sa_mask));\n> +\tsigaddset(&sa.sa_mask, SIGCHLD);\n> +\tsa.sa_flags = oldsa_.sa_flags | SA_SIGINFO;\n> +\n> +\tsigaction(SIGCHLD, &sa, NULL);\n> +\n> +\tpipe2(pipe_, O_CLOEXEC | O_DIRECT | O_NONBLOCK);\n> +\tsigEvent_ = new EventNotifier(pipe_[0], EventNotifier::Read);\n> +\tsigEvent_->activated.connect(this, &ProcessManager::sighandler);\n> +}\n> +\n> +ProcessManager::~ProcessManager()\n> +{\n> +\tdelete sigEvent_;\n> +\tclose(pipe_[0]);\n> +\tclose(pipe_[1]);\n> +\tsigaction(SIGCHLD, &oldsa_, NULL);\n> +}\n> +\n> +/**\n> + * \\brief Retrieve the Process manager instance\n> + *\n> + * The ProcessManager is a singleton and can't be constructed manually. This\n> + * method shall instead be used to retrieve the single global instance of the\n> + * manager.\n> + *\n> + * \\return The Process manager instance\n> + */\n> +ProcessManager *ProcessManager::instance()\n> +{\n> +\tstatic ProcessManager processManager;\n> +\treturn &processManager;\n> +}\n> +\n> +/**\n> + * \\brief Retrieve the Process manager's write pipe\n> + *\n> + * This method is meant only to be used by the static signal handler.\n> + *\n> + * \\return Pipe for writing\n> + */\n> +int ProcessManager::writePipe() const\n> +{\n> +\treturn pipe_[1];\n> +}\n> +\n> +/**\n> + * \\brief Retrive the old signal action data\n> + *\n> + * This method is meant only to be used by the static signal handler.\n> + *\n> + * \\return The old signal action data\n> + */\n> +const struct sigaction &ProcessManager::oldsa() const\n> +{\n> +\treturn oldsa_;\n> +}\n> +\n> +\n> +/**\n> + * \\class Process\n> + * \\brief Process object\n> + *\n> + * The Process class models a process, and simplifies spawning new processes\n> + * and monitoring the exiting of a process.\n> + */\n> +\n> +/**\n> + * \\enum Process::ExitStatus\n> + * \\brief Exit status of process\n> + * \\var Process::NotExited\n> + * The process hasn't exited yet\n> + * \\var Process::NormalExit\n> + * The process exited normally, either via exit() or returning from main\n> + * \\var Process::SignalExit\n> + * The process was terminated by a signal (this includes crashing)\n> + */\n> +\n> +Process::Process()\n> +\t: pid_(-1), running_(false), exitStatus_(NotExited), exitCode_(0)\n> +{\n> +}\n> +\n> +Process::~Process()\n> +{\n> +\tkill();\n> +\t/* \\todo wait for child process to exit */\n> +}\n> +\n> +/**\n> + * \\brief Fork and exec a process, and close fds\n> + * \\param[in] path Path to executable\n> + * \\param[in] args Arguments to pass to executable (optional)\n> + * \\param[in] fds Vector of file descriptors to keep open (optional)\n> + *\n> + * Fork a process, and exec the executable specified by path. Prior to\n> + * exec'ing, but after forking, all file descriptors except for those\n> + * specified in fds will be closed.\n> + *\n> + * All indexes of args will be incremented by 1 before being fed to exec(),\n> + * so args[0] should not need to be equal to path.\n> + *\n> + * \\return zero on successful fork, exec, and closing the file descriptors,\n> + * or a negative error code otherwise\n> + */\n> +int Process::start(const std::string &path,\n> +\t\t   const std::vector<std::string> &args,\n> +\t\t   const std::vector<int> &fds)\n> +{\n> +\tint ret;\n> +\n> +\tif (running_ == true)\n\nYou can drop == true.\n\n> +\t\treturn 0;\n> +\n> +\tint childPid = fork();\n> +\tif (childPid == -1) {\n> +\t\tret = -errno;\n> +\t\tLOG(Process, Error) << \"Failed to fork: \" << strerror(-ret);\n> +\t\treturn ret;\n> +\t} else if (childPid) {\n> +\t\tpid_ = childPid;\n> +\t\tProcessManager::instance()->registerProcess(this);\n> +\n> +\t\trunning_ = true;\n> +\n> +\t\treturn 0;\n> +\t} else {\n> +\t\tif (isolate())\n> +\t\t\t_exit(EXIT_FAILURE);\n> +\n> +\t\tcloseAllFdsExcept(fds);\n> +\n> +\t\tunsetenv(\"LIBCAMERA_LOG_FILE\");\n> +\n> +\t\tconst char **argv = new const char *[args.size() + 2];\n> +\t\tunsigned int len = args.size();\n> +\t\targv[0] = path.c_str();\n> +\t\tfor (unsigned int i = 0; i < len; i++)\n> +\t\t\targv[i+1] = args[i].c_str();\n> +\t\targv[len+1] = nullptr;\n> +\n> +\t\texecv(path.c_str(), (char **)argv);\n> +\n> +\t\texit(EXIT_FAILURE);\n> +\t}\n> +}\n> +\n> +void Process::closeAllFdsExcept(const std::vector<int> &fds)\n> +{\n> +\tstd::vector<int> v(fds);\n> +\tsort(v.begin(), v.end());\n> +\n> +\tDIR *dir = opendir(\"/proc/self/fd\");\n> +\tif (!dir)\n> +\t\treturn;\n> +\n> +\tint dfd = dirfd(dir);\n> +\n> +\tstruct dirent *ent;\n> +\twhile ((ent = readdir(dir)) != nullptr) {\n> +\t\tchar *endp;\n> +\t\tint fd = strtoul(ent->d_name, &endp, 10);\n> +\t\tif (*endp)\n> +\t\t\tcontinue;\n> +\n> +\t\tif (fd >= 0 && fd != dfd &&\n> +\t\t    !std::binary_search(v.begin(), v.end(), fd))\n> +\t\t\tclose(fd);\n> +\t}\n> +\n> +\tclosedir(dir);\n> +}\n> +\n> +int Process::isolate()\n> +{\n> +\treturn unshare(CLONE_NEWUSER | CLONE_NEWNET);\n> +}\n> +\n> +/**\n> + * \\brief SIGCHLD handler\n> + * \\param[in] wstatus The status as output by waitpid()\n> + *\n> + * This method is called when the process associated with Process terminates.\n> + * It emits the Process::finished signal.\n> + */\n> +void Process::died(int wstatus)\n> +{\n> +\trunning_ = false;\n> +\texitStatus_ = WIFEXITED(wstatus) ? NormalExit : SignalExit;\n> +\texitCode_ = exitStatus_ == NormalExit ? WEXITSTATUS(wstatus) : -1;\n> +\n> +\tfinished.emit(this, exitStatus_, exitCode_);\n> +}\n> +\n> +/**\n> + * \\fn Process::exitStatus()\n> + * \\brief Retrieve the exit status of the process\n> + *\n> + * Return the exit status of the process, that is, whether the process\n> + * has exited via exit() or returning from main, or if the process was\n> + * terminated by a signal.\n> + *\n> + * \\sa ExitStatus\n> + *\n> + * \\return The process exit status\n> + */\n> +\n> +/**\n> + * \\fn Process::exitCode()\n> + * \\brief Retrieve the exit code of the process\n> + *\n> + * This method is only valid if exitStatus() returned NormalExit.\n> + *\n> + * \\return Exit code\n> + */\n> +\n> +/**\n> + * \\var Process::finished\n> + *\n> + * Signal that is emitted when the process is confirmed to have terminated.\n> + */\n> +\n> +/**\n> + * \\brief Kill the process\n> + *\n> + * Sends SIGKILL to the process.\n> + */\n> +void Process::kill()\n> +{\n> +\t::kill(pid_, SIGKILL);\n> +}\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/libcamera/process_manager.cpp b/src/libcamera/process_manager.cpp\n> new file mode 100644\n> index 0000000..e69de29\n> diff --git a/test/meson.build b/test/meson.build\n> index d308ac9..ad1a2f2 100644\n> --- a/test/meson.build\n> +++ b/test/meson.build\n> @@ -6,6 +6,7 @@ subdir('ipa')\n>  subdir('ipc')\n>  subdir('media_device')\n>  subdir('pipeline')\n> +subdir('process')\n>  subdir('stream')\n>  subdir('v4l2_subdevice')\n>  subdir('v4l2_videodevice')\n> diff --git a/test/process/meson.build b/test/process/meson.build\n> new file mode 100644\n> index 0000000..c4d83d6\n> --- /dev/null\n> +++ b/test/process/meson.build\n> @@ -0,0 +1,12 @@\n> +process_tests = [\n> +    [ 'process_test',  'process_test.cpp' ],\n> +]\n> +\n> +foreach t : process_tests\n> +    exe = executable(t[0], t[1],\n> +                     dependencies : libcamera_dep,\n> +                     link_with : test_libraries,\n> +                     include_directories : test_includes_internal)\n> +\n> +    test(t[0], exe, suite : 'process', is_parallel : false)\n> +endforeach\n> diff --git a/test/process/process_test.cpp b/test/process/process_test.cpp\n> new file mode 100644\n> index 0000000..8a7d09f\n> --- /dev/null\n> +++ b/test/process/process_test.cpp\n> @@ -0,0 +1,100 @@\n> +/* SPDX-License-Identifier: GPL-2.0-or-later */\n> +/*\n> + * Copyright (C) 2019, Google Inc.\n> + *\n> + * process_test.cpp - Process test\n> + */\n> +\n> +#include <iostream>\n> +#include <unistd.h>\n> +#include <vector>\n> +\n> +#include <libcamera/camera_manager.h>\n> +#include <libcamera/event_dispatcher.h>\n> +#include <libcamera/timer.h>\n> +\n> +#include \"process.h\"\n> +#include \"test.h\"\n> +#include \"utils.h\"\n> +\n> +using namespace std;\n> +using namespace libcamera;\n> +\n> +class ProcessTestChild\n> +{\n> +public:\n> +\tint run(int status)\n> +\t{\n> +\t\tusleep(50000);\n> +\n> +\t\treturn status;\n> +\t}\n> +};\n> +\n> +class ProcessTest : public Test\n> +{\n> +public:\n> +\tProcessTest()\n> +\t{\n> +\t}\n> +\n> +protected:\n> +\tint run()\n> +\t{\n> +\t\tEventDispatcher *dispatcher = CameraManager::instance()->eventDispatcher();\n> +\t\tTimer timeout;\n> +\n> +\t\tint exitCode = 42;\n> +\t\tvector<std::string> args;\n> +\t\targs.push_back(to_string(exitCode));\n> +\t\tint ret = proc_.start(\"/proc/self/exe\", args);\n> +\t\tif (ret) {\n> +\t\t\tcerr << \"failed to fork\" << endl;\n\n\"failed to start process\"\n\nReviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>\n\n> +\t\t\treturn TestFail;\n> +\t\t}\n> +\t\tproc_.finished.connect(this, &ProcessTest::procFinished);\n> +\n> +\t\ttimeout.start(80);\n> +\t\twhile (timeout.isRunning())\n> +\t\t\tdispatcher->processEvents();\n> +\n> +\t\tif (exitStatus_ != Process::NormalExit) {\n> +\t\t\tcerr << \"process did not exit normally\" << endl;\n> +\t\t\treturn TestFail;\n> +\t\t}\n> +\n> +\t\tif (exitCode != exitCode_) {\n> +\t\t\tcerr << \"exit code should be \" << exitCode\n> +\t\t\t     << \", actual is \" << exitCode_ << endl;\n> +\t\t\treturn TestFail;\n> +\t\t}\n> +\n> +\t\treturn TestPass;\n> +\t}\n> +\n> +private:\n> +\tvoid procFinished(Process *proc, enum Process::ExitStatus exitStatus, int exitCode)\n> +\t{\n> +\t\texitStatus_ = exitStatus;\n> +\t\texitCode_ = exitCode;\n> +\t}\n> +\n> +\tProcess proc_;\n> +\tenum Process::ExitStatus exitStatus_;\n> +\tint exitCode_;\n> +};\n> +\n> +/*\n> + * Can't use TEST_REGISTER() as single binary needs to act as both\n> + * parent and child processes.\n> + */\n> +int main(int argc, char **argv)\n> +{\n> +\tif (argc == 2) {\n> +\t\tint status = std::stoi(argv[1]);\n> +\t\tProcessTestChild child;\n> +\t\treturn child.run(status);\n> +\t}\n> +\n> +\treturn ProcessTest().execute();\n> +}","headers":{"Return-Path":"<laurent.pinchart@ideasonboard.com>","Received":["from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 488D260BC8\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 12 Jul 2019 08:35:48 +0200 (CEST)","from pendragon.ideasonboard.com (softbank126209254147.bbtec.net\n\t[126.209.254.147])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 63CA52B2;\n\tFri, 12 Jul 2019 08:35:45 +0200 (CEST)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1562913347;\n\tbh=5biuT3iJX+sToK07wt4pIsCts3IwMYu6DiJ8W8HrOeE=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=ZhFvDIVfBpXFu3usLKUj9JdK3gXvbMuJV63//cc5VeexQ0KOhn4IQjQJ/exzfltNe\n\tv1T6wH+zA3c1gMlkeISikXNsWSHwGz3ENEEmVKiy18Ctzbiu745DPVF+pTF34utwcN\n\tisdtuad2dhYvhe55wkwiouTauKLccxPOQcyRuWpQ=","Date":"Fri, 12 Jul 2019 09:35:19 +0300","From":"Laurent Pinchart <laurent.pinchart@ideasonboard.com>","To":"Paul Elder <paul.elder@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Message-ID":"<20190712063519.GD4831@pendragon.ideasonboard.com>","References":"<20190711185047.11671-1-paul.elder@ideasonboard.com>\n\t<20190711185047.11671-4-paul.elder@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20190711185047.11671-4-paul.elder@ideasonboard.com>","User-Agent":"Mutt/1.10.1 (2018-07-13)","Subject":"Re: [libcamera-devel] [PATCH v4 3/8] libcamera: Add Process and\n\tProcessManager classes","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":"Fri, 12 Jul 2019 06:35:48 -0000"}}]