にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

Windows 10+AnacondaでGPU版TensorFlow 2.3, 2.4, 2.5のインストール

Windows 10上のAnacondaに、TensorFlowのバージョン2.3.0, 2.4.1, 2.5.0のGPUサポート付きのものを、仮想環境ごとに併用可能な状態でインストールする。

概要

TensorFlowを導入しようとしてめんどくせ~って思ったのでインストール方法をメモしておく。

ディープラーニングライブラリごとに、またバージョンごとに、GPUサポートに必要とするCUDAのバージョンが異なっている。しかし、環境変数に同時に複数のバージョンのCUDAをPathに設定することはできない(複数設定しても一つしか参照されないので意味がない)。

そのため、仮想環境ごとにPathを変更したコマンドプロンプトを起動できるようにしたらいいのでは?と思った。これをすることで、違うバージョンを併用できる。

以下では、TensorFlowのバージョン2.3.0, 2.4.1, 2.5.0のGPUサポート付きのものを、仮想環境ごとに併用可能な状態でインストールする方法を書く。

環境

CUDAに必要なものをインストール

CUDA, cuDNNバージョンの確認

TensorFlowは、そのバージョンごとに、動作が確認されているPython, CUDAとcuDNNのバージョンがある。これに従うことで、CUDA等のバージョンに関連する問題を防ぐことができる。

テストされたPython, CUDA, cuDNNのバージョンは次のページで確認できる。

TensorFlowのバージョンごとにPython, cuDNN, CUDAのバージョンが書かれているので、それを使うようにする。

TensorFlow 2.3.0のインストール(condaコマンド使用)

仮想環境の作成

ここではtf230gpuという名前の仮想環境を作成する。TensorFlow 2.3.0はPython 3.5-3.8に対応しているようなので3.8を使うことにする。

Anaconda Promptを起動し、次のコマンドを実行する。

conda create -n tf230gpu python=3.8

すると、Anacondaのインストールディレクトリの中にenvs\tf230gpuディレクトリができているはず。

TensorFlow 2.3.0(GPU版)のインストール

Anaconda Promptから、仮想環境をtf230gpuに切り替える。

activate tf230gpu

その後、tensorflow-gpuのインストールを行う。

conda install tensorflow-gpu=2.3 tensorflow=2.3=mkl_py38h1fcfbd6_0

tensorflow=2.3=mkl_py38h1fcfbd6_0は本来要らないはずだが、これについてWindows 10ではビルドの自動選択にバグがあるっぽくて、ビルドを指定してインストールしないとGPUが使えない。

CUDA, cuDNNも一緒にインストールしてくれるので楽。


インストールが終わったら、

python

を実行し、

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

とか入力して、GPUが認識されているか試してみる。device_typeGPUXLA_GPUなどになっているデバイスがリストアップされていればOK。

仮想環境を起動するショートカットの作成(任意)

仮想環境が、Anacondaのインストールディレクトリの下のenvs\tf230gpuにあるので、Scripts\activate.datにこれを指定して起動すると、仮想環境に切り替わった状態になるらしい。


Windowsのスタートメニューの「Anaconda Prompt (anaconda3)」を右クリック→「ファイルの場所を開く」を選択し、開いたフォルダのAnaconda Prompt (anaconda3)のショートカットをコピーし、デスクトップとかに貼り付ける。
貼り付けたショートカットの名前をAnaconda tf230gpuとかの分かりやすい名前に変えておく。

貼り付けたショートカットの右クリックメニューから「プロパティ」を開き、「リンク先」の最後を
C:\Users\USERNAME\anaconda3C:\Users\USERNAME\anaconda3\envs\tf230gpu
のように変更し、

%windir%\System32\cmd.exe "/K" C:\Users\USERNAME\anaconda3\Scripts\activate.bat C:\Users\USERNAME\anaconda3\envs\tf230gpu

のようにする。ここで、USERNAMEWindowsのユーザー名で、環境により異なる。

これで、このショートカットをダブルクリックして起動すると、TensorFlow 2.3.0をインストールした仮想環境に切り替わるようになる。

TensorFlow 2.4.1のインストール(pipコマンド使用)

CUDA, cuDNNのインストール

tensorflow-2.4.0のテスト済みバージョンは

  • Python 3.6-3.8
  • cuDNN 8.0
  • CUDA 11.0

なので、CUDA Toolkit 11.0 Update1とcuDNN v8.0.5 (November 9th, 2020), for CUDA 11.0を入れるといいと思う。

CUDAは、CUDA Toolkitをダウンロードしてきて、実行ファイルを実行し、指示に従えばインストールされる。

cuDNNは、ダウンロードしてきた圧縮ファイルのcudaフォルダの中身を、CUDAのインストール先C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\以下に突っ込むとよい(手抜き)


その後、環境変数のPathからCUDAのパスを削除する(しなくても問題ないが、他で使わないので…)。

  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\bin
  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\libnvvp

仮想環境の作成

仮想環境tf241gpuを作成する。

conda create -n tf241gpu python=3.8

すると、Anacondaのインストールディレクトリの中にenvs\tf241gpuディレクトリができているはず。

PathにCUDAを追加した仮想環境を起動するショートカットの作成

Windowsのスタートメニューの「Anaconda Prompt (anaconda3)」を右クリック→「ファイルの場所を開く」を選択し、開いたフォルダのAnaconda Prompt (anaconda3)をコピーしてデスクトップかどこかに貼り付ける。
ショートカットを右クリック→「プロパティ」を開き、から開いたプロパティから「リンク先」をメモする。

デフォルトでインストールすると「リンク先」は次のようになっていると思う。ユーザー名の部分USERNAMEはそれぞれの環境で異なるので、以下適切に読み替える。

%windir%\System32\cmd.exe "/K" C:\Users\USERNAME\anaconda3\Scripts\activate.bat C:\Users\USERNAME\anaconda3

ここでは、C:\Users\USERNAME\anaconda3\Scripts\activate.batコマンドプロンプト起動時に実行されるバッチファイル、C:\Users\USERNAME\anaconda3がAnacondaのインストールディレクトリとなっている。

ここで、Anacondaのインストールディレクトリ以下のところに、
C:\Users\USERNAME\anaconda3\Scripts\activate_tf241gpu.batを次の内容で作成する。USERNAMEは(というか、仮想環境のパスは)適切なものに置き換える。

@set PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\bin;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\extras\CUPTI\lib64;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0\include;%PATH%
@set CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.0
@CALL "%~dp0activate.bat" C:\Users\USERNAME\anaconda3\envs\tf241gpu

@は、コマンドの最初につけるとechoを抑制する。Pathを設定した後、仮想環境を指定してアクティベート用のバッチファイルを呼び出している。

先ほど貼り付けたショートカットを右クリック→「プロパティ」から、リンク先を

%windir%\System32\cmd.exe "/K" C:\Users\USERNAME\anaconda3\Scripts\activate_tf241gpu.bat

のように変更し、OKを押して閉じる。ショートカットの名前をAnaconda tf241gpuとかの分かりやすい名前に変えておくとよい。

これで、このショートカットをダブルクリックして起動すると、CUDAをPathに設定した上で、さっき作成した仮想環境に切り替わるようになる。

TensorFlow 2.4.1をpipでインストール

先ほど用意したショートカットをダブルクリックしてコマンドプロンプトを起動し、次のコマンドを実行する。

pip install tensorflow==2.4.1

インストールが終わったら、Python

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

