Even moar refactoring...

- Moved DumpstateUtil functions to its own .cpp file.
- Created a DumpstateInternal file.
- Moved properties setting to a PropertiesHelper class.
- Added title to functions that uses a FD.
- Moved Nanotime() out of DurationReporter.
- Restricted number of default CommandOptions constants.

BUG: 31982882
Test: manual verification
Test: dumpstate_test pass

Change-Id: Iab3e61594f6f7298484185f6f302472d31064f7d
diff --git a/cmds/dumpstate/Android.mk b/cmds/dumpstate/Android.mk
index 7edc7de..2b23a3d 100644
--- a/cmds/dumpstate/Android.mk
+++ b/cmds/dumpstate/Android.mk
@@ -12,6 +12,7 @@
 COMMON_LOCAL_CFLAGS := \
        -Wall -Werror -Wno-missing-field-initializers -Wno-unused-variable -Wunused-parameter
 COMMON_SRC_FILES := \
+        DumpstateInternal.cpp \
         utils.cpp
 COMMON_SHARED_LIBRARIES := \
         android.hardware.dumpstate@1.0 \
@@ -38,10 +39,10 @@
 LOCAL_C_INCLUDES := $(LOCAL_PATH)
 LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)
 LOCAL_SRC_FILES := \
-        utils.cpp # TODO: temporary, until functions are moved to DumpstateUtil.cpp
-# TODO: include just what it uses (libbase, libcutils, etc...) once split from utils.cpp
-LOCAL_SHARED_LIBRARIES := $(COMMON_SHARED_LIBRARIES)
-LOCAL_STATIC_LIBRARIES := $(COMMON_ZIP_LIBRARIES)
+        DumpstateInternal.cpp \
+        DumpstateUtil.cpp
+LOCAL_SHARED_LIBRARIES := \
+        libbase
 
 include $(BUILD_STATIC_LIBRARY)
 
diff --git a/cmds/dumpstate/DumpstateInternal.cpp b/cmds/dumpstate/DumpstateInternal.cpp
new file mode 100644
index 0000000..2d74377
--- /dev/null
+++ b/cmds/dumpstate/DumpstateInternal.cpp
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#define LOG_TAG "dumpstate"
+
+#include "DumpstateInternal.h"
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/capability.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <cutils/log.h>
+#include <private/android_filesystem_config.h>
+
+uint64_t Nanotime() {
+    timespec ts;
+    clock_gettime(CLOCK_MONOTONIC, &ts);
+    return static_cast<uint64_t>(ts.tv_sec * NANOS_PER_SEC + ts.tv_nsec);
+}
+
+// Switches to non-root user and group.
+bool DropRootUser() {
+    if (getgid() == AID_SHELL && getuid() == AID_SHELL) {
+        MYLOGD("drop_root_user(): already running as Shell\n");
+        return true;
+    }
+    /* ensure we will keep capabilities when we drop root */
+    if (prctl(PR_SET_KEEPCAPS, 1) < 0) {
+        MYLOGE("prctl(PR_SET_KEEPCAPS) failed: %s\n", strerror(errno));
+        return false;
+    }
+
+    gid_t groups[] = {AID_LOG,          AID_SDCARD_R, AID_SDCARD_RW, AID_MOUNT,    AID_INET,
+                      AID_NET_BW_STATS, AID_READPROC, AID_WAKELOCK,  AID_BLUETOOTH};
+    if (setgroups(sizeof(groups) / sizeof(groups[0]), groups) != 0) {
+        MYLOGE("Unable to setgroups, aborting: %s\n", strerror(errno));
+        return false;
+    }
+    if (setgid(AID_SHELL) != 0) {
+        MYLOGE("Unable to setgid, aborting: %s\n", strerror(errno));
+        return false;
+    }
+    if (setuid(AID_SHELL) != 0) {
+        MYLOGE("Unable to setuid, aborting: %s\n", strerror(errno));
+        return false;
+    }
+
+    struct __user_cap_header_struct capheader;
+    struct __user_cap_data_struct capdata[2];
+    memset(&capheader, 0, sizeof(capheader));
+    memset(&capdata, 0, sizeof(capdata));
+    capheader.version = _LINUX_CAPABILITY_VERSION_3;
+    capheader.pid = 0;
+
+    capdata[CAP_TO_INDEX(CAP_SYSLOG)].permitted =
+        (CAP_TO_MASK(CAP_SYSLOG) | CAP_TO_MASK(CAP_BLOCK_SUSPEND));
+    capdata[CAP_TO_INDEX(CAP_SYSLOG)].effective =
+        (CAP_TO_MASK(CAP_SYSLOG) | CAP_TO_MASK(CAP_BLOCK_SUSPEND));
+    capdata[0].inheritable = 0;
+    capdata[1].inheritable = 0;
+
+    if (capset(&capheader, &capdata[0]) < 0) {
+        MYLOGE("capset failed: %s\n", strerror(errno));
+        return false;
+    }
+
+    return true;
+}
+
+int DumpFileFromFdToFd(const std::string& title, const std::string& path_string, int fd, int out_fd,
+                       bool dry_run) {
+    const char* path = path_string.c_str();
+    if (!title.empty()) {
+        dprintf(out_fd, "------ %s (%s", title.c_str(), path);
+
+        struct stat st;
+        // Only show the modification time of non-device files.
+        size_t path_len = strlen(path);
+        if ((path_len < 6 || memcmp(path, "/proc/", 6)) &&
+            (path_len < 5 || memcmp(path, "/sys/", 5)) &&
+            (path_len < 3 || memcmp(path, "/d/", 3)) && !fstat(fd, &st)) {
+            char stamp[80];
+            time_t mtime = st.st_mtime;
+            strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", localtime(&mtime));
+            dprintf(out_fd, ": %s", stamp);
+        }
+        dprintf(out_fd, ") ------\n");
+        fsync(out_fd);
+    }
+    if (dry_run) {
+        if (out_fd != STDOUT_FILENO) {
+            // There is no title, but we should still print a dry-run message
+            dprintf(out_fd, "%s: skipped on dry run\n", path);
+        } else if (!title.empty()) {
+            dprintf(out_fd, "\t(skipped on dry run)\n");
+        }
+        fsync(out_fd);
+        return 0;
+    }
+    bool newline = false;
+    fd_set read_set;
+    timeval tm;
+    while (true) {
+        FD_ZERO(&read_set);
+        FD_SET(fd, &read_set);
+        /* Timeout if no data is read for 30 seconds. */
+        tm.tv_sec = 30;
+        tm.tv_usec = 0;
+        uint64_t elapsed = Nanotime();
+        int ret = TEMP_FAILURE_RETRY(select(fd + 1, &read_set, nullptr, nullptr, &tm));
+        if (ret == -1) {
+            dprintf(out_fd, "*** %s: select failed: %s\n", path, strerror(errno));
+            newline = true;
+            break;
+        } else if (ret == 0) {
+            elapsed = Nanotime() - elapsed;
+            dprintf(out_fd, "*** %s: Timed out after %.3fs\n", path, (float)elapsed / NANOS_PER_SEC);
+            newline = true;
+            break;
+        } else {
+            char buffer[65536];
+            ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd, buffer, sizeof(buffer)));
+            if (bytes_read > 0) {
+                android::base::WriteFully(out_fd, buffer, bytes_read);
+                newline = (buffer[bytes_read - 1] == '\n');
+            } else {
+                if (bytes_read == -1) {
+                    dprintf(out_fd, "*** %s: Failed to read from fd: %s", path, strerror(errno));
+                    newline = true;
+                }
+                break;
+            }
+        }
+    }
+    close(fd);
+
+    if (!newline) dprintf(out_fd, "\n");
+    if (!title.empty()) dprintf(out_fd, "\n");
+    return 0;
+}
diff --git a/cmds/dumpstate/DumpstateInternal.h b/cmds/dumpstate/DumpstateInternal.h
new file mode 100644
index 0000000..2f7704d
--- /dev/null
+++ b/cmds/dumpstate/DumpstateInternal.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+#ifndef FRAMEWORK_NATIVE_CMD_DUMPSTATE_INTERNAL_H_
+#define FRAMEWORK_NATIVE_CMD_DUMPSTATE_INTERNAL_H_
+
+#include <cstdint>
+#include <string>
+
+// TODO: rename macros to DUMPSTATE_LOGXXX
+#ifndef MYLOGD
+#define MYLOGD(...)               \
+    fprintf(stderr, __VA_ARGS__); \
+    ALOGD(__VA_ARGS__);
+#endif
+
+#ifndef MYLOGI
+#define MYLOGI(...)               \
+    fprintf(stderr, __VA_ARGS__); \
+    ALOGI(__VA_ARGS__);
+#endif
+
+#ifndef MYLOGE
+#define MYLOGE(...)               \
+    fprintf(stderr, __VA_ARGS__); \
+    ALOGE(__VA_ARGS__);
+#endif
+
+// Internal functions used by .cpp files on multiple build targets.
+// TODO: move to android::os::dumpstate::internal namespace
+
+// TODO: use functions from <chrono> instead
+const uint64_t NANOS_PER_SEC = 1000000000;
+uint64_t Nanotime();
+
+// Switches to non-root user and group.
+bool DropRootUser();
+
+// TODO: move to .cpp as static once is not used by utils.cpp anymore.
+int DumpFileFromFdToFd(const std::string& title, const std::string& path_string, int fd, int out_fd,
+                       bool dry_run = false);
+
+#endif  // FRAMEWORK_NATIVE_CMD_DUMPSTATE_INTERNAL_H_
diff --git a/cmds/dumpstate/DumpstateService.cpp b/cmds/dumpstate/DumpstateService.cpp
index f2dcf2b..5430956 100644
--- a/cmds/dumpstate/DumpstateService.cpp
+++ b/cmds/dumpstate/DumpstateService.cpp
@@ -22,6 +22,8 @@
 
 #include "android/os/BnDumpstate.h"
 
+#include "DumpstateInternal.h"
+
 namespace android {
 namespace os {
 
diff --git a/cmds/dumpstate/DumpstateUtil.cpp b/cmds/dumpstate/DumpstateUtil.cpp
new file mode 100644
index 0000000..9faa63e
--- /dev/null
+++ b/cmds/dumpstate/DumpstateUtil.cpp
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#define LOG_TAG "dumpstate"
+
+#include "DumpstateUtil.h"
+
+#include <fcntl.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include <android-base/properties.h>
+#include <cutils/log.h>
+
+#include "DumpstateInternal.h"
+
+// TODO: move to unnamed namespace
+static constexpr const char* kSuPath = "/system/xbin/su";
+
+// TODO: move to unnamed namespace
+static bool waitpid_with_timeout(pid_t pid, int timeout_seconds, int* status) {
+    sigset_t child_mask, old_mask;
+    sigemptyset(&child_mask);
+    sigaddset(&child_mask, SIGCHLD);
+
+    if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) {
+        printf("*** sigprocmask failed: %s\n", strerror(errno));
+        return false;
+    }
+
+    timespec ts;
+    ts.tv_sec = timeout_seconds;
+    ts.tv_nsec = 0;
+    int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, NULL, &ts));
+    int saved_errno = errno;
+    // Set the signals back the way they were.
+    if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
+        printf("*** sigprocmask failed: %s\n", strerror(errno));
+        if (ret == 0) {
+            return false;
+        }
+    }
+    if (ret == -1) {
+        errno = saved_errno;
+        if (errno == EAGAIN) {
+            errno = ETIMEDOUT;
+        } else {
+            printf("*** sigtimedwait failed: %s\n", strerror(errno));
+        }
+        return false;
+    }
+
+    pid_t child_pid = waitpid(pid, status, WNOHANG);
+    if (child_pid != pid) {
+        if (child_pid != -1) {
+            printf("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid);
+        } else {
+            printf("*** waitpid failed: %s\n", strerror(errno));
+        }
+        return false;
+    }
+    return true;
+}
+
+CommandOptions CommandOptions::DEFAULT = CommandOptions::WithTimeout(10).Build();
+CommandOptions CommandOptions::AS_ROOT = CommandOptions::WithTimeout(10).AsRoot().Build();
+CommandOptions CommandOptions::AS_ROOT_5 = CommandOptions::WithTimeout(5).AsRoot().Build();
+
+CommandOptions::CommandOptionsBuilder::CommandOptionsBuilder(int64_t timeout) : values(timeout) {
+}
+
+CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::Always() {
+    values.always_ = true;
+    return *this;
+}
+
+CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::AsRoot() {
+    values.account_mode_ = SU_ROOT;
+    return *this;
+}
+
+CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::DropRoot() {
+    values.account_mode_ = DROP_ROOT;
+    return *this;
+}
+
+CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::RedirectStderr() {
+    values.output_mode_ = REDIRECT_TO_STDERR;
+    return *this;
+}
+
+CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::Log(
+    const std::string& message) {
+    values.logging_message_ = message;
+    return *this;
+}
+
+CommandOptions CommandOptions::CommandOptionsBuilder::Build() {
+    return CommandOptions(values);
+}
+
+CommandOptions::CommandOptionsValues::CommandOptionsValues(int64_t timeout)
+    : timeout_(timeout),
+      always_(false),
+      account_mode_(DONT_DROP_ROOT),
+      output_mode_(NORMAL_OUTPUT),
+      logging_message_("") {
+}
+
+CommandOptions::CommandOptions(const CommandOptionsValues& values) : values(values) {
+}
+
+int64_t CommandOptions::Timeout() const {
+    return values.timeout_;
+}
+
+bool CommandOptions::Always() const {
+    return values.always_;
+}
+
+PrivilegeMode CommandOptions::PrivilegeMode() const {
+    return values.account_mode_;
+}
+
+OutputMode CommandOptions::OutputMode() const {
+    return values.output_mode_;
+}
+
+std::string CommandOptions::LoggingMessage() const {
+    return values.logging_message_;
+}
+
+CommandOptions::CommandOptionsBuilder CommandOptions::WithTimeout(int64_t timeout) {
+    return CommandOptions::CommandOptionsBuilder(timeout);
+}
+
+std::string PropertiesHelper::build_type_ = "";
+int PropertiesHelper::dry_run_ = -1;
+
+bool PropertiesHelper::IsUserBuild() {
+    if (build_type_.empty()) {
+        build_type_ = android::base::GetProperty("ro.build.type", "user");
+    }
+    return "user" == build_type_;
+}
+
+bool PropertiesHelper::IsDryRun() {
+    if (dry_run_ == -1) {
+        dry_run_ = android::base::GetBoolProperty("dumpstate.dry_run", false) ? 1 : 0;
+    }
+    return dry_run_ == 1;
+}
+
+int DumpFileToFd(int out_fd, const std::string& title, const std::string& path) {
+    int fd = TEMP_FAILURE_RETRY(open(path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC));
+    if (fd < 0) {
+        int err = errno;
+        if (title.empty()) {
+            dprintf(out_fd, "*** Error dumping %s: %s\n", path.c_str(), strerror(err));
+        } else {
+            dprintf(out_fd, "*** Error dumping %s (%s): %s\n", path.c_str(), title.c_str(),
+                    strerror(err));
+        }
+        fsync(out_fd);
+        return -1;
+    }
+    return DumpFileFromFdToFd(title, path, fd, out_fd, PropertiesHelper::IsDryRun());
+}
+
+int RunCommandToFd(int fd, const std::string& title, const std::vector<std::string>& full_command,
+                   const CommandOptions& options) {
+    if (full_command.empty()) {
+        MYLOGE("No arguments on RunCommandToFd(%s)\n", title.c_str());
+        return -1;
+    }
+
+    int size = full_command.size() + 1;  // null terminated
+    int starting_index = 0;
+    if (options.PrivilegeMode() == SU_ROOT) {
+        starting_index = 2;  // "su" "root"
+        size += starting_index;
+    }
+
+    std::vector<const char*> args;
+    args.resize(size);
+
+    std::string command_string;
+    if (options.PrivilegeMode() == SU_ROOT) {
+        args[0] = kSuPath;
+        command_string += kSuPath;
+        args[1] = "root";
+        command_string += " root ";
+    }
+    for (size_t i = 0; i < full_command.size(); i++) {
+        args[i + starting_index] = full_command[i].data();
+        command_string += args[i + starting_index];
+        if (i != full_command.size() - 1) {
+            command_string += " ";
+        }
+    }
+    args[size - 1] = nullptr;
+
+    const char* command = command_string.c_str();
+
+    if (options.PrivilegeMode() == SU_ROOT && PropertiesHelper::IsUserBuild()) {
+        dprintf(fd, "Skipping '%s' on user build.\n", command);
+        return 0;
+    }
+
+    if (!title.empty()) {
+        dprintf(fd, "------ %s (%s) ------\n", title.c_str(), command);
+        fsync(fd);
+    }
+
+    const std::string& logging_message = options.LoggingMessage();
+    if (!logging_message.empty()) {
+        MYLOGI(logging_message.c_str(), command_string.c_str());
+    }
+
+    bool silent = (options.OutputMode() == REDIRECT_TO_STDERR);
+    bool redirecting_to_fd = STDOUT_FILENO != fd;
+
+    if (PropertiesHelper::IsDryRun() && !options.Always()) {
+        if (!title.empty()) {
+            dprintf(fd, "\t(skipped on dry run)\n");
+        } else if (redirecting_to_fd) {
+            // There is no title, but we should still print a dry-run message
+            dprintf(fd, "%s: skipped on dry run\n", command_string.c_str());
+        }
+        fsync(fd);
+        return 0;
+    }
+
+    const char* path = args[0];
+
+    uint64_t start = Nanotime();
+    pid_t pid = fork();
+
+    /* handle error case */
+    if (pid < 0) {
+        if (!silent) dprintf(fd, "*** fork: %s\n", strerror(errno));
+        MYLOGE("*** fork: %s\n", strerror(errno));
+        return pid;
+    }
+
+    /* handle child case */
+    if (pid == 0) {
+        if (options.PrivilegeMode() == DROP_ROOT && !DropRootUser()) {
+            if (!silent) {
+                dprintf(fd, "*** failed to drop root before running %s: %s\n", command,
+                        strerror(errno));
+            }
+            MYLOGE("*** could not drop root before running %s: %s\n", command, strerror(errno));
+            return -1;
+        }
+
+        if (silent) {
+            // Redirects stdout to stderr
+            TEMP_FAILURE_RETRY(dup2(STDERR_FILENO, STDOUT_FILENO));
+        } else if (redirecting_to_fd) {
+            // Redirect stdout to fd
+            TEMP_FAILURE_RETRY(dup2(fd, STDOUT_FILENO));
+            close(fd);
+        }
+
+        /* make sure the child dies when dumpstate dies */
+        prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+        /* just ignore SIGPIPE, will go down with parent's */
+        struct sigaction sigact;
+        memset(&sigact, 0, sizeof(sigact));
+        sigact.sa_handler = SIG_IGN;
+        sigaction(SIGPIPE, &sigact, NULL);
+
+        execvp(path, (char**)args.data());
+        // execvp's result will be handled after waitpid_with_timeout() below, but
+        // if it failed, it's safer to exit dumpstate.
+        MYLOGD("execvp on command '%s' failed (error: %s)\n", command, strerror(errno));
+        // Must call _exit (instead of exit), otherwise it will corrupt the zip
+        // file.
+        _exit(EXIT_FAILURE);
+    }
+
+    /* handle parent case */
+    int status;
+    bool ret = waitpid_with_timeout(pid, options.Timeout(), &status);
+    fsync(fd);
+
+    uint64_t elapsed = Nanotime() - start;
+    if (!ret) {
+        if (errno == ETIMEDOUT) {
+            if (!silent)
+                dprintf(fd, "*** command '%s' timed out after %.3fs (killing pid %d)\n", command,
+                        static_cast<float>(elapsed) / NANOS_PER_SEC, pid);
+            MYLOGE("*** command '%s' timed out after %.3fs (killing pid %d)\n", command,
+                   static_cast<float>(elapsed) / NANOS_PER_SEC, pid);
+        } else {
+            if (!silent)
+                dprintf(fd, "*** command '%s': Error after %.4fs (killing pid %d)\n", command,
+                        static_cast<float>(elapsed) / NANOS_PER_SEC, pid);
+            MYLOGE("command '%s': Error after %.4fs (killing pid %d)\n", command,
+                   static_cast<float>(elapsed) / NANOS_PER_SEC, pid);
+        }
+        kill(pid, SIGTERM);
+        if (!waitpid_with_timeout(pid, 5, nullptr)) {
+            kill(pid, SIGKILL);
+            if (!waitpid_with_timeout(pid, 5, nullptr)) {
+                if (!silent)
+                    dprintf(fd, "could not kill command '%s' (pid %d) even with SIGKILL.\n",
+                            command, pid);
+                MYLOGE("could not kill command '%s' (pid %d) even with SIGKILL.\n", command, pid);
+            }
+        }
+        return -1;
+    }
+
+    if (WIFSIGNALED(status)) {
+        if (!silent)
+            dprintf(fd, "*** command '%s' failed: killed by signal %d\n", command, WTERMSIG(status));
+        MYLOGE("*** command '%s' failed: killed by signal %d\n", command, WTERMSIG(status));
+    } else if (WIFEXITED(status) && WEXITSTATUS(status) > 0) {
+        status = WEXITSTATUS(status);
+        if (!silent) dprintf(fd, "*** command '%s' failed: exit code %d\n", command, status);
+        MYLOGE("*** command '%s' failed: exit code %d\n", command, status);
+    }
+
+    return status;
+}
diff --git a/cmds/dumpstate/DumpstateUtil.h b/cmds/dumpstate/DumpstateUtil.h
index ede23c9..8bda6f2 100644
--- a/cmds/dumpstate/DumpstateUtil.h
+++ b/cmds/dumpstate/DumpstateUtil.h
@@ -16,6 +16,9 @@
 #ifndef FRAMEWORK_NATIVE_CMD_DUMPSTATE_UTIL_H_
 #define FRAMEWORK_NATIVE_CMD_DUMPSTATE_UTIL_H_
 
