NFSサーバをElectronアプリでシャットダウンしてみた

# 背景・目的
以前、下記の記事でUTM + Ubuntu + Electron でNFS マウント / アンマウントを試しました。
今回は、NFS先のマシンのシャットダウンを試します。

https://qiita.com/zumax/items/958184c300c2985495ce

# 実践
## 前提
下記の環境で試しています。
- Mac PC
- UTM（下記の記事でインストールした環境を使用します）

https://qiita.com/zumax/items/8090f59fc2b779af55be


## NFSサーバの構築
1. UTMを起動します
1. 「新規仮想マシンを作成」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/2a50eb60-59d9-45ad-bf35-386ec7816afe.png)

1. 「仮想化」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/70eab634-81b7-4558-9857-cede5d858612.png)

1. 「Linux」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/71a43006-9a2f-4951-93bc-5cbe762026fd.png)

1. 選択をクリックし、「ubuntu-24.04.2-live-server-amd64.iso」を指定し、「続ける」をクリックします
1. 下記を指定して「続ける」をクリックします
    - メモリ:4096MB
    - CPUコア数:2
1. ストレージは、「64GiB」を指定して、「続ける」をクリックします
1. 共有ディレクトリを指定して、「続ける」をクリックします
1. 問題なければ、「保存」をクリックします

1. 起動します
1. 「Try or Install Ubuntu Server」を選択し、「Enter」します
1. 言語設定など行い、Ubuntuのインストールを行います
1. 最後にrebootします

### パッケージのインストール
1. 必要パッケージのインストール
    ```bash
    sudo apt update
    sudo apt install -y nfs-kernel-server
    ```

### 共有ディレクトリの作成
1. 下記のコマンドで、ディレクトリを作成します
    ```bash
    sudo mkdir -p /srv/nfs_share
    sudo chown nobody:nogroup /srv/nfs_share
    sudo chmod 777 /srv/nfs_share
    ```

1. `/etc/exports`を編集し、下記を指定します。IPアドレスは適宜変わります
    ```bash
    /srv/nfs_share 192.168.64.0/24(rw,sync,no_subtree_check)
    ```

### 設定反映とNFSサーバを起動
1. `exportfs`コマンドを使って、ファイルの内容を反映させます
    ```bash
    sudo exportfs -a
    sudo systemctl restart nfs-kernel-server
    ```

1. 反映されているか確認します
    ```bash
    $ sudo exportfs -v
    /srv/nfs_share	192.168.64.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,root_squash,no_all_squash)
    $ 
    ```

## Ubuntu DesktopからNFSマウント確認
1. パッケージをインストールします
    ```bash
    sudo apt update
    sudo apt install -y nfs-common
    ```

1. マウントポイントを作成します
    ```bash
    sudo mkdir -p /mnt/nfs_share
    ```

1. NFSマウントを実行します
    ```bash
    sudo mount -t nfs 192.168.64.5:/srv/nfs_share /mnt/nfs_share
    ```

### 動作確認（NFS→クライアント）
NFSサーバ側でファイルを用意し、クライアントから確認します。

#### （実行前）クライアント
1. クライアント側でディレクトリの状況を確認します
    ```bash
    $ df -h | grep nfs
    192.168.64.5:/srv/nfs_share         30G  6.3G   23G  23% /mnt/nfs_share
    $
    $ ls -l /mnt/nfs_share/
    total 0
    $ 
    ```

#### NFSサーバでファイルを用意
1. ファイルをtouchし準備します
    ```bash
    $ ls -l /srv/nfs_share/
    total 0
    $ touch /srv/nfs_share/nfs_test.txt
    $ ls -l /srv/nfs_share/
    total 0
    -rw-rw-r-- 1 ubuntu ubuntu 0 Apr 27 11:48 nfs_test.txt
    $ 
    ```

#### クライアントで確認
1. 確認できました
    ```
    $ ls -l /mnt/nfs_share/
    total 0
    -rw-rw-r-- 1 XXXX XXXX 0 Apr 27 11:48 nfs_test.txt
    $ 
    ```

## Electronアプリの作成
### プロジェクトの初期化
1. 下記のコマンドで初期化します
    ```bash
    mkdir nfs-electron-app
    cd nfs-electron-app
    npm init -y
    npm install electron --save-dev
    ```