を実行してみて、device_nameGPUになっているものがあればGPUが認識されていることがわかる。

TensorFlow 2.5.0のインストール(pipコマンド使用)

CUDA, cuDNNのインストール

tensorflow-2.5.0のテスト済みバージョンは

  • Python 3.6-3.9
  • cuDNN 8.1
  • CUDA 11.2

なので、CUDA Toolkit 11.2.2とcuDNN v8.1.1 (Feburary 26th, 2021), for CUDA 11.0,11.1 and 11.2を入れるといいと思う。

CUDAは、CUDA Toolkitをダウンロードしてきて、実行ファイルを実行し、指示に従えばインストールされる。

cuDNNは、ダウンロードしてきた圧縮ファイルのcudaフォルダの中身を、CUDAのインストール先C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\以下に突っ込むとよい(手抜き)


その後、環境変数のPathからCUDAのパスを削除する(しなくても問題ないが、他で使わないので…)。

  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\bin
  • C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\libnvvp

仮想環境の作成

仮想環境tf250gpuを作成する。なんとなく他と揃えてPython 3.8にしたが、3.9を使ってもいい。

conda create -n tf250gpu python=3.8

すると、Anacondaのインストールディレクトリの中にenvs\tf250gpuディレクトリができているはず。

PathにCUDAを追加した仮想環境を起動するショートカットの作成

Windowsのスタートメニューの「Anaconda Prompt (anaconda3)」を右クリック→「ファイルの場所を開く」を選択し、開いたフォルダのAnaconda Prompt (anaconda3)をコピーしてデスクトップかどこかに貼り付ける。
ショートカットを右クリック→「プロパティ」を開き、から開いたプロパティから「リンク先」をメモする。

デフォルトでインストールすると「リンク先」は次のようになっていると思う。ユーザー名の部分USERNAMEはそれぞれの環境で異なるので、以下適切に読み替える。

%windir%\System32\cmd.exe "/K" C:\Users\USERNAME\anaconda3\Scripts\activate.bat C:\Users\USERNAME\anaconda3

ここでは、C:\Users\USERNAME\anaconda3\Scripts\activate.batコマンドプロンプト起動時に実行されるバッチファイル、C:\Users\USERNAME\anaconda3がAnacondaのインストールディレクトリとなっている。

ここで、Anacondaのインストールディレクトリ以下のところに、
C:\Users\USERNAME\anaconda3\Scripts\activate_tf250gpu.batを次の内容で作成する。USERNAMEは(というか、仮想環境のパスは)適切なものに置き換える。

@set PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\bin;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\extras\CUPTI\lib64;C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\include;%PATH%
@set CUDA_PATH=C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2
@CALL "%~dp0activate.bat" C:\Users\USERNAME\anaconda3\envs\tf250gpu

@は、コマンドの最初につけるとechoを抑制する。Pathを設定した後、仮想環境を指定してアクティベート用のバッチファイルを呼び出している。

先ほど貼り付けたショートカットを右クリック→「プロパティ」から、リンク先を

%windir%\System32\cmd.exe "/K" C:\Users\USERNAME\anaconda3\Scripts\activate_tf250gpu.bat

のように変更し、OKを押して閉じる。ショートカットの名前をAnaconda tf250gpuとかの分かりやすい名前に変えておくとよい。

これで、このショートカットをダブルクリックして起動すると、CUDAのディレクトリをPathに追加した上で、さっき作成した仮想環境に切り替わるようになる。

TensorFlow 2.5.0をpipでインストール

先ほど用意したショートカットをダブルクリックしてコマンドプロンプトを起動し、次のコマンドを実行する。

pip install tensorflow==2.5.0

インストールが終わったら、Python

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

を実行してみて、device_nameGPUになっているものがあればGPUが認識されていることがわかる。

Windows 10+AnacondaでGPU版TensorFlow 2.3.0がうまく入らなかった

概要

Windows 10上で、Anacondaでtensorflow-gpu 2.3.0をインストールしたが、GPUが認識されなかった。ビルドの自動選択に不具合があるらしく、インストール時にビルドを指定すると問題なく認識されるようになった。

問題

現在、AnacondaのcondaコマンドでインストールできるTensorFlowは2.3.0が最新っぽい。
普通であれば、次のコマンドでGPU版のTensorFlowがインストールできるはずである。

conda install tensorflow-gpu==2.3.0

しかし、Windows 10環境で、上のコマンドでインストールした場合に、python

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

を見てみると、GPUやXLA_GPUなどのdevice_typeをもつものが表示されず、GPUを認識していないようだった。

解決法

なぜかWindows 10のAnacondaのTensorFlow 2.3.0において、ビルドの自動選択がうまく動いていないらしい。
そのため、ビルドを自分で指定してやるとうまくインストールできる。

Python 3.8をインストールする場合、conda install tensorflow-gpu==2.3.0の代わりに
conda install tensorflow-gpu=2.3 tensorflow=2.3=mkl_py38h1fcfbd6_0とするといいらしい。例えば次のようなコマンドになる。

conda create -n tf230gpu python=3.8
conda activate tf230gpu
conda install tensorflow-gpu=2.3 tensorflow=2.3=mkl_py38h1fcfbd6_0

Raspberry Pi Zero Wを超A&G+音声再生機にする(2021年5月版)

Raspberry Pi Zero Wを超A&G+(AGQR)音声再生機にする。


以前書いた記事

が古くなってるので、今やってできる手順を書く。

環境

母艦

手順

Micro SDへのイメージ書き込み

  1. Operating SystemにRaspberry Pi OS (32-bit)を選択
    • SDカードの容量が小さければRaspberry Pi OS Lite (32-bit)とかでもOK
  2. StorageにSDカードを選択
  3. WRITEをクリック
  4. 書き込みが終わったらCONTINUEを押してウィンドウを閉じる
    • 後で見てみたら入ったバージョンはRaspbian GNU/Linux 10 (buster)だった

SSH, Wifiのセットアップ

(SDカードが認識されない場合はSDカードを指しなおす)
SDカード(bootという名前のパーティション)を開く

SSH有効化

sshという名前の空ファイルを作成

Wifi接続設定

wpa_supplicant.confという名前のファイルを作成し、次のような内容にする。

ctrl_interface=/var/run/wpa_supplicant
network={
    ssid="xxxxxxxxxxxxxxxxxxxx"
    psk=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    key_mgmt=WPA-PSK
}

ただし、

その後、PCからSDカードを取り外す。

起動

Raspberry Pi Zero WにSDカードを挿す。
電源に接続する。端の方にあるUSB端子が電源専用となっている。電源に繋ぐと起動し、自動的にWifiに繋がる。

SSHで接続する

IPアドレスRaspberry Pi Zero Wにディスプレイを繋げていればそこに表示されるが、分からない場合はローカルIPは限られているので順番に試せばそのうち繋がる。面倒ならAdvanced IP Scannerなどを利用して調べる。

CygwinのOpenSSHを入れてるので次のようなコマンドでつなぐ。(Cygwinを使ってない場合はTera Termとかを利用して接続する)
Raspberry Pi Zero WのIPが192.168.11.10だとすると

ssh pi@192.168.11.10
  • Are you sure you want to continue connecting (yes/no/[fingerprint])と聞かれたらyesと入力してEnter
  • 初期パスワードraspberryを入力してログイン

各種設定

パスワード変更

外に公開するわけではないといえ、一応パスワード位は変えた方がいいかも?

passwd

