browser-driver.ts 11.0 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
        log.warn('PuppetWebBrowserDriver', 'initChromeDriver() exception: %s, retry: %d', e.message, retry)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145
        driverError = e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
146
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147

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

150
    if (!valid) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
151 152
      log.warn('PuppetWebBrowserDriver', 'initChromeDriver() not valid after retry: %d times: %s', retry, driverError.stack)
      throw driverError
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153
    } else {
154
      log.silly('PuppetWebBrowserDriver', 'initChromeDriver() success')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155 156
    }

157
    return
158 159
  }

160
  private async getPhantomJsDriver(): Promise<WebDriver> {
161 162 163 164 165 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
    // 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 (李卓桓) 已提交
197 198
    log.silly('PuppetWebBrowserDriver', 'phantomjs binary: ' + phantomjsExe)
    log.silly('PuppetWebBrowserDriver', 'phantomjs args: ' + phantomjsArgs.join(' '))
199 200 201 202 203

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
206 207 208
    // if (!valid) {
    //   throw new Error('invalid driver founded')
    // }
209

210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
    /* 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
  }

236
  private async valid(driver: WebDriver): Promise<boolean> {
237
    log.verbose('PuppetWebBrowserDriver', 'valid()')
238

239
    try {
240
      log.verbose('PuppetWebBrowserDriver', 'valid() getSession()')
241
      const session = await new Promise((resolve, reject) => {
242
        const timer = setTimeout(() => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243 244
          const e = new Error('valid() driver.getSession() timeout(halt?)')
          log.warn('PuppetWebBrowserDriver'   , e.message)
245 246 247 248

          // 1. Promise rejected
          return reject(e)

249
        }, 67 * 1000)
250

251 252
        driver.getSession()
              .then(session => {
253
                log.verbose('PuppetWebBrowserDriver', 'valid() getSession() done')
254
                clearTimeout(timer)
255 256 257 258

                // 2. Promise resolved
                return resolve(session)

259
              })
260 261
              .catch(e => {
                log.warn('PuppetWebBrowserDriver', 'valid() getSession() rejected: %s', e && e.message || e)
262 263 264 265

                // 3. Promise rejected
                return reject(e)

266
              })
267

268 269
      })

270
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.getSession() done()')
271 272 273 274 275

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

277 278
    } catch (e) {
      log.warn('PuppetWebBrowserDriver', 'valid() driver.getSession() exception: %s', e.message)
279 280 281 282 283
      return false
    }

    let two
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
284
      two = await driver.executeScript('return 1+1')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285
      log.verbose('PuppetWebBrowserDriver', 'valid() driver.executeScript() done')
286 287 288 289 290 291
    } catch (e) {
      two = e
      log.warn('BrowserDriver', 'valid() fail: %s', e.message)
    }

    if (two !== 2) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
292
      log.warn('BrowserDriver', 'valid() fail: two = %s ?', two)
293
      return false
294 295
    }

296 297
    log.silly('PuppetWebBrowserDriver', 'valid() driver ok')
    return true
298 299
  }

300 301 302 303 304 305
  // 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 (李卓桓) 已提交
306
  //     log.verbose('PuppetWebBrowserDriver', 'driver(%s)'
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  //                                   , 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 (李卓桓) 已提交
328
  //     log.warn('PuppetWebBrowserDriver', 'driver() exception: %s', e.message)
329 330 331 332
  //     throw e
  //   }
  //   // if (!this.driver.getSession()) {
  //   //   const e = new Error('no driver session')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
333
  //   //   log.warn('PuppetWebBrowserDriver', 'driver() exception: %s', e.message)
334 335 336 337 338 339 340 341
  //   //   this.driver.quit()
  //   //   throw e
  //   // }

  //   return this.driver
  // }

  public close()              { return this.driver.close() }
342 343
  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) }
344 345 346 347 348 349
  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() }
}