1. 下記のようになりました
    ```bash
    $ ls -l
    total 40
    drwxrwxr-x 66 XXX XXX  4096 Apr 27 12:30 node_modules
    -rw-rw-r--  1 XXX XXX   284 Apr 27 12:30 package.json
    -rw-rw-r--  1 XXX XXX 30778 Apr 27 12:30 package-lock.json
    $
    ```
### バックエンド
1. main.jsを作成します
    ```js
    const { app, BrowserWindow, ipcMain } = require('electron');
    const { exec } = require('child_process');
    
    function createWindow() {
      const win = new BrowserWindow({
        width: 400,
        height: 300,
        webPreferences: {
          nodeIntegration: true,  // 簡易版なので有効化
          contextIsolation: false // セキュリティ的にはfalse（本番ではpreload.jsを使う）
        }
      });
      win.loadFile('index.html');
    }
    
    app.whenReady().then(createWindow);
    
    // NFSマウント
    ipcMain.handle('mount-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec('sudo mount -t nfs 192.168.64.5:/srv/nfs_share /mnt/nfs_share', (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve('mounted');
        });
      });
    });
    
    // NFSアンマウント
    ipcMain.handle('umount-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec('sudo umount /mnt/nfs_share', (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve('unmounted');
        });
      });
    });
    
    // ファイル一覧（ls）
    ipcMain.handle('ls-nfs', async () => {
      return new Promise((resolve, reject) => {
        exec('ls /mnt/nfs_share', (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve(stdout);
        });
      });
    });
    
    // NFSサーバをSSH経由でシャットダウン
    ipcMain.handle('shutdown-nfs-server', async () => {
      return new Promise((resolve, reject) => {
        exec('ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null XXX@192.168.64.4 "sudo systemctl stop nfs-kernel-server"', (error, stdout, stderr) => {
          if (error) return reject(stderr);
          resolve('nfs server stopped');
        });
      });
    });
    
    ```

### フロントエンド
1. index.htmlを作成します
    ```html
    <!DOCTYPE html>
    <html>
      <body>
        <h1>NFS Control</h1>
        <button onclick="mountNFS()">Mount</button>
        <button onclick="umountNFS()">Umount</button>
        <button onclick="listFiles()">List Files</button>
        <button onclick="shutdownNFSServer()">Shutdown NFS Server</button>
        <pre id="output"></pre>
    
        <script>
          const { ipcRenderer } = require('electron');
    
          async function mountNFS() {
            try {
              const result = await ipcRenderer.invoke('mount-nfs');
              document.getElementById('output').innerText = result;
            } catch (e) {
              document.getElementById('output').innerText = e;
            }
          }
    
          async function umountNFS() {
            try {
              const result = await ipcRenderer.invoke('umount-nfs');
              document.getElementById('output').innerText = result;
            } catch (e) {
              document.getElementById('output').innerText = e;
            }
          }
    
          async function listFiles() {
            try {
              const result = await ipcRenderer.invoke('ls-nfs');
              document.getElementById('output').innerText = result;
            } catch (e) {
              document.getElementById('output').innerText = e;
            }
          }
    
          async function shutdownNFSServer() {
            try {
              const result = await ipcRenderer.invoke('shutdown-nfs-server');
              document.getElementById('output').innerText = result;
            } catch (e) {
              document.getElementById('output').innerText = e;
            }
          }
        </script>
      </body>
    </html>
    ```

### package.jsonを修正
1. package.jsonを修正します
    ```json
    $ cat package.json 
    {
      "name": "nfs-electron-app",
      "version": "1.0.0",
      "main": "main.js",
      "scripts": {
        "start": "electron .",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "description": "",
      "devDependencies": {
        "electron": "^35.2.1"
      }
    }
    $ 
    ```

### chrome-sandboxに正しいパーミッションをつける
1. 下記を実行します
    ```bash
    sudo chown root:root ./node_modules/electron/dist/chrome-sandbox
    sudo chmod 4755 ./node_modules/electron/dist/chrome-sandbox
    ```

## Mount/Umountをsudoなしで実行できるように変更
1. `sudo visudo`を実行します
    ```bash
    sudo visudo
    ```