現在のパスワードを1回、変更後のパスワードを2回入力する。

ソフトを最新に更新
sudo apt update
sudo apt -y upgrade
タイムゾーンを東京にする
sudo raspi-config

で設定を開き、

  • 5 Localisation OptionsTimezoneAsiaTokyoFinish

にする。

ラジオにする

pHAT DACを使った。しかしpHAT DACはすでにディスコンなので、新しく用意するには代わりにPirate Audioを使うといいっぽい。次のようなやつ。セットアップはPirate Audioの説明に従うとよい。

pHAT DACセットアップ

まず、Raspberry Pi Zero W側にピンヘッダ(別売り)を、pHAT DAC側にピンソケットをはんだ付けし、それらを接続する。
また、pHAT DACのミニジャックにスピーカーなどを接続しておく。ヘッドフォンを繋ぐ場合、音量が結構大きいので注意。音量コントロールのついているものを接続するのがよいと思う。

次にソフトウェアのインストールを行う。次のコマンドを実行:

curl https://get.pimoroni.com/phatdac | bash

yでインストールする。
その後、再起動する。

スピーカーから流れる謎の音声メッセージを消す

スピーカーから“To install screen reader, press Ctrl-Alt-Space”って流れてうるさいので消す。

sudo rm /etc/xdg/autostart/piwiz.desktop
sudo reboot now

参考: update - How do I stop the audio message "To install the screen reader press control alt spce"? - Raspberry Pi Stack Exchange

超A&G+(AGQR)音声の再生

Raspberry Pi OS Liteの場合はffmpegのインストールが必要かも。

ffplay -nodisp "https://fms2.uniqueradio.jp/agqr10/aandg1.m3u8"

を実行すると再生される。

起動時に自動再生するように設定

再生用シェルスクリプトの用意
nano playagqr.sh

playagqr.shを次の内容で作成

#!/bin/bash

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ffplay -nodisp "https://fms2.uniqueradio.jp/agqr10/aandg1.m3u8"

実行属性をつけて/opt/にコピーする。

chmod +x playagqr.sh
sudo cp playagqr.sh /opt/
systemdを利用した自動起動

systemdを利用して起動時に自動実行されるようにする。

sudo nano /etc/systemd/system/playagqr.service

playagqr.serviceを次のような内容で作成する:

[Unit]
Description = Play AGQR radio

[Service]
ExecStart=/opt/playagqr.sh
Restart=always
Type=simple
User=pi
Group=audio

[Install]
WantedBy=multi-user.target

自動起動を設定する:

sudo systemctl enable playagqr

これで、電源を繋いでしばらく(1分20秒ぐらい)するとAGQRの音が再生されるようになる。結構時間かかる……Raspberry Pi OS Liteの方がよかったかも?

Pleromaのサーバ移行ログ(2021-04-25)

やったこと

Pleromaのサーバを、別のサーバに移行した。
nixeneko.info
nixeneko.info

モチベーション

2018年にPleroma (nixeneko.info)のサーバを立てた。詳しくは次の記事を参照:

…のだが、OSがUbuntu 16.04だったので、OSのサポートが今年(2021年)4月で切れてしまう。そのため、サーバ移行しないとまずいと思って移行をした。

流れ

  • 新サーバセットアップ
  • 旧→新サーバにデータベースバックアップ(一応…)
  • 新サーバにPleromaをインストール
  • 旧サーバのPleromaを新サーバのと同じバージョンに更新
  • 旧サーバ止める
  • 旧→新サーバにデータベースをバックアップ、データ移行
  • 新サーバでデータベースのリストア
  • 新サーバでPleromaのセットアップを続行
  • ドメインIPアドレス変更
  • Let'senctyptの証明書の設定
  • 起動~

作業ログ

新サーバセットアップ

新しいVPSを契約した。

  • Conoha
  • メモリ 512 MB, ストレージ 30 GB
  • Ubuntu 20.04
SSHで接続

以下NEW_SERVER_IP_ADDRESSはサーバのIPで読み替えてください。

ローカルから

ssh root@NEW_SERVER_IP_ADDRESS
ユーザー作成
apt update
apt upgrade
adduser workuser
gpasswd -a workuser sudo
su workuser
SSH設定
cd ~
mkdir .ssh
chmod 700 .ssh
cd .ssh
touch authorized_keys
chmod 600 authorized_keys
nano authorized_keys

ローカルの公開鍵をコピペ、保存

sshdの設定
sudo nano /etc/ssh/sshd_config

設定を適当に変更して次のようにした

port 10022 #コメントアウト、変更

LoginGraceTime 30 #コメントアウト、変更
PermitRootLogin no #変更
StrictModes yes #コメントアウト、変更
MaxAuthTries 3 #コメントアウト、変更
MaxSessions 4 #コメントアウト、変更

PubkeyAuthentication yes #コメントアウト

IgnoreRhosts yes #コメントアウト

PasswordAuthentication no #そのまま
PermitEmptyPasswords no #コメントアウト 

保存し、再起動

sudo sshd -t
sudo service sshd restart
rootのauthorized_keysを消す
sudo su root
rm ~/.ssh/authorized_keys

まあそもそもVPSを作成する際にrootに公開鍵登録する必要なかったのではという気もする。
ログアウト。

サーバーに接続できることを確認

ローカルから

ssh -p 10022 workuser@NEW_SERVER_IP_ADDRESS

として、新しい設定でログインし直す。

ファイアウォール(ufw)を設定
sudo ufw allow 80
sudo ufw allow 443
sudo ufw limit 10022/tcp
sudo ufw enable
sudo ufw status

参考: https://qiita.com/hana_shin/items/a630871dce209cff04f3

Pleromaのセットアップ

新サーバにPleromaをセットアップする。
参考: Installing on Debian Based Distributions - Pleroma Documentation

いろいろインストール
 sudo apt install postgresql postgresql-contrib elixir erlang-dev erlang-nox libmagic-dev git build-essential cmake nginx certbot imagemagick ffmpeg exiftool

Ubuntu 20.04ではElixirのバージョンが足りてるっぽいのでそのまま入れた。

Pleroma用ユーザの追加
sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
  • -r システムアカウントを作成
  • -s /bin/false ログインシェルを指定
  • -m ホームディレクトリを作成
  • -d /var/lib/pleroma ユーザのホームディレクトリの場所
  • -U ユーザと同名のグループを作成

参考: 【 useradd 】コマンド――新規ユーザーを作成する:Linux基本コマンドTips(255) - @IT
ユーザpleromaでのコマンドの実行はsudo -Hu pleroma commandとする

Pleromaのインストール
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
cd /opt/pleroma
sudo -Hu pleroma mix deps.get

hexのインストールをするかどうかにはyを入力

sudo -Hu pleroma mix pleroma.instance gen

rebar3のインストールをするか→yを入力
この後いろいろな質問がなされる。まあ適当に書く。

データベース名、データベース用のユーザー名、パスワード、を旧サーバーに合わせる。

  • データベース名がpleroma-dev
  • データベースに繋ぐユーザーがpleroma
  • データベースに繋ぐパスワードは旧サーバのprod.secret.exsに合わせる
  • アップロードファイル名の匿名化はyにした
sudo -Hu pleroma mv config/generated_config.exs config/prod.secret.exs
sudo -Hu pleroma nano config/prod.secret.exs

前の設定ファイルを参考に編集する。

  secret_key_base: "前の設定ファイルと合わせる?",
  #…
  limit: 400,
  registrations_open: false

