2022/12/25

wslで動かしたdockerは全部閉じても動いていた

Windows10 でも wsl2 は動く。
ファイルシステムがどうなっているのかは調べていないが、lmdb も動いたことだしなかなか動いてくれそうだ。

 

で、最近お仕事で docker のコンテナを動かすことがあり、wsl 上の Ubuntu 20.04 でどうなのか気になっていた。
起動するときはコンソールを立ち上げるが、そのコンソールが閉じたときにどうなっているのだろうか?

 

docker だとわかりにくいと思ったので、 watch でフォアグラウンドではあるが動かし続けた。

image

う^-ん?

止めてみよう。

image

変わらんな。。。

慎重に見ればまた違ったのかもしれないが、タスクマネージャーのレベルでは現れなさそうだ。
dockerコンテナでも同じようなことをしてみたが、ちょっとわからんかった。

 

「詳細」を見てみると、wsl.exe が 2つ、wslhost.exe が複数、 wslservice.exe が 1つ立ち上がっていた。

image

wslhost.exe は 6つ立ち上がっていたのだが、たぶんここには現れないのではないかな。

もちろん ps で見ると複数立ち上がっているので、動いていないわけではない。

まあ、Windows の動作にとって wsl が動いているというのは考慮すべきではないというか、うちはうち、よそはよそ、というところなのではないかと思っている(個人の感想です)。
Unix/Linux 的な環境で本番運用するのであれば、わざわざ Windows が動いている必要はないのだ。Windows の立ち位置としては開発環境の提供くらいなものだろう。

 

なので Windows 側から wsl で動いているプロセスが見えないのは、そんなものだろうと思っている。
ただ・・・あのペンギンはなんとかならなかったのだろうか。

image

怖いよ。

2022/12/24

nvs autoとcdするスクリプトは相性が悪そうだ

node.js の切替に nvs を使っている。

https://github.com/jasongin/nvs

nvs auto on しておくと、.node-version に使用したい node.js のバージョンが書いてあれば cd したときに自動で切り替わる。

 

私は Ubuntu を使っているのだが、デフォルトの node.js は v12.22.9 になっていた。
よく使うのは v16系なのだが、グローバルでインストールするようなコマンドも意識的に使っていないのでデフォルトのままにしている。
何か実装したり動かしたりするときに nvs use で指定しているのだが、たまに忘れてデフォルトのまま使ってしまい、なんか動きがおかしいなぁ、というところで気付いたりする。

そういうのを避けるために nvs auto on と .node-version は便利そうなのだ。


ただ、私の環境だと .bashrc で nvs auto on しているとなんか変だったのだ。
勝手に知らないディレクトリが作成されたり、エラーで mkdir の使い方が間違っていると出てきたり。

そして先ほど、.bashrc がこうなっているのに気付いた。

  • NVS_HOME などの設定
  • nvs auto on
  • GOPATH を設定するスクリプトの実行

よろしくなかったのは最後の GOPATH を設定するスクリプト。
ここ最近 GOPATH が必要なプロジェクトの作業ばかりしていたので GOPATH を設定するスクリプトを作って source コマンドで読み込ませていた。

#!/bin/bash

export GOPATH=$(cd $(dirname $BASH_SOURCE); pwd)
export PATH=$GOPATH/bin:$PATH
mkdir -p $GOPATH/src $GOPATH/pkg $GOPATH/bin

GOPATH をあちこち置いていたので、このスクリプトファイルを置いて読み込めばそのディレクトリを GOPATH になるようにしていた。

スクリプト中で cd しているところがあるのだが、これによって nvs auto on が作動してしまうようなのだ。
cd だから作動するのは当たり前なのだが、 .bashrc でこのスクリプトを実行することと nvs auto on の組み合わせによってターミナルを新しく開いたとき、特に vscode で開いたときの挙動がよくなかったようだ。

 

まあ、これは .bashrc の中でこういうスクリプトを走らせる方が良くないな。
スクリプトの場所=GOPATH の場所なので、直接そう書けば良いだけなのだ。あるいは nvs auto on の前に呼び出すようにするかだ。

2022/12/04

[wsl2] lmdbは動くようになった

4年ほど前、こういう記事を書いていた。

hiro99ma blog: [lmdb][win10]Windows10 1803でWSLのファイル書込みがうまく動いてない?(2018/05/05)
https://blog.hirokuma.work/2018/05/lmdbwin10windows10-1803wsl20180505.html

あれから、WSL2 といってよいのかな? 正式版になったそうだ。

ASCII.jp: Microsoftストア版WSLが正式版になり、Windows 10でも動作可能に (1/2)
https://ascii.jp/elem/000/004/114/4114859/

image

Windows10 Pro にインストールして使ってみた。

Ubuntu 20.04 は今回新規だが、WSL についてはUbuntu on Windows の時代から同じ PC を使っているので、まっさらな状態から試すわけではない。
・・・この PC ももう6年も使ってるんだな・・・。

 

gist に作っていたサンプルコードを動かした。

$ ./tst
test1(): OK
test2(): OK
  key(5)=test1, data(5)=TEST1
  

エラーになってないということは大丈夫になったのかな?
ファイルアクセスが遅いとかで見直しが行われるようなことをどこかで読んだ気がする。

 

VirtualBox などではなく WSL が使えると、ストレージにあらかじめ確保されるサイズがなくなると思うので、あまり空き容量がない私としてはとても助かるのだ。
いや、そうだろうか?

WSL 2 仮想ハード ディスクのサイズを拡張する | Microsoft Learn
https://learn.microsoft.com/ja-jp/windows/wsl/vhd-size

確保するやん。
探すと、確かに ext4.vhdx というファイルがある。

image

df で見ると 2GB に近いのは /dev/sdc だろうか。

/dev/sdc       1007G  1.7G  955G   1% /

にしても、この PC は 1007GBもないし、よくわからん。

2022/12/03

[vbox] VirtualBox が急に動かなくなる

Windows11 で VirtualBox v7.0.4 + Ubuntu 22.04 を動かしているのだが、なんか急に操作を受け付けなくなることに気付いた。
ホスト側も重たくなってしまっている。

こちらの人はゲスト OS で Windows10 を動かしているようだが、こちらと同じような感じだ。

https://forums.virtualbox.org/viewtopic.php?f=6&t=107712

VBox.log を見るとこんなのが出ていて応答しなくなったようなのだ。

VMMDev: vmmDevHeartbeatFlatlinedTimer: Guest seems to be unresponsive. Last heartbeat received 4 seconds ago

あきらめて v6 にダウングレードした。
まあ、最初は仕方ないよねー

 

2022年12月3日 20:28追記

と思っていたのだが、ダウングレードしても応答しなくなるようになってしまった。
もしかしたら、いまやっている docker buildx build でやっている作業が原因なのか??

お仕事で docker buildx build する作業をやっているのだが、VirtualBox でやるのは初めてなのでこれが原因かどうかを判断できない。ありがちだが、ダウングレードしても前のファイルか何かが残っていて変になってしまったという可能性もありそうだ。アンインストールって、いろいろな可能性を考えてしまってファイルを残しがちになるので難しいよね。

 

・・・docker の線は消えた。
ゲストOS を立ち上げて vscode を立ち上げて放置していただけだが固まってしまった。
方式が違う VM の重ね合わせになるからかも、なんて考えていたが違ったようだ。

vscode が原因と言うことはないよな・・・?
ただ最近は direnv hook とか使ったり他にも設定し直したりしたので、vscode が直接ではないにしてもなくはないのか。
あとは、長い時間放置した後しか見ていないのでスリープみたいな電源制御もちょっと気になる。

わかるのは VM の CPU負荷が高いということだけで、本当にゲストOS 側でプロセスが立ち上がって非常に重たくなっているだけかもしれないのだ。

 

2022年12月4日 21:17追記

まだ現象が発生する。

いま疑っているのは kernel だ。
私の環境だと最新は GNU/Linux 5.15.0-56-generic x86_64 なのだが、インストールされている1つ前の GNU/Linux 5.15.0-53-generic x86_64 だと発生しない気がするのだ。

 

ブートローダで切り替えるのが面倒なのでデフォルトを 5.15.0-53 にしたかったのだが、どうにもうまくいかない。。。
/boot/grub/grub.cfg が最終的には使われているようだが、/etc/default/grub を変更して update-grub してもうまくいかんので、あきらめて apt remove/purge で削除した。

これで改善すると良いのだが。

 

2022年12月4日 22:24追記

あれから1時間が経過した。
未だに止まる気配がないので、これは成功したんじゃなかろうか。

りゆうはよくわからないが、VirtualBox と Linux 5.15.0-56 が相性が悪いとか、VBox Additions をやったことで相性が悪くなるとか、そんなことかもしれん。

む、ということは VirtualBox 7 にしたこと自体は影響がなかったのか。
元に戻そう。

 

2022年12月4日 23:41追記

VirtualBox 7 に戻して1時間が経過した。
未だに止まる気配がないので、やはり VirtualBox のバージョンには関係が無かったのか。
すまん、疑って。

2022/11/20

[vbox] VirtualBox7にするとGuest Additionsのアップグレードがあった

VirtualBox のバージョン 7 がリリースされていた。

https://www.virtualbox.org/

アップグレードして Ubuntu を起動し、さあ Guest Additions をアップデートしようと CDイメージの挿入をしようとしたところ、メニュー項目が増えて「Upgrade Guest Additions...」があった。

選択するとアップグレードされるようだった。画像はアップグレード後に撮影したのでグレーアウトされているが、ちゃんと選択できるようになっていたのだよ。

image

CDイメージを挿入しても大した手間ではないとはいえ、面倒であることに変わりは無かったから助かる。

2022/11/13

[npm] systemctl で npm を動かすのは面倒そうだ

お仕事で JavaScript というか TypeScript というか、node.js で動くアプリを作った。
サーバみたいなもので、ずっと動いていてほしい。

となるt、systemd というか systemctl というか、そういうやつで動いていてほしい。


最初は node.js へのフルパスを書いて、直接パラメータなども書いて動かしていた。
が、パラメータの種類が増えて、最後の方になると TypeScript のトランスパイルも行いたくなったので npm で pre などを付けて開発をしていた。

そこまでくると、node.js ではなく npm の方を使って systemctl で実行して欲しいと思うのが人情だろう。

npm も同じようにフルパスで書いてあげると、それは systemctl で動いた。
しかしそこから node を実行するようにしていると、そちらは PATH が通っていないために実行エラーになってしまった。
systemctl が動くユーザでも動くような PATH に置いてあれば良かったのだが、node は nvm とか nvs のようなバージョン管理している方のパスに置いていたのだった。
まあ、動かんよね。

 

検索してみたものの、あまり対策はなさそうだった。

  • node はパスが通ったところに置く
    • そうするとバージョン管理している意味が無いよね
  • スクリプトファイルを置いてそちらに全部書く
    • まあこっちが妥当なのかな
  • node をフルパスで書いて npm を使わない

というわけで、今回は 最後のパターンを使った。
systemctl みたいな権限を使って実行するのだったら、パスの回避だけでスクリプトを作りたくなかったというところか。スクリプトで実行すること自体に意味があるんだったらやったのだけど、今回はそうじゃなかったからなぁ。

2022/11/05

[win11] エクスプローラの上側が大きい

うまく表現できなくて済まない・・・。

image

Windows Update をしたら見た目が変わっていた。
タブが増えて、使わないツールバーだけでも広かった領域がさらに広々してしまった。
表示を小さくできないだろうか。

 

困ったときの Explorer Patcher !!

image

という設定にするとここまで小さくなった。

image

そうそう、このくらいがありがたいのだよ。

フォルダーオプションがなくなってしまったのだが、それは Open Shell のツールバーに追加してしまえばよさそうだ。
以前は Open Shel のツールバーはアイコンが右側に寄っていたのだが、Windows Update してからか左側に寄って表示されるようになった。まあ、そのくらいは気にしなくてよいか。

2022/11/03

[win11]ディスプレイ間でカーソルを簡単に移動させる

うちはトリプルモニタだ。
すごそうに聞こえるかもしれないが、昔使ったモニタを捨てられずに使っていたり、Raspberry Pi 用に買ったものをつないだりした結果である。

image

ディスプレイの配置は Windows10 のときから変えていなかったのだが、カーソルの動きが違った。
1番から3番にカーソルを移動するとき、3番の範囲にない位置から下げても 3番のモニタにカーソルが出てくるのだ。

私はトラックボールで適当にカーソルを動かすので、1番から2番に移動するときに斜めになって2番にたどり着く前に 1番の画面下にたどり着くことがしばしばある。
今までだとカーソルが下を滑って2番にたどり着いていたのだが、今の設定だと3番に飛んでしまうのだ。
そして 3番から 1番に上げても、それは 3番のあった位置の真上にカーソルが戻ってしまう。

