init: Move developer logic into dev_utils.sh and load on demand.

The chromeos_startup has been bloated with many developer-specific logic
that should be ignored in normal path.

This change tries to move them to a new utility library (dev_utils.sh)
and only loaded if the system boots with CROS_DEBUG=1. Also changed most
shell variables to local variables in functions.

BUG=chromium:705414
TEST=Manually boots a device in normal mode and developer mode.
     Tested dev_gather_logs:
       - echo '/var/log' >/mnt/stateful_partition/.gatherme
       - reboot and found files in unencrypted/prior_lots
     Tested dev_mount_packages:
       - boot and see /usr/local, and symlink of /var/lib/portage
     Tested dev_check_block_dev_mode:
       - crossystem block_devmode=1; reboot
       - See blocking messages.

Change-Id: Ic170fc6f58df5e2fd3cbd3b979f4faaf1e202d9f
Reviewed-on: https://chromium-review.googlesource.com/448440
Commit-Ready: Hung-Te Lin <hungte@chromium.org>
Tested-by: Hung-Te Lin <hungte@chromium.org>
Reviewed-by: Hung-Te Lin <hungte@chromium.org>
Reviewed-by: Mattias Nissler <mnissler@chromium.org>
diff --git a/init/chromeos_startup b/init/chromeos_startup
index 0a93fca..51d7857 100755
--- a/init/chromeos_startup
+++ b/init/chromeos_startup
@@ -74,6 +74,17 @@
 crossystem "cros_debug?1"
 CROS_DEBUG=$((! $?))
 
+# Developer mode functions (defined in dev_utils.sh and will be loaded
+# only when CROS_DEBUG=1).
+dev_check_block_dev_mode() { true; }
+dev_update_stateful_partition() { true; }
+dev_gather_logs() { true; }
+dev_mount_packages() { true; }
+
+if [ "${CROS_DEBUG}" -eq 1 ]; then
+  . /usr/share/cros/dev_utils.sh
+fi
+
 # Prepare to mount stateful partition
 ROOT_DEV=$(rootdev -s)
 ROOTDEV_RET_CODE=$?
@@ -158,45 +169,11 @@
   date 010200001970.00
 fi
 
-# This file indicates a blocked developer mode transition attempt has occurred.
+# The file indicates a blocked developer mode transition attempt has occurred.
 BLOCKED_DEV_MODE_FILE="/mnt/stateful_partition/.blocked_dev_mode"
 
-# Check whether the device is allowed to boot in dev mode.
-# 1. If a debug build is already installed on the system, ignore block_devmode.
-#    It is pointless in this case, as the device is already in a state where the
-#    local user has full control.
-# 2. According to recovery mode only boot with signed images, the block_devmode
-#    could be ignored here -- otherwise factory shim will be blocked expecially
-#    that RMA center can't reset this device.
-#
-# The up-front CROS_DEBUG check avoids forking a crossystem process in verified
-# mode, thus keeping the check as lightweight as possible for normal boot.
-if [ $CROS_DEBUG -eq 1 ] && \
-   crossystem "devsw_boot?1" "debug_build?0" "recovery_reason?0"; then
-  # Checks ordered by run time: First try reading VPD through sysfs.
-  VPD_BLOCK_DEVMODE_FILE=/sys/firmware/vpd/rw/block_devmode
-  if [ -f "${VPD_BLOCK_DEVMODE_FILE}" ] &&
-     [ "$(cat "${VPD_BLOCK_DEVMODE_FILE}")" = "1" ]; then
-    BLOCK_DEVMODE=1
-  # Second try crossystem.
-  elif crossystem "block_devmode?1"; then
-    BLOCK_DEVMODE=1
-  # Third re-read VPD directly from SPI flash (slow!) but only for systems that
-  # don't have VPD in sysfs and only when NVRAM indicates that it has been
-  # cleared.
-  elif [ ! -d /sys/firmware/vpd/rw ] &&
-     crossystem "nvram_cleared?1" &&
-     [ "$(vpd -i RW_VPD -g block_devmode)" = "1" ]; then
-    BLOCK_DEVMODE=1
-  fi
-
-  if [ -n "${BLOCK_DEVMODE}" ]; then
-    # Put a flag file into place that will trigger a stateful partition wipe
-    # after reboot in verified mode.
-    touch ${BLOCKED_DEV_MODE_FILE}
-    chromeos-boot-alert block_devmode
-  fi
-fi
+# Checks if developer mode is blocked.
+dev_check_block_dev_mode "${BLOCKED_DEV_MODE_FILE}"
 
 # 'firmware-boot-update' is provided by chromeos-firmware for legacy systems.
 # On most new boards, it should be simply an empty file.
