Electronのコードの内部でShell書いた時に困った出来事と対応策

皆さんこんにちは。suginokoです。
Qiitaで書くのは久しぶりな気がしてます。

新年明けてまだまだ寒い日が続きますが頑張っていきましょう。

さて、今回は(も)またElectronを使ってて困ったことを解決した話です。

ElelctronのJSのコードの中にShellスクリプトを書いて実装している部分があり、挙動がElelctron独特のもの（？）で実際に起こった事例の紹介と、その解決方法について書いていきます。どなたかの役に立てれば。

※調べても出てこないので、AIに聞いたりしたけど嘘つかれるので対応するのに結構時間かかりました。（調べ方も下手なのかもしれませんが）

※また、JavaScriptで解決できるのにわざわざShellスクリプト使っているところがあります。こういった実装をし、トラブルがあったときにこんな解決方法がありますよ、という感じで見て頂ければ幸いです。（本来JavaScriptで書く方がElectronのアプリケーションに埋め込まれて隠蔽できるのでその方がいいかと思います）

## 環境
* Node v18.17.1
* Elelctron v27.0.0
* electron-builder v23.6.0

レンダラープロセス周りについて、詳細は割愛しますがVite+React使ってます。

## 1. JSの汎用関数をメインプロセスから分離したファイルにしメインプロセスで使えるようにしたい
私がやりがちなミスです。これはググっても出てくる話です。
初歩的な話ですが備忘録として書きます。

通常Webサービスではファイルをimportするかrequireすればいいですが、Elelctronだと(メインプロセスだと)微妙に違います。

私の開発環境ではrequireさせてますが、ローカル環境ではrequireして別ファイルにしても読み込めたのにElectronでbuildすると別ファイルが機能せず困ったときの対応策です。

レンダラープロセスではReact使っているので、cssなどの別ファイル使う分には通常のWeb制作するようにimportすればいい話なのですが、Electronでは（メインプロセスでは）違うようです。

### 解決策
`package.json`にelectron-builderの設定を書いていたので使用したいファイル（ディレクトリ）を指定し、ファイルを読み込めば使用できました。

package.json
```json
{
    "build": {
        "files": [
            "./src/main/const/**/*"
        ]
    }
}
```
これでconst配下のものは全て読み込めるようになります。
下の例では/const/indexを使っているので/const/indexだけの使用なら直の指定の方がいいと思います。

メインプロセスのファイル
```JavaScript
const appPath = app.getAppPath();
const ConstFilePath = path.join(appPath, "/src/main/const/index");
const Const = require(ConstFilePath);

console.log(Const.TESTTEST)
```
このように使うとJSファイルを分けても使用できるので、分けれるものは分けてもいいかもしれません。


## 2. ShellファイルをElectronのメインプロセスから分離したファイルにしメインプロセスで使えるようにしたい
あれ？1と同じようにするんじゃないの？という感じですが、微妙に違いました。（これもググればまあ出てくる話ですがせっかくなので紹介したいと思います。）

ElectronでJSではない言語やアプリケーションなどを使う場合、何かしらコマンドを叩くとなると`child_process` を使って叩くということになり、直接コマンドを叩く場合は`exec`や`spawn`を使うことになると思います。

ですが、この記事の3番、4番から出てくる不具合のおかげでexecやspawnを使ってコマンドを叩くと想定しているデータやエラー、結果が返ってこず、仕方なくファイルを分ける必要があったのでファイルを分けて対応することになりました。

詳しくは3，4番の項目をご覧ください。
今回はshellスクリプトで色々対応しているため、.shファイルを作成します。（尚、この方法は手軽に.shファイルを編集され重大なインシデントにつながる可能性があるのでオススメいたしません）

### 解決策
package.json
```json
{
    "build": {
        "files": [
            "./src/main/lib/testFolder.sh",
            "./src/main/lib/debFileDownload.sh"
        ]
    }
}
```
1番の項目のような`省略～～/lib/**/*`だとshファイルが読み込めなかったので直接指定しています。

メインプロセスのファイル
```JavaScript
const appPath = app.getAppPath();
const testFolderPath = path.join(
  appPath,
  "/src/main/lib/testFolder.sh"
);

const getTest = `${testFolderPath}`;

execFile(getTest, [yourAccessToken, TESTDATA], (err) => {
    if (err) {
        // err 
    } 
})
```

package.jsonにshファイルを追加し、そのファイルをexecFileを使ってshを叩けば解決しました。

shファイルの中でcurlコマンドを叩いています。