説明がわかりにくいので図にすると、赤のようにカーソルを滑らせて青のように進んでほしいのだが、実際は緑色のようになってしまうのである。

image

 

対処はすぐ見つかった。
設定画面のディスプレイのところに項目がある。

image

これにチェックが入っていたので外せば今までのような移動に戻った。
よかったよかった。

Windows11 にアップグレードした

会社の PC を新調したら Windows11 Home だった。

なんとかなるかなー、と思っていたが、意外と何ともならないところが多かった。
「何ともならない=素のままでは使うのに手間がかかると私が考える」であって、Windows11 がどうのこうのは知らん。
あくまで主観だ。

会社だけだと使い勝手をよくするために時間が取りづらいので、家の PC も Windows11 Pro にアップグレードした。以前が Windows10 Pro だったのでそのまま Pro が引き継がれたようだ。
会社の PC は新規だったので Home だが、それまで使っていた PC が Windows10 Pro だからアップグレードだけ持って行けないか調べたけどよくわからんかった。元々 Windows10 Home から Pro にアップグレードしたものだからプロダクトキーがあればよさそうだけど、忙しくて探す暇が無いのよねぇ。

 

コンテキストメニュー

Explorer で右クリックすると出てくるコンテキストメニューだが、Windows10 まではいろいろとツールが登録したメニュー項目が出てきて邪魔だと思われたせいか、Windows11 ではシンプルなものが最初に表示されるようになった。
Windows10 風のメニューを表示するにはメニューからクリックするか、最初からシフトキーを押しながら右クリックするかの操作になる。

最初はそのまま使っていたのだが、シンプルなコンテキストメニューの内容を使うことが無いので毎回1クリック操作が必要だった(そのときはまだシフトキー+右クリックが使えなかった)。

image

この中でよく使いそうなのって新規作成とプロパティくらいしかなさそうだ。それも「かろうじて」くらいなレベルである。昔に戻したいという気持ちも分かってもらえるだろう。

 

レジストリを編集してやればできるそうだが、今後もしばしばやり直すことになりそうなのでツールを使った。

Windows 11の秩序ある右クリックメニューを混沌へと戻す「Windows 11 Classic Context Menu」 - Windows 11で絶対使いたいアプリ - 窓の杜
https://forest.watch.impress.co.jp/docs/serial/win11must/1363937.html

タスクバー

タスクバーのメニューを左端に移動することはできたが、なんだかなんだかな感じだ。

こちらのツールでいろいろ変更できる。

GitHub - valinet/ExplorerPatcher: This project aims to enhance the working environment on Windows
https://github.com/valinet/ExplorerPatcher

コンテキストメニューの件もこのツールでできそうだが、まあよかろう。

タスクバーの高さを低くできたのがありがたい。Quick Launch を使っているのだけどそのままだと妙に高さが合わなかったのだ。
あと、このツールのおかげかどうか分からないが時計のカスタマイズに TTClock が使えるようになったのも助かっている。

こういったアイコンの表示も消すことができる。
歯車アイコンって私の PC だと使うものが無いから消したかったのだ。

image

右側の吹き出しアイコンも使い道がない。 Windows10 だと設定を開いたりスクリーンショットを撮るアプリを起動するために使っていたのだけど Windows11 ではどちらもない。カレンダーは見ないし、通知は再起動とかアプリのインストールが出てくるくらいなので用途を見いだせない。
が、一番右端に時計が来ると妙に見づらいのだ。なんでだろう? まあいいや。

 

昔は、コンテキストメニューの整理や新規作成する項目の整理を頻繁にしていたのだが、もうあきらめた・・・。やってもやっても復活してくるので心が折れたのだろう。
そういう点ではコンテキストメニューの方式が新しくなったのはよいと思っている。ユーザが表示したいメニューをカスタマイズできるようになるとありがたいのだが。アプリによってはそっちのメニューにも追加されている。ターミナルとか DropBox なんかは出てきてたので、できなくもないのだろう。

 

メニュー

左側に移動したメニューアイコンだが、メニュー自体は Windows10 から使っている Open Shell がそのまま使えている。

Welcome to the Open-Shell-Menu | Open-Shell-Menu
https://open-shell.github.io/Open-Shell-Menu/

このツールにはいつもお世話になっております。

メニューだけでなく Explorer にツールバーを出すことができて、そこで表示の切り替え(小アイコン、詳細などのあれ)や非表示ファイルを表示させたりのアイコンを置くことができて便利だ。


こんな感じでツールを使いながらどうにかこうにか Windows11 で生きていけそうだと思っている。

使う頻度が高い Explorer の表示がまだちょっと慣れないかな。
なんでこんなにフォルダの左側にスペースが必要なんだろう、とか。

image

ファイラーを別途導入するのも考えた方が良いのか。

2022/10/15

[git] LFS を使ってみたい。できれば Google Drive で。

GitHub で Android のプロジェクトを管理していたのだが、その中に他で作った AAR ファイルも管理していた。
頻繁に変わるからバージョン管理が面倒になって追加していたのだが、git clone するときにかなり負担になってしまうことに今さら気づいた。遅くなるだけならまだしも、時間がかかりすぎるためか切断されて失敗することすらあるのだった。

git にバイナリファイルを別扱いにするしくみがあったような気がする、ということで調査し始めた。
別の場所に置くとしてもそんなに候補がないので、できれば Google Drive に置いておきたい。そしてプライベートリポジトリでもできるようにしたいし、Github Actions でビルドするときに使えるようになっていてほしい。

要望が多いね。


まずはここか。

About large files on GitHub - GitHub Docs
https://docs.github.com/ja/repositories/working-with-files/managing-large-files/about-large-files-on-github

LFS というしくみは GitHub が進めていたようだった。
とりあえず、書いてあるとおりにやってみる。

https://github.com/hirokuma/lfstest/blob/c0a6e955f949a213101aa78bf6fe3a4522ae1bc6/app-debug.apk

image

LFS 領域?に置かれたようだ。
Settings からもそうなっている。

image

Windows からできたので他の環境でもたぶんできるだろう。


これだと GitHub の LFS を使うようになっている。
無料で使うには容量に制限があるので、これを Google Drive に置きたいというわけだ。 Free版だと 2GB までなので Google Drive の無料版と変わらんのだが、お仕事では Google Workspace を使っているのでまだ余裕がある。

GitHub を使ってるのなら GitHub の LFS しか使えないのでは?と思ったが、説明には Dropbox や Google Drive も書いてあるので、きっとできるのだろう。

Git Large File Storage でのコラボレーション - GitHub Docs
https://docs.github.com/ja/repositories/working-with-files/managing-large-files/collaboration-with-git-large-file-storage

.gitconfig に lfs.url を設定すると、そこに対して LFS API でうまいこと push なり pull なりしてくれるらしい。

Git LFSの使用方法 – Backlog ヘルプセンター
https://support-ja.backlog.com/hc/ja/articles/360038329474-Git-LFS%E3%81%AE%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95#LFS%E3%81%AE%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9

こちらを使うと Dropbox や Google Drive をインストールしている環境でローカルのフォルダと同期するようであればプラグインのようなものを使うことで同じようなことをしてくれるのだと思う。

sinbad/lfs-folderstore: git-lfs custom transfer adapter which simply uses a folder as the remote LFS media store (e.g. a shared NAS folder)
https://github.com/sinbad/lfs-folderstore

これはこれで使えそうなのでやっておく。
既存のリポジトリを使う場合は面倒そうなので新規でやった。

https://github.com/hirokuma/lfsdrive2/blob/fee886e081b7c1cd867a4db81d00fbde0f106439/app-debug.apk

ポインタファイルになっていた。Settings を見ても Git LFS Data はそのままだ。
このテストでは Google Drive をローカルドライブでマッピングしているフォルダを指定したのだが、そこにはフォルダがいくつか作られ、oid sha256 に書いてあるハッシュ値と同じファイル名で同じサイズのファイルが置かれていた。「G:\マイドライブ\lfstest」みたいなフォルダ名だったが問題ないようだ。

この方式はローカルで作業する分には問題ないと思う。チームで作業するときも同じ Google Drive などのフォルダにアクセスできるなら各人に git config で設定してもらえばよいだけだろう。
もちろん、GitHub Actions でダウンロードなどはできない。がんばればできるのかもしれんが、Google Drive へのアクセスを許したりダウンロードさせたり しないといけないだろうからほぼ無理だと思う。それくらいだったら直接 Google Drive からダウンロードする方が楽だ。


そうだ、直接ダウンロードすれば良いのだ。

私が期待したのは git でバイナリファイルそのものの履歴管理はしなくてよいが、それを別のところからダウンロードするしくみがあって、そのダウンロード先については履歴管理してほしい、というわがままなものだ。
そういうことをしたかったら API を使って curl でアクセスするようなものの方がやりやすいと思う。ダウンロードするだけしかしないのだったら Git のフック機能を使って clone やら checkout やらのタイミングでダウンロードすればよいのではなかろうか。

Google Drive上の公開されたファイルをダウンロードする - Qiita
https://qiita.com/tanaike/items/f609a29ccb8d764d74b3

なんでもかんでもダウンロードするのもなんだから、ファイルがなければダウンロード、あってもハッシュ値か何かが不一致ならダウンロード、みたいなのを仕込むことになるだろう。

ただ・・・Windows と Linux, Mac とではスクリプトが共通化できなさそうだ。 curl は Windows10 でも使えるようになったが sha256sum はないし、あったとしても戻り値を比較するのは文法が違いそうだ。それなら Git for Windows をインストールしたときに使える bash.exe を使って Linux系のスクリプトで共通化させるのがよさそうだ。

 

git hook はリポジトリに埋め込むことができないのかな。。。
それだったらダウンロードするようなスクリプトを一緒に commit しておいて、 clone なり pull なりした後は実行してね、でよいかもしれん。忘れそうだけど。

https://github.com/hirokuma/lfsdrive3

これは、main ブランチで README の commit までやったあと、another-apk というブランチで app-debug.apk を差し替えてプルリクしてマージされ、そのあと git pull で mainブランチを更新する、というシナリオだった。

image

git pull は fetch と merge をまとめたコマンドだから、post-merge にも同じように仕込まないといけない。

image

こちらは git pull でダウンロードが行われた。

もうちょっと格好良くできそうな感じもするのだが、まあこのくらいできればよいかな。

2022/09/11

[android] ブート後に intent を受け取る

AlarmManager の章にデバイスの再起動後にアラームを開始したい場合について書かれている。

デバイスの再起動時にアラームを開始する
https://developer.android.com/training/scheduling/alarms#boot

デバイスの再起動をアプリは検知することは通常できないので、 intent を受け取るようにするようにしておくのだ。
つまり、アラームとは関係が無いのだが、アラームを設定したアプリを起動していたのに、いつの間にか再起動されてアラームが動作しなかったということがあるからここで説明しているのだろう。

 

書いてあるとおりに実装した。
一度アプリを起動しておけば、再起動すると notification が表示される。

https://github.com/hirokuma/AndroidAlarmTest/compare/20488ad5377f82a2d529b2654dbb6262a50985e6..4f69554d7f177e051d3f57b33e59c7701986507d

(関係ないけど、Windows で作っていたからか AndroidManifest.xml だけ CR/LF になっていた。 Android Studio の設定だけでは足りないのか?)

 

また、このブート時に受け取るような設定は永続的なものではないようだ。
このアプリを「アプリ情報」から強制停止してからデバイスを再起動すると notification が出てこなかったからだ。一度設定しておけば良いものでも無く、アプリがブート前に立ち上がって設定しておくだけで良いものでも無く、アプリがまったく死んでいない状態にしておかなければならないようだ。

 

では、ブート時の notification が表示されただけで setComponentEnabledSettings() を実行していない状態で再起動したらどうなるだろうか?
これはブート時に notification が表示された。 AlarmTest アプリは Activity の onCreate() で setComponentEnabledSettings() を呼び出しているのでおそらく呼ばれていないだろう。3分間隔の notification も出ないのでそのはずだ。
プロセスとしては立ち上がっているけれども、 onCreate() から始まる経路は使われていないということかな。

[android] アラームの復習

さて、少し期間が空いてしまったため Android の Alarm についてかなり忘れてしまった。
ほんの少し・・・それですら命取りになるのが加齢よ。。。


前回は AlarmManager を使って通知を出すようなしくみを考えていたが、Android というシステムからするとデバイスの起動状態を管理するという分野になるそうだ。宇宙の成り立ちみたいな広い話ですな。

