Add syslog-cat: the replacement of systemd-cat for rsyslog

This CL introduces the replacement of systemd-cat.

Summary:
- syslog-cat is a command to send stdout/stderr from a process to
  the rsyslog plugin.

See go/cros-log-syslogcat for the detailed design.

BUG=chromium:1128630
TEST=Confirms the plugin works correctly

Change-Id: I679afda46059010412467cbe8b78c3439ef98f65
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2371542
Tested-by: Yoshiki Iguchi <yoshiki@chromium.org>
Commit-Queue: Yoshiki Iguchi <yoshiki@chromium.org>
Auto-Submit: Yoshiki Iguchi <yoshiki@chromium.org>
Reviewed-by: Hidehiko Abe <hidehiko@chromium.org>
diff --git a/README.md b/README.md
index c110bfa..07a3d3a 100644
--- a/README.md
+++ b/README.md
@@ -122,6 +122,7 @@
 | [smogcheck](./smogcheck/) | Developer library for working with raw I2C devices |
 | [st_flash](./st_flash/) ||
 | [storage_info](./storage_info/) | Helper shell functions for retrieving disk information) |
+| [syslog-cat](./syslog-cat/) | Helper command to forward stdout/stderr from process to syslog |
 | [system-proxy](./system-proxy/) | Daemon for web proxy authentication support on Chrome OS |
 | [system_api](./system_api/) | Headers and .proto files etc. to be shared with chromium |
 | [thd](./thd/) | Thermal daemon to help keep systems running cool |
