browser-driver.ts 12.8 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 13
  Builder,
  Capabilities,
  WebDriver,
14 15 16
}               from 'selenium-webdriver'

import {
17 18 19
  Config,
  HeadName,
  log,
20 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)
  }

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

32 33 34 35 36 37 38 39 40 41 42 43 44
    // if (this.driver) {
    //   try {
    //     // const valid = await this.valid(this.driver)
    //     // if (valid) {
    //     //   // await this.driver.close()
    //       await this.driver.quit()
    //     // }
    //   } catch (e) {
    //     log.verbose('PuppetWebBrowserDriver', 'init() this.driver.quit() soft exception: %s'
    //                                       , e.message
    //     )
    //   }
    // }
45

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
46 47
    switch (this.head) {
      case 'phantomjs':
48
        this.driver = await this.getPhantomJsDriver()
49 50
        break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51
      case 'firefox':
52 53 54 55 56 57
        this.driver = new Builder()
                            .setAlertBehavior('ignore')
                            .forBrowser('firefox')
                            .build()
        break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
58
      case 'chrome':
59
        await this.initChromeDriver()
60 61 62
        break

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
66 67 68
    await this.driver.manage()
                      .timeouts()
                      .setScriptTimeout(10000)
69 70 71 72

    return this
  }

73 74
  private async initChromeDriver(): Promise<void> {
    log.verbose('PuppetWebBrowserDriver', 'initChromeDriver()')
75 76 77 78 79 80

    /**
     * http://stackoverflow.com/a/27733960/1123955
     * issue #56
     * only need under win32 with cygwin
     * and will cause strange error:
81 82
     *
     */
83 84

    /*
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85 86
    const chrome  = require('selenium-webdriver/chrome')
    const path    = require('chromedriver').path
87 88

    const service = new chrome.ServiceBuilder(path).build()
89 90 91
    try {
      chrome.setDefaultService(service)
    } catch (e) { // fail safe
92 93
       // `The previously configured ChromeDriver service is still running.`
       // `You must shut it down before you may adjust its configuration.`
94
    }
95
   */
96 97

    const options = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
98 99 100 101
      args: [
          '--homepage=about:blank'
        , '--no-sandbox'
      ]  // issue #26 for run inside docker
102 103
    }
    if (Config.isDocker) {
104
      log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() wechaty in docker confirmed(should not show this in CI)')
105 106 107 108 109 110
      options['binary'] = Config.CMD_CHROMIUM
    }

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

