2 +----------------------------------------------------------------------+
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>
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
;
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
;
93 raise_error("getDebuggerOption: Unknown option specified");
96 setDebuggerOptions(options
);
99 void Debugger::setDebuggerOptions(DebuggerOptions options
) {
101 m_debuggerOptions
= options
;
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
;
129 std::unique_lock
<std::mutex
> lock(m_sessionCleanupLock
);
131 // Read m_sessionCleanupTerminating while the lock is held.
132 terminating
= m_sessionCleanupTerminating
;
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
144 sessionsToDelete
= m_cleanupSessions
;
145 m_cleanupSessions
.clear();
148 // Free the sessions.
149 for (DebuggerSession
* sessionToDelete
: sessionsToDelete
) {
150 delete sessionToDelete
;
160 void Debugger::setClientConnected(
162 bool synchronous
/* = false */,
163 ClientInfo
* clientInfo
/* = nullptr */
165 DebuggerSession
* sessionToDelete
= nullptr;
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.
173 delete sessionToDelete
;
175 std::unique_lock
<std::mutex
> lock(m_sessionCleanupLock
);
176 m_cleanupSessions
.insert(sessionToDelete
);
177 m_sessionCleanupCondition
.notify_all();
180 VSDebugLogger::LogFlush();
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
);
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
;
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) {
217 if (clientInfo
== nullptr) {
219 VSDebugLogger::LogLevelInfo
,
220 "Clearing client info. Connected client has no ID."
222 logger
->clearClientInfo();
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
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) {
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
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) {
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
299 std::unique_lock
<std::mutex
> lock(m_connectionNotifyLock
);
300 m_connectionNotifyCondition
.notify_all();
304 auto logger
= Eval::Debugger::GetUsageLogger();
305 if (logger
!= nullptr) {
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
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(),
362 void Debugger::setClientInitialized() {
364 if (m_clientInitialized
) {
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() {
384 if (!clientConnected() || getCurrentThreadId() != kDummyTheadId
) {
388 return m_session
->getDebuggerSessionAuth();
391 request_id_t
Debugger::getCurrentThreadId() {
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()) {
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);
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
;
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
) {
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) {
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);
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() {
541 folly::dynamic event
= folly::dynamic::object
;
542 sendEventMessage(event
, "terminated", true);
545 void Debugger::sendStoppedEvent(
547 const char* displayReason
,
548 request_id_t threadId
,
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.
563 event
["allThreadsStopped"] = allThreadsStopped
;
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";
580 event
["description"] = displayReason
;
583 event
["preserveFocusHint"] = !focusedThread
;
584 sendEventMessage(event
, "stopped");
587 void Debugger::sendContinuedEvent(request_id_t threadId
) {
589 folly::dynamic event
= folly::dynamic::object
;
590 event
["allThreadsContinued"] = m_pausedRequestCount
== 0;
592 event
["threadId"] = threadId
;
595 sendEventMessage(event
, "continued");
598 void Debugger::sendUserMessage(const char* message
, const char* level
) {
601 if (!clientConnected()) {
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 */
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();
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
);
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
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).
661 folly::dynamic event
= folly::dynamic::object
;
663 event
["reason"] = eventType
== ThreadEventType::ThreadStarted
667 event
["threadId"] = threadId
;
669 sendEventMessage(event
, "thread", true);
672 void Debugger::requestInit() {
675 m_totalRequestCount
++;
677 if (!clientConnected()) {
678 // Don't pay for attaching to the thread if no debugger client is connected.
682 RequestInfo
* const threadInfo
= &RI();
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.
691 requestInfo
= attachToRequest(threadInfo
);
692 pauseRequest
= m_state
!= ProgramState::Running
;
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) {
701 getCurrentThreadId(),
711 void Debugger::enterDebuggerIfPaused(DebuggerRequestInfo
* requestInfo
) {
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.
719 VSDebugLogger::LogLevelInfo
,
720 "Debugger client detached and we launched this script. "
721 "Killing request with fatal error."
725 "Request terminated due to debugger client detaching.",
733 if (m_state
!= ProgramState::Running
) {
734 if (requestInfo
->m_stepReason
!= nullptr) {
736 getCurrentThreadId(),
739 requestInfo
->m_stepReason
,
743 } else if (m_state
== ProgramState::AsyncPaused
) {
745 getCurrentThreadId(),
754 getCurrentThreadId(),
765 void Debugger::processCommandQueue(
766 request_id_t threadId
,
767 DebuggerRequestInfo
* requestInfo
,
769 const char* displayReason
,
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
;
787 sendStoppedEvent(reason
, displayReason
, threadId
, focusedThread
, bpId
);
791 VSDebugLogger::LogLevelInfo
,
796 // Drop the lock before entering the command queue and re-acquire it
797 // when leaving the command queue.
799 requestInfo
->m_commandQueue
.processCommands();
802 requestInfo
->m_pauseRecurseCount
--;
803 if (requestInfo
->m_pauseRecurseCount
== 0) {
804 m_pausedRequestCount
--;
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
);
822 VSDebugLogger::LogLevelInfo
,
827 m_resumeCondition
.notify_all();
830 DebuggerRequestInfo
* Debugger::createRequestInfo() {
831 DebuggerRequestInfo
* requestInfo
= new DebuggerRequestInfo();
832 if (requestInfo
== nullptr) {
833 // Failed to allocate request info.
835 VSDebugLogger::LogLevelError
,
836 "Failed to allocate request info!"
841 assertx(requestInfo
->m_allFlags
== 0);
843 requestInfo
->m_breakpointInfo
= new RequestBreakpointInfo();
844 if (requestInfo
->m_breakpointInfo
== nullptr) {
845 // Failed to allocate breakpoint info.
847 VSDebugLogger::LogLevelError
,
848 "Failed to allocate request breakpoint info!"
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.
867 while (m_requestIdMap
.find(threadId
) != m_requestIdMap
.end()) {
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.
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
));
897 VSDebugLogger::LogLevelInfo
,
898 "Created new request info for request %d (current thread=%d), flags=%u",
900 (int64_t)Process::GetThreadId(),
901 static_cast<unsigned int>(requestInfo
->m_allFlags
)
905 requestInfo
= it
->second
;
906 auto idIt
= m_requestInfoMap
.find(ti
);
907 assertx(idIt
!= m_requestInfoMap
.end());
908 threadId
= idIt
->second
;
911 VSDebugLogger::LogLevelInfo
,
912 "Found existing request info for thread %d, flags=%u",
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
);
942 m_transport
->enqueueOutgoingUserMessage(
944 "Failed to attach to new HHVM request: another debugger is already "
946 DebugTransport::OutputLevelError
950 VSDebugLogger::LogLevelError
,
951 "Failed to attach to new HHVM request: another debugger is already "
957 VSDebugLogger::LogLevelInfo
,
958 "Not attaching to request %d, a debug hook is already attached.",
963 updateUnresolvedBpFlag(requestInfo
);
967 void Debugger::requestShutdown() {
968 auto const threadInfo
= &RI();
969 DebuggerRequestInfo
* requestInfo
= nullptr;
970 request_id_t threadId
= -1;
973 if (clientConnected() && threadId
>= 0) {
974 sendThreadEventMessage(threadId
, ThreadEventType::ThreadExited
);
975 m_session
->getBreakpointManager()->onRequestShutdown(threadId
);
978 if (requestInfo
!= nullptr) {
979 cleanupRequestInfo(threadInfo
, requestInfo
);
985 auto it
= m_requests
.find(threadInfo
);
986 if (it
== m_requests
.end()) {
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()) {
1019 return m_session
->m_dummyRequestInfo
;
1022 DebuggerRequestInfo
* Debugger::getRequestInfo(request_id_t threadId
/* = -1 */) {
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
;
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()) {
1053 bool Debugger::executeClientCommand(
1055 std::function
<bool(DebuggerSession
* session
,
1056 folly::dynamic
& responseMsg
)> callback
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()) {
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
1072 folly::dynamic responseMsg
= folly::dynamic::object
;
1074 m_processingClientCommand
= true;
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());
1096 reportClientMessageError(command
->getMessage(), InternalErrorMsg
);
1099 // On error, do not resume the request thread.
1103 void Debugger::executeWithoutLock(std::function
<void()> callback
) {
1104 m_lock
.assertOwnedBySelf();
1110 m_lock
.assertOwnedBySelf();
1113 void Debugger::reportClientMessageError(
1114 folly::dynamic
& clientMsg
,
1115 const char* errorMessage
1119 VSDebugLogger::LogLevelError
,
1120 "Failed to process client message (%s): %s",
1121 folly::toJson(clientMsg
).c_str(),
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(
1133 DebugTransport::MessageTypeResponse
1140 VSDebugLogger::LogLevelError
,
1141 "Unexpected failure while trying to send response to client."
1146 void Debugger::sendCommandResponse(
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(
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(),
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.
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.
1239 m_resumeCondition
.wait(conditionLock
);
1240 conditionLock
.unlock();
1242 // And re-acquire it before continuing.
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
,
1270 m_lock
.assertOwnedBySelf();
1272 DebuggerRequestInfo
* ri
= nullptr;
1273 if (requestId
== kDummyTheadId
) {
1274 ri
= getDummyRequestInfo();
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.
1289 ri
->m_commandQueue
.dispatchCommand(command
);
1293 void Debugger::logClientCommand(
1296 auto logger
= Eval::Debugger::GetUsageLogger();
1297 if (logger
== nullptr) {
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") {
1314 folly::json::serialization_opts jsonOptions
;
1315 jsonOptions
.sort_keys
= true;
1316 jsonOptions
.pretty_formatting
= true;
1318 const std::string sandboxId
= g_context
.isNull()
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"
1326 logger
->log(mode
, sandboxId
, cmd
, data
);
1328 VSDebugLogger::LogLevelVerbose
,
1329 "Logging client command: %s: %s\n",
1330 cmd
.c_str(), data
.c_str()
1334 VSDebugLogger::LogLevelError
,
1335 "Error logging client command"
1340 void Debugger::onClientMessage(folly::dynamic
& message
) {
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()) {
1350 VSCommand
* command
= nullptr;
1352 if (command
!= nullptr) {
1359 // All valid client messages should have a sequence number and type.
1361 const auto& seq
= message
["seq"];
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);
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.
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();
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
);
1430 ri
->m_commandQueue
.dispatchCommand(command
);
1432 // Lifetime of command is now owned by the request thread's queue.
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.
1447 } catch (DebuggerCommandException
&e
) {
1448 reportClientMessageError(message
, e
.what());
1450 reportClientMessageError(message
, InternalErrorMsg
);
1454 void Debugger::waitForClientConnection() {
1455 std::unique_lock
<std::mutex
> lock(m_connectionNotifyLock
);
1456 if (clientConnected()) {
1460 while (!clientConnected()) {
1461 m_connectionNotifyCondition
.wait(lock
);
1465 void Debugger::setClientPreferences(ClientPreferences
& preferences
) {
1467 if (!clientConnected()) {
1471 assertx(m_session
!= nullptr);
1472 m_session
->setClientPreferences(preferences
);
1475 ClientPreferences
Debugger::getClientPreferences() {
1477 if (!clientConnected()) {
1478 ClientPreferences 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
1494 if (!clientConnected()) {
1498 assertx(m_session
!= nullptr);
1499 m_session
->startDummyRequest(
1503 debuggerSessionAuthToken
,
1508 void Debugger::setDummyThreadId(int64_t threadId
) {
1510 m_dummyThreadId
= threadId
;
1513 void Debugger::onBreakpointAdded(int bpId
) {
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
) {
1554 if (!clientConnected()) {
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) {
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 &&
1602 bp
->m_path
.rbegin(),
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.";
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
,
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!
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
);
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
))) {
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) {
1692 VSDebugLogger::LogLevelError
,
1693 "NOT installing bp ID %d in file %s. No source locations matching "
1694 " line %d were found.",
1696 unitFilePath
.c_str(),
1703 VSDebugLogger::LogLevelInfo
,
1704 "Installing bp ID %d at line %d (original line was %d) of file %s.",
1708 unitFilePath
.c_str()
1711 if (!phpAddBreakPointLine(compilationUnit
, lines
.first
)) {
1713 VSDebugLogger::LogLevelError
,
1714 "Installing %d at line %d of file %s FAILED in phpAddBreakPointLine!",
1717 unitFilePath
.c_str()
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()) + "::"
1741 functionName
= clsName
;
1742 functionName
+= func
->name()->data();
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
1756 bpMgr
->sendMemoizeWarning(bpId
);
1760 bpMgr
->onBreakpointResolved(
1773 std::pair
<int, int> Debugger::calibrateBreakpointLineInUnit(
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
)) {
1812 candidateLocations
.insert(sourceLocation
);
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
,
1830 const std::string
& funcName
1834 if (!clientConnected()) {
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
);
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
);
1858 // Still no match, move on to the next unresolved function breakpoint.
1863 updateUnresolvedBpFlag(ri
);
1866 void Debugger::onCompilationUnitLoaded(
1867 DebuggerRequestInfo
* ri
,
1868 const HPHP::Unit
* compilationUnit
1872 if (!clientConnected()) {
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
);
1885 VSDebugLogger::LogLevelInfo
,
1886 "Compilation unit for %s changed/reloaded in request %d. ",
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
);
1924 updateUnresolvedBpFlag(ri
);
1927 void Debugger::onFuncIntercepted(std::string funcName
) {
1930 if (!clientConnected()) {
1934 BreakpointManager
* bpMgr
= m_session
->getBreakpointManager();
1935 bpMgr
->onFuncIntercepted(getCurrentThreadId(), funcName
);
1938 void Debugger::onFuncBreakpointHit(
1939 DebuggerRequestInfo
* ri
,
1940 const HPHP::Func
* func
1944 if (func
!= nullptr) {
1945 onBreakpointHit(ri
, func
->unit(), func
, func
->line1());
1949 void Debugger::onLineBreakpointHit(
1950 DebuggerRequestInfo
* ri
,
1951 const HPHP::Unit
* compilationUnit
,
1955 onBreakpointHit(ri
, compilationUnit
, nullptr, line
);
1958 void Debugger::onBreakpointHit(
1959 DebuggerRequestInfo
* ri
,
1960 const HPHP::Unit
* compilationUnit
,
1961 const HPHP::Func
* func
,
1964 std::string stopReason
;
1965 const std::string filePath
= getFilePathForUnit(compilationUnit
);
1967 if (prepareToPauseTarget(ri
) != PrepareToPauseResult::ReadyToPause
) {
1969 VSDebugLogger::LogLevelError
,
1970 "onBreakpointHit: Not pausing target, the client disconnected."
1975 BreakpointManager
* bpMgr
= m_session
->getBreakpointManager();
1977 const auto removeBreakpoint
=
1978 [&](BreakpointType type
) {
1979 if (type
== BreakpointType::Source
) {
1980 phpRemoveBreakPointLine(compilationUnit
, line
);
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) {
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(
2009 pauseTarget(ri
, false);
2010 bpMgr
->onBreakpointHit(bpId
);
2012 processCommandQueue(
2013 getCurrentThreadId(),
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
) {
2055 removeBreakpoint(BreakpointType::Source
);
2058 pauseTarget(ri
, false);
2059 processCommandQueue(
2060 getCurrentThreadId(),
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());
2082 userMsg
+= stopReason
+ ": ";
2083 userMsg
+= exceptionMsg
;
2087 if (!clientConnected()) {
2091 BreakpointManager
* bpMgr
= m_session
->getBreakpointManager();
2092 ExceptionBreakMode breakMode
= bpMgr
->getExceptionBreakMode();
2094 if (breakMode
== BreakNone
) {
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.
2107 assertx(breakMode
== BreakAll
);
2109 if (prepareToPauseTarget(ri
) != PrepareToPauseResult::ReadyToPause
) {
2113 pauseTarget(ri
, false);
2114 processCommandQueue(
2115 getCurrentThreadId(),
2124 bool Debugger::onHardBreak() {
2125 static constexpr char* stopReason
= "hphp_debug_break()";
2128 VMRegAnchor regAnchor
;
2129 DebuggerRequestInfo
* ri
= getRequestInfo();
2131 if (ri
== nullptr) {
2135 if (!Debugger::isStepInProgress(ri
)) {
2136 enterDebuggerIfPaused(ri
);
2139 if (g_context
->m_dbgNoBreak
|| ri
->m_flags
.doNotBreak
) {
2143 if (!clientConnected() ||
2144 prepareToPauseTarget(ri
) != PrepareToPauseResult::ReadyToPause
) {
2149 pauseTarget(ri
, false);
2150 processCommandQueue(
2151 getCurrentThreadId(),
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
2162 phpDebuggerStepOut();
2167 void Debugger::onAsyncBreak() {
2170 if (m_state
== ProgramState::Paused
|| m_state
== ProgramState::AsyncPaused
) {
2175 if (prepareToPauseTarget(nullptr) != PrepareToPauseResult::ReadyToPause
) {
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";
2204 case ErrorMode::RECOVERABLE_ERROR
:
2205 phpError
= "Catchable fatal error";
2207 case ErrorMode::WARNING
:
2208 case ErrorMode::CORE_WARNING
:
2209 case ErrorMode::COMPILE_WARNING
:
2210 case ErrorMode::USER_WARNING
:
2211 phpError
= "Warning";
2213 case ErrorMode::PARSE
:
2214 phpError
= "Parse error";
2216 case ErrorMode::NOTICE
:
2217 case ErrorMode::USER_NOTICE
:
2218 phpError
= "Notice";
2220 case ErrorMode::STRICT
:
2221 phpError
= "Strict standards";
2223 case ErrorMode::PHP_DEPRECATED
:
2224 case ErrorMode::USER_DEPRECATED
:
2225 phpError
= "Deprecated";
2228 phpError
= "Unknown error";
2232 onExceptionBreakpointHit(requestInfo
, phpError
, message
);
2235 std::string
Debugger::getFilePathForUnit(const HPHP::Unit
* compilationUnit
) {
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
,
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
;
2253 description
+= std::to_string(line
);
2257 if (bp
->m_type
== BreakpointType::Function
) {
2258 description
+= " - " + bp
->m_functionFullName
+ "()";
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
) {
2276 write(fileno(stdout
), str
, len
);
2278 // Quickly no-op if there's no client.
2279 if (!m_debugger
->clientConnected()) {
2283 std::string output
= std::string(str
, len
);
2284 m_debugger
->sendUserMessage(
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()) {
2297 m_debugger
->sendUserMessage(msg
, DebugTransport::OutputLevelStderr
);
2300 SilentEvaluationContext::SilentEvaluationContext(
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
);