Implement async-break
[hiphop-php.git] / hphp / runtime / ext / vsdebug / debugger.cpp
blob00fc32df79e69460d03f14631f763cca7be26a94
1 /*
2 +----------------------------------------------------------------------+
3 | HipHop for PHP |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2017-present Facebook, Inc. (http://www.facebook.com) |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
17 #include "hphp/runtime/base/file.h"
18 #include "hphp/runtime/base/exceptions.h"
19 #include "hphp/runtime/base/intercept.h"
20 #include "hphp/runtime/base/stat-cache.h"
21 #include "hphp/runtime/base/unit-cache.h"
22 #include "hphp/runtime/debugger/debugger.h"
23 #include "hphp/runtime/ext/vsdebug/ext_vsdebug.h"
24 #include "hphp/runtime/ext/vsdebug/debugger.h"
25 #include "hphp/runtime/ext/vsdebug/command.h"
26 #include "hphp/util/process.h"
28 #include <boost/filesystem.hpp>
30 namespace HPHP {
31 namespace VSDEBUG {
33 Debugger::Debugger() :
34 m_sessionCleanupThread(this, &Debugger::runSessionCleanupThread) {
36 m_sessionCleanupThread.start();
39 void Debugger::setTransport(DebugTransport* transport) {
40 assertx(m_transport == nullptr);
41 m_transport = transport;
42 setClientConnected(m_transport->clientConnected());
45 bool Debugger::getDebuggerOption(const HPHP::String& option) {
46 std::string optionStr = option.toCppString();
48 // It's easier for the user if this routine is not case-sensitive.
49 std::transform(optionStr.begin(), optionStr.end(), optionStr.begin(),
50 [](unsigned char c){ return std::tolower(c); });
52 DebuggerOptions options = getDebuggerOptions();
54 if (optionStr == "showdummyonasyncpause") {
55 return options.showDummyOnAsyncPause;
56 } else if (optionStr == "warnoninterceptedfunctions") {
57 return options.warnOnInterceptedFunctions;
58 } else if (optionStr == "notifyonbpcalibration") {
59 return options.notifyOnBpCalibration;
60 } else if (optionStr == "disableuniquevarref") {
61 return options.disableUniqueVarRef;
62 } else if (optionStr == "disabledummypsps") {
63 return options.disableDummyPsPs;
64 } else if (optionStr == "disableStdoutRedirection") {
65 return options.disableStdoutRedirection;
66 } else {
67 raise_error("setDebuggerOption: Unknown option specified");
71 void Debugger::setDebuggerOption(const HPHP::String& option, bool value) {
72 std::string optionStr = option.toCppString();
74 // It's easier for the user if this routine is not case-sensitive.
75 std::transform(optionStr.begin(), optionStr.end(), optionStr.begin(),
76 [](unsigned char c){ return std::tolower(c); });
78 DebuggerOptions options = getDebuggerOptions();
80 if (optionStr == "showdummyonasyncpause") {
81 options.showDummyOnAsyncPause = value;
82 } else if (optionStr == "warnoninterceptedfunctions") {
83 options.warnOnInterceptedFunctions = value;
84 } else if (optionStr == "notifyonbpcalibration") {
85 options.notifyOnBpCalibration = value;
86 } else if (optionStr == "disableuniquevarref") {
87 options.disableUniqueVarRef = value;
88 } else if (optionStr == "disabledummypsps") {
89 options.disableDummyPsPs = value;
90 } else if (optionStr == "disableStdoutRedirection") {
91 options.disableStdoutRedirection = value;
92 } else {
93 raise_error("getDebuggerOption: Unknown option specified");
96 setDebuggerOptions(options);
99 void Debugger::setDebuggerOptions(DebuggerOptions options) {
100 Lock lock(m_lock);
101 m_debuggerOptions = options;
103 VSDebugLogger::Log(
104 VSDebugLogger::LogLevelInfo,
105 "Client options set:\n"
106 "showDummyOnAsyncPause: %s\n"
107 "warnOnInterceptedFunctions: %s\n"
108 "notifyOnBpCalibration: %s\n"
109 "disableUniqueVarRef: %s\n"
110 "disableDummyPsPs: %s\n"
111 "maxReturnedStringLength: %d\n"
112 "disableStdoutRedirection: %s\n",
113 options.showDummyOnAsyncPause ? "YES" : "NO",
114 options.warnOnInterceptedFunctions ? "YES" : "NO",
115 options.notifyOnBpCalibration ? "YES" : "NO",
116 options.disableUniqueVarRef ? "YES" : "NO",
117 options.disableDummyPsPs ? "YES" : "NO",
118 options.maxReturnedStringLength,
119 options.disableStdoutRedirection ? "YES" : "NO"
123 void Debugger::runSessionCleanupThread() {
124 bool terminating = false;
125 std::unordered_set<DebuggerSession*> sessionsToDelete;
127 while (true) {
129 std::unique_lock<std::mutex> lock(m_sessionCleanupLock);
131 // Read m_sessionCleanupTerminating while the lock is held.
132 terminating = m_sessionCleanupTerminating;
134 if (!terminating) {
135 // Wait for signal
136 m_sessionCleanupCondition.wait(lock);
138 // Re-check terminating flag while the lock is still re-acquired.
139 terminating = m_sessionCleanupTerminating;
142 // Make a local copy of the session pointers to delete and drop
143 // the lock.
144 sessionsToDelete = m_cleanupSessions;
145 m_cleanupSessions.clear();
148 // Free the sessions.
149 for (DebuggerSession* sessionToDelete : sessionsToDelete) {
150 delete sessionToDelete;
153 if (terminating) {
154 break;
160 void Debugger::setClientConnected(
161 bool connected,
162 bool synchronous /* = false */,
163 ClientInfo* clientInfo /* = nullptr */
165 DebuggerSession* sessionToDelete = nullptr;
166 SCOPE_EXIT {
167 if (sessionToDelete != nullptr) {
168 // Unless the debugger is shutting down (in which case we need to
169 // join with the worker threads), delete the session asynchronously.
170 // The session destructor needs to wait to join with the dummy thread,
171 // which could take a while if it's running native code.
172 if (synchronous) {
173 delete sessionToDelete;
174 } else {
175 std::unique_lock<std::mutex> lock(m_sessionCleanupLock);
176 m_cleanupSessions.insert(sessionToDelete);
177 m_sessionCleanupCondition.notify_all();
180 VSDebugLogger::LogFlush();
185 Lock lock(m_lock);
187 VSDebugLogger::SetLogRotationEnabled(connected);
189 // Store connected first. New request threads will first check this value
190 // to quickly determine if a debugger client is connected to avoid having
191 // to grab a lock on the request init path in the case where this extension
192 // is enabled, but not in use by any client.
193 m_clientConnected.store(connected, std::memory_order_release);
195 VSDebugLogger::Log(
196 VSDebugLogger::LogLevelInfo,
197 "Debugger client connected: %s",
198 connected ? "YES" : "NO"
201 // Defer cleaning up the session until after we've dropped the lock.
202 // Shutting down the dummy request thread will cause the worker to call
203 // back into this extension since the HHVM context will call requestShutdown
204 // on the dummy request.
205 if (m_session != nullptr) {
206 sessionToDelete = m_session;
207 m_session = nullptr;
210 if (connected) {
211 // Ensure usage logging is initialized if configured. Init is a no-op if
212 // the logger is already initialized.
213 auto logger = Eval::Debugger::GetUsageLogger();
214 if (logger != nullptr) {
215 logger->init();
217 if (clientInfo == nullptr) {
218 VSDebugLogger::Log(
219 VSDebugLogger::LogLevelInfo,
220 "Clearing client info. Connected client has no ID."
222 logger->clearClientInfo();
223 } else {
224 VSDebugLogger::Log(
225 VSDebugLogger::LogLevelInfo,
226 "Setting connected client info: user=%s,uid=%d,pid=%d",
227 clientInfo->clientUser.c_str(),
228 clientInfo->clientUid,
229 clientInfo->clientPid
231 logger->setClientInfo(
232 clientInfo->clientUser,
233 clientInfo->clientUid,
234 clientInfo->clientPid
237 } else {
238 VSDebugLogger::Log(
239 VSDebugLogger::LogLevelWarning,
240 "Not logging user: no usage logger configured."
244 VSDebugLogger::LogFlush();
246 // Create a new debugger session.
247 assertx(m_session == nullptr);
248 m_session = new DebuggerSession(this);
249 if (m_session == nullptr) {
250 VSDebugLogger::Log(
251 VSDebugLogger::LogLevelError,
252 "Failed to allocate debugger session!"
254 m_clientConnected.store(false, std::memory_order_release);
257 // When the client connects, break the entire program to get it into a
258 // known state, set initial breakpoints and then wait for
259 // the client to send a configurationDone command, which will resume
260 // the target.
261 m_state = ProgramState::LoaderBreakpoint;
263 // Attach the debugger to any request threads that were already
264 // running before the client connected. We did not attach to them if they
265 // were initialized when the client was disconnected to avoid taking a
266 // perf hit for a debugger hook that wasn't going to be used.
268 // Only do this after seeing at least 1 request via the requestInit path
269 // in script mode: on initial startup the script request thread will have
270 // been created already in HHVM main, but is not ready for us to attach
271 // yet because extensions are still being initialized.
272 if (RuntimeOption::ServerExecutionMode() ||
273 m_totalRequestCount.load() > 0) {
275 RequestInfo::ExecutePerRequest([this] (RequestInfo* ti) {
276 this->attachToRequest(ti);
280 executeForEachAttachedRequest(
281 [&](RequestInfo* ti, DebuggerRequestInfo* ri) {
282 if (ri->m_breakpointInfo == nullptr) {
283 ri->m_breakpointInfo = new RequestBreakpointInfo();
286 if (ri->m_breakpointInfo == nullptr) {
287 VSDebugLogger::Log(
288 VSDebugLogger::LogLevelError,
289 "Failed to allocate request breakpoint info!"
293 true /* includeDummyRequest */
296 // If the script startup thread is waiting for a client connection, wake
297 // it up now.
299 std::unique_lock<std::mutex> lock(m_connectionNotifyLock);
300 m_connectionNotifyCondition.notify_all();
302 } else {
303 // disconnected case
304 auto logger = Eval::Debugger::GetUsageLogger();
305 if (logger != nullptr) {
306 VSDebugLogger::Log(
307 VSDebugLogger::LogLevelInfo,
308 "Clearing client info - user disconnected."
310 logger->clearClientInfo();
313 // The client has detached. Walk through any requests we are currently
314 // attached to and release them if they are blocked in the debugger.
315 executeForEachAttachedRequest(
316 [&](RequestInfo* ti, DebuggerRequestInfo* ri) {
317 // Clear any undelivered client messages in the request's command
318 // queue, since they apply to the debugger session that just ended.
319 // NOTE: The request's DebuggerRequestInfo will be cleaned up and freed when
320 // the request completes in requestShutdown. It is not safe to do that
321 // from this thread.
322 ri->m_commandQueue.clearPendingMessages();
324 // Have the debugger hook update the output hook on next interrupt.
325 ri->m_flags.outputHooked = false;
326 ti->m_reqInjectionData.setDebuggerIntr(true);
328 // Clear the breakpoint info.
329 if (ri->m_breakpointInfo != nullptr) {
330 delete ri->m_breakpointInfo;
331 ri->m_breakpointInfo = nullptr;
334 ri->m_breakpointInfo = new RequestBreakpointInfo();
335 if (ri->m_breakpointInfo != nullptr) {
336 assertx(ri->m_breakpointInfo->m_pendingBreakpoints.empty());
337 assertx(ri->m_breakpointInfo->m_unresolvedBreakpoints.empty());
339 updateUnresolvedBpFlag(ri);
341 true /* includeDummyRequest */
344 m_clientInitialized = false;
346 // If we launched this request in debugger launch mode, detach of
347 // the client should terminate the request.
348 if (VSDebugExtension::s_launchMode) {
349 interruptAllThreads();
352 DebuggerHook::setActiveDebuggerInstance(
353 VSDebugHook::GetInstance(),
354 false
357 resumeTarget();
362 void Debugger::setClientInitialized() {
363 Lock lock(m_lock);
364 if (m_clientInitialized) {
365 return;
368 m_clientInitialized = true;
370 // Send a thread start event for any thread that exists already at the point
371 // the debugger client initializes communication so that they appear in the
372 // client side thread list.
373 for (auto it = m_requestIdMap.begin(); it != m_requestIdMap.end(); it++) {
374 if (it->second->m_executing == RequestInfo::Executing::UserFunctions ||
375 it->second->m_executing == RequestInfo::Executing::RuntimeFunctions) {
376 sendThreadEventMessage(it->first, ThreadEventType::ThreadStarted);
381 std::string Debugger::getDebuggerSessionAuth() {
382 Lock lock(m_lock);
384 if (!clientConnected() || getCurrentThreadId() != kDummyTheadId) {
385 return "";
388 return m_session->getDebuggerSessionAuth();
391 request_id_t Debugger::getCurrentThreadId() {
392 Lock lock(m_lock);
394 if (isDummyRequest()) {
395 return kDummyTheadId;
398 RequestInfo* const threadInfo = &RI();
399 const auto it = m_requestInfoMap.find(threadInfo);
400 if (it == m_requestInfoMap.end()) {
401 return -1;
404 return it->second;
407 void Debugger::cleanupRequestInfo(RequestInfo* ti, DebuggerRequestInfo* ri) {
408 std::atomic_thread_fence(std::memory_order_acquire);
409 if (ti != nullptr && isDebuggerAttached(ti)) {
410 DebuggerHook::detach(ti);
413 // Shut down the request's command queue. This releases the thread if
414 // it is waiting inside the queue.
415 ri->m_commandQueue.shutdown();
417 if (ri->m_breakpointInfo != nullptr) {
418 delete ri->m_breakpointInfo;
421 assertx(ri->m_serverObjects.size() == 0);
422 delete ri;
425 void Debugger::cleanupServerObjectsForRequest(DebuggerRequestInfo* ri) {
426 m_lock.assertOwnedBySelf();
428 std::unordered_map<unsigned int, ServerObject*>& objs = ri->m_serverObjects;
430 for (auto it = objs.begin(); it != objs.end();) {
431 unsigned int objectId = it->first;
433 if (m_session != nullptr) {
434 m_session->onServerObjectDestroyed(objectId);
437 // Free the object. Note if the object is a variable in request memory,
438 // the destruction of ServerObject releases the GC root we're holding.
439 ServerObject* object = it->second;
440 delete object;
442 it = objs.erase(it);
445 assertx(objs.size() == 0);
448 void Debugger::executeForEachAttachedRequest(
449 std::function<void(RequestInfo* ti, DebuggerRequestInfo* ri)> callback,
450 bool includeDummyRequest
452 m_lock.assertOwnedBySelf();
454 DebuggerRequestInfo* dummyRequestInfo = getDummyRequestInfo();
455 std::for_each(m_requests.begin(), m_requests.end(), [](auto &it) {
456 it.second->m_flags.alive = false;
460 RequestInfo::ExecutePerRequest([&] (RequestInfo* ti) {
461 auto it = m_requests.find(ti);
462 if (it != m_requests.end() && it->second != dummyRequestInfo) {
463 it->second->m_flags.alive = true;
464 callback(it->first, it->second);
468 // This is to catch requests that have fallen out of the list without a
469 // requestShutdown notification.
470 auto it = m_requests.begin();
471 while (it != m_requests.end()) {
472 if (it->second->m_flags.alive) {
473 it++;
474 continue;
476 it = m_requests.erase(it);
479 if (includeDummyRequest && dummyRequestInfo != nullptr) {
480 callback(nullptr, dummyRequestInfo);
484 void Debugger::getAllThreadInfo(folly::dynamic& threads) {
485 assertx(threads.isArray());
486 executeForEachAttachedRequest(
487 [&](RequestInfo* ti, DebuggerRequestInfo* ri) {
488 threads.push_back(folly::dynamic::object);
489 folly::dynamic& threadInfo = threads[threads.size() - 1];
491 auto it = m_requestInfoMap.find(ti);
492 if (it != m_requestInfoMap.end()) {
493 request_id_t requestId = it->second;
494 threadInfo["id"] = requestId;
495 threadInfo["name"] = std::string("Request ") +
496 std::to_string(requestId);
498 if (!ri->m_requestUrl.empty()) {
499 threadInfo["name"] += ": " + ri->m_requestUrl;
503 false /* includeDummyRequest */
506 // the dummy request isn't in the request map, so add info for it manually
507 threads.push_back(folly::dynamic::object);
508 folly::dynamic& threadInfo = threads[threads.size() - 1];
509 threadInfo["id"] = 0;
510 threadInfo["name"] = std::string("Console/REPL request");
513 void Debugger::shutdown() {
514 if (m_transport == nullptr) {
515 return;
518 trySendTerminatedEvent();
520 m_transport->shutdown();
521 setClientConnected(false, true);
523 // m_session is deleted and set to nullptr by setClientConnected(false).
524 assertx(m_session == nullptr);
526 delete m_transport;
527 m_transport = nullptr;
530 std::unique_lock<std::mutex> lock(m_sessionCleanupLock);
531 m_sessionCleanupTerminating = true;
532 m_sessionCleanupCondition.notify_all();
535 m_sessionCleanupThread.waitForEnd();
538 void Debugger::trySendTerminatedEvent() {
539 Lock lock(m_lock);
541 folly::dynamic event = folly::dynamic::object;
542 sendEventMessage(event, "terminated", true);
545 void Debugger::sendStoppedEvent(
546 const char* reason,
547 const char* displayReason,
548 request_id_t threadId,
549 bool focusedThread,
550 int breakpointId
552 Lock lock(m_lock);
554 folly::dynamic event = folly::dynamic::object;
555 const bool allThreadsStopped = m_pausedRequestCount == m_requests.size();
557 if (!allThreadsStopped && threadId < 0) {
558 // Don't send a stop message for a specific thread if this stop
559 // event doesn't have a valid thread id.
560 return;
563 event["allThreadsStopped"] = allThreadsStopped;
565 if (threadId >= 0) {
566 event["threadId"] = threadId;
569 if (reason != nullptr) {
570 event["reason"] = reason;
573 if (breakpointId >= 0) {
574 event["breakpointId"] = breakpointId;
577 if (displayReason == nullptr) {
578 event["description"] = "execution paused";
579 } else {
580 event["description"] = displayReason;
583 event["preserveFocusHint"] = !focusedThread;
584 sendEventMessage(event, "stopped");
587 void Debugger::sendContinuedEvent(request_id_t threadId) {
588 Lock lock(m_lock);
589 folly::dynamic event = folly::dynamic::object;
590 event["allThreadsContinued"] = m_pausedRequestCount == 0;
591 if (threadId >= 0) {
592 event["threadId"] = threadId;
595 sendEventMessage(event, "continued");
598 void Debugger::sendUserMessage(const char* message, const char* level) {
599 Lock lock(m_lock);
601 if (!clientConnected()) {
602 return;
605 if (m_transport != nullptr) {
606 m_transport->enqueueOutgoingUserMessage(
607 getCurrentThreadId(), message, level
612 void Debugger::sendEventMessage(
613 folly::dynamic& event,
614 const char* eventType,
615 bool sendImmediately /* = false */
617 Lock lock(m_lock);
619 // Deferred events are sent after the response to the current debugger client
620 // request is sent, except in the case where we're hitting a nested breakpoint
621 // during an evaluation: that we must send immediately because the evaluation
622 // response won't happen until the client resumes from the inner breakpoint.
623 DebuggerRequestInfo* ri = getRequestInfo();
624 if (ri != nullptr &&
625 (ri->m_evaluateCommandDepth > 0 || ri-> m_pauseRecurseCount > 0)) {
627 sendImmediately = true;
630 if (!sendImmediately && m_processingClientCommand) {
631 // If an outgoing client event is generated while the debugger is processing
632 // a command for the client, keep it in a queue and send it after the
633 // response is sent for the current client command.
634 PendingEventMessage pendingEvent;
635 pendingEvent.m_message = event;
636 pendingEvent.m_eventType = eventType;
637 m_pendingEventMessages.push_back(pendingEvent);
638 } else {
639 // Otherwise, go ahead and send it immediately.
640 if (m_transport != nullptr) {
641 m_transport->enqueueOutgoingEventMessage(event, eventType);
646 void Debugger::sendThreadEventMessage(
647 request_id_t threadId,
648 ThreadEventType eventType
650 Lock lock(m_lock);
652 if (!m_clientInitialized) {
653 // Don't start sending the client "thread started" and "thread exited"
654 // events before its finished its initialization flow.
655 // In this case, we'll send a thread started event for any threads that are
656 // currently running when initialization completes (and any threads that
657 // exit before that point, the debugger never needs to know about).
658 return;
661 folly::dynamic event = folly::dynamic::object;
663 event["reason"] = eventType == ThreadEventType::ThreadStarted
664 ? "started"
665 : "exited";
667 event["threadId"] = threadId;
669 sendEventMessage(event, "thread", true);
672 void Debugger::requestInit() {
673 Lock lock(m_lock);
675 m_totalRequestCount++;
677 if (!clientConnected()) {
678 // Don't pay for attaching to the thread if no debugger client is connected.
679 return;
682 RequestInfo* const threadInfo = &RI();
683 bool pauseRequest;
684 DebuggerRequestInfo* requestInfo;
686 bool dummy = isDummyRequest();
688 // Don't attach to the dummy request thread. DebuggerSession manages the
689 // dummy requests debugger hook state.
690 if (!dummy) {
691 requestInfo = attachToRequest(threadInfo);
692 pauseRequest = m_state != ProgramState::Running;
693 } else {
694 pauseRequest = false;
697 // If the debugger was already paused when this request started, block the
698 // request in its command queue until the debugger resumes.
699 if (pauseRequest && requestInfo != nullptr) {
700 processCommandQueue(
701 getCurrentThreadId(),
702 requestInfo,
703 "entry",
704 nullptr,
705 false,
711 void Debugger::enterDebuggerIfPaused(DebuggerRequestInfo* requestInfo) {
712 Lock lock(m_lock);
714 if (!clientConnected() && VSDebugExtension::s_launchMode) {
715 // If the debugger client launched this script in launch mode, and
716 // has detached while the request is still running, terminate the
717 // request by throwing a fatal PHP exception.
718 VSDebugLogger::Log(
719 VSDebugLogger::LogLevelInfo,
720 "Debugger client detached and we launched this script. "
721 "Killing request with fatal error."
724 raise_fatal_error(
725 "Request terminated due to debugger client detaching.",
726 null_array,
727 false,
728 true,
729 true
733 if (m_state != ProgramState::Running) {
734 if (requestInfo->m_stepReason != nullptr) {
735 processCommandQueue(
736 getCurrentThreadId(),
737 requestInfo,
738 "step",
739 requestInfo->m_stepReason,
740 true,
743 } else if (m_state == ProgramState::AsyncPaused) {
744 processCommandQueue(
745 getCurrentThreadId(),
746 requestInfo,
747 "async-pause",
748 "async-pause",
749 true,
752 } else {
753 processCommandQueue(
754 getCurrentThreadId(),
755 requestInfo,
756 "pause",
757 nullptr,
758 false,
765 void Debugger::processCommandQueue(
766 request_id_t threadId,
767 DebuggerRequestInfo* requestInfo,
768 const char* reason,
769 const char* displayReason,
770 bool focusedThread,
771 int bpId
773 m_lock.assertOwnedBySelf();
775 if (requestInfo->m_pauseRecurseCount == 0) {
776 m_pausedRequestCount++;
779 requestInfo->m_pauseRecurseCount++;
780 requestInfo->m_totalPauseCount++;
782 // Don't actually tell the client about the stop if it's due to the
783 // loader breakpoint, this is an internal implementation detail that
784 // should not be visible to the client.
785 bool sendEvent = m_state != ProgramState::LoaderBreakpoint;
786 if (sendEvent) {
787 sendStoppedEvent(reason, displayReason, threadId, focusedThread, bpId);
790 VSDebugLogger::Log(
791 VSDebugLogger::LogLevelInfo,
792 "Thread %d pausing",
793 threadId
796 // Drop the lock before entering the command queue and re-acquire it
797 // when leaving the command queue.
798 m_lock.unlock();
799 requestInfo->m_commandQueue.processCommands();
800 m_lock.lock();
802 requestInfo->m_pauseRecurseCount--;
803 if (requestInfo->m_pauseRecurseCount == 0) {
804 m_pausedRequestCount--;
807 if (sendEvent) {
808 sendContinuedEvent(threadId);
811 // Any server objects stored for the client for this request are invalid
812 // as soon as the thread is allowed to step.
813 cleanupServerObjectsForRequest(requestInfo);
815 // Once any request steps, we must invalidate cached globals and constants,
816 // since the user code could have modified them.
817 if (m_session != nullptr) {
818 m_session->clearCachedVariable(DebuggerSession::kCachedVariableKeyAll);
821 VSDebugLogger::Log(
822 VSDebugLogger::LogLevelInfo,
823 "Thread %d resumed",
824 threadId
827 m_resumeCondition.notify_all();
830 DebuggerRequestInfo* Debugger::createRequestInfo() {
831 DebuggerRequestInfo* requestInfo = new DebuggerRequestInfo();
832 if (requestInfo == nullptr) {
833 // Failed to allocate request info.
834 VSDebugLogger::Log(
835 VSDebugLogger::LogLevelError,
836 "Failed to allocate request info!"
838 return nullptr;
841 assertx(requestInfo->m_allFlags == 0);
843 requestInfo->m_breakpointInfo = new RequestBreakpointInfo();
844 if (requestInfo->m_breakpointInfo == nullptr) {
845 // Failed to allocate breakpoint info.
846 VSDebugLogger::Log(
847 VSDebugLogger::LogLevelError,
848 "Failed to allocate request breakpoint info!"
850 delete requestInfo;
851 return nullptr;
854 return requestInfo;
857 request_id_t Debugger::nextThreadId() {
858 request_id_t threadId = m_nextThreadId++;
860 // Unlikely: handle rollver. Id 0 is reserved for the dummy, and then
861 // in the very unlikley event that there's a very long running request
862 // we need to ensure we don't reuse its id.
863 if (threadId == 0) {
864 threadId++;
867 while (m_requestIdMap.find(threadId) != m_requestIdMap.end()) {
868 threadId++;
871 return threadId;
874 DebuggerRequestInfo* Debugger::attachToRequest(RequestInfo* ti) {
875 m_lock.assertOwnedBySelf();
877 DebuggerRequestInfo* requestInfo = nullptr;
879 request_id_t threadId;
880 auto it = m_requests.find(ti);
881 if (it == m_requests.end()) {
882 // New request. Insert a request info object into our map.
883 threadId = nextThreadId();
884 assertx(threadId > 0);
886 requestInfo = createRequestInfo();
887 if (requestInfo == nullptr) {
888 // Failed to allocate request info.
889 return nullptr;
892 m_requests.emplace(std::make_pair(ti, requestInfo));
893 m_requestIdMap.emplace(std::make_pair(threadId, ti));
894 m_requestInfoMap.emplace(std::make_pair(ti, threadId));
896 VSDebugLogger::Log(
897 VSDebugLogger::LogLevelInfo,
898 "Created new request info for request %d (current thread=%d), flags=%u",
899 threadId,
900 (int64_t)Process::GetThreadId(),
901 static_cast<unsigned int>(requestInfo->m_allFlags)
904 } else {
905 requestInfo = it->second;
906 auto idIt = m_requestInfoMap.find(ti);
907 assertx(idIt != m_requestInfoMap.end());
908 threadId = idIt->second;
910 VSDebugLogger::Log(
911 VSDebugLogger::LogLevelInfo,
912 "Found existing request info for thread %d, flags=%u",
913 threadId,
914 static_cast<unsigned int>(requestInfo->m_allFlags)
918 assertx(requestInfo != nullptr && requestInfo->m_breakpointInfo != nullptr);
920 // Have the debugger hook update the output hook when we enter the debugger.
921 requestInfo->m_flags.outputHooked = false;
923 ti->m_reqInjectionData.setDebuggerIntr(true);
925 if (ti->m_executing == RequestInfo::Executing::UserFunctions ||
926 ti->m_executing == RequestInfo::Executing::RuntimeFunctions) {
927 sendThreadEventMessage(threadId, ThreadEventType::ThreadStarted);
930 // Try to attach our debugger hook to the request.
931 if (!isDebuggerAttached(ti)) {
932 if (DebuggerHook::attach<VSDebugHook>(ti)) {
933 ti->m_reqInjectionData.setFlag(DebuggerSignalFlag);
935 // Install all breakpoints as pending for this request.
936 const std::unordered_set<int> breakpoints =
937 m_session->getBreakpointManager()->getAllBreakpointIds();
938 for (auto it = breakpoints.begin(); it != breakpoints.end(); it++) {
939 requestInfo->m_breakpointInfo->m_pendingBreakpoints.emplace(*it);
941 } else {
942 m_transport->enqueueOutgoingUserMessage(
943 kDummyTheadId,
944 "Failed to attach to new HHVM request: another debugger is already "
945 "attached.",
946 DebugTransport::OutputLevelError
949 VSDebugLogger::Log(
950 VSDebugLogger::LogLevelError,
951 "Failed to attach to new HHVM request: another debugger is already "
952 "attached."
955 } else {
956 VSDebugLogger::Log(
957 VSDebugLogger::LogLevelInfo,
958 "Not attaching to request %d, a debug hook is already attached.",
959 threadId
963 updateUnresolvedBpFlag(requestInfo);
964 return requestInfo;
967 void Debugger::requestShutdown() {
968 auto const threadInfo = &RI();
969 DebuggerRequestInfo* requestInfo = nullptr;
970 request_id_t threadId = -1;
972 SCOPE_EXIT {
973 if (clientConnected() && threadId >= 0) {
974 sendThreadEventMessage(threadId, ThreadEventType::ThreadExited);
975 m_session->getBreakpointManager()->onRequestShutdown(threadId);
978 if (requestInfo != nullptr) {
979 cleanupRequestInfo(threadInfo, requestInfo);
984 Lock lock(m_lock);
985 auto it = m_requests.find(threadInfo);
986 if (it == m_requests.end()) {
987 return;
990 requestInfo = it->second;
991 m_requests.erase(it);
993 auto infoItr = m_requestInfoMap.find(threadInfo);
994 assertx(infoItr != m_requestInfoMap.end());
996 threadId = infoItr->second;
997 auto idItr = m_requestIdMap.find(threadId);
998 assertx(idItr != m_requestIdMap.end());
1000 m_requestIdMap.erase(idItr);
1001 m_requestInfoMap.erase(infoItr);
1003 if (!g_context.isNull()) {
1004 g_context->removeStdoutHook(getStdoutHook());
1006 Logger::SetThreadHook(nullptr);
1008 // Cleanup any server objects for this request before dropping the lock.
1009 cleanupServerObjectsForRequest(requestInfo);
1013 DebuggerRequestInfo* Debugger::getDummyRequestInfo() {
1014 m_lock.assertOwnedBySelf();
1015 if (!clientConnected()) {
1016 return nullptr;
1019 return m_session->m_dummyRequestInfo;
1022 DebuggerRequestInfo* Debugger::getRequestInfo(request_id_t threadId /* = -1 */) {
1023 Lock lock(m_lock);
1025 if (threadId != -1) {
1026 // Find the info for the requested thread ID.
1027 if (threadId == kDummyTheadId) {
1028 return getDummyRequestInfo();
1031 auto it = m_requestIdMap.find(threadId);
1032 if (it != m_requestIdMap.end()) {
1033 auto requestIt = m_requests.find(it->second);
1034 if (requestIt != m_requests.end()) {
1035 return requestIt->second;
1038 } else {
1039 // Find the request info for the current request thread.
1040 if (isDummyRequest()) {
1041 return getDummyRequestInfo();
1044 auto it = m_requests.find(&RI());
1045 if (it != m_requests.end()) {
1046 return it->second;
1050 return nullptr;
1053 bool Debugger::executeClientCommand(
1054 VSCommand* command,
1055 std::function<bool(DebuggerSession* session,
1056 folly::dynamic& responseMsg)> callback
1058 Lock lock(m_lock);
1060 // If there is no debugger client connected anymore, the client command
1061 // should not be processed, and the target request thread should resume.
1062 if (!clientConnected()) {
1063 return true;
1066 try {
1067 enforceRequiresBreak(command);
1069 // Invoke the command execute callback. It will return true if this thread
1070 // should be resumed, or false if it should continue to block in its
1071 // command queue.
1072 folly::dynamic responseMsg = folly::dynamic::object;
1074 m_processingClientCommand = true;
1075 SCOPE_EXIT {
1076 m_processingClientCommand = false;
1078 for (PendingEventMessage& message : m_pendingEventMessages) {
1079 sendEventMessage(message.m_message, message.m_eventType);
1082 m_pendingEventMessages.clear();
1085 // Log command if logging is enabled.
1086 logClientCommand(command);
1088 bool resumeThread = callback(m_session, responseMsg);
1089 if (command->commandTarget() != CommandTarget::WorkItem) {
1090 sendCommandResponse(command, responseMsg);
1092 return resumeThread;
1093 } catch (DebuggerCommandException &e) {
1094 reportClientMessageError(command->getMessage(), e.what());
1095 } catch (...) {
1096 reportClientMessageError(command->getMessage(), InternalErrorMsg);
1099 // On error, do not resume the request thread.
1100 return false;
1103 void Debugger::executeWithoutLock(std::function<void()> callback) {
1104 m_lock.assertOwnedBySelf();
1105 m_lock.unlock();
1107 callback();
1109 m_lock.lock();
1110 m_lock.assertOwnedBySelf();
1113 void Debugger::reportClientMessageError(
1114 folly::dynamic& clientMsg,
1115 const char* errorMessage
1117 try {
1118 VSDebugLogger::Log(
1119 VSDebugLogger::LogLevelError,
1120 "Failed to process client message (%s): %s",
1121 folly::toJson(clientMsg).c_str(),
1122 errorMessage
1125 folly::dynamic responseMsg = folly::dynamic::object;
1126 responseMsg["success"] = false;
1127 responseMsg["request_seq"] = clientMsg["seq"];
1128 responseMsg["command"] = clientMsg["command"];
1129 responseMsg["message"] = errorMessage;
1131 m_transport->enqueueOutgoingMessageForClient(
1132 responseMsg,
1133 DebugTransport::MessageTypeResponse
1137 } catch (...) {
1138 // We tried.
1139 VSDebugLogger::Log(
1140 VSDebugLogger::LogLevelError,
1141 "Unexpected failure while trying to send response to client."
1146 void Debugger::sendCommandResponse(
1147 VSCommand* command,
1148 folly::dynamic& responseMsg
1150 folly::dynamic& clientMsg = command->getMessage();
1151 responseMsg["success"] = true;
1152 responseMsg["request_seq"] = clientMsg["seq"];
1153 responseMsg["command"] = clientMsg["command"];
1155 m_transport->enqueueOutgoingMessageForClient(
1156 responseMsg,
1157 DebugTransport::MessageTypeResponse
1161 void Debugger::resumeTarget() {
1162 m_lock.assertOwnedBySelf();
1164 m_state = ProgramState::Running;
1166 // Resume every paused request. Each request will send a thread continued
1167 // event when it exits its command loop.
1168 executeForEachAttachedRequest(
1169 [&](RequestInfo* ti, DebuggerRequestInfo* ri) {
1170 ri->m_stepReason = nullptr;
1172 if (ri->m_pauseRecurseCount > 0) {
1173 VSCommand* resumeCommand = ContinueCommand::createInstance(this);
1174 ri->m_commandQueue.dispatchCommand(resumeCommand);
1177 true /* includeDummyRequest */
1180 sendContinuedEvent(-1);
1183 Debugger::PrepareToPauseResult
1184 Debugger::prepareToPauseTarget(DebuggerRequestInfo* requestInfo) {
1185 m_lock.assertOwnedBySelf();
1187 if ((m_state == ProgramState::Paused ||
1188 m_state == ProgramState::AsyncPaused) &&
1189 isStepInProgress(requestInfo)) {
1190 // A step operation for a single request is still in the middle of
1191 // handling whatever caused us to break execution of the other threads
1192 // in the first place. We don't need to wait for resume here.
1193 return clientConnected() ? ReadyToPause : ErrorNoClient;
1196 // Wait here until the program is in a consistent state, and this thread
1197 // is ready to pause the target: this is the case when the current thread
1198 // is holding m_lock, the program is running (m_state == Running), and no
1199 // threads are still in the process of resuming from the previous break
1200 // (m_pausedRequestCount == 0).
1201 while (m_state != ProgramState::Running || m_pausedRequestCount > 0) {
1202 m_lock.assertOwnedBySelf();
1204 if (m_state != ProgramState::Running) {
1205 // The target is paused by another thread. Process this thread's
1206 // command queue to service the current break.
1207 // NOTE: processCommandQueue drops and re-acquires m_lock.
1208 if (requestInfo != nullptr) {
1209 processCommandQueue(
1210 getCurrentThreadId(),
1211 requestInfo,
1212 "pause",
1213 nullptr,
1214 false,
1217 } else {
1218 // This is true only in the case of async-break, which is not
1219 // specific to any request. Ok to proceed here, async-break just
1220 // wants to break the target, and it is broken.
1221 break;
1223 } else {
1224 assertx(m_state == ProgramState::Running && m_pausedRequestCount > 0);
1226 // The target is running, but at least one thread is still paused.
1227 // This means a resume is in progress, drop the lock and wait for
1228 // all threads to finish resuming.
1230 // If a resume is currently in progress, we must wait for all threads
1231 // to resume execution before breaking again. Otherwise, the client will
1232 // see interleaved continue and stop events and the the state of the UX
1233 // becomes undefined.
1234 std::unique_lock<std::mutex> conditionLock(m_resumeMutex);
1236 // Need to drop m_lock before waiting.
1237 m_lock.unlock();
1239 m_resumeCondition.wait(conditionLock);
1240 conditionLock.unlock();
1242 // And re-acquire it before continuing.
1243 m_lock.lock();
1244 conditionLock.lock();
1248 m_lock.assertOwnedBySelf();
1249 assertx(requestInfo == nullptr || m_state == ProgramState::Running);
1251 return clientConnected() ? ReadyToPause : ErrorNoClient;
1254 void Debugger::pauseTarget(DebuggerRequestInfo* ri, bool isAsyncPause) {
1255 m_lock.assertOwnedBySelf();
1257 m_state = isAsyncPause ? ProgramState::AsyncPaused : ProgramState::Paused;
1259 if (ri != nullptr) {
1260 clearStepOperation(ri);
1263 interruptAllThreads();
1266 void Debugger::dispatchCommandToRequest(
1267 request_id_t requestId,
1268 VSCommand* command
1270 m_lock.assertOwnedBySelf();
1272 DebuggerRequestInfo* ri = nullptr;
1273 if (requestId == kDummyTheadId) {
1274 ri = getDummyRequestInfo();
1275 } else {
1276 auto it = m_requestIdMap.find(requestId);
1277 if (it != m_requestIdMap.end()) {
1278 const auto request = m_requests.find(it->second);
1279 assertx(request != m_requests.end());
1280 ri = request->second;
1284 if (ri == nullptr) {
1285 // Delete the command because the caller expects the command queue
1286 // to have taken ownership of it.
1287 delete command;
1288 } else {
1289 ri->m_commandQueue.dispatchCommand(command);
1293 void Debugger::logClientCommand(
1294 VSCommand* command
1296 auto logger = Eval::Debugger::GetUsageLogger();
1297 if (logger == nullptr) {
1298 return;
1301 const std::string cmd(command->commandName());
1303 // Don't bother logging certain very chatty commands. These can
1304 // be sent frequently by the client UX and their arguments aren't
1305 // interesting from a security perspective.
1306 if (cmd == "CompletionsCommand" ||
1307 cmd == "ContinueCommand" ||
1308 cmd == "StackTraceCommand" ||
1309 cmd == "ThreadsCommand") {
1310 return;
1313 try {
1314 folly::json::serialization_opts jsonOptions;
1315 jsonOptions.sort_keys = true;
1316 jsonOptions.pretty_formatting = true;
1318 const std::string sandboxId = g_context.isNull()
1319 ? ""
1320 : g_context->getSandboxId().toCppString();
1321 const std::string data =
1322 folly::json::serialize(command->getMessage(), jsonOptions);
1323 const std::string mode = RuntimeOption::ServerExecutionMode()
1324 ? "vsdebug-webserver"
1325 : "vsdebug-script";
1326 logger->log(mode, sandboxId, cmd, data);
1327 VSDebugLogger::Log(
1328 VSDebugLogger::LogLevelVerbose,
1329 "Logging client command: %s: %s\n",
1330 cmd.c_str(), data.c_str()
1332 } catch (...) {
1333 VSDebugLogger::Log(
1334 VSDebugLogger::LogLevelError,
1335 "Error logging client command"
1340 void Debugger::onClientMessage(folly::dynamic& message) {
1341 Lock lock(m_lock);
1343 // It's possible the client disconnected between the time the message was
1344 // received and when the lock was acquired in this routine. If the client
1345 // has gone, do not process the message.
1346 if (!clientConnected()) {
1347 return;
1350 VSCommand* command = nullptr;
1351 SCOPE_EXIT {
1352 if (command != nullptr) {
1353 delete command;
1357 try {
1359 // All valid client messages should have a sequence number and type.
1360 try {
1361 const auto& seq = message["seq"];
1362 if (!seq.isInt()) {
1363 throw DebuggerCommandException("Invalid message sequence number.");
1366 const auto& type = message["type"];
1367 if (!type.isString() || type.getString().empty()) {
1368 throw DebuggerCommandException("Invalid command type.");
1370 } catch (std::out_of_range &e) {
1371 throw DebuggerCommandException(
1372 "Message is missing a required attribute."
1376 if (!VSCommand::parseCommand(this, message, &command)) {
1377 assertx(command == nullptr);
1379 try {
1380 auto cmdName = message["command"];
1381 if (cmdName.isString()) {
1382 std::string commandName = cmdName.asString();
1383 std::string errorMsg("The command \"");
1384 errorMsg += commandName;
1385 errorMsg += "\" was invalid or is not implemented in the debugger.";
1386 throw DebuggerCommandException(errorMsg.c_str());
1388 } catch (std::out_of_range &e) {
1391 throw DebuggerCommandException(
1392 "The command was invalid or is not implemented in the debugger."
1396 assertx(command != nullptr);
1397 enforceRequiresBreak(command);
1399 // Otherwise this is a normal command. Dispatch it to its target.
1400 switch(command->commandTarget()) {
1401 case CommandTarget::None:
1402 case CommandTarget::WorkItem:
1403 if (command->execute()) {
1404 // The command requested that the target be resumed. A command with
1405 // CommandTarget == None that does this resumes the entire program.
1406 resumeTarget();
1408 break;
1409 case CommandTarget::Request:
1410 // Dispatch this command to the correct request.
1412 DebuggerRequestInfo* ri = nullptr;
1413 const auto threadId = command->targetThreadId(m_session);
1414 if (threadId == kDummyTheadId) {
1415 ri = getDummyRequestInfo();
1416 } else {
1417 auto it = m_requestIdMap.find(threadId);
1418 if (it != m_requestIdMap.end()) {
1419 const auto request = m_requests.find(it->second);
1420 assertx(request != m_requests.end());
1421 ri = request->second;
1425 if (ri == nullptr) {
1426 constexpr char* errorMsg =
1427 "The requested thread ID does not exist in the target.";
1428 reportClientMessageError(message, errorMsg);
1429 } else {
1430 ri->m_commandQueue.dispatchCommand(command);
1432 // Lifetime of command is now owned by the request thread's queue.
1433 command = nullptr;
1436 break;
1437 case CommandTarget::Dummy:
1438 // Dispatch this command to the dummy thread.
1439 m_session->enqueueDummyCommand(command);
1441 // Lifetime of command is now owned by the dummy thread's queue.
1442 command = nullptr;
1443 break;
1444 default:
1445 assertx(false);
1447 } catch (DebuggerCommandException &e) {
1448 reportClientMessageError(message, e.what());
1449 } catch (...) {
1450 reportClientMessageError(message, InternalErrorMsg);
1454 void Debugger::waitForClientConnection() {
1455 std::unique_lock<std::mutex> lock(m_connectionNotifyLock);
1456 if (clientConnected()) {
1457 return;
1460 while (!clientConnected()) {
1461 m_connectionNotifyCondition.wait(lock);
1465 void Debugger::setClientPreferences(ClientPreferences& preferences) {
1466 Lock lock(m_lock);
1467 if (!clientConnected()) {
1468 return;
1471 assertx(m_session != nullptr);
1472 m_session->setClientPreferences(preferences);
1475 ClientPreferences Debugger::getClientPreferences() {
1476 Lock lock(m_lock);
1477 if (!clientConnected()) {
1478 ClientPreferences empty = {};
1479 return empty;
1482 assertx(m_session != nullptr);
1483 return m_session->getClientPreferences();
1486 void Debugger::startDummyRequest(
1487 const std::string& startupDoc,
1488 const std::string& sandboxUser,
1489 const std::string& sandboxName,
1490 const std::string& debuggerSessionAuthToken,
1491 bool displayStartupMsg
1493 Lock lock(m_lock);
1494 if (!clientConnected()) {
1495 return;
1498 assertx(m_session != nullptr);
1499 m_session->startDummyRequest(
1500 startupDoc,
1501 sandboxUser,
1502 sandboxName,
1503 debuggerSessionAuthToken,
1504 displayStartupMsg
1508 void Debugger::setDummyThreadId(int64_t threadId) {
1509 Lock lock(m_lock);
1510 m_dummyThreadId = threadId;
1513 void Debugger::onBreakpointAdded(int bpId) {
1514 Lock lock(m_lock);
1516 assertx(m_session != nullptr);
1518 // Now to actually install the breakpoints, each request thread needs to
1519 // process the bp and set it in some TLS data structures. If the program
1520 // is already paused, then every request thread is blocking in its command
1521 // loop: we'll put a work item to resolve the bp in each command queue.
1523 // Otherwise, if the program is running, we need to gain control of each
1524 // request thread by interrupting it. It will install the bp when it
1525 // calls into the command hook on the next Hack opcode.
1526 executeForEachAttachedRequest(
1527 [&](RequestInfo* ti, DebuggerRequestInfo* ri) {
1528 if (ti != nullptr) {
1529 ti->m_reqInjectionData.setDebuggerIntr(true);
1532 ri->m_breakpointInfo->m_pendingBreakpoints.emplace(bpId);
1533 updateUnresolvedBpFlag(ri);
1535 // If the program is running, the request thread will pick up and install
1536 // the breakpoint the next time it calls into the opcode hook, except for
1537 // the dummy thread, it's not running anything, so it won't ever call the
1538 // opcode hook. Ask it to resolve the breakpoint.
1540 // Per contract with executeForEachAttachedRequest, ti == nullptr if and
1541 // only if DebuggerRequestInfo points to the dummy's request info.
1542 if (m_state != ProgramState::Running || ti == nullptr) {
1543 const auto cmd = ResolveBreakpointsCommand::createInstance(this);
1544 ri->m_commandQueue.dispatchCommand(cmd);
1547 true /* includeDummyRequest */
1551 void Debugger::tryInstallBreakpoints(DebuggerRequestInfo* ri) {
1552 Lock lock(m_lock);
1554 if (!clientConnected()) {
1555 return;
1558 // Create a map of the normalized file paths of all compilation units that
1559 // have already been loaded by this request before the debugger attached to
1560 // it to allow for quick lookup when resolving breakpoints. Any units loaded
1561 // after this will be added to the map by onCompilationUnitLoaded().
1562 if (!ri->m_flags.compilationUnitsMapped) {
1563 ri->m_flags.compilationUnitsMapped = true;
1564 const auto evaledFiles = g_context->m_evaledFiles;
1565 for (auto it = evaledFiles.begin(); it != evaledFiles.end(); it++) {
1566 const HPHP::Unit* compilationUnit = it->second.unit;
1567 const std::string filePath = getFilePathForUnit(compilationUnit);
1568 ri->m_breakpointInfo->m_loadedUnits[filePath] = compilationUnit;
1572 // For any breakpoints that are pending for this request, try to resolve
1573 // and install them, or mark them as unresolved.
1574 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1575 auto& pendingBps = ri->m_breakpointInfo->m_pendingBreakpoints;
1577 for (auto it = pendingBps.begin(); it != pendingBps.end();) {
1578 const int breakpointId = *it;
1579 const Breakpoint* bp = bpMgr->getBreakpointById(breakpointId);
1581 // Remove the breakpoint from "pending". After this point, it will
1582 // either be resolved, or unresolved, and no longer pending install.
1583 it = pendingBps.erase(it);
1585 // It's ok if bp was not found. The client could have removed the
1586 // breakpoint before this request got a chance to install it.
1587 if (bp == nullptr) {
1588 continue;
1591 bool resolved = tryResolveBreakpoint(ri, breakpointId, bp);
1592 // This breakpoint could not be resolved yet. As new compilation units
1593 // are loaded, we'll try again.
1594 // In debugger clients that support multiple languages like Nuclide,
1595 // users tend to leave breakpoints set in files that are for other
1596 // debuggers. It's annoying to see warnings in those cases. Assume
1597 // any file path that doesn't end in PHP is ok not to tell the user
1598 // that the breakpoint failed to set.
1599 const bool phpFile =
1600 bp->m_path.size() >= 4 &&
1601 std::equal(
1602 bp->m_path.rbegin(),
1603 bp->m_path.rend(),
1604 std::string(".php").rbegin()
1606 if (phpFile && !resolved) {
1607 std::string resolveMsg = "Warning: request ";
1608 resolveMsg += std::to_string(getCurrentThreadId());
1609 resolveMsg += " could not resolve breakpoint #";
1610 resolveMsg += std::to_string(breakpointId);
1611 resolveMsg += ". The Hack/PHP file at ";
1612 resolveMsg += bp->m_path;
1613 resolveMsg += " is not yet loaded, or failed to compile.";
1615 sendUserMessage(
1616 resolveMsg.c_str(),
1617 DebugTransport::OutputLevelWarning
1620 if (!resolved || bp->isRelativeBp()) {
1621 ri->m_breakpointInfo->m_unresolvedBreakpoints.emplace(breakpointId);
1625 updateUnresolvedBpFlag(ri);
1626 assertx(ri->m_breakpointInfo->m_pendingBreakpoints.empty());
1629 bool Debugger::tryResolveBreakpoint(
1630 DebuggerRequestInfo* ri,
1631 const int bpId,
1632 const Breakpoint* bp
1634 if (bp->m_type == BreakpointType::Source) {
1635 // Search all compilation units loaded by this request for a matching
1636 // location for this breakpoint.
1637 const auto& loadedUnits = ri->m_breakpointInfo->m_loadedUnits;
1638 for (auto it = loadedUnits.begin(); it != loadedUnits.end(); it++) {
1639 if (tryResolveBreakpointInUnit(ri, bpId, bp, it->first, it->second)) {
1640 // Found a match, and installed the breakpoint!
1641 return true;
1644 } else {
1645 assertx(bp->m_type == BreakpointType::Function);
1647 const HPHP::String functionName(bp->m_function);
1648 Func* func = Func::lookup(functionName.get());
1650 if (func != nullptr) {
1651 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1653 if ((func->fullName() != nullptr &&
1654 bp->m_function == func->fullName()->toCppString()) ||
1655 (func->name() != nullptr &&
1656 bp->m_function == func->name()->toCppString())) {
1658 // Found a matching function!
1659 phpAddBreakPointFuncEntry(func);
1660 bpMgr->onFuncBreakpointResolved(*const_cast<Breakpoint*>(bp), func);
1662 return true;
1667 return false;
1670 bool Debugger::tryResolveBreakpointInUnit(const DebuggerRequestInfo* /*ri*/, int bpId,
1671 const Breakpoint* bp,
1672 const std::string& unitFilePath,
1673 const HPHP::Unit* compilationUnit) {
1675 if (bp->m_type != BreakpointType::Source ||
1676 !BreakpointManager::bpMatchesPath(
1678 boost::filesystem::path(unitFilePath))) {
1680 return false;
1683 std::pair<int,int> lines =
1684 calibrateBreakpointLineInUnit(compilationUnit, bp->m_line);
1686 if (lines.first > 0 && lines.second != lines.first) {
1687 lines = calibrateBreakpointLineInUnit(compilationUnit, lines.first);
1690 if (lines.first < 0) {
1691 VSDebugLogger::Log(
1692 VSDebugLogger::LogLevelError,
1693 "NOT installing bp ID %d in file %s. No source locations matching "
1694 " line %d were found.",
1695 bpId,
1696 unitFilePath.c_str(),
1697 bp->m_line
1699 return false;
1702 VSDebugLogger::Log(
1703 VSDebugLogger::LogLevelInfo,
1704 "Installing bp ID %d at line %d (original line was %d) of file %s.",
1705 bpId,
1706 lines.first,
1707 bp->m_line,
1708 unitFilePath.c_str()
1711 if (!phpAddBreakPointLine(compilationUnit, lines.first)) {
1712 VSDebugLogger::Log(
1713 VSDebugLogger::LogLevelError,
1714 "Installing %d at line %d of file %s FAILED in phpAddBreakPointLine!",
1715 bpId,
1716 lines.first,
1717 unitFilePath.c_str()
1719 return false;
1722 // Warn the user if the breakpoint is going into a unit that has intercepted
1723 // functions or a memoized function. We can't be certain this breakpoint is
1724 // reachable in code anymore.
1725 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1727 std::string functionName = "";
1728 const HPHP::Func* function = nullptr;
1729 if (m_debuggerOptions.notifyOnBpCalibration &&
1730 !bpMgr->warningSentForBp(bpId)) {
1732 compilationUnit->forEachFunc([&](const Func* func) {
1733 if (func != nullptr &&
1734 func->name() != nullptr &&
1735 func->line1() <= lines.first &&
1736 func->line2() >= lines.second) {
1737 std::string clsName =
1738 func->preClass() != nullptr && func->preClass()->name() != nullptr
1739 ? std::string(func->preClass()->name()->data()) + "::"
1740 : "";
1741 functionName = clsName;
1742 functionName += func->name()->data();
1743 function = func;
1744 return true;
1746 return false;
1749 if (function != nullptr &&
1750 (function->isMemoizeWrapper() || function->isMemoizeImpl())) {
1751 // This breakpoint looks like it's going into either a memoized
1752 // function, or the wrapper for a memoized function. That means after
1753 // the first time this routine executes, it might not execute again,
1754 // even if the code is invoked again. Warn the user because this can
1755 // cause confusion.
1756 bpMgr->sendMemoizeWarning(bpId);
1760 bpMgr->onBreakpointResolved(
1761 bpId,
1762 lines.first,
1763 lines.second,
1766 unitFilePath,
1767 functionName
1770 return true;
1773 std::pair<int, int> Debugger::calibrateBreakpointLineInUnit(
1774 const Unit* unit,
1775 int bpLine
1777 // Attempt to find a matching source location entry in the compilation unit
1778 // that corresponds to the breakpoint's requested line number. Note that the
1779 // line provided by the client could be in the middle of a multi-line
1780 // statement, or could be on a line that contains multiple statements. It
1781 // could also be in whitespace, or past the end of the file.
1782 std::pair<int, int> bestLocation = {-1, -1};
1783 struct sourceLocCompare {
1784 bool operator()(const SourceLoc& a, const SourceLoc& b) const {
1785 if (a.line0 == b.line0) {
1786 return a.line1 < b.line1;
1789 return a.line0 < b.line0;
1793 std::set<SourceLoc, sourceLocCompare> candidateLocations;
1794 unit->forEachFunc([&](const Func* func) {
1795 const auto& table = func->getLocTable();
1796 for (auto const& tableEntry : table) {
1797 const SourceLoc& sourceLocation = tableEntry.val();
1799 // If this source location is invalid, ends before the line we are
1800 // looking for, or starts before the line we are looking for there
1801 // is no match. Exception: if it is a multi-line statement that begins
1802 // before the target line and ends ON the target line, that is a match
1803 // to allow, for example, setting a breakpoint on the line containing
1804 // a closing paren for a multi-line function call.
1805 if (!sourceLocation.valid() ||
1806 sourceLocation.line1 < bpLine ||
1807 (sourceLocation.line0 < bpLine && sourceLocation.line1 != bpLine)) {
1809 continue;
1812 candidateLocations.insert(sourceLocation);
1814 return false;
1817 if (candidateLocations.size() > 0) {
1818 const auto it = candidateLocations.begin();
1819 const SourceLoc& location = *it;
1820 bestLocation.first = location.line0;
1821 bestLocation.second = location.line1;
1824 return bestLocation;
1827 void Debugger::onFunctionDefined(
1828 DebuggerRequestInfo* ri,
1829 const Func* func,
1830 const std::string& funcName
1832 Lock lock(m_lock);
1834 if (!clientConnected()) {
1835 return;
1838 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1839 auto& unresolvedBps = ri->m_breakpointInfo->m_unresolvedBreakpoints;
1841 for (auto it = unresolvedBps.begin(); it != unresolvedBps.end(); ) {
1842 const int bpId = *it;
1843 const Breakpoint* bp = bpMgr->getBreakpointById(bpId);
1844 if (bp == nullptr) {
1845 // Breakpoint has been removed by the client.
1846 it = unresolvedBps.erase(it);
1847 continue;
1850 if (bp->m_type == BreakpointType::Function && funcName == bp->m_function) {
1851 // Found a matching function!
1852 phpAddBreakPointFuncEntry(func);
1853 bpMgr->onFuncBreakpointResolved(*const_cast<Breakpoint*>(bp), func);
1855 // Breakpoint is no longer unresolved!
1856 it = unresolvedBps.erase(it);
1857 } else {
1858 // Still no match, move on to the next unresolved function breakpoint.
1859 it++;
1863 updateUnresolvedBpFlag(ri);
1866 void Debugger::onCompilationUnitLoaded(
1867 DebuggerRequestInfo* ri,
1868 const HPHP::Unit* compilationUnit
1870 Lock lock(m_lock);
1872 if (!clientConnected()) {
1873 return;
1876 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1877 const auto filePath = getFilePathForUnit(compilationUnit);
1879 if (ri->m_breakpointInfo->m_loadedUnits[filePath] != nullptr &&
1880 ri->m_breakpointInfo->m_loadedUnits[filePath] != compilationUnit) {
1882 const auto& bps = bpMgr->getBreakpointIdsForPath(filePath);
1884 VSDebugLogger::Log(
1885 VSDebugLogger::LogLevelInfo,
1886 "Compilation unit for %s changed/reloaded in request %d. ",
1887 filePath.c_str(),
1888 getCurrentThreadId()
1891 // The unit has been re-loaded from disk since the last time we saw it.
1892 // We must re-place any breakpoints in this file into the new unit.
1893 for (const auto bpId : bps) {
1894 auto it = ri->m_breakpointInfo->m_unresolvedBreakpoints.find(bpId);
1895 if (it == ri->m_breakpointInfo->m_unresolvedBreakpoints.end()) {
1896 ri->m_breakpointInfo->m_unresolvedBreakpoints.emplace(bpId);
1901 ri->m_breakpointInfo->m_loadedUnits[filePath] = compilationUnit;
1903 // See if any unresolved breakpoints for this request can be placed in the
1904 // compilation unit that just loaded.
1905 auto& unresolvedBps = ri->m_breakpointInfo->m_unresolvedBreakpoints;
1907 for (auto it = unresolvedBps.begin(); it != unresolvedBps.end();) {
1908 const int bpId = *it;
1909 const Breakpoint* bp = bpMgr->getBreakpointById(bpId);
1910 if (bp == nullptr ||
1911 tryResolveBreakpointInUnit(ri, bpId, bp, filePath, compilationUnit)) {
1913 if (bp == nullptr || !bp->isRelativeBp()) {
1914 // If this breakpoint no longer exists (it was removed by the client),
1915 // or it was successfully installed, then it is no longer unresolved.
1916 it = unresolvedBps.erase(it);
1917 continue;
1921 it++;
1924 updateUnresolvedBpFlag(ri);
1927 void Debugger::onFuncIntercepted(std::string funcName) {
1928 Lock lock(m_lock);
1930 if (!clientConnected()) {
1931 return;
1934 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1935 bpMgr->onFuncIntercepted(getCurrentThreadId(), funcName);
1938 void Debugger::onFuncBreakpointHit(
1939 DebuggerRequestInfo* ri,
1940 const HPHP::Func* func
1942 Lock lock(m_lock);
1944 if (func != nullptr) {
1945 onBreakpointHit(ri, func->unit(), func, func->line1());
1949 void Debugger::onLineBreakpointHit(
1950 DebuggerRequestInfo* ri,
1951 const HPHP::Unit* compilationUnit,
1952 int line
1954 Lock lock(m_lock);
1955 onBreakpointHit(ri, compilationUnit, nullptr, line);
1958 void Debugger::onBreakpointHit(
1959 DebuggerRequestInfo* ri,
1960 const HPHP::Unit* compilationUnit,
1961 const HPHP::Func* func,
1962 int line
1964 std::string stopReason;
1965 const std::string filePath = getFilePathForUnit(compilationUnit);
1967 if (prepareToPauseTarget(ri) != PrepareToPauseResult::ReadyToPause) {
1968 VSDebugLogger::Log(
1969 VSDebugLogger::LogLevelError,
1970 "onBreakpointHit: Not pausing target, the client disconnected."
1972 return;
1975 BreakpointManager* bpMgr = m_session->getBreakpointManager();
1977 const auto removeBreakpoint =
1978 [&](BreakpointType type) {
1979 if (type == BreakpointType::Source) {
1980 phpRemoveBreakPointLine(compilationUnit, line);
1981 } else {
1982 assertx(type == BreakpointType::Function);
1983 assertx(func != nullptr);
1984 phpRemoveBreakPointFuncEntry(func);
1988 const auto bps = bpMgr->getAllBreakpointIds();
1989 for (auto it = bps.begin(); it != bps.end(); it++) {
1990 const int bpId = *it;
1991 Breakpoint* bp = bpMgr->getBreakpointById(bpId);
1992 if (bp == nullptr) {
1993 continue;
1996 const auto resolvedLocation = bpMgr->bpResolvedInfoForFile(bp, filePath);
1997 bool lineInRange = line >= resolvedLocation.m_startLine &&
1998 line <= resolvedLocation.m_endLine;
2000 if (resolvedLocation.m_path == filePath && lineInRange) {
2001 if (bpMgr->isBreakConditionSatisified(ri, bp)) {
2002 stopReason = getStopReasonForBp(
2004 filePath,
2005 line
2008 // Breakpoint hit!
2009 pauseTarget(ri, false);
2010 bpMgr->onBreakpointHit(bpId);
2012 processCommandQueue(
2013 getCurrentThreadId(),
2015 "breakpoint",
2016 stopReason.c_str(),
2017 true,
2018 bpId
2021 return;
2022 } else {
2023 VSDebugLogger::Log(
2024 VSDebugLogger::LogLevelInfo,
2025 "onBreakpointHit: Not pausing target, breakpoint found but the bp "
2026 " condition (%s) is not satisfied.",
2027 bp->getCondition().c_str()
2033 if (ri->m_runToLocationInfo.path == filePath &&
2034 line == ri->m_runToLocationInfo.line) {
2036 // Hit our run to location destination!
2037 stopReason = "Run to location";
2038 ri->m_runToLocationInfo.path = "";
2039 ri->m_runToLocationInfo.line = -1;
2041 // phpRemoveBreakpointLine doesn't refcount or anything, so it's only
2042 // safe to remove this if there is no real bp at the line.
2043 bool realBp = false;
2044 BreakpointManager* bpMgr = m_session->getBreakpointManager();
2045 const auto bpIds = bpMgr->getBreakpointIdsForPath(filePath);
2046 for (auto it = bpIds.begin(); it != bpIds.end(); it++) {
2047 Breakpoint* bp = bpMgr->getBreakpointById(*it);
2048 if (bp != nullptr && bp->m_line == line) {
2049 realBp = true;
2050 break;
2054 if (!realBp) {
2055 removeBreakpoint(BreakpointType::Source);
2058 pauseTarget(ri, false);
2059 processCommandQueue(
2060 getCurrentThreadId(),
2062 "step",
2063 stopReason.c_str(),
2064 true,
2070 void Debugger::onExceptionBreakpointHit(
2071 DebuggerRequestInfo* ri,
2072 const std::string& exceptionName,
2073 const std::string& exceptionMsg
2075 std::string stopReason("Exception (");
2076 stopReason += exceptionName;
2077 stopReason += ") thrown";
2079 std::string userMsg = "Request ";
2080 userMsg += std::to_string(getCurrentThreadId());
2081 userMsg += ": ";
2082 userMsg += stopReason + ": ";
2083 userMsg += exceptionMsg;
2085 Lock lock(m_lock);
2087 if (!clientConnected()) {
2088 return;
2091 BreakpointManager* bpMgr = m_session->getBreakpointManager();
2092 ExceptionBreakMode breakMode = bpMgr->getExceptionBreakMode();
2094 if (breakMode == BreakNone) {
2095 return;
2098 sendUserMessage(userMsg.c_str(), DebugTransport::OutputLevelWarning);
2100 if (breakMode == BreakUnhandled || breakMode == BreakUserUnhandled) {
2101 // The PHP VM doesn't give us any way to distinguish between handled
2102 // and unhandled exceptions. A message was already printed indicating
2103 // the exception, but we won't actually break in.
2104 return;
2107 assertx(breakMode == BreakAll);
2109 if (prepareToPauseTarget(ri) != PrepareToPauseResult::ReadyToPause) {
2110 return;
2113 pauseTarget(ri, false);
2114 processCommandQueue(
2115 getCurrentThreadId(),
2117 "exception",
2118 stopReason.c_str(),
2119 true,
2124 bool Debugger::onHardBreak() {
2125 static constexpr char* stopReason = "hphp_debug_break()";
2127 Lock lock(m_lock);
2128 VMRegAnchor regAnchor;
2129 DebuggerRequestInfo* ri = getRequestInfo();
2131 if (ri == nullptr) {
2132 return false;
2135 if (!Debugger::isStepInProgress(ri)) {
2136 enterDebuggerIfPaused(ri);
2139 if (g_context->m_dbgNoBreak || ri->m_flags.doNotBreak) {
2140 return false;
2143 if (!clientConnected() ||
2144 prepareToPauseTarget(ri) != PrepareToPauseResult::ReadyToPause) {
2146 return false;
2149 pauseTarget(ri, false);
2150 processCommandQueue(
2151 getCurrentThreadId(),
2153 "breakpoint",
2154 stopReason,
2155 true,
2159 // We actually need to step out here, because as far as the PC filter is
2160 // concerned, hphp_debug_break() was a function call that increased the
2161 // stack depth.
2162 phpDebuggerStepOut();
2164 return true;
2167 void Debugger::onAsyncBreak() {
2168 Lock lock(m_lock);
2170 if (m_state == ProgramState::Paused || m_state == ProgramState::AsyncPaused) {
2171 // Already paused.
2172 return;
2175 if (prepareToPauseTarget(nullptr) != PrepareToPauseResult::ReadyToPause) {
2176 return;
2179 VSDebugLogger::Log(
2180 VSDebugLogger::LogLevelInfo,
2181 "Debugger paused due to async-break request from client."
2184 pauseTarget(nullptr, true);
2186 if (m_debuggerOptions.showDummyOnAsyncPause) {
2187 // Show the dummy request as stopped.
2188 constexpr char* reason = "Async-break";
2189 sendStoppedEvent(reason, reason, 0, false, -1);
2193 void Debugger::onError(DebuggerRequestInfo* requestInfo,
2194 const ExtendedException& /*extendedException*/,
2195 int errnum, const std::string& message) {
2196 const char* phpError;
2197 switch (static_cast<ErrorMode>(errnum)) {
2198 case ErrorMode::ERROR:
2199 case ErrorMode::CORE_ERROR:
2200 case ErrorMode::COMPILE_ERROR:
2201 case ErrorMode::USER_ERROR:
2202 phpError = "Fatal error";
2203 break;
2204 case ErrorMode::RECOVERABLE_ERROR:
2205 phpError = "Catchable fatal error";
2206 break;
2207 case ErrorMode::WARNING:
2208 case ErrorMode::CORE_WARNING:
2209 case ErrorMode::COMPILE_WARNING:
2210 case ErrorMode::USER_WARNING:
2211 phpError = "Warning";
2212 break;
2213 case ErrorMode::PARSE:
2214 phpError = "Parse error";
2215 break;
2216 case ErrorMode::NOTICE:
2217 case ErrorMode::USER_NOTICE:
2218 phpError = "Notice";
2219 break;
2220 case ErrorMode::STRICT:
2221 phpError = "Strict standards";
2222 break;
2223 case ErrorMode::PHP_DEPRECATED:
2224 case ErrorMode::USER_DEPRECATED:
2225 phpError = "Deprecated";
2226 break;
2227 default:
2228 phpError = "Unknown error";
2229 break;
2232 onExceptionBreakpointHit(requestInfo, phpError, message);
2235 std::string Debugger::getFilePathForUnit(const HPHP::Unit* compilationUnit) {
2236 const auto path =
2237 HPHP::String(const_cast<StringData*>(compilationUnit->filepath()));
2238 const auto translatedPath = File::TranslatePath(path).toCppString();
2239 return StatCache::realpath(translatedPath.c_str());
2242 std::string Debugger::getStopReasonForBp(
2243 const Breakpoint* bp,
2244 const std::string& path,
2245 const int line
2247 std::string description("Breakpoint " + std::to_string(bp->m_id));
2248 if (!path.empty()) {
2249 auto const name = boost::filesystem::path(path).filename().string();
2250 description += " (";
2251 description += name;
2252 description += ":";
2253 description += std::to_string(line);
2254 description += ")";
2257 if (bp->m_type == BreakpointType::Function) {
2258 description += " - " + bp->m_functionFullName + "()";
2261 return description;
2264 void Debugger::interruptAllThreads() {
2265 executeForEachAttachedRequest(
2266 [&](RequestInfo* ti, DebuggerRequestInfo* ri) {
2267 assertx(ti != nullptr);
2268 ti->m_reqInjectionData.setDebuggerIntr(true);
2270 false /* includeDummyRequest */
2274 void DebuggerStdoutHook::operator()(const char* str, int len) {
2275 fflush(stdout);
2276 write(fileno(stdout), str, len);
2278 // Quickly no-op if there's no client.
2279 if (!m_debugger->clientConnected()) {
2280 return;
2283 std::string output = std::string(str, len);
2284 m_debugger->sendUserMessage(
2285 output.c_str(),
2286 DebugTransport::OutputLevelStdout);
2289 void DebuggerStderrHook::
2290 operator()(const char*, const char* msg, const char* /*ending*/
2292 // Quickly no-op if there's no client.
2293 if (!m_debugger->clientConnected()) {
2294 return;
2297 m_debugger->sendUserMessage(msg, DebugTransport::OutputLevelStderr);
2300 SilentEvaluationContext::SilentEvaluationContext(
2301 Debugger* debugger,
2302 DebuggerRequestInfo* ri,
2303 bool suppressOutput /* = true */
2304 ) : m_ri(ri), m_suppressOutput(suppressOutput) {
2305 // Disable hitting breaks of any kind due to this eval.
2306 m_ri->m_flags.doNotBreak = true;
2307 std::atomic_thread_fence(std::memory_order_release);
2309 g_context->m_dbgNoBreak = true;
2311 RequestInjectionData& rid = RID();
2313 if (m_suppressOutput) {
2314 // Disable raising of PHP errors during this eval.
2315 m_errorLevel = rid.getErrorReportingLevel();
2316 rid.setErrorReportingLevel(0);
2318 // Disable all sorts of output during this eval.
2319 m_oldHook = debugger->getStdoutHook();
2320 m_savedOutputBuffer = g_context->swapOutputBuffer(&m_sb);
2321 g_context->removeStdoutHook(m_oldHook);
2322 g_context->addStdoutHook(&m_noOpHook);
2325 // Set aside the flow filters to disable all stepping and bp filtering.
2326 m_savedFlowFilter.swap(rid.m_flowFilter);
2327 m_savedBpFilter.swap(rid.m_breakPointFilter);
2330 SilentEvaluationContext::~SilentEvaluationContext() {
2331 m_ri->m_flags.doNotBreak = false;
2332 std::atomic_thread_fence(std::memory_order_release);
2334 g_context->m_dbgNoBreak = false;
2336 RequestInjectionData& rid = RID();
2338 if (m_suppressOutput) {
2339 rid.setErrorReportingLevel(m_errorLevel);
2340 g_context->swapOutputBuffer(m_savedOutputBuffer);
2341 g_context->removeStdoutHook(&m_noOpHook);
2342 g_context->addStdoutHook(m_oldHook);
2345 m_savedFlowFilter.swap(rid.m_flowFilter);
2346 m_savedBpFilter.swap(rid.m_breakPointFilter);
2347 m_sb.clear();