1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 // Implements a Browser Helper Object (BHO) which opens a socket 6 // and waits to receive URLs over it. Visits those URLs, measuring 7 // how long it takes between the start of navigation and the 8 // DocumentComplete event, and returns the time in milliseconds as 9 // a string to the caller. 10 11 #include "stdafx.h" 12 #include "MeasurePageLoadTimeBHO.h" 13 14 #define MAX_URL 1024 // size of URL buffer 15 #define MAX_PAGELOADTIME (4*60*1000) // assume all pages take < 4 minutes 16 #define PORT 42492 // port to listen on. Also jhaas's 17 // old MSFT employee number 18 19 20 // Static function to serve as thread entry point, takes a "this" 21 // pointer as pParam and calls the method in the object 22 static DWORD WINAPI ProcessPageTimeRequests(LPVOID pThis) { 23 reinterpret_cast<CMeasurePageLoadTimeBHO*>(pThis)->ProcessPageTimeRequests(); 24 25 return 0; 26 } 27 28 29 STDMETHODIMP CMeasurePageLoadTimeBHO::SetSite(IUnknown* pUnkSite) 30 { 31 if (pUnkSite != NULL) 32 { 33 // Cache the pointer to IWebBrowser2. 34 HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void **)&m_spWebBrowser); 35 if (SUCCEEDED(hr)) 36 { 37 // Register to sink events from DWebBrowserEvents2. 38 hr = DispEventAdvise(m_spWebBrowser); 39 if (SUCCEEDED(hr)) 40 { 41 m_fAdvised = TRUE; 42 } 43 44 // Stash the interface in the global interface table 45 CComGITPtr<IWebBrowser2> git(m_spWebBrowser); 46 m_dwCookie = git.Detach(); 47 48 // Create the event to be signaled when navigation completes. 49 // Start it in nonsignaled state, and allow it to be triggered 50 // when the initial page load is done. 51 m_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 52 53 // Create a thread to wait on the socket 54 HANDLE hThread = CreateThread(NULL, 0, ::ProcessPageTimeRequests, this, 0, NULL); 55 } 56 } 57 else 58 { 59 // Unregister event sink. 60 if (m_fAdvised) 61 { 62 DispEventUnadvise(m_spWebBrowser); 63 m_fAdvised = FALSE; 64 } 65 66 // Release cached pointers and other resources here. 67 m_spWebBrowser.Release(); 68 } 69 70 // Call base class implementation. 71 return IObjectWithSiteImpl<CMeasurePageLoadTimeBHO>::SetSite(pUnkSite); 72 } 73 74 75 void STDMETHODCALLTYPE CMeasurePageLoadTimeBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL) 76 { 77 if (pDisp == m_spWebBrowser) 78 { 79 // Fire the event when the page is done loading 80 // to unblock the other thread. 81 SetEvent(m_hEvent); 82 } 83 } 84 85 86 void CMeasurePageLoadTimeBHO::ProcessPageTimeRequests() 87 { 88 CoInitialize(NULL); 89 90 // The event will start in nonsignaled state, meaning that 91 // the initial page load isn't done yet. Wait for that to 92 // finish before doing anything. 93 // 94 // It seems to be the case that the BHO will get loaded 95 // and SetSite called always before the initial page load 96 // even begins, but just to be on the safe side, we won't 97 // wait indefinitely. 98 WaitForSingleObject(m_hEvent, MAX_PAGELOADTIME); 99 100 // Retrieve the web browser interface from the global table 101 CComGITPtr<IWebBrowser2> git(m_dwCookie); 102 IWebBrowser2* browser; 103 git.CopyTo(&browser); 104 105 // Create a listening socket 106 m_sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 107 if (m_sockListen == SOCKET_ERROR) 108 ErrorExit(); 109 110 BOOL on = TRUE; 111 if (setsockopt(m_sockListen, SOL_SOCKET, SO_REUSEADDR, 112 (const char*)&on, sizeof(on))) 113 ErrorExit(); 114 115 // Bind the listening socket 116 SOCKADDR_IN addrBind; 117 118 addrBind.sin_family = AF_INET; 119 addrBind.sin_addr.s_addr = htonl(INADDR_LOOPBACK); 120 addrBind.sin_port = htons(PORT); 121 122 if (bind(m_sockListen, (sockaddr*)&addrBind, sizeof(addrBind))) 123 ErrorExit(); 124 125 // Listen for incoming connections 126 if (listen(m_sockListen, 1)) 127 ErrorExit(); 128 129 // Ensure the socket is blocking... it should be by default, but 130 // it can't hurt to make sure 131 unsigned long nNonblocking = 0; 132 if (ioctlsocket(m_sockListen, FIONBIO, &nNonblocking)) 133 ErrorExit(); 134 135 m_sockTransport = 0; 136 137 // Loop indefinitely waiting for connections 138 while(1) 139 { 140 SOCKADDR_IN addrConnected; 141 int sConnected = sizeof(addrConnected); 142 143 // Wait for a client to connect and send a URL 144 m_sockTransport = accept( 145 m_sockListen, (sockaddr*)&addrConnected, &sConnected); 146 147 if (m_sockTransport == SOCKET_ERROR) 148 ErrorExit(); 149 150 char pbBuffer[MAX_URL], strURL[MAX_URL]; 151 DWORD cbRead, cbWritten; 152 153 bool fDone = false; 154 155 // Loop until we're done with this client 156 while (!fDone) 157 { 158 *strURL = '\0'; 159 bool fReceivedCR = false; 160 161 do 162 { 163 // Only receive up to the first carriage return 164 cbRead = recv(m_sockTransport, pbBuffer, MAX_URL-1, MSG_PEEK); 165 166 // An error on read most likely means that the remote peer 167 // closed the connection. Go back to waiting 168 if (cbRead == 0) 169 { 170 fDone = true; 171 break; 172 } 173 174 // Null terminate the received characters so strchr() is safe 175 pbBuffer[cbRead] = '\0'; 176 177 if(char* pchFirstCR = strchr(pbBuffer, '\n')) 178 { 179 cbRead = (DWORD)(pchFirstCR - pbBuffer + 1); 180 fReceivedCR = true; 181 } 182 183 // The below call will not block, since we determined with 184 // MSG_PEEK that at least cbRead bytes are in the TCP receive buffer 185 recv(m_sockTransport, pbBuffer, cbRead, 0); 186 pbBuffer[cbRead] = '\0'; 187 188 strcat_s(strURL, sizeof(strURL), pbBuffer); 189 } while (!fReceivedCR); 190 191 // If an error occurred while reading, exit this loop 192 if (fDone) 193 break; 194 195 // Strip the trailing CR and/or LF 196 int i; 197 for (i = (int)strlen(strURL)-1; i >= 0 && isspace(strURL[i]); i--) 198 { 199 strURL[i] = '\0'; 200 } 201 202 if (i < 0) 203 { 204 // Sending a carriage return on a line by itself means that 205 // the client is done making requests 206 fDone = true; 207 } 208 else 209 { 210 // Send the browser to the requested URL 211 CComVariant vNavFlags( navNoReadFromCache ); 212 CComVariant vTargetFrame("_self"); 213 CComVariant vPostData(""); 214 CComVariant vHTTPHeaders(""); 215 216 ResetEvent(m_hEvent); 217 DWORD dwStartTime = GetTickCount(); 218 219 HRESULT hr = browser->Navigate( 220 CComBSTR(strURL), 221 &vNavFlags, 222 &vTargetFrame, // TargetFrameName 223 &vPostData, // PostData 224 &vHTTPHeaders // Headers 225 ); 226 227 // The main browser thread will call OnDocumentComplete() when 228 // the page is done loading, which will in turn trigger 229 // m_hEvent. Wait here until then; the event will reset itself 230 // once this thread is released 231 if (WaitForSingleObject(m_hEvent, MAX_PAGELOADTIME) == WAIT_TIMEOUT) 232 { 233 sprintf_s(pbBuffer, sizeof(pbBuffer), "%s,timeout\n", strURL); 234 235 browser->Stop(); 236 } 237 else 238 { 239 // Format the elapsed time as a string 240 DWORD dwLoadTime = GetTickCount() - dwStartTime; 241 sprintf_s( 242 pbBuffer, sizeof(pbBuffer), "%s,%d\n", strURL, dwLoadTime); 243 } 244 245 // Send the result. Just in case the TCP buffer can't handle 246 // the whole thing, send in parts if necessary 247 char *chSend = pbBuffer; 248 249 while (*chSend) 250 { 251 cbWritten = send( 252 m_sockTransport, chSend, (int)strlen(chSend), 0); 253 254 // Error on send probably means connection reset by peer 255 if (cbWritten == 0) 256 { 257 fDone = true; 258 break; 259 } 260 261 chSend += cbWritten; 262 } 263 } 264 } 265 266 // Close the transport socket and wait for another connection 267 closesocket(m_sockTransport); 268 m_sockTransport = 0; 269 } 270 } 271 272 273 void CMeasurePageLoadTimeBHO::ErrorExit() 274 { 275 // Unlink from IE, close the sockets, then terminate this 276 // thread 277 SetSite(NULL); 278 279 if (m_sockTransport && m_sockTransport != SOCKET_ERROR) 280 { 281 closesocket(m_sockTransport); 282 m_sockTransport = 0; 283 } 284 285 if (m_sockListen && m_sockListen != SOCKET_ERROR) 286 { 287 closesocket(m_sockListen); 288 m_sockListen = 0; 289 } 290 291 TerminateThread(GetCurrentThread(), -1); 292 } 293