Commit 26c8b12c authored by Erik Jensen's avatar Erik Jensen Committed by Commit Bot

Implement wait-for-logs functionality in user_session binary

This ports the wait_for_logs method from the Python script, and forwards
the pipe write fd to the Python script.

Change-Id: Ief54da136894cd13e0761b9755aa3c7d51fe17f8
Reviewed-on: https://chromium-review.googlesource.com/594807Reviewed-by: default avatarLambros Lambrou <lambroslambrou@chromium.org>
Reviewed-by: default avatarJamie Walch <jamiewalch@chromium.org>
Commit-Queue: Erik Jensen <rkjnsn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#491058}
parent 28048d21
...@@ -130,6 +130,11 @@ MAX_LAUNCH_FAILURES = SHORT_BACKOFF_THRESHOLD + 10 ...@@ -130,6 +130,11 @@ MAX_LAUNCH_FAILURES = SHORT_BACKOFF_THRESHOLD + 10
# Number of seconds to save session output to the log. # Number of seconds to save session output to the log.
SESSION_OUTPUT_TIME_LIMIT_SECONDS = 30 SESSION_OUTPUT_TIME_LIMIT_SECONDS = 30
# This is the file descriptor used to pass messages to the user_session binary
# during startup. It must be kept in sync with kMessageFd in
# remoting_user_session.cc.
USER_SESSION_MESSAGE_FD = 202
# Globals needed by the atexit cleanup() handler. # Globals needed by the atexit cleanup() handler.
g_desktop = None g_desktop = None
g_host_hash = hashlib.md5(socket.gethostname()).hexdigest() g_host_hash = hashlib.md5(socket.gethostname()).hexdigest()
...@@ -885,23 +890,46 @@ class ParentProcessLogger(object): ...@@ -885,23 +890,46 @@ class ParentProcessLogger(object):
daemon removes the log-handler and closes the pipe, causing the the parent daemon removes the log-handler and closes the pipe, causing the the parent
process to reach end-of-file while reading the pipe and exit. process to reach end-of-file while reading the pipe and exit.
The (singleton) logger should be instantiated before forking. The parent When daemonizing, the (singleton) logger should be instantiated before
process should call wait_for_logs() before exiting. The (grand-)child process forking. The parent process should call wait_for_logs() before exiting. When
should call start_logging() when it starts, and then use logging.* to issue running via user-session, the file descriptor for the pipe to the parent
log statements, as usual. When the child has either succesfully started the process should be passed to the constructor. In the latter case, wait_for_logs
host or terminated, it must call release_parent() to allow the parent to exit. may not be used.
In either case, the (grand-)child process should call start_logging() when it
starts, and then use logging.* to issue log statements, as usual. When the
child has either succesfully started the host or terminated, it must call
release_parent() to allow the parent to exit.
""" """
__instance = None __instance = None
def __init__(self): def __init__(self, write_fd=None):
"""Constructor. Must be called before forking.""" """Constructor. When daemonizing, must be called before forking.
read_pipe, write_pipe = os.pipe()
Constructs the singleton instance of ParentProcessLogger. This should be
called at most once.
write_fd: If specified, the logger will use the file descriptor provided
for sending log messages instead of creating a new pipe. It is
assumed that this is the write end of a pipe created by an
already-existing parent process, such as user-session. If
write_fd is not a valid file descriptor, the constructor will
throw either IOError or OSError.
"""
if write_fd is None:
read_pipe, write_pipe = os.pipe()
else:
read_pipe = None
write_pipe = write_fd
# Ensure write_pipe is closed on exec, otherwise it will be kept open by # Ensure write_pipe is closed on exec, otherwise it will be kept open by
# child processes (X, host), preventing the read pipe from EOF'ing. # child processes (X, host), preventing the read pipe from EOF'ing.
old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD) old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD)
fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
self._read_file = os.fdopen(read_pipe, 'r') if read_pipe is not None:
self._read_file = os.fdopen(read_pipe, 'r')
else:
self._read_file = None
self._write_file = os.fdopen(write_pipe, 'w') self._write_file = os.fdopen(write_pipe, 'w')
self._logging_handler = None self._logging_handler = None
ParentProcessLogger.__instance = self ParentProcessLogger.__instance = self
...@@ -913,7 +941,8 @@ class ParentProcessLogger(object): ...@@ -913,7 +941,8 @@ class ParentProcessLogger(object):
Must be called by the child process. Must be called by the child process.
""" """
self._read_file.close() if self._read_file is not None:
self._read_file.close()
self._logging_handler = logging.StreamHandler(self._write_file) self._logging_handler = logging.StreamHandler(self._write_file)
self._logging_handler.setFormatter(logging.Formatter(fmt='MSG:%(message)s')) self._logging_handler.setFormatter(logging.Formatter(fmt='MSG:%(message)s'))
logging.getLogger().addHandler(self._logging_handler) logging.getLogger().addHandler(self._logging_handler)
...@@ -1490,6 +1519,16 @@ Web Store: https://chrome.google.com/remotedesktop""" ...@@ -1490,6 +1519,16 @@ Web Store: https://chrome.google.com/remotedesktop"""
# requested to run in the foreground. # requested to run in the foreground.
if not options.foreground: if not options.foreground:
daemonize() daemonize()
else:
# Log to existing messaging pipe if it exists.
try:
ParentProcessLogger(USER_SESSION_MESSAGE_FD).start_logging()
except (IOError, OSError):
# One of these will be thrown if the file descriptor is invalid, such as
# if the script is not being run under the wrapper or the fd got closed by
# the login shell. In either case, just continue without sending log
# messages.
pass
if host.host_id: if host.host_id:
logging.info("Using host_id: " + host.host_id) logging.info("Using host_id: " + host.host_id)
......
...@@ -20,8 +20,11 @@ ...@@ -20,8 +20,11 @@
#include <grp.h> #include <grp.h>
#include <limits.h> #include <limits.h>
#include <pwd.h> #include <pwd.h>
#include <signal.h>
#include <unistd.h> #include <unistd.h>
#include <security/pam_appl.h>
#include <cerrno> #include <cerrno>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
...@@ -35,8 +38,6 @@ ...@@ -35,8 +38,6 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <security/pam_appl.h>
#include "base/environment.h" #include "base/environment.h"
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/files/file_util.h" #include "base/files/file_util.h"
...@@ -45,9 +46,16 @@ ...@@ -45,9 +46,16 @@
#include "base/optional.h" #include "base/optional.h"
#include "base/process/launch.h" #include "base/process/launch.h"
#include "base/strings/string_piece.h" #include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
namespace { namespace {
// This is the file descriptor used for the Python session script to pass us
// messages during startup. It must be kept in sync with USER_SESSION_MESSAGE_FD
// in linux_me2me_host.py. It should be high enough that login scripts are
// unlikely to interfere with it, but is otherwise arbitrary.
const int kMessageFd = 202;
const char kPamName[] = "chrome-remote-desktop"; const char kPamName[] = "chrome-remote-desktop";
const char kScriptName[] = "chrome-remote-desktop"; const char kScriptName[] = "chrome-remote-desktop";
const char kStartCommand[] = "start"; const char kStartCommand[] = "start";
...@@ -359,6 +367,8 @@ void ExecuteSession(std::string user, ...@@ -359,6 +367,8 @@ void ExecuteSession(std::string user,
CHECK(pam_environment) << "Failed to get environment from PAM"; CHECK(pam_environment) << "Failed to get environment from PAM";
ExecMe2MeScript(std::move(*pam_environment), pwinfo); // Never returns ExecMe2MeScript(std::move(*pam_environment), pwinfo); // Never returns
} else { } else {
// Close pipe write fd if it is open
close(kMessageFd);
// waitpid will return if the child is ptraced, so loop until the process // waitpid will return if the child is ptraced, so loop until the process
// actually exits. // actually exits.
int status; int status;
...@@ -388,22 +398,29 @@ void ExecuteSession(std::string user, ...@@ -388,22 +398,29 @@ void ExecuteSession(std::string user,
} }
} }
struct LogFile {
int fd;
std::string path;
};
// Opens a temp file for logging. Exits the program on failure. // Opens a temp file for logging. Exits the program on failure.
int OpenLogFile() { // Returns open file descriptor and path to log file.
LogFile OpenLogFile() {
char logfile[265]; char logfile[265];
std::time_t time = std::time(nullptr); std::time_t time = std::time(nullptr);
CHECK_NE(time, (std::time_t)(-1)); CHECK_NE(time, (std::time_t)(-1));
// Safe because we're single threaded // Safe because we're single threaded
std::tm* localtime = std::localtime(&time); std::tm* localtime = std::localtime(&time);
CHECK_NE(std::strftime(logfile, sizeof(logfile), kLogFileTemplate, localtime), CHECK_NE(std::strftime(logfile, sizeof(logfile), kLogFileTemplate, localtime),
(std::size_t) 0); static_cast<std::size_t>(0))
<< "Failed to format log file name";
mode_t mode = umask(0177); mode_t mode = umask(0177);
int fd = mkstemp(logfile); int fd = mkstemp(logfile);
PCHECK(fd != -1); PCHECK(fd != -1) << "Failed to open log file";
umask(mode); umask(mode);
return fd; return {fd, logfile};
} }
// Find the username for the current user. If either USER or LOGNAME is set to // Find the username for the current user. If either USER or LOGNAME is set to
...@@ -433,56 +450,167 @@ std::string FindCurrentUsername() { ...@@ -433,56 +450,167 @@ std::string FindCurrentUsername() {
return pwinfo->pw_name; return pwinfo->pw_name;
} }
// Daemonizes the process. Output is redirected to a file. Exits the program on // Handle SIGINT and SIGTERM by printing a message and reraising the signal.
// failure. // This handler expects to be registered with the SA_RESETHAND and SA_NODEFER
// options to sigaction. (Don't register using signal.)
void HandleInterrupt(int signal) {
static const char kInterruptedMessage[] =
"Interrupted. The daemon is still running in the background.\n";
write(STDERR_FILENO, kInterruptedMessage, arraysize(kInterruptedMessage) - 1);
raise(signal);
}
// Handle SIGALRM timeout
void HandleAlarm(int) {
static const char kTimeoutMessage[] =
"Timeout waiting for session to start. It may have crashed, or may still "
"be running in the background.\n";
write(STDERR_FILENO, kTimeoutMessage, arraysize(kTimeoutMessage) - 1);
std::_Exit(EXIT_FAILURE);
}
// Relay messages from the host session and then exit.
void WaitForMessagesAndExit(int read_fd, const std::string& log_name) {
// Use initializer-list syntax to avoid trailing null
static const base::StringPiece kMessagePrefix = "MSG:";
static const base::StringPiece kReady = "READY\n";
struct sigaction action = {};
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESETHAND | SA_NODEFER;
// If Ctrl-C is pressed or TERM is received, inform the user that the daemon
// is still running before exiting.
action.sa_handler = HandleInterrupt;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);
// Install a fallback timeout to end the parent process, in case the daemon
// never responds (e.g. host crash-looping, daemon killed).
//
// The value of 120s is chosen to match the heartbeat retry timeout in
// hearbeat_sender.cc.
action.sa_handler = HandleAlarm;
sigaction(SIGALRM, &action, nullptr);
alarm(120);
std::FILE* stream = fdopen(read_fd, "r");
char* buffer = nullptr;
std::size_t buffer_size = 0;
ssize_t line_size;
bool message_received = false;
bool host_ready = false;
while ((line_size = getline(&buffer, &buffer_size, stream)) >= 0) {
message_received = true;
base::StringPiece line(buffer, line_size);
if (base::StartsWith(line, kMessagePrefix, base::CompareCase::SENSITIVE)) {
line.remove_prefix(kMessagePrefix.size());
fwrite(line.data(), sizeof(char), line.size(), stderr);
} else if (line == kReady) {
host_ready = true;
} else {
fputs("Unrecognized command: ", stderr);
fwrite(line.data(), sizeof(char), line.size(), stderr);
}
}
// If we're not at EOF, it means a read error occured and we don't know if the
// host is still running or not. Similarly, if we received an EOF before any
// messages were received, it probably means the user's log-in shell closed
// the pipe before execing the python script, so again we don't know the state
// of the host. This latter behavior has only been observed in csh and tcsh.
// All other shells tested allowed the python script to inherit the pipe file
// descriptor without trouble.
if (!std::feof(stream) || !message_received) {
LOG(WARNING) << "Failed to read from message pipe. Please check log to "
"determine host status.\n";
// Assume host started so native messaging host allows flow to complete.
host_ready = true;
}
fprintf(stderr, "Log file: %s\n", log_name.c_str());
std::exit(host_ready ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Daemonizes the process. Output is redirected to a log file. Exits the program
// on failure. Only returns in the child process.
// //
// This logic is mostly the same as daemonize() in linux_me2me_host.py. Log- // When executed by root (almost certainly via the init script), or if a pipe
// file redirection especially should be kept in sync. Note that this does // cannot be created, the parent will immediately exit. When executed by a
// not currently wait for the host to start successfully before exiting the // user, the parent process will drop privileges and wait for the host to
// parent process like the Python script does, as that functionality is // start, relaying any start-up messages to stdout.
// probably not useful at boot, where the wrapper is expected to be used. If
// it turns out to be desired, it can be implemented by setting up a pipe and
// passing a file descriptor to the Python script.
void Daemonize() { void Daemonize() {
int log_fd = OpenLogFile(); LogFile log_file = OpenLogFile();
int devnull_fd = open("/dev/null", O_RDONLY); int devnull_fd = open("/dev/null", O_RDONLY);
PCHECK(devnull_fd != -1); PCHECK(devnull_fd != -1) << "Failed to open /dev/null";
PCHECK(dup2(devnull_fd, STDIN_FILENO) != -1); uid_t real_uid = getuid();
PCHECK(dup2(log_fd, STDOUT_FILENO) != -1);
PCHECK(dup2(log_fd, STDERR_FILENO) != -1);
// Close all file descriptors except stdio, including any we may have // Set up message pipe
// inherited. bool pipe_created = false;
base::CloseSuperfluousFds(base::InjectiveMultimap()); int read_fd;
if (real_uid != 0) {
int pipe_fd[2];
int pipe_result = ::pipe(pipe_fd);
if (pipe_result != 0 || dup2(pipe_fd[1], kMessageFd) != kMessageFd) {
PLOG(WARNING) << "Failed to create message pipe. Please check log to "
"determine host status.\n";
} else {
pipe_created = true;
read_fd = pipe_fd[0];
close(pipe_fd[1]);
}
}
// Allow parent to exit, and ensure we're not a session leader so setsid can // Allow parent to exit, and ensure we're not a session leader so setsid can
// succeed // succeed
pid_t pid = fork(); pid_t pid = fork();
PCHECK(pid != -1); PCHECK(pid != -1) << "fork failed";
if (pid != 0) { if (pid != 0) {
std::exit(EXIT_SUCCESS); if (!pipe_created) {
std::exit(EXIT_SUCCESS);
} else {
PCHECK(setuid(real_uid) == 0) << "setuid failed";
close(kMessageFd);
WaitForMessagesAndExit(read_fd, log_file.path);
CHECK(false);
}
} }
// Start a new process group and session with no controlling terminal. // Start a new process group and session with no controlling terminal.
PCHECK(setsid() != -1); PCHECK(setsid() != -1) << "setsid failed";
// Fork again so we're no longer a session leader and can't get a controlling // Fork again so we're no longer a session leader and can't get a controlling
// terminal. // terminal.
pid = fork(); pid = fork();
PCHECK(pid != -1); PCHECK(pid != -1) << "fork failed";
if (pid != 0) { if (pid != 0) {
std::exit(EXIT_SUCCESS); std::exit(EXIT_SUCCESS);
} }
LOG(INFO) << "Daemon process started in the background, logging to '"
<< log_file.path << "'";
// We don't want to change to the target user's home directory until we've // We don't want to change to the target user's home directory until we've
// dropped privileges, so change to / to make sure we're not keeping any other // dropped privileges, so change to / to make sure we're not keeping any other
// directory in use. // directory in use.
PCHECK(chdir("/") == 0); PCHECK(chdir("/") == 0) << "chdir / failed";
// Done! PCHECK(dup2(devnull_fd, STDIN_FILENO) != -1) << "dup2 failed";
PCHECK(dup2(log_file.fd, STDOUT_FILENO) != -1) << "dup2 failed";
PCHECK(dup2(log_file.fd, STDERR_FILENO) != -1) << "dup2 failed";
// Close all file descriptors except stdio and kMessageFd, including any we
// may have inherited.
if (pipe_created) {
base::CloseSuperfluousFds(
{base::InjectionArc(kMessageFd, kMessageFd, false)});
} else {
base::CloseSuperfluousFds(base::InjectiveMultimap());
}
} }
} // namespace } // namespace
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment