| //===-- Host.cpp ------------------------------------------------*- C++ -*-===// |
| // |
| // The LLVM Compiler Infrastructure |
| // |
| // This file is distributed under the University of Illinois Open Source |
| // License. See LICENSE.TXT for details. |
| // |
| //===----------------------------------------------------------------------===// |
| |
| // C includes |
| #include <errno.h> |
| #include <limits.h> |
| #include <stdlib.h> |
| #include <sys/types.h> |
| #ifndef _WIN32 |
| #include <dlfcn.h> |
| #include <grp.h> |
| #include <netdb.h> |
| #include <pwd.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| #endif |
| |
| #if defined(__APPLE__) |
| #include <mach-o/dyld.h> |
| #include <mach/mach_init.h> |
| #include <mach/mach_port.h> |
| #endif |
| |
| #if defined(__linux__) || defined(__FreeBSD__) || \ |
| defined(__FreeBSD_kernel__) || defined(__APPLE__) || \ |
| defined(__NetBSD__) || defined(__OpenBSD__) |
| #if !defined(__ANDROID__) |
| #include <spawn.h> |
| #endif |
| #include <sys/syscall.h> |
| #include <sys/wait.h> |
| #endif |
| |
| #if defined(__FreeBSD__) |
| #include <pthread_np.h> |
| #endif |
| |
| #if defined(__NetBSD__) |
| #include <lwp.h> |
| #endif |
| |
| // C++ Includes |
| #include <csignal> |
| |
| #include "lldb/Host/Host.h" |
| #include "lldb/Host/HostInfo.h" |
| #include "lldb/Host/HostProcess.h" |
| #include "lldb/Host/MonitoringProcessLauncher.h" |
| #include "lldb/Host/Predicate.h" |
| #include "lldb/Host/ProcessLauncher.h" |
| #include "lldb/Host/ThreadLauncher.h" |
| #include "lldb/Host/posix/ConnectionFileDescriptorPosix.h" |
| #include "lldb/Target/FileAction.h" |
| #include "lldb/Target/ProcessLaunchInfo.h" |
| #include "lldb/Target/UnixSignals.h" |
| #include "lldb/Utility/DataBufferLLVM.h" |
| #include "lldb/Utility/FileSpec.h" |
| #include "lldb/Utility/Log.h" |
| #include "lldb/Utility/Status.h" |
| #include "lldb/lldb-private-forward.h" |
| #include "llvm/ADT/SmallString.h" |
| #include "llvm/ADT/StringSwitch.h" |
| #include "llvm/Support/Errno.h" |
| #include "llvm/Support/FileSystem.h" |
| |
| #if defined(_WIN32) |
| #include "lldb/Host/windows/ConnectionGenericFileWindows.h" |
| #include "lldb/Host/windows/ProcessLauncherWindows.h" |
| #else |
| #include "lldb/Host/posix/ProcessLauncherPosixFork.h" |
| #endif |
| |
| #if defined(__APPLE__) |
| #ifndef _POSIX_SPAWN_DISABLE_ASLR |
| #define _POSIX_SPAWN_DISABLE_ASLR 0x0100 |
| #endif |
| |
| extern "C" { |
| int __pthread_chdir(const char *path); |
| int __pthread_fchdir(int fildes); |
| } |
| |
| #endif |
| |
| using namespace lldb; |
| using namespace lldb_private; |
| |
| #if !defined(__APPLE__) && !defined(_WIN32) |
| struct MonitorInfo { |
| lldb::pid_t pid; // The process ID to monitor |
| Host::MonitorChildProcessCallback |
| callback; // The callback function to call when "pid" exits or signals |
| bool monitor_signals; // If true, call the callback when "pid" gets signaled. |
| }; |
| |
| static thread_result_t MonitorChildProcessThreadFunction(void *arg); |
| |
| HostThread Host::StartMonitoringChildProcess( |
| const Host::MonitorChildProcessCallback &callback, lldb::pid_t pid, |
| bool monitor_signals) { |
| MonitorInfo *info_ptr = new MonitorInfo(); |
| |
| info_ptr->pid = pid; |
| info_ptr->callback = callback; |
| info_ptr->monitor_signals = monitor_signals; |
| |
| char thread_name[256]; |
| ::snprintf(thread_name, sizeof(thread_name), |
| "<lldb.host.wait4(pid=%" PRIu64 ")>", pid); |
| return ThreadLauncher::LaunchThread( |
| thread_name, MonitorChildProcessThreadFunction, info_ptr, NULL); |
| } |
| |
| #ifndef __linux__ |
| //------------------------------------------------------------------ |
| // Scoped class that will disable thread canceling when it is constructed, and |
| // exception safely restore the previous value it when it goes out of scope. |
| //------------------------------------------------------------------ |
| class ScopedPThreadCancelDisabler { |
| public: |
| ScopedPThreadCancelDisabler() { |
| // Disable the ability for this thread to be cancelled |
| int err = ::pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &m_old_state); |
| if (err != 0) |
| m_old_state = -1; |
| } |
| |
| ~ScopedPThreadCancelDisabler() { |
| // Restore the ability for this thread to be cancelled to what it |
| // previously was. |
| if (m_old_state != -1) |
| ::pthread_setcancelstate(m_old_state, 0); |
| } |
| |
| private: |
| int m_old_state; // Save the old cancelability state. |
| }; |
| #endif // __linux__ |
| |
| #ifdef __linux__ |
| #if defined(__GNUC__) && (__GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 8)) |
| static __thread volatile sig_atomic_t g_usr1_called; |
| #else |
| static thread_local volatile sig_atomic_t g_usr1_called; |
| #endif |
| |
| static void SigUsr1Handler(int) { g_usr1_called = 1; } |
| #endif // __linux__ |
| |
| static bool CheckForMonitorCancellation() { |
| #ifdef __linux__ |
| if (g_usr1_called) { |
| g_usr1_called = 0; |
| return true; |
| } |
| #else |
| ::pthread_testcancel(); |
| #endif |
| return false; |
| } |
| |
| static thread_result_t MonitorChildProcessThreadFunction(void *arg) { |
| Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS)); |
| const char *function = __FUNCTION__; |
| if (log) |
| log->Printf("%s (arg = %p) thread starting...", function, arg); |
| |
| MonitorInfo *info = (MonitorInfo *)arg; |
| |
| const Host::MonitorChildProcessCallback callback = info->callback; |
| const bool monitor_signals = info->monitor_signals; |
| |
| assert(info->pid <= UINT32_MAX); |
| const ::pid_t pid = monitor_signals ? -1 * getpgid(info->pid) : info->pid; |
| |
| delete info; |
| |
| int status = -1; |
| #if defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || defined(__OpenBSD__) |
| #define __WALL 0 |
| #endif |
| const int options = __WALL; |
| |
| #ifdef __linux__ |
| // This signal is only used to interrupt the thread from waitpid |
| struct sigaction sigUsr1Action; |
| memset(&sigUsr1Action, 0, sizeof(sigUsr1Action)); |
| sigUsr1Action.sa_handler = SigUsr1Handler; |
| ::sigaction(SIGUSR1, &sigUsr1Action, nullptr); |
| #endif // __linux__ |
| |
| while (1) { |
| log = lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS); |
| if (log) |
| log->Printf("%s ::waitpid (pid = %" PRIi32 ", &status, options = %i)...", |
| function, pid, options); |
| |
| if (CheckForMonitorCancellation()) |
| break; |
| |
| // Get signals from all children with same process group of pid |
| const ::pid_t wait_pid = ::waitpid(pid, &status, options); |
| |
| if (CheckForMonitorCancellation()) |
| break; |
| |
| if (wait_pid == -1) { |
| if (errno == EINTR) |
| continue; |
| else { |
| LLDB_LOG(log, |
| "arg = {0}, thread exiting because waitpid failed ({1})...", |
| arg, llvm::sys::StrError()); |
| break; |
| } |
| } else if (wait_pid > 0) { |
| bool exited = false; |
| int signal = 0; |
| int exit_status = 0; |
| const char *status_cstr = NULL; |
| if (WIFSTOPPED(status)) { |
| signal = WSTOPSIG(status); |
| status_cstr = "STOPPED"; |
| } else if (WIFEXITED(status)) { |
| exit_status = WEXITSTATUS(status); |
| status_cstr = "EXITED"; |
| exited = true; |
| } else if (WIFSIGNALED(status)) { |
| signal = WTERMSIG(status); |
| status_cstr = "SIGNALED"; |
| if (wait_pid == abs(pid)) { |
| exited = true; |
| exit_status = -1; |
| } |
| } else { |
| status_cstr = "(\?\?\?)"; |
| } |
| |
| // Scope for pthread_cancel_disabler |
| { |
| #ifndef __linux__ |
| ScopedPThreadCancelDisabler pthread_cancel_disabler; |
| #endif |
| |
| log = lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS); |
| if (log) |
| log->Printf("%s ::waitpid (pid = %" PRIi32 |
| ", &status, options = %i) => pid = %" PRIi32 |
| ", status = 0x%8.8x (%s), signal = %i, exit_state = %i", |
| function, pid, options, wait_pid, status, status_cstr, |
| signal, exit_status); |
| |
| if (exited || (signal != 0 && monitor_signals)) { |
| bool callback_return = false; |
| if (callback) |
| callback_return = callback(wait_pid, exited, signal, exit_status); |
| |
| // If our process exited, then this thread should exit |
| if (exited && wait_pid == abs(pid)) { |
| if (log) |
| log->Printf("%s (arg = %p) thread exiting because pid received " |
| "exit signal...", |
| __FUNCTION__, arg); |
| break; |
| } |
| // If the callback returns true, it means this process should exit |
| if (callback_return) { |
| if (log) |
| log->Printf("%s (arg = %p) thread exiting because callback " |
| "returned true...", |
| __FUNCTION__, arg); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| log = lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_PROCESS); |
| if (log) |
| log->Printf("%s (arg = %p) thread exiting...", __FUNCTION__, arg); |
| |
| return NULL; |
| } |
| |
| #endif // #if !defined (__APPLE__) && !defined (_WIN32) |
| |
| #if !defined(__APPLE__) |
| |
| void Host::SystemLog(SystemLogType type, const char *format, va_list args) { |
| vfprintf(stderr, format, args); |
| } |
| |
| #endif |
| |
| void Host::SystemLog(SystemLogType type, const char *format, ...) { |
| va_list args; |
| va_start(args, format); |
| SystemLog(type, format, args); |
| va_end(args); |
| } |
| |
| lldb::pid_t Host::GetCurrentProcessID() { return ::getpid(); } |
| |
| #ifndef _WIN32 |
| |
| lldb::thread_t Host::GetCurrentThread() { |
| return lldb::thread_t(pthread_self()); |
| } |
| |
| const char *Host::GetSignalAsCString(int signo) { |
| switch (signo) { |
| case SIGHUP: |
| return "SIGHUP"; // 1 hangup |
| case SIGINT: |
| return "SIGINT"; // 2 interrupt |
| case SIGQUIT: |
| return "SIGQUIT"; // 3 quit |
| case SIGILL: |
| return "SIGILL"; // 4 illegal instruction (not reset when caught) |
| case SIGTRAP: |
| return "SIGTRAP"; // 5 trace trap (not reset when caught) |
| case SIGABRT: |
| return "SIGABRT"; // 6 abort() |
| #if defined(SIGPOLL) |
| #if !defined(SIGIO) || (SIGPOLL != SIGIO) |
| // Under some GNU/Linux, SIGPOLL and SIGIO are the same. Causing the build to |
| // fail with 'multiple define cases with same value' |
| case SIGPOLL: |
| return "SIGPOLL"; // 7 pollable event ([XSR] generated, not supported) |
| #endif |
| #endif |
| #if defined(SIGEMT) |
| case SIGEMT: |
| return "SIGEMT"; // 7 EMT instruction |
| #endif |
| case SIGFPE: |
| return "SIGFPE"; // 8 floating point exception |
| case SIGKILL: |
| return "SIGKILL"; // 9 kill (cannot be caught or ignored) |
| case SIGBUS: |
| return "SIGBUS"; // 10 bus error |
| case SIGSEGV: |
| return "SIGSEGV"; // 11 segmentation violation |
| case SIGSYS: |
| return "SIGSYS"; // 12 bad argument to system call |
| case SIGPIPE: |
| return "SIGPIPE"; // 13 write on a pipe with no one to read it |
| case SIGALRM: |
| return "SIGALRM"; // 14 alarm clock |
| case SIGTERM: |
| return "SIGTERM"; // 15 software termination signal from kill |
| case SIGURG: |
| return "SIGURG"; // 16 urgent condition on IO channel |
| case SIGSTOP: |
| return "SIGSTOP"; // 17 sendable stop signal not from tty |
| case SIGTSTP: |
| return "SIGTSTP"; // 18 stop signal from tty |
| case SIGCONT: |
| return "SIGCONT"; // 19 continue a stopped process |
| case SIGCHLD: |
| return "SIGCHLD"; // 20 to parent on child stop or exit |
| case SIGTTIN: |
| return "SIGTTIN"; // 21 to readers pgrp upon background tty read |
| case SIGTTOU: |
| return "SIGTTOU"; // 22 like TTIN for output if (tp->t_local<OSTOP) |
| #if defined(SIGIO) |
| case SIGIO: |
| return "SIGIO"; // 23 input/output possible signal |
| #endif |
| case SIGXCPU: |
| return "SIGXCPU"; // 24 exceeded CPU time limit |
| case SIGXFSZ: |
| return "SIGXFSZ"; // 25 exceeded file size limit |
| case SIGVTALRM: |
| return "SIGVTALRM"; // 26 virtual time alarm |
| case SIGPROF: |
| return "SIGPROF"; // 27 profiling time alarm |
| #if defined(SIGWINCH) |
| case SIGWINCH: |
| return "SIGWINCH"; // 28 window size changes |
| #endif |
| #if defined(SIGINFO) |
| case SIGINFO: |
| return "SIGINFO"; // 29 information request |
| #endif |
| case SIGUSR1: |
| return "SIGUSR1"; // 30 user defined signal 1 |
| case SIGUSR2: |
| return "SIGUSR2"; // 31 user defined signal 2 |
| default: |
| break; |
| } |
| return NULL; |
| } |
| |
| #endif |
| |
| #if !defined(__APPLE__) // see Host.mm |
| |
| bool Host::GetBundleDirectory(const FileSpec &file, FileSpec &bundle) { |
| bundle.Clear(); |
| return false; |
| } |
| |
| bool Host::ResolveExecutableInBundle(FileSpec &file) { return false; } |
| #endif |
| |
| #ifndef _WIN32 |
| |
| FileSpec Host::GetModuleFileSpecForHostAddress(const void *host_addr) { |
| FileSpec module_filespec; |
| #if !defined(__ANDROID__) |
| Dl_info info; |
| if (::dladdr(host_addr, &info)) { |
| if (info.dli_fname) |
| module_filespec.SetFile(info.dli_fname, true, FileSpec::Style::native); |
| } |
| #endif |
| return module_filespec; |
| } |
| |
| #endif |
| |
| #if !defined(__linux__) |
| bool Host::FindProcessThreads(const lldb::pid_t pid, TidMap &tids_to_attach) { |
| return false; |
| } |
| #endif |
| |
| struct ShellInfo { |
| ShellInfo() |
| : process_reaped(false), pid(LLDB_INVALID_PROCESS_ID), signo(-1), |
| status(-1) {} |
| |
| lldb_private::Predicate<bool> process_reaped; |
| lldb::pid_t pid; |
| int signo; |
| int status; |
| }; |
| |
| static bool |
| MonitorShellCommand(std::shared_ptr<ShellInfo> shell_info, lldb::pid_t pid, |
| bool exited, // True if the process did exit |
| int signo, // Zero for no signal |
| int status) // Exit value of process if signal is zero |
| { |
| shell_info->pid = pid; |
| shell_info->signo = signo; |
| shell_info->status = status; |
| // Let the thread running Host::RunShellCommand() know that the process |
| // exited and that ShellInfo has been filled in by broadcasting to it |
| shell_info->process_reaped.SetValue(true, eBroadcastAlways); |
| return true; |
| } |
| |
| Status Host::RunShellCommand(const char *command, const FileSpec &working_dir, |
| int *status_ptr, int *signo_ptr, |
| std::string *command_output_ptr, |
| const Timeout<std::micro> &timeout, |
| bool run_in_default_shell) { |
| return RunShellCommand(Args(command), working_dir, status_ptr, signo_ptr, |
| command_output_ptr, timeout, run_in_default_shell); |
| } |
| |
| Status Host::RunShellCommand(const Args &args, const FileSpec &working_dir, |
| int *status_ptr, int *signo_ptr, |
| std::string *command_output_ptr, |
| const Timeout<std::micro> &timeout, |
| bool run_in_default_shell) { |
| Status error; |
| ProcessLaunchInfo launch_info; |
| launch_info.SetArchitecture(HostInfo::GetArchitecture()); |
| if (run_in_default_shell) { |
| // Run the command in a shell |
| launch_info.SetShell(HostInfo::GetDefaultShell()); |
| launch_info.GetArguments().AppendArguments(args); |
| const bool localhost = true; |
| const bool will_debug = false; |
| const bool first_arg_is_full_shell_command = false; |
| launch_info.ConvertArgumentsForLaunchingInShell( |
| error, localhost, will_debug, first_arg_is_full_shell_command, 0); |
| } else { |
| // No shell, just run it |
| const bool first_arg_is_executable = true; |
| launch_info.SetArguments(args, first_arg_is_executable); |
| } |
| |
| if (working_dir) |
| launch_info.SetWorkingDirectory(working_dir); |
| llvm::SmallString<PATH_MAX> output_file_path; |
| |
| if (command_output_ptr) { |
| // Create a temporary file to get the stdout/stderr and redirect the output |
| // of the command into this file. We will later read this file if all goes |
| // well and fill the data into "command_output_ptr" |
| if (FileSpec tmpdir_file_spec = HostInfo::GetProcessTempDir()) { |
| tmpdir_file_spec.AppendPathComponent("lldb-shell-output.%%%%%%"); |
| llvm::sys::fs::createUniqueFile(tmpdir_file_spec.GetPath(), |
| output_file_path); |
| } else { |
| llvm::sys::fs::createTemporaryFile("lldb-shell-output.%%%%%%", "", |
| output_file_path); |
| } |
| } |
| |
| FileSpec output_file_spec{output_file_path.c_str(), false}; |
| |
| launch_info.AppendSuppressFileAction(STDIN_FILENO, true, false); |
| if (output_file_spec) { |
| launch_info.AppendOpenFileAction(STDOUT_FILENO, output_file_spec, false, |
| true); |
| launch_info.AppendDuplicateFileAction(STDOUT_FILENO, STDERR_FILENO); |
| } else { |
| launch_info.AppendSuppressFileAction(STDOUT_FILENO, false, true); |
| launch_info.AppendSuppressFileAction(STDERR_FILENO, false, true); |
| } |
| |
| std::shared_ptr<ShellInfo> shell_info_sp(new ShellInfo()); |
| const bool monitor_signals = false; |
| launch_info.SetMonitorProcessCallback( |
| std::bind(MonitorShellCommand, shell_info_sp, std::placeholders::_1, |
| std::placeholders::_2, std::placeholders::_3, |
| std::placeholders::_4), |
| monitor_signals); |
| |
| error = LaunchProcess(launch_info); |
| const lldb::pid_t pid = launch_info.GetProcessID(); |
| |
| if (error.Success() && pid == LLDB_INVALID_PROCESS_ID) |
| error.SetErrorString("failed to get process ID"); |
| |
| if (error.Success()) { |
| if (!shell_info_sp->process_reaped.WaitForValueEqualTo(true, timeout)) { |
| error.SetErrorString("timed out waiting for shell command to complete"); |
| |
| // Kill the process since it didn't complete within the timeout specified |
| Kill(pid, SIGKILL); |
| // Wait for the monitor callback to get the message |
| shell_info_sp->process_reaped.WaitForValueEqualTo( |
| true, std::chrono::seconds(1)); |
| } else { |
| if (status_ptr) |
| *status_ptr = shell_info_sp->status; |
| |
| if (signo_ptr) |
| *signo_ptr = shell_info_sp->signo; |
| |
| if (command_output_ptr) { |
| command_output_ptr->clear(); |
| uint64_t file_size = output_file_spec.GetByteSize(); |
| if (file_size > 0) { |
| if (file_size > command_output_ptr->max_size()) { |
| error.SetErrorStringWithFormat( |
| "shell command output is too large to fit into a std::string"); |
| } else { |
| auto Buffer = |
| DataBufferLLVM::CreateFromPath(output_file_spec.GetPath()); |
| if (error.Success()) |
| command_output_ptr->assign(Buffer->GetChars(), |
| Buffer->GetByteSize()); |
| } |
| } |
| } |
| } |
| } |
| |
| llvm::sys::fs::remove(output_file_spec.GetPath()); |
| return error; |
| } |
| |
| // The functions below implement process launching for non-Apple-based |
| // platforms |
| #if !defined(__APPLE__) |
| Status Host::LaunchProcess(ProcessLaunchInfo &launch_info) { |
| std::unique_ptr<ProcessLauncher> delegate_launcher; |
| #if defined(_WIN32) |
| delegate_launcher.reset(new ProcessLauncherWindows()); |
| #else |
| delegate_launcher.reset(new ProcessLauncherPosixFork()); |
| #endif |
| MonitoringProcessLauncher launcher(std::move(delegate_launcher)); |
| |
| Status error; |
| HostProcess process = launcher.LaunchProcess(launch_info, error); |
| |
| // TODO(zturner): It would be better if the entire HostProcess were returned |
| // instead of writing it into this structure. |
| launch_info.SetProcessID(process.GetProcessId()); |
| |
| return error; |
| } |
| #endif // !defined(__APPLE__) |
| |
| #ifndef _WIN32 |
| void Host::Kill(lldb::pid_t pid, int signo) { ::kill(pid, signo); } |
| |
| #endif |
| |
| #if !defined(__APPLE__) |
| bool Host::OpenFileInExternalEditor(const FileSpec &file_spec, |
| uint32_t line_no) { |
| return false; |
| } |
| |
| #endif |
| |
| const UnixSignalsSP &Host::GetUnixSignals() { |
| static const auto s_unix_signals_sp = |
| UnixSignals::Create(HostInfo::GetArchitecture()); |
| return s_unix_signals_sp; |
| } |
| |
| std::unique_ptr<Connection> Host::CreateDefaultConnection(llvm::StringRef url) { |
| #if defined(_WIN32) |
| if (url.startswith("file://")) |
| return std::unique_ptr<Connection>(new ConnectionGenericFile()); |
| #endif |
| return std::unique_ptr<Connection>(new ConnectionFileDescriptor()); |
| } |
| |
| #if defined(LLVM_ON_UNIX) |
| WaitStatus WaitStatus::Decode(int wstatus) { |
| if (WIFEXITED(wstatus)) |
| return {Exit, uint8_t(WEXITSTATUS(wstatus))}; |
| else if (WIFSIGNALED(wstatus)) |
| return {Signal, uint8_t(WTERMSIG(wstatus))}; |
| else if (WIFSTOPPED(wstatus)) |
| return {Stop, uint8_t(WSTOPSIG(wstatus))}; |
| llvm_unreachable("Unknown wait status"); |
| } |
| #endif |
| |
| void llvm::format_provider<WaitStatus>::format(const WaitStatus &WS, |
| raw_ostream &OS, |
| StringRef Options) { |
| if (Options == "g") { |
| char type; |
| switch (WS.type) { |
| case WaitStatus::Exit: |
| type = 'W'; |
| break; |
| case WaitStatus::Signal: |
| type = 'X'; |
| break; |
| case WaitStatus::Stop: |
| type = 'S'; |
| break; |
| } |
| OS << formatv("{0}{1:x-2}", type, WS.status); |
| return; |
| } |
| |
| assert(Options.empty()); |
| const char *desc; |
| switch(WS.type) { |
| case WaitStatus::Exit: |
| desc = "Exited with status"; |
| break; |
| case WaitStatus::Signal: |
| desc = "Killed by signal"; |
| break; |
| case WaitStatus::Stop: |
| desc = "Stopped by signal"; |
| break; |
| } |
| OS << desc << " " << int(WS.status); |
| } |