+#include <cstdint>
+#include <string>
+
 // TODO: use android::os::dumpstate (must wait until device code is refactored)
 
 /*
@@ -116,28 +119,59 @@
 
     // Common options.
     static CommandOptions DEFAULT;
+    static CommandOptions AS_ROOT;
+
+    // TODO: temporary, until device implementations use AS_ROOT
     static CommandOptions AS_ROOT_5;
-    static CommandOptions AS_ROOT_10;
-    static CommandOptions AS_ROOT_20;
+};
+
+/*
+ * System properties helper.
+ */
+class PropertiesHelper {
+    friend class DumpstateBaseTest;
+
+  public:
+    /*
+     * Gets whether device is running a `user` build.
+     */
+    static bool IsUserBuild();
+
+    /*
+     * When running in dry-run mode, skips the real dumps and just print the section headers.
+     *
+     * Useful when debugging dumpstate or other bugreport-related activities.
+     *
+     * Dry-run mode is enabled by setting the system property `dumpstate.dry_run` to true.
+     */
+    static bool IsDryRun();
+
+  private:
+    static std::string build_type_;
+    static int dry_run_;
 };
 
 /*
  * Forks a command, waits for it to finish, and returns its status.
  *
  * |fd| file descriptor that receives the command's 'stdout'.
+ * |title| description of the command printed on `stdout` (or empty to skip
+ * description).
  * |full_command| array containing the command (first entry) and its arguments.
  *                Must contain at least one element.
  * |options| optional argument defining the command's behavior.
  */
-int RunCommandToFd(int fd, const std::vector<std::string>& full_command,
+int RunCommandToFd(int fd, const std::string& title, const std::vector<std::string>& full_command,
                    const CommandOptions& options = CommandOptions::DEFAULT);
 
 /*
  * Dumps the contents of a file into a file descriptor.
  *
  * |fd| file descriptor where the file is dumped into.
+ * |title| description of the command printed on `stdout` (or empty to skip
+ * description).
  * |path| location of the file to be dumped.
  */
-int DumpFileToFd(int fd, const std::string& path);
+int DumpFileToFd(int fd, const std::string& title, const std::string& path);
 
 #endif  // FRAMEWORK_NATIVE_CMD_DUMPSTATE_UTIL_H_
diff --git a/cmds/dumpstate/dumpstate.cpp b/cmds/dumpstate/dumpstate.cpp
index 71511fc..30b5d9a 100644
--- a/cmds/dumpstate/dumpstate.cpp
+++ b/cmds/dumpstate/dumpstate.cpp
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 #define LOG_TAG "dumpstate"
 
 #include <dirent.h>
@@ -49,6 +50,7 @@
 #include <private/android_filesystem_config.h>
 #include <private/android_logger.h>
 
+#include "DumpstateInternal.h"
 #include "DumpstateService.h"
 #include "dumpstate.h"
 
@@ -99,9 +101,6 @@
 static int DumpFile(const std::string& title, const std::string& path) {
     return ds.DumpFile(title, path);
 }
-bool IsUserBuild() {
-    return ds.IsUserBuild();
-}
 
 // Relative directory (inside the zip) for all files copied as-is into the bugreport.
 static const std::string ZIP_ROOT_DIR = "FS";
@@ -110,6 +109,8 @@
 static constexpr char PROPERTY_LAST_ID[] = "dumpstate.last_id";
 static constexpr char PROPERTY_VERSION[] = "dumpstate.version";
 
+static const CommandOptions AS_ROOT_20 = CommandOptions::WithTimeout(20).AsRoot().Build();
+
 /* gets the tombstone data, according to the bugreport type: if zipped, gets all tombstones;
  * otherwise, gets just those modified in the last half an hour. */
 static void get_tombstone_fds(tombstone_data_t data[NUM_TOMBSTONES]) {
@@ -359,7 +360,7 @@
 }
 
 static void dump_raft() {
-    if (IsUserBuild()) {
+    if (PropertiesHelper::IsUserBuild()) {
         return;
     }
 
@@ -430,7 +431,7 @@
 
 static void DumpModemLogs() {
     DurationReporter durationReporter("DUMP MODEM LOGS");
-    if (IsUserBuild()) {
+    if (PropertiesHelper::IsUserBuild()) {
         return;
     }
 
@@ -686,11 +687,11 @@
 
     printf("Kernel: ");
     fflush(stdout);
-    DumpFileToFd(STDOUT_FILENO, "/proc/version");
+    DumpFileToFd(STDOUT_FILENO, "", "/proc/version");
     printf("Command line: %s\n", strtok(cmdline_buf, "\n"));
     printf("Bugreport format version: %s\n", version_.c_str());
     printf("Dumpstate info: id=%d pid=%d dry_run=%d args=%s extra_options=%s\n", id_, pid_,
-           dry_run_, args_.c_str(), extra_options_.c_str());
+           PropertiesHelper::IsDryRun(), args_.c_str(), extra_options_.c_str());
     printf("\n");
     fflush(stdout);
 }
@@ -916,7 +917,7 @@
     DumpFile("MEMORY INFO", "/proc/meminfo");
     RunCommand("CPU INFO", {"top", "-b", "-n", "1", "-H", "-s", "6", "-o",
                             "pid,tid,user,pr,ni,%cpu,s,virt,res,pcy,cmd,name"});
-    RunCommand("PROCRANK", {"procrank"}, CommandOptions::AS_ROOT_20);
+    RunCommand("PROCRANK", {"procrank"}, AS_ROOT_20);
     DumpFile("VIRTUAL MEMORY STATS", "/proc/vmstat");
     DumpFile("VMALLOC INFO", "/proc/vmallocinfo");
     DumpFile("SLAB INFO", "/proc/slabinfo");
@@ -931,7 +932,7 @@
 
     RunCommand("PROCESSES AND THREADS",
                {"ps", "-A", "-T", "-Z", "-O", "pri,nice,rtprio,sched,pcy"});
-    RunCommand("LIBRANK", {"librank"}, CommandOptions::AS_ROOT_10);
+    RunCommand("LIBRANK", {"librank"}, CommandOptions::AS_ROOT);
 
     RunCommand("PRINTENV", {"printenv"});
     RunCommand("NETSTAT", {"netstat", "-nW"});
@@ -944,7 +945,7 @@
 
     do_dmesg();
 
-    RunCommand("LIST OF OPEN FILES", {"lsof"}, CommandOptions::AS_ROOT_10);
+    RunCommand("LIST OF OPEN FILES", {"lsof"}, CommandOptions::AS_ROOT);
     for_each_pid(do_showmap, "SMAPS OF ALL PROCESSES");
     for_each_tid(show_wchan, "BLOCKED PROCESS WAIT-CHANNELS");
     for_each_pid(show_showtime, "PROCESS TIMES (pid cmd user system iowait+percentage)");
@@ -1047,7 +1048,7 @@
 #ifdef FWDUMP_bcmdhd
     RunCommand("ND OFFLOAD TABLE", {WLUTIL, "nd_hostip"}, CommandOptions::AS_ROOT_5);
 
-    RunCommand("DUMP WIFI INTERNAL COUNTERS (1)", {WLUTIL, "counters"}, CommandOptions::AS_ROOT_20);
+    RunCommand("DUMP WIFI INTERNAL COUNTERS (1)", {WLUTIL, "counters"}, AS_ROOT_20);
 
     RunCommand("ND OFFLOAD STATUS (1)", {WLUTIL, "nd_status"}, CommandOptions::AS_ROOT_5);
 
@@ -1058,9 +1059,9 @@
                CommandOptions::WithTimeout(10).Build());
 
 #ifdef FWDUMP_bcmdhd
-    RunCommand("DUMP WIFI STATUS", {"dhdutil", "-i", "wlan0", "dump"}, CommandOptions::AS_ROOT_20);
+    RunCommand("DUMP WIFI STATUS", {"dhdutil", "-i", "wlan0", "dump"}, AS_ROOT_20);
 
-    RunCommand("DUMP WIFI INTERNAL COUNTERS (2)", {WLUTIL, "counters"}, CommandOptions::AS_ROOT_20);
+    RunCommand("DUMP WIFI INTERNAL COUNTERS (2)", {WLUTIL, "counters"}, AS_ROOT_20);
 
     RunCommand("ND OFFLOAD STATUS (2)", {WLUTIL, "nd_status"}, CommandOptions::AS_ROOT_5);
 #endif
@@ -1105,7 +1106,7 @@
         // root can run on user builds.
         CommandOptions::CommandOptionsBuilder options =
             CommandOptions::WithTimeout(rilDumpstateTimeout);
-        if (!IsUserBuild()) {
+        if (!PropertiesHelper::IsUserBuild()) {
             options.AsRoot();
         }
         RunCommand("DUMP VENDOR RIL LOGS", {"vril-dump"}, options.Build());
@@ -1199,6 +1200,7 @@
     }
     handle->data[0] = fd;
 
+    // TODO: need a timeout mechanism so dumpstate does not hang on device implementation call.
     dumpstate_device->dumpstateBoard(handle);
 
     AddZipEntry("dumpstate-board.txt", path);
@@ -1498,7 +1500,7 @@
         }
     }
 