## 3. とあるAPIをcurlで叩くと権限エラーが出る（エラーが出るのが正しい）のにElectronで叩くとエラーにならず、処理が成功扱いになる

とあるダウンロード用のAPIを叩きたかったのですが、権限がなくAPIが叩けないということがありました。大体、Electronで叩く前にcurlでAPIが叩けるか確認してから作業に入るのですが、curlを叩く時点で
```
Error in call to API function "files/download": Your app is not permitted to access this endpoint because it does not have the required scope 'files.content.read'. The owner of the app can enable the scope for the app using the Permissions tab on the App Console.
```
と出てしまい（このエラー見たら何のAPI叩いてるかわかりそうですが）、いわゆる何かファイルをダウンロードするAPIが叩けない状態でしたので、権限付与してくれとお願いしていたのです。

その間にせめてエラー対応でもしておこうと思い、Electronで対応していたわけですが、execを使って以下のように書いていました。
```JavaScript
const dlShellCommand = `
    curl -o dl/${NEW_APP_FILE_NAME} -X POST https://ダウンロードAPIのURL \
    --header "Authorization: Bearer ${ACCESS_TOKEN}" \
    --header "Arg: {\"path\":\"/${NEW_APP_FILE_NAME}\"}"
  `;

return new Promise(async (resolve, reject) => {
    // ここでreject、resolve書かなくていいのだけど一応
    let downloadResult = exec(dlShellCommand, (error) => {
      if (error) {
        return reject(error);
      } else {
        // 処理
        resolve();
      }
    });

    // stdoutやstderrの処理を書いて（省略）

    // 結果のcodeが返ってきたら
    downloadResult.on("close", (code) => {
      console.log({ result: code === 0 ? "dl success" : "dl failed" });
      if (code === 0) {
        resolve();
      } else return reject();
    });
})
```
大体こんな感じで書いており、ダウンロードするファイルはdebファイルで想定していました。

まだ権限をもらっていない状態でしたので、当然Permission Errorで返ってくるのだろうと思っていたのですが、`1kbのdebファイルがダウンロードされた`のです。

これは色々調べましたところ、オプションに`-o`を付けると起こることがあるということも調べて出てきましたので、`-O`にしてみたり、試しにダウンロードするディレクトリを指定せずに試してみても1kbのdebファイルがダウンロードされるようになってしまったのです。

そのため、エラーと判断されずに処理としてはsuccess状態でエラーになってくれなかったので困りました。

### 解決策
2番の項目の対応をします。shファイルを分けてexecFileを使ってshファイルを実行します。

すると実行結果としてcurlで叩いたような権限エラーで返ってくるようになり、エラーとして正しく出力されるようになりました。

尚、権限が付与された状態で正しいdebファイルがダウンロードできるかどうか、execで叩いて試してないのですが、execFile使ったところ正しくダウンロードできるようになりましたのでexecFileをそのまま使用して進めました。

## 4. とあるAPIを直接curlコマンドで叩くと正しいjsonデータが返ってくるのに、Elelctron経由で叩くと正しいjsonが返ってこない
これが一番自分の中でよくわからなかった現象です。
「正しいjsonが返ってこないって何？」なのですが、ここを詳しく書いていきます。

こちらでもとあるAPIを叩くと色んなデータを含んだ配列が返ってくるのが正常の形で、返ってくると（情報は端折りますが）
```json
{"entries":[{".tag":"file","name":"testA.deb","id":"id:AABBCCDDEEEEEEEE","client_modified":"2000-10-22T11:31:19Z","server_modified":"2000-10-22T11:31:19Z","size":58814732,"is_downloadable":true},{".tag":"file","name":"testB.deb","id":"id:EEFFGGHHIIIIII","client_modified":"2023-10-22T11:47:47Z","server_modified":"2023-10-22T11:47:48Z","size":58810048,"is_downloadable":true}],"cursor":"AABBCCDD","has_more":false}
```
こんな感じで返ってきます。整形すると
```json
{
  "entries": [
    {
      ".tag": "file",
      "name": "testA.deb",
      "id": "id:AABBCCDDEEEEEEEE",
      "client_modified": "2000-10-22T11:31:19Z",
      "server_modified": "2000-10-22T11:31:19Z",
      "size": 58814732,
      "is_downloadable": true
    },
    {
      ".tag": "file",
      "name": "testB.deb",
      "id": "id:EEFFGGHHIIIIII",
      "client_modified": "2023-10-22T11:47:47Z",
      "server_modified": "2023-10-22T11:47:48Z",
      "size": 58810048,
      "is_downloadable": true
    }
  ],
  "cursor": "AABBCCDD",
  "has_more": false
}

```
こういったデータが返ってきます。curlでapiを叩く分には正常なjsonデータで返ってきます。