@@ -267,63 +244,8 @@
   exec clobber-state "$ARGS"
 fi
 
-# Check if we have an update to stateful pending.
-STATEFUL_UPDATE="/mnt/stateful_partition/.update_available"
-if [ $CROS_DEBUG -eq 1 -a -f "$STATEFUL_UPDATE" ] ; then
-  # To remain compatible with the prior update_stateful tarballs, expect
-  # the "var_new" unpack location, but move it into the new "var_overlay"
-  # target location.
-  VAR_TARGET="/mnt/stateful_partition/var"
-  VAR_NEW="${VAR_TARGET}_new"
-  VAR_OLD="${VAR_TARGET}_old"
-  VAR_TARGET="${VAR_TARGET}_overlay"
-  DEVELOPER_TARGET="/mnt/stateful_partition/dev_image"
-  DEVELOPER_NEW="${DEVELOPER_TARGET}_new"
-  DEVELOPER_OLD="${DEVELOPER_TARGET}_old"
-  STATEFUL_UPDATE_ARGS=$(cat "$STATEFUL_UPDATE")
-
-  # Only replace the developer and var_overlay directories if new replacements
-  # are available.
-  if [ -d "$DEVELOPER_NEW" -a -d "$VAR_NEW" ]; then
-    clobber-log -- "Updating from $DEVELOPER_NEW && $VAR_NEW."
-    rm -rf "$DEVELOPER_OLD" "$VAR_OLD"
-    mv "$VAR_TARGET" "$VAR_OLD" || true
-    mv "$DEVELOPER_TARGET" "$DEVELOPER_OLD" || true
-    mv "$VAR_NEW" "$VAR_TARGET"
-    mv "$DEVELOPER_NEW" "$DEVELOPER_TARGET"
-  else
-    clobber-log -- "Stateful update did not find $DEVELOPER_NEW && $VAR_NEW."
-    clobber-log -- "Keeping old development tools."
-  fi
-
-  # Check for clobber.
-  if [ "$STATEFUL_UPDATE_ARGS" = "clobber" ] ; then
-    PRESERVE_DIR="/mnt/stateful_partition/unencrypted/preserve"
-
-    # Find everything in stateful and delete it, except for protected paths, and
-    # non-empty directories. The non-empty directories contain protected content
-    # or they would already be empty from depth first traversal.
-
-    find "/mnt/stateful_partition"  -depth -mindepth 1 \
-        -not -path "/mnt/stateful_partition/.labmachine" \
-        -not -path "${DEVELOPER_TARGET}/*" \
-        -not -path "${VAR_TARGET}/*" \
-        -not -path "${PRESERVE_DIR}/*" \
-        -not -type d -print0 | xargs --null -r rm -f
-
-    find "/mnt/stateful_partition"  -depth -mindepth 1 \
-        -not -path "${DEVELOPER_TARGET}/*" \
-        -not -path "${VAR_TARGET}/*" \
-        -not -path "${PRESERVE_DIR}/*" \
-        -type d -print0 | xargs --null -r rmdir --ignore-fail-on-non-empty
-
-    # Let's really be done before coming back.
-    sync
-  fi
-
-  # Backgrounded to take off boot path.
-  rm -rf "$STATEFUL_UPDATE" "$DEVELOPER_OLD" "$VAR_OLD" &
-fi
+# Checks and updates stateful partition.
+dev_update_stateful_partition
 
 # Make sure unencrypted stateful partition has the needed common directories.
 # Any non-common directories should be created in the device implementation of