-    if (ds.IsDryRun()) {
+    if (PropertiesHelper::IsDryRun()) {
         MYLOGI("Running on dry-run mode (to disable it, call 'setprop dumpstate.dry_run false')\n");
     }
 
@@ -1660,7 +1662,7 @@
     ds.AddDir(RECOVERY_DIR, true);
     ds.AddDir(RECOVERY_DATA_DIR, true);
     ds.AddDir(LOGPERSIST_DATA_DIR, false);
-    if (!IsUserBuild()) {
+    if (!PropertiesHelper::IsUserBuild()) {
         ds.AddDir(PROFILE_DATA_DIR_CUR, true);
         ds.AddDir(PROFILE_DATA_DIR_REF, true);
     }
@@ -1674,7 +1676,7 @@
     // Run ss as root so we can see socket marks.
     RunCommand("DETAILED SOCKET STATE", {"ss", "-eionptu"}, CommandOptions::WithTimeout(10).Build());
 
-    if (!drop_root_user()) {
+    if (!DropRootUser()) {
         return -1;
     }
 
diff --git a/cmds/dumpstate/dumpstate.h b/cmds/dumpstate/dumpstate.h
index 9f56fce..fcf0683 100644
--- a/cmds/dumpstate/dumpstate.h
+++ b/cmds/dumpstate/dumpstate.h
@@ -17,18 +17,6 @@
 #ifndef FRAMEWORK_NATIVE_CMD_DUMPSTATE_H_
 #define FRAMEWORK_NATIVE_CMD_DUMPSTATE_H_
 
-#ifndef MYLOGD
-#define MYLOGD(...) fprintf(stderr, __VA_ARGS__); ALOGD(__VA_ARGS__);
-#endif
-
-#ifndef MYLOGI
-#define MYLOGI(...) fprintf(stderr, __VA_ARGS__); ALOGI(__VA_ARGS__);
-#endif
-
-#ifndef MYLOGE
-#define MYLOGE(...) fprintf(stderr, __VA_ARGS__); ALOGE(__VA_ARGS__);
-#endif
-
 #include <time.h>
 #include <unistd.h>
 #include <stdbool.h>
@@ -67,8 +55,6 @@
 
     ~DurationReporter();
 
-    static uint64_t Nanotime();
-
   private:
     std::string title_;
     bool log_only_;
@@ -164,26 +150,14 @@
  */
 class Dumpstate {
     friend class DumpstateTest;
-    friend class DumpstateUtilTest;
 
   public:
     static CommandOptions DEFAULT_DUMPSYS;
 
     static Dumpstate& GetInstance();
 
-    /*
-     * When running in dry-run mode, skips the real dumps and just print the section headers.
-     *
-     * Useful when debugging dumpstate or other bugreport-related activities.
-     *
-     * Dry-run mode is enabled by setting the system property dumpstate.dry_run to true.
-     */
-    bool IsDryRun() const;
-
-    /*
-     * Gets whether device is running a `user` build.
-     */
-    bool IsUserBuild() const;
+    // TODO: temporary function until device code uses PropertiesHelper::IsUserBuild()
+    bool IsUserBuild();
 
     /* Checkes whether dumpstate is generating a zipped bugreport. */
     bool IsZipping() const;
@@ -349,14 +323,7 @@
 
   private:
     // Used by GetInstance() only.
-    Dumpstate(const std::string& version = VERSION_CURRENT, bool dry_run = false,
-              const std::string& build_type = "user");
-
-    // Whether this is a dry run.
-    bool dry_run_;
-
-    // Build type (such as 'user' or 'eng').
-    std::string build_type_;
+    Dumpstate(const std::string& version = VERSION_CURRENT);
 
     DISALLOW_COPY_AND_ASSIGN(Dumpstate);
 };
@@ -384,9 +351,6 @@
 int dump_files(const std::string& title, const char* dir, bool (*skip)(const char* path),
                int (*dump_from_fd)(const char* title, const char* path, int fd));
 
-/* switch to non-root user and group */
-bool drop_root_user();
-
 /* sends a broadcast using Activity Manager */
 void send_broadcast(const std::string& action, const std::vector<std::string>& args);
 
@@ -453,9 +417,6 @@
 /** Gets command-line arguments. */
 void format_args(int argc, const char *argv[], std::string *args);
 
-/** Tells if the device is running a user build. */
-bool is_user_build();
-
 #ifdef __cplusplus
 }
 #endif
diff --git a/cmds/dumpstate/tests/dumpstate_test.cpp b/cmds/dumpstate/tests/dumpstate_test.cpp
index eb8f961..2e35112 100644
--- a/cmds/dumpstate/tests/dumpstate_test.cpp
+++ b/cmds/dumpstate/tests/dumpstate_test.cpp
@@ -17,6 +17,7 @@
 #define LOG_TAG "dumpstate"
 #include <cutils/log.h>
 
+#include "DumpstateInternal.h"
 #include "DumpstateService.h"
 #include "android/os/BnDumpstate.h"
 #include "dumpstate.h"
@@ -75,18 +76,23 @@
   public:
     virtual void SetUp() override {
         calls_++;
+        SetDryRun(false);
     }
 
-    bool IsStandalone() {
+    void SetDryRun(bool dry_run) const {
+        PropertiesHelper::dry_run_ = dry_run;
+    }
+
+    void SetBuildType(const std::string& build_type) const {
+        PropertiesHelper::build_type_ = build_type;
+    }
+
+    bool IsStandalone() const {
         return calls_ == 1;
     }
 
-    bool IsUserBuild() {
-        return "user" == android::base::GetProperty("ro.build.type", "(unknown)");
-    }
-
-    void DropRoot() {
-        drop_root_user();
+    void DropRoot() const {
+        DropRootUser();
         uid_t uid = getuid();
         ASSERT_EQ(2000, (int)uid);
     }
@@ -165,18 +171,6 @@
         return status;
     }
 
-    // TODO: should set the system property directly, rather than messing with Dumpstate variable
-    void SetDryRun(bool dry_run) {
-        ALOGD("Setting dry_run_ to %s\n", dry_run ? "true" : "false");
-        ds.dry_run_ = dry_run;
-    }
-
-    // TODO: should set the system property directly, rather than messing with Dumpstate variable
-    void SetBuildType(const std::string& build_type) {
-        ALOGD("Setting build_type_ to '%s'\n", build_type.c_str());
-        ds.build_type_ = build_type;
-    }
-
     void SetProgress(long progress, long initial_max, long threshold = 0) {
         ds.update_progress_ = true;
         ds.update_progress_threshold_ = threshold;
@@ -453,7 +447,7 @@
         MYLOGE("Skipping DumpstateTest.RunCommandAsRootUserBuild() on test suite\n")
         return;
     }
-    if (!IsUserBuild()) {
+    if (!PropertiesHelper::IsUserBuild()) {
         // Emulates user build if necessarily.
         SetBuildType("user");
     }
@@ -475,7 +469,7 @@
         MYLOGE("Skipping DumpstateTest.RunCommandAsRootNonUserBuild() on test suite\n")
         return;
     }
-    if (IsUserBuild()) {
+    if (PropertiesHelper::IsUserBuild()) {
         ALOGI("Skipping RunCommandAsRootNonUserBuild on user builds\n");
         return;
     }
@@ -544,7 +538,6 @@
         out, StartsWith("------ Might as well dump. Dump! (" + kTestDataPath + "single-line.txt:"));
     EXPECT_THAT(out, HasSubstr("\n\t(skipped on dry run)\n------"));
     EXPECT_THAT(out, EndsWith("s was the duration of 'Might as well dump. Dump!' ------\n"));
-    EXPECT_THAT(err, IsEmpty());
 }
 
 TEST_F(DumpstateTest, DumpFileUpdateProgress) {
@@ -815,25 +808,8 @@
         SetDryRun(false);
     }
 
-    // TODO: should set the system property directly, rather than messing with Dumpstate variable
-    void SetDryRun(bool dry_run) {
-        ALOGD("Setting dry_run_ to %s\n", dry_run ? "true" : "false");
-        Dumpstate::GetInstance().dry_run_ = dry_run;
-    }
-
-    // TODO: should set the system property directly, rather than messing with Dumpstate variable
-    void SetBuildType(const std::string& build_type) {
-        ALOGD("Setting build_type_ to '%s'\n", build_type.c_str());
-        Dumpstate::GetInstance().build_type_ = build_type;
-    }
-
     void CaptureFdOut() {
-        // TODO: for some obscure, black-magic C++ curse, the ASSERT_TRUE assertion inside
-        // ReadFileToString() fails, even though the returned value is true, so it's using the
-        // core library function directly, without checking the result (if the file cannot be read,
-        // the test case will eventually fail anyways because of the contents of out).
-        // ReadFileToString(path_, &out);
-        android::base::ReadFileToString(path_, &out);
+        ReadFileToString(path_, &out);
     }
 
     void CreateFd(const std::string& name) {
@@ -847,10 +823,10 @@
     }
 
     // Runs a command into the `fd` and capture `stderr`.
-    int RunCommand(const std::vector<std::string>& full_command,
+    int RunCommand(const std::string& title, const std::vector<std::string>& full_command,
                    const CommandOptions& options = CommandOptions::DEFAULT) {
         CaptureStderr();
-        int status = RunCommandToFd(fd, full_command, options);
+        int status = RunCommandToFd(fd, title, full_command, options);
         close(fd);
 
         CaptureFdOut();
@@ -859,9 +835,9 @@
     }
 
     // Dumps a file and into the `fd` and `stderr`.
-    int DumpFile(const std::string& path) {
+    int DumpFile(const std::string& title, const std::string& path) {
         CaptureStderr();
-        int status = DumpFileToFd(fd, path);
+        int status = DumpFileToFd(fd, title, path);
         close(fd);
 
         CaptureFdOut();
@@ -879,26 +855,34 @@
 };
 
 TEST_F(DumpstateUtilTest, RunCommandNoArgs) {
-    EXPECT_EQ(-1, RunCommand({}));
+    CreateFd("RunCommandNoArgs.txt");
+    EXPECT_EQ(-1, RunCommand("", {}));
 }
 
-TEST_F(DumpstateUtilTest, RunCommandWithNoArgs) {
+TEST_F(DumpstateUtilTest, RunCommandNoTitle) {
     CreateFd("RunCommandWithNoArgs.txt");
-    EXPECT_EQ(0, RunCommand({kSimpleCommand}));
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand}));
     EXPECT_THAT(out, StrEq("stdout\n"));
     EXPECT_THAT(err, StrEq("stderr\n"));
 }
 
+TEST_F(DumpstateUtilTest, RunCommandWithTitle) {
+    CreateFd("RunCommandWithNoArgs.txt");
+    EXPECT_EQ(0, RunCommand("I AM GROOT", {kSimpleCommand}));
+    EXPECT_THAT(out, StrEq("------ I AM GROOT (" + kSimpleCommand + ") ------\nstdout\n"));
+    EXPECT_THAT(err, StrEq("stderr\n"));
+}
+
 TEST_F(DumpstateUtilTest, RunCommandWithOneArg) {
     CreateFd("RunCommandWithOneArg.txt");
-    EXPECT_EQ(0, RunCommand({kEchoCommand, "one"}));
+    EXPECT_EQ(0, RunCommand("", {kEchoCommand, "one"}));
     EXPECT_THAT(err, IsEmpty());
     EXPECT_THAT(out, StrEq("one\n"));
 }
 
 TEST_F(DumpstateUtilTest, RunCommandWithMultipleArgs) {
     CreateFd("RunCommandWithMultipleArgs.txt");
-    EXPECT_EQ(0, RunCommand({kEchoCommand, "one", "is", "the", "loniest", "number"}));
+    EXPECT_EQ(0, RunCommand("", {kEchoCommand, "one", "is", "the", "loniest", "number"}));
     EXPECT_THAT(err, IsEmpty());
     EXPECT_THAT(out, StrEq("one is the loniest number\n"));
 }
@@ -906,7 +890,7 @@
 TEST_F(DumpstateUtilTest, RunCommandWithLoggingMessage) {
     CreateFd("RunCommandWithLoggingMessage.txt");
     EXPECT_EQ(
-        0, RunCommand({kSimpleCommand},
+        0, RunCommand("", {kSimpleCommand},
                       CommandOptions::WithTimeout(10).Log("COMMAND, Y U NO LOG FIRST?").Build()));
     EXPECT_THAT(out, StrEq("stdout\n"));
     EXPECT_THAT(err, StrEq("COMMAND, Y U NO LOG FIRST?stderr\n"));
@@ -914,8 +898,8 @@
 
 TEST_F(DumpstateUtilTest, RunCommandRedirectStderr) {
     CreateFd("RunCommandRedirectStderr.txt");
-    EXPECT_EQ(
-        0, RunCommand({kSimpleCommand}, CommandOptions::WithTimeout(10).RedirectStderr().Build()));
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand},
+                            CommandOptions::WithTimeout(10).RedirectStderr().Build()));
     EXPECT_THAT(out, IsEmpty());
     EXPECT_THAT(err, StrEq("stdout\nstderr\n"));
 }
@@ -923,7 +907,17 @@
 TEST_F(DumpstateUtilTest, RunCommandDryRun) {
     CreateFd("RunCommandDryRun.txt");
     SetDryRun(true);
-    EXPECT_EQ(0, RunCommand({kSimpleCommand}));
+    EXPECT_EQ(0, RunCommand("I AM GROOT", {kSimpleCommand}));
+    EXPECT_THAT(out, StrEq(android::base::StringPrintf(
+                         "------ I AM GROOT (%s) ------\n\t(skipped on dry run)\n",
+                         kSimpleCommand.c_str())));
+    EXPECT_THAT(err, IsEmpty());
+}
+
+TEST_F(DumpstateUtilTest, RunCommandDryRunNoTitle) {
+    CreateFd("RunCommandDryRun.txt");
+    SetDryRun(true);
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand}));
     EXPECT_THAT(
         out, StrEq(android::base::StringPrintf("%s: skipped on dry run\n", kSimpleCommand.c_str())));
     EXPECT_THAT(err, IsEmpty());
@@ -932,21 +926,21 @@
 TEST_F(DumpstateUtilTest, RunCommandDryRunAlways) {
     CreateFd("RunCommandDryRunAlways.txt");
     SetDryRun(true);
-    EXPECT_EQ(0, RunCommand({kSimpleCommand}, CommandOptions::WithTimeout(10).Always().Build()));
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand}, CommandOptions::WithTimeout(10).Always().Build()));
     EXPECT_THAT(out, StrEq("stdout\n"));
     EXPECT_THAT(err, StrEq("stderr\n"));
 }
 
 TEST_F(DumpstateUtilTest, RunCommandNotFound) {
     CreateFd("RunCommandNotFound.txt");
-    EXPECT_NE(0, RunCommand({"/there/cannot/be/such/command"}));
+    EXPECT_NE(0, RunCommand("", {"/there/cannot/be/such/command"}));
     EXPECT_THAT(out, StartsWith("*** command '/there/cannot/be/such/command' failed: exit code"));
     EXPECT_THAT(err, StartsWith("execvp on command '/there/cannot/be/such/command' failed"));
 }
 
 TEST_F(DumpstateUtilTest, RunCommandFails) {
     CreateFd("RunCommandFails.txt");
-    EXPECT_EQ(42, RunCommand({kSimpleCommand, "--exit", "42"}));
+    EXPECT_EQ(42, RunCommand("", {kSimpleCommand, "--exit", "42"}));
     EXPECT_THAT(out, StrEq("stdout\n*** command '" + kSimpleCommand +
                            " --exit 42' failed: exit code 42\n"));
     EXPECT_THAT(err, StrEq("stderr\n*** command '" + kSimpleCommand +
@@ -955,7 +949,7 @@
 
 TEST_F(DumpstateUtilTest, RunCommandCrashes) {
     CreateFd("RunCommandCrashes.txt");
-    EXPECT_NE(0, RunCommand({kSimpleCommand, "--crash"}));
+    EXPECT_NE(0, RunCommand("", {kSimpleCommand, "--crash"}));
     // We don't know the exit code, so check just the prefix.
     EXPECT_THAT(
         out, StartsWith("stdout\n*** command '" + kSimpleCommand + " --crash' failed: exit code"));
@@ -965,8 +959,8 @@
 
 TEST_F(DumpstateUtilTest, RunCommandTimesout) {
     CreateFd("RunCommandTimesout.txt");
-    EXPECT_EQ(-1,
-              RunCommand({kSimpleCommand, "--sleep", "2"}, CommandOptions::WithTimeout(1).Build()));
+    EXPECT_EQ(-1, RunCommand("", {kSimpleCommand, "--sleep", "2"},
+                             CommandOptions::WithTimeout(1).Build()));
     EXPECT_THAT(out, StartsWith("stdout line1\n*** command '" + kSimpleCommand +
                                 " --sleep 2' timed out after 1"));
     EXPECT_THAT(err, StartsWith("sleeping for 2s\n*** command '" + kSimpleCommand +
@@ -978,7 +972,7 @@
     CaptureStderr();
 
     std::thread t([=]() {
-        EXPECT_EQ(SIGTERM, RunCommandToFd(fd, {kSimpleCommand, "--pid", "--sleep", "20"},
+        EXPECT_EQ(SIGTERM, RunCommandToFd(fd, "", {kSimpleCommand, "--pid", "--sleep", "20"},
                                           CommandOptions::WithTimeout(100).Always().Build()));
     });
 
@@ -1020,14 +1014,14 @@
         return;
     }
     CreateFd("RunCommandAsRootUserBuild.txt");
-    if (!IsUserBuild()) {
+    if (!PropertiesHelper::IsUserBuild()) {
         // Emulates user build if necessarily.
         SetBuildType("user");
     }
 
     DropRoot();
 
-    EXPECT_EQ(0, RunCommand({kSimpleCommand}, CommandOptions::WithTimeout(1).AsRoot().Build()));
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand}, CommandOptions::WithTimeout(1).AsRoot().Build()));
 
     // We don't know the exact path of su, so we just check for the 'root ...' commands
     EXPECT_THAT(out, StartsWith("Skipping"));
@@ -1043,15 +1037,15 @@
         return;
     }
     CreateFd("RunCommandAsRootNonUserBuild.txt");
-    if (IsUserBuild()) {
+    if (PropertiesHelper::IsUserBuild()) {
         ALOGI("Skipping RunCommandAsRootNonUserBuild on user builds\n");
         return;
     }
 
     DropRoot();
 
-    EXPECT_EQ(
-        0, RunCommand({kSimpleCommand, "--uid"}, CommandOptions::WithTimeout(1).AsRoot().Build()));
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand, "--uid"},
+                            CommandOptions::WithTimeout(1).AsRoot().Build()));
 
     EXPECT_THAT(out, StrEq("0\nstdout\n"));
     EXPECT_THAT(err, StrEq("stderr\n"));
@@ -1068,59 +1062,78 @@
     // First check root case - only available when running with 'adb root'.
     uid_t uid = getuid();
     if (uid == 0) {
-        EXPECT_EQ(0, RunCommand({kSimpleCommand, "--uid"}));
+        EXPECT_EQ(0, RunCommand("", {kSimpleCommand, "--uid"}));
         EXPECT_THAT(out, StrEq("0\nstdout\n"));
         EXPECT_THAT(err, StrEq("stderr\n"));
         return;
     }
     // Then run dropping root.
-    EXPECT_EQ(0, RunCommand({kSimpleCommand, "--uid"},
+    EXPECT_EQ(0, RunCommand("", {kSimpleCommand, "--uid"},
                             CommandOptions::WithTimeout(1).DropRoot().Build()));
     EXPECT_THAT(out, StrEq("2000\nstdout\n"));
     EXPECT_THAT(err, StrEq("drop_root_user(): already running as Shell\nstderr\n"));
 }
 
-TEST_F(DumpstateUtilTest, DumpFileNotFound) {
+TEST_F(DumpstateUtilTest, DumpFileNotFoundNoTitle) {
     CreateFd("DumpFileNotFound.txt");
-    EXPECT_EQ(-1, DumpFile("/I/cant/believe/I/exist"));
+    EXPECT_EQ(-1, DumpFile("", "/I/cant/believe/I/exist"));
     EXPECT_THAT(out,
                 StrEq("*** Error dumping /I/cant/believe/I/exist: No such file or directory\n"));
     EXPECT_THAT(err, IsEmpty());
 }
 
+TEST_F(DumpstateUtilTest, DumpFileNotFoundWithTitle) {
+    CreateFd("DumpFileNotFound.txt");
+    EXPECT_EQ(-1, DumpFile("Y U NO EXIST?", "/I/cant/believe/I/exist"));
+    EXPECT_THAT(out, StrEq("*** Error dumping /I/cant/believe/I/exist (Y U NO EXIST?): No such "
+                           "file or directory\n"));
+    EXPECT_THAT(err, IsEmpty());
+}
+
 TEST_F(DumpstateUtilTest, DumpFileSingleLine) {
     CreateFd("DumpFileSingleLine.txt");
-    EXPECT_EQ(0, DumpFile(kTestDataPath + "single-line.txt"));
+    EXPECT_EQ(0, DumpFile("", kTestDataPath + "single-line.txt"));
     EXPECT_THAT(err, IsEmpty());
     EXPECT_THAT(out, StrEq("I AM LINE1\n"));  // dumpstate adds missing newline
 }
 
 TEST_F(DumpstateUtilTest, DumpFileSingleLineWithNewLine) {
     CreateFd("DumpFileSingleLineWithNewLine.txt");
-    EXPECT_EQ(0, DumpFile(kTestDataPath + "single-line-with-newline.txt"));
+    EXPECT_EQ(0, DumpFile("", kTestDataPath + "single-line-with-newline.txt"));
     EXPECT_THAT(err, IsEmpty());
     EXPECT_THAT(out, StrEq("I AM LINE1\n"));
 }
 
 TEST_F(DumpstateUtilTest, DumpFileMultipleLines) {
     CreateFd("DumpFileMultipleLines.txt");
-    EXPECT_EQ(0, DumpFile(kTestDataPath + "multiple-lines.txt"));
+    EXPECT_EQ(0, DumpFile("", kTestDataPath + "multiple-lines.txt"));
     EXPECT_THAT(err, IsEmpty());
     EXPECT_THAT(out, StrEq("I AM LINE1\nI AM LINE2\nI AM LINE3\n"));
 }
 
 TEST_F(DumpstateUtilTest, DumpFileMultipleLinesWithNewLine) {
     CreateFd("DumpFileMultipleLinesWithNewLine.txt");
-    EXPECT_EQ(0, DumpFile(kTestDataPath + "multiple-lines-with-newline.txt"));
+    EXPECT_EQ(0, DumpFile("", kTestDataPath + "multiple-lines-with-newline.txt"));
     EXPECT_THAT(err, IsEmpty());
     EXPECT_THAT(out, StrEq("I AM LINE1\nI AM LINE2\nI AM LINE3\n"));
 }
 
+TEST_F(DumpstateUtilTest, DumpFileOnDryRunNoTitle) {
+    CreateFd("DumpFileOnDryRun.txt");
+    SetDryRun(true);
+    std::string path = kTestDataPath + "single-line.txt";
+    EXPECT_EQ(0, DumpFile("", kTestDataPath + "single-line.txt"));
+    EXPECT_THAT(err, IsEmpty());
+    EXPECT_THAT(out, StrEq(path + ": skipped on dry run\n"));
+}
+
 TEST_F(DumpstateUtilTest, DumpFileOnDryRun) {
     CreateFd("DumpFileOnDryRun.txt");
     SetDryRun(true);
     std::string path = kTestDataPath + "single-line.txt";
-    EXPECT_EQ(0, DumpFile(kTestDataPath + "single-line.txt"));
+    EXPECT_EQ(0, DumpFile("Might as well dump. Dump!", kTestDataPath + "single-line.txt"));
     EXPECT_THAT(err, IsEmpty());
-    EXPECT_THAT(out, StrEq(path + ": skipped on dry run\n"));
+    EXPECT_THAT(
+        out, StartsWith("------ Might as well dump. Dump! (" + kTestDataPath + "single-line.txt:"));
+    EXPECT_THAT(out, EndsWith("skipped on dry run\n"));
 }
diff --git a/cmds/dumpstate/utils.cpp b/cmds/dumpstate/utils.cpp
index 2fa43ed..01a60da 100644
--- a/cmds/dumpstate/utils.cpp
+++ b/cmds/dumpstate/utils.cpp
@@ -14,60 +14,42 @@
  * limitations under the License.
  */
 
+#define LOG_TAG "dumpstate"
+
+#include "dumpstate.h"
+
 #include <dirent.h>
-#include <errno.h>
 #include <fcntl.h>
 #include <libgen.h>
-#include <limits.h>
 #include <math.h>
 #include <poll.h>
-#include <signal.h>
-#include <stdarg.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/capability.h>
 #include <sys/inotify.h>
 #include <sys/klog.h>
-#include <sys/prctl.h>
-#include <sys/stat.h>
-#include <sys/time.h>
-#include <sys/wait.h>
-#include <time.h>
-#include <unistd.h>
-#include <string>
-#include <vector>
-
-#define LOG_TAG "dumpstate"
 
 #include <android-base/file.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <cutils/debugger.h>
-#include <cutils/log.h>
 #include <cutils/properties.h>
 #include <cutils/sockets.h>
 #include <private/android_filesystem_config.h>
 
-#include <selinux/android.h>
-
-#include "dumpstate.h"
-
-#define SU_PATH "/system/xbin/su"
-
-static const int64_t NANOS_PER_SEC = 1000000000;
+#include "DumpstateInternal.h"
 
 static const int TRACE_DUMP_TIMEOUT_MS = 10000; // 10 seconds
 
+/* Most simple commands have 10 as timeout, so 5 is a good estimate */
+static const int32_t WEIGHT_FILE = 5;
+
 // TODO: temporary variables and functions used during C++ refactoring
 static Dumpstate& ds = Dumpstate::GetInstance();
 static int RunCommand(const std::string& title, const std::vector<std::string>& full_command,
                       const CommandOptions& options = CommandOptions::DEFAULT) {
     return ds.RunCommand(title, full_command, options);
 }
-static bool IsDryRun() {
-    return Dumpstate::GetInstance().IsDryRun();
+bool Dumpstate::IsUserBuild() {
+    return PropertiesHelper::IsUserBuild();
 }
 
 /* list of native processes to include in the native dumps */
@@ -86,112 +68,31 @@
         NULL,
 };
 
-/* Most simple commands have 10 as timeout, so 5 is a good estimate */
-static const int32_t WEIGHT_FILE = 5;
-
 // Reasonable value for max stats.
 static const int STATS_MAX_N_RUNS = 1000;
 static const long STATS_MAX_AVERAGE = 100000;
 
-CommandOptions CommandOptions::DEFAULT = CommandOptions::WithTimeout(10).Build();
 CommandOptions Dumpstate::DEFAULT_DUMPSYS = CommandOptions::WithTimeout(30).Build();
-CommandOptions CommandOptions::AS_ROOT_5 = CommandOptions::WithTimeout(5).AsRoot().Build();
-CommandOptions CommandOptions::AS_ROOT_10 = CommandOptions::WithTimeout(10).AsRoot().Build();
-CommandOptions CommandOptions::AS_ROOT_20 = CommandOptions::WithTimeout(20).AsRoot().Build();
 
-CommandOptions::CommandOptionsBuilder::CommandOptionsBuilder(int64_t timeout) : values(timeout) {
-}
-
-CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::Always() {
-    values.always_ = true;
-    return *this;
-}
-
-CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::AsRoot() {
-    values.account_mode_ = SU_ROOT;
-    return *this;
-}
-
-CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::DropRoot() {
-    values.account_mode_ = DROP_ROOT;
-    return *this;
-}
-
-CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::RedirectStderr() {
-    values.output_mode_ = REDIRECT_TO_STDERR;
-    return *this;
-}
-
-CommandOptions::CommandOptionsBuilder& CommandOptions::CommandOptionsBuilder::Log(
-    const std::string& message) {
-    values.logging_message_ = message;
-    return *this;
-}
-
-CommandOptions CommandOptions::CommandOptionsBuilder::Build() {
-    return CommandOptions(values);
-}
-
-CommandOptions::CommandOptionsValues::CommandOptionsValues(int64_t timeout)
-    : timeout_(timeout),
-      always_(false),
-      account_mode_(DONT_DROP_ROOT),
-      output_mode_(NORMAL_OUTPUT),
-      logging_message_("") {
-}
-
-CommandOptions::CommandOptions(const CommandOptionsValues& values) : values(values) {
-}
-
-int64_t CommandOptions::Timeout() const {
-    return values.timeout_;
-}
-
-bool CommandOptions::Always() const {
-    return values.always_;
-}
-
-PrivilegeMode CommandOptions::PrivilegeMode() const {
-    return values.account_mode_;
-}
-
-OutputMode CommandOptions::OutputMode() const {
-    return values.output_mode_;
-}
-
-std::string CommandOptions::LoggingMessage() const {
-    return values.logging_message_;
-}
-
-CommandOptions::CommandOptionsBuilder CommandOptions::WithTimeout(int64_t timeout) {
-    return CommandOptions::CommandOptionsBuilder(timeout);
-}
-
-Dumpstate::Dumpstate(const std::string& version, bool dry_run, const std::string& build_type)
-    : pid_(getpid()),
-      version_(version),
-      now_(time(nullptr)),
-      dry_run_(dry_run),
-      build_type_(build_type) {
+Dumpstate::Dumpstate(const std::string& version)
+    : pid_(getpid()), version_(version), now_(time(nullptr)) {
 }
 
 Dumpstate& Dumpstate::GetInstance() {
-    static Dumpstate singleton_(android::base::GetProperty("dumpstate.version", VERSION_CURRENT),
-                                android::base::GetBoolProperty("dumpstate.dry_run", false),
-                                android::base::GetProperty("ro.build.type", "(unknown)"));
+    static Dumpstate singleton_(android::base::GetProperty("dumpstate.version", VERSION_CURRENT));
     return singleton_;
 }
 
 DurationReporter::DurationReporter(const std::string& title, bool log_only)
     : title_(title), log_only_(log_only) {
     if (!title_.empty()) {
-        started_ = DurationReporter::Nanotime();
+        started_ = Nanotime();
     }
 }
 
 DurationReporter::~DurationReporter() {
     if (!title_.empty()) {
-        uint64_t elapsed = DurationReporter::Nanotime() - started_;
+        uint64_t elapsed = Nanotime() - started_;
         if (log_only_) {
             MYLOGD("Duration of '%s': %.3fs\n", title_.c_str(), (float)elapsed / NANOS_PER_SEC);
         } else {
@@ -203,12 +104,6 @@
     }
 }
 
-uint64_t DurationReporter::DurationReporter::Nanotime() {
-    struct timespec ts;
-    clock_gettime(CLOCK_MONOTONIC, &ts);
-    return (uint64_t) ts.tv_sec * NANOS_PER_SEC + ts.tv_nsec;
-}
-
 const int32_t Progress::kDefaultMax = 5000;
 
 Progress::Progress(const std::string& path) : Progress(Progress::kDefaultMax, 1.1, path) {
@@ -318,14 +213,6 @@
     dprintf(fd, "%saverage_max: %d\n", pr, average_max_);
 }
 
-bool Dumpstate::IsDryRun() const {
-    return dry_run_;
-}
-
-bool Dumpstate::IsUserBuild() const {
-    return "user" == build_type_;
-}
-
 bool Dumpstate::IsZipping() const {
     return zip_writer_ != nullptr;
 }
@@ -340,7 +227,7 @@
 }
 
 void for_each_userid(void (*func)(int), const char *header) {
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
 
     DIR *d;
     struct dirent *de;
@@ -423,7 +310,7 @@
 }
 
 void for_each_pid(for_each_pid_func func, const char *header) {
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
 
     __for_each_pid(for_each_pid_helper, header, (void *) func);
 }
@@ -477,13 +364,13 @@
 }
 
 void for_each_tid(for_each_tid_func func, const char *header) {
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
 
     __for_each_pid(for_each_tid_helper, header, (void *) func);
 }
 
 void show_wchan(int pid, int tid, const char *name) {
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
 
     char path[255];
     char buffer[255];
@@ -550,7 +437,7 @@
 }
 
 void show_showtime(int pid, const char *name) {
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
 
     char path[255];
     char buffer[1023];
@@ -618,7 +505,7 @@
     DurationReporter duration_reporter(title);
     printf("------ %s ------\n", title);
 
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
 
     /* Get size of kernel buffer */
     int size = klogctl(KLOG_SIZE_BUFFER, NULL, 0);
@@ -649,125 +536,15 @@
 
     snprintf(title, sizeof(title), "SHOW MAP %d (%s)", pid, name);
     snprintf(arg, sizeof(arg), "%d", pid);
-    RunCommand(title, {"showmap", "-q", arg}, CommandOptions::AS_ROOT_10);
-}
-
-// TODO: when converted to a Dumpstate function, it should be const
-static int _dump_file_from_fd_to_fd(const std::string& title, const char* path, int fd, int out_fd,
-                                    int32_t* duration) {
-    if (!title.empty()) {
-        dprintf(out_fd, "------ %s (%s", title.c_str(), path);
-
-        struct stat st;
-        // Only show the modification time of non-device files.
-        size_t path_len = strlen(path);
-        if ((path_len < 6 || memcmp(path, "/proc/", 6)) &&
-                (path_len < 5 || memcmp(path, "/sys/", 5)) &&
-                (path_len < 3 || memcmp(path, "/d/", 3)) &&
-                !fstat(fd, &st)) {
-            char stamp[80];
-            time_t mtime = st.st_mtime;
-            strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", localtime(&mtime));
-            dprintf(out_fd, ": %s", stamp);
-        }
-        dprintf(out_fd, ") ------\n");
-        fsync(out_fd);
-    }
-
-    if (IsDryRun()) {
-        if (out_fd != STDOUT_FILENO) {
-            // There is no title, but we should still print a dry-run message
-            dprintf(out_fd, "%s: skipped on dry run\n", path);
-        } else if (!title.empty()) {
-            dprintf(out_fd, "\t(skipped on dry run)\n");
-        }
-        fsync(out_fd);
-        if (duration != nullptr) {
-            *duration = WEIGHT_FILE;
-        }
-        return 0;
-    }
-
-    bool newline = false;
-    fd_set read_set;
-    struct timeval tm;
-    while (1) {
-        FD_ZERO(&read_set);
-        FD_SET(fd, &read_set);
-        /* Timeout if no data is read for 30 seconds. */
-        tm.tv_sec = 30;
-        tm.tv_usec = 0;
-        uint64_t elapsed = DurationReporter::Nanotime();
-        int ret = TEMP_FAILURE_RETRY(select(fd + 1, &read_set, NULL, NULL, &tm));
-        if (ret == -1) {
-            dprintf(out_fd, "*** %s: select failed: %s\n", path, strerror(errno));
-            newline = true;
-            break;
-        } else if (ret == 0) {
-            elapsed = DurationReporter::Nanotime() - elapsed;
-            dprintf(out_fd, "*** %s: Timed out after %.3fs\n", path, (float)elapsed / NANOS_PER_SEC);
-            newline = true;
-            break;
-        } else {
-            char buffer[65536];
-            ssize_t bytes_read = TEMP_FAILURE_RETRY(read(fd, buffer, sizeof(buffer)));
-            if (bytes_read > 0) {
-                android::base::WriteFully(out_fd, buffer, bytes_read);
-                newline = (buffer[bytes_read-1] == '\n');
-            } else {
-                if (bytes_read == -1) {
-                    dprintf(out_fd, "*** %s: Failed to read from fd: %s", path, strerror(errno));
-                    newline = true;
-                }
-                break;
-            }
-        }
-    }
-    if (duration != nullptr) {
-        *duration = WEIGHT_FILE;
-    }
-    close(fd);
-
-    if (!newline) dprintf(out_fd, "\n");
-    if (!title.empty()) dprintf(out_fd, "\n");
-    return 0;
-}
-
-// Internal function used by both Dumpstate::DumpFile and DumpFileToFd - the former prints title
-// information, while the latter doesn't.
-static int InternalDumpFileToFd(const std::string& title, int out_fd, const std::string& path,
-                                int32_t* duration) {
-    if (duration) {
-        *duration = 0;
-    }
-    int fd = TEMP_FAILURE_RETRY(open(path.c_str(), O_RDONLY | O_NONBLOCK | O_CLOEXEC));
-    if (fd < 0) {
-        int err = errno;
-        if (title.empty()) {
-            dprintf(out_fd, "*** Error dumping %s: %s\n", path.c_str(), strerror(err));
-        } else {
-            dprintf(out_fd, "*** Error dumping %s (%s): %s\n", path.c_str(), title.c_str(),
-                    strerror(err));
-        }
-        fsync(out_fd);
-        return -1;
-    }
-    return _dump_file_from_fd_to_fd(title, path.c_str(), fd, out_fd, duration);
-}
-
-int DumpFileToFd(int out_fd, const std::string& path) {
-    return InternalDumpFileToFd("", out_fd, path, nullptr);
+    RunCommand(title, {"showmap", "-q", arg}, CommandOptions::AS_ROOT);
 }
 
 int Dumpstate::DumpFile(const std::string& title, const std::string& path) {
     DurationReporter duration_reporter(title);
-    int32_t duration;
 
-    int status = InternalDumpFileToFd(title, STDOUT_FILENO, path, &duration);
+    int status = DumpFileToFd(STDOUT_FILENO, title, path);
 
-    if (duration > 0) {
-        UpdateProgress(duration);
-    }
+    UpdateProgress(WEIGHT_FILE);
 
     fflush(stdout);
 
@@ -813,7 +590,7 @@
     if (!title.empty()) {
         printf("------ %s (%s) ------\n", title.c_str(), dir);
     }
-    if (IsDryRun()) return 0;
+    if (PropertiesHelper::IsDryRun()) return 0;
 
     if (dir[strlen(dir) - 1] == '/') {
         ++slash;
@@ -870,7 +647,7 @@
  * stuck.
  */
 int dump_file_from_fd(const char *title, const char *path, int fd) {
-    if (IsDryRun()) return 0;
+    if (PropertiesHelper::IsDryRun()) return 0;
 
     int flags = fcntl(fd, F_GETFL);
     if (flags == -1) {
@@ -882,248 +659,26 @@
         close(fd);
         return -1;
     }
-    return _dump_file_from_fd_to_fd(title, path, fd, STDOUT_FILENO, nullptr);
-}
-
-bool waitpid_with_timeout(pid_t pid, int timeout_seconds, int* status) {
-    sigset_t child_mask, old_mask;
-    sigemptyset(&child_mask);
-    sigaddset(&child_mask, SIGCHLD);
-
-    if (sigprocmask(SIG_BLOCK, &child_mask, &old_mask) == -1) {
-        printf("*** sigprocmask failed: %s\n", strerror(errno));
-        return false;
-    }
-
-    struct timespec ts;
-    ts.tv_sec = timeout_seconds;
-    ts.tv_nsec = 0;
-    int ret = TEMP_FAILURE_RETRY(sigtimedwait(&child_mask, NULL, &ts));
-    int saved_errno = errno;
-    // Set the signals back the way they were.
-    if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) {
-        printf("*** sigprocmask failed: %s\n", strerror(errno));
-        if (ret == 0) {
-            return false;
-        }
-    }
-    if (ret == -1) {
-        errno = saved_errno;
-        if (errno == EAGAIN) {
-            errno = ETIMEDOUT;
-        } else {
-            printf("*** sigtimedwait failed: %s\n", strerror(errno));
-        }
-        return false;
-    }
-
-    pid_t child_pid = waitpid(pid, status, WNOHANG);
-    if (child_pid != pid) {
-        if (child_pid != -1) {
-            printf("*** Waiting for pid %d, got pid %d instead\n", pid, child_pid);
-        } else {
-            printf("*** waitpid failed: %s\n", strerror(errno));
-        }
-        return false;
-    }
-    return true;
-}
-
-// Internal function used by both Dumpstate::DumpFile and DumpFileToFd - the former prints title
-// information, while the latter doesn't.
-static int InternalRunCommandToFd(const std::string& title, int fd,
-                                  const std::vector<std::string>& full_command,
-                                  const CommandOptions& options, int32_t* duration) {
-    if (duration) {
-        *duration = 0;
-    }
-    if (full_command.empty()) {
-        MYLOGE("No arguments on RunCommandToFd(%s)\n", title.c_str());
-        return -1;
-    }
-
-    int size = full_command.size() + 1;  // null terminated
-    int starting_index = 0;
-    if (options.PrivilegeMode() == SU_ROOT) {
-        starting_index = 2;  // "su" "root"
-        size += starting_index;
-    }
-
-    std::vector<const char*> args;
-    args.resize(size);
-
-    std::string command_string;
-    if (options.PrivilegeMode() == SU_ROOT) {
-        args[0] = SU_PATH;
-        command_string += SU_PATH;
-        args[1] = "root";
-        command_string += " root ";
-    }
-    for (size_t i = 0; i < full_command.size(); i++) {
-        args[i + starting_index] = full_command[i].data();
-        command_string += args[i + starting_index];
-        if (i != full_command.size() - 1) {
-            command_string += " ";
-        }
-    }
-    args[size - 1] = nullptr;
-
-    const char* command = command_string.c_str();
-
-    if (options.PrivilegeMode() == SU_ROOT && ds.IsUserBuild()) {
-        dprintf(fd, "Skipping '%s' on user build.\n", command);
-        return 0;
-    }
-
-    if (!title.empty()) {
-        dprintf(fd, "------ %s (%s) ------\n", title.c_str(), command);
-        fsync(fd);
-    }
-
-    const std::string& logging_message = options.LoggingMessage();
-    if (!logging_message.empty()) {
-        MYLOGI(logging_message.c_str(), command_string.c_str());
-    }
-
-    /* TODO: for now we're simplifying the progress calculation by using the
-     * timeout as the weight. It's a good approximation for most cases, except when calling dumpsys,
-     * where its weight should be much higher proportionally to its timeout.
-     * Ideally, it should use a options.EstimatedDuration() instead...*/
-    if (duration != nullptr) {
-        *duration = options.Timeout();
-    }
-
-    bool silent = (options.OutputMode() == REDIRECT_TO_STDERR);
-    bool redirecting_to_fd = STDOUT_FILENO != fd;
-
-    if (IsDryRun() && !options.Always()) {
-        if (redirecting_to_fd) {
-            // There is no title, but we should still print a dry-run message
-            dprintf(fd, "%s: skipped on dry run\n", command_string.c_str());
-        } else if (!title.empty()) {
-            dprintf(fd, "\t(skipped on dry run)\n");
-        }
-        fsync(fd);
-        return 0;
-    }
-
-    const char* path = args[0];
-
-    uint64_t start = DurationReporter::Nanotime();
-    pid_t pid = fork();
-
-    /* handle error case */
-    if (pid < 0) {
-        if (!silent) dprintf(fd, "*** fork: %s\n", strerror(errno));
-        MYLOGE("*** fork: %s\n", strerror(errno));
-        return pid;
-    }
-
-    /* handle child case */
-    if (pid == 0) {
-        if (options.PrivilegeMode() == DROP_ROOT && !drop_root_user()) {
-            if (!silent) {
-                dprintf(fd, "*** failed to drop root before running %s: %s\n", command,
-                        strerror(errno));
-            }
-            MYLOGE("*** could not drop root before running %s: %s\n", command, strerror(errno));
-            return -1;
-        }
-
-        if (silent) {
-            // Redirects stdout to stderr
-            TEMP_FAILURE_RETRY(dup2(STDERR_FILENO, STDOUT_FILENO));
-        } else if (redirecting_to_fd) {
-            // Redirect stdout to fd
-            TEMP_FAILURE_RETRY(dup2(fd, STDOUT_FILENO));
-            close(fd);
-        }
-
-        /* make sure the child dies when dumpstate dies */
-        prctl(PR_SET_PDEATHSIG, SIGKILL);
-
-        /* just ignore SIGPIPE, will go down with parent's */
-        struct sigaction sigact;
-        memset(&sigact, 0, sizeof(sigact));
-        sigact.sa_handler = SIG_IGN;
-        sigaction(SIGPIPE, &sigact, NULL);
-
-        execvp(path, (char**)args.data());
-        // execvp's result will be handled after waitpid_with_timeout() below, but
-        // if it failed, it's safer to exit dumpstate.
-        MYLOGD("execvp on command '%s' failed (error: %s)\n", command, strerror(errno));
-        // Must call _exit (instead of exit), otherwise it will corrupt the zip
-        // file.
-        _exit(EXIT_FAILURE);
-    }
-
-    /* handle parent case */
-    int status;
-    bool ret = waitpid_with_timeout(pid, options.Timeout(), &status);
-    fsync(fd);
-
-    uint64_t elapsed = DurationReporter::Nanotime() - start;
-    if (!ret) {
-        if (errno == ETIMEDOUT) {
-            if (!silent)
-                dprintf(fd, "*** command '%s' timed out after %.3fs (killing pid %d)\n", command,
-                        (float)elapsed / NANOS_PER_SEC, pid);
-            MYLOGE("*** command '%s' timed out after %.3fs (killing pid %d)\n", command,
-                   (float)elapsed / NANOS_PER_SEC, pid);
-        } else {
-            if (!silent)
-                dprintf(fd, "*** command '%s': Error after %.4fs (killing pid %d)\n", command,
-                        (float)elapsed / NANOS_PER_SEC, pid);
-            MYLOGE("command '%s': Error after %.4fs (killing pid %d)\n", command,
-                   (float)elapsed / NANOS_PER_SEC, pid);
-        }
-        kill(pid, SIGTERM);
-        if (!waitpid_with_timeout(pid, 5, nullptr)) {
-            kill(pid, SIGKILL);
-            if (!waitpid_with_timeout(pid, 5, nullptr)) {
-                if (!silent)
-                    dprintf(fd, "could not kill command '%s' (pid %d) even with SIGKILL.\n",
-                            command, pid);
-                MYLOGE("could not kill command '%s' (pid %d) even with SIGKILL.\n", command, pid);
-            }
-        }
-        return -1;
-    }
-
-    if (WIFSIGNALED(status)) {
-        if (!silent)
-            dprintf(fd, "*** command '%s' failed: killed by signal %d\n", command, WTERMSIG(status));
-        MYLOGE("*** command '%s' failed: killed by signal %d\n", command, WTERMSIG(status));
-    } else if (WIFEXITED(status) && WEXITSTATUS(status) > 0) {
-        status = WEXITSTATUS(status);
-        if (!silent) dprintf(fd, "*** command '%s' failed: exit code %d\n", command, status);
-        MYLOGE("*** command '%s' failed: exit code %d\n", command, status);
-    }
-
-    return status;
+    return DumpFileFromFdToFd(title, path, fd, STDOUT_FILENO, PropertiesHelper::IsDryRun());
 }
 
 int Dumpstate::RunCommand(const std::string& title, const std::vector<std::string>& full_command,
                           const CommandOptions& options) {
     DurationReporter duration_reporter(title);
 
-    int32_t duration;
-    int status = InternalRunCommandToFd(title, STDOUT_FILENO, full_command, options, &duration);
+    int status = RunCommandToFd(STDOUT_FILENO, title, full_command, options);
 
-    if (duration > 0) {
-        UpdateProgress(duration);
-    }
+    /* TODO: for now we're simplifying the progress calculation by using the
+     * timeout as the weight. It's a good approximation for most cases, except when calling dumpsys,
+     * where its weight should be much higher proportionally to its timeout.
+     * Ideally, it should use a options.EstimatedDuration() instead...*/
+    UpdateProgress(options.Timeout());
 
     fflush(stdout);
 
     return status;
 }
 
-int RunCommandToFd(int fd, const std::vector<std::string>& full_command,
-                   const CommandOptions& options) {
-    return InternalRunCommandToFd("", fd, full_command, options, nullptr);
-}
-
 void Dumpstate::RunDumpsys(const std::string& title, const std::vector<std::string>& dumpsys_args,
                            const CommandOptions& options, long dumpsysTimeout) {
     long timeout = dumpsysTimeout > 0 ? dumpsysTimeout : options.Timeout();
@@ -1132,55 +687,6 @@
     RunCommand(title, dumpsys, options);
 }
 
-bool drop_root_user() {
-    if (getgid() == AID_SHELL && getuid() == AID_SHELL) {
-        MYLOGD("drop_root_user(): already running as Shell\n");
-        return true;
-    }
-    /* ensure we will keep capabilities when we drop root */
-    if (prctl(PR_SET_KEEPCAPS, 1) < 0) {
-        MYLOGE("prctl(PR_SET_KEEPCAPS) failed: %s\n", strerror(errno));
-        return false;
-    }
-
-    gid_t groups[] = { AID_LOG, AID_SDCARD_R, AID_SDCARD_RW,
-            AID_MOUNT, AID_INET, AID_NET_BW_STATS, AID_READPROC, AID_WAKELOCK,
-            AID_BLUETOOTH };
-    if (setgroups(sizeof(groups)/sizeof(groups[0]), groups) != 0) {
-        MYLOGE("Unable to setgroups, aborting: %s\n", strerror(errno));
-        return false;
-    }
-    if (setgid(AID_SHELL) != 0) {
-        MYLOGE("Unable to setgid, aborting: %s\n", strerror(errno));
-        return false;
-    }
-    if (setuid(AID_SHELL) != 0) {
-        MYLOGE("Unable to setuid, aborting: %s\n", strerror(errno));
-        return false;
-    }
-
-    struct __user_cap_header_struct capheader;
-    struct __user_cap_data_struct capdata[2];
-    memset(&capheader, 0, sizeof(capheader));
-    memset(&capdata, 0, sizeof(capdata));
-    capheader.version = _LINUX_CAPABILITY_VERSION_3;
-    capheader.pid = 0;
-
-    capdata[CAP_TO_INDEX(CAP_SYSLOG)].permitted =
-            (CAP_TO_MASK(CAP_SYSLOG) | CAP_TO_MASK(CAP_BLOCK_SUSPEND));
-    capdata[CAP_TO_INDEX(CAP_SYSLOG)].effective =
-            (CAP_TO_MASK(CAP_SYSLOG) | CAP_TO_MASK(CAP_BLOCK_SUSPEND));
-    capdata[0].inheritable = 0;
-    capdata[1].inheritable = 0;
-
-    if (capset(&capheader, &capdata[0]) < 0) {
-        MYLOGE("capset failed: %s\n", strerror(errno));
-        return false;
-    }
-
-    return true;
-}
-
 void send_broadcast(const std::string& action, const std::vector<std::string>& args) {
     std::vector<std::string> am = {"/system/bin/am", "broadcast", "--user", "0", "-a", action};
 
@@ -1215,7 +721,7 @@
     const char* title = "SYSTEM PROPERTIES";
     DurationReporter duration_reporter(title);
     printf("------ %s ------\n", title);
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
     size_t i;
     num_props = 0;
     property_list(print_prop, NULL);
@@ -1402,7 +908,7 @@
             }
 
             ++dalvik_found;
-            uint64_t start = DurationReporter::Nanotime();
+            uint64_t start = Nanotime();
             if (kill(pid, SIGQUIT)) {
                 MYLOGE("kill(%d, SIGQUIT): %s\n", pid, strerror(errno));
                 continue;
@@ -1424,7 +930,7 @@
                 MYLOGE("lseek: %s\n", strerror(errno));
             } else {
                 dprintf(fd, "[dump dalvik stack %d: %.3fs elapsed]\n", pid,
-                        (float)(DurationReporter::Nanotime() - start) / NANOS_PER_SEC);
+                        (float)(Nanotime() - start) / NANOS_PER_SEC);
             }
         } else if (should_dump_native_traces(data)) {
             /* dump native process if appropriate */
@@ -1432,7 +938,7 @@
                 MYLOGE("lseek: %s\n", strerror(errno));
             } else {
                 static uint16_t timeout_failures = 0;
-                uint64_t start = DurationReporter::Nanotime();
+                uint64_t start = Nanotime();
 
                 /* If 3 backtrace dumps fail in a row, consider debuggerd dead. */
                 if (timeout_failures == 3) {
@@ -1444,7 +950,7 @@
                     timeout_failures = 0;
                 }
                 dprintf(fd, "[dump native stack %d: %.3fs elapsed]\n", pid,
-                        (float)(DurationReporter::Nanotime() - start) / NANOS_PER_SEC);
+                        (float)(Nanotime() - start) / NANOS_PER_SEC);
             }
         }
     }
@@ -1474,7 +980,7 @@
 
 void dump_route_tables() {
     DurationReporter duration_reporter("DUMP ROUTE TABLES");
-    if (IsDryRun()) return;
+    if (PropertiesHelper::IsDryRun()) return;
     const char* const RT_TABLES_PATH = "/data/misc/net/rt_tables";
     ds.DumpFile("RT_TABLES", RT_TABLES_PATH);
     FILE* fp = fopen(RT_TABLES_PATH, "re");