ところがこれをElectronのexec使って叩くと
```JavaScript
const curlGetList = `
    curl -X POST JSONデータが返ってくるURL \
    -H "Authorization: Bearer ${ACCESS_TOKEN}" \
    -H "Content-Type: application/json" -d '${JSON.stringify(params)}'
  `;

getFileList = exec(curlGetList);
getFileList.stdout.on("data", (data) => {
    console.log("data", data);
})
```

以下のように返ってきます。
```
data {"entries":[{".tag":"file","name":"testA.deb","id":"id:AABBCCDDEEEEEEEE","client_modified":"2000-10-22T11:31:19Z","server_modified":"2000-10-22T11:31:19Z","size":58814732,"is_downloadable":true},{".tag":"file","name":"te
100  6565    0  6347  100   218  11273    387 --:--:-- --:--:-- --:--:-- 11681
data stB.deb","id":"id:EEFFGGHHIIIIII","client_modified":"2023-10-22T11:47:47Z","server_modified":"2023-10-22T11:47:48Z","size":58810048,"is_downloadable":true}],"cursor":"AABBCCDD","has_more":false}
```
違いが分かりますでしょうか。

```
100  6565    0  6347  100   218  11273    387 --:--:-- --:--:-- --:--:-- 11681
```
というログがjsonの途中に入ってきます。さらに、stdoutのdataが2回走ってることがわかります。
jsonデータの中にログが入ってしまい、シンタックスエラーになります。

こちらを解決するにはオプションに-sを付けると余計ななログが入らなくなるので一応これでなくなるのですが……（最終的に-sSにしたような…）

オプションをつけても
```
data {"entries":[{".tag":"file","name":"testA.deb","id":"id:AABBCCDDEEEEEEEE","client_modified":"2000-10-22T11:31:19Z","server_modified":"2000-10-22T11:31:19Z","size":58814732,"is_downloadable":true},{".tag":"file","name":"tes

data tB.deb","id":"id:EEFFGGHHIIIIII","client_modified":"2023-10-22T11:47:47Z","server_modified":"2023-10-22T11:47:48Z","size":58810048,"is_downloadable":true}],"cursor":"AABBCCDD","has_more":false}
```
わかりやすく2行にしてしまいましたが、途中の
```
data {".tag":"file","name":"tes

data tB.deb","id":"id:EEFFGGHHIIIIII","
```
でログが出なくなっただけでstdoutが２回走って返ってくるべきデータが分解される形で返ってきます。（この記事ではわかりやすく改行2つしてますが実際返ってきているデータの改行は1つです。tesのところで改行入れてますが改行の箇所はランダムに入ります）
そのため、正しいjsonと扱われずにシンタックスエラーとなります。

こちらでは返ってきたデータの対応を色々検索しても出てこず、それっぽい回答を試しても改行が入ったりしました。

試したものとしては例えば
```
data.replace(/\n/g, "")
```
でreplaceしてみたり。（一応調べた回答にあったから試したけど、\nではない改行も色々試したけど効果なし。そもそも改行という扱いでもない気がするから意味はなかったと思う。そもそも２回返って？きているので改行ではないっていう）

または
```
data.replace(/\n/g, "").toString("utf8")
```
UTF-8エンコーディングで文字列を返すようにしてみたり（調べたら載ってたので一応試したけど）

他にも色々試しましたが変わらず、
execではなくてspawnで試しても変わらずでした。

しかも、この形で返ってくるのは毎回ではなくて、たまーにデータが分解してか返ってくるので、毎回起こるわけではないのです。正しく返ってくることもあり非常に厄介でした。

### 解決策
2番の項目と同じく、shのファイルを分けてexecFileで呼び出してcurlを叩くようにしました。

これで正しいjsonデータが返ってくるようになりました。

## 最後に
Electronで他言語使うときに正常なデータが返ってこないならファイル分けて実行するのがいいのかなって思ってしまいましたが、冒頭にある通りElectronのようなバイナリファイルで配布しているのでshellファイルのようなユーザーが自由に編集できるファイルに処理を記載すべきではなく、今回は実験の報告として記載していますが実際はcurlを使わずにElectron側でDLまで終わらせるのが正解だと思います。

最後の4番に関しては本当になんで結果がデータの中途半端なところで切れて２回に分けて返ってくるかわからず、child_process使ってるからなのか、原因が特定できずだったし、かなり時間をかけてしまったので辛かったですが解決してよかったです。

どなたかのお役に立てれば幸いです。