BrowserLocalChrome.prototype.startWdServer = function(browserCaps) { 'use strict'; var requestedBrowserName = browserCaps[webdriver.Capability.BROWSER_NAME]; if (webdriver.Browser.CHROME !== requestedBrowserName) { throw new Error('BrowserLocalChrome called with unexpected browser ' + requestedBrowserName); } if (!this.chromedriver_) { throw new Error('Must set chromedriver before starting it'); } var serverCommand = this.chromedriver_; var serverArgs = ['--port=' + this.serverPort_]; if (logger.isLogging('extra')) { serverArgs.push('--verbose'); } browserCaps.chromeOptions = {args: CHROME_FLAGS.slice()}; if (this.chrome_) { browserCaps.chromeOptions.binary = this.chrome_; } this.startChildProcess(serverCommand, serverArgs, 'WD server'); // Make sure we set serverUrl_ only after the child process start success. this.app_.schedule('Set WD server URL', function() { this.serverUrl_ = 'http://localhost:' + this.serverPort_; }.bind(this)); };
/** * Adds "logger.extra" level logging to all process scheduler tasks. * * @param {string=} appName log message prefix. * @param {webdriver.promise.ControlFlow} app The app to inject logging into. */ function injectWdAppLogging(appName, app) { 'use strict'; if (!app.isLoggingInjected) { var realExecute = app.execute; if (logger.isLogging('extra')) { var realGetNextTask = app.getNextTask_; app.getNextTask_ = function() { var task = realGetNextTask.apply(app, arguments); if (task) { logger.extra('(%s) %s', appName, task.getDescription()); } else { logger.extra('(%s) no next task', appName); } return task; }; app.execute = function(fn, opt_description) { logger.extra('(%s) %s', appName, opt_description || 'function ' + (fn.name || '<unnamed>')); return realExecute.apply(app, arguments); }; } // TODO(klm): Migrate all code to execute(). // Monkey-patching the old schedule(desc,fn) API just for gradual migration. app.schedule = function(description, fn) { logger.extra('(%s) %s', appName, description); return realExecute.call(app, fn, description); }; app.isLoggingInjected = true; } }
WebDriverServer.prototype.onDevToolsMessage_ = function(message) { 'use strict'; if (this.driver_) { throw new Error('Internal error: DevTools callback called with WebDriver'); } var level = DEVTOOLS_METHOD_TO_LEVEL[message.method] || 'debug'; if (logger.isLogging(level)) { logger[level].apply(undefined, ['%sDevTools message: %s', (this.isRecordingDevTools_ ? '' : '#'), JSON.stringify(message)]); } if (message.method && 0 === message.method.indexOf('Tracing.')) { this.onTracingMessage_(message); return; // Don't clutter the DevTools log with tracing events. } if (this.isRecordingDevTools_) { this.devToolsMessages_.push(message); } // If abortTimer_ is set, it means we received an 'Inspector.detached' // message, as noted below, so ignore messages until our abortTimer fires. if (!this.abortTimer_) { if ('Page.loadEventFired' === message.method) { if (this.isRecordingDevTools_) { this.onPageLoad_(); } } else if ('Inspector.detached' === message.method) { if (this.pageLoadDonePromise_ && this.pageLoadDonePromise_.isPending()) { // This message typically means that the browser has crashed. // Instead of waiting for the timeout, we'll give the browser a couple // seconds to paint an error message (for our screenshot) and then fail // the page load. var err = new Error('Inspector detached on run ' + this.runNumber_ + ', did the browser crash?', this.runNumber_); this.abortTimer_ = global.setTimeout( this.onPageLoad_.bind(this, err), DETACH_TIMEOUT_MS_); } else { // TODO detach during coalesce? logger.warn('%s after Page.loadEventFired?', message.method); } } // We might be able to detect timeouts via Network.loadingFailed and // Page.frameStoppedLoading messages. For now we'll let our timeoutTimer // handle this. } };
this.scheduleNeedsXvfb_().then(function(useXvfb) { var cmd = this.chromedriver_; var args = ['-port=' + this.serverPort_]; if (logger.isLogging('extra')) { args.push('--verbose'); } if (useXvfb) { // Use a fake X display, otherwise a scripted "sendKeys" fails with: // an X display is required for keycode conversions, consider using Xvfb // TODO(wrightt) submit a crbug; Android shouldn't use the host's keymap! args.splice(0, 0, '-a', cmd); cmd = 'xvfb-run'; } this.startChildProcess(cmd, args, 'WD server'); // Make sure we set serverUrl_ only after the child process start success. this.app_.schedule('Set DevTools URL', function() { this.serverUrl_ = 'http://localhost:' + this.serverPort_; }.bind(this)); }.bind(this));
Client.prototype.postResultFile_ = function(job, resultFile, fields, callback) { 'use strict'; logger.extra('postResultFile: job=%s resultFile=%s fields=%j callback=%s', job.id, (resultFile ? 'present' : null), fields, callback); var servlet = WORK_DONE_SERVLET; var mp = new multipart.Multipart(); mp.addPart('id', job.id, ['Content-Type: text/plain']); mp.addPart('location', this.location_); if (this.apiKey) { mp.addPart('key', this.apiKey); } if (fields) { fields.forEach(function(nameValue) { mp.addPart(nameValue[0], nameValue[1]); }); } if (resultFile) { // A part with name="resultType" and then the content with name="file" if (exports.ResultFile.ResultType.IMAGE === resultFile.resultType) { // Images go to a different servlet and don't need the resultType part servlet = RESULT_IMAGE_SERVLET; } else { if (resultFile.resultType) { mp.addPart(resultFile.resultType, '1'); } mp.addPart('_runNumber', String(job.runNumber)); mp.addPart('_cacheWarmed', job.isCacheWarm ? '1' : '0'); } var fileName = job.runNumber + (job.isCacheWarm ? '_Cached_' : '_') + resultFile.fileName; mp.addFilePart( 'file', fileName, resultFile.contentType, resultFile.content); if (logger.isLogging('debug')) { logger.debug('Writing a local copy of %s', fileName); var body = resultFile.content; var bodyBuffer = (body instanceof Buffer ? body : new Buffer(body)); fs.mkdir('results/', parseInt('0755', 8), function(e) { if (!e || 'EEXIST' === e.code) { var subdir = 'results/' + job.id + '/'; fs.mkdir(subdir, parseInt('0755', 8), function(e) { if (!e || 'EEXIST' === e.code) { fs.writeFile(subdir + fileName, bodyBuffer); } }); } }); } } // TODO(klm): change body to chunked request.write(). // Only makes sense if done for file content, the rest is peanuts. var mpResponse = mp.getHeadersAndBody(); var options = { method: 'POST', host: this.baseUrl_.hostname, port: this.baseUrl_.port, path: path.join(this.baseUrl_.path, servlet), headers: mpResponse.headers }; var request = http.request(options, function(res) { exports.processResponse(res, callback); }); request.on('error', function(e) { logger.warn('Unable to post result: ' + e.message); if (callback) { callback(e, ''); } }); request.end(mpResponse.bodyBuffer, 'UTF-8'); };
Client.prototype.postResultFile_ = function(job, resultFile, fields, callback) { 'use strict'; logger.extra('postResultFile: job=%s resultFile=%s fields=%j callback=%s', job.id, (resultFile ? 'present' : null), fields, callback); var servlet = WORK_DONE_SERVLET; var mp = new multipart.Multipart(); mp.addPart('id', job.id, ['Content-Type: text/plain']); mp.addPart('location', this.location_); if (this.apiKey_) { mp.addPart('key', this.apiKey_); } if (this.name_) { mp.addPart('pc', this.name_); } else if (this.deviceSerial_) { mp.addPart('pc', this.deviceSerial_); } if (fields) { fields.forEach(function(nameValue) { mp.addPart(nameValue[0], nameValue[1]); }); } if (resultFile) { if (exports.ResultFile.ResultType.IMAGE === resultFile.resultType || exports.ResultFile.ResultType.PCAP === resultFile.resultType) { // Images and pcaps must be uploaded to the RESULT_IMAGE_SERVLET, with no // resultType or run/cache parts. // // If we submit the pcap via the regular servlet, it would be used for // the waterfall instead of the DevTools trace, which we don't want. servlet = RESULT_IMAGE_SERVLET; } else { if (resultFile.resultType) { mp.addPart(resultFile.resultType, '1'); } mp.addPart('_runNumber', String(job.runNumber)); mp.addPart('_cacheWarmed', job.isCacheWarm ? '1' : '0'); } var fileName = createFileName(job, resultFile.fileName); mp.addFilePart( 'file', fileName, resultFile.contentType, resultFile.content); if (logger.isLogging('debug')) { logger.debug('Writing a local copy of %s', fileName); var body = resultFile.content; var bodyBuffer = (body instanceof Buffer ? body : new Buffer(body)); fs.mkdir('results', parseInt('0755', 8), function(e) { if (!e || 'EEXIST' === e.code) { var subdir = path.join('results', job.id); fs.mkdir(subdir, parseInt('0755', 8), function(e) { if (!e || 'EEXIST' === e.code) { fs.writeFile(path.join(subdir, fileName), bodyBuffer); } }); } }); } } // TODO(klm): change body to chunked request.write(). // Only makes sense if done for file content, the rest is peanuts. var mpResponse = mp.getHeadersAndBody(); var options = { method: 'POST', host: this.baseUrl_.hostname, port: this.baseUrl_.port, path: this.baseUrl_.path.replace(/\/+$/, '') + '/' + servlet, headers: mpResponse.headers }; var request = http.request(options, function(res) { exports.processResponse(res, callback); }); request.on('error', function(e) { logger.warn('Unable to post result: ' + e.message); if (callback) { callback(e, ''); } }); request.end(mpResponse.bodyBuffer, 'UTF-8'); };