読者です 読者をやめる 読者になる 読者になる

サンゴラボ

4年目ソシャゲエンジニア

Atomコードリーディング Part 2

エントリーポイント以降

前回Atomのビルドからアプリケーションのエントリーポイントまでみた。今回はエントリーポイントからhtmlのロードまでみた。

src/browser/atom-application.coffee

EventEmitterを継承したAtomApplicationクラスがある。Atomアプリケーションのシングルトンクラス。エントリーポイントであり、アプリの状態を保持する。

open()

src/browser/main.coffeeから呼ばれるAtomApplicationクラスのスタティックメソッド

# Public: The entry point into the Atom application.
@open: (options) ->
  options.socketPath ?= DefaultSocketPath

  createAtomApplication = -> new AtomApplication(options)

  # FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
  # take a few seconds to trigger 'error' event, it could be a bug of node
  # or atom-shell, before it's fixed we check the existence of socketPath to
  # speedup startup.
  if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test
    createAtomApplication()
    return

  # ローカルドメインソケットに接続
  client = net.connect {path: options.socketPath}, ->
    client.write JSON.stringify(options), ->
      client.end()
      app.terminate() # なぜ終了?

  client.on 'error', createAtomApplication

windowsでなくて、ローカルドメインソケット(macならatom-#{process.env.USER}.sock)がなかったら、AtomApplication自身のインスタンスを生成して終わり。

それ以外の場合は、AtomApplicationインスタンス化はしないで、代わりにnetモジュールでローカルドメインソケットにつないで、コールバックでデータ送信してる。データ送信終わったらapp.terminate()が呼ばれるようになってるんだけど、なんだろこれ?

constructor

処理を順にみる。

自身をグローバルに追加

global.atomApplication = this

AtomApplicationはいろんなとこで参照するらしい。

3つのクラスのインスタンス

以下をインスタンス化してた。

  • AutoUpdateManager(src/browser/auto-update-manager.coffee)
  • ApplicationMenu(src/browser/auto-update-manager.coffee)
  • AtomProtocolHandler(src/browser/atom-protocol-handler.coffee)

AutoUpdateManagerAtomの自動更新を管理するクラス。ApplicationMenuはグローバルメニューを管理するクラス。AtomProtocolHandleratomのURLスキーム(atom://)を扱うクラス。必要になったら中身を追えばよさそう。

atomコマンドの複数回実行対応

@listenForArgumentsFromNewProcess()

メソッドを追って見る。

# Creates server to listen for additional atom application launches.
#
# You can run the atom command multiple times, but after the first launch
# the other launches will just pass their information to this server and then
# close immediately.
listenForArgumentsFromNewProcess: ->
  @deleteSocketFile()
  server = net.createServer (connection) =>
    connection.on 'data', (data) =>
      @openWithOptions(JSON.parse(data))

  server.listen @socketPath
  server.on 'error', (error) -> console.error 'Application server failed', error

コメントによるとatomコマンドは複数回実行できるようになってるらしい。2回目以降の起動は、初回の起動で生成したサーバーに情報送るだけして、クローズ(app.teminate()?)されるらしい。openメソッド中でローカルドメインソケットにつなごうとしてたのはこのためみたい。2回目以降はすでに起動しているサーバーにウィンドウを開いてもらっている。

chromiumコマンドラインにスイッチを追加

app.commandLine.appendSwitch 'js-flags', '--harmony'

chromiumコマンドラインにスイッチを追加は、appモジュールのAPIでやってる。多分、rendererプロセスの方でES6使えるようにしてるんだと思う。

各種イベントハンドラの設定

@handleEvents()

大きく分けて3種類あった。

  • AtomApplicationインスタンスで監視するアプリ固有のイベント。イベント名はapplication:*のような感じ
  • appモジュールのライフサイクル系のイベント
  • ipcモジュールのイベント

ipcモジュールはmainプロセスとrendererプロセス間の通信を行うためのモジュール。ここで、前回とばした2つのプロセスの違いを簡単にまとめておく(ほぼドキュメントの訳)。

mainプロセスとrendererプロセスの違い

mainプロセスはBrowserWindowインスタンスを生成することにより、ウェブページを生成する。BrowserWindowインスタンスは自身のrendererプロセスでウェブページを実行する。BrowserWindowインスタンスが破棄されたら、対応するrendererプロセスも終了する。

あとmainプロセスはネイティブのAPIを呼べるみたい。rendererプロセスでは無理。でもipcを使えばrendererプロセスを起点にmainプロセスを通してネイティブのAPIを呼べる。

新しいウィンドウを開く

@openWithOptions(options)

パスで開くか、URLスキームで開くかなどを判定してる。パスで開くとこはopenPathsメソッド見ればよさそう。プロセスIDのを管理していて、同じパスで開いたらウィンドウが使いまわされる実装になってる。ちなみに-n or --new-windowオプションを使って起動すると同じパスでも新しいウィンドウで開ける。

とりあえず単純に新しいウィンドウを開く場合を追ってみる。

bootstrapScript ?= require.resolve('../window-bootstrap')
resourcePath ?= @resourcePath
openedWindow = new AtomWindow({locationsToOpen, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions})

まずsrc/window-bootstrap.coffeeのパスをrequire.resolve()で取得してる。(ここではじめてsrc/browser以下以外のファイルがでてきた。)その後、取得したパスや各種設定情報を引数としてAtomWindowコンストラクタに渡してAtomWindowクラス(src/browser/atom-window.coffee)をインスタンス化をしてる。src/window-bootstrap.coffeeは名前からして、rendererプロセスの最初の方で実行するスクリプトなんだと思う。

AtomWindowコンストラクタBrowserWindowインスタンス化してる。これが上で書いたmainプロセスにおけるBrowserWindowインスタンスの生成にあたる。

src/browser/atom-window.coffee

EventEmitterを継承したAtomWindowクラスがある。BrowserWindowクラスのラッパークラスみたいな立ち位置。

constructor

適当に処理を抜粋してみる。

BrowserWindowインスタンスの生成

@browserWindow = new BrowserWindow options

インスタンスbrowserWindowプロパティで保持

AtomApplicationにウィンドウを追加

global.atomApplication.addWindow(this)

AtomApplication.addWindow(window)でアプリケーションに自分自身をウィンドウとして追加してる。

AtomApplication.addWindow(window)の中身。

# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
  @windows.push window
  @applicationMenu?.addWindow(window.browserWindow)
  window.once 'window:loaded', =>
    @autoUpdateManager.emitUpdateAvailableEvent(window)

  unless window.isSpec
    focusHandler = => @lastFocusedWindow = window
    window.browserWindow.on 'focus', focusHandler
    window.browserWindow.once 'closed', =>
      @lastFocusedWindow = null if window is @lastFocusedWindow
      window.browserWindow.removeListener 'focus', focusHandler

AtomWindowインスタンスwindow:lodedイベントのハンドラを設定してる。ハンドラの中でAutoUpdateManagerがでてきた。

AutoUpdateManagerに寄り道。

emitUpdateAvailableEvent: (windows...) ->
  return unless @releaseVersion?
  for atomWindow in windows
    atomWindow.sendMessage('update-available', {@releaseVersion})
  return

Atomの更新が利用できる場合に、AtomWindow.sendMessageでウインドウにupdate-availableメッセージを送ってる。

再度、AtomWindowに戻る。

sendMessage: (message, detail) ->
  @browserWindow.webContents.send 'message', message, detail

BrowserWindow.webContentsを介して、rendererプロセスにメッセージを送ってる。renderer側のどこかでこのメッセージを処理してると思う。

各種イベントハンドラの設定

@handleEvents()

閉じたときとか反応しなくなったときなどのイベントを設定してた。

ロード設定をセットする

@setLoadSettings(loadSettings)

loadSettingsというローカル変数にはいろいろつめてた。

メソッドの中を追ってみる。

setLoadSettings: (loadSettingsObj) ->
  # Ignore the windowState when passing loadSettings via URL, since it could
  # be quite large.
  loadSettings = _.clone(loadSettingsObj)
  delete loadSettings['windowState']

  @browserWindow.loadUrl url.format
    protocol: 'file'
    pathname: "#{@resourcePath}/static/index.html"
    slashes: true
    hash: encodeURIComponent(JSON.stringify(loadSettings))

さっそくBrowserWindow.loadUrl(url)でhtmlをロードしてる。fileプロトコルstatic/index.html(macなら/Applications/Atom.app/Contents/Resources/app/static/index.html)を指定してる。loadSettingsJSONにしてURLエンコードしてURLのハッシュに入れるみたい。

コメントには、アプリの状態(windowState)は巨大だから削ってると書いてある。確認のためAtomのDeveloper Tools(⌥⌘I)でURLハッシュをデコードして確認してみる。

decodeURIComponent(location.hash.substr(1))
"{"locationsToOpen":[{"pathToOpen":"/Users/sangotaro/work/atom"}],"bootstrapScript":"/Applications/Atom.app/Contents/Resources/app/src/window-bootstrap.js","resourcePath":"/Applications/Atom.app/Contents/Resources/app","devMode":false,"safeMode":false,"appVersion":"0.188.0","initialPaths":["/Users/sangotaro/work/atom"]}"

htmlのロードまできたので、mainプロセス側の初期化処理はだいたい終わりだと思う。初期化処理の最後にロード時間をコンソールに出力してた。

# src/browser/main.coffeeのstart関数の最後
console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test

mainプロセスでのコンソール出力はatom -fで起動すると確認できる。mainプロセスを確認したいときに便利そう。

$ atom -f
2015-04-08 01:02:46.684 Atom[55711:507] App load time: 226ms