デバイスの起動状態を管理する  |  Android デベロッパー  |  Android Developers
https://developer.android.com/training/scheduling

定期的に intent を発行する、というだけでなく、もっとデバイスの中で限られたリソースをうまいこと使いたいとなると電源やネットワークの状態によってアプリの動作を制限するというのも理解できる。
アプリを作っている人たちはみんな「私のアプリを使って!」と思っているので、1つしかデバイスがないのであれば優先順位を付けるしかないのだ。

そう考えると、アクションゲーム?のように前面にアプリが起動していないと意味が無いようなアプリの場合はあまり考えなくて良いだろう。バックグラウンドで動作できるアプリが複数あるのに対して、フォアグラウンドで動作できるアプリは1つしかないからだ。画面を分割することもできたと思うが、それでも入力のフォーカスを持っているのは 1アプリだけだろう。

そう考えると、定期的にアプリを起動したいのはフォアグラウンドにはいないアプリが主になるはずだ。
Android 10 からはバックグラウンドアプリから Activity を表に出すことに制限がかかった。調べ切れていないが OS のバージョンが上がってできなくなったことが増えていてもおかしくない。

こういった実装やバージョンの事情がいろいろと複雑にしているのだと思う。
developer のドキュメントはよくできていると思うのだが、Google で検索しても本家の情報が出てこないのであれば実装する人が混乱するのも仕方ないと思う。


AlarmManager 以外にも WorkManager がある。
最近は WorkManager を使うよう勧めているように思うが、得意とする分野が少し異なる。

他の API との関係性
https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja#other-APIs

Dozeモードを解除できるのが AlarmManager らしい。

プラットフォームの電源管理  |  Android オープンソース プロジェクト  |  Android Open Source Project
https://source.android.com/docs/core/power/platform_mgmt

Doze とアプリ スタンバイ用に最適化する  |  Android デベロッパー  |  Android Developers
https://developer.android.com/training/monitoring-device-state/doze-standby?hl=ja

検索していると Dozeモードが搭載された Android 6.0 のときの話が良く出てくるが、Android 7.0 でさらに拡張されたのか。
上のリンクは AOSP なので OS そのものの話になっている。今回はアプリを作る方の立場なので下のリンクの方がわかりやすい。

  • 電源と接続していない
  • 静止状態にしている(センサーで見ているのだろう)
  • 画面オフのまま一定時間過ぎる

これで Dozeモードになるそうだ。それ以降は「メンテナンス枠」ということでときどき Dozeモードを解除して、また戻る。ということは、アプリが Dozeモードになるのではなく本体が Dozeモードになるということか。
時間経過のグラフを見るとメンテナンス枠になるタイミングがどんどん延びているので、放置状態が長くなるほど働かなくなるようにすることで電池の減りをなくそうということだろう。

AlarmManager はこの Dozeモードを無視して動くことができるというのが強みなのだろう。ただそこからすぐに Dozeモードに再突入するかもしれんが、そこはアプリ次第なのか。その API も追加されたもので、なおかつ 9分に 2回以上はアラームが発生しない(ということは 9分に 1回までということか)。

 

もう1つの「アプリスタンバイ」はアプリ単位。いくつか条件が整わないといけないので、アプリスタンバイにならないように作るのは難しくないかもしれない。ただ、アプリスタンバイでなくてもリソースを使いすぎだと判断されたら OS から止められるしくみはあったと思うので、結局は実験して確認するのがよいのか? でも対策していない要因でアプリが止められたら結局調べるしかないだろうし、知識を補充するのが先か・・・。

 

アプリが途中で動かなくなることについての YES/NO シートとか、logcat にこのメッセージが出たらこの状態、みたいなものがあるとよいのだが。

2022/08/21

[android] AlarmManager でアプリを起こしたい

やりたいこと

AlarmManager を使って、アプリが起動していないように見える状況でもアプリを起動させたい。

 

結果

アプリがタスク一覧に載っていなくてもアラームから起動することはできるが、強制停止状態では無理。

本体の OS が API 29 以降では Alarm のようなバックグラウンドからアプリを起動させることに制限がかかっているので、一旦 Notification などでユーザに通知だけ行い、ユーザ操作によってアプリを起動させることになるだろう。

バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts

 


前回、AlarmManager を使って反復アラームを設定し、AndroidManifest.xml に設定した <receiver> の class がブロードキャストされた intent を受け取ってログ出力するところまで確認した。

私がやりたいのは、アプリが立ち上がっていない(と思われる)状況でも AlarmManager での発火をトリガにしてアプリを起動したい、というものだ。
プリインストールのような system 属性のアプリであれば好き勝手できそうだし、Google Play からインストールしたアプリであればあまり勝手なことをされたいとは思わないだろう。

なので今回は Google Play からインストールするような system 属性を持たないアプリで期待したことができるのか、できないならどこまでやれるのかを調べたい。


前回は 15 分にしたが、あれは WorkManager の最短が 15分だったはず、くらいで決めたものだ。
AlarmManager なら 3 分でもできた。

2022-08-20 20:48:32.650/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.MainActivity: setAlarm
2022-08-20 20:53:47.674/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 20:56:28.482/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 20:59:47.663/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 21:02:47.665/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!

まあ、間隔が短くできるということはリソースを食いやすいということでもあるだろうから、アプリの審査としては厳しくなってしまうのかも? どういう基準で見ているのかは知らんのだけど、面倒なアプリの方が審査に時間がかかりそうな気がするじゃあないか。そういう基準だったら、AlarmManager ではなく WorkManager の方が緩いと思うのだ。

ともかく、3分くらいだったらテストしやすいのでそれで見ていこう。


まず「アプリが終了している」という状態について確認が必要だ。

「アプリが動いている」という状態が一番確実なのは、そのアプリがフォアグラウンドになっていて Activity が表示されているときだ。これは間違いなく動いているだろう。

ならばActivity が表に出ていないときがどうなのか、ということになる。
調べていて出てきたのが「 Doze」と「アプリスタンバイ」というものだ。

Doze とアプリ スタンバイ用に最適化する  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=ja

書いてあるとおりなら、

【Doze】
デバイスが長い間使用されていない場合にアプリのバックグラウンド CPU とネットワーク アクティビティを保留する

【アプリスタンバイ】
ユーザーがしばらくの間操作していないアプリのバックグラウンド ネットワーク アクティビティを保留

アプリスタンバイはネットワークだけしか保留しないのか? 文章だけ読むと、先に「アプリスタンバイ」になり、それでも長い間使用されないようであれば「Doze」になるようにも見えたのだが、そうではないのだろう。

アプリで Doze をテストする
https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=ja#testing_doze

「dumpsys deviceidle force-idle」を実行してそのアプリの動作が変わるなら、Dozeモードで動作が変わるアプリということだろう。

アプリスタンバイの場合はどれをスタンバイにするか指定しているので別物かなぁ、というところだ。
ネットで検索したらもっとよい情報が出てくるだろうが、今回はそこまで考えたくないので無視だ。


私が「アプリが「終了している」というのは、アプリスタンバイに近いと思う。
そして Android のタスク一覧から該当するアプリを上スワイプしたときも「終了している」だろうから起動してほしい。
できればアプリ情報画面で強制停止(force stop)させたあとからも起動してほしいのだが、これは難しいと思う。

あとは、アプリの中から「終了」を行った場合だ。
これが何をしているのかがよくわからんのだが、Activity が 1枚しかないときに「戻る」を選ぶと「終了しますか?」と尋ねてくるアプリがあったように思う。

ユーザが「アプリを終了したい」と思うことと、アプリとして終了するという実装は、一致しないとはいわないまでも、一致しづらいことが多いと思う。例えばゲームのハイスコアをサーバで管理してみんなで共有するようなアプリがあったとして、ユーザが「ハイスコアを更新したから終了させよう」と思ったタイミングと、アプリが「スコアのデータをサーバにアップしよう」と実装されているタイミングは必ずしも一致しないだろう。ユーザは終わった気分でいても、他のユーザがアクセス中だったりしてアップロードを保留しているかもしれない。サーバにアクセスが集中しないようにわざと遅らせるようにしているかもしれない。

そう考えると、モバイルのアプリは「終了」が即時行われるとは考えづらい。たぶん即時終了させるのがアプリ情報画面から「強制停止」を行う操作だろう。
一覧から上スワイプするのは強制停止ではないと思う。Doze にさせるか、スタンバイにしているかのどちらかではないか。調べていないけどスタンバイじゃないかなぁと思っている(個人の感想です)。

端末を USB接続しているとスリープしない状態になったりするし、その設定をオフにしても実は何かあるんじゃないの?とか、15分を 3分にしたことで挙動が違ったりするんじゃないの?とかいろいろ考えてしまうのだが、まあそのときはそのときだ。

Android Studio の Logcat タブでアプリが (DEAD) になるようであればアラームを受け付けることはさすがにできないだろう(と思ったが、タスク一覧から上スワイプすると DEAD になったので違うのか)。


adb shell でログインすると、 dumpsys というコマンドが使用できる。

dumpsys  |  Android デベロッパー  |  Android Developers
https://developer.android.com/studio/command-line/dumpsys?hl=ja

dumpsys コマンドだけたたくとすべてのシステムサービス情報を出力しようとするので、対象とするサービスを指定するのがよろしい。今回であれば alarm だ。
指定してもたくさん出力されるのだが、試作している alarmtest2 で検索するとこういう情報が見つかった。

    ELAPSED_WAKEUP #5: Alarm{bef203a type 2 origWhen 27853758 whenElapsed 27853758 com.hiro99ma.alarmtest2}
      tag=*walarm*:com.hiro99ma.alarmtest2/.NotificationReceiver
      type=ELAPSED_WAKEUP origWhen=+2m54s204ms window=+2m15s0ms repeatInterval=180000 count=0 flags=0x0
      policyWhenElapsed: requester=+2m54s204ms app_standby=-5s796ms device_idle=-- battery_saver=-5s796ms
      whenElapsed=+2m54s204ms maxWhenElapsed=+5m9s204ms
      operation=PendingIntent{77e94eb: PendingIntentRecord{b073248 com.hiro99ma.alarmtest2 broadcastIntent}}

ここでは ELASPSED_WAKEUP と経過時間で発火するように指定している。おそらく 3行目の type=ELAPSED_WAKEUP の次に出ている origWhen が発火するまでの残り時間だと思う。時間をおいて取得すると減っていたからだ。

これは、alarmtest2 アプリを起動すると出てきたし、タスク一覧で上スワイプして消したときも残っている。たぶん発火すると「App Alarm history」というところにも出てきたし、Alarm stats にも出てくるようになった。しかしアプリ情報から強制停止させると出てこなくなった。おそらくだが、アプリ情報から強制停止ボタンがタップできるような状態であればアラームは動くんじゃないかな。強制停止でアラームが消えるのは、登録元が消えたからか通知先が消えたからか。まあどっちでもよいが、確認するためには通知先を別アプリにしないとわからんな。

 

ん?
ということは、自分で自分にアラームを仕掛けておいて自分を起動する、というのは無理というか、既に起動しているから表に出してあげるだけで目的を達成できるんじゃなかろうか。

override fun onReceive(context: Context?, intent: Intent?) {
    val actionIntent = Intent(context?.applicationContext, MainActivity::class.java).apply {
        action = Intent.ACTION_MAIN
        addCategory(Intent.CATEGORY_LAUNCHER)
        flags = Intent.FLAG_ACTIVITY_NEW_TASK
    }
    context?.startActivity(actionIntent)
}

こんな感じだろうと思ったのだが、例外は発生しないもののアプリが表に出てくることはなかった。 Notification をタップしてアプリを表に出すときはこれでよかったのだが、状況が違うのか。

? I/ActivityTaskManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.hiro99ma.alarmtest2/.MainActivity} from uid 10502
? W/ActivityTaskManager: Background activity start [callingPackage: com.hiro99ma.alarmtest2; ...(中略)...; inVisibleTask: false]
? D/ActivityTaskManager: getPreferredLaunchDisplay userset_displayid false current displayId -1
? D/ActivityTaskManager: getPreferredLaunchDisplay userset_displayid false current displayId -1
? E/ActivityTaskManager: Abort background activity starts from 10502

BroadcastReceiver だからダメなのかと思い、AlarmManager から setRepeating() するときの PendingIntent で同じ actionIntent を呼ぶようにして設定したのだが、それでも同じだった。

 

「ダメだった」という状況について説明していなかった。
手順はこうだ。

  1. alarmtest2 を起動する
  2. タスク一覧を出して alarmstart2 を上スワイプする
  3. アラームが発火するまで待つ(本体は 2分でスリープになる)

3分以上待っているとアラームが発火して Logcat に何か出てくるので、そのときにスマホをスリープ解除して画面を出し、alarmtest2 アプリが表に出ていれば成功、出ていなければダメ、という判定の仕方をしている。


logcat の 「Abort background activity start」がエラー出力なので、そこら辺か。

バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts

あー、ユーザが操作しているのにいきなり表に出てくる可能性があるから制限がかかるのか。
わかる。私だったら怒るね。

ただ、私は alarmtest2 を API 26 以上ということで作っている。であれば API 29 からというこの制限から外れるんじゃないのか?

image

雰囲気からすると、ここに書いている APIレベルは本体の OS のものということだろう。

 

ともかく、Android としては「時間に依存する通知」を使ってほしいそうだ。リンク先や "ヘッドアップ通知" という言葉も出てくるので、いわゆる Notification を使えってことなのだと思う。 Notification → 通知をタップしてアプリを出す、がよいのかな。


というわけで、Notification を追加した alarmtest2 の最新版がこちら。

https://github.com/hirokuma/AndroidAlarmTest/commit/7aea025ed07fd53d4774a526f3328984ff4994a7

前々回の Notification 記事から持ってきたのだが、Notification を表示するタイミングとコンテキストが違うので多少変更されている。

アプリが起動して行うのは Notificationチャネルの作成のみ。
BroadcastReceiver の onReceive() で Notificationの作成と表示を行っている。記事を書いたときは with という Kotlin のスコープ関数なるもので notify() を呼んでいたが、BroadcastReceiver からは使えなかったので普通に書いた。

これを実行すると、数分おきに Notification の通知が行われる。スリープしていても Notification が出てくるので非常にうっとうしい。 API が間違っていなくても用法を間違うと嫌な動作をするという例と思ってもらおう。

2022/08/15

[android] AlarmManager でアラームする

Android の基礎学習。
AlarmManager でアプリが起動していなくても何かしたい。 notification が出したいのだが、まずは Log が出るだけでも良いだろう。

Schedule alarms  |  Android Developers
https://developer.android.com/training/scheduling/alarms

 

いろいろ書いてあるのだが、こういう系統はだいたいイベントが発火するときに PendingIntent で何かするものだろう。
PendingIntent は作るとき?に Intent を与えられて、PendingIntent の殻をかぶった Intent みたいなものだろうと思ってる。

作るといっても、PendingIntent.getActivity() だったり PendingIntent.getBroadcast() だったりといくつか作り方がある。前回はnotification の学習だったが、そこでは notification をタップしたときの挙動用に PendingIntent.getActivity() を使っていた。
タップすると自アプリが表に出てくる=Activity に対して通知するからだと思っている。

では、notification を出したい場合は誰が受け取るのだろう?
今回は Activity を表に出したくないので getActivity() ではないだろう。
受け取るために Service がいるのだったら getService() かもしれない。でもわざわざ service を作りたくないとも思う。
PendingIntent で他に PendingIntent を返す static メソッドはなさそうだ(ForegroundService みたいなの以外は)。

PendingIntent.getBroadcast() で作ってやって、それを受け取るところで処理できるなら一番楽そうな気がする。


ブロードキャストを受け取る方法について。

Broadcasts overview  |  Android Developers
https://developer.android.com/guide/components/broadcasts

ローカルだったら LocalBroadcastManager があるようなことを書いてあるが、class 自体が deprecated 扱いだったから今から使わなくてもよいだろう。他の observable pattern で置き換えられるようなことは書いてあったが、これを使えとは書かれていないのでチュートリアルの通りにやれば良いのだろう。

英語版を見ているが、受け取り方は「Manifest-declared receivers」と「Context-registered receivers」の2種類のようだ。
AndroidManifest.xml にあらかじめ書いておくタイプか、ソフトウェア上で必要なときだけ受け付けるタイプか。
その2択であれば、アラームについては AndroidManifest に書くタイプだろう。アプリが立ち上がっていないときに受け取りたいのだから。でも、もしかしたら一度登録しておけばよいというタイプかもしれんので実験はしてよかろう。


これに従ってアラームを作っていく。
「反復アラーム」というのは、ワンショットではないアラームのことだろう。

AlarmManager  |  Android Developers
https://developer.android.com/reference/android/app/AlarmManager

 

アラームの大きな設定は、タイプと精度のようだ。

タイプは、「何分後」みたいな経過時間タイプ(ELAPSED_REALTIME 系)と「何時何分」みたいな時間指定タイプ(RTC 系)があるようだ。
ELAPSED は経過時間といいつつ、デバイスが起動してからの経過時間という基準になっているそうだ。実装は、例では「SystemClock.elapsedRealtime()」からの相対値を指定していた。

精度は、曖昧だけど電池に優しいか、時間きっちりかを選択するもののようだ。メソッドとしてはおそらく第1引数に type を取るものがそれだと思う。

image

チュートリアルでは setInexactRepeating() と setRepeating() が紹介されている。 exact の頭に in- が付くことで「正確ではない」ということなのだろう。反復アラームだからその 2つだけ紹介されているのだろう。

アラームというものが Androidのバージョンで変化しているもののようで、APIの説明にいろいろ書いてある。

  • API 31以降で Intent.EXTRA_ALARM_COUNT を使うなら、 PendingIntent の flags に FLAG_MUTABLE がいる
  • API 19 の時点で反復アラームはすべて inexact になっている。繰り返しで正確にしたいなら、ワンショットのアラームを繰り返すことになる。

うーん、よくわからん。
API 20 以降なら setRepeating() だけでexact にできるということかな?

 

とりあえず動いているようなので記録を残しておいた。
Kotlin + Android 8以降 + Empty Activyty をベースにしている。

https://github.com/hirokuma/AndroidAlarmTest/tree/260da35b09a5012999a96de6d1e62903e7d525c2

 

Logcat のログを必要そうなところだけ抜粋。

2022-08-15 17:33:25.893/com.hiro99ma.alarmtest2 D/MainActivity: setAlarm
2022-08-15 17:52:25.390/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:14:40.987/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:23:33.683/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:35:38.638/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!

15分にしたのだが、15分より短いのもある。
WAKEUP の方にしたから放置した状態で確認したかったのだけど、ときどき触ったりしたので実際はどうだか分からん。

何が苦労したかって、 Activity::onCreate() でアラームを設定すると動作しなかったことだ。
何か方法はあるのかもしれないが、setAlarm() を onCreate() で呼び出すとダメだったのだ。動くサンプルがあったのだけどアラームの設定に違いがなさそうだったのでひどく悩んだのだ。そのサンプルはボタンをタップしてアラームを設定するようになっていたので、ためしに onResume() に持っていったら動作したのだった。

まだいろいろわかっていないが、今回はアラーム動いた記念なのでよしとしよう。

2022/08/11

[android] Notificationを出す

基礎の勉強。

Android Studio でプロジェクトを作る。
対象は Android 8.0 以降。Kotlin。Empty Activity。

Create a Notification  |  Android Developers
https://developer.android.com/training/notify-user/build-notification

日本語版もあるのだけど、若干更新されていない。まあ、実行時エラーで気づける程度のものだが。
ともかく、基礎的な notification を作成する。

Create a basic notification
https://developer.android.com/training/notify-user/build-notification#SimpleNotification

手順

  1. notification で表示する中身を作る。
    NotificationCompat.Builder() の後ろに設定するメソッドを連結して呼んでいくだけ。
    Builder() の第2引数である CHANNEL_ID は String型。これは Android 8.0 以降で追加されたものらしく、それより前は無視するとのこと。
    これは、この次に作る notification channel を指定するものだ。
    なお、日本語版で~と書いたのは、ここの PendingIntent.getActivity() の最後の引数が 0 になっていたからだ。今は何か指定しないといけないそうである。フラグの違いは検索すると出てくるので探してほしい。
  2. notification channel を作る。
    そういえば、Android のどこからかアプリの上にドットを出したりするようになったなぁ。
  3. notify() で表示。

 

こんな感じだ。

    private fun createNotificationChannel() {
        val name = "のてぃふぃけーしょんちゃんねる"
        val descriptionText = "のてぃふぃけーしょんちゃんねるのですくりぷしょん"
        val importance = NotificationManager.IMPORTANCE_DEFAULT
        val channel = NotificationChannel(NOTIFICATION_ID, name, importance).apply {
            description = descriptionText
        }
        val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }

    private fun createNotification(): NotificationCompat.Builder {
        // 自アプリを起こす
        val wakeupMeIntent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val wakeupMePendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, wakeupMeIntent, FLAG_IMMUTABLE)

        return NotificationCompat.Builder(this, NOTIFICATION_ID)
            .setSmallIcon(com.google.android.material.R.drawable.ic_clock_black_24dp)
            .setContentTitle("のてぃふぃけーしょんたいとる")
            .setContentText("のてぃふぃけーしょんてきすと")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(wakeupMePendingIntent)
            .setAutoCancel(true)
    }

これらを、たとえば onCreate() などで呼び出す。
notify() の第1引数は Int 型で、ユニークならよいらしい。あとから中身を更新したり削除したりするときに識別するものだそうだ。

        createNotificationChannel()
        val builder = createNotification()
        with(NotificationManagerCompat.from(this)) {
            notify(NOTIFY_TEST_ID, builder.build())
        }

まあ、アプリの起動と同時に notification を出すことは少ないかもしれんが、うまいことやってほしい。


このアプリを起動すると notification が出てくる。
左側の時計アイコンは、適当に Android Studio の自動補完で候補に出てきたものだから、ちゃんとしたものかどうか知らん。

image

notification の中身は出てきたから良いとして、notification channel で設定したものはどこで出てくるのか?

見つけたのは、アプリごとの「アプリ情報」画面から「通知」をタップして表示される画面だった。

image

これだと notification channel の name だけなのだが、さらに表示されている項目をタップすると description の方も出てきた。

image

 

日本語にしておくと、そしてひらがなにしておくと他の文字列やデバッグ情報と重ならないからありがたいね。

 

ちなみにアプリ名が「AlarmTest」なのは、AlarmManager の実験をしたかったからだ。
時間になったら何かしたい→ notification を出すか、という流れだ。

2022/08/07

[android] ネットワーク通信のライブラリがいろいろある

Linux でネットワーク通信を行うならば、クライアント側だと

  1. socket つくる
  2. サーバ側と connect する
  3. あとはよしなにー

という感じだったと思う。

ただまあ、socket のような素に近いところを直接アプリで触らせたくないと思う。ラップするならそこだろう。
なので「Android では通信するのに volley というライブラリを使う」と書いてあったので、うまいことラップされているのかな、くらいに思っていた。

しかしそこにあったのは、socket をラップしたライブラリとかではなく、なんだかいろいろ隠したライブラリだった。
最終的には「便利」なのかもしれないが、ちょっと通信したいのに使うには難しそうだった。
まあ、使わないことにはわからないので、基本的なところはやってみよう。

関係ない話だが、volley を検索して見ていたページが google.cn の方だった。偽サイトに引っかかったのか?とあせったが google.cn も google の証明書だった。よかった。


まず、最初に書かれているこちら。

Volley は、Android アプリのネットワーク操作を容易化、高速化する HTTP ライブラリです。

socket 通信ではなく、HTTP系専用らしい。
HTTP だったら、というわけでもないが、ネットワークに関しては普通の Java でもライブラリがあったはずだ。 volley だって Android の最初からあったわけではなく、どちらかといえばつい最近だったと思う。それまで HTTP で通信できなかったわけではないので、 volley でなくてもよいはずなのだ。

しかし、Android Developer の接続に関するページを見ると、2項目目にはもう volley が出てきているので、なんらかの理由で推しているのだろう。

接続  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/topics/connectivity?hl=ja

ただねぇ、ちょっと内側に行くと Retrofit というライブラリを使う説明をしてあった。なんなんだ。。。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting?hl=ja#choose-http-client

どうやらライブラリについていろいろと経緯があったらしい。

非同期通信をVolleyとOkHttpとRetrofitそれぞれで書いた場合の超簡易サンプル - Qiita
https://qiita.com/yamacraft/items/2bbc8867ef0a8ffb0945

ああ、okhttp も聞いたことがある。 gRPC を動かすときも okhttp のライブラリがなくてエラーになったし。
しかしすべてを把握したいわけでもない初心者としては、廃れなさそうなライブラリを 1つだけ覚えたいのだ。
volley はgoogle が開発しているから無難なのかな?
しかし OkHttp は HTTP/2 のことが書いてあったり、Retrofit と同じ Square が開発しているというのもあったりして魅力を感じてしまう

