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
22static DWORD WINAPI ProcessPageTimeRequests(LPVOID pThis) {
23  reinterpret_cast<CMeasurePageLoadTimeBHO*>(pThis)->ProcessPageTimeRequests();
24
25  return 0;
26}
27
28
29STDMETHODIMP 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
75void 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
86void 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
273void 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