1. 以下の追加します
    ```bash
    XXXX ALL=(ALL) NOPASSWD: /bin/mount, /bin/umount
    ```


## 実行
1. `npm start`を実行します。画面が起動されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/26185afc-4f19-4e9e-93e1-c70823c20b08.png)

### ファイルを確認
1. `List Files`ボタンをクリックします。見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/da95a118-9a4c-4048-941e-e82460414728.png)

1. NFSサーバ側でファイルを追加します。あらたにファイルを作成されました
    ```bash
    $ ls -l
    total 0
    -rw-rw-r-- 1 ubuntu ubuntu 0 Apr 27 11:54 nfs_client.txt
    -rw-rw-r-- 1 ubuntu ubuntu 0 Apr 27 11:48 nfs_test.txt
    $ touch file1.txt
    $ ls -l
    total 0
    -rw-rw-r-- 1 ubuntu ubuntu 0 Apr 27 12:54 file1.txt
    -rw-rw-r-- 1 ubuntu ubuntu 0 Apr 27 11:54 nfs_client.txt
    -rw-rw-r-- 1 ubuntu ubuntu 0 Apr 27 11:48 nfs_test.txt
    $ 
    ```

1. 再び `List Files`ボタンをクリックします。追加したファイルが見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3db88b4f-4cba-4d12-91b7-7dd840f417dc.png)


### アンマウントを確認
1. 「Umount」ボタンをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3b6480ca-b165-4fb8-bdb1-e9edc599feb8.png)

1. 「unmounted」が表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/7034c1ac-e2d5-4bb2-8858-af8a05c96541.png)

1. ファイルが見えなくなりました
    ```bash
    $ mount | grep nfs
    nfsd on /proc/fs/nfsd type nfsd (rw,relatime)
    $ 
    $ ls -l /mnt/nfs_share/
    total 0
    $ 
    ```
1. `List Files`ボタンをクリックします。ファイルは見えません
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/3b66c79a-0808-4096-ac8c-8114684748f7.png)


### マウントを確認
1. 「Mount」ボタンをクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/68ebcf1c-d79c-4646-8d11-90020425c3e2.png)

1. 「mounted」が表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/138f07c3-d89f-4f17-af3d-02431e7f70c4.png)

1. ファイルが見えました
    ```bash
    $ mount | grep nfs
    nfsd on /proc/fs/nfsd type nfsd (rw,relatime)
    192.168.64.5:/srv/nfs_share on /mnt/nfs_share type nfs4 (rw,relatime,vers=4.2,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.64.4,local_lock=none,addr=192.168.64.5)
    $ ls -l /mnt/nfs_share/
    total 0
    -rw-rw-r-- 1 XXX XXX 0 Apr 27 12:54 file1.txt
    -rw-rw-r-- 1 XXX XXX 0 Apr 27 11:54 nfs_client.txt
    -rw-rw-r-- 1 XXX XXX 0 Apr 27 11:48 nfs_test.txt
    $ 
    ```
1. 「List Files」ボタンをクリックします。ファイルが見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/dfbafea4-26ef-4c6c-b145-9fbdd1bed3c9.png)

### シャットダウンを確認
1. 「Shutdown NFS Server」をクリックします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/bdc45ddd-69bd-42fb-afa4-3f7bdec08ceb.png)

1. コマンドラインでパスワードが求められるので入力します
1. 「nfs server stopped」が表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/0f51e2c8-f323-42fd-994e-2effcee7318d.png)

#### NFSサーバ
1. NFSサーバ側で`systemctl status nfs-kernel-server`を実行します。inactiveになりました
    ```bash
    $ systemctl status nfs-kernel-server
    ○ nfs-server.service - NFS server and services
         Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; preset: enabled)
         Active: inactive (dead) since Sun 2025-04-27 14:12:14 UTC; 2min 45s ago
       Duration: 2h 55min 10.900s
        Process: 3652 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS)
        Process: 3654 ExecStart=/usr/sbin/rpc.nfsd (code=exited, status=0/SUCCESS)
        Process: 4316 ExecStop=/usr/sbin/rpc.nfsd 0 (code=exited, status=0/SUCCESS)
        Process: 4322 ExecStopPost=/usr/sbin/exportfs -au (code=exited, status=0/SUCCESS)
        Process: 4324 ExecStopPost=/usr/sbin/exportfs -f (code=exited, status=0/SUCCESS)
       Main PID: 3654 (code=exited, status=0/SUCCESS)
            CPU: 17ms
    
    Apr 27 11:17:03 nfs systemd[1]: Starting nfs-server.service - NFS server and services...
    Apr 27 11:17:03 nfs systemd[1]: Finished nfs-server.service - NFS server and services.
    Apr 27 14:12:14 nfs systemd[1]: Stopping nfs-server.service - NFS server and services...
    Apr 27 14:12:14 nfs systemd[1]: nfs-server.service: Deactivated successfully.
    Apr 27 14:12:14 nfs systemd[1]: Stopped nfs-server.service - NFS server and services.
    $ 
    ```