Overview - OkHttp
https://square.github.io/okhttp/

Retrofit は OkHttp を使っているので、HTTP 向けにしたラッパみたいなものなのかな? いや、 HTTP client だから違うな。 GitHub の About では、

OkHttp
Square’s meticulous HTTP client for the JVM, Android, and GraalVM

Retrofit
A type-safe HTTP client for Android and the JVM

となっているから、Retrofit はタイプセーフなところが売りなのだろう。

タイプセーフってなんじゃ?と思ったが、文字列ではなく Java や Kotlin の型で扱えることを指すのだろう。 volley はサンプルを見た感じでは文字列が返ってくるようだ。ただ、レスポンスが文字列、画像、JSON の場合は通常の Request で処理してくれるらしいし、特殊なことがしたければカスタマイズもできるようだ。画像って base64 なのかな?

どうせ私が使うなら JSON でやりとりするだろうし、それだったらどっちでもよいのかな。となると決め手がない。。。
なにか明確な違いがあればよかったのだが。
なので、どっちも同じことができるなら、実装してみて気に入った方を使うようにすれば良いと考えることにした。


ライブラリを試す前にちょっと脇道に逸れる。

新規でプロジェクトを作ろうとしたのだが、こっちもいろいろある。。。
どれが「普通」なのだろう?

image

まず、(Material3) の付いているのと付いていないのがある。
今のマテリアルデザインは 3 らしいので、付いていないのは 2 ってことか? 4 のページはなかったのでそうだと思う。

あとは、ただの Activity か Compose Activity かだ。
たぶん Jetpack Compose のことだろうと思うが、私に分かるんだろうか?

とりあえず Net1 という名前で作ってみた。
最初の 3行くらいは Kotlin の class の書き方なので分かるが、setContent 以降が初めて見る形だ。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    Net1Theme {
        Greeting("Android")
    }
}

Kotlin の文法をある程度把握する方が先かもしれんな・・・。


まず、Project Settings の Dependencies で "com.android.volley:volley" を search して追加。
今の時点では 1.2.1 が見つかった。

スニペットを貼り付けてみた。
Alt + Enter で解決させていったのだが、そもそもスニペットを Compose Activity の構造としてどこに置いたらいいのかがよくわからない。
@Compose の関数内だとコンテキストがダメそうだったので MainActivity の private 関数として追加し、 onCreate() から呼ぶようにした。画面に出すのはわからないから Log にした。

いつものように Android Studio の再生ボタンを押したのだが、画面は出るものの関数が呼ばれている気配がない。ブレークポイントも止まらない。
どうやら Compose Activity でプロジェクトを作った際には Configuration が DefaultPreview になっているようで、それだと MainActivity の onCreate() すら呼ばれないようだった。

Configuration を app に切り替えると、ようやく動きがあった。

E/Volley: [37] NetworkDispatcher.processRequest: Unhandled exception java.lang.SecurityException: Permission denied (missing INTERNET permission?)
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)

ああ、AndroidManifest.xml を追加していなかった。

<uses-permission android:name="android.permission.INTERNET" />