config :pleroma, Pleroma.Repo,
  #…  
  password: "前のやつと合わせる",
  database: "pleroma_dev",

旧サーバから新サーバにバックアップ

念のため。

sshfsを使ってリモートサーバに書き出す方法、いらなかったけど残しておく。

そんな空き容量ないと思っていたので、ネットワーク越しにバックアップデータを出力させたけど、データをcustomフォーマットでダンプしたら300 MB程度だったので、正直sshfsでリモートに書き出さなくても手元で書き出せばよかった。

旧サーバにログイン

sshfsのインストール
sudo apt install sshfs
sshfsでマウント

初期設定

screen
mkdir remote
sudo nano /etc/fuse.conf

user_allow_otherコメントアウト、保存
参考: sshfsで別サーバのディレクトリをマウントする | server-memo.net
参考: ssh - sshfs mount, sudo gets permission denied - Unix & Linux Stack Exchange
マウント

sshfs workuser@NEW_SERVER_IP_ADDRESS:/home/workuser/share /home/workuser/remote -p 10022 -o allow_other

バックアップ

cd /home/pleroma/pleroma/
sudo -Hu postgres pg_dump -d pleroma_dev --format=custom -f /home/workuser/remote/pleroma.pgdump
旧サーバのPleromaをアップデート

developブランチだった。新サーバにインストールしたバージョンに合わせてv2.3.0にアップデート

sudo su pleroma
git pull
git checkout -b backup v2.3.0
MIX_ENV=prod mix deps.get
MIX_ENV=prod mix ecto.migrate
exit
sudo systemctl restart pleroma
データベースをダンプ

参考: Backup/Restore/Move/Remove your instance - Pleroma Documentation

sudo systemctl stop pleroma
cd /home/pleroma/pleroma
mkdir /home/workuser/bkup
chmod 777 /home/workuser/bkup
sudo -Hu postgres pg_dump -d pleroma_dev -v --format=plain -f /home/workuser/bkup/pleroma_dev.sql
scp -P 10022 ~/bkup/pleroma_dev.sql workuser@NEW_SERVER_IP_ADDRESS:~/share/pleroma_dev.sql

後で書くが、手順書に従って--format=customとしたが、うまくリストアできなかったので(今考えるとオプションの-1外したらうまく行ったかもしれないが…)、--format=plainにした。そしたら容量が3 GB程度になった。容量に余裕なければsshfs使うと良いかも。他にも方法はあるっぽいが。

ファイルのバックアップ

新サーバで

sudo -Hu pleroma chmod 777 /opt/pleroma/uploads
sudo -Hu pleroma chmod 777 /opt/pleroma/config

旧サーバで

scp -P 10022 -r uploads uploads/ workuser@NEW_SERVER_IP_ADDRESS:/opt/pleroma
scp -P 10022 config/setup_db.psql workuser@NEW_SERVER_IP_ADDRESS:/opt/pleroma/config/setup_db.psql.old

新サーバで

sudo chown -R pleroma:pleroma /opt/pleroma/uploads
sudo chown pleroma:pleroma /opt/pleroma/config/setup_db.psql.old
sudo -Hu pleroma chmod 775 /opt/pleroma/uploads
sudo -Hu pleroma chmod 775 /opt/pleroma/config
sudo -Hu nano /opt/pleroma/config/setup_db.psql

setup_db.psql.oldを参考に編集(必要があれば)。ユーザー、パスワード等をあわせる

データベースの復旧

新サーバで

cd /opt/pleroma
#sudo -Hu postgres psql -c 'DROP DATABASE pleroma_dev;'; sudo -Hu postgres psql -c 'DROP USER pleroma;'
sudo -Hu postgres psql -f config/setup_db.psql
sudo -Hu postgres psql -a -f /home/workuser/share/pleroma_dev.sql -d pleroma_dev