111 112 113
    /**
     * XXX when will Builder().build() throw exception???
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114
    let retry = 0
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
115
    let driverError = new Error('initChromeDriver() invalid driver error')
116
    let valid = false
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118 119
    do {
     if (retry > 0) {
120
        log.warn('PuppetWebBrowserDriver', 'initChromeDriver() with retry: %d', retry)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
121
      }
122

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123
      try {
124
        log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() new Builder()')
125

126
        this.driver = new Builder()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127 128 129 130
                      .setAlertBehavior('ignore')
                      .forBrowser('chrome')
                      .withCapabilities(customChrome)
                      .build()
131

132
        log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() new Builder() done')
133

134 135
        valid = await this.valid(this.driver)
        log.verbose('PuppetWebBrowserDriver', 'initChromeDriver() valid() done: %s', valid)
136

137
        if (!valid) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138 139 140
          const e = new Error('initChromeDriver() got invalid driver')
          log.warn('PuppetWebBrowserDriver', e.message)
          driverError = e
141 142
        }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143
      } catch (e) {
144 145 146 147 148
        if (/could not be found/.test(e.message)) {
          // The ChromeDriver could not be found on the current PATH
          log.error('PuppetWebBrowserDriver', 'initChromeDriver() Wechaty require `chrome-driver` to be installed.')
          throw e
        }
149
        log.warn('PuppetWebBrowserDriver', 'initChromeDriver() exception: %s, retry: %d', e.message, retry)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
150
        driverError = e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
151
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
    } while (!valid && retry++ < 3)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154

155
    if (!valid) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
156 157
      log.warn('PuppetWebBrowserDriver', 'initChromeDriver() not valid after retry: %d times: %s', retry, driverError.stack)
      throw driverError
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158
    } else {
159
      log.silly('PuppetWebBrowserDriver', 'initChromeDriver() success')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160 161
    }

162
    return
163 164
  }

165
  private async getPhantomJsDriver(): Promise<WebDriver> {
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
    // 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 = [
      '--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
      // , '--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 (李卓桓) 已提交
202 203
    log.silly('PuppetWebBrowserDriver', 'phantomjs binary: ' + phantomjsExe)
    log.silly('PuppetWebBrowserDriver', 'phantomjs args: ' + phantomjsArgs.join(' '))
204 205 206 207 208

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
211 212 213
    // if (!valid) {
    //   throw new Error('invalid driver founded')
    // }
214

215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
    /* 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
  }

241
  private async valid(driver: WebDriver): Promise<boolean> {
242
    log.verbose('PuppetWebBrowserDriver', 'valid()')
243

244 245
    try {
      const session = await new Promise((resolve, reject) => {
246 247 248 249 250 251 252 253

        /**
         * 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

254
        let watchdogTimer: NodeJS.Timer | null
255

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

260
          // record timeout by set timer to null
261
          watchdogTimer = null
Huan (李卓桓)'s avatar
#122  
Huan (李卓桓) 已提交
262
          log.verbose('PuppetWebBrowserDriver', 'watchdogTimer = %s after set null', watchdogTimer)
263

264
          // 1. Promise rejected
265 266
          reject(e)
          return
267

268
        }, TIMEOUT)
269

270
        log.verbose('PuppetWebBrowserDriver', 'valid() getSession()')
271 272
        driver.getSession()
              .then(session => {
273 274 275 276 277
                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 (李卓桓) 已提交
278
                  log.verbose('PuppetWebBrowserDriver', 'watchdogTimer = %s after set null', watchdogTimer)
279 280
                } else {
                  log.verbose('PuppetWebBrowserDriver', 'valid() getSession() then() watchdog timer not exist?')
281
                }
282 283

                // 2. Promise resolved
284 285
                resolve(session)
                return
286

287
              })
288
              .catch(e => {
289
                log.warn('PuppetWebBrowserDriver', 'valid() getSession() catch() rejected: %s', e && e.message || e)
290

291 292 293 294 295 296 297 298 299 300 301
                // 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')
302
                }
303

304
              })
305

306 307
      })

308
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.getSession() done()')
309 310 311 312 313

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

315 316
    } catch (e) {
      log.warn('PuppetWebBrowserDriver', 'valid() driver.getSession() exception: %s', e.message)
317 318 319 320 321
      return false
    }

    let two
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
322
      two = await driver.executeScript('return 1+1')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
323
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.executeScript() done')
324 325 326 327 328 329
    } catch (e) {
      two = e
      log.warn('BrowserDriver', 'valid() fail: %s', e.message)
    }

    if (two !== 2) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
330
      log.warn('BrowserDriver', 'valid() fail: two = %s ?', two)
331
      return false
332 333
    }

334 335
    log.silly('PuppetWebBrowserDriver', 'valid() driver ok')
    return true
336 337
  }

338 339 340 341 342 343
  // public driver1(): WebDriver
  // public driver1(empty: null): void
  // public driver1(newDriver: WebDriver): WebDriver

  // public driver1(newDriver?: WebDriver | null): WebDriver | void {
  //   if (newDriver !== undefined) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
344
  //     log.verbose('PuppetWebBrowserDriver', 'driver(%s)'
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
  //                                   , newDriver
  //                                     ? newDriver.constructor.name
  //                                     : null
  //     )
  //   }

  //   if (newDriver !== undefined) {
  //     if (newDriver) {
  //       this.driver = newDriver
  //       return this.driver
  //     } else { // null
  //       if (this.driver && this.driver.getSession()) {
  //         throw new Error('driver still has session, can not set null')
  //       }
  //       this.driver = null
  //       return
  //     }
  //   }

  //   if (!this.driver) {
  //     const e = new Error('no driver')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
366
  //     log.warn('PuppetWebBrowserDriver', 'driver() exception: %s', e.message)
367 368 369 370
  //     throw e
  //   }
  //   // if (!this.driver.getSession()) {
  //   //   const e = new Error('no driver session')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
371
  //   //   log.warn('PuppetWebBrowserDriver', 'driver() exception: %s', e.message)
372 373 374 375 376 377 378 379
  //   //   this.driver.quit()
  //   //   throw e
  //   // }

  //   return this.driver
  // }

  public close()              { return this.driver.close() }
380 381
  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) }
382 383 384 385 386 387
  public get(url: string)     { return this.driver.get(url) }
  public getSession()         { return this.driver.getSession() }
  public manage()             { return this.driver.manage() }
  public navigate()           { return this.driver.navigate() }
  public quit()               { return this.driver.quit() }
}