ElectronアプリからOSの管理者権限が必要なコマンドを実行する方法(Vue CLI使用)

Electron, JavaScript, Linux, macOS, Node.js, vue-cli, Vue.js, Web, Windows, Windows 10, クロスプラットフォーム

( ´-`).。oO(この記事はだいたい 7 分くらいで読めるかも)

もくじ


はじめに

ElectronでつくるアプリでNode.jsのchild_processという機能を使えば、Electronアプリから手軽にコマンドが実行できとても便利だ。

でも、管理者権限が必要な処理を行うときにはちょっと工夫が必要になる。

この記事ではLinuxでコマンドを実行する場合を例に取り上げるけど、WindowsやmacOSでも同様にしてできると思う。

対象とする読者は次のいずれにも当てはまる人:

  • ElectronとVue CLIを使ってHello Worldアプリくらいなら作れる。
  • Windows 10、macOS、LinuxいずれかのPCでアプリを作っている。(それ以外の人はかなり少数かも)
  • PCで管理者権限コマンドが使える/使ってもよい状況にある。

Electronのおさらい

まず、Electronの基本的な処理の流れから。

Electronでつくるアプリは、基本的に次のような処理の流れでOSや他のアプリなどと連携して動作できるようになっている。

HTML/CSS/JSでつくるUI部分は「レンダラープロセス」と呼ばれる。

レンダラープロセスからのリクエストに応じてOSや他のアプリとの橋渡しを担うbackground.jsの処理(メインプロセス)が行われる。

このbackground.jsに、管理者権限が必要なコマンド(Linuxではsudo)を実行する処理を定義していくことになる。

レンダラープロセスとメインプロセスの接続

通常、electron-builderを使ってElectronアプリをつくるときは、package.jsonファイルの中にelectron-builderの設定を書く。

でも、Vue CLI環境の場合は、package.jsonファイルと同じディレクトリ階層にvue.config.jsファイルをつくり、その中に記述する。

これにより、Electronの設定が行えるのと同時にVueコンポーネント(レンダラープロセス)からbackground.js(メインプロセス)に記述した処理が呼び出せるようにもなる。

vue.config.jsファイルの記載内容は例えば次のようになる。

module.exports = {
    pluginOptions: {
        electronBuilder: {
            nodeIntegration: true
        }
    }
};

必要なnpmパッケージのインストール

この例では「sudo-prompt」というnpmパッケージを使う。

これは、Node.jsのプロジェクトでLinux、macOS、Windowsのどれでも管理者権限を必要とするコマンドが実行できるようになる優れもの。

コマンド実行時に自動でパスワード入力画面(Windowsの場合は確認画面)を表示して管理者権限で実行することをユーザーに明示してくれる。

こんな感じに。(画像はLinuxの例)

「sudo-prompt」のインストールは次のnpmコマンドで行える。

npm i sudo-prompt

レンダラープロセスの作り込み

ここから各アプリ固有の処理を書いていく。

この記事では例として、Linux環境で

HellowWorldコンポーネントのmounted(ライフサイクルフック)から、/opt/mochimochi/test/ ディレクトリを作成する

という処理を作り込んでみる。ただし、作成するディレクトリのパスはレンダラープロセス側で指定するものとする。

この処理は通常のユーザー権限では行えず、sudoコマンドによる実行もしくは管理者アカウントによる実行が必要となる。

したがって、ここでは次のコマンドをElectronアプリから実行することを目標とする。

sudo mkdir -p /opt/mochimochi/test/

HelloWorldコンポーネント(HelloWorld.vueファイル)のscriptタグの中の最初に次の1行を追加する。

import { ipcRenderer } from "electron";

次に、コンポーネントのmountedの処理を次のように書く。awaitを使っているので、mountedの前にasyncを書いている。

async mounted() {
  const ret = await ipcRenderer.invoke(
    "test-invoke",
    "/opt/mochimochi/test/"
  );
  // エラー処理
  if(ret.error) {
    alert("mkdir failed.");
    return;
  }
},

ipcRenderer.invokeメソッドの第一引数でメインプロセスのどの処理を呼び出すのかを指定して、第二引数でパラメータ(この例では作成するディレクトリのパス)を渡している。

第一引数は次で説明するメインプロセスの中に記載するキーワードと一致させる必要があるけど、自分の好きな文字列でOK。

Vuexを使っている人は、コンポーネントに具体的な処理を直接定義するのではなく、ミューテーションに定義した方が処理が整理できてメンテナンス性が良くなる場合がありそう。

メインプロセスの作り込み

background.jsの冒頭にelectronをインポートしている箇所があると思うので、そこでipcMainもインポートするように作り変える。

例えば次のようになる。

import { app, protocol, BrowserWindow, ipcMain } from "electron";

background.jsの一番下に次のように処理を追加する。

// レンダラープロセスでinvokeメソッドの第一引数に渡した文字列と同じものを
// 次のhandleメソッドの第一引数にも渡す。
// handleメソッドの第二引数で、実行する処理を定義する。
ipcMain.handle("test-invoke", (event, dirPath) => {
  try {
    // sudo-promptを読み込む。
    var sudo = require("sudo-prompt");
    var options = {
      name: "Electron",
      icns: "/Applications/Electron.app/Contents/Resources/Electron.icns" // (optional)
    };
    // 管理者権限で実行したいコマンドをOSに渡す。execメソッドの第一引数には"sudo"の記載は不要。
    sudo.exec("mkdir -p " + dirPath, options, function(error, stdout) {
      if (error) throw error;
    });
    return dirPath;
  } catch (e) {
    return "test-invoke failed.";
  }
});

動作確認

アプリを実行してみよう。

HelloWorldコンポーネントが画面に表示されると同時に、画面にパスワード入力画面が表示される。

正しいパスワードを入力したら、所望のパスにディレクトリが作成される。