### 起動後のマウント確認
シャットダウン後の状態から再びマウントします。

#### NFSサーバ
1. NFSを起動します
    ```bash 
        $ systemctl status nfs-kernel-server
        ○ nfs-server.service - NFS server and services
             Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; preset: enabled)
             Active: inactive (dead) since Sun 2025-04-27 14:12:14 UTC; 2min 45s ago
           Duration: 2h 55min 10.900s
            Process: 3652 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS)
            Process: 3654 ExecStart=/usr/sbin/rpc.nfsd (code=exited, status=0/SUCCESS)
            Process: 4316 ExecStop=/usr/sbin/rpc.nfsd 0 (code=exited, status=0/SUCCESS)
            Process: 4322 ExecStopPost=/usr/sbin/exportfs -au (code=exited, status=0/SUCCESS)
            Process: 4324 ExecStopPost=/usr/sbin/exportfs -f (code=exited, status=0/SUCCESS)
           Main PID: 3654 (code=exited, status=0/SUCCESS)
                CPU: 17ms
        
        Apr 27 11:17:03 nfs systemd[1]: Starting nfs-server.service - NFS server and services...
        Apr 27 11:17:03 nfs systemd[1]: Finished nfs-server.service - NFS server and services.
        Apr 27 14:12:14 nfs systemd[1]: Stopping nfs-server.service - NFS server and services...
        Apr 27 14:12:14 nfs systemd[1]: nfs-server.service: Deactivated successfully.
        Apr 27 14:12:14 nfs systemd[1]: Stopped nfs-server.service - NFS server and services.
        $ 
    
    $ sudo systemctl start nfs-kernel-server
    $ systemctl status nfs-kernel-server
    ● nfs-server.service - NFS server and services
         Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; enabled; preset: enabled)
         Active: active (exited) since Sun 2025-04-27 14:20:45 UTC; 3s ago
        Process: 4356 ExecStartPre=/usr/sbin/exportfs -r (code=exited, status=0/SUCCESS)
        Process: 4358 ExecStart=/usr/sbin/rpc.nfsd (code=exited, status=0/SUCCESS)
       Main PID: 4358 (code=exited, status=0/SUCCESS)
            CPU: 25ms
    
    Apr 27 14:20:45 nfs systemd[1]: Starting nfs-server.service - NFS server and services...
    Apr 27 14:20:45 nfs systemd[1]: Finished nfs-server.service - NFS server and services.
    $ 
    ```

#### クライアント
1. 「Mount」ボタンをクリックします。mountedが表示されました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/138f07c3-d89f-4f17-af3d-02431e7f70c4.png)

1. `List Files`ボタンをクリックします。ファイルが見えました
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/f3345f38-5270-49de-ae9c-c9f685566d9f.png)


1. lsコマンドでも確認できました
    ```bash
    $ ls -l /mnt/nfs_share/
    total 0
    -rw-rw-r-- 1 XXX XXX 0 Apr 27 12:54 file1.txt
    -rw-rw-r-- 1 XXX XXX 0 Apr 27 11:54 nfs_client.txt
    -rw-rw-r-- 1 XXX XXX 0 Apr 27 11:48 nfs_test.txt
    $ 
    ```

# 考察
今回、NFSマウント、アンマウントに加えて、シャットダウンも実装しました。
今回はパスワードも入れてリモートログインしましたが、次回は鍵を用意してパスワードレスを実装したいと思います。

# 参考
https://qiita.com/zumax/items/958184c300c2985495ce