browser.ts 22.2 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2
/**
 * Wechat for Bot. and for human who can talk with bot/robot
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 * Interface for puppet
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
5
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
6 7 8 9
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 */
10 11 12 13 14 15 16 17 18
import * as fs from 'fs'
import { EventEmitter } from 'events'
import {
  Builder
  , Capabilities
  , WebDriver
} from 'selenium-webdriver'

/* tslint:disable:no-var-requires */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
19
const retryPromise  = require('retry-promise').default // https://github.com/olalonde/retry-promise
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
20

21
import log    from'../brolog-env'
22

23
import Config from'../config'
24

25 26 27 28 29
type BrowserSetting = {
  head?:        string
  sessionFile?: string
}

30
class Browser extends EventEmitter {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
31

32 33 34
  private _targetState
  private _currentState

35
  public driver: WebDriver
36

37
  constructor(private setting: BrowserSetting = {}) {
38
    super()
39
    log.verbose('PuppetWebBrowser', 'constructor() with head(%s) sessionFile(%s)', setting.head, setting.sessionFile)
40

41
    setting.head = setting.head || process.env['WECHATY_HEAD'] || Config.DEFAULT_HEAD
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
42

43 44
    this.targetState('close')
    this.currentState('close')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
45 46
  }

47
  // targetState : 'open' | 'close'
48
  public targetState(newState?) {
49
    if (newState) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
50
      log.verbose('PuppetWebBrowser', 'targetState(%s)', newState)
51 52 53 54 55 56
      this._targetState = newState
    }
    return this._targetState
  }

  // currentState : 'opening' | 'open' | 'closing' | 'close'
57
  private currentState(newState?) {
58
    if (newState) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
59
      log.verbose('PuppetWebBrowser', 'currentState(%s)', newState)
60 61 62 63
      this._currentState = newState
    }
    return this._currentState
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
64

65
  public toString() { return `Browser({head:${this.setting.head})` }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
66