(2行目の#で始まる行は既にデータベースを作っちゃっていた場合に実行する)

最初、手順書どおりに--fomrat==customで出力してpg_restoreでやってたらうまくリストアできなくて、次のようなエラーが出た。

pg_restore: error: could not execute query: ERROR:  schema "public" already exists
Command was: CREATE SCHEMA public;

って出た。ので代りに--format=plainで出力してpsqlでリストアしたらうまくいったっぽい。PostgreSQLのバージョンが9.6→12.6とかなり離れてたのが原因かもしれない。

(2021-08-05追記: ここでVACUUM ANALYZEやっておくといいっぽい: Pleromaのホームタイムラインが500エラー返すようになってた - にせねこメモ)

データベースのマイグレーション

まあ一応…

sudo -Hu pleroma MIX_ENV=prod mix ecto.migrate
動くか確認
sudo -Hu pleroma MIX_ENV=prod mix phx.server

動くのを確かめたら、Ctrl-C→aで終了

ドメインDNSレコードを更新

ドメインDNSレコード設定でIPアドレスを旧サーバ→新サーバに変更

Nginxの設定

新サーバにて行う

SSL証明書を設定
sudo mkdir -p /var/lib/letsencrypt/
sudo systemctl stop nginx.service
sudo certbot certonly --email EMAIL_ADDRESS -d SERVER_DOMAIN --standalone

EMAIL_ADDRESSSERVER_DOMAINは環境に合わせる。
/etc/letsencryptに保存される。

…と、これ、やったけど、証明書はドメインに対して発行されるので、IP変わっても旧サーバからコピーしてくればそれで済むっぽい。

NginxへのPleromaの設定
sudo cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx
sudo ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx

設定の編集

sudo nano /etc/nginx/sites-available/pleroma.nginx

次のように変更。SERVER_DOMAINドメイン名に変える。

    server_name SERVER_DOMAIN;
    #…
    server_name SERVER_DOMAIN;
    #…
    ssl_trusted_certificate   /etc/letsencrypt/live/SERVER_DOMAIN/chain.pem;
    ssl_certificate           /etc/letsencrypt/live/SERVER_DOMAIN/fullchain.pem;
    ssl_certificate_key       /etc/letsencrypt/live/SERVER_DOMAIN/privkey.pem;

(2022-01-07追記)
certbot --webrootで証明書を更新するためには、Nginxの設定を変更する必要があったらしい。
まず

sudo mkdir -p /var/lib/letsencrypt/.well-known/

ディレクトリを作成して、そのあと
pleroma.nginxの次掲部分の

    # Uncomment this if you need to use the 'webroot' method with certbot. Make sure
    # that the directory exists and that it is accessible by the webserver. If you followed
    # the guide, you already ran 'mkdir -p /var/lib/letsencrypt' to create the folder.
    # You may need to load this file with the ssl server block commented out, run certbot
    # to get the certificate, and then uncomment it.
    #
    # location ~ /\.well-known/acme-challenge {
    #     root /var/lib/letsencrypt/;
    # }

この設定部分のコメントアウトを外す:

    location ~ /\.well-known/acme-challenge {
        root /var/lib/letsencrypt/;
    }

これによりcertbot --webrootで証明書を更新できるようになった。
多分自動更新ができる。
(2022-01-07追記終わり)

Nginx起動
sudo systemctl enable --now nginx.service
sudo cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service

必要に応じて/etc/systemd/system/pleroma.serviceを編集

sudo systemctl enable --now pleroma.service

let'sencryptの自動更新、設定した覚えないけど/etc/cron.d/certbotに何か入ってる…。これでそのまま使えるのかな。更新されなかったら更新スクリプトを設定する必要があるかも。
(2021-08-02追記:
certbotの設定で、authenticator = standaloneになってたため、Nginxが起動している間は更新できない状態になっていたので、証明書が期限切れになった。
/etc/letsencrypt/renewal/SERVER_DOMAIN.confを編集して、

authenticator = standalone

authenticator = webroot

に変更、

renew_hook = /usr/bin/systemctl reload nginx.service
webroot_path = /var/lib/letsencrypt,

[[webroot_map]]
SERVER_DOMAIN = /var/lib/letsencrypt

になっていることを確認、または変更(なければ追加)する(SERVER_DOMAINは環境に合わせる)。
参考: Let's Encrypt更新!certbot renewコマンドでうまくいかなかったときの対処法 - Qiita
これで大丈夫かな…) (2022-01-07: webroot_path, webroot_mapを変更) (2022-04-07: post_hookを追加) (2023-02-02: post_hookのコマンドが間違っていたので修正、ついでにrenew_hookに変更)

Static Dir

参考: Static Directory - Pleroma Documentation

config :pleroma, :instance,
  #…
  static_dir: "instance/static/"

mix ecto.migrateして再起動

復元
  • instance/static/static/terms-of-service.html
  • instance/static/instance/panel.html
  • instance/static/instance/thumbnail.jpeg
  • instance/static/static/logo.png

問題点

動いた。何とか…

なんかわからんけどハッシュタグタイムラインが開けないっぽい。何が悪いのかわからんので困る。それ以外は動いているのでとりあえず様子見…。

Raspberry Pi Zero Wで温度ロガーをつくる

Raspberry Pi Zero Wと温度センサを利用して、屋外の温度を記録する温度ロガーをつくった。記録した温度データをブラウザから閲覧する仕組みもつくった。

モチベーション

外気温を記録出来たら楽しいので。

注意

素人が適当に勘と見よう見まねでつくっているだけなので、不適切な部分があるかもしれません。参考にする場合は専門家に相談するようにしてください。責任取れませんので…。

システム構成

  • Raspberry Pi Zero Wを使う。
    • 家の外に設置して記録したデータを取り出すのに、家のWifiに繋がれば楽なので。というかSSHで繋げられるので管理が楽。
  • 16GBのmicro SDカード
  • 温度センサは防水プローブに入ったDS18B20を利用
  • あとプルアップ用に4.7 kΩの抵抗器が1つ必要。プルアップ用なので別にその辺りの抵抗値なら何でも良さそう。

環境

母艦パソコンのOSはWindows 10で、Cygwinsshを使っている。

Raspberry Pi Zero Wのセットアップ

Raspberry Pi OSのインストール

  • SDカードをパソコンに挿す
  • https://www.raspberrypi.org/software/ からDownload for WindowsをクリックしてRaspberry Pi Imagerをダウンロードして実行する。
    1. CHOOSE OS→Raspberry Pi OS (32-bit)
    2. CHOOSE STORAGE→SDカードを選択
    3. WRITEをクリック
    • 書き込みが終ったらCONTINUEを押して、ウィンドウを閉じる
  • 入れてみたらバージョンは Raspbian GNU/Linux 10 (buster) だった。

ssh, Wifiのセットアップ

  • パソコンにSDを挿しなおす
  • イメージを書き込んだSDカード(boot)を開く→sshという名前の空ファイルを作成(拡張子なし!)
  • SDカード(boot)の直下にwpa_supplicant.confというファイルを次の内容で作成する
ctrl_interface=/var/run/wpa_supplicant
network={
    ssid="xxxxxxxxxxxxxxxxxxxx"
    psk=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    key_mgmt=WPA-PSK
}

ssidは繋ぎたい無線LANSSIDを入力する(クォートで囲む)。
pskには次のサイトにSSIDパスフレーズを入力して計算した16進ハッシュ値を書く(クォートで囲まない)。

(改行コードはLFにしたけどCRLFでも動くんだろうか?)

電源につないで起動する。

Raspberry Piへの接続

起動すると自動的にWifiに接続される。IPアドレスRaspberry Pi Zero Wにディスプレイを繋げていればそこに表示されるが、わからなければAdvanced IP Scannerなどを利用して調べる。ローカルIPは限られてるので総当たりでも何とかなるはず。

以下、Raspberry Piのアドレスを192.168.11.10とする。
パソコンからRaspberry PiSSHで繋げる。

ssh pi@192.168.11.10
  • Are you sure you want to continue connecting (yes/no/[fingerprint])と聞かれたら yesと入力してEnter
  • 初期パスワードraspberryを入力してログイン

初期設定

パスワード変更
passwd

現行のパスワード(raspberry)1回、変更後の好きなパスワード2回を入力してパスワードを変更する。


adminのパスワードを変更するのは、

sudo passwd admin

とする。

ソフトウェアを最新に更新
sudo apt update
sudo apt upgrade -y
タイムゾーンを東京にする
sudo raspi-config

で設定を開き、

  • 5 Localisation OptionsTimezoneAsiaTokyoFinish

参考: Raspberry Piの設定【raspi-config/言語・タイムゾーン・キーボードの設定】 - Aldebaranな人のブログ

IPアドレスの固定
sudo nano /etc/dhcpcd.conf

次のような内容を書き加えるか、コメントアウトを外して変更する。アドレスは環境に合わせる。ここではルーターのアドレスを192.168.11.1としている。

interface wlan0
static ip_address=192.168.11.10/24    
static routers=192.168.11.1
static domain_name_servers=192.168.11.1 8.8.8.8

参考: TCP/IP networking - Raspberry Pi Documentation

SSHを公開鍵認証だけにする

cd ~
mkdir .ssh
nano .ssh/authorized_keys

(ここで接続元のコンピュータの公開鍵を書き込む)

chmod 600 .ssh/authorized_keys
chmod 700 .ssh
SSHD設定
sudo nano /etc/ssh/sshd_config

コメントアウトしたりして次のような設定に変更する。書いてない部分はそのまま。

Port 22 #外部公開しないのでデフォルトで良し

LoginGraceTime 30
PermitRootLogin no
StrictModes yes
MaxAuthTries 3
MaxSessions 5

PubkeyAuthentication yes

IgnoreRhosts yes

PasswordAuthentication no
PermitEmptyPasswords no

参考: sshd_configの設定項目の理解を目指す | Unskilled?

センサーのはんだ付け

接続

f:id:nixeneko:20210501175928p:plain
f:id:nixeneko:20210501175942p:plain
(画像はFritzingで作成)

写真

f:id:nixeneko:20210501171446p:plain
f:id:nixeneko:20210501171307p:plain
面倒なので直にはんだ付けした。構成を変更したくなったら面倒だけど…。

参考: 5ドル!ラズパイ・ゼロ(Raspberry pi Zero)でIoT (16) ディジタル温度センサ1 1-Wire DS18B20 | 電子工作の環境向上

1-Wire有効化

sudo nano /boot/config.txt

/boot/config.txtを開き、

dtoverlay=w1-gpio

を追記、保存、再起動。

sudo reboot now

参考: 1-WIRE at Raspberry Pi GPIO Pinout

温度自動記録の設定

自動記録のコードを用意した。温度を読み取ってファイルに時刻と温度を書き込むだけのコード。
github.com

これを動かす。

cd ~
git clone https://github.com/nixeneko/record_temperature
git checkout v1.0
cp settings.py.template settings.py
ls /sys/bus/w1/devices/
nano settings.py 

/sys/bus/w1/devices/の中に温度センサのデバイスがあるので、使いたい温度センサ(たぶん28-で始まってるやつ)をメモして、settings.pyをそれに合わせて変更する。

自動実行の設定

cron.dに書く

sudo nano /etc/cron.d/record_temperature

で設定ファイルを開いて、

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

*/1 * * * * pi /home/pi/record_temperature/rec_temp.py

と書き込み、保存する。これは1分毎に記録する場合。*/1*/5にすれば5分ごとの記録になる。(*/1*と同じ。)

防水プラボックスの加工

f:id:nixeneko:20210501183207p:plain
屋外に設置するので防水プラボックス(今回使ったのはタカチ BCAP112107T)に入れる。大は小を兼ねるということで大きめにした、入らなかったら嫌なので。固定用の金具も購入。蓋が透明な奴にしたけど別に蓋が透明である必要なかったなと思った。

配線を通すために穴を開ける。

穴あけ

f:id:nixeneko:20210501183527p:plain
ドリルで穴を開け、穴をつなげてやする。

f:id:nixeneko:20210501183655p:plain

プラボックスにいれる

f:id:nixeneko:20210501184745p:plain
延長コード通す穴がでかすぎるので木材を切ってストッパーにする。動くと困るので接着剤で接着した。
f:id:nixeneko:20210501184118p:plain
f:id:nixeneko:20210501184201p:plain

シリコンシーラントで穴をふさぐ

f:id:nixeneko:20210501184830p:plain
これ本当にうまく行ってんのか謎。
ケーブルが上下に動くと普通にはがれるっぽいのでケーブルを固定してからやった方がいいっぽい。最終的に設置してからもう一回もりもりやった。

延長コードと遮断機付きのコードを繋ぎ、自己融着テープで絶縁・防雨対策

延長コードと遮断機付きのコードは適当に屋外でも使えそうなやつを通販で買った。室外機とかの屋外配線見る限りもっと簡易な作りのやつが使われてるような気がするんだけど…。

f:id:nixeneko:20210501185311p:plain

過電流遮断器なので短絡くらいしか検出できないけど、まあないよりマシかなあという感じ。漏電は配電盤の漏電遮断器が検出してくれると思う。

コード同士を接続し、自己融着テープで絶縁する。
f:id:nixeneko:20210501185422p:plain
自己融着テープは光に弱いらしく、ビニールテープでさらに巻いておくといいらしい。直射日光に当たらないのでそのままにしたが。

接地

2x4材を立てて、そこにタッピングネジでプラボックスを固定。
f:id:nixeneko:20210501190431p:plain
コードを屋外コンセントにつなぎ、電源を入れる。

インターネットからドメインでアクセスできるようにする

Dynamic DNS申込み

https://www.mydns.jp/ を使った。別にドメインが取れればなんでもいいと思う。

サービスに合わせて設定する。

IP更新

cd ~
nano update_ip.sh

を次の内容で作成する。

#!/bin/sh
wget https://ipv4.mydns.jp/login.html --http-user=ID --http-passwd=PASSWORD -O /dev/null

IDPASSWORDは自分のアカウントに合わせて変更し、保存する。

chmod +xしてcrontabで一日一回実行するように設定

chmod +x update_ip.sh
./update_ip.sh

IPの更新が動いているのを確かめる。

自動更新の設定

sudo nano /etc/cron.d/update_dynamic_dns

次の内容にして保存する。

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

12 5 * * * pi /home/pi/update_ip.sh

(環境変数の設定、とりあえず書いといたけど必要ないかも?)

これでドメインからIPが引けるようになるが、まだルータで止まってアクセスはできない。

参考: DDNSにBasic認証でIPアドレスを通知する - Qiita

ローカルからドメインでアクセスできるようにする

Raspberry PiDNSサーバーにする

Dnsmasqのインストール

sudo apt -y install dnsmasq 
sudo nano /etc/dnsmasq.conf

設定は、Raspberry PiのIPが192.168.11.10、ルータのIPが192.168.11.1ドメインexample.comの場合を書く。次の項目のコメントアウトを外し、serverはルータ(というかDNSサーバ)のIPに書き換える。

domain-needed
bogus-priv
no-resolv
address=/example.com/192.168.11.10
server=192.168.11.1

有効化する。

sudo systemctl restart dnsmasq
sudo systemctl enable dnsmasq

参考: ラズパイ(Raspberry Pi)とDnsmasqで作るDNSサーバ | Device Plus - デバプラ

ルータのDNSルーティング設定を行う

ルータの設定画面を開き、
詳細設定→DNSルーティング設定(ルータによって異なる)から

を追加。

これでローカルからもドメインでアクセスできるようになった。

気温のグラフを表示するWebサイト+CGIの設置

サイトのデータ置く用のユーザwwwを追加(この辺知識がないので何がベストプラクティスなのか分からない…)

sudo adduser --shell /bin/false www

適当に入力してユーザを作成。


サイトのデータを用意を次に用意したのでこれを使う。特定の日のデータをJSONで取得するCGIと、JSONで取得した温度データをChart.jsを使ってグラフにするHTMLファイルからなる。
github.com

用意。

sudo -Hu www /bin/bash
cd ~
git clone https://github.com/nixeneko/temperature_website
cd temperature_website/public/
cp settings.js.template settings.js
cp api/settings/settings.py.template api/settings/settings.py

Ctrl+D押してログアウト

HTTPサーバの設定

Apach2のインストールと設定

sudo apt -y install apache2
sudo nano /etc/apache2/sites-available/example.com.conf

設定を環境に合わせて次のようにする。

<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    ServerName example.com
    DocumentRoot /home/www/temperature_website/public
    <Directory /home/www/temperature_website/public>
        Require all granted
    </Directory>
    <Directory /home/www/temperature_website/public/api>
        Options +ExecCGI
        SetHandler cgi-script
    </Directory>
    <Directory /home/www/temperature_website/public/api/settings>
        Require all denied
    </Directory>
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

サイトやモジュールの有効化

sudo a2enmod cgid
sudo a2ensite example.com.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl restart apache2
sudo gpasswd -a www-data www

参考: Ubuntu 20.04にApache Webサーバーをインストールする方法 | DigitalOcean

ファイアフォールの設定(ufw)

sudo apt install ufw
sudo ufw default deny
sudo ufw allow 53
sudo ufw allow 80/tcp
sudo ufw limit 22/tcp
sudo ufw enable
sudo ufw status
sudo systemctl restart ufw

参考: ufwコマンドの使い方 - Qiita

ルータのポートフォワーディングの設定

80番ポートをRaspberry PiIPアドレスに!
→全世界に公開!


ドメインにアクセスすると次のような画面が表示される。
f:id:nixeneko:20210501222613p:plain

百葉箱の設置

センサー部分は百葉箱の中に突っ込む。どうせ家の蔭で日には当たらないので別にいらない気もするが。
もともとは防水プラボックスごと百葉箱のなかに突っ込むつもりだったが思ったよりでかかったので外に出すことにした。なのでもっと小さいやつで良かったかもしれない…

まともな百葉箱は高いのでレーザーカッターによるそれっぽいキットを使った。

この中型ウェザーシールド。

梱包はこんな感じで、側面の鎧戸(戸ではないが)だけ組み立てられた状態で来た。
f:id:nixeneko:20210501204048p:plain

仮組みしようとしたら手前の板の片方の穴が3mm下にずれていて嵌まらなかった。
f:id:nixeneko:20210501204154p:plain
…ので、ナイフで削って嵌まるようにした。

仮組。
f:id:nixeneko:20210501204608p:plain
手前の取り外せるようになっている窓が、扉みたいになっていないので、取り外しはやや面倒かもしれない。あまり頻繁に開け閉めしたい場合には向かないと思う。


2x4材に固定できるように背面板に穴を開け、接着し塗装した(塗装と接着を交互にやって上手い具合にやろうとした)。塗装は白の水性塗料を刷毛で2重~3重に塗り重ねた(塗料がなくなったのでやめた)。
f:id:nixeneko:20210501205340p:plain

防水プラボックスの上方、2x4材にネジとナットで固定し、底に開けた穴からセンサーを中に入れ、センサーを針金で固定した。
f:id:nixeneko:20210501210137p:plain

ついでにケーブルをナイロンクランプで固定している。
蓋をして完成。
f:id:nixeneko:20210501211020p:plain

材料費

合計17,600円程度。

他に送料、工具類、固定用のネジ類、2x4材等などは書いてないが5千円はかかってるかもしれない。
百葉箱除けば1万円しないと考えると、いい感じかもしれない。

感想

要素要素の技術は大したことない(というか、誰かがしっかり舗装してくれているのでそれを辿るだけでいい)が、組み合わさるとなんかすごいことやってる気がしてくる。

温度センサの誤差が±0.5℃なので結構大きめ。もっと高精度のやつが使えればいいんだけど、防水プローブに入ったやつが見つからなかったのでどうすればいいのかよくわからない。直接雨が当たらなければ別に良さそうな気もするが。

ラズパイはLinuxなので楽だが、Wifiと電源がないところでは使えないので、設置場所が限られる。バッテリーで数ヶ月動くデータロガーが作れればいいんだけど。

Windows UpdateでWindows 10を20H2に更新しようとしたら途中で止まった

まとめ: Windows 10をバージョン1909以前から20H2へアップデートする場合、Windows 10更新アシスタントを使う。

問題の説明

Windows Update配信されたアップデートで20H2に更新しようとしたら、61%になってから動かなくなった。半日放置しても変化なし。
ドライバなどを最新に更新したりしてやり直してみても同様。

どうやらWindows 10のバージョンが古かったので、更新でエラーが出てたらしい?
しかし何でこんな古いものがずっと動き続けてきたんだろうか。自動でアップデートがなされてても良さそうに思うが…。サポートが切れるから早よ更新しろ、という通知が来たので、更新されてないことに気付いた。

環境

  • パソコン ASUS UX310UQ
  • 更新前OS: Windows 10 バージョン1709
  • 更新後OS: Windows 10 バージョン20H2

解決策

バージョン1909以前から、バージョン20H2にアップデートする場合、Windows 10更新アシスタントを使うのが推奨されているらしい。

上のページから、「今すぐアップデート」をクリックして更新アシスタントをダウンロードする。

Windows Updateが動いているとインストールできないし、一方で勝手にWindows Updateが動き出すので、

  1. まず機内モードにして再起動し、
  2. Windows 10更新アシスタントを実行してから機内モード解除、

とすることで動いた。

そのまま無事更新された。よかった。

GASを使って作業開始・終了時刻記録Webアプリを作った話

概要

Google Apps Script (GAS)を使って、自分で使うための、作業開始・終了時刻を記録するWebアプリを作成した。

f:id:nixeneko:20210311005657p:plain
作成したアプリ
この記事にあるもの
  • GASによるWebアプリ作成の流れ
  • 作ったアプリのコード
ないもの
  • コードの書き方・解説

モチベーション

  • 作業時間(開始時刻, 終了時刻)を記録したかった
  • 開始時と終了時にボタンを押すだけで記録できるとよい
  • スマホで使えると良い

これを満たすのに、勤怠管理システムが使えるかと思い、無料で使える勤怠管理Webサービスを使っていた。しかし、一日に2回以上の勤怠データを登録することができず、自分の使いたい目的では使えないことが分かった。

そのため、自分で使うためのものを作ることにした。

要件

  • 「出勤」「退勤」ボタンが表示される(実際には「作業開始」「作業終了」だが長いのでこうした)
  • ボタンを押すとその時の時刻およびボタンに対応する「入」「退」のどちらかが記録される
    • 時刻の記録だけに特化する。作業時間の計算等は別にプログラムを書いてどうにかする

GASとは

Google Apps Script (GAS)とは、Googleサービス等と組み合わせて使える、Javascriptをベースとしたスクリプト言語で、Googleドライブとかで実行できる。また、GASを使ってWebサービスを開発したり公開したりできる。

Webサービスであればスマホで使えるのでいいだろうということでGASで作ることにした。

実装方針

  • 勤怠時刻データはそれ用のGoogleスプレッドシートを作成しそこに保存する
  • GASによるWebサービスでは、HTTPリクエストを受けると決められた関数がトリガーされる。GETとPOSTで別の関数となっている:
    • GET (doGet関数)では、ボタンを表示するページを返す
    • POST (doPost関数)では、時刻の記録を行う。記録した後はGETにリダイレクトする(POSTでページを表示した場合、リロードすると再度記録されてしまうため)

実際にやって動かしてみる

まず、Googleドライブを開く*1

記録用スプレッドシートの用意

先に記録用のスプレッドシートを用意する。

「新規」→「Google スプレッドシート」をクリック
f:id:nixeneko:20210310221702p:plain

  • スプレッドシートのタイトルを変更(任意)
  • 見出しを適当に入力。ここでは「時刻」「入退」「備考」にした。
  • シート名を入力。シート名は何でもいいがここでは"time_log"とした。後で使う。
  • スプレッドシートのIDをコピーしておく。後で使う。
    • スプレッドシートのURLがhttps://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXX/editみたいな感じになっているので、XXXXXXXXXXXXXXXXXXXXの部分がIDになる。

Apps Scriptの作成

「新規」をクリック*2→「その他」→「Google Apps Script」をクリック
f:id:nixeneko:20210310215715p:plain

エディタが開くので、「無題のプロジェクト」と書かれている部分をクリックしてプロジェクトの名前を適当に変更する(任意)

実行の流れとしては、コードを変更する→保存する→デプロイする→ページを開いて確認という流れになる。

動作確認

最初にWebアプリを動かしてみる。エディタに次のコードを入力する。

function doGet() {
  const html = '<!DOCTYPE html><html><head><base target="_top"></head><body><h1>Hello World!</h1></body></html>';
  return HtmlService.createHtmlOutput(html);
}
デプロイ

保存ボタン「💾」をクリック、あるいはCtrl+Sを押す→「デプロイ」→「新しいデプロイ」をクリック
f:id:nixeneko:20210310225542p:plain

「種類の選択」の右の歯車「⚙」をクリック→「ウェブアプリ」をクリック
f:id:nixeneko:20210310225656p:plain

「デプロイ」をクリック
f:id:nixeneko:20210310225923p:plain

すると、ウェブアプリのURLが表示されるのでコピーして控えておく。後で使う。
f:id:nixeneko:20210310230247p:plain

ウェブアプリのURLを開くと次のように表示される。
f:id:nixeneko:20210310230440p:plain

コードを変更した際は、まず保存し、「デプロイ」→「新しいデプロイ」→「デプロイ」を繰り返すと変更が反映される。*3

アプリを動かしてみる

さて、こちらに作ったアプリを用意した。これを動かしていく。

「ファイル」の右の「+」をクリック→「HTML」をクリック
f:id:nixeneko:20210310231515p:plain

名前を「index」と入力して確定
f:id:nixeneko:20210310231609p:plain

index.htmlのコードを消して次の内容に書き換える:

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style type="text/css">
      button {font-size: 15vw; height: 30vw; width: 45vw;}
      input {width: 80vw;}
    </style>
  </head>
  <body>
    <h1>タイムカード</h1>
    <form action="<?= app_url ?>" method="POST" name="f">
      <div>
        <button type="submit" name="action" value="in" <?= in_disabled ?> >出勤</button>
        <button type="submit" name="action" value="out" <?= out_disabled ?> >退勤</button>
      </div>
      <div>備考: <input name="comment"></input></div>
    </form>
    <div>最終: <?= last_time_log ?></div>
  </body>
</html>

コード.gsの内容を次のコードに書き換える。spread_sheet_id, sheet_nameの値は先ほど控えた値に変更する。

const spread_sheet_id = "ここにスプレッドシートのIDを入力"; //記録するスプレッドシートのID
const sheet_name = "time_log" //スプレッドシートのシートの名前

const app_url = ScriptApp.getService().getUrl(); //このアプリのデプロイURL

function getSheet(){
  const spreadsheet = SpreadsheetApp.openById(spread_sheet_id);
  const sheet = spreadsheet.getSheetByName(sheet_name);
  return sheet;
}

function getLastRow(){
  let sheet = getSheet();
  let last_row_idx = sheet.getLastRow();
  let values = sheet.getRange(last_row_idx, 1, 1, 3).getValues()[0];
  return values;
}

function addLastRow(time_str, in_out, comment){
  let sheet = getSheet();
  let last_row_idx = sheet.getLastRow();
  sheet.getRange(last_row_idx+1, 1, 1, 3).setValues([[time_str, in_out, comment]]);
}

function formattedDate(date){
  return Utilities.formatDate(date, "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss");
}

function doGet(e) {
  let html = HtmlService.createTemplateFromFile('index');
  html.app_url = app_url;
  let date, in_out, comment;
  [date, in_out, comment] = getLastRow();
  let date_str;
  if (date instanceof Date) {
    date_str = formattedDate(date);
  } else {
    date_str = date;
  }
  html.last_time_log = date_str + " " + in_out + " " + comment;
  if (in_out == "入"){ //最後が出勤なら退勤のみ表示
    html.in_disabled = "disabled";
    html.out_disabled = "";
  } else {
    html.in_disabled = "";
    html.out_disabled = "disabled";
  }

  return html.evaluate()
              .addMetaTag('viewport', 'width=device-width, initial-scale=1, user-scalable=no')
              .setTitle('タイムカード');
}

function doPost(e){
  let action = e.parameter.action;
  let now = new Date();
  let date_str = formattedDate(now);
  const action_to_japanese = {"in": "入", "out": "退"};
  let in_out = action_to_japanese[action];
  if (!in_out) throw new Error('POSTパラメータが異常です'); //不正な呼び出し
  let comment = e.parameter.comment;
  addLastRow(date_str, in_out, comment); //書き込み

  // GETにリダイレクトする
  return HtmlService.createHtmlOutput(
    "<script>window.top.location.href='"+ app_url + "';</script>"
  );
}

入力したら保存し、デプロイをする。
(2021-10-11追記: デプロイURLはScriptApp.getService().getUrl();で取得できることがわかったのでそのようにコードを変更した)

認証

デプロイをしようとすると、スプレッドシートにアクセスするために認証が必要になる。

「アクセスを承認」をクリック
f:id:nixeneko:20210310232713p:plain

ウィンドウが開くので今使っているアカウントを選択
f:id:nixeneko:20210310233040p:plain

「このアプリは Google で確認されていません」と表示されるので「詳細」をクリック
f:id:nixeneko:20210310233341p:plain

「time recorder (安全ではないページ) に移動」をクリック(time recorderは設定したプロジェクト名)
f:id:nixeneko:20210310233612p:plain

「許可」をクリック
f:id:nixeneko:20210310233738p:plain

これでデプロイができる。一度やればOK。

ブラウザで開いて確認

次のような画面になる。スマホのサイズでちょうどいい感じ(だがパソコンで開くとすごいバランスになる)。
f:id:nixeneko:20210311005657p:plain

ボタンを押すとスプレッドシートに時刻が書き込まれることが確認できる。
f:id:nixeneko:20210311013206p:plain


これで完成。デザインは適当だが自分しか使わないのでヨシ!
スマホでアプリのURLを開けば実行できる。アプリを作成したのと同じGoogleアカウントでのログインが必要。
ちょっと読み込みが遅い気がする。


ほか工夫点。

  • 入退は交互に来るはずなので、ボタンは交互にグレーアウトするようにした。
  • 作業内容等を書けるようにコメント欄も作った。
  • スプレッドシートの最終行=最後に記録された内容を表示するようにした。

GAS引っかかりメモ

Googleアカウントを切り替えられない?

エディタを開く際、同時に複数のGoogleアカウントにログインしている場合、メインのアカウント以外に切り替えることができないようで、使いたいアカウント以外をログアウトする必要があるっぽい?

デプロイする前に保存するのを忘れて変更が反映されない

よくある。保存してデプロイし直す。

WebアプリのHTML内のJavaScriptから、ブラウザのアドレスバーにあるWebアプリのURLを取得

無理っぽい。iframeに入っていてかつクロスドメインになっているため。なぜ…

タイトルやmetaタグの設定

GASのWebアプリで表示されるページは、二重にiframeが入ったその中にアプリで設定したHTMLが表示される。そのため、HtmlOutputを作成する際のHTMLの中でタイトルやスマホ対応のviewportを指定するmetaタグを設定しても効果がない。

代わりにHtmlOutputオブジェクトの.setTitleメソッド、.addMetaTagメソッドを呼び出して、それぞれタイトルとメタタグを設定する。メソッドチェーンもできる。

return HtmlService.createTemplateFromFile('index').evaluate()
              .addMetaTag('viewport', 'width=device-width, initial-scale=1, user-scalable=no')
              .setTitle('タイムカード');
リダイレクト

window.top.location.hrefにリダイレクト先のURLを代入するJavascriptコードを実行するHTMLを返せばいいっぽい。

  return HtmlService.createHtmlOutput(
    "<script>window.top.location.href='"+ app_url + "';</script>"
  );

参考: google apps script - Automatically Redirecting to a Page - Stack Overflow

(2021-09-03追記: <iframe>sandbox属性にallow-top-navigation-by-user-activationが指定されているためリダイレクトが動かなくなった。これどうしようかな…。)

まとめ

  • 自分だけ使えるWebアプリをタダで作って使えるので便利*4
  • Googleドライブ上のデータを読み書きできる(認証が必要)
  • 自分しか使わないので必要なものを最低限だけ実装すればいい
    • コーナーケースに対するエラー処理もしなくていいかもしれない(運用でカバー)
  • 100行もコードを書かずにできた

*1:使いたいアカウント以外をログアウトしておく必要があるかもしれない。

*2:画像ではマイドライブ▼をクリックしてるけど出るメニューは同じ。ファイル一覧のペインを右クリックしてもよい。

*3:エディタで「デプロイ」→「デプロイをテスト」とすると、最新のコードをテストできるURLが得られるので、開発に有用かもしれない。

*4:設定次第で、他人に使わせたり、一般公開することも可能。