1bd2d88d6SJason Molenda //===-- libdebugserver.cpp --------------------------------------*- C++ -*-===// 2bd2d88d6SJason Molenda // 3bd2d88d6SJason Molenda // The LLVM Compiler Infrastructure 4bd2d88d6SJason Molenda // 5bd2d88d6SJason Molenda // This file is distributed under the University of Illinois Open Source 6bd2d88d6SJason Molenda // License. See LICENSE.TXT for details. 7bd2d88d6SJason Molenda // 8bd2d88d6SJason Molenda //===----------------------------------------------------------------------===// 9d676074dSJason Molenda 10d676074dSJason Molenda #include <errno.h> 11d676074dSJason Molenda #include <getopt.h> 12d676074dSJason Molenda #include <netinet/in.h> 13d676074dSJason Molenda #include <sys/select.h> 14*b9c1b51eSKate Stone #include <sys/socket.h> 15d676074dSJason Molenda #include <sys/sysctl.h> 16*b9c1b51eSKate Stone #include <sys/types.h> 17d676074dSJason Molenda 18d676074dSJason Molenda #include "DNB.h" 19d676074dSJason Molenda #include "DNBLog.h" 20d676074dSJason Molenda #include "DNBTimer.h" 21d676074dSJason Molenda #include "PseudoTerminal.h" 22d676074dSJason Molenda #include "RNBContext.h" 23*b9c1b51eSKate Stone #include "RNBRemote.h" 24d676074dSJason Molenda #include "RNBServices.h" 25d676074dSJason Molenda #include "RNBSocket.h" 26d676074dSJason Molenda #include "SysSignal.h" 27d676074dSJason Molenda 28d676074dSJason Molenda //---------------------------------------------------------------------- 29d676074dSJason Molenda // Run loop modes which determine which run loop function will be called 30d676074dSJason Molenda //---------------------------------------------------------------------- 31*b9c1b51eSKate Stone typedef enum { 32d676074dSJason Molenda eRNBRunLoopModeInvalid = 0, 33d676074dSJason Molenda eRNBRunLoopModeGetStartModeFromRemoteProtocol, 34d676074dSJason Molenda eRNBRunLoopModeInferiorExecuting, 35d676074dSJason Molenda eRNBRunLoopModeExit 36d676074dSJason Molenda } RNBRunLoopMode; 37d676074dSJason Molenda 38d676074dSJason Molenda //---------------------------------------------------------------------- 39d676074dSJason Molenda // Global Variables 40d676074dSJason Molenda //---------------------------------------------------------------------- 41d676074dSJason Molenda RNBRemoteSP g_remoteSP; 42d676074dSJason Molenda int g_disable_aslr = 0; 43d676074dSJason Molenda int g_isatty = 0; 44d676074dSJason Molenda 45*b9c1b51eSKate Stone #define RNBLogSTDOUT(fmt, ...) \ 46*b9c1b51eSKate Stone do { \ 47*b9c1b51eSKate Stone if (g_isatty) { \ 48*b9c1b51eSKate Stone fprintf(stdout, fmt, ##__VA_ARGS__); \ 49*b9c1b51eSKate Stone } else { \ 50*b9c1b51eSKate Stone _DNBLog(0, fmt, ##__VA_ARGS__); \ 51*b9c1b51eSKate Stone } \ 52*b9c1b51eSKate Stone } while (0) 53*b9c1b51eSKate Stone #define RNBLogSTDERR(fmt, ...) \ 54*b9c1b51eSKate Stone do { \ 55*b9c1b51eSKate Stone if (g_isatty) { \ 56*b9c1b51eSKate Stone fprintf(stderr, fmt, ##__VA_ARGS__); \ 57*b9c1b51eSKate Stone } else { \ 58*b9c1b51eSKate Stone _DNBLog(0, fmt, ##__VA_ARGS__); \ 59*b9c1b51eSKate Stone } \ 60*b9c1b51eSKate Stone } while (0) 61d676074dSJason Molenda 62d676074dSJason Molenda //---------------------------------------------------------------------- 63d676074dSJason Molenda // Get our program path and arguments from the remote connection. 64d676074dSJason Molenda // We will need to start up the remote connection without a PID, get the 65d676074dSJason Molenda // arguments, wait for the new process to finish launching and hit its 66d676074dSJason Molenda // entry point, and then return the run loop mode that should come next. 67d676074dSJason Molenda //---------------------------------------------------------------------- 68*b9c1b51eSKate Stone RNBRunLoopMode RNBRunLoopGetStartModeFromRemote(RNBRemoteSP &remoteSP) { 69d676074dSJason Molenda std::string packet; 70d676074dSJason Molenda 71*b9c1b51eSKate Stone if (remoteSP.get() != NULL) { 72d676074dSJason Molenda RNBRemote *remote = remoteSP.get(); 73d676074dSJason Molenda RNBContext &ctx = remote->Context(); 74d676074dSJason Molenda uint32_t event_mask = RNBContext::event_read_packet_available; 75d676074dSJason Molenda 76d676074dSJason Molenda // Spin waiting to get the A packet. 77*b9c1b51eSKate Stone while (1) { 78*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MAX, 79*b9c1b51eSKate Stone "%s ctx.Events().WaitForSetEvents( 0x%08x ) ...", 80*b9c1b51eSKate Stone __FUNCTION__, event_mask); 81d676074dSJason Molenda nub_event_t set_events = ctx.Events().WaitForSetEvents(event_mask); 82*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MAX, 83*b9c1b51eSKate Stone "%s ctx.Events().WaitForSetEvents( 0x%08x ) => 0x%08x", 84*b9c1b51eSKate Stone __FUNCTION__, event_mask, set_events); 85d676074dSJason Molenda 86*b9c1b51eSKate Stone if (set_events & RNBContext::event_read_packet_available) { 87d676074dSJason Molenda rnb_err_t err = rnb_err; 88d676074dSJason Molenda RNBRemote::PacketEnum type; 89d676074dSJason Molenda 90d676074dSJason Molenda err = remote->HandleReceivedPacket(&type); 91d676074dSJason Molenda 92d676074dSJason Molenda // check if we tried to attach to a process 93*b9c1b51eSKate Stone if (type == RNBRemote::vattach || type == RNBRemote::vattachwait) { 94d676074dSJason Molenda if (err == rnb_success) 95d676074dSJason Molenda return eRNBRunLoopModeInferiorExecuting; 96*b9c1b51eSKate Stone else { 97d676074dSJason Molenda RNBLogSTDERR("error: attach failed."); 98d676074dSJason Molenda return eRNBRunLoopModeExit; 99d676074dSJason Molenda } 100d676074dSJason Molenda } 101d676074dSJason Molenda 102*b9c1b51eSKate Stone if (err == rnb_success) { 103d676074dSJason Molenda DNBLogThreadedIf(LOG_RNB_MINIMAL, "%s Got success...", __FUNCTION__); 104d676074dSJason Molenda continue; 105*b9c1b51eSKate Stone } else if (err == rnb_not_connected) { 106d676074dSJason Molenda RNBLogSTDERR("error: connection lost."); 107d676074dSJason Molenda return eRNBRunLoopModeExit; 108*b9c1b51eSKate Stone } else { 109d676074dSJason Molenda // a catch all for any other gdb remote packets that failed 110*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MINIMAL, "%s Error getting packet.", 111*b9c1b51eSKate Stone __FUNCTION__); 112d676074dSJason Molenda continue; 113d676074dSJason Molenda } 114d676074dSJason Molenda 115d676074dSJason Molenda DNBLogThreadedIf(LOG_RNB_MINIMAL, "#### %s", __FUNCTION__); 116*b9c1b51eSKate Stone } else { 117*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MINIMAL, 118*b9c1b51eSKate Stone "%s Connection closed before getting \"A\" packet.", 119*b9c1b51eSKate Stone __FUNCTION__); 120d676074dSJason Molenda return eRNBRunLoopModeExit; 121d676074dSJason Molenda } 122d676074dSJason Molenda } 123d676074dSJason Molenda } 124d676074dSJason Molenda return eRNBRunLoopModeExit; 125d676074dSJason Molenda } 126d676074dSJason Molenda 127d676074dSJason Molenda //---------------------------------------------------------------------- 128d676074dSJason Molenda // Watch for signals: 129d676074dSJason Molenda // SIGINT: so we can halt our inferior. (disabled for now) 130d676074dSJason Molenda // SIGPIPE: in case our child process dies 131d676074dSJason Molenda //---------------------------------------------------------------------- 132d676074dSJason Molenda nub_process_t g_pid; 133d676074dSJason Molenda int g_sigpipe_received = 0; 134*b9c1b51eSKate Stone void signal_handler(int signo) { 135*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MINIMAL, "%s (%s)", __FUNCTION__, 136*b9c1b51eSKate Stone SysSignal::Name(signo)); 137d676074dSJason Molenda 138*b9c1b51eSKate Stone switch (signo) { 139d676074dSJason Molenda // case SIGINT: 140d676074dSJason Molenda // DNBProcessKill (g_pid, signo); 141d676074dSJason Molenda // break; 142d676074dSJason Molenda 143d676074dSJason Molenda case SIGPIPE: 144d676074dSJason Molenda g_sigpipe_received = 1; 145d676074dSJason Molenda break; 146d676074dSJason Molenda } 147d676074dSJason Molenda } 148d676074dSJason Molenda 149d676074dSJason Molenda // Return the new run loop mode based off of the current process state 150*b9c1b51eSKate Stone RNBRunLoopMode HandleProcessStateChange(RNBRemoteSP &remote, bool initialize) { 151d676074dSJason Molenda RNBContext &ctx = remote->Context(); 152d676074dSJason Molenda nub_process_t pid = ctx.ProcessID(); 153d676074dSJason Molenda 154*b9c1b51eSKate Stone if (pid == INVALID_NUB_PROCESS) { 155*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MINIMAL, "#### %s error: pid invalid, exiting...", 156*b9c1b51eSKate Stone __FUNCTION__); 157d676074dSJason Molenda return eRNBRunLoopModeExit; 158d676074dSJason Molenda } 159d676074dSJason Molenda nub_state_t pid_state = DNBProcessGetState(pid); 160d676074dSJason Molenda 161*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MINIMAL, 162*b9c1b51eSKate Stone "%s (&remote, initialize=%i) pid_state = %s", __FUNCTION__, 163*b9c1b51eSKate Stone (int)initialize, DNBStateAsString(pid_state)); 164d676074dSJason Molenda 165*b9c1b51eSKate Stone switch (pid_state) { 166d676074dSJason Molenda case eStateInvalid: 167d676074dSJason Molenda case eStateUnloaded: 168d676074dSJason Molenda // Something bad happened 169d676074dSJason Molenda return eRNBRunLoopModeExit; 170d676074dSJason Molenda break; 171d676074dSJason Molenda 172d676074dSJason Molenda case eStateAttaching: 173d676074dSJason Molenda case eStateLaunching: 174d676074dSJason Molenda return eRNBRunLoopModeInferiorExecuting; 175d676074dSJason Molenda 176d676074dSJason Molenda case eStateSuspended: 177d676074dSJason Molenda case eStateCrashed: 178d676074dSJason Molenda case eStateStopped: 179*b9c1b51eSKate Stone if (initialize == false) { 180d676074dSJason Molenda // Compare the last stop count to our current notion of a stop count 181d676074dSJason Molenda // to make sure we don't notify more than once for a given stop. 182d676074dSJason Molenda nub_size_t prev_pid_stop_count = ctx.GetProcessStopCount(); 183*b9c1b51eSKate Stone bool pid_stop_count_changed = 184*b9c1b51eSKate Stone ctx.SetProcessStopCount(DNBProcessGetStopCount(pid)); 185*b9c1b51eSKate Stone if (pid_stop_count_changed) { 186d676074dSJason Molenda remote->FlushSTDIO(); 187d676074dSJason Molenda 188*b9c1b51eSKate Stone if (ctx.GetProcessStopCount() == 1) { 189*b9c1b51eSKate Stone DNBLogThreadedIf( 190*b9c1b51eSKate Stone LOG_RNB_MINIMAL, "%s (&remote, initialize=%i) pid_state = %s " 191*b9c1b51eSKate Stone "pid_stop_count %zu (old %zu)) Notify??? no, " 192*b9c1b51eSKate Stone "first stop...", 193*b9c1b51eSKate Stone __FUNCTION__, (int)initialize, DNBStateAsString(pid_state), 194*b9c1b51eSKate Stone ctx.GetProcessStopCount(), prev_pid_stop_count); 195*b9c1b51eSKate Stone } else { 196d676074dSJason Molenda 197*b9c1b51eSKate Stone DNBLogThreadedIf( 198*b9c1b51eSKate Stone LOG_RNB_MINIMAL, "%s (&remote, initialize=%i) pid_state = %s " 199*b9c1b51eSKate Stone "pid_stop_count %zu (old %zu)) Notify??? YES!!!", 200*b9c1b51eSKate Stone __FUNCTION__, (int)initialize, DNBStateAsString(pid_state), 201*b9c1b51eSKate Stone ctx.GetProcessStopCount(), prev_pid_stop_count); 202d676074dSJason Molenda remote->NotifyThatProcessStopped(); 203d676074dSJason Molenda } 204*b9c1b51eSKate Stone } else { 205*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_MINIMAL, "%s (&remote, initialize=%i) " 206*b9c1b51eSKate Stone "pid_state = %s pid_stop_count %zu " 207*b9c1b51eSKate Stone "(old %zu)) Notify??? skipping...", 208*b9c1b51eSKate Stone __FUNCTION__, (int)initialize, 209*b9c1b51eSKate Stone DNBStateAsString(pid_state), ctx.GetProcessStopCount(), 210*b9c1b51eSKate Stone prev_pid_stop_count); 211d676074dSJason Molenda } 212d676074dSJason Molenda } 213d676074dSJason Molenda return eRNBRunLoopModeInferiorExecuting; 214d676074dSJason Molenda 215d676074dSJason Molenda case eStateStepping: 216d676074dSJason Molenda case eStateRunning: 217d676074dSJason Molenda return eRNBRunLoopModeInferiorExecuting; 218d676074dSJason Molenda 219d676074dSJason Molenda case eStateExited: 220d676074dSJason Molenda remote->HandlePacket_last_signal(NULL); 221d676074dSJason Molenda return eRNBRunLoopModeExit; 222d676074dSJason Molenda case eStateDetached: 223d676074dSJason Molenda return eRNBRunLoopModeExit; 224d676074dSJason Molenda } 225d676074dSJason Molenda 226d676074dSJason Molenda // Catch all... 227d676074dSJason Molenda return eRNBRunLoopModeExit; 228d676074dSJason Molenda } 229d676074dSJason Molenda // This function handles the case where our inferior program is stopped and 230d676074dSJason Molenda // we are waiting for gdb remote protocol packets. When a packet occurs that 231d676074dSJason Molenda // makes the inferior run, we need to leave this function with a new state 232d676074dSJason Molenda // as the return code. 233*b9c1b51eSKate Stone RNBRunLoopMode RNBRunLoopInferiorExecuting(RNBRemoteSP &remote) { 234d676074dSJason Molenda DNBLogThreadedIf(LOG_RNB_MINIMAL, "#### %s", __FUNCTION__); 235d676074dSJason Molenda RNBContext &ctx = remote->Context(); 236d676074dSJason Molenda 237d676074dSJason Molenda // Init our mode and set 'is_running' based on the current process state 238d676074dSJason Molenda RNBRunLoopMode mode = HandleProcessStateChange(remote, true); 239d676074dSJason Molenda 240*b9c1b51eSKate Stone while (ctx.ProcessID() != INVALID_NUB_PROCESS) { 241d676074dSJason Molenda 242d676074dSJason Molenda std::string set_events_str; 243d676074dSJason Molenda uint32_t event_mask = ctx.NormalEventBits(); 244d676074dSJason Molenda 245*b9c1b51eSKate Stone if (!ctx.ProcessStateRunning()) { 246*b9c1b51eSKate Stone // Clear the stdio bits if we are not running so we don't send any async 247*b9c1b51eSKate Stone // packets 248d676074dSJason Molenda event_mask &= ~RNBContext::event_proc_stdio_available; 249d676074dSJason Molenda } 250d676074dSJason Molenda 251d676074dSJason Molenda // We want to make sure we consume all process state changes and have 252d676074dSJason Molenda // whomever is notifying us to wait for us to reset the event bit before 253d676074dSJason Molenda // continuing. 254d676074dSJason Molenda // ctx.Events().SetResetAckMask (RNBContext::event_proc_state_changed); 255d676074dSJason Molenda 256*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_EVENTS, 257*b9c1b51eSKate Stone "%s ctx.Events().WaitForSetEvents(0x%08x) ...", 258*b9c1b51eSKate Stone __FUNCTION__, event_mask); 259d676074dSJason Molenda nub_event_t set_events = ctx.Events().WaitForSetEvents(event_mask); 260*b9c1b51eSKate Stone DNBLogThreadedIf(LOG_RNB_EVENTS, 261*b9c1b51eSKate Stone "%s ctx.Events().WaitForSetEvents(0x%08x) => 0x%08x (%s)", 262*b9c1b51eSKate Stone __FUNCTION__, event_mask, set_events, 263*b9c1b51eSKate Stone ctx.EventsAsString(set_events, set_events_str)); 264d676074dSJason Molenda 265*b9c1b51eSKate Stone if (set_events) { 266d676074dSJason Molenda if ((set_events & RNBContext::event_proc_thread_exiting) || 267*b9c1b51eSKate Stone (set_events & RNBContext::event_proc_stdio_available)) { 268d676074dSJason Molenda remote->FlushSTDIO(); 269d676074dSJason Molenda } 270d676074dSJason Molenda 271*b9c1b51eSKate Stone if (set_events & RNBContext::event_read_packet_available) { 272d676074dSJason Molenda // handleReceivedPacket will take care of resetting the 273d676074dSJason Molenda // event_read_packet_available events when there are no more... 274d676074dSJason Molenda set_events ^= RNBContext::event_read_packet_available; 275d676074dSJason Molenda 276*b9c1b51eSKate Stone if (ctx.ProcessStateRunning()) { 277*b9c1b51eSKate Stone if (remote->HandleAsyncPacket() == rnb_not_connected) { 278d676074dSJason Molenda // TODO: connect again? Exit? 279d676074dSJason Molenda } 280*b9c1b51eSKate Stone } else { 281*b9c1b51eSKate Stone if (remote->HandleReceivedPacket() == rnb_not_connected) { 282d676074dSJason Molenda // TODO: connect again? Exit? 283d676074dSJason Molenda } 284d676074dSJason Molenda } 285d676074dSJason Molenda } 286d676074dSJason Molenda 287*b9c1b51eSKate Stone if (set_events & RNBContext::event_proc_state_changed) { 288d676074dSJason Molenda mode = HandleProcessStateChange(remote, false); 289d676074dSJason Molenda ctx.Events().ResetEvents(RNBContext::event_proc_state_changed); 290d676074dSJason Molenda set_events ^= RNBContext::event_proc_state_changed; 291d676074dSJason Molenda } 292d676074dSJason Molenda 293*b9c1b51eSKate Stone if (set_events & RNBContext::event_proc_thread_exiting) { 294d676074dSJason Molenda mode = eRNBRunLoopModeExit; 295d676074dSJason Molenda } 296d676074dSJason Molenda 297*b9c1b51eSKate Stone if (set_events & RNBContext::event_read_thread_exiting) { 298d676074dSJason Molenda // Out remote packet receiving thread exited, exit for now. 299*b9c1b51eSKate Stone if (ctx.HasValidProcessID()) { 300d676074dSJason Molenda // TODO: We should add code that will leave the current process 301d676074dSJason Molenda // in its current state and listen for another connection... 302*b9c1b51eSKate Stone if (ctx.ProcessStateRunning()) { 303ec2f90c0SDaniel Malea DNBProcessKill(ctx.ProcessID()); 304d676074dSJason Molenda } 305d676074dSJason Molenda } 306d676074dSJason Molenda mode = eRNBRunLoopModeExit; 307d676074dSJason Molenda } 308d676074dSJason Molenda } 309d676074dSJason Molenda 310d676074dSJason Molenda // Reset all event bits that weren't reset for now... 311d676074dSJason Molenda if (set_events != 0) 312d676074dSJason Molenda ctx.Events().ResetEvents(set_events); 313d676074dSJason Molenda 314d676074dSJason Molenda if (mode != eRNBRunLoopModeInferiorExecuting) 315d676074dSJason Molenda break; 316d676074dSJason Molenda } 317d676074dSJason Molenda 318d676074dSJason Molenda return mode; 319d676074dSJason Molenda } 320d676074dSJason Molenda 321*b9c1b51eSKate Stone void ASLLogCallback(void *baton, uint32_t flags, const char *format, 322*b9c1b51eSKate Stone va_list args) { 323d676074dSJason Molenda #if 0 324d676074dSJason Molenda vprintf(format, args); 325d676074dSJason Molenda #endif 326d676074dSJason Molenda } 327d676074dSJason Molenda 328*b9c1b51eSKate Stone extern "C" int debug_server_main(int fd) { 329d676074dSJason Molenda #if 1 330d676074dSJason Molenda g_isatty = 0; 331d676074dSJason Molenda #else 332d676074dSJason Molenda g_isatty = ::isatty(STDIN_FILENO); 333d676074dSJason Molenda 334d676074dSJason Molenda DNBLogSetDebug(1); 335d676074dSJason Molenda DNBLogSetVerbose(1); 336d676074dSJason Molenda DNBLogSetLogMask(-1); 337d676074dSJason Molenda DNBLogSetLogCallback(ASLLogCallback, NULL); 338d676074dSJason Molenda #endif 339d676074dSJason Molenda 340d676074dSJason Molenda signal(SIGPIPE, signal_handler); 341d676074dSJason Molenda 342d676074dSJason Molenda g_remoteSP.reset(new RNBRemote); 343d676074dSJason Molenda 344d676074dSJason Molenda RNBRemote *remote = g_remoteSP.get(); 345*b9c1b51eSKate Stone if (remote == NULL) { 346d676074dSJason Molenda RNBLogSTDERR("error: failed to create a remote connection class\n"); 347d676074dSJason Molenda return -1; 348d676074dSJason Molenda } 349d676074dSJason Molenda 350d676074dSJason Molenda RNBRunLoopMode mode = eRNBRunLoopModeGetStartModeFromRemoteProtocol; 351d676074dSJason Molenda 352*b9c1b51eSKate Stone while (mode != eRNBRunLoopModeExit) { 353*b9c1b51eSKate Stone switch (mode) { 354d676074dSJason Molenda case eRNBRunLoopModeGetStartModeFromRemoteProtocol: 355d676074dSJason Molenda if (g_remoteSP->Comm().useFD(fd) == rnb_success) { 356d676074dSJason Molenda RNBLogSTDOUT("Starting remote data thread.\n"); 357d676074dSJason Molenda g_remoteSP->StartReadRemoteDataThread(); 358d676074dSJason Molenda 359d676074dSJason Molenda RNBLogSTDOUT("Waiting for start mode from remote.\n"); 360d676074dSJason Molenda mode = RNBRunLoopGetStartModeFromRemote(g_remoteSP); 361*b9c1b51eSKate Stone } else { 362d676074dSJason Molenda mode = eRNBRunLoopModeExit; 363d676074dSJason Molenda } 364d676074dSJason Molenda break; 365d676074dSJason Molenda 366d676074dSJason Molenda case eRNBRunLoopModeInferiorExecuting: 367d676074dSJason Molenda mode = RNBRunLoopInferiorExecuting(g_remoteSP); 368d676074dSJason Molenda break; 369d676074dSJason Molenda 370d676074dSJason Molenda default: 371d676074dSJason Molenda mode = eRNBRunLoopModeExit; 372d676074dSJason Molenda break; 373d676074dSJason Molenda 374d676074dSJason Molenda case eRNBRunLoopModeExit: 375d676074dSJason Molenda break; 376d676074dSJason Molenda } 377d676074dSJason Molenda } 378d676074dSJason Molenda 379d676074dSJason Molenda g_remoteSP->StopReadRemoteDataThread(); 380d676074dSJason Molenda g_remoteSP->Context().SetProcessID(INVALID_NUB_PROCESS); 381d676074dSJason Molenda 382d676074dSJason Molenda return 0; 383d676074dSJason Molenda } 384