67
  public async init(): Promise<Browser> {
68 69 70 71 72 73 74
    this.targetState('open')
    this.currentState('opening')

    // fastUrl is used to open in browser for we can set cookies.
    // backup: 'https://res.wx.qq.com/zh_CN/htmledition/v2/images/icon/ico_loading28a2f7.gif'
    const fastUrl = 'https://wx.qq.com/zh_CN/htmledition/v2/images/webwxgeticon.jpg'

75 76 77
    // return co.call(this, function* () {
    try {
      await this.initDriver()
78 79
      // this.live = true

80 81
      await this.open(fastUrl)
      await this.loadSession()
82
                .catch(e => { // fail safe
83
                  log.verbose('PuppetWeb', 'browser.loadSession(%s) exception: %s', this.setting.sessionFile, e && e.message || e)
84
                })
85
      await this.open()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86 87
      /**
       * when open url, there could happen a quit() call.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88
       * should check here: if we are in `close` target state, we should clean up
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89
       */
90 91
      if (this.targetState() !== 'open') {
        throw new Error('init() finished but found targetState() is close. quit().')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
92
      }
93 94 95 96

      this.currentState('open')
      return this

97 98
    } catch (e) {
    // .catch(e => {
99 100
      // XXX: must has a `.catch` here, or promise will hang! 2016/6/7
      // XXX: if no `.catch` here, promise will hang!
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101
      // with selenium-webdriver v2.53.2
102 103
      // XXX: https://github.com/SeleniumHQ/selenium/issues/2233
      log.error('PuppetWebBrowser', 'init() exception: %s', e.message)
104 105 106 107 108 109

      this.currentState('closing')
      this.quit().then(_ => {
        this.currentState('close')
      })

110
      throw e
111
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
112 113
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114
  public open(url: string = 'https://wx.qq.com'): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
115 116 117
    log.verbose('PuppetWebBrowser', `open(${url})`)

    // TODO: set a timer to guard driver.get timeout, then retry 3 times 201607
118
    return new Promise((resolve, reject) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
119 120
      // wrap another Promise is because selenium use another Promise library,
      // which is incompatible with node typescript types
121
      this.driver.get(url)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
122
                  .then(_ => resolve(url))
123 124 125 126 127
                  .catch(e => {
                    log.error('PuppetWebBrowser', 'open() exception: %s', e.message)
                    this.dead(e.message)
                    reject(e)
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
128 129 130
    })
  }

131 132 133 134
  public initDriver(): Promise<WebDriver> {
    log.verbose('PuppetWebBrowser', 'initDriver(head: %s)', this.setting.head)

    const head = this.setting.head
135

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
136
    switch (true) {
137 138 139
      case !head: // no head default to phantomjs
      case /phantomjs/i.test(head):
      case /phantom/i.test(head):
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140 141 142
        this.driver = this.getPhantomJsDriver()
        break

143
      case /firefox/i.test(head):
144
        this.driver = new Builder()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145 146 147 148 149
        .setAlertBehavior('ignore')
        .forBrowser('firefox')
        .build()
        break

150
      case /chrome/i.test(head):
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
151 152 153 154
        this.driver = this.getChromeDriver()
        break

      default: // unsupported browser head
155
        throw new Error('unsupported head: ' + head)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
156 157
    }

158 159 160 161
    this.driver.manage()
          .timeouts()
          .setScriptTimeout(10000)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162 163 164 165 166 167
    // XXX: if no `setTimeout()` here, promise will hang forever!
    // with a confirmed bug in selenium-webdriver v2.53.2:
    // https://github.com/SeleniumHQ/selenium/issues/2233
    // FIXED: selenium v3 released 20160807
    // setTimeout(() => { resolve(this.driver) }, 0)
    return Promise.resolve(this.driver)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168
  }
169

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
  public refresh(): Promise<any> {
171
    log.verbose('PuppetWebBrowser', 'refresh()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172 173 174 175 176
    return new Promise((resolve, reject) => {
      this.driver.navigate().refresh()
            .then(_ => resolve())
            .catch(e => reject(e))
    })
177 178
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179
  private getChromeDriver(): WebDriver {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
180 181
    log.verbose('PuppetWebBrowser', 'getChromeDriver()')

182
    const options = {
183
      args: ['--no-sandbox']  // issue #26 for run inside docker
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
184
      , binary: undefined
185
    }
186
    if (Config.isDocker) {
187
      options.binary = Config.CMD_CHROMIUM
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
188 189
    }

190
    const customChrome = Capabilities.chrome()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191
                                    .set('chromeOptions', options)
192

193 194 195 196 197
    return new Builder()
                .setAlertBehavior('ignore')
                .forBrowser('chrome')
                .withCapabilities(customChrome)
                .build()
198
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
199

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
200
  private getPhantomJsDriver(): WebDriver {
201
    // setup custom phantomJS capability https://github.com/SeleniumHQ/selenium/issues/2069
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202
    const phantomjsExe = require('phantomjs-prebuilt').path
203
    // const phantomjsExe = require('phantomjs2').path
204 205

    const phantomjsArgs = [
206 207 208
      '--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
209
      , '--ssl-protocol=any'        // http://stackoverflow.com/a/26503588/1123955
210
      // , '--ssl-protocol=TLSv1'    // https://github.com/ariya/phantomjs/issues/11239#issuecomment-42362211
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
211

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212
      // issue: Secure WebSocket(wss) do not work with Self Signed Certificate in PhantomJS #12
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
213
      // , '--ssl-certificates-path=D:\\cygwin64\\home\\zixia\\git\\wechaty' // http://stackoverflow.com/a/32690349/1123955
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
214
      // , '--ssl-client-certificate-file=cert.pem' //
215
    ]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216

217
    if (Config.debug) {
218 219 220
      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')
221
    } else {
222
      if (log && log.level() === 'silent') {
223 224 225 226
        phantomjsArgs.push('--webdriver-loglevel=NONE')
      } else {
        phantomjsArgs.push('--webdriver-loglevel=ERROR')
      }
227 228
    }

229 230 231 232
    const customPhantom = Capabilities.phantomjs()
                                      .setAlertBehavior('ignore')
                                      .set('phantomjs.binary.path', phantomjsExe)
                                      .set('phantomjs.cli.args', phantomjsArgs)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
233

234
    log.silly('PuppetWebBrowser', 'phantomjs binary: ' + phantomjsExe)
235
    log.silly('PuppetWebBrowser', 'phantomjs args: ' + phantomjsArgs.join(' '))
236

237 238 239
    const driver = new Builder()
                        .withCapabilities(customPhantom)
                        .build()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
240

241
    /* tslint:disable:jsdoc-format */
242
		/**
243
		 *  FIXME: ISSUE #21 - https://github.com/zixia/wechaty/issues/21
244 245 246 247 248 249
	 	 *
 	 	 *	http://phantomjs.org/api/webpage/handler/on-resource-requested.html
		 *	http://stackoverflow.com/a/29544970/1123955
		 *  https://github.com/geeeeeeeeek/electronic-wechat/pull/319
		 *
		 */
250 251 252 253 254 255 256 257 258 259
    //   	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();
    //    }
    // }
    // `)
260

261 262
    // https://github.com/detro/ghostdriver/blob/f976007a431e634a3ca981eea743a2686ebed38e/src/session.js#L233
    // driver.manage().timeouts().pageLoadTimeout(2000)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263

264
    return driver
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265
  }
266

267
  public async quit(restart?: boolean): Promise<any> {
268
    log.verbose('PuppetWebBrowser', 'quit()')
269 270 271 272 273 274

    if (!restart) {
      this.targetState('close')
      this.currentState('closing')
    }

275 276
    // this.live = false

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
277
    if (!this.driver) {
278
      log.verbose('PuppetWebBrowser', 'driver.quit() skipped because no driver')
279 280

      this.currentState('close')
281
      return Promise.resolve('no driver')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282
    } else if (!this.driver.getSession()) {
283
      this.driver = null
284
      log.verbose('PuppetWebBrowser', 'driver.quit() skipped because no driver session')
285 286

      this.currentState('close')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287
      return Promise.resolve('no driver session')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
288
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289

290 291
    // return co.call(this, function* () {
    try {
292
      log.silly('PuppetWebBrowser', 'quit() co()')
293
      await this.driver.close() // http://stackoverflow.com/a/32341885/1123955
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294
      log.silly('PuppetWebBrowser', 'quit() driver.close()-ed')
295
      await this.driver.quit()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296
      log.silly('PuppetWebBrowser', 'quit() driver.quit()-ed')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
      this.driver = null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
      log.silly('PuppetWebBrowser', 'quit() this.driver = null')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
299

300
      /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301 302
       *
       * if we use AVA to test, then this.clean will cause problems
303
       * because there will be more than one instance of browser with the same nodejs process id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304
       *
305
       */
306
      await this.clean()
307

308
      this.currentState('close')
309
      log.silly('PuppetWebBrowser', 'quit() co() end')
310 311
    // }).catch(e => {
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
312 313
      // console.log(e)
      // log.warn('PuppetWebBrowser', 'err: %s %s %s %s', e.code, e.errno, e.syscall, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
314
      log.warn('PuppetWebBrowser', 'quit() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
315 316 317 318 319 320 321 322 323 324

      const crashMsgs = [
        'ECONNREFUSED'
        , 'WebDriverError: .* not reachable'
        , 'NoSuchWindowError: no such window: target window already closed'
      ]
      const crashRegex = new RegExp(crashMsgs.join('|'), 'i')

      if (crashRegex.test(e.message)) { log.warn('PuppetWebBrowser', 'driver.quit() browser crashed') }
      else                            { log.warn('PuppetWebBrowser', 'driver.quit() exception: %s', e.message) }
325 326 327

      // XXX fail safe to `close` ?
      this.currentState('close')
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
328 329

      /* fail safe */
330 331 332
    }

    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
333
  }
334

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
335
  public clean(): Promise<any> {
336
    const max = 30
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
337
    const backoff = 100
338

339 340 341
    /**
     * max = (2*totalTime/backoff) ^ (1/2)
     * timeout = 45000 for {max: 30, backoff: 100}
342
     * timeout = 11250 for {max: 15, backoff: 100}
343
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
344
    const timeout = max * (backoff * max) / 2
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
345

346
    return retryPromise({ max: max, backoff: backoff }, attempt => {
347
      log.silly('PuppetWebBrowser', 'clean() retryPromise: attempt %s time for timeout %s'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
348
        , attempt,  timeout)
349 350

      return new Promise((resolve, reject) => {
351 352 353
        this.getBrowserPids()
        .then(pids => {
          if (pids.length === 0) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
354
            log.verbose('PuppetWebBrowser', 'clean() retryPromise() resolved')
355
            resolve('clean() browser process not found, at attemp#' + attempt)
356
          } else {
357
            reject(new Error('clean() found browser process, not clean, dirty'))
358 359
          }
        })
360
        .catch(e => reject(e))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
361 362
      })
    })
363
    .catch(e => {
364
      log.error('PuppetWebBrowser', 'retryPromise failed: %s', e.message)
365 366
      throw e
    })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367
  }
368

369
  public getBrowserPids(): Promise<string[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
370
    log.silly('PuppetWebBrowser', 'getBrowserPids()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
371

372 373
    const head = this.setting.head

374 375 376 377 378 379
    return new Promise((resolve, reject) => {
      require('ps-tree')(process.pid, (err, children) => {
        if (err) {
          reject(err)
          return
        }
380
        let browserRe
381 382

        switch (true) {
383 384 385
          case !head: // no head default to phantomjs
          case /phantomjs/i.test(head):
          case /phantom/i.test(head):
386 387 388
            browserRe = 'phantomjs'
            break

389 390
          case !!(head): // head default to chrome
          case /chrome/i.test(head):
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
391
            browserRe = 'chrome(?!driver)|chromium'
392 393
            break

394
          default:
395 396
            log.warn('PuppetWebBrowser', 'getBrowserPids() for unsupported head: %s', head)
            browserRe = head
397
        }
398

399
        let matchRegex = new RegExp(browserRe, 'i')
400
        const pids: string[] = children.filter(child => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
401
          log.silly('PuppetWebBrowser', 'getBrowserPids() child: %s', JSON.stringify(child))
402 403
          // https://github.com/indexzero/ps-tree/issues/18
          return matchRegex.test('' + child.COMMAND + child.COMM)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
404 405
        }).map(child => child.PID)

406 407 408 409 410 411
        resolve(pids)
        return
      })
    })
  }

412 413 414 415 416 417
  /**
   * only wrap addCookies for convinience
   *
   * use this.driver.manage() to call other functions like:
   * deleteCookie / getCookie / getCookies
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
418 419 420 421 422 423

  // TypeScript Overloading: http://stackoverflow.com/a/21385587/1123955
  public addCookies(cookies: Object[]): Promise<any>[]
  public addCookies(cookie: Object): Promise<any>

  public addCookies(cookie: Object|Object[]): Promise<any>|Promise<any>[] {
424
    if (this.dead()) { return Promise.reject(new Error('addCookies() - browser dead'))}
425

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426
    if (Array.isArray(cookie)) {
427
      return cookie.map(c => {
428
        return this.addCookies(c)
429 430
      })
    }
431 432 433 434 435 436
    /**
     * convert expiry from seconds to milliseconds. https://github.com/SeleniumHQ/selenium/issues/2245
     * with selenium-webdriver v2.53.2
     * NOTICE: the lastest branch of selenium-webdriver for js has changed the interface of addCookie:
     * https://github.com/SeleniumHQ/selenium/commit/02f407976ca1d516826990f11aca7de3c16ba576
     */
437
    // if (cookie.expiry) { cookie.expiry = cookie.expiry * 1000 /* XXX: be aware of new version of webdriver */}
438

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
439
    log.silly('PuppetWebBrowser', 'addCookies(%s)', JSON.stringify(cookie))
440

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
441 442
    return new Promise((resolve, reject) => {
      (this.driver.manage() as any)
443 444 445 446 447
                  // this is old webdriver format
                  // .addCookie(cookie.name, cookie.value, cookie.path
                  //   , cookie.domain, cookie.secure, cookie.expiry)
                  // this is new webdriver format
                  .addCookie(cookie)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
448
                  .then(r => resolve(r))
449 450
                  .catch(e => {
                    log.warn('PuppetWebBrowser', 'addCookies() exception: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
451
                    reject(e)
452
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453
    })
454
  }
455

456
  public execute(script, ...args): Promise<any> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457 458 459 460 461 462
    log.silly('PuppetWebBrowser', 'Browser.execute("%s")'
                                , (
                                    script.slice(0, 80)
                                          .replace(/[\n\s]+/g, ' ')
                                    + (script.length > 80 ? ' ... ' : '')
                                )
463
            )
464
    // log.verbose('PuppetWebBrowser', `Browser.execute() driver.getSession: %s`, util.inspect(this.driver.getSession()))
465
    if (this.dead()) { return Promise.reject(new Error('browser dead')) }
466

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
467 468 469 470
    // XXX
    // console.log('#############')
    // console.log(script)
    // console.log(args)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471

472 473
    return this.driver.executeScript.apply(this.driver, arguments)
    .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
474
      // this.dead(e)
475
      log.warn('PuppetWebBrowser', 'execute() exception: %s', e.message.substr(0, 99))
476 477 478 479
      throw e
    })
  }

480
  public executeAsync(script, ...args): Promise<any> {
481 482 483 484 485 486
    log.silly('PuppetWebBrowser', 'Browser.executeAsync(%s)', script.slice(0, 80))
    if (this.dead()) { return Promise.reject(new Error('browser dead')) }
// console.log(script)
    return this.driver.executeAsyncScript.apply(this.driver, arguments)
                .catch(e => {
                  // this.dead(e)
487
                  log.warn('PuppetWebBrowser', 'executeAsync() exception: %s', e.message.slice(0, 99))
488 489 490 491
                  throw e
                })
  }

492 493 494 495 496
  /**
   *
   * check whether browser is full functional
   *
   */
497
  public readyLive(): Promise<any> {
498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
    log.verbose('PuppetWebBrowser', 'readyLive()')
    if (this.dead()) {
      return Promise.reject(new Error('this.dead() true'))
    }
    return new Promise((resolve, reject) => {
      this.execute('return 1+1')
      .then(r => {
        if (r === 2) {
          resolve(true) // browser ok, living
          return
        }
        const errMsg = 'deadEx() found dead browser coz 1+1 = ' + r + ' (not 2)'
        log.verbose('PuppetWebBrowser', errMsg)
        this.dead(errMsg)
        reject(new Error(errMsg)) // browser not ok, dead
        return
      })
      .catch(e => {
        const errMsg = 'deadEx() found dead browser coz 1+1 = ' + e.message
        log.verbose('PuppetWebBrowser', errMsg)
        this.dead(errMsg)
        reject(new Error(errMsg)) // browser not live
        return
      })
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
524

525
  public dead(forceReason?) {
526 527 528 529 530 531
    let errMsg
    let dead = false

    if (forceReason) {
      dead = true
      errMsg = forceReason
532 533
    // } else if (!this.live) {
    } else if (this.targetState() !== 'open') {
534
      dead = true
535 536
      // errMsg = 'browser not live'
      errMsg = 'targetState not open'
537 538 539 540 541 542
    } else if (!this.driver || !this.driver.getSession()) {
      dead = true
      errMsg = 'no driver or session'
    }

    if (dead) {
543
      log.warn('PuppetWebBrowser', 'dead() because %s', errMsg)
544 545 546
      // this.live = false
      this.currentState('closing')
      this.quit().then(_ => this.currentState('close'))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
547

548
      // must use nextTick here, or promise will hang... 2016/6/10
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
549 550
      process.nextTick(_ => {
        log.verbose('PuppetWebBrowser', 'dead() emit a `dead` event because %s', errMsg)
551 552 553 554 555 556
        this.emit('dead', errMsg)
      })
    }
    return dead
  }

557
  public checkSession(): Promise<Object[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
558 559 560
    // just check cookies, no file operation
    log.verbose('PuppetWebBrowser', 'checkSession()')

561
    if (this.dead()) { Promise.reject(new Error('checkSession() - browser dead'))}
562

563 564
    return new Promise((resolve, reject) => {
      this.driver.manage().getCookies()
565 566
                .then(cookies => {
                  log.silly('PuppetWebBrowser', 'checkSession %s', cookies.map(c => c.name).join(','))
567
                  resolve(cookies)
568 569 570
                })
                .catch(e => {
                  log.error('PuppetWebBrowser', 'checkSession() getCookies() exception: %s', e && e.message || e)
571
                  reject(e)
572
                })
573
    })
574 575
  }

576
  public cleanSession() {
577 578
    log.verbose('PuppetWebBrowser', `cleanSession(${this.setting.sessionFile})`)
    if (!this.setting.sessionFile) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
579 580 581
      return Promise.reject(new Error('cleanSession() no session'))
    }

582
    if (this.dead())  { return Promise.reject(new Error('cleanSession() - browser dead'))}
583

584
    const filename = this.setting.sessionFile
585
    return new Promise((resolve, reject) => {
586
      fs.unlink(filename, err => {
587
        if (err && err.code !== 'ENOENT') {
588
          log.silly('PuppetWebBrowser', 'cleanSession() unlink session file %s fail: %s', filename, err.message)
589 590 591 592 593 594
        }
        resolve()
      })
    })
  }

595 596 597
  public saveSession(): Promise<Object[]> {
    log.silly('PuppetWebBrowser', `saveSession(${this.setting.sessionFile})`)
    if (!this.setting.sessionFile) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
598 599 600 601 602
      return Promise.reject(new Error('saveSession() no session'))
    } else if (this.dead()) {
      return Promise.reject(new Error('saveSession() - browser dead'))
    }

603
    const filename = this.setting.sessionFile
604

605 606 607 608 609 610 611 612 613 614 615 616 617
    function cookieFilter(cookies) {
      const skipNames = [
        'ChromeDriver'
        , 'MM_WX_SOUND_STATE'
        , 'MM_WX_NOTIFY_STATE'
      ]
      const skipNamesRegex = new RegExp(skipNames.join('|'), 'i')
      return cookies.filter(c => {
        if (skipNamesRegex.test(c.name)) { return false }
        // else if (!/wx\.qq\.com/i.test(c.domain))  { return false }
        else                             { return true }
      })
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
618

619 620
    return new Promise((resolve, reject) => {
      this.driver.manage().getCookies()
621 622
      .then(cookieFilter)
      .then(cookies => {
623 624
        // log.silly('PuppetWeb', 'saving %d cookies for session: %s', cookies.length
        //   , util.inspect(cookies.map(c => { return {name: c.name /*, value: c.value, expiresType: typeof c.expires, expires: c.expires*/} })))
625
        log.silly('PuppetWebBrowser', 'saving %d cookies for session: %s', cookies.length, cookies.map(c => c.name).join(','))
626 627 628

        const jsonStr = JSON.stringify(cookies)
        fs.writeFile(filename, jsonStr, function(err) {
629 630
          if (err) {
            log.error('PuppetWebBrowser', 'saveSession() fail to write file %s: %s', filename, err.errno)
631 632
            return reject(err)
          }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
633
          log.silly('PuppetWebBrowser', 'saved session(%d cookies) to %s', cookies.length, filename)
634
          return resolve(cookies)
635 636 637 638 639
        })
      })
      .catch(e => {
        log.error('PuppetWebBrowser', 'saveSession() getCookies() exception: %s', e.message)
        reject(e)
640 641 642 643
      })
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
644
  public loadSession(): Promise<any> {
645 646
    log.verbose('PuppetWebBrowser', `loadSession(${this.setting.sessionFile})`)
    if (!this.setting.sessionFile) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
647 648 649 650
      return Promise.reject(new Error('loadSession() no sessionFile'))
    } else if (this.dead()) {
      return Promise.reject(new Error('loadSession() - browser dead'))
    }
651

652
    const filename = this.setting.sessionFile
653 654 655 656

    return new Promise((resolve, reject) => {
      fs.readFile(filename, (err, jsonStr) => {
        if (err) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
657
          if (err) { log.silly('PuppetWebBrowser', 'loadSession(%s) skipped because error code: %s', filename, err.code) }
658
          return reject(new Error('error code:' + err.code))
659
        }
660
        const cookies = JSON.parse(jsonStr.toString())
661

662 663 664 665
        let ps = this.addCookies(cookies)
        if (!Array.isArray(ps)) {
          ps = [ps]
        }
666
        Promise.all(ps)
667
        .then(() => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
668
          log.verbose('PuppetWebBrowser', 'loaded session(%d cookies) from %s', cookies.length, filename)
669 670 671
          resolve(cookies)
        })
        .catch(e => {
672
          log.error('PuppetWebBrowser', 'loadSession() addCookies() exception: %s', e.message)
673 674
          reject(e)
        })
675 676 677
      })
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
678 679
}

680 681
// module.exports = Browser
export default Browser