@@ -342,23 +264,8 @@
 remember_mount /home/chronos
 mount_var_and_home_chronos ${FACTORY_MODE} || cleanup_mounts "var and home"
 
-# For dev/test images, if .gatherme presents, copy files listed in .gatherme to
-# /mnt/stateful_partition/unencrypted/prior_logs.
-LAB_PRESERVE_LOGS="/mnt/stateful_partition/.gatherme"
-PRIOR_LOG_DIR="/mnt/stateful_partition/unencrypted/prior_logs"
-
-if [ ${CROS_DEBUG} -eq 1 -a -f "${LAB_PRESERVE_LOGS}" ]; then
-  for log_path in $(sed -e '/^#/ d' -e '/^$/ d' "${LAB_PRESERVE_LOGS}"); do
-    if [ -d "${log_path}" ]; then
-      cp -a -r --parents "${log_path}" "${PRIOR_LOG_DIR}" || true
-    elif [ -f "${log_path}" ]; then
-      cp -a "${log_path}" "${PRIOR_LOG_DIR}" || true
-    fi
-  done
-  rm -rf /var/*
-  rm -rf /home/chronos/*
-  rm "${LAB_PRESERVE_LOGS}"
-fi
+# Gather log if needed.
+dev_gather_logs
 
 # /run is now tmpfs used for runtime data. Make sure /var/run and /var/lock
 # are sym links to /run and /run/lock respectively for backwards compatibility.
@@ -396,50 +303,8 @@
 # "--make-shared" to let ARC container access mount points under /media.
 mount --make-shared -n -t tmpfs -o nodev,noexec,nosuid media /media
 
-# Mount stateful partition for dev packages.
-if [ ${CROS_DEBUG} -eq 1 ]; then
-  # Set up the logging dir that ASAN compiled programs will write to.  We want
-  # any privileged account to be able to write here so unittests need not worry
-  # about settings things up ahead of time.  See crbug.com/453579 for details.
-  mkdir -p /var/log/asan
-  chmod 1777 /var/log/asan
-
-  # Capture a snapshot of "normal" mount state here, for auditability,
-  # before we start applying devmode-specific changes.
-  cat /proc/mounts > /var/log/mount_options.log
-  # Create dev_image directory in base images in developer mode.
-  if [ ! -d /mnt/stateful_partition/dev_image ]; then
-    mkdir -p -m 0755 /mnt/stateful_partition/dev_image
-  fi
-  # Mount and then remount to enable exec/suid.
-  mount_or_fail --bind /mnt/stateful_partition/dev_image /usr/local
-  mount -n -o remount,exec,suid /usr/local
-
-  # Set up /var elements needed by gmerge.
-  # TODO(keescook) Use dev/test package installs instead of piling more
-  # things here (crosbug.com/14091).
-  BASE=/mnt/stateful_partition/var_overlay
-  if [ -d ${BASE} ]; then
-    # Keep this list in sync with the var_overlay elements in the DIRLIST
-    # found in chromeos-install from chromeos-base/chromeos-installer.
-    DIRLIST="
-      db/pkg
-      lib/portage
-    "
-    for DIR in ${DIRLIST}; do
-      if [ ! -d ${BASE}/${DIR} ]; then
-        continue
-      fi
-      DEST=/var/${DIR}
-      if [ -e ${DEST} ]; then
-        continue
-      fi
-      PARENT=$(dirname ${DEST})
-      mkdir -p ${PARENT}
-      ln -sf ${BASE}/${DIR} ${DEST}
-    done
-  fi
-fi
+# Mount dev packages.
+dev_mount_packages
 
 bootstat post-startup
 
diff --git a/init/dev_utils.sh b/init/dev_utils.sh
new file mode 100644
index 0000000..9ac1792
--- /dev/null
+++ b/init/dev_utils.sh
@@ -0,0 +1,186 @@
+#!/bin/sh
+# Copyright 2017 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.
+
+# Utility functions for chromeos_startup to run in developer mode.
+
+STATEFUL_PARTITION="/mnt/stateful_partition"
+
+# Check whether the device is allowed to boot in dev mode.
+# 1. If a debug build is already installed on the system, ignore block_devmode.
+#    It is pointless in this case, as the device is already in a state where the
+#    local user has full control.
+# 2. According to recovery mode only boot with signed images, the block_devmode
+#    could be ignored here -- otherwise factory shim will be blocked expecially
+#    that RMA center can't reset this device.
+#
+# Usage: dev_check_block_dev_mode BLOCKED_DEV_MODE_FILE
+dev_check_block_dev_mode() {
+  if ! crossystem "devsw_boot?1" "debug_build?0" "recovery_reason?0"; then
+    return
+  fi
+
+  # The file indicates a blocked developer mode transition attempt has occurred.
+  local blocked_dev_mode_file="$1"
+  local vpd_block_devmode_file=/sys/firmware/vpd/rw/block_devmode
+  local block_devmode=
+
+  # Checks ordered by run time: First try reading VPD through sysfs.
+  if [ -f "${vpd_block_devmode_file}" ] &&
+     [ "$(cat "${vpd_block_devmode_file}")" = "1" ]; then
+    block_devmode=1
+  # Second try crossystem.
+  elif crossystem "block_devmode?1"; then
+    block_devmode=1
+  # Third re-read VPD directly from SPI flash (slow!) but only for systems
+  # that don't have VPD in sysfs and only when NVRAM indicates that it has
+  # been cleared.
+  elif [ ! -d /sys/firmware/vpd/rw ] &&
+     crossystem "nvram_cleared?1" &&
+     [ "$(vpd -i RW_VPD -g block_devmode)" = "1" ]; then
+    block_devmode=1
+  fi
+
+  if [ -n "${block_devmode}" ]; then
+    # Put a flag file into place that will trigger a stateful partition wipe
+    # after reboot in verified mode.
+    touch "${blocked_dev_mode_file}"
+    chromeos-boot-alert block_devmode
+  fi
+}
+
+# Updates stateful partition if pending update is available.
+dev_update_stateful_partition() {
+  local stateful_update_file="${STATEFUL_PARTITION}/.update_available"
+  if [ ! -f "${stateful_update_file}" ]; then
+    return
+  fi
+
+  # To remain compatible with the prior update_stateful tarballs, expect
+  # the "var_new" unpack location, but move it into the new "var_overlay"
+  # target location.
+  local var_target="${STATEFUL_PARTITION}/var"
+  local var_new="${var_target}_new"
+  local var_old="${var_target}_old"
+  local var_target="${var_target}_overlay"
+  local developer_target="${STATEFUL_PARTITION}/dev_image"
+  local developer_new="${developer_target}_new"
+  local developer_old="${developer_target}_old"
+  local stateful_update_args=$(cat "${stateful_update_file}")
+
+  # Only replace the developer and var_overlay directories if new replacements
+  # are available.
+  if [ -d "${developer_new}" -a -d "${var_new}" ]; then
+    clobber-log -- "Updating from ${developer_new} && ${var_new}."
+    rm -rf "${developer_old}" "${var_old}"
+    mv "${var_target}" "${var_old}" || true
+    mv "${developer_target}" "${developer_old}" || true
+    mv "${var_new}" "${var_target}"
+    mv "${developer_new}" "${developer_target}"
+  else
+    clobber-log -- "Stateful update did not find ${developer_new} & ${var_new}."
+    clobber-log -- "Keeping old development tools."
+  fi
+
+  # Check for clobber.
+  if [ "${stateful_update_args}" = "clobber" ]; then
+    local preserve_dir="${STATEFUL_PARTITION}/unencrypted/preserve"
+
+    # Find everything in stateful and delete it, except for protected paths, and
+    # non-empty directories. The non-empty directories contain protected content
+    # or they would already be empty from depth first traversal.
+
+    find "${STATEFUL_PARTITION}" -depth -mindepth 1 \
+        -not -path "${STATEFUL_PARTITION}/.labmachine" \
+        -not -path "${developer_target}/*" \
+        -not -path "${var_target}/*" \
+        -not -path "${preserve_dir}/*" \
+        -not -type d -print0 | xargs -0 -r rm -f
+
+    find "${STATEFUL_PARTITION}" -depth -mindepth 1 \
+        -not -path "${developer_target}/*" \
+        -not -path "${var_target}/*" \
+        -not -path "${preserve_dir}/*" \
+        -type d -print0 | xargs -0 -r rmdir --ignore-fail-on-non-empty
+
+    # Let's really be done before coming back.
+    sync
+  fi
+
+  # Backgrounded to take off boot path.
+  rm -rf "${stateful_update_file}" "${developer_old}" "${var_old}" &
+}
+
+# Gather logs.
+dev_gather_logs() {
+  # For dev/test images, if .gatherme presents, copy files listed in .gatherme
+  # to ${STATEFUL_PARTITION}/unencrypted/prior_logs.
+  local lab_preserve_logs="${STATEFUL_PARTITION}/.gatherme"
+  local prior_log_dir="${STATEFUL_PARTITION}/unencrypted/prior_logs"
+  local log_path
+
+  if [ ! -f "${lab_preserve_logs}" ]; then
+    return
+  fi
+
+  sed -e '/^#/ d' -e '/^$/ d' "${lab_preserve_logs}" | while read log_path; do
+    if [ -d "${log_path}" ]; then
+      cp -a -r --parents "${log_path}" "${prior_log_dir}" || true
+    elif [ -f "${log_path}" ]; then
+      cp -a "${log_path}" "${prior_log_dir}" || true
+    fi
+  done
+  # shellcheck disable=SC2115
+  rm -rf /var/*
+  rm -rf /home/chronos/*
+  rm "${lab_preserve_logs}"
+}
+
+# Mount stateful partition for dev packages.
+dev_mount_packages() {
+  # Set up the logging dir that ASAN compiled programs will write to.  We want
+  # any privileged account to be able to write here so unittests need not worry
+  # about settings things up ahead of time.  See crbug.com/453579 for details.
+  mkdir -p /var/log/asan
+  chmod 1777 /var/log/asan
+
+  # Capture a snapshot of "normal" mount state here, for auditability,
+  # before we start applying devmode-specific changes.
+  cat /proc/mounts > /var/log/mount_options.log
+
+  # Create dev_image directory in base images in developer mode.
+  if [ ! -d "${stateful_partition}/dev_image" ]; then
+    mkdir -p -m 0755 "${STATEFUL_PARTITION}/dev_image"
+  fi
+
+  # Mount and then remount to enable exec/suid.
+  mount_or_fail --bind ${STATEFUL_PARTITION}/dev_image /usr/local
+  mount -n -o remount,exec,suid /usr/local
+
+  # Set up /var elements needed by gmerge.
+  # TODO(keescook) Use dev/test package installs instead of piling more
+  # things here (crosbug.com/14091).
+  local base="${STATEFUL_PARTITION}/var_overlay"
+  if [ -d "${base}" ]; then
+    # Keep this list in sync with the var_overlay elements in the DIRLIST
+    # found in chromeos-install from chromeos-base/chromeos-installer.
+    local dir dest parent
+    local dirlist="
+      db/pkg
+      lib/portage
+    "
+    for dir in ${dirlist}; do
+      if [ ! -d "${base}/${dir}" ]; then
+        continue
+      fi
+      dest="/var/${dir}"
+      if [ -e "${dest}" ]; then
+        continue
+      fi
+      parent="$(dirname "${dest}")"
+      mkdir -p "${parent}"
+      ln -sf "${base}/${dir}" "${dest}"
+    done
+  fi
+}