browser-driver.ts 12.2 KB
Newer Older
1
/**
2
 * Wechaty - Wechat for Bot. Connecting ChatBots
3 4 5 6 7 8 9 10
 *
 * BrowserDriver
 *
 * Licenst: ISC
 * https://github.com/wechaty/wechaty
 *
 */
import {
11 12
  Builder,
  Capabilities,
13
  logging,
14
  WebDriver,
15 16 17
}               from 'selenium-webdriver'

import {
18 19 20
  Config,
  HeadName,
  log,
21 22 23 24 25 26 27 28 29
}               from '../config'

export class BrowserDriver {
  private driver: WebDriver

  constructor(private head: HeadName) {
    log.verbose('PuppetWebBrowserDriver', 'constructor(%s)', head)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
30
  public async init(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
31
    log.verbose('PuppetWebBrowserDriver', 'init() for head: %s', this.head)
32

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
33 34
    switch (this.head) {
      case 'phantomjs':
35
        this.driver = await this.getPhantomJsDriver()
36 37
        break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
38
      case 'firefox':
39 40 41 42 43 44
        this.driver = new Builder()
                            .setAlertBehavior('ignore')
                            .forBrowser('firefox')
                            .build()
        break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
45
      case 'chrome':
46
        this.driver = await this.getChromeDriver()
47 48 49
        break

      default: // unsupported browser head
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
50
        throw new Error('unsupported head: ' + this.head)
51 52
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
53 54 55
    await this.driver.manage()
                      .timeouts()
                      .setScriptTimeout(10000)
56

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
    return
58 59
  }

60 61 62
  public getWebDriver(): WebDriver {
    return this.driver
  }
63 64

  private async getChromeDriver(): Promise<WebDriver> {
65
    log.verbose('PuppetWebBrowserDriver', 'initChromeDriver()')
66 67 68 69 70 71

    /**
     * http://stackoverflow.com/a/27733960/1123955
     * issue #56
     * only need under win32 with cygwin
     * and will cause strange error:
72 73
     *
     */
74 75

    /*
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
76 77
    const chrome  = require('selenium-webdriver/chrome')
    const path    = require('chromedriver').path
78 79

    const service = new chrome.ServiceBuilder(path).build()
80 81 82
    try {
      chrome.setDefaultService(service)
    } catch (e) { // fail safe
83 84
       // `The previously configured ChromeDriver service is still running.`
       // `You must shut it down before you may adjust its configuration.`
85
    }
86
   */
87 88

    const options = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89
      args: [
L
lijiarui 已提交
90 91 92
        '--homepage=about:blank',
        '--no-sandbox',
      ],  // issue #26 for run inside docker
93 94
    }
    if (Config.isDocker) {
95
      log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() wechaty in docker confirmed(should not show this in CI)')
96
      options['binary'] = Config.CMD_CHROMIUM
97 98 99 100 101 102 103 104
    } else {
      /**
       * https://github.com/Chatie/wechaty/pull/416
       * In some circumstances, ChromeDriver could not be found on the current PATH when not in Docker.
       * The chromedriver package always adds directory of chrome driver binary to PATH.
       * So we requires chromedriver here to avoid PATH issue.
       */
      require('chromedriver')
105 106 107 108 109
    }

    const customChrome = Capabilities.chrome()
                                    .set('chromeOptions', options)

110 111 112 113 114 115 116 117 118 119 120 121 122
    // TODO: chromedriver --silent
    if (!/^(verbose|silly)$/i.test(log.level())) {
      const prefs = new logging.Preferences()

      prefs.setLevel(logging.Type.BROWSER     , logging.Level.OFF)
      prefs.setLevel(logging.Type.CLIENT      , logging.Level.OFF)
      prefs.setLevel(logging.Type.DRIVER      , logging.Level.OFF)
      prefs.setLevel(logging.Type.PERFORMANCE , logging.Level.OFF)
      prefs.setLevel(logging.Type.SERVER      , logging.Level.OFF)

      customChrome.setLoggingPrefs(prefs)
    }

123 124 125
    /**
     * XXX when will Builder().build() throw exception???
     */
126
    let ttl = 3
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127
    let driverError = new Error('initChromeDriver() invalid driver error')
128
    let valid = false
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
129

130 131 132 133
    let driver: WebDriver

    while (ttl--) {
      log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() ttl: %d', ttl)
134

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
135
      try {
136
        log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() new Builder()')
137

138
        driver = new Builder()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
139 140 141 142
                      .setAlertBehavior('ignore')
                      .forBrowser('chrome')
                      .withCapabilities(customChrome)
                      .build()
143

144
        log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() new Builder() done')
145

146
        valid = await this.valid(driver)
147
        log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() valid() done: %s', valid)
148

149 150 151 152 153 154
        if (valid) {
          log.silly('PuppetWebBrowserDriver', 'initChromeDriver() success')

          return driver

        } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155 156 157
          const e = new Error('initChromeDriver() got invalid driver')
          log.warn('PuppetWebBrowserDriver', e.message)
          driverError = e
158 159
        }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
      } catch (e) {
161 162
        if (/could not be found/.test(e.message)) {
          // The ChromeDriver could not be found on the current PATH
163
          log.error('PuppetWebBrowserDriver', 'initChromeDriver() Wechaty require `chromedriver` to be installed.(try to run: "npm install chromedriver" to fix this issue)')
164 165
          throw e
        }
166
        log.warn('PuppetWebBrowserDriver', 'initChromeDriver() ttl:%d exception: %s, ttl: %d', ttl, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167
        driverError = e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
169

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170 171
    }

172 173 174
    log.warn('PuppetWebBrowserDriver', 'initChromeDriver() not valid with ttl expired: %s', driverError.stack)
    throw driverError

175 176
  }

177
  private async getPhantomJsDriver(): Promise<WebDriver> {
178 179 180 181 182 183 184 185
    // setup custom phantomJS capability https://github.com/SeleniumHQ/selenium/issues/2069
    const phantomjsExe = require('phantomjs-prebuilt').path
    if (!phantomjsExe) {
      throw new Error('phantomjs binary path not found')
    }
    // const phantomjsExe = require('phantomjs2').path

    const phantomjsArgs = [
L
lijiarui 已提交
186 187 188 189
      '--load-images=false',
      '--ignore-ssl-errors=true',  // this help socket.io connect with localhost
      '--web-security=false',      // https://github.com/ariya/phantomjs/issues/12440#issuecomment-52155299
      '--ssl-protocol=any',        // http://stackoverflow.com/a/26503588/1123955
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
      // , '--ssl-protocol=TLSv1'    // https://github.com/ariya/phantomjs/issues/11239#issuecomment-42362211

      // issue: Secure WebSocket(wss) do not work with Self Signed Certificate in PhantomJS #12
      // , '--ssl-certificates-path=D:\\cygwin64\\home\\zixia\\git\\wechaty' // http://stackoverflow.com/a/32690349/1123955
      // , '--ssl-client-certificate-file=cert.pem' //
    ]

    if (Config.debug) {
      phantomjsArgs.push('--remote-debugger-port=8080') // XXX: be careful when in production env.
      phantomjsArgs.push('--webdriver-loglevel=DEBUG')
      // phantomjsArgs.push('--webdriver-logfile=webdriver.debug.log')
    } else {
      if (log && log.level() === 'silent') {
        phantomjsArgs.push('--webdriver-loglevel=NONE')
      } else {
        phantomjsArgs.push('--webdriver-loglevel=ERROR')
      }
    }

    const customPhantom = Capabilities.phantomjs()
                                      .setAlertBehavior('ignore')
                                      .set('phantomjs.binary.path', phantomjsExe)
                                      .set('phantomjs.cli.args', phantomjsArgs)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
214 215
    log.silly('PuppetWebBrowserDriver', 'phantomjs binary: ' + phantomjsExe)
    log.silly('PuppetWebBrowserDriver', 'phantomjs args: ' + phantomjsArgs.join(' '))
216 217 218 219 220

    const driver = new Builder()
                        .withCapabilities(customPhantom)
                        .build()

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
221
    // const valid = await this.valid(driver)
222

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223 224 225
    // if (!valid) {
    //   throw new Error('invalid driver founded')
    // }
226

227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
    /* tslint:disable:jsdoc-format */
		/**
		 *  FIXME: ISSUE #21 - https://github.com/zixia/wechaty/issues/21
	 	 *
 	 	 *	http://phantomjs.org/api/webpage/handler/on-resource-requested.html
		 *	http://stackoverflow.com/a/29544970/1123955
		 *  https://github.com/geeeeeeeeek/electronic-wechat/pull/319
		 *
		 */
    //   	driver.executePhantomJS(`
    // this.onResourceRequested = function(request, net) {
    //    console.log('REQUEST ' + request.url);
    //    blockRe = /wx\.qq\.com\/\?t=v2\/fake/i
    //    if (blockRe.test(request.url)) {
    //        console.log('Abort ' + request.url);
    //        net.abort();
    //    }
    // }
    // `)

    // https://github.com/detro/ghostdriver/blob/f976007a431e634a3ca981eea743a2686ebed38e/src/session.js#L233
    // driver.manage().timeouts().pageLoadTimeout(2000)

    return driver
  }

253
  private async valid(driver: WebDriver): Promise<boolean> {
254
    log.verbose('PuppetWebBrowserDriver', 'valid()')
255

256 257
    try {
      const session = await new Promise((resolve, reject) => {
258 259 260 261 262 263 264 265

        /**
         * Be careful about this TIMEOUT, the total time(TIMEOUT x retry) should not trigger Watchdog Reset
         * because we are in state(open, false) state, which will cause Watchdog Reset failure.
         * https://travis-ci.org/wechaty/wechaty/jobs/179022657#L3246
         */
        const TIMEOUT = 7 * 1000

266
        let watchdogTimer: NodeJS.Timer | null
267

268
        watchdogTimer = setTimeout(() => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
269
          const e = new Error('valid() driver.getSession() timeout(halt?)')
Huan (李卓桓)'s avatar
#122  
Huan (李卓桓) 已提交
270
          log.warn('PuppetWebBrowserDriver', e.message)
271

272
          // record timeout by set timer to null
273
          watchdogTimer = null
Huan (李卓桓)'s avatar
#122  
Huan (李卓桓) 已提交
274
          log.verbose('PuppetWebBrowserDriver', 'watchdogTimer = %s after set null', watchdogTimer)
275

276
          // 1. Promise rejected
277 278
          reject(e)
          return
279

280
        }, TIMEOUT)
281

282
        log.verbose('PuppetWebBrowserDriver', 'valid() getSession()')
283
        driver.getSession()
284
              .then(driverSession => {
285 286 287 288 289
                log.verbose('PuppetWebBrowserDriver', 'valid() getSession() then() done')
                if (watchdogTimer) {
                  log.verbose('PuppetWebBrowserDriver', 'valid() getSession() then() watchdog timer exist, will be cleared')
                  clearTimeout(watchdogTimer)
                  watchdogTimer = null
Huan (李卓桓)'s avatar
#122  
Huan (李卓桓) 已提交
290
                  log.verbose('PuppetWebBrowserDriver', 'watchdogTimer = %s after set null', watchdogTimer)
291 292
                } else {
                  log.verbose('PuppetWebBrowserDriver', 'valid() getSession() then() watchdog timer not exist?')
293
                }
294 295

                // 2. Promise resolved
296
                resolve(driverSession)
297
                return
298

299
              })
300
              .catch(e => {
301
                log.warn('PuppetWebBrowserDriver', 'valid() getSession() catch() rejected: %s', e && e.message || e)
302

303 304 305 306 307 308 309 310 311 312 313
                // do not call reject again if there's already a timeout
                if (watchdogTimer) {
                  log.verbose('PuppetWebBrowserDriver', 'valid() getSession() catch() watchdog timer exist, will set it to null and call reject()')

                  // 3. Promise rejected
                  watchdogTimer = null
                  reject(e)
                  return

                } else {
                  log.verbose('PuppetWebBrowserDriver', 'valid() getSession() catch() watchdog timer not exist, will not call reject() again')
314
                }
315

316
              })
317

318 319
      })

320
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.getSession() done()')
321 322 323 324 325

      if (!session) {
        log.verbose('PuppetWebBrowserDriver', 'valid() found an invalid driver')
        return false
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
326

327 328
    } catch (e) {
      log.warn('PuppetWebBrowserDriver', 'valid() driver.getSession() exception: %s', e.message)
329 330 331 332 333
      return false
    }

    let two
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
334
      two = await driver.executeScript('return 1+1')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
335
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.executeScript() done')
336 337 338 339 340 341
    } catch (e) {
      two = e
      log.warn('BrowserDriver', 'valid() fail: %s', e.message)
    }

    if (two !== 2) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
342
      log.warn('BrowserDriver', 'valid() fail: two = %s ?', two)
343
      return false
344 345
    }

346 347
    log.silly('PuppetWebBrowserDriver', 'valid() driver ok')
    return true
348 349
  }

350
  public close()              { return this.driver.close() as any as Promise<void> }
351 352
  public executeAsyncScript(script: string|Function, ...args: any[])  { return this.driver.executeAsyncScript.apply(this.driver, arguments) }
  public executeScript     (script: string|Function, ...args: any[])  { return this.driver.executeScript.apply(this.driver, arguments) }
353 354 355 356 357
  public get(url: string)     { return this.driver.get(url) as any as Promise<void> }
  public getSession()         { return this.driver.getSession() as any as Promise<void> }
  public manage()             { return this.driver.manage() as any }
  public navigate()           { return this.driver.navigate() as any }
  public quit()               { return this.driver.quit() as any as Promise<void> }
358
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
359 360

export default BrowserDriver