これだけで動いた。
Koglin で変更したのは MainActivity だけで済んだ。青文字がそれである。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        network()
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }

    private fun network() {
        // Instantiate the cache
        val cache = DiskBasedCache(cacheDir, 1024 * 1024) // 1MB cap

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.google.com"

        // Formulate the request and handle the response.
        val stringRequest = StringRequest(
            Request.Method.GET, url,
            { response ->
                // Do something with the response
                Log.d("network", response)
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }
}

呼び出しが一回だけだったら Volley.newRequestQueue() でよいそうだ。キャッシュを確保するのにもコストがかかるので、そこら辺は自分のアプリと相談することになるだろう。

JSON での API 実行のようにデータが小さいことがわかっている場合は、むしろキャッシュ無しという選択もあるんじゃなかろうか?
ああ、ちゃんと NoCache.java という

https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json

cache のところを書き換えても動いたので、それでよいのかな?
ついでに JSON のデータを取ってくるようにした。返ってくるデータが JSON の Array なのか Object なのかによって関数名が変わるのは注意しておきたいところだ。

    private fun network() {
        // Instantiate the cache
        val cache = NoCache()

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json"

        // Formulate the request and handle the response.
        val stringRequest = JsonArrayRequest(
            Request.Method.GET, url, null,
            { response ->
                // Do something with the response
                Log.d("network", response.toString())
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }

最初は String で受け取っていたのだけど、日本語の部分が文字化けするのだった。 JSON で取ると化けなかったのだけど何でだろう? Windows だからか?


volley の欠片くらいは味わえたと思うので、次は Retrofit だ。

https://square.github.io/retrofit/

こういう感じかな?

  1. Retrofilt.Builder() でアクセスする根っこを指定する
  2. interface を作って、 GET したりする下側を準備する
  3. Call で呼び出す

ここだと、根っこの部分が「https://api.github.com/」で、GET するのは「https://api.github.com/users/{ユーザ名}/repos」。
ユーザ名は "octocat" を指定している。

https://api.github.com/users/octocat/repos

 

まず Project Structure > Dependencies からライブラリを追加。
これを書いている時点では 2.9.0 だった。

com.squareup.retrofit2:retrofit

INTERNET の permission は既に追加してあるので、あとはまねしていけばよいだけ。

だとおもったのだが、「android.os.NetworkOnMainThreadException」が出てしまった。
コルーチンでやるのが Compose Activity っぽい気はするが、別スレッドにすればよかろうということで Tread で囲んだ。

    interface Tenki {
        @GET("/bosai/forecast/data/forecast/{area}.json")
        fun listTenkis(@Path("area") area: String?): Call<List<TenkiData?>?>?
    }

    class TenkiData(val publishingOffice: String, val reportDatetime: String)

    private fun networkRetrofit() {
        Thread {
            val retrofit = Retrofit.Builder()
                .baseUrl("https://www.jma.go.jp/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

            val service = retrofit.create(Tenki::class.java)
            val data = service.listTenkis("400000")
            val result: List<TenkiData?>? = data?.execute()?.body()
            Log.d("network", "${result?.get(0)?.publishingOffice}, ${result?.get(0)?.reportDatetime}")
        }.start()
    }

"?" が多いのは Android Studio の指摘をそのまま採用したためだ。
ここで TenkiData に取得しないパラメータを追加しても、それは別にエラーとはならなかった。なので、あれば読み取るし無ければ読み取らないだけのよう。型が違う場合には変換動作によって例外が発生する。


こうやってみると、楽かどうかという意味では volley の方が事前準備が少なくて楽だった。ただ、読み取って使うくせに型も知らないのか?という意味では Retrofit の方がよいのかもしれんと思った。 JavaScript 風に扱えるといえばよいのかな。

2022/08/06

[android] gRPCの準備をしてみよう(2022年8月)

Android で gRPC をしてみたいので、今の最新と思われる情報でやってみる。
便利なライブラリが公開されているかもしれんが、まずは gRPC のサイトに従ってやってみよう。

Quick start | Kotlin for Android | gRPC
https://grpc.io/docs/platforms/android/kotlin/quickstart/

プロジェクトは Kotlin で、Android OS は 8.0 にした。あまり下げすぎても上げすぎてもなあ、というくらいで決めた。

そこまではよかったのだが、この Quick Start は example code を使うようになっていた。
うーん、プロジェクトに何も入っていないところからやりたかったのだが・・・。
仕方ないので、プロジェクトファイルから変更箇所を探していこう。

grpc-kotlin/examples at master · grpc/grpc-kotlin · GitHub
https://github.com/grpc/grpc-kotlin/tree/0681fc85677e2cca53bdf1cbf71f8d92d0355117/examples

ここが protobuf のファイルから生成された gRPC API のライブラリ?にアクセスするヶ所を作っているのだと思う。

private val greeter = GreeterGrpcKt.GreeterCoroutineStub(channel)
  

しかし GreeterGrpcKt という class はGitHub にはない。
package が io.grpc.examples.helloworld なのでこの proto ファイルでよいと思うので、自動生成されるのだろうか? 検索したら "BUILD.bazel" というファイルの中に protos/ がしばしば見つかる。

Bazel上のAndroid  |  Android オープンソース プロジェクト  |  Android Open Source Project
https://source.android.com/setup/build/bazel/introduction

うーん、サンプル伸びるど手順だと gradle を使うようだから、とりあえずは気にしなくてよいのかな?

それにしても Stub とかテスト用のコードが多いな・・・と思ったら、テストのスタブとは別物だそうだ。

2022年はネイティブ×gRPCが激アツかもしれん
https://zenn.dev/efx/articles/e90a93c1bd210e

そうなのか。

 

2021年11月の Google Developers には、Kotlin が protobuf をサポートしたという記事があった。

Google Developers Blog: Announcing Kotlin support for protocol buffers
https://developers.googleblog.com/2021/11/announcing-kotlin-support-for-protocol.html

そこにリンクされていたのが 2020年4月の記事で、これは grpc-kotlin のことを指しているのかな?

gRPC, meet Kotlin | Google Cloud Blog
https://cloud.google.com/blog/products/application-development/use-grpc-with-kotlin

grpc-kotlin のことなら、まあそれを使うと言うことでよしとしよう。
気になるのは Kotlin の protobuf  サポートについてだ。

From a proto definition, you can use the new built-in Kotlin support in the proto compiler to generate idiomatic Kotlin Domain Specific Languages (DSLs).

protobuf のコンパイラを用意しなくてもうまいことやってくれるということだろうか。。。
いや、記事の真ん中くらいに gradle の設定が書いてあるな。サンプルはこちら。なのだが、このサンプルは拡張子が .kts なので Android Studio で普通にプロジェクトを作った場合の build.gradle を文法が異なる。困ったものだ。


grpc-kotlin/examples を Android で使うだけだったらどのくらい削除して大丈夫か調べてみた。
そうしないと、どれがいるものかわからないからだ。

  • grpc-kotlin/ 直下は examples/ 以外は削除してよさそうだ
  • examples/ 直下は、 client/ , native-client/ , server/ , stub-lite/ , stub/ は削除してもよさそうだ
  • examples/protos/ の中は、下の方までたどって helloworld/ 以外は削除してよさそう

ビルドできるかどうかという観点だとこうなった。動くかどうかとはまた別だろうが、動的にファイルを読み込むこともないと思うから大丈夫なのかな?

stub-android/ はいらないかと思ったのだけど android/build.gradle.kts が参照しているのだった。なくすと io.grpc.ManagedChannelBuilder が import できなくなるので何か使っているんだろう。

 

差分がこちら。
JavaVersion や jvmTarget が 11 になったり、androidx.core:core-ktx が 1.8.0 になったりするのは、まあおまけのようなものだ。

https://github.com/hirokuma/grpc-kotlin-sample/commit/64d02c28220ef5b7c7611c0df8ab3f105c09bf61

protos/ と stub-android/ は grpc-kotlin から持ってきた。
追加した位置は grpc-kotlin と同じ高さで、 app/ の中ではなく app/ と同列。 settings.gradle を変更しているのはそれらを module として追加するためだと思う。
build.gradle は protobuf のプラグインを読み込んでいたのをそのまま持ってきた。 stub-android だけ追加してもなんとかなりそうな気もしたが試してはいない。

プロジェクトの依存関係としては、 app が stub-android を参照するようにした。 stub-android は protos を参照するようになっている(もともと)。

 

取りあえずビルドは通ったのだが、Android から呼び出そうとすると、今の私には知識が足りなかった。。

https://github.com/grpc/grpc-kotlin/blob/0681fc85677e2cca53bdf1cbf71f8d92d0355117/examples/android/src/main/kotlin/io/grpc/examples/helloworld/MainActivity.kt

by lazy ?  setContent ??  suspend fun ???  @Composable ???? といった感じだ。

Android Compose のチュートリアル  |  Android デベロッパー  |  Android Developers
https://developer.android.com/jetpack/compose/tutorial?hl=ja

Kotlin もまだ足りてないのに、コルーチンだの Jetpack だのいろいろ追いついていないのだった。
見栄えは悪いが、簡易的に表示まで確認した。

response · hirokuma/grpc-kotlin-sample@27e6b39
https://github.com/hirokuma/grpc-kotlin-sample/commit/27e6b39fe06e66681bc148d0c8a631d2dfbe3179

2022/07/31

[vscode] vscode と editorconfig

私はテキストエディタでハードタブ、キャラクタコードで 0x09 の表現をスペース4つで見えるようにしていることが多い。

UNIX とかは 8 文字だけど、横幅が広くなると困るので 4 つにしている。
もともと C言語で書いていて、X Window とかで書いているときはそのまま従って 8 つだったのだけど、当時の文化として「横幅はだいたい 80桁で!」だったはずだ。メールも 76 だったと思う。
そうなるとインデントで幅を取られることが馬鹿らしくなり、自然と短くしていったのだった。

 

HTML を触ることがあった。
そのときに「タブは 2文字」というのがよく使われていたのだ(私の作業では)。
そもそもその時代になると、ハードタブで個人差が生じるくらいだったらソフトタブでスペースを直接埋め込めば間違いがない、という風潮だったように思う。
「インデントを削るのに文字数分削除するのが面倒!」「間違えて 3 とか 5 になってしまうかもしれないじゃないか!」と心の中で反発しつつも使っていくと、うん、慣れてしまった。
今やスペースでタブを表現せずにどうするんだ、くらいまでになっている。
人間、そんなもんだよね。

 

さて、ここまでは時代の風潮くらいのものなのだが、言語仕様でタブを意識するモノが出てきた。
Python と Golang だ。

Python は、インデントが正しくなっていないと実行時エラーになっていたと思う。
面倒なので例として PEP8 のインデントルールを見てみよう。

PEP 8 – Style Guide for Python Code | peps.python.org
https://peps.python.org/pep-0008/#indentation

> Use 4 spaces per indentation level.

なるほど、4スペースか。
・・・ではなく、Python の仕様確認が先だろう。

2.1.8 Indentation
https://docs.python.org/release/2.5.1/ref/indentation.html#l2h-9

> First, tabs are replaced (from left to right) by one to eight spaces such that the total number of characters up to and including the replacement is a multiple of eight (this is intended to be the same rule as used by Unix). The total number of spaces preceding the first non-blank character then determines the line's indentation. Indentation cannot be split over multiple physical lines using backslashes; the whitespace up to the first backslash determines the indentation.

Google翻訳
> まず、タブは (左から右へ) 1 から 8 個のスペースで置き換えられ、置換を含む最大文字数が 8 の倍数になります (これは、Unix で使用されるのと同じ規則になるように意図されています)。次に、空白以外の最初の文字の前にあるスペースの総数によって、行のインデントが決まります。バックスラッシュを使用してインデントを複数の物理行に分割することはできません。最初のバックスラッシュまでの空白がインデントを決定します。

うーん、これを正しく解釈できる自信がない。
tabs を one to eight spaces に置換するということは、純粋に 8文字のスペース文字に変換するのではなく、 8文字インデントになっている文章と見なして最大 8 文字のスペース文字に変換して頭を揃えようとする、ということだろうか。
インデントエラーの例も、けっこう緩いと思った。

  • 関数の先頭がインデントされているのはエラー
  • 関数の中身がインデントされていないのはエラー
  • インデントされないはずの行でインデントが変化しているのはエラー
  • インデントが戻るはずがない行でインデントが変化しているのはエラー

いくつインデントすべき、みたいなのは要件に入らないということだろう。


さて、いつものように本題から逸れていますが、いつものことです。

お勉強中の Go言語だけど、あっちは「インデントはハードタブ」という仕様だ!・・・と思っていたのだけど、Launguage Specification に "indent" は出てこなかった。

The Go Programming Language Specification - The Go Programming Language
https://go.dev/ref/spec

あーれー、私の勘違い??
まあいいや。golang のフォーマッタを使うと自動的にタブ文字になっていたと思うし。

 

そこらへんはよかったのだが、他から clone した GitHub のプロジェクトのファイルを開いたときに何か違和感があった。
おかしい・・・初めて見るコードなのに違和感がある・・・。

ああ、タブ文字が 8 スペースで表示されているんだ。
でも vscode の設定では 4 スペースにしているし。。。
4 スペースに変更しても別のファイルでは 8 スペースで表示されるし。。。
なんなの??

 

ようやく本題ですが、このプロジェクトには `.editorconfig` というファイルがあり、それを vscode が読み込んで反映させていたようだった。

EditorConfig
https://editorconfig.org/

私の設定よりもこっちが優先されるのね。
まあ、そうでないと意味が無いわな。


EditorConfig のページはなかなか気合いが入っている。
サポートしているサイトの紹介があるのだが、そのリンク先画像がたぶん自作なのだ。 GitHub なんかこれだよ。

image

普通に引用した方がかんたんだろうに、あえてこのページの世界観でアイコンを描くというところに気合いを感じる。だって、アイコンをそのまま借りた方がはるかに楽なのですぞ。

 

EditorConfig はすべてのエディタがサポートしているわけではないと思うが、いくつかの主要なエディタが採用しているならファイルとして作って置いても損にはならないかもしれない。テキストエディタで外部からの設定を求めるというのはどうなんだろうと考えなくもないが、嫌なら使わなければよいだけの話である。

vscode だとプラグインがいりそうなことを書いてあるのだが、使っていないつもりなのに反映されてしまった。
こういうのは個人の好みもあるから、けっこう難しいね。

2022/07/30

[win10] キーアサインの変更(2022年7月)

Windows でキーアサインを変更したいという要望はそこそこあると思う。
そうでなければ PowerToys にそういう機能を付けないだろう。

PowerToysWindows 用の Keyboard Manager ユーティリティ | Microsoft Docs
https://docs.microsoft.com/ja-jp/windows/powertoys/keyboard-manager

私は AutoHotKey と Change Key で割り当てている。

AutoHotkey
https://www.autohotkey.com/

「Change Key」非常駐型でフリーのキー配置変更ソフト - 窓の杜
https://forest.watch.impress.co.jp/library/software/changekey/

Change Key は CapsLock を 左Ctrl にするのと、半角/全角 を Esc と入れ替えるのに使っている。
AutoHotKey だけでいいやん、と思うかもしれないが、少なくとも私が AutoHotKey を使い始めた頃はまだうまくいかなかったのだ。

それに、AutoHotKey はアプリが起動しないと使えないが、Change Key はその前から使えるのでログインする前でも確か使えていたと思う。

そんな Change Key 万能!という雰囲気をかもし出しつつも、これは単独のキーしか変更できない。
Ctrl+F で右矢印キーの代わりにする、なんてことはできない。


そんなわけで、ショートカットキーのキーアサインを変更するアプリについてだ。

 

AutoHotKey も PowerToys もだが、Ctrl+[何か] のアサインを割り当てた場合、たまに Ctrl が押されない動作をしてしまうことがある。

AutoHotKey は長押しすると発生しやすいし、CPU負荷が高まるとさらに発生しやすいと思う。私は Ctrl+F を右矢印キーに割り当てているのだが、 Android Studio のエディタでそれをやるとときどき「f」が打ち込まれてしまってあせる。そして Android Studio は保存キーを使わずに勝手に保存したりするので、コンパイルエラーになって気付くことがたまにある。

PowerToys の場合はキーの押し始めに抜けることが多いと思った。処理が重たくなると押しっぱなしの状態でもすり抜けることはあるが、AutoHotKey よりは少ないかも?

 

AutoHotKey についてはもう 9年も前の記事だが、これがやっぱり出てくる。

AutoHotkey:キー押しっぱなし病・ホットキーすり抜け病対策の研究
https://did2memo.net/2013/10/03/autohotkey-ctrl-key-is-stuck/

まあ、すり抜けるときはすり抜けるのだけど、PowerToys で発生するということは OS として発生するのではないかと思う。それを対策したら AutoHotKey などでもうまくいくようになってくれるとよいのだがね。

AutoHotKey はスクリプトで書けるから楽なのが良い。自由度も高いし。そのせいで PowerToys よりもすり抜けやすくなっているのではないかという気がしなくもない。

難しいもんだね。

2022/07/10

[golang] 埋め込む順番で type は異なる

構造体の勉強中。

interface は「こういうメンバたちを持ってるよね? 持ってないと我が一族の名を名乗れないからね?」という脅しだと思う。いや、コンピュータ言語で脅しても仕方ないのだが、コンパイルエラーになるという意味では脅しと受け取った方がよいんじゃなかろうか。

ここ数年 C++ から離れているが、 class のメソッドで =0 にしていると abstract なメソッドという扱いになって、必ず派生した class でオーバーライドしないといけないようになってた気がする。
=0 が 1つでも入っているとインスタンスにできなかったと思うので、それがなくなるごとに抽象度が減るという表現をするのかもしれない。

 

その interface と似てるような似てないようなのが「埋め込み」とか「annonymous field」というやつだと思う。今のところお仕事で使う機会が無いのだが、C言語で class 的なものを使いたい場合のやり方だと認識している。
C言語だと構造体のメンバを定義した順にメモリ配置するので、

struct A {
  int a;
  char b;
}

struct B {
  int a;
  char b;
  double c;
}

という構造体を用意すると、a と b については型とメモリ上のアドレス相対位置が一致するので struct A と struct B の変数は a と b については気にせずアクセスできる。
c については、 struct A の変数を struct B としてキャストすればアクセスはできるだろうが、読み込めるのはそのアドレスにあるデータなのでどうなるかは不明だ。まあ alignment はpack(0) とかしてなければ問題ないので死にはしないだろう。

結局のところ、メモリ上にあるものはすべて「データ」に過ぎない。
それがプログラムなのか文字列なのかExcelのデータなのかというのは後付けの理由だ。画像データなんてフォーマット以外の部分はバイナリデータに過ぎないので、そこにプログラムとして動作する値があったとしても不思議ではない。ウイルスだったりセキュリティホールだったりはそういうところからやってくるんだろう。

 

さて話を戻して。

struct の説明で、メンバの並びが違うと違うものとして扱われるというのをどこかで読んだ気がする。まあ、私は C言語のことを思い浮かべながら Go言語を見ているので、書いてなくても「きっとそうなんだろう」と思い込んでいるだけかもしれない。

埋め込みだとどうなのか、ちょっと確認してみよう。

package main

import "fmt"

func main() {
    // annonymous field
    type Name struct {
        name string
    }
    type Age struct {
        age int
    }
    type User1 struct {
        Name
        Age
    }
    type User2 struct {
        Age
        Name
    }

    var u1 User1
    var u2 User2
    u1.name = "kuma"
    u1.age = 98
    u2.age = u1.age
    u2.name = u1.name
    fmt.Printf("u1=%v\n", u1)
    fmt.Printf("u2=%v\n", u2)
    fmt.Printf("compare: %v\n", u1 == u2)
}

最後の u1 == u2 がコンパイルエラーになる。

invalid operation: u1 == u2 (mismatched types User1 and User2)

ただ・・・これはAge と Name が違うからではなく、純粋に struct で定義したものが違うからエラーにしているだけだと思う。並びを同じにしてもエラーになるからだ。

 

多少異なるのがキャストについてだ。
埋め込む順番を同じにした場合、 User2(u1) も User1(u2) もエラーにならない。

しかし Name(u1) も Name(u2) もエラーになる。
そこは通してやってもいいんじゃないかと思ったが、そうすると Age(u1) なんかも通さないといけなくなってしまうのかもしれない。メンバの先頭だけ OK というのは説明しづらいからな。

2022/07/09

[javascript] プリミティブ型と toString()

困ったことに、JavaScript の try-catch で catch するものは Error 型とは限らないそうだ。
取りあえずログに出したかったのだが、JSON.stringify(e) で {} になったり、 e で [Object Object] になったりして、もう考えるのが面倒になってきた。
Error 型だったら、e.message と e.name はあるのだけど、ライブラリを使っていると派生したオブジェクトを返したりして、もうどうしてよいやら・・・。

Error - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Error

よく見ると、インスタンスメソッドで toString() があった。

Error - JavaScript | MDN
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#instance_methods

Error を派生したんだったら toString() もオーバーライドしてほどほどの文字列を作るのが礼儀なんじゃなかろうか、きっとそうだ、と思い込むことにした。

 

しかし、throw が Error 型じゃなくてもよいということはプリミティブ型でもよいはずだ。
プリミティブ型はオブジェクトじゃないはずだ。
オブジェクトじゃないなら Object を継承しないので toString() を持たないかもしれない。
だったら e の型チェックしてからじゃないと toString() が使えないのか。
めんどくさい・・・。

 

と思って文字列や数字を throw したのだが、toString() で文字列として出力されていた。
なんで??

本を読んだところ、プリミティブ型は使われる際にラッパーオブジェクトが一時的に生成されるようになっているらしい。
つまり、文字列なら String型、数字なら Number型が生成される。
これらはオブジェクトなので toString() を使える、という理屈のようだ。

もしかしたら toString() などのメソッドを使う直前に生成されるのかもしれん。そっちの方が理屈に合うか。

 

それはそうと、元の問題である catch したデータをログに出した意見だが、これは昔からどうしたもんだかって話のようだ。 Error の派生についてStackoverflow のリンクが載っているくらいだから、 throw となるとさらにめんどくさそうだ。

throw - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/throw

オンラインでコードを動かせるので throw を書き換えながら試したが、 new Error(文字列) は console.log で出力できるが、単に Object として {message: 文字列} なんかを throw すると [object Object] になる。
じゃあ console.log で JSON.stringify() したものを出せば良いのだが、 new Error() だと {} だけになる。

 

そういえば、console.log を使いたいわけでは無く、ログファイルに出力させたいので「文字列」として取得したいのを忘れていた。 console.dir が使えるという記事があったのだけど、そういうわけで今回は対象外だ。

となると、無難に toString() しておけば文字列として取得できるのか。
オブジェクトの中を全て見れば詳細な情報が確認できる場合もありそうだが、そこはもうピンポイントで調べたい場合だけとあきらめても許してくれるんじゃないだろうか。

2022/07/03

[golang] よく出てくる context.Context

通信するライブラリなどを使おうとするとしばしば登場するのが context.Context だ。
gRPC だとこんな感じ。

Basics tutorial | Go | gRPC
https://grpc.io/docs/languages/go/basics/#simple-rpc-1

context.Background() でコンテキストを作って与えておけば取りあえず使えるので深く考えずに使っていたのだが、真面目に実装していくと理解していないとまずいと感じた。というより、あまり理解していなくてコードレビューで指摘を受けてしまった。

よく出てくるのでほどほどに理解したい。
ネットで「golang context」で検索するとたくさん出てくるので、きれいな解釈はそちらを見た方がよいだろう。なら書くなよと思われそうだが、私は書かないと理解ができないタイプなので仕方ないのだ。


まずは godoc

context package - context - Go Packages
https://pkg.go.dev/context#Context

出てくるのは context.Context だが、パッケージの存在目的が Overview に書いてあるので目を通す。

  • プロセス間で伝播
    • デッドライン
    • キャンセル
    • シグナル
    • その他 request-scoped な値

「between proceses」とあるが、Linux でいうところのプロセスではなく goroutine を指してるのだろうか。
「request-scoped values」は、処理の要求を行ったコンテキストが持っている値だろうか。 goroutine だったらそういう値はがんばらなくても参照できそうなものだが、うーん?

「プログラミング言語Go」には context が載っていない。本は v1.5 のときに書かれているのだが context が生まれたのは v1.7 のようだ。ちなみに本では goroutine のキャンセルについて書かれており、それは channel を使って行うようになっている。

 

こちらが、よく紹介されている go.dev のブログ。

Go Concurrency Patterns: Context - The Go Programming Language
https://go.dev/blog/context

まあ、もう 8年も経っているので日本語で紹介しているサイトの文面を読むだけでいいや。。。


いろいろ使い道はあるのだが、主な使い方はキャンセル処理の通知だと思う。
チャネルを使えば処理の終了を待ち合わせることもできるし、キャンセル処理をさせることもできる。

hiro99ma blog: [golang] chan で待つ
https://blog.hirokuma.work/2022/06/golang-chan.html

ただ、それをおのおのの実装でやっていくと違いが出てくるだろうし、似たような処理があちこちに出てきて格好が悪くなるから context にそういうのをまとめておいてみんな使いましょう、ということなのだろう。
A が Bライブラリを使って、 B が Cライブラリを使って、とやっていって、Aがキャンセルする処理を Bに伝えて、それを B が Cに伝えて、ということになるのをルール化したというところか。

なので、goroutine を走らせておいて「あとは好きにやっとけ」というタイプの処理だと使うことが無いかもしれない。多少なりとも goroutine を走らせた方が結果を待ったりするからこそ自分がキャンセルすることを下々に通知する必要が出てくると思われるからだ。

ただ、goroutine を呼び出した方が先に終了してしまうと goroutine の処理が終わる前に中断させられてしまうことになるから、何かしら終了を待つようなことにはなるのではなかろうか。
そう考えると、あまり難しく考えず、goroutine の中で context.Context を引数にとる API を使うなら その外側で context.Context を渡すようにした方がよい、くらいでいいのかな。

 

そういう見方をすると context のメンバーの用途がわかりやすい。

Done() は呼び出し元がキャンセルしたのを子が知るための channel 。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    fmt.Printf("start\n")
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    go func(ctx context.Context) {
        fmt.Printf("goroutine start\n")
        time.Sleep(10 * time.Second)
        fmt.Printf("goroutine done\n")
        <-ctx.Done()
        fmt.Printf("detect Done!")
    }(ctx)

    time.Sleep(5 * time.Second)
    cancel()
    fmt.Printf("main done.\n")
}

$ go run .
start
goroutine start
main done.

まあ、待ち始めるのが 10秒後なので、5秒後に cancel() するとそうなるよね。。。
だからといって goroutine の方を先に終わらせて Done() を待つというのは意味が無い。それなら goroutine を終われば良いだけだ。
となると、select で Done() を待つのと同じように、別の channel も待つようなシーンでないと意味が無いのか。

やるなら、cancel() を通知した後、自分が実行した goroutine が終わるのは待つようにするというところか。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    fmt.Printf("start\n")
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    done := make(chan struct{})
    go func(ctx context.Context) {
        fmt.Printf("goroutine start\n")
        time.Sleep(10 * time.Second)
        fmt.Printf("goroutine done\n")
        <-ctx.Done()
        fmt.Printf("detect Done!")
        done <- struct{}{}
    }(ctx)

    time.Sleep(5 * time.Second)
    cancel()
    <-done
    fmt.Printf("main done.\n")
}

5秒経過したら cancel() を呼び、待ち合わせている goroutine からの channel done を待ってから終了させる。
done を通知するのは自作の goroutine だから、done しなかったら自分のバグということでよいだろう。

では、もしその goroutine の中で別の goroutine を呼び出しているのであればどうするか?
同じだ。 Done() の channel を受け取ったら配下の goroutine をキャンセルさせるように通信して終わるまで待つのだと思う。

いやー、難しいね。

2022/06/25

[vbox] 全画面とディスプレイの電源を入れる順番

VirtualBox にて、ホスト環境は Windows10 、ゲストOS で Ubuntu という環境で作業することが多い。
地味に困っていたのが、全画面表示するとなぜか対象になるディスプレイが固定されないという現象だ。

うちの PC はマルチディスプレイ環境で、構成はこうなっている。

image

3つもいらんやろう、といわれそうだが、USB の DisplayLink モニタがあったのでつないでいるのだ。
TeraTerm などを立ち上げておくのにちょうど良かったし。
なお接続方法は、1番が DisplayPort、2番が HDMI、3番が DisplayLinkである。

 

VirtualBox のゲストOS としてはシングルディスプレイで動かしているのだが、全画面表示させると、なぜか 3番になることがほとんどだったのだ。
希望としては 1番で、2番でもまあいいや、くらいなのになぜか 3番の一番小さい(800x600)になるのだ。
非常に困る。やむなくウィンドウの最大表示を使っていたのだが、ちょっととはいえ小さいウィンドウなのが気に障るのだ。

 

ようやく気付いたのは、PCに電源を入れるときの順番によって違いがあるということだ。
私の場合、ディスプレイの電源を手動でオフにしている。
そして、本体の電源を入れて、ディスプレイをオフにしたままパスワードを入力してからディスプレイの電源を入れている。手順が変わらないので表示があってもなくてもいいや、と。

しかし、USB機器は接続したままなので、最初に 3番のモニタに表示が行われる。そのあとで電源を入れたディスプレイに表示が行われることになるのだが、おそらくこの順番が影響している。
検証はしていないが、先にディスプレイの電源を入れてから本体の電源を入れるようにすると今のところ 1番に全画面表示されている。

よかったよかった。

2022/06/19

[golang] 空文字列の []byte キャストは nil ではない

ちょっとだけ不安になったので確認しておこう。

 

package main

import (
    "fmt"
)

func main() {
    x := []byte("")
    if x == nil {
        fmt.Printf("nil\n");
    } else {
        fmt.Printf("not nil\n");
    }
}

$ go run .
not nil

よかった、認識は間違えてなかった。
扱いとしては []byte{} のときと同じかな。

初期値だったり nil を代入しない限りは nil にならないという認識だ。
でないと struct{} なんかも nil になってしまって区別が付かなくなるはずだ。
ただ、 struct{} が必ず別の値になるとは限らないという記述をどこかで見かけた気がする。。。

2022/06/12

[golang] chan で待つ (3)

2番目の記事で疑問になっていたところを解消しておこう。

hiro99ma blog: [golang] chan で待つ (2)
https://blog.hirokuma.work/2022/06/golang-chan-2.html

のこちら。

その後で A が終わって待ち解除になると B 待ち状態になるが、既に処理が終わった B についてどうなるのかわからん。

についてだ。


まず、勘違いしていた件。

 

done := make(chan struct{}, 3)

こう書いたとしても、

done[0] <- struct{}{}

のようには書けないということだ。
make() は capacity を設定するものだから len() ではなく cap() を使って容量を取ってくるのだった。

 

それを踏まえて。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := []chan struct{}{
		make(chan struct{}),
		make(chan struct{}),
		make(chan struct{}),
	}
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done[0] <- struct{}{}
		fmt.Printf("goroutine sent - 1\n")
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done[1] <- struct{}{}
		fmt.Printf("goroutine sent - 2\n")
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done[2] <- struct{}{}
		fmt.Printf("goroutine sent - 3\n")
	}()
	fmt.Printf("waiting...\n")
	<-done[0]
	fmt.Printf("done - 1\n")
	<-done[1]
	fmt.Printf("done - 2\n")
	<-done[2]
	fmt.Printf("done - 3\n")
	fmt.Printf("all done\n")
}

