Electronで自動アップデートをしたい

:::note info
viviONグループでは、DLsiteやcomipoなど、二次元コンテンツを世の中に届けるためのサービスを運営しています。
ともに働く仲間を募集していますので、興味のある方は[こちら](#一緒に二次元業界を盛り上げていきませんか)まで。
:::

# Electronで自動アップデートをしたい
知り合いの自転車屋さんに、商品のポップを簡単にデザインできるツールを作れないか相談を受けたので、Electronを使ったデスクトップアプリで作ってみることにしました :bike: 

実際に使ってもらって、そのフィードバックを反映した新しいバージョンのアプリを配信したいのですが、毎回exeファイルを送ってインストールしなおしてもらうのはちょっと手間ですよね。
こちらが新しいバージョンのアプリを登録したら、アプリの起動時にアップデートチェックが走って勝手に再インストールされる仕組みを導入してみます。

# Electron自動アップデートの概要
軽く調べてみたところElectronアプリの自動アップデートを実現したい場合、いくつかの方法があるようです。

https://www.electronjs.org/ja/docs/latest/tutorial/updates

 - AWS S3のようなクラウドストレージを利用する
     - アプリ本体とメタデータをクラウドストレージに配置するだけなので一番簡単
 - update.electronjs.orgというElectronチームが提供するオープンソースWEBサービスを利用する
     - アプリがパブリックなGitHubリポジトリのReleasesで公開されている必要がある
     - [専用のモジュール](https://github.com/electron/update-electron-app)が公開されているので、アプリ側での制御が簡単
     - プライベートなGitHubリポジトリでは利用できない
 - アップデート用サーバーを自分でデプロイする
     - こちらはプライベートなGitHubリポジトリでも可能
     - [Hazel](https://github.com/vercel/hazel)
     - [Nuts](https://github.com/GitbookIO/nuts)
     - [electron-release-server](https://github.com/ArekSredzki/electron-release-server)
     - [Nucleus](https://github.com/atlassian/nucleus)

今回はパブリックなリポジトリを使っていませんが、別にアプリ自体が公開されて問題があるわけではないので、**AWS S3**を利用した方法を試してみます。

# 検証環境
Windows10
Node：20.10.0
Vue CLI：5.0.8

# 環境構築
ある程度これに倣えば同じように検証ができるように記載しています。

前回投稿から引き続き同じ環境での検証になります。
[Electronでマルチウィンドウを制御したい](https://qiita.com/keko/items/5fc36b3d9885141c7ddb)

https://qiita.com/keko/items/5fc36b3d9885141c7ddb

まずは自動更新をするために必要となる[autoUpdaterモジュール](https://www.electronjs.org/ja/docs/latest/api/auto-updater)をインストールします。

https://www.electronjs.org/ja/docs/latest/api/auto-updater
```sh
npm install electron-updater
```

そして、Electronアプリを配布可能形式にパッケージング（exeやdmgファイルを作る）するためのモジュールであるelectron-builderも導入します。
```sh
npm install --save-dev electron-builder
```

# 更新処理を組み込む
メインプロセス側に更新処理を組み込みます。
アプリが起動したタイミングでアップデートがあるかをチェックして、もし更新があればダイアログを表示してアップデートを実行します。
```javascript:background.ts
'use strict'

import path from "path";
import { app, protocol, BrowserWindow, ipcMain, dialog } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import { autoUpdater } from "electron-updater";
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'

// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([
  { scheme: 'app', privileges: { secure: true, standard: true } }
])

// ★★★　更新ファイルダウンロード完了　★★★
autoUpdater.addListener("update-downloaded", () => {
  if (!mainWindow) return;
  // ダイアログを表示して更新があることを伝える
  dialog.showMessageBoxSync(mainWindow, {
    type: "info",
    buttons: ["Yes"],
    defaultId: 1,
    title: "お知らせ",
    message: "新しいバージョンのアプリが存在します",
    detail: "アップデートを実行します",
  });
  // アプリを終了してインストール
  autoUpdater.quitAndInstall();

  return true;
});

let mainWindow, subWindow

async function createWindow() {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      
      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
      contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION,
      preload: path.join(__dirname, 'preload.js')
    }
  })

  // メインウィンドウがレンダリングされたタイミング
  mainWindow.once("ready-to-show", async () => {
    // ★★★　最新バージョンがあるか、チェックしてダウンロード　★★★
    await autoUpdater.checkForUpdates();
  });

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    // Load the url of the dev server if in development mode
    await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
    if (!process.env.IS_TEST) mainWindow.webContents.openDevTools()
  } else {
    createProtocol('app')
    // Load the index.html when not in development
    mainWindow.loadURL('app://./index.html')
  }
}
```

アプリのバージョンを画面に表示して更新できたかわかるようにしてみます。
```vue:MainWindow.vue
<template>
  <h1>アプリバージョン：{{ appVersion }}</h1>
</template>

<script>
export default {
  name: 'MainWindow',
  data() {
    return {
      appVersion: require('../../package.json').version,
    }
  },
}
</script>

```


# S3にバケットを作成する
パッケージされたアプリを配置しておくS3のバケットを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/176c04b9-d4e1-515e-866a-8fc176a9eb58.png)

バケット名やその他の設定は任意で問題ないです。
注意点としては、パブリックなアクセスを可能とするために、ここのチェックを以下のようにする必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/6b06d786-6db6-089b-b753-3941d9f5d6ae.png)

バケットを作成したらバケットポリシーを設定します。
オブジェクトの取得だけができる状態にします。
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::auto-update-electron-sample/*"
        }
    ]
}
```

**今回はお試しなのでバケットを公開状態で進めていますが、実運用では適切なIAMポリシーを付与したIAMユーザーを作成して、バケットへのアクセスを制限することをオススメします。**
**Electron Builder側もIAMユーザーのアクセスキーを介してのS3アクセスに対応しているようです。**


# アプリをパッケージング
更新用ファイルをチェックしにいくエンドポイントを定義します。
`builderOptions`内が追加した項目です。
```javascript:vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  pluginOptions: {
    electronBuilder: {
      preload: "src/preload.js",
      builderOptions: {
        appId: "com.app.multi-window-app",
        win: {
          publish: [
            {
              provider: "generic",
              url: "https://auto-update-electron-sample.s3.ap-northeast-1.amazonaws.com/win32/",
            },
          ],
        },
      },
    },
  },
})

```

ビルドコマンドを実行してパッケージング。
```sh
npm run electron:build
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/046a0ad1-f90f-3a39-b092-570e8ba8db67.png)

うまくいけばdist_electronディレクトリに以下の3ファイルが作成されます。
 - [アプリ名] Setup 1.0.0.exe
     - アプリ本体
 - [アプリ名] Setup 1.0.0.exe.blockmap
     - おそらく更新差分情報
 - latest.yml
     - バージョンの情報（これをチェックして更新があるか判定しているはず）

# 試してみる
まずは作成したexeファイルを実行してアプリをインストールします。

package.jsonにしているバージョンが表示されているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/3790996c-9310-5755-ef19-344f99d8e02d.png)

次にpackage.jsonの`version`を更新して再ビルドします。
```json
{
  "version": "1.0.1",
}
```

S3のバケットに`win32`というディレクトリを作成して、その中に作成した3ファイルをアップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/75a18ce2-630b-c729-b9e4-00919365e644.png)

この状態でアプリを起動すると…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/1c140a33-5c8f-55bc-1bea-ba54de4a0546.png)

ダイアログが表示され、「はい」を選択すると自動でアップデートが始まります。

無事にアプリを自動更新することができました :clap: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84371/bbf6acb4-6cf6-385d-3b35-6e94b2a384d2.png)

# 余談
今回は手動で更新ファイルをS3にアップロードしていますが、これをコマンド一発で実行する方法もあるようです。
後ほどそちらも試してみようと思います。

# 一緒に二次元業界を盛り上げていきませんか？
株式会社viviONでは、フロントエンドエンジニアを募集しています。

https://vivion.jp/recruit/

また、フロントエンドエンジニアに限らず、バックエンド・SRE・スマホアプリなど様々なエンジニア職を募集していますので、ぜひ採用情報をご覧ください。