google-usb-network: Stage dynamic support
This adds a daemon that reads the entity manager data dynamically and
creates devices as they are presented.
Change-Id: I69021a0baf156f578a03434e4783cae8e8dc9d51
Signed-off-by: William A. Kennington III <wak@google.com>
diff --git a/recipes-google/networking/google-usb-network/google-usb-dynamic.cpp b/recipes-google/networking/google-usb-network/google-usb-dynamic.cpp
new file mode 100644
index 0000000..2d83e00
--- /dev/null
+++ b/recipes-google/networking/google-usb-network/google-usb-dynamic.cpp
@@ -0,0 +1,369 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <fmt/format.h>
+#include <systemd/sd-daemon.h>
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/source/signal.hpp>
+#include <stdplus/signal.hpp>
+
+#include <memory>
+#include <span>
+#include <stdexcept>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <variant>
+
+namespace google
+{
+namespace usb_network
+{
+
+constexpr char kInterface[] = "com.google.gbmc.USB";
+
+using sdeventplus::source::Signal;
+using USBProps =
+ std::unordered_map<std::string,
+ std::variant<std::monostate, std::string, uint64_t>>;
+
+void execute(std::span<std::string> args)
+{
+ fmt::print(stderr, "Executing: usb_network.sh `{}`\n",
+ fmt::join(args, "` `"));
+ pid_t pid = fork();
+ if (pid != 0)
+ {
+ int status;
+ waitpid(pid, &status, 0);
+ if (status != 0)
+ {
+ throw std::runtime_error(
+ fmt::format("Execution failed: {}", status));
+ }
+ return;
+ }
+ std::vector<char*> cptr;
+ cptr.reserve(args.size() + 3);
+ char arg0[] = "usb_network.sh";
+ cptr.push_back(arg0);
+ for (auto& a : args)
+ {
+ cptr.push_back(a.data());
+ }
+ cptr.push_back(nullptr);
+ exit(execv("/usr/bin/usb_network.sh", cptr.data()));
+}
+
+class DeviceManager
+{
+ public:
+ DeviceManager(sdbusplus::bus_t& bus) : bus(bus)
+ {}
+ ~DeviceManager();
+
+ void handleInterfacesAdded(sdbusplus::message_t& m);
+ void handleInterfacesRemoved(sdbusplus::message_t& m);
+ void populate();
+
+ private:
+ std::reference_wrapper<sdbusplus::bus_t> bus;
+ struct Device
+ {
+ size_t idx;
+ std::vector<std::string> args;
+ };
+ std::vector<std::unique_ptr<Device>> devices;
+ std::unordered_map<std::string, std::reference_wrapper<Device>> device_map;
+
+ void addDev(const char* obj, USBProps& props);
+ void readDev(const char* svc, const char* obj, const char* intf);
+};
+
+DeviceManager::~DeviceManager()
+{
+ for (auto& dev : devices)
+ {
+ if (dev)
+ {
+ dev->args.push_back("stop");
+ try
+ {
+ execute(dev->args);
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(stderr, "Cleanup: {}\n", e.what());
+ }
+ }
+ }
+}
+
+template <typename>
+inline constexpr bool always_false_v = false;
+
+void DeviceManager::addDev(const char* obj, USBProps& props)
+{
+ size_t idx = 0;
+ auto it = device_map.find(obj);
+ if (it != device_map.end())
+ {
+ idx = it->second.get().idx;
+ }
+ else
+ {
+ for (; idx < devices.size(); ++idx)
+ {
+ if (!devices[idx])
+ {
+ break;
+ }
+ }
+ }
+ auto device = std::make_unique<Device>(idx);
+ auto& args = device->args;
+
+ auto add = [&](const char* arg, const char* key, bool required) {
+ auto pn = props.find(key);
+ if (pn != props.end())
+ {
+ args.push_back(arg);
+ std::visit(
+ [&](auto&& arg) {
+ using T = std::decay_t<decltype(arg)>;
+ if constexpr (std::is_arithmetic_v<T>)
+ {
+ args.push_back(fmt::format("{}", arg));
+ }
+ else if constexpr (std::is_same_v<T, std::string>)
+ {
+ args.push_back(std::move(arg));
+ }
+ else if constexpr (std::is_same_v<T, std::monostate>)
+ {
+ throw std::runtime_error(fmt::format(
+ "Obj {} unrecognized type for {}", obj, key));
+ }
+ else
+ {
+ static_assert(always_false_v<T>, "Invalid type");
+ }
+ },
+ pn->second);
+ }
+ else if (required)
+ {
+ throw std::runtime_error(
+ fmt::format("Obj {} missing param {}", obj, key));
+ }
+ };
+ add("--product-id", "ProductId", /*required=*/true);
+ add("--bind-device", "BindDevice", /*required=*/true);
+ add("--product-name", "ProductName", /*required=*/false);
+ add("--dev-type", "DevType", /*required=*/false);
+
+ auto pn = props.find("IFName");
+ std::string ifname;
+ if (pn != props.end())
+ {
+ ifname = std::move(std::get<std::string>(pn->second));
+ }
+ else
+ {
+ ifname = fmt::format("gusbem{}", idx);
+ }
+ args.push_back("--iface-name");
+ args.push_back(ifname);
+
+ if (it != device_map.end())
+ {
+ if (it->second.get().args == args)
+ {
+ fmt::print(stderr, "Device config {} duplicate, ignoring\n", obj);
+ return;
+ }
+ fmt::print(stderr, "Replacing interface {}\n", obj);
+ it->second.get().args.push_back("stop");
+ execute(it->second.get().args);
+ }
+ else
+ {
+ fmt::print(stderr, "Adding interface {}\n", obj);
+ }
+ execute(args);
+
+ if (idx >= devices.size())
+ {
+ devices.resize(devices.size() * 2 + 7);
+ }
+ devices[idx] = std::move(device);
+ if (it != device_map.end())
+ {
+ it->second = *devices[idx];
+ }
+ else
+ {
+ device_map.emplace(obj, *devices[idx]);
+ }
+}
+
+void DeviceManager::handleInterfacesAdded(sdbusplus::message_t& m)
+{
+ sdbusplus::message::object_path path;
+ std::unordered_map<std::string, USBProps> ip;
+ m.read(path, ip);
+
+ auto it = ip.find(kInterface);
+ if (it == ip.end())
+ {
+ return;
+ }
+ addDev(path.str.c_str(), it->second);
+}
+
+void DeviceManager::handleInterfacesRemoved(sdbusplus::message_t& m)
+{
+ sdbusplus::message::object_path path;
+ std::unordered_set<std::string> ip;
+ m.read(path, ip);
+
+ if (!ip.contains(kInterface))
+ {
+ return;
+ }
+
+ auto it = device_map.find(path.str);
+ if (it == device_map.end())
+ {
+ return;
+ }
+ auto dev = std::move(devices[it->second.get().idx]);
+ device_map.erase(it);
+
+ dev->args.push_back("stop");
+ execute(dev->args);
+}
+
+void DeviceManager::readDev(const char* svc, const char* obj, const char* intf)
+{
+ USBProps props;
+ auto req = bus.get().new_method_call(
+ svc, obj, "org.freedesktop.DBus.Properties", "GetAll");
+ req.append(intf);
+ req.call().read(props);
+ addDev(obj, props);
+}
+
+void DeviceManager::populate()
+{
+ std::unordered_map<
+ std::string, std::unordered_map<std::string, std::vector<std::string>>>
+ subtree;
+ auto req = bus.get().new_method_call("xyz.openbmc_project.ObjectMapper",
+ "/xyz/openbmc_project/object_mapper",
+ "xyz.openbmc_project.ObjectMapper",
+ "GetSubTree");
+ req.append("/", int32_t{0}, std::vector<std::string>{kInterface});
+ req.call().read(subtree);
+ for (const auto& [obj, svcm] : subtree)
+ {
+ for (const auto& [svc, intfs] : svcm)
+ {
+ for (const auto& intf : intfs)
+ {
+ if (intf != kInterface)
+ {
+ continue;
+ }
+ try
+ {
+ readDev(svc.c_str(), obj.c_str(), intf.c_str());
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(stderr, "Init {}: {}\n", obj, e.what());
+ }
+ }
+ }
+ }
+}
+
+int execute()
+{
+ // Set up our DBus and event loop
+ auto event = sdeventplus::Event::get_default();
+ auto bus = sdbusplus::bus::new_default();
+ bus.attach_event(event.get(), SD_EVENT_PRIORITY_NORMAL);
+
+ DeviceManager devm(bus);
+
+ // Configure basic signal handling
+ auto exit_handler = [&event](Signal&, const struct signalfd_siginfo*) {
+ fmt::print(stderr, "Interrupted, Exiting\n");
+ event.exit(0);
+ };
+ stdplus::signal::block(SIGINT);
+ Signal sig_int(event, SIGINT, exit_handler);
+ stdplus::signal::block(SIGTERM);
+ Signal sig_term(event, SIGTERM, exit_handler);
+
+ sdbusplus::bus::match_t addedMatch(
+ bus, sdbusplus::bus::match::rules::interfacesAdded(),
+ [&devm](sdbusplus::message_t& m) {
+ try
+ {
+ devm.handleInterfacesAdded(m);
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(stderr, "Add handler: {}\n", e.what());
+ }
+ });
+ sdbusplus::bus::match_t removedMatch(
+ bus, sdbusplus::bus::match::rules::interfacesRemoved(),
+ [&devm](sdbusplus::message_t& m) {
+ try
+ {
+ devm.handleInterfacesRemoved(m);
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(stderr, "Removed handler: {}\n", e.what());
+ }
+ });
+
+ devm.populate();
+
+ sd_notify(0, "READY=1");
+ return event.loop();
+}
+
+} // namespace usb_network
+} // namespace google
+
+int main()
+{
+ try
+ {
+ return google::usb_network::execute();
+ }
+ catch (const std::exception& e)
+ {
+ fmt::print(stderr, "FAILED: {}\n", e.what());
+ return 1;
+ }
+}
diff --git a/recipes-google/networking/google-usb-network/google-usb-dynamic.service b/recipes-google/networking/google-usb-network/google-usb-dynamic.service
new file mode 100644
index 0000000..93aea10
--- /dev/null
+++ b/recipes-google/networking/google-usb-network/google-usb-dynamic.service
@@ -0,0 +1,10 @@
+[Unit]
+Requires=xyz.openbmc_project.ObjectMapper.service
+After=xyz.openbmc_project.ObjectMapper.service
+
+[Service]
+Type=notify
+ExecStart=/usr/libexec/google-usb-dynamic
+
+[Install]
+WantedBy=multi-user.target
diff --git a/recipes-google/networking/google-usb-network/meson.build b/recipes-google/networking/google-usb-network/meson.build
new file mode 100644
index 0000000..f3d294d
--- /dev/null
+++ b/recipes-google/networking/google-usb-network/meson.build
@@ -0,0 +1,53 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+project(
+ 'google-usb-network',
+ 'cpp',
+ version: '0.1',
+ meson_version: '>=0.57.0',
+ default_options: [
+ 'warning_level=3',
+ 'cpp_std=c++20',
+ ])
+
+fmt_dep = dependency('fmt', required: false)
+if not fmt_dep.found()
+ fmt_opts = import('cmake').subproject_options()
+ fmt_opts.add_cmake_defines({
+ 'CMAKE_POSITION_INDEPENDENT_CODE': 'ON',
+ 'MASTER_PROJECT': 'OFF',
+ })
+ fmt_proj = import('cmake').subproject(
+ 'fmt',
+ options: fmt_opts,
+ required: false)
+ assert(fmt_proj.found(), 'fmtlib is required')
+ fmt_dep = fmt_proj.dependency('fmt')
+endif
+
+executable(
+ 'google-usb-dynamic',
+ 'google-usb-dynamic.cpp',
+ implicit_include_directories: false,
+ dependencies: [
+ fmt_dep,
+ dependency('libsystemd'),
+ dependency('sdbusplus'),
+ dependency('sdeventplus'),
+ dependency('stdplus'),
+ ],
+ install: true,
+ install_dir: get_option('libexecdir'))
+
diff --git a/recipes-google/networking/google-usb-network_%.bbappend b/recipes-google/networking/google-usb-network_%.bbappend
new file mode 100644
index 0000000..701604f
--- /dev/null
+++ b/recipes-google/networking/google-usb-network_%.bbappend
@@ -0,0 +1,30 @@
+FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"
+
+inherit meson pkgconfig
+
+DEPENDS += " \
+ fmt \
+ sdbusplus \
+ sdeventplus \
+ stdplus \
+ systemd \
+ "
+
+SYSTEMD_SERVICE:${PN} += "google-usb-dynamic.service"
+
+SRC_URI += " \
+ file://meson.build \
+ file://google-usb-dynamic.cpp \
+ file://google-usb-dynamic.service \
+ "
+
+do_configure:prepend() {
+ mkdir -p ${S}
+ cp ${WORKDIR}/meson.build ${S}/
+ cp ${WORKDIR}/google-usb-dynamic.cpp ${S}/
+}
+
+do_install:append() {
+ install -d ${D}${systemd_system_unitdir}
+ install -m 0644 ${WORKDIR}/google-usb-dynamic.service ${D}${systemd_system_unitdir}/
+}