実行。

$ go run .
start
waiting...
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
goroutine sent - 1
done - 1
done - 2
done - 3
goroutine sent - 3
all done

時間の経過が見えないが、「goroutine done - 1」以降は一気に出力されている。
「sent」のログは全部出ていないが、これはその前にプロセスが終了したからだろう。

まあ、これなら前回の最後に書いた for でぐるぐる回して待つ方がシンプルに見えるな。

Blogger に StackEdit からアップするも画像に悩む

普段、ブログにアップする際は Windows の OpenLiveWriter を使っている。
ただ、Microsoft Store だったり GitHub の exe に上がっていたりするのは最新バージョンではない。最近の(というわけでもないが) Gooble Blogger には使えなかったと思う。
自分でビルドして使っていたのだが、新しくセットアップした PC にはそれをしていない。
Blogger の管理画面でエディタを使うことはできるのだが、コードを貼り付けるても見やすくない。
面倒なので、古い PC でアップしていたのだが、そこまでしてブログを書きたいかといわれると悩ましいし、かといってこういうのを書かなくなると老化が進みそうで怖い。

そんな感じでブログエディタをどうしたものか悩んでいたのだが、 StackEdit で連携させることができるというのを偶然知った。
私のブログは README.md の延長みたいなものだから、コードが見やすく貼り付けられて、あとは画像を貼り付けられればよい。


今日はそうやってブログを書いていた。
問題は画像だ。
StackEdit はマークダウンで書くことができるだけなので、画像については範囲外だ。画像ファイルのリンクがあれば貼り付けられる。
しかし、Blogger でいつも使っていた Google Photo だと直接画像ファイルにならないのでマークダウンの画像貼り付け表記ではうまくいかない。

例えば、これは OpenLiveWriter から画像を貼り付けてアップしたときに作ってくれたリンク&画像だ。

https://drive.google.com/uc?id=1V5UE1X-xNM9F3XqVJrOpbuxc4eXqMwft

ああ、貼り付けて分かったけど Photo じゃなくて Drive なんだ。
Drive のファイルからリンクを作ってみるとこうなった。

https://drive.google.com/file/d/1V5UE1X-xNM9F3XqVJrOpbuxc4eXqMwft/view?usp=sharing

うーん、 file/duc?id= に変換して /view 以降は削除する?
大した作業ではないが、来週の私、いや明日の私もきっと忘れているな。
画像のリンクだけなら OpenLiveWriter の方が楽だな。

[golang] chan で待つ (2)

golang の goroutine は、確か実装依存だったと思う。
Linux だったらきっと pthread なんだろうな、と思っていたが、そうではなくて自前で並列処理を作っていたような記憶がある。
まあ、使う方としてはそこまで意識をしないけど、OS 無しだったり独自OS だとどうなるのか気になるね。


さて、前回は 1つのチャネルで 1つの処理が終わるのを待つのを確認した。処理結果がいらない場合にわざわざ struct{}{} を返すので、別に bool とかでいいんじゃないとも思ったが、本に struct{}{} を返すよう書かれているので、そういうお作法と考えるのが良いだろう。値を返す、ということ自体が結構めんどうな処理になりそうだしね。

