[Electron] 自動アップデートに対応する - electron-builder + AWS S3

アプリを起動すると自動的に最新版があるか確認し、もし更新されていれば自動的にバージョンアップしてくれる機能を実装します。

いくつか方法はあるのですが今回は

  1. electron-builderの機能を使う
  2. ビルドしたアプリはAWS S3へアップ
  3. 更新があるとユーザーの確認無しでダウンロードしちゃう

という方向でまとめていきます。

最終的なファイル

GitHubにこのページのファイルの一覧をアップしました。必要に応じてご利用ください。 github.com

準備

macOS用アプリはコード署名が必要

自動アップデートに対応するために、macOS用のアプリはコード署名が必須です。先に以下のページを参考に設定を行ってください。初めての方は正直ちょっと大変です(;´∀`) Windowsではコード署名は必須ではありません。 blog.katsubemakito.net

AWS側の準備

今回はAWS上のS3へアップロードします。専用のIAMとS3のバケットを作成します。

IAM

electron-builderからAWS S3を利用するためのIAMを作成します。IAMは情報が漏れた場合などを考え、使い回すさずに環境ごとに作成するのが定石です。

AWSのコンソールにあるIAMにアクセスし、左側メニュー「ユーザー」をクリック。

IAM

「ユーザーを追加」ボタンをクリック。

IAM

ユーザー名には適当な文字列を入力します。個人的には「ユーザー名@利用場所(端末やサーバ名)」的な感じでつけることが多いです。アクセスの種類は「プログラムによるアクセス」にチェック。コンソールにはログインしません。

IAM

最後にこのIAMに付与する権限を設定します。今回はS3が使えれば良いので「既存のポリシーを直接アタッチ」をクリックし、「AmazonS3FullAccess」にチェックします。

IAM

最終的に作成が完了したら、「アクセスキーID」と「シークレットアクセスキー」を適当な場所にメモします。

IAM

S3のバケット

こちらもIAMと同様にAWSのコンソールにあるS3へアクセスし、右側にある「バケットを作成」ボタンから画面の指示に従って作成します。

注意点としては「パブリックアクセスをすべてブロック」のチェックを外してください。

作成するリージョンやバケットの名前、バージョニングの有無などその他の項目は自由に設定してください。ただ一般に公開しますので専用のバケットを作成し(他と流用しない)、アクセスログを記録する設定を行うのが万が一の時のためにおすすめです。

IAMの情報をセットする

先ほど作成したIAMの情報をホームディレクトリに「.aws」という名前のフォルダを作成、その中に「credentials」という名前のファイルに以下のようなフォーマットで記述し保存します。

$ cat ~/.aws/credentials
[default]
aws_access_key_id=AAAAAAAAAAAAAAAAAAAA
aws_secret_access_key=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

詳細はAWSのドキュメントを参照してください。 docs.aws.amazon.com

モジュールを追加

electron本体の機能ではなく、electron-builderの機能を利用する関係で「electron-updater」と、ログを記録するための「electron-log」を追加でインストールします。

$ npm install electron-updater electron-log

electron-builderの自動アップデートはよく出来ており、ビルドが終わると自動的にS3へアップロードしてくれる機能が付いています。後述しますがアップデートに必要な情報をまとめたファイル(latest.yml, latest-mac.yml)も自動生成されるので、コマンドを一発叩けばあとは全自動ですべてが終わります。

これで準備が整いました。ではここから具体的なコードを書いていきましょう。

ソースコード

自動アップデートに必要な登場人物は2人。 メインプロセス用の「JavaScript(index.js)」と「package.json」です。今回はアプリを起動したらバージョン情報だけを表示するレンダラープロセス側のHTML(index.html)も用意しました。

メインプロセス

ポイントはElectronから提供されるautoUpdaterではなく、electron-updaterのautoUpdaterを利用している点です。利用方法も変わりますので別物と考えてください。

ここではアプリの実行準備が整った段階で autoUpdater.checkForUpdatesAndNotify() を呼び、アップデートがあれば自動的にダウンロードしています。ダウンロードが完了するとupdate-downloadedイベントが発生しますのですぐに適用するかダイアログでユーザーに確認します。なおダイアログでキャンセル(「あとで」をクリック)した場合、次回の起動時に反映されます。

どこにどういった方法でアップロードするかはpackage.jsonで指定するため、ここではロジックのみを記述します。コード量もそれほど多くないので手軽に利用できますね。

const { app, dialog, BrowserWindow } = require('electron');
const { autoUpdater } = require('electron-updater');
const log = require('electron-log');

// アップデートに関する情報をログファイルへ出力
autoUpdater.logger = log;
autoUpdater.logger.transports.file.level = 'info';

// ウィンドウ管理用
let mainWin;

/**
 * ウィンドウを作成
 */
function createWindow () {
  mainWin = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      enableRemoteModule: true
    }
  })

  mainWin.loadFile('index.html')
}

// Electronの準備が完了
app.whenReady().then(()=>{
  // ウィンドウを作成
  createWindow();

  // アップデートをチェック
  autoUpdater.checkForUpdatesAndNotify();
})

// ウィンドウがすべて閉じられたら
app.on('window-all-closed', () => {
  // macOS以外は終了
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// ウィンドウが0個の状態でアクティブになったら(macOS用)
app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})


//-------------------------------------------
// 自動アップデート関連のイベント処理
//-------------------------------------------
// アップデートをチェック開始
autoUpdater.on('checking-for-update', () => {
  log.info(process.pid, 'checking-for-update...');
})
// アップデートが見つかった
autoUpdater.on('update-available', (ev, info) => {
  log.info(process.pid, 'Update available.');
})
// アップデートがなかった(最新版だった)
autoUpdater.on('update-not-available', (ev, info) => {
  log.info(process.pid, 'Update not available.');
})
// アップデートのダウンロードが完了
autoUpdater.on('update-downloaded', (info) => {
  const dialogOpts = {
    type: 'info',
    buttons: ['更新して再起動', 'あとで'],
    message: 'アップデート',
    detail: '新しいバージョンをダウンロードしました。再起動して更新を適用しますか?'
  }

  // ダイアログを表示しすぐに再起動するか確認
  dialog.showMessageBox(mainWin, dialogOpts).then((returnValue) => {
    if (returnValue.response === 0){
      autoUpdater.quitAndInstall()
    }
  })
});
// エラーが発生
autoUpdater.on('error', (err) => {
  log.error(process.pid, err);
})

レンダラープロセス

メインプロセスから呼び出されるHTML(index.html)です。バージョン情報を取り出してH1タグに挿入しているだけのもの。

<!DOCTYPE html>
<html>
<head>
  <meta charset=&quot;UTF-8&quot;>
  <meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;script-src 'self' 'unsafe-inline';&quot; />
  <title>autoUpdate sample</title>
  <style>
    body{background-color: white;}
  </style>
</head>
<body>

  <h1></h1>

<script>
  const {app} = require('electron').remote;
  const version = app.getVersion();    // 現在のバージョン情報を取得(package.json)

  // h1タグにバージョンを入れる
  document.querySelector(&quot;h1&quot;).textContent = version;
</script>
</body>
</html>

package.json

次の内容を既存のpackage.json追加してください。

{
 "scripts": {
    "build-mac-publish":"electron-builder --mac --x64 --publish always",
    "build-win-publish": "electron-builder --win --x64 --publish always"
  },
  "build": {
    "mac": {
      "target": ["zip", "dmg"],
      "publish": [
        {
          "provider": "s3",
          "bucket": "electron.blog.katsubemakito.net",
          "region": "ap-northeast-1",
          "acl":"public-read",
          "storageClass": "STANDARD",
          "path": "/darwin/"
        }
      ]
    },
    "win": {
      "publish": [
        {
          "provider": "s3",
          "bucket": "electron.blog.katsubemakito.net",
          "region": "ap-northeast-1",
          "acl":"public-read",
          "storageClass": "STANDARD",
          "path": "/win32/"
        }
      ]
    }
  }
}
scripts
electron-builderコマンドに--publish alwaysオプションを付けると、S3へ自動的にアップロードしてくれます。逆にこれを付けないとローカルでビルドした後に終了します。
target
macOSのtargetには「dmg」や「pkg」などのインストーラーを指定することが多いと思いますが、zip形式がないとアップデート時にエラー終了してしまうため、ここではdmgとzipの2つの形式を同時に指定しています(2種類作成されます)
publish
providerでサーバに公開する際の設定を行います。今回はAWS S3を指定しますが、他にも「github」「spaces」「generic」が利用できます。

S3を利用する際に指定できる設定項目には以下のような物があります。

項目 説明
region バケットが存在するリージョン ap-northeast-1
bucket S3バケット electron.blog.katsubemakito.net
path バケット内のサブディレクト /darwin/
acl アクセス権限。privateかpublic-readを指定 public-read
storageClass ストレージタイプ。 STANDARD, REDUCED_REDUNDANCY, STANDARD_IAのいずれかを指定 STANDARD

ビルドする

ビルド実行

ビルドはこれまでと変わりません。今回はpackage.jsonのscriptsに定義した以下のコマンドを実行すれば、ビルドからS3へのアップロードまですべて自動で行ってくれます。

$ npm run build-mac-publish
$ npm run build-win-publish

実際に実行するとこれまでの表示に加え、最後にuploadingが加わっていますね。

$ npm run build-mac-publish

> sample-autoupdate@1.0.0 build-mac-publish /Users/katsube/Develop/electron-sample-autoupdate
> electron-builder --mac --x64 --publish always

  • electron-builder  version=22.9.1 os=19.6.0
  • loaded configuration  file=package.json (&quot;build&quot; field)
  • writing effective config  file=dist/builder-effective-config.yaml
  • packaging       platform=darwin arch=x64 electron=10.2.0 appOutDir=dist/mac
  • default Electron icon is used  reason=application icon is not set
  • signing         file=dist/mac/sample-autoupdate.app identityName=Developer ID Application: Makito Katsube (AAAAAAAAAA) identityHash=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA provisioningProfile=none
  • building        target=DMG arch=x64 file=dist/sample-autoupdate-1.0.0.dmg
  • building block map  blockMapFile=dist/sample-autoupdate-1.0.0.dmg.blockmap
  • publishing      publisher=S3 (bucket: electron.blog.katsubemakito.net)
  • uploading       file=sample-autoupdate-1.0.0.dmg.blockmap provider=S3
  • uploading       file=sample-autoupdate-1.0.0.dmg provider=S3
  • uploading       file=latest-mac.yml provider=S3

Windowsはそれほど時間はかからないのですが、macOSの場合はビルドする度に「公証」で数分止まることもあり正直ちょっと億劫ですねw

S3の状態を確認

マネジメントコンソールか、CLIからバケットを確認すると確かにアップロードされているのがわかります。

$ aws s3 ls s3://electron.blog.katsubemakito.net/darwin/
2020-12-31 19:47:12        539 latest-mac.yml
2020-12-31 19:47:00   75910960 sample-autoupdate-1.0.0-mac.zip
2020-12-31 19:46:35   78163579 sample-autoupdate-1.0.0.dmg
2020-12-31 19:46:35      82960 sample-autoupdate-1.0.0.dmg.blockmap

$ aws s3 ls s3://electron.blog.katsubemakito.net/win32/
2020-12-31 01:15:33        362 latest.yml
2020-12-31 00:46:10   54567528 sample-autoupdate Setup 1.0.0.exe
2020-12-31 00:46:10      58506 sample-autoupdate Setup 1.0.0.exe.blockmap

electron-updaterの「autoUpdater」は、Windowsなら「latest.yml」、macOSは「latest-mac.yml」を参照し、最新のバージョンはいくつか、アプリのファイルがどこにあるかなどを確認しています。

latest.yml, latest-mac.ymlの中身

実際の中身は以下のようになっています。Windows用もmacOS用もフォーマットは同じです。

version: 1.0.0
files:
  - url: sample-autoupdate-1.0.0-mac.zip
    sha512: ZjA+AWAcl9e8xqU1y4wydG+jFYYqu9A2Iep4sXsdeYcvNOvmrE2rSdNe+metIUsZmzTaNgGSH2u1ozc78U52dA==
    size: 75910554
    blockMapSize: 81375
  - url: sample-autoupdate-1.0.0.dmg
    sha512: 2qyhbVj0nSK1ECz8SSDhl8loBD+fNlYRtqesxdWzdbT2hCE/YBf+2XbYDgYJ4ZcmFwBh7gdAN2RfNy2HSUHm9g==
    size: 78166321
path: sample-autoupdate-1.0.0-mac.zip
sha512: ZjA+AWAcl9e8xqU1y4wydG+jFYYqu9A2Iep4sXsdeYcvNOvmrE2rSdNe+metIUsZmzTaNgGSH2u1ozc78U52dA==
releaseDate: '2020-12-30T13:18:34.817Z'

実際にバージョンアップしてみる

現在のバージョンをインストール

忘れないようにインストールしておきます。注意すべきはnpm startではautoUpdateが実行されないので、自動アップデートを試す際には必ずexeやdmgなどからインストールしておく必要があります。

試しにnpm startすると以下のようなメッセージが表示され、アップデートのチェックがスキップされます。

$ npm start 
20:55:28.021 › Skip checkForUpdatesAndNotify because application is not packed

package.jsonを修正

ではバージョンアップしていきます。 package.jsonのversionの箇所を先ほどよりも大きな数値にします。

{
  "name": "sample-autoupdate",
  "version": "1.1.0",
  // 以下略
}

以上ですw 実際にはコードやアセット(画像等の素材)の修正を行った後に上記の作業を行います。

ビルドする

あとは先ほどと同様にビルド&パブリッシュすれば完了です。

$ npm run build-mac-publish
$ npm run build-win-publish

(設定がうまく終わっていれば)驚くほど簡単!

自動アップデートする

Windows

旧バージョンのアプリを起動してしばらく待つと、裏側でS3上にあるlatest.ymlを取得し、現在インストールされているものより新しいバージョンがあるかチェックします。もし新しいバージョンがある場合は自動的にダウンロードし次のようなダイアログを表示します。

「更新して再起動」を押すか、次回起動時に最新のバージョンが適用されます。

macOS

こちらもWindowsと同様です。 旧バージョンのアプリを起動してしばらく待つと、裏側でS3上にあるlatest-mac.ymlを取得し、現在インストールされているものより新しいバージョンがあるかチェック、もし新しいバージョンがある場合は自動的にダウンロードし次のようなダイアログを表示します。

「更新して再起動」を押すか、次回起動時に最新のバージョンが適用されます。

ログを確認する

今回のようにうまく更新できればよいのですが、一発でうまく行かないことも多いと思います。そんな時にはログを確認します。特に広く一般に配布するexeやdmgだとconsole.logデバッグ内容を確認できないため、最初に設定したelectron-logが活躍してくれます。

例えばmacOSの場合は以下のファイルにどこで何が起こったかが記録されます。

$ cat ~/Library/Logs/sample-autoupdate/main.log
[2020-12-30 21:56:50.846] [info] Checking for update
[2020-12-30 21:56:50.861] [info] 42155 checking-for-update...
[2020-12-30 21:56:51.102] [info] Generated new staging user ID: ac7e80a1-7159-593b-9519-06b55d99735a
[2020-12-30 21:56:51.530] [info] Update for version 1.0.0 is not available (latest version: 1.0.0, downgrade is disallowed).
[2020-12-30 21:56:51.532] [info] 42155 Update not available.

Windowsの場合は以下です。

PS C:\Users\katsube> type '.\AppData\Roaming\sample-autoupdate\logs\main.log'
[2020-12-31 21:27:29.509] [info] Checking for update
[2020-12-31 21:27:29.526] [info] 2196 checking-for-update...
[2020-12-31 21:27:30.866] [info] Update for version 1.1.0 is not available (latest version: 1.1.0, downgrade is disallowed).
[2020-12-31 21:27:30.879] [info] 2196 Update not available.

electron-logについて詳しい利用方法は以下のページをご覧ください。 blog.katsubemakito.net

トラブルシューティング

Error: ZIP file not provided

macOSでアップデートができないと思ってログを確認すると、以下のようなエラーが出ていた場合は、package.jsonの指定をミスっています。targetにzipを指定すると解消します。

$ cat ~/Library/Logs/sample-autoupdate/main.log
[2020-12-30 22:02:37.493] [error] 42565 Error: ZIP file not provided: [
  {
    "url": "https://s3-ap-northeast-1.amazonaws.com/electron.blog.katsubemakito.net/sample-autoupdate-1.1.0.dmg",
    "info": {
      "url": "sample-autoupdate-1.1.0.dmg",
      "sha512": "5gp0AdyD5efK8gt0i7An1rUmOqx0eSiyDjxY1ZlvonLh4w8U2A2N7/Gl7c+nrzrTadf2TpT23xmrHuEq3rx2dQ==",
      "size": 78166291
    }
  }
]

GitHubのelectron-builderのIssueでも報告されていますが、どうもメンテナーの方が面倒なようでZipを不要な仕様に改修はしない方針のようです(寄付があれば別とも書かれていますがw) github.com

Windowsでアンインストールできない

WindowsにインストールしたElectronアプリを削除しようとすると以下のようなダイアログが表示され、アンインストールできない場合があります。

NSIS Error Installer integrity check has failed. Common causes include incomplete download and damaged media. Contact the installer's author to obtain a new copy. More information at: http://nsis.sf.net/NSIS_Error

これはmacOSでビルドした物をWindowsに入れた場合に発生するようです。試しにWindows用のEXEはWindowsでビルドすると発生しなくなりました。

こちらもGitHubのelectron-builderのIssueでも報告されていますが、この記事を書いている時点ではまだ解決していないようです。 github.com

参考ページ