browser-driver.ts 11.6 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
        const timer = setTimeout(() => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255 256
          const e = new Error('valid() driver.getSession() timeout(halt?)')
          log.warn('PuppetWebBrowserDriver'   , e.message)
257 258 259 260

          // 1. Promise rejected
          return reject(e)

261
        }, TIMEOUT)
262

263
        log.verbose('PuppetWebBrowserDriver', 'valid() getSession()')
264 265
        driver.getSession()
              .then(session => {
266
                log.verbose('PuppetWebBrowserDriver', 'valid() getSession() done')
267
                clearTimeout(timer)
268 269 270 271

                // 2. Promise resolved
                return resolve(session)

272
              })
273 274
              .catch(e => {
                log.warn('PuppetWebBrowserDriver', 'valid() getSession() rejected: %s', e && e.message || e)
275 276 277 278

                // 3. Promise rejected
                return reject(e)

279
              })
280

281 282
      })

283
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.getSession() done()')
284 285 286 287 288

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

290 291
    } catch (e) {
      log.warn('PuppetWebBrowserDriver', 'valid() driver.getSession() exception: %s', e.message)
292 293 294 295 296
      return false
    }

    let two
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
      two = await driver.executeScript('return 1+1')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.executeScript() done')
299 300 301 302 303 304
    } catch (e) {
      two = e
      log.warn('BrowserDriver', 'valid() fail: %s', e.message)
    }

    if (two !== 2) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
305
      log.warn('BrowserDriver', 'valid() fail: two = %s ?', two)
306
      return false
307 308
    }

309 310
    log.silly('PuppetWebBrowserDriver', 'valid() driver ok')
    return true
311 312
  }

313 314 315 316 317 318
  // 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 (李卓桓) 已提交
319
  //     log.verbose('PuppetWebBrowserDriver', 'driver(%s)'
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
  //                                   , 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 (李卓桓) 已提交
341
  //     log.warn('PuppetWebBrowserDriver', 'driver() exception: %s', e.message)
342 343 344 345
  //     throw e
  //   }
  //   // if (!this.driver.getSession()) {
  //   //   const e = new Error('no driver session')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
346
  //   //   log.warn('PuppetWebBrowserDriver', 'driver() exception: %s', e.message)
347 348 349 350 351 352 353 354
  //   //   this.driver.quit()
  //   //   throw e
  //   // }

  //   return this.driver
  // }

  public close()              { return this.driver.close() }
355 356
  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) }
357 358 359 360 361 362
  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() }
}