では複数の goroutine が全部終わるのを待つ場合も考えよう。
make() を使うので第2引数に待ちたいだけの数を設定して待てばよさそうな気がしたのだが、for で待つにしてもどの順番で処理が終わるか分からないから、make() で 3つ作って順に 処理A, B, C に割り当てたとすると、A を待っている間に B, C が先に終わってしまうこともあるだろう。その後で A が終わって待ち解除になると B 待ち状態になるが、既に処理が終わった B についてどうなるのかわからん。


 それはともかく、make()で複数の chan を作るのは「バッファ有りチャネル」として紹介されていた。
本の説明では 3つ作っている。そして 4つめは「送信文は待たされます」と書かれている。ということは chan は受信待ちだけでなく送信待ちというものもあるということか。
ただ、これは対象がスライス全体を与えているからそうなるのだと思う。スライスの各要素で送受信するようにしたらバッファ無しのチャネルと同じになるはずだ。

あるいは、スライス全体にすることで「早い者勝ち」になるようだから、それをループで回数だけ待てばよいはずだ。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := make(chan struct{}, 3)
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done <- struct{}{}
	}()
	for range done {
		<-done
	}
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        /home/xxx/golang/src/chan/main.go:30 +0x125
exit status 2

あれ、ダメなんだ。
for の回数を固定させるとエラーは起きないようなので range done

package main

import (
	"fmt"
	"time"
)

func main() {
	const eventNum = 3
	fmt.Printf("start\n")
	done := make(chan struct{}, eventNum)
	go func() {
		fmt.Printf("goroutine start - 1\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done - 1\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 2\n")
		time.Sleep(5 * time.Second)
		fmt.Printf("goroutine done - 2\n")
		done <- struct{}{}
	}()
	go func() {
		fmt.Printf("goroutine start - 3\n")
		time.Sleep(3 * time.Second)
		fmt.Printf("goroutine done - 3\n")
		done <- struct{}{}
	}()
	for i := 0; i < eventNum; i++ {
		<-done
	}
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start - 3
goroutine start - 1
goroutine start - 2
goroutine done - 3
goroutine done - 2
goroutine done - 1
done

本には、

  • len() は現在バッファされている要素の個数を返す
  • cap() はバッファ容量を返す

とある。
range donelen(done) と同じルールならダメそうだ。
本によると、 range でチャネルを回すのはチャネルに対して送信されたすべての値を受信するときと書かれているので len()と同じなのだろう。

いろいろやってみらんとわからんもんやね。

[golang] chan で待つ

オライリーさんからGo言語の本が出たので悩み中。

O’Reilly Japan - 実用 Go言語
https://www.oreilly.co.jp/books/9784873119694/

まだプログラミング言語Goも読み進めていないので早すぎるという気もするが、お仕事で使っている以上実用の本もほしいのが正直なところだ。
まあ、そのうち買うんだろうね。


さて、本題。

処理Aがあって、その処理の後に処理Bを行いたいことがあった。
それだけなら続けて書けば良いだけだったのだが、コンテキストが同じ状態で処理Aと処理Bを実行できないようで(自分で作ってない処理なのでよくわからん)、処理A は goroutine してやらないと処理Bが必ず失敗していた。
つまり、処理Aが非同期になったのだ。

今は処理Aが終わりそうな時間だけtime.Sleep()で待ってから処理Bを実行させているのだが、そういえば非同期処理の待ち合わせがあったなあ、と思い出した。
channelだ。
まあ、goroutine と channel はプログラミング言語Goでも同じ章で説明してあるくらいだしセットで覚えるものよね。

チャネルでの待ち合わせ

チャネルはmake()で作り、close()で閉じられる。使わなければよいだけなのだろうけど、明示的に「これ以降は使えない」というフラグを立ててくれるそうだ。

make(chan 型)で作るが、型は戻り値と思っておけばよさそうだ。
TypeScriptでいえばPromise<型>みたいな感じかな。
単に待ち合わせたいだけだったら戻り値はいらないのだが、そういうときはstruct {}を使えばよいようだ。golangにvoidはないのだね。

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Printf("start\n")
	done := make(chan struct{})
	go func() {
		fmt.Printf("goroutine start\n")
		time.Sleep(10 * time.Second)
		fmt.Printf("goroutine done\n")
		done <- struct{}{}
	}()
	<-done
	fmt.Printf("done\n")
}

実行。

$ go run .
start
goroutine start
goroutine done
done

10秒開けているのだけど、終わってからdoneが実行されている。

2022/06/05

[golang] go.mod の pseudo-version は go mod tidy で修正される

相変わらず go.mod で悩んでいた。

hiro99ma blog: [golang] go.mod の pseudo-version の調べ方がわからん
https://blog.hirokuma.work/2021/11/golang-gomod-pseudo-version.html

hiro99ma blog: [golang] go.mod の pseudo-version
https://blog.hirokuma.work/2021/12/golang-gomod-pseudo-version.html

なんとなく pseudo-version の書き方は分かったものの、面倒であることに変わりは無い。

開発中は replace で相対パス指定しておけばごまかせるのだが、GitHub Actions で linter を仕込んでいたりするとエラーになって気持ちが悪い。 Actions の方も相対パスで使えるようにすることができるのかも試練が、そこまでするくらいだったら go.mod を修正した方が楽そうだが、修正が面倒。。。

 

何か pseudo-version を自動で取ってくるツールがないか調べていた。

Mastering Go Modules Pseudoversions | JFrog GoCenter
https://jfrog.com/blog/go-big-with-pseudo-versions-and-gocenter/

真ん中当たりに「How to Fix Improper Pseudo-versions」という項目があり、修正の仕方が載っていた。なんと go mod tidy でうまいことやってくれるそうだ。

書き方は、pseudo-version を書く代わりに git の commit-id を書くだけ。これで go mod tidy すると pseudo-version に書き換えてくれた。 replace のところでもよいようだ。

今までの苦労は一体。。。

 

まあよい。できれば良かろうなのだ。

2022/05/29

[golang] 配列の比較とスライスの比較

似てるようで似ていない golang の配列とスライス。

 

配列同士の比較

package main

import "fmt"

func main() {
    array1 := [...]int{1, 2, 3}
    array2 := [...]int{2, 2, 3}
    fmt.Printf("array1 == array2: %v\n", array1 == array2)
    array2[0] = 1
    fmt.Printf("array1 == array2: %v\n", array1 == array2)
}

$ go run .
array1 == array2: false
array1 == array2: true

 

配列は宣言時の要素数がそのまま型として扱われると考えておくと良いだろう。
なので、[3]int と [4]int は別の方だから実行時に失敗するのではなくコンパイルエラーになる。

    array3 := [...]int{1, 2, 3, 4}
    fmt.Printf("array1 == array3: %v\n", array1 == array3)

invalid operation: array1 == array3 (mismatched types [3]int and [4]int)

 


スライスは直接の比較ができない。

    slice1 := array1[:]
    slice2 := array2[:]
    fmt.Printf("slice1 == slice2: %v\n", slice1 == slice2)

invalid operation: slice1 == slice2 (slice can only be compared to nil)

 

よく出てくるのが reflect.DeepEqual() を使う方法だった。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    array1 := [...]int{1, 2, 3}
    array2 := [...]int{2, 2, 3}
    array3 := [...]int{1, 2, 3, 4}

    slice1 := array1[:]
    slice2 := array2[:]
    slice3 := array3[:]
    fmt.Printf("slice1 == slice2: %v\n", reflect.DeepEqual(slice1, slice2))
    fmt.Printf("slice1 == slice3: %v\n", reflect.DeepEqual(slice1, slice3))
    array2[0] = 1
    fmt.Printf("slice1 == slice2: %v\n", reflect.DeepEqual(slice1, slice2))
    slice1 = append(slice1, 4)
    fmt.Printf("slice1 == slice3: %v\n", reflect.DeepEqual(slice1, slice3))
}

$ go run .
slice1 == slice2: false
slice1 == slice3: false
slice1 == slice2: true
slice1 == slice3: true

配列をスライスに変換しているのは、単に前のコードを使い回していただけで意味はない。
いま気付いたが、スライスに変換しているといっても、その場で値をコピーしてまるまる別物になるわけではないのだな。
なんでかというと、array2[0] に代入したら slice1 と slice2 が同じ値と判定されているからだ。
COW(Copy On Write)かな?

package main

import (
    "fmt"
)

func main() {
    array2 := [...]int{2, 2, 3}
    slice2 := array2[:]

    slice2[0] = 1
    fmt.Printf("array2[:]: %p\n", &array2)
    fmt.Printf("slice2: %p\n", slice2)
    slice2 = append(slice2, 4)
    fmt.Printf("slice2: %p\n", slice2)
}

$ go run .
array2[:]: 0xc0000ae000
slice2: 0xc0000ae000
slice2: 0xc0000a8030

さすがに明示的に変数への代入をしない限りは書き換わらないか。

 

[]int 型なので reflect.DeepEqual() を使ったが、 []byte 型の場合は bytes.Equal() がある。

package main

import (
    "bytes"
    "fmt"
)

func main() {
    array1 := [...]byte{1, 2, 3}
    array2 := [...]byte{2, 2, 3}
    array3 := [...]byte{1, 2, 3, 4}

    slice1 := array1[:]
    slice2 := array2[:]
    slice3 := array3[:]
    fmt.Printf("slice1 == slice2: %v\n", bytes.Equal(slice1, slice2))
    fmt.Printf("slice1 == slice3: %v\n", bytes.Equal(slice1, slice3))
    array2[0] = 1
    fmt.Printf("slice1 == slice2: %v\n", bytes.Equal(slice1, slice2))
    slice1 = append(slice1, 4)
    fmt.Printf("slice1 == slice3: %v\n", bytes.Equal(slice1, slice3))
}

$ go run .
slice1 == slice2: false
slice1 == slice3: false
slice1 == slice2: true
slice1 == slice3: true

 

シンプルなスライスだったら reflect.DeepEqual() を使うのにちゅうちょしないのだが、構造体のスライスとかになると心配になって使いづらい。説明が長いだけで使うのに不安を感じる。

理解して使えばよいのだが、それくらいだったら自分でループ回して比較した方が安心だと考えてしまう。比較するメソッドを作ればそこまで苦痛ではないだろう。

DeepEqual の実装は説明文に比べるとかなり短い。 go1.18.2 だとこうなっていた。

func DeepEqual(x, y any) bool {
    if x == nil || y == nil {
        return x == y
    }
    v1 := ValueOf(x)
    v2 := ValueOf(y)
    if v1.Type() != v2.Type() {
        return false
    }
    return deepValueEqual(v1, v2, make(map[visit]bool))
}

・片方でも nil があるなら単純比較
・型が違えば false
deepValueEqual()

短いのは条件だけだからだった......

単純比較は nil 同士なら true になるかと思ったのだがそうではなかった。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyType1 = struct {
        Value int
    }
    type MyType2 = struct {
        Value float32
    }
    var val1 *MyType1
    var val2 *MyType2
    val1 = nil
    val2 = nil
    fmt.Printf("val1=%v, val2=%v, compare=%v\n", val1 == nil, val2 == nil, any(val1) == any(val2))
    fmt.Printf("val1 == val2: %v\n", reflect.DeepEqual(val1, val2))
}

$ go run .
val1=true, val2=true, compare=false
val1 == val2: false

なお、型が違うので val1 == val2 と書くとコンパイルエラーになる。

 

DeepEqual() で期待している比較の処理は deepValueEqual() で行われていて、こちらは長い。
あまり見ていないが、型によって比較の仕方が違うから処理に時間がかかりそうだ。
パフォーマンスを気にするシーンだったら自前で書いた方がよさそう。

2022/05/28

[excel]

私がよく使う Excel 2016 では、線に関してはこのくらい選択肢がある。

image

 

線を何本か描いて、複数の関係があるのですよ、という図を描くことがしばしばある。
使えそうなのはこれらだ。

image

 

この記事を読むくらいだからわかると思うが、こういう描き方くらいでしか複数本を描くのは難しい。

image

ベジエ曲線のように、そこまで自由にはできないのだ。

面倒なので、私は間に円のオブジェクトを置いている。
曲線部分にも接続点があるので自由が多少きくからだ。

image

右下のだけ、Excel の補助無しで接続させた。
このくらい意図通りにならないので、だいたい自分で設定している。
楕円の中で接続する線がないが、それは描けば良いだけのことだ。


Excel 以外で描けば良いだけなのだが、私の開発スタイルが Excel を基準にしているのでちょっと難しいね。