diff --git a/syslog-cat/BUILD.gn b/syslog-cat/BUILD.gn
new file mode 100644
index 0000000..e7e0f3b
--- /dev/null
+++ b/syslog-cat/BUILD.gn
@@ -0,0 +1,20 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//common-mk/pkg_config.gni")
+
+group("all") {
+  deps = [ ":syslog-cat" ]
+}
+
+pkg_config("target_defaults") {
+  pkg_deps = [ "libchrome-${libbase_ver}" ]
+}
+
+executable("syslog-cat") {
+  configs += [ ":target_defaults" ]
+
+  sources = [ "main.cc" ]
+  install_path = "sbin"
+}
diff --git a/syslog-cat/OWNERS b/syslog-cat/OWNERS
new file mode 100644
index 0000000..407b30d
--- /dev/null
+++ b/syslog-cat/OWNERS
@@ -0,0 +1 @@
+yoshiki@chromium.org
diff --git a/syslog-cat/README.md b/syslog-cat/README.md
new file mode 100644
index 0000000..c523311
--- /dev/null
+++ b/syslog-cat/README.md
@@ -0,0 +1,3 @@
+Helper command to send stdout/stderr from a process to syslog.
+
+See go/cros-log-syslogcat for detail.
diff --git a/syslog-cat/main.cc b/syslog-cat/main.cc
new file mode 100644
index 0000000..1940517
--- /dev/null
+++ b/syslog-cat/main.cc
@@ -0,0 +1,240 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <stdio.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include <base/command_line.h>
+#include <base/strings/string_piece.h>
+#include <base/files/file_path.h>
+#include <base/files/file_util.h>
+#include <base/files/scoped_file.h>
+#include <base/logging.h>
+#include <base/process/process.h>
+#include <base/posix/eintr_wrapper.h>
+#include <base/strings/stringprintf.h>
+#include <base/strings/string_number_conversions.h>
+#include <base/strings/string_util.h>
+
+namespace {
+constexpr int kDefaultSeverityStdout = 6;
+constexpr int kDefaultSeverityStderr = 4;
+constexpr char kSyslogSocketPath[] = "/run/rsyslogd/stdout";
+
+void ShowUsage() {
+  fprintf(
+      stderr,
+      "Usage: syslog_cat [OPTION] -- target-command arguments...\n"
+      "  options:\n"
+      "    --identifier=IDENTIFIER     specify the identifier of log.\n"
+      "    --severity-stdout=PRIORITY  specify the severity of log from\n"
+      "                                stdout. PRIORITY is a number 0-8.\n"
+      "    --severity-stderr=PRIORITY  specify the severity of log from\n"
+      "                                stderr. PRIORITY is a number 0-8.\n");
+}
+
+base::ScopedFD PrepareSocket(const std::string& identifier,
+                             int severity,
+                             int pid) {
+  DCHECK(!identifier.empty());
+  DCHECK_GE(severity, 0);
+  DCHECK_LE(severity, 7);
+
+  // Open the unix socket to write logs.
+  base::ScopedFD sock(HANDLE_EINTR(socket(AF_UNIX, SOCK_STREAM, 0)));
+  if (!sock.is_valid()) {
+    PLOG(ERROR) << "opening stream socket";
+    return base::ScopedFD();
+  }
+
+  // Connect the syslog unix socket file.
+  struct sockaddr_un server {};
+  server.sun_family = AF_UNIX;
+  std::strncpy(server.sun_path, kSyslogSocketPath, sizeof(kSyslogSocketPath));
+  if (HANDLE_EINTR(connect(sock.get(), (struct sockaddr*)&server,
+                           sizeof(struct sockaddr_un))) < 0) {
+    PLOG(ERROR) << "connecting stream socket";
+    return base::ScopedFD();
+  }
+
+  // Construct the header string to send.
+  std::string header = base::StringPrintf("TAG=%s[%d]\nPRIORITY=%d\n\n",
+                                          identifier.c_str(), pid, severity);
+
+  // Send headers (tag and severity).
+  if (!base::WriteFileDescriptor(sock.get(), header.c_str(), header.size())) {
+    PLOG(ERROR) << "writing headers on stream socket";
+    return base::ScopedFD();
+  }
+
+  return sock;
+}
+
+int SeverityFromString(base::StringPiece severity_str) {
+  if (severity_str == "0" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "emerg") == 0) {
+    return 0;
+  }
+
+  if (severity_str == "1" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "alert") == 0) {
+    return 1;
+  }
+
+  if (severity_str == "2" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "critical") == 0 ||
+      base::CompareCaseInsensitiveASCII(severity_str, "crit") == 0) {
+    return 2;
+  }
+
+  if (severity_str == "3" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "err") == 0 ||
+      base::CompareCaseInsensitiveASCII(severity_str, "error") == 0) {
+    return 3;
+  }
+
+  if (severity_str == "4" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "warn") == 0 ||
+      base::CompareCaseInsensitiveASCII(severity_str, "warning") == 0) {
+    return 4;
+  }
+
+  if (severity_str == "5" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "notice") == 0) {
+    return 5;
+  }
+
+  if (severity_str == "6" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "info") == 0) {
+    return 6;
+  }
+
+  if (severity_str == "7" ||
+      base::CompareCaseInsensitiveASCII(severity_str, "debug") == 0) {
+    return 7;
+  }
+
+  return -1;
+}
+
+// Extract a severity from the command line argument.
+// Return the default severity if the argument is not specified
+// Return "-1" if an invalid value is specified.
+int ExtractSeverityFromCommnadLine(const base::CommandLine* command_line,
+                                   const char* switch_name,
+                                   int default_severity) {
+  if (!command_line->HasSwitch(switch_name))
+    return default_severity;
+
+  return SeverityFromString(command_line->GetSwitchValueASCII(switch_name));
+}
+
+bool CreateSocketAndBindToFD(const std::string& identifier,
+                             int severity,
+                             int pid,
+                             int target_fd) {
+  base::ScopedFD sock = PrepareSocket(identifier, severity, pid);
+  if (!sock.is_valid()) {
+    LOG(ERROR) << "Failed to open the rsyslog socket for stderr.";
+    return false;
+  }
+
+  // Connect the socket to stderr.
+  if (HANDLE_EINTR(dup2(sock.get(), target_fd)) == -1) {
+    PLOG(ERROR) << "duping the stderr";
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace
+
+int main(int argc, char* argv[]) {
+  base::CommandLine::Init(argc, argv);
+
+  const base::CommandLine* command_line =
+      base::CommandLine::ForCurrentProcess();
+  const auto& sv = command_line->GetArgs();
+
+  if (sv.size() == 0 || command_line->HasSwitch("help")) {
+    ShowUsage();
+    return 1;
+  }
+
+  // Prepare a identifier.
+  constexpr char kIdentifierSwitchName[] = "identifier";
+  std::string identifier;
+  if (command_line->HasSwitch(kIdentifierSwitchName)) {
+    identifier = command_line->GetSwitchValueASCII(kIdentifierSwitchName);
+  } else {
+    const std::string& target_command_str = sv[0].c_str();
+    const base::FilePath& target_command_path =
+        base::FilePath(target_command_str);
+    identifier = target_command_path.BaseName().value();
+  }
+  if (identifier.empty()) {
+    LOG(ERROR) << "Failed to extract a identifier string.";
+    return 1;
+  }
+
+  // Prepare a severity for stdout.
+  constexpr char kSeverityOutSwitchName[] = "severity-stdout";
+  int severity_stdout = ExtractSeverityFromCommnadLine(
+      command_line, kSeverityOutSwitchName, kDefaultSeverityStdout);
+  if (severity_stdout < 0) {
+    LOG(ERROR) << "Invalid severity value. It must be a number between "
+               << "0 (EMERG) and 7 (DEBUG) or valid severity string.";
+    return 1;
+  }
+
+  // Prepare a severity for stderr.
+  constexpr char kSeverityErrSwitchName[] = "severity-stderr";
+  int severity_stderr = ExtractSeverityFromCommnadLine(
+      command_line, kSeverityErrSwitchName, kDefaultSeverityStderr);
+  if (severity_stderr < 0) {
+    LOG(ERROR) << "Invalid severity value. It must be a number between "
+               << "0 (EMERG) and 7 (DEBUG) or valid severity string.";
+    return 1;
+  }
+
+  // Prepare a pid.
+  base::ProcessId pid = base::Process::Current().Pid();
+
+  // Prepare a command line for the target process.
+  int target_command_argc = sv.size();
+  std::vector<const char*> target_command_argv(target_command_argc + 1);
+  for (int i = 0; i < target_command_argc; i++)
+    target_command_argv[i] = sv[i].c_str();
+  target_command_argv[target_command_argc] = nullptr;
+
+  // Open the unix socket to redirect logs from stdout (and maybe stderr).
+  bool ret_stdout =
+      CreateSocketAndBindToFD(identifier, severity_stdout, pid, STDOUT_FILENO);
+  CHECK(ret_stdout) << "Failed to bind stdout.";
+
+  // Open the unix socket to redirect logs from stderr.
+  // We prepare a sock for stderr even if the severarities are same, in order to
+  // prevent interleave of simultaneous lines.
+  bool ret_stderr =
+      CreateSocketAndBindToFD(identifier, severity_stderr, pid, STDERR_FILENO);
+  CHECK(ret_stderr) << "Failed to bind stderr.";
+
+  // Execute the target process.
+  execvp(const_cast<char*>(sv[0].c_str()),
+         const_cast<char**>(target_command_argv.data()));
+
+  /////////////////////////////////////////////////////////////////////////////
+  // The code below should not be executed if the execvp() above succeeds.
+
+  PLOG(ERROR) << "execvp '" << sv[0] << "'";
+  exit(1);
+}