Plan 9とGo言語のブログ

主にPlan 9やGo言語の日々気づいたことを書きます。

NodeJS+TypeScriptのchild_processモジュールで非同期にコマンドをパイプする

さいきん業務ではReactを使っていて、フロントに対する苦手意識も減ってきたので、いろいろ実験するための環境として自サイトをNextJSのSSGで生成するようにしたのですが、このとき、もともとはPlan 9mkfileでリソースを管理していた事情もあって、NodeJS+TypeScriptで複数の外部コマンドを扱う必要がありました。

NodeJSで非同期実行する場合、単純なコマンドならchild_processモジュールのspawnを使えばいいだけなのですが、

  • コマンドにファイル名を渡す以外の方法で標準入力にデータを流したい
  • コマンドの標準出力を、別コマンドの入力にしたい

などの場合に、どうすればいいのか分からなかったので調べました。ただ、最後にも書いているけどchild_processモジュールだけで実装するのは意外と大変なので、execaのようなライブラリを使ったほうがいいだろうと思います。

ファイルを指定して単一のコマンドを実行する

child_processモジュールにはコマンドを実行するための関数が複数ありますが、非同期で実行する場合はspawnを使います。コマンドの実行結果はStreamになっているので、以下のように書くと標準出力の内容を取得できます。

import { spawn } from "child_process";

async function main() {
    const p = await spawn("ls", ["-al"], {
        stdio: ["pipe", "pipe", "inherit"],
    });
    for await (const s of p.stdout){
        console.log(`${s}`);
    }
    const status = await new Promise((resolve, reject) => {
        p.on("close", resolve);
    });
    console.log("Status:", status);
}

main();

spawnstdioオプションは[(stdin), (stdout), (stderr)]の順になっていて、ここでpipeを与えるとプログラムから読み書きできるようになり、inheritの場合は親プロセスのファイルディスクリプタを共有します。上記コード例の場合は、stdinstdoutpipeなのでプログラムから読み書きできる状態でプロセスを実行しますし、stderrのところがinheritなので、コマンドのエラーは標準エラー出力にそのまま流れます。ただし、ここで実行しているlsコマンドは標準入力を読まないので、stdinpipeになっている意味はありません。

標準エラー出力を読む場合は、stdioオプションをpipeに設定して、p.stdoutの代わりにp.stderrを読めば得られます。

任意のストリームをコマンドに渡したい

コマンドの扱う入力がファイルで足りる場合はspawnだけで良いのですが、コマンドに流したいデータは必ずしもファイル名で与えられるとは限りません。例えばHTTPで取得したレスポンスかもしれないし、別のコマンドを実行した結果かもしれません。具体的には、

createReadStream("file").pipe(spawn("sed", ...)).pipe(spawn("wc", ...))

のようにStreamを繋げたい場合は、意外と素直に書くことができませんでした。spawnstdioオプションはStreamを受け取れるので、

import { spawn } from "child_process";
import { createReadStream } from "fs";

async function main() {
    const fin = createReadStream("package.json", "utf-8");
    const p = await spawn("wc", ["-l"], {
        stdio: [fin, "pipe", "inherit"],
    });
    ...
}

とすれば実現できるかというと、このコードはERR_INVALID_ARG_VALUEで失敗します。

$ npx ts-node main.ts 
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'stdio' is invalid. Received ReadStream {
  path: 'package.json',
  flags: 'r',
  mode: 438,
  fd: null,
  start: undefined,
  end: Infinity,
  pos: undefine...
    at new NodeError (node:internal/errors:371:5)
    at node:internal/child_process:1042:13
    at Array.reduce (<anonymous>)
    at getValidStdio (node:internal/child_process:967:11)
    at ChildProcess.spawn (node:internal/child_process:354:11)
    at Object.spawn (node:child_process:698:9)
    at main (/home/lufia/Downloads/x/main.ts:6:18)
    at Object.<anonymous> (/home/lufia/Downloads/x/main.ts:18:1)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Module.m._compile (/home/lufia/Downloads/x/node_modules/ts-node/src/index.ts:1311:23) {
  code: 'ERR_INVALID_ARG_VALUE'
}

どうやらcreateReadStreamしただけではfinが内部に持っているファイルディスクリプタがまだnullのようです。fin.on("data", ...)すればファイルディスクリプタがセットされそうでしたが、それだと本当はコマンドに流したいデータがコールバック関数に流れてしまうので、意図していた動作にはなりません。

Transformを実装してパイプする

Stramには、読み込み用のReadableや書き込み用のWritable以外にも、ストリームを流れるデータを加工して流すためのTransformが用意されています。上でやりたかったコマンドのパイプも、このTransformを実装すると実現できます。Transformの実装方法は、コンストラクタに変換するための関数を渡す方法と、クラスとして実装する方法の2種類ありますが、ここでは前者で実装しました。

import { ChildProcess, spawn } from "child_process";
import { createReadStream } from "fs";
import { Transform } from "stream";

function createTransform(p: ChildProcess): Transform {
    const data: string[] = [];
    p.stdout?.on("data", s => {
        data.push(s); // 実行したコマンドの標準出力を貯めておく
    });

    const t = new Transform({
        // 定期的に呼ばれるので貯めていたデータを流す
        transform: (s, encoding, callback): void => {
            // 前プロセスから届いたデータをコマンドに流す
            p.stdin?.write(s);

            // コマンドの結果を次のストリームに渡す
            while(data.length > 0)
                t.push(data.shift());
            callback();
        },
        final: async (callback): Promise<void> => {
            // コマンドが終わるのを待つ
            p.stdin?.end();
            const status = await new Promise((resolve, reject) => {
                p.on("close", resolve);
            });
            if(status !== 0)
                throw new Error(`${p.spawnfile}: exit with ${status}`);

            // p.stdin.end()の後で貯まったデータを次のストリームへ全部流す
            while(data.length > 0)
                t.push(data.shift());
            callback();
        },
    });
    return t;
}

// 使用例
async function main() {
    const fin = createReadStream("package.json", "utf-8");
    const p = spawn("grep", ["ts"], {
        stdio: ["pipe", "pipe", "inherit"],
    });
    const t = createTransform(p);
    const stream = fin.pipe(t);
    try {
        for await (const s of stream){
            console.log(`${s}`);
        }
    } catch(e) {
        console.error(e.message)
    }
}

pipeではなくpipelineを使う

ところで、pipeでストリームを接続した場合、ストリームのうちどれか1つでもエラーが発生すると、残りのプロセスが回収されない問題があります。イベントを監視して適切な対応をすればいいのですが、接続するプロセスが多くなると面倒なので、今はpipelineを使うとよいそうです。

ただし、pipeと異なりpipelineは戻り値がPromise<void>になっているため、これまでのようにfor...ofを使った出力結果の取得ができません。代わりにpipelineでは、最後の引数にWritableなストリームを受け取るようになっていて、例えばファイルに書き出す場合はここへcreateWriteStreamで生成したストリームを渡します。

とはいえ、ここではコマンドの実行結果をプログラムから扱いたいだけなので、ファイルに書き出す必要はありません。流れてきたデータをメモリ上で保持するためのWritableなストリームを探したのですが見つからなかった*1ので、以下のようにWritableMemoryStreamを用意して対応しました。

import { ChildProcess, spawn } from "child_process";
import { createReadStream } from "fs";
import { Transform, Writable } from "stream";
import { pipeline } from "stream/promises";

async function main() {
    const fin = createReadStream("package.json", "utf-8");
    const grep = spawn("grep", ["ts"], {
        stdio: ["pipe", "pipe", "inherit"],
    });
    const wc = spawn("wc", ["-l"], {
        stdio: ["pipe", "pipe", "inherit"],
    });
    const w = new WritableMemoryStream();
    try {
        await pipeline(fin, createTransform(grep), createTransform(wc), w);
    } catch(e) {
        console.error(e.message)
    }
    console.log(w.toString());
}

function createTransform(p: ChildProcess): Transform {
    const data: string[] = [];
    p.stdout?.on("data", s => {
        data.push(s);
    });

    const t = new Transform({
        transform: (s, encoding, callback): void => {
            p.stdin?.write(s);
            while(data.length > 0)
                t.push(data.shift());
            callback();
        },
        final: async (callback): Promise<void> => {
            p.stdin?.end();
            const status = await new Promise((resolve, reject) => {
                p.on("close", resolve);
            });
            if(status !== 0)
                throw new Error(`${p.spawnfile}: exit with ${status}`);
            while(data.length > 0)
                t.push(data.shift());
            callback();
        },
    });
    return t;
}

class WritableMemoryStream extends Writable {
    private data: string[];

    constructor() {
        super();
        this.data = [];
    }

    _write(data: any, encoding: BufferEncoding, callback: (error?: Error | null) => void) {
        this.data.push(data);
        callback();
    }

    toString(): string {
        return this.data.join("");
    }
}

Transformと同じように、Writableのコンストラクタを使ってもよかったけれど、toStringで最終的な結果を取得する方が自然に思えたのでクラスとして実装しています。

感想

パイプの実装なんてCで何度も書いたことがあるし、spawnインターフェイスがやりたいことを満たしてそうだったのもあって、油断してchild_processだけで対応してしまったけれど、やってみると意外と大変でした。もっとうまい実装方法はあるかなとは思いつつ、個人的には、次に同じ処理が必要ならexecaのようなライブラリを使うと思います。

*1:npmにmemorystreamはあるけどメンテナンスされてなさそうだった

Plan 9 CのARGBEGIN、ARGENDマクロ

Plan 9のCは独自のライブラリを持っていて、stdio.hのようなANSIライブラリもAPE(ANSI/POSIX Environment)として存在してはいるが、基本的にはPlan 9独自のライブラリを使う方が好まれる。ANSI Cとの違いは挙げればいくつもあるのでPlan 9 Cを知らない人には伝わりづらいけれども、Goの標準ライブラリにもPlan 9のCライブラリの面影が部分的に残っているので、Goの基礎的なパッケージ(bufio, utf8, strconv, stringsなど)に似ているといえば伝わるかもしれない。いくつか挙げると、Plan 9 Cではバッファリングを行うライブラリはlibbioにまとまっているが、libbio

#include <bio.h>

int Binit(Biobuf *bp, int fd, int mode);
int Brdline(Biobufhdr *bp, int delim);

は、ほとんどGoの

package bufio

func NewReader(rd io.Reader) *Reader
func (b *Reader) ReadBytes(delim byte) ([]byte, error)

と同じ使用感になっているし、

#include <libc.h>

int tokenize(char *str, char **args, int maxargs);

も、言語の違いこそあるものの、Goのstrings.Fieldsとだいたい同じように使える。また、UTF-8Plan 9から出てきたものなので当然といえば当然だけど、Plan 9 CはGoと同様に、Unicode文字を扱う場合はRune*1を使う。他にも探せば似たものはいくつもあるけれど、詳細には取り挙げない。

コマンドラインパーサ

Plan 9独自のCライブラリには、コマンドラインオプションを解析するARGBEGINマクロとARGENDマクロがある。これらはコマンドラインオプションを扱うときに使用するマクロで、典型的にはEARGFなどと組み合わせて以下のように使う。

#include <u.h>
#include <libc.h>

int debug;
char *file;

void
usage(void)
{
    fprint(2, "usage: %s [-f file] [-v] [arg ...]\n", argv0);
    exits("usage");
}

void
main(int argc, char **argv)
{
    int i;

    ARGBEGIN {
    case 'f':
        file = EARGF(usage());
        break;
    case 'v':
        debug++;
        break;
    default:
        usage();
    } ARGEND

    print("debug: %d\n", debug);
    print("file: %s\n", file);
    for(i = 0; i < argc; i++)
        print("argv[%d]: %s\n", i, argv[i]);
    exits(nil);
}

特筆すべきところは、ARGENDを抜けた後のargv[0]は、コマンド名ではなくオプションを除いた最初の引数を参照している。上記コードを実行結果は以下のようになる。

% opts -f file -v a b c
debug: 1
file: file
argv[0]: a
argv[1]: b
argv[2]: c

% opts -f # 引数が足りない場合
usage: opts [-f file] [-v] [arg ...]

ロングオプションは対応していないけれど、Plan 9のコマンドはオプションがあまりないので、これで充分なんだろうと思う。cat(1)はオプションが無いし、cp(1)は3個、ls(1)でも12個しかない。

usage: ls [-dlmnpqrstuFQ] [file ...]

usage: cp [-gux] fromfile tofile

サブコマンド

Plan 9の標準シェルであるrc(1)は、$path配下のディレクトリも検索対象となるので、

% fossil/conf -w file /dev/sd01/fossil
% auth/factotum
% upas/fs

のように関連するコマンドをディレクトリでまとめることが普通となっていて、サブコマンドが使われることはほとんどない*2が、ARGBEGINマクロを使ってサブコマンドを実装できるので参考程度に書いておく。単純に、サブコマンド関数の引数がargcargvになっていればARGBEGINマクロを利用できる。

#include <u.h>
#include <libc.h>

void cmdinit(int, char **);
void cmdprint(int, char **);

struct {
    char *name;
    void (*cmd)(int, char **);
} cmds[] = {
    { "init", cmdinit },
    { "print", cmdprint },
};

int debug;
char *file;

void
usage(void)
{
    fprint(2, "usage:\t%s init [-y]\n", argv0);
    fprint(2, "\t%s print\n", argv0);
    exits("usage");
}

void
main(int argc, char **argv)
{
    int i;

    ARGBEGIN {
    case 'f':
        file = EARGF(usage());
        break;
    case 'v':
        debug++;
        break;
    default:
        usage();
    } ARGEND
    if(argc == 0)
        usage();

    for(i = 0; i < nelem(cmds); i++)
        if(strcmp(cmds[i].name, argv[0]) == 0){
            cmds[i].cmd(argc, argv);
            exits(nil);
        }
    usage();
}

void
cmdinit(int argc, char **argv)
{
    int yes;

    yes = 0;
    ARGBEGIN {
    case 'y':
        yes++;
        break;
    default:
        usage();
    } ARGEND
    print("init: debug=%d yes=%d\n", debug, yes);
}

void
cmdprint(int argc, char **argv)
{
    ARGBEGIN {
    default:
        usage();
    } ARGEND
    print("print: file=%s\n", file);
}

*1:以前は16bitだったけど今は22bitのはず

*2:OS標準のコマンドでは全く見た記憶がない

systemd-logindにSuspend key pressedと記録されてサスペンドする問題とthermal-conf.xmlの書き方

12インチMacBookLinuxをインストールして使っていたが、負荷が上がったときにMacBookサスペンドする問題に困っていた。サスペンドが発生した時刻には、systemd-logindのログに

systemd-logind[299]: Suspend key pressed.

のようなイベントが記録されていた。このログは、LinuxカーネルKEY_SLEEPと定義されたキーが押されたときにsystemd-logindbutton_dispatch関数が記録しているものだった。button_dispatchの主な処理を引用する。

static int button_dispatch(sd_event_source *s, int fd, uint32_t revents, void *userdata)
{
    Button *b = userdata;
    struct input_event ev;
    ssize_t l;

    l = read(b->fd, &ev, sizeof ev);
    switch(ev.code){
    case KEY_SLEEP:
        log_struct(LOG_INFO, "Suspend key pressed.", ...);
        manager_handle_action(b->manager,
            INHIBIT_HANDLE_SUSPEND_KEY,
            b->manager->handle_suspend_key,
            b->manager->suspend_key_ignore_inhibited,
            true);
        break;
    case KEY_SUSPEND:
        log_struct(LOG_INFO, "Hibernate key pressed.", ...);
        ...
    }
}

KEY_SLEEP定数はLinuxのヘッダファイルで定義されたもので、カーネルsystemdではSuspendが意味するものが異なっているところは少し難しいが、とにかくスリープのようなキーが押されたことを意味する。

#define KEY_SLEEP 142 /* SC System Sleep */
#define KEY_SUSPEND 205

上のコードを読む限りでは、button_dispatchはキーイベントをread(2)しているだけなので、少なくともなんらかのハードウェアが原因だろうと思ったが、MacBookには電源ボタンはあるけれど押していないし、なんならリッドクローズドで使っている時は電源ボタンもないキーボードを使っているので、誤って物理キーを押したわけではない。

systemd-logindでサスペンドを無効化してみる

systemd-logindサスペンドキーが押されたときの動作を/etc/systemd/logind.confで変更できる。例えば以下のようにignoreを設定するとキーイベントを無視できる。

-#HandleSuspendKey=suspend
+HandleSuspendKey=ignore

これは実際に、systemd-logindmanager_handle_actionで、HANDLE_IGNOREの場合はすぐに関数を抜けているところからもイベントを無視している様子が読み取れる。

 /* If the key handling is turned off, don't do anything */
if (handle == HANDLE_IGNORE) {
    log_debug("Handling of %s (%s) is disabled, taking no action.",
        inhibit_key == 0 ? "idle timeout" : inhibit_what_to_string(inhibit_key),
        is_edge ? "edge" : "level");
    return 0;
}

しかし試したけれど残念ながら、状況は改善されなかった。負荷をかけるとすぐにサスペンドしてしまった。

デスクトップ環境でサスペンドを無効にする

GNOMEKDEなどのデスクトップ環境は独自の電源管理を行っているので、これを無効化してみる。GNOMEの場合は、関連する設定は以下の通り。

$ gsettings get org.gnome.settings-daemon.plugins.power power-button-action
'suspend'
$ gsettings get  org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type
'suspend'
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type
'suspend'
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout
200
$ gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout
1200

これらの設定を無効にする。

$ gsettings set org.gnome.settings-daemon.plugins.power power-button-action nothing
$ gsettings set  org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type nothing
$ gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing

これも、設定したあとで負荷をかけて様子をみたが、再発してしまったので関係がなかった。

CPU周波数を落とす

12インチMacBookファンレスなので、熱の問題だろうと当たりをつけて対策する。このハードウェアのデフォルトはintel_pstateドライバのpowersaveガバナー(Governor)だった。

$ grep . /sys/devices/system/cpu/cpu?/cpufreq/scaling_driver
/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver:intel_pstate
/sys/devices/system/cpu/cpu1/cpufreq/scaling_driver:intel_pstate
/sys/devices/system/cpu/cpu2/cpufreq/scaling_driver:intel_pstate
/sys/devices/system/cpu/cpu3/cpufreq/scaling_driver:intel_pstate

$ grep . /sys/devices/system/cpu/cpu?/cpufreq/scaling_governor
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu1/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu2/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu3/cpufreq/scaling_governor:powersave

Intel Turbo Boostを無効にする

Turbo Boostは使っていないコアの電源を落とす替わりに、特定コアのクロックを上げる仕様らしい。

Turbo Boostはno_turboファイルに1を書き込むことで無効にできる。

$ echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo

TurboBoostを無効にすると、突然サスペンドすることはなくなったが、クロックが最大1.4GHzまで落ちてしまうので普段使いするには少々厳しい性能になってしまった。

cpuの最大クロック数を制限する

cpupowerを使ってクロックを落とす方法。

$ sudo pacman -S cpupower
$ sudo cpupower frequency-set -u '2.2GHz'

これで全てのコアが最大2.2GHzに制限される。ただしこの設定は再起動すると消えるので、永続化したい場合はsystemdで実行する。cpupowerをインストールしていれば、/etc/default/cpupowerに設定ファイルがあるので利用するといい。

-#max_freq="3GHz"
+max_freq="2.2GHz"

その後、systemctlでサービスを有効にする。

$ systemctl enable cpupower.service

クロックを変更して実験した結果、2.2GHzまで落とさなければサスペンドが発生していた。実験したときは5月だけど、夏はもっと下げなければだめかもしれない。もっと下げると、TurboBoost無効時の1.4GHzとそれほど変らず、性能の劣化が気になる。

Thermaldで冷却する

一般的にはインストールして起動するだけで適切な動作をする。Ask Ubuntuの回答によると、このモードはZero Configuration Modeといって、DTS温度測定センサーやIntel P-stateを使って適切な調整をする。

coretempドライバのドキュメントは以下のURLにあった。

だけれども、MacBookの場合はZero Configuration Modeでは冷却されている様子がなかったので、/etc/thermald/thermal-conf.xmlで設定することにした。この方法は、上記Ask Ubuntuの回答によるとUser defined configuration modeにあたる。Thermaldに関係するマニュアルは以下の2つ。

thermal-conf.xml(5)には設定例もあるので、読めばだいたい雰囲気で分かるのだけど、難しかったところを少しまとめる。thermal-conf.xmlは大きく分けて3つの要素で構成される。

  • ThermalSensor
  • CoolingDevice
  • TripPoint

ThermalSensor

センサーは温度を計測する場所のことで、たとえばCPUコアなどがそれに該当する。センサーを扱うためには/sys以下のファイルなどを<ThermalSensor>要素で設定が必要になるが、/sys/class/thermal以下のデバイスはデフォルトで組み込まれているので、特に設定を追加せずとも利用できる。12インチMacBookの場合は以下の通り。

$ grep . /sys/class/thermal/thermal_zone*/type
/sys/class/thermal/thermal_zone0/type:BAT0
/sys/class/thermal/thermal_zone1/type:x86_pkg_temp

BAT0はバッテリーで、x86_pkg_tempはCPUパッケージかな。/sys/class/thermal以下のファイル郡はACPI由来のもので、マニュアルではThermal sysfsと呼ばれている。/sys/class/thermalカーネルドキュメントは以下のURLにある。

Thermal sysfs以外にも、MacBookには/sys/devices/platform/coretemp.0/subsystem/devices/applesmc.768などのセンサーがある。これらThermal sysfs以外のセンサーを参照して温度管理をする場合は、<ThermalSensor>要素を使った設定が必要になる。sensorsコマンドを実行すると、他にどのようなセンサーがあるのか調べられる。

$ sensors
BAT0-acpi-0
Adapter: ACPI interface
in0:           8.58 V  
temp:         +32.8°C  
curr1:         0.00 A  (avg =  +0.00 A)

coretemp-isa-0000
Adapter: ISA adapter
Package id 0:  +44.0°C  (high = +100.0°C, crit = +100.0°C)
Core 0:        +43.0°C  (high = +100.0°C, crit = +100.0°C)
Core 1:        +43.0°C  (high = +100.0°C, crit = +100.0°C)

applesmc-isa-0300
Adapter: ISA adapter
TA0V:         +29.0°C  
TB0T:         +32.5°C  
TB1T:         +32.0°C  
TB2T:         +32.5°C  
TBXT:         +32.5°C  
TC0E:         +43.8°C  
TC0F:         +46.0°C  
TC0P:         +39.5°C  
TC1C:         +43.0°C  
TC2C:         +43.0°C  
TCGC:         +43.0°C  
TCHP:         +37.0°C  
TCSA:         +44.0°C  
TCXC:         +43.5°C  
TH0A:         +37.2°C  
TH0B:        -127.0°C  
TH0C:        -127.0°C  
TH0F:         -47.8°C  
TH0R:         -47.8°C  
TKBV:         +36.8°C  
TM0P:         +38.8°C  
TPCD:         +39.0°C  
TPMV:         +31.8°C  
TW0P:         +37.2°C  
Th0N:         +36.2°C  
Th0R:         +31.8°C  
Ts0P:         +32.2°C  
Ts0S:         +36.0°C  
Ts1P:         +30.5°C  
TsCH:         +47.5°C  
TsFD:         +60.0°C  
TsFS:         +53.0°C  
TsHS:          +2.0°C  
TsTH:          +3.0°C  
TsTP:         +50.0°C  
TsWS:         +49.0°C  

BAT0-virtual-0
Adapter: Virtual device
temp1:        +32.8°C  

nvme-pci-0100
Adapter: PCI adapter
Composite:    +39.9°C  

sensorsコマンドで出力された結果の意味は、以下の記事が参考になると思う。

CoolingDevice

冷却デバイスは温度を下げるための機構またはハードウェアのこと。空冷ファンは当然だけど、CPUのクロックを落とすしくみのような機構もCoolingDeviceに該当する。これもセンサーと同様に、/sys/class/thermal以下のデバイスは何も書かなくても利用できる。12インチMacBookの場合は以下の通り。

$ grep . /sys/class/thermal/cooling_device*/type
/sys/class/thermal/cooling_device0/type:Processor
/sys/class/thermal/cooling_device1/type:Processor
/sys/class/thermal/cooling_device2/type:Processor
/sys/class/thermal/cooling_device3/type:Processor
/sys/class/thermal/cooling_device4/type:intel_powerclamp
/sys/class/thermal/cooling_device5/type:LCD

また、マニュアルによると、以下のデバイスもCoolingDeviceとして使えるらしい。

  • rapl_controller
  • intel_pstate (CPU周波数ドライバ)
  • cpufreq
  • LCD (モニターのバックライトを薄暗くする)

それぞれがどんな動作をするのかは、ArchWikiのCPU 周波数スケーリングが詳しい。

TripPoint

この要素で、センサーと冷却デバイスをまとめて温度管理を行う。例えば以下のような設定になる。

<TripPoint>
    <SensorType>x86_pkg_temp</SensorType>
    <Temperature>75000</Temperature>
    <type>passive</type>
    <ControlType>PARALLEL</ControlType>
    <CoolingDevice>
      <index>1</index>
      <type>rapl_controller</type>
      <influence>50</influence>
      <SamplingPeriod>1</SamplingPeriod>
    </CoolingDevice>
</TripPoint>

<Temperature>は冷却を開始するセンサーの温度を設定する。75000の場合は75℃以上になったら開始する。<type>は以下の3種類。

  • active
  • passive
  • max

activeは、空冷ファンなどコストのかかる(追加の電源やノイズなどが発生する)を使う。passiveならパフォーマンスを落として温度を下げる。maxについてはよくわからない。

<SamplingPeriod>を設定すると、前回の変化から設定した秒が経過するまでは温度の変化を検出しなくなる。未設定または0なら常に検出する。

設定例

現在設定している/etc/thermald/thermal-conf.xmlを貼っておく。だいたい期待どおりに動いているが、長時間の負荷を与えたときはまだ稀にサスペンドする場合があるので、もう少し調整は必要だと思う。

<?xml version="1.0"?>
<ThermalConfiguration>
  <Platform>
    <Name>Macbook 2017</Name>
    <ProductName>*</ProductName>
    <Preference>QUIET</Preference>   <!-- 空冷ファンなどactiveなデバイスを使わず冷却する -->
    <ThermalZones>
      <ThermalZone>
        <Type>x86_pkg_temp</Type>
        <TripPoints>
          <TripPoint>
            <SensorType>x86_pkg_temp</SensorType>
            <Temperature>75000</Temperature>
            <type>passive</type>
            <ControlType>PARALLEL</ControlType>
            <CoolingDevice>
              <index>1</index>
              <type>rapl_controller</type>
              <influence>50</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>2</index>
              <type>intel_pstate</type>
              <influence>40</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>3</index>
              <type>intel_powerclamp</type>
              <influence>30</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>4</index>
              <type>cpufreq</type>
              <influence>20</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>5</index>
              <type>Processor</type>
              <influence>10</influence>
            </CoolingDevice>
          </TripPoint>
        </TripPoints>
      </ThermalZone>
    </ThermalZones>
  </Platform>
</ThermalConfiguration>

以下のURLにも設定例が書かれているので、参考になる。

FIDO U2Fセキュリティキーを使ってSSHする

今年の5月に、GitHubがFIDO U2Fセキュリティキーを利用したSSH接続に対応したので、手元にあったTitan Security Keyで試してみました。

Titan Security Keyを買ったときの話はこちら。

SSH鍵の生成

SSHでFIDO U2Fセキュリティキーを参照するためにはOpenSSH 8.2以上が必要です。Arch Linuxでは少なくともopenssllibfido2の2つが必要なのでpacmanでインストールします。

$ sudo pacman -S openssh libfido2
$ ssh -V
OpenSSH_8.6p1, OpenSSL 1.1.1k  25 Mar 2021

また、鍵ペアはecdsa-skまたはed25519-skで作っておく必要があります。セキュリティキーを接続した状態でssh-keygenを使って作成しましょう。YubiKeyなど、この時点でセキュリティキーへのタッチを要求される場合もあるそうですが、Titan Seciruty Keyではタッチは要求されず、パスフレーズを聞かれただけでした。

$ ssh-keygen -t ecdsa-sk -C user@example.org
Generating public/private ecdsa-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter file in which to save the key (/home/lufia/.ssh/id_ecdsa_sk): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/lufia/.ssh/id_ecdsa_sk
Your public key has been saved in /home/lufia/.ssh/id_ecdsa_sk.pub
SHA256:h7O7eF62nwJCHxnnelOX0QUlufa144r/gjOZWBWhmnM lufia@linux-pc
(snip)

ここで、セキュリティキーを接続せずにssh-keygenを実行すると、

Key enrollment failed: invalid format

というエラーで終了します。

ecdsa-skまたはed25519-sk(以下めんどうなのでecdsa-skだけを表記します)では、他のものと同様にid_ecdsa_skid_ecdsa_sk.pubの2つファイルを生成しますが、他のものと異なり秘密鍵はセキュリティキーに保存*1されていて、id_ecdsa_skファイルには、セキュリティキーへの参照などの盗まれても安全な情報だけが含まれるようです。id_ecdsa_skのフォーマットはデフォルトではRFC 4716 - The Secure Shell (SSH) Public Key File Formatです。

id_ecdsa_skファイルに保存されている具体的な情報については面倒なので追ってませんが、秘密鍵をセキュリティキーに要求する部分のコードはsshconnect2.c:1232-1270の辺りなので、ここでsshkey_is_sksshkey_typeするために必要な情報が含まれているのでしょう。興味があればload_identity_filesshkey_load_private_type辺りを読んでみてください。

SSH鍵を使う

ecdsa-sk鍵を用意できたら、GitHubのSettings → SSH and GPG keysに公開鍵(id_ecdsa_sk.pub)を登録します。ここはよくある手順なので省略。

登録が終わったら、以下のコマンドで確認してみましょう。

$ ssh -T -i id_ecdsa_sk git@github.com
Enter passphrase for key 'id_ecdsa_sk': ***
Confirm user presence for key ECDSA-SK SHA256:h7O7eF62nwJCHxnnelOX0QUlufa144r/gjOZWBWhmnM
User presence confirmed
Hi lufia! You've successfully authenticated, but GitHub does not provide shell access.

実行例だと分かりづらいですが、

Confirm user presence for key ECDSA-SK SHA256:h7O7eF62nwJCHxnnelOX0QUlufa144r/gjOZWBWhmnM

のところで止まるので、セキュリティキーにタッチすると先に進みます。セキュリティキーが接続されていない場合は、

sign_and_send_pubkey: signing failed for ECDSA-SK "id_ecdsa_sk": invalid format

というエラーで接続に失敗します。

感想

最近は、GitHubへのアクセスではアクセストークンを使っているし、SSHでリモートログインする機会もだいぶ減りましたが、とはいえたまに使うことはあるので、ファイルを盗まれても安全で、TOTPほど面倒でもないのはいいですね。ただ、セキュリティキーが手元にないときは何もできないので忘れないようにしましょう。

セキュリティキー便利なので使っていきたいんですが、意外と対応しているサービスは少ないですね*2AWS CLIでもFIDO U2Fセキュリティキーがサポートされたら嬉しいのですが、コンソールログインでは使えるけどCLIでは使えず、MFAデバイスは1つだけしか登録できない制約があるので、現状ではTOTPするしかないのが悲しい*3

*1:Titan Security Keyは複数の鍵を管理する機能を持っていないと思いますが

*2:Mackerelも対応していないけどそこは見なかったことに...

*3:Mackerelも1つしか登録できないけどこれも忘れてください...

デスクトップ環境をKDE 5 Plasmaデスクトップに変更した

先月の記事ではGNOMEをインストールしましたが、KDE 5 Plasmaデスクトップの方が見た目も操作感も好みだったので変更しました。WebブラウザとターミナルとPlan 9ツールの利用が主な使い方なので、どれを使ってもそんなに変わらないのですが。

やったこと

まず必要なパッケージを追加します。Waylandバックエンドを使うのでplasma-wayland-sessionも入れておきます。plasma-metaはメタパッケージといって複数のパッケージがまとまったものです。

$ sudo pacman -S plasma-meta sddm plasma-wayland-session

KDEアプリケーションはこれだけ入れました。kde-applicationsはパッケージグループで、こちらも複数のパッケージをまとめたものです。

$ pacman -Qge kde-applications
kde-applications ark
kde-applications dolphin
kde-applications gwenview
kde-applications kcalc
kde-applications konsole
kde-applications ksystemlog
kde-applications okular

pacmanのメタパッケージとパッケージグループ

上でみたように、パッケージグループとメタパッケージはどちらも複数のパッケージをまとめるものですが、利用方法は全然別のものです。

パッケージグループ

  • pacman -gフラグでグループの絞り込みができる
  • パッケージグループをインストールする時に必要なパッケージを選択できる
  • 選択したパッケージは明示的インストールとして扱う
  • グループに新しい依存パッケージが追加されても追従しない

メタパッケージ

  • パッケージ名に-metaが付く(全てではないらしい)
  • メタパッケージをインストールする時はパッケージの選択はできない
  • メタパッケージを明示的インストールとして、個別のパッケージは依存パッケージとして扱う
  • メタパッケージに新しい依存が増えた場合、メタパッケージをアップデートするとインストールされる

SDDM

ログイン画面でSDDMを使うようにします。どのディスプレイマネージャを使うのかについては、/etc/systemd/system/display-manager.serviceシンボリックリンクになっていて、SDDMの場合は参照先が/usr/lib/systemd/system/sddm.serviceとなります。必ず1つしか有効にできないので、GDMは無効にします。

$ sudo systemctl disable gdm.service
$ sudo systemctl enable sddm.service

/etc/sddm.conf.d/uid.conf

SDDMはデフォルトにより、UIDが60000以上のユーザを扱いません。だけどもsystemd-homedでユーザを管理している場合、UIDは60000より大きな値が使われるので、SDDMにユーザが表示されずログインできません。なのでUIDの最大値を変更します。

[Users]
MaximumUid=60350

/etc/sddm.conf.d/hidpi.conf

12インチMacBookRetinaディスプレイなので、HiDPIオプションも有効にしておきます。

[Wayland]
EnableHiDPI=true

[X11]
EnableHiDPI=true

Firefox

WaylandバックエンドのPlasmaデスクトップでは、FirefoxはXWaylandで動作するようですが、HiDPIディスプレイで使うと全体的にぼやけたり滲んだりといった表示になります*1FirefoxはWaylandに対応しているので、環境変数で切り替えましょう。

~/.pam_environment

+MOZ_ENABLE_WAYLAND DEFAULT=1

ところで、このファイルはpam_envによるものみたいですね。マニュアルはpam_envにありました。

ショートカットキー

Plasmaデスクトップの場合でも、GTKベースアプリケーションはgsettings(dconf)の設定を参照します。Firefoxも同様なので、以下の設定は必要です。

$ gsettings set org.gnome.desktop.interface gtk-key-theme Emacs

IBus

GNOMEの場合は何もしなくてもIBusを使えていましたが、KDEでは自分で書いてあげる必要があるようです。~/.config/autostart以下に設定を作ります。

~/.config/autostart/ibus-daemon.desktop

[Desktop Entry]
Type=Application
Name=IBus Daemon
Exec=ibus-daemon -drx --panel=/usr/lib/kimpanel-ibus-panel

~/.config以下にはautostart-scriptsディレクトリもあります。autostartは拡張子.desktopなiniファイルを管理する場所で、autostart-scriptsシェルスクリプトなどを管理するために使うようです。

KDEシステム設定

基本的にはデフォルトのまま使って慣れる派ですが、どうしても無理なものをシステム設定で変更しました。変更した箇所だけ列挙。

  • キーボードの設定でCapsLockをControlに置き替え
    • キーボード→詳細→Ctrl position/Caps LockをCtrlとして扱う
  • トラックパッドをいつも通りに設定
    • タップしてクリック
    • タップしてドラッグ
    • Tap-and-drag lock
    • スクロールの方向を反転(自然なスクロール)

ショートカット→KRunner/KRunnerで、KRunner*2の開始をCtrl+Spaceでもできるようにしようかと思ったけど、これは慣れの問題なのでAlt+Spaceのまま使うことにしました。

気になりメモ

WaylandバックエンドでPlasmaデスクトップを使う場合、スリープからの復帰や外部ディスプレイ接続などで突然クラッシュすることがあります。Waylandでの不具合などは以下でまとめられています。

GNOME関連のパッケージをアンインストールしてもgsettings list-schemasで確認するとorg.gnome.*スキーマが残っています。これらのスキーマgsettings-desktop-schemasによって追加されたもので、このパッケージはphonon-qt5-gstreamergtk3などが依存しているようでした。

*1:どうして滲むのかはHiDPI support in Chromiumで説明されていました

*2:macOSにおけるSpotlightのようなもの

12インチMacBookにArch Linuxをインストールした

手元のデスクトップ環境をLinuxに切り替えました。2009年頃からmacOS(当時はMac OS X)を使っていたけど、QEMUFUSEを不自由なく使える方がPlan 9との相性が良いので、Linuxの方がいいかなと思ったのでした。

やったこと

MacBook10,1 (Retina, 12-inch, 2017)にArch Linuxをインストールしました。このハードウェアではネットワークなど一通り使えていますが、バージョンによっては使えないケースもあるようです。MacBook Proの対応状況はState of Linux on the MacBook Pro 2016 & 2017にまとまっていますが、MacBookのものは無いので、近いハードウェアから推測する必要があります。

バックアップを取得

事前にMacBookのバックアップを取得しましょう。Time Machineがいちばんお手軽ですし、Linuxにデータを渡すことも考えるならexFATなディスクにコピーしておくと便利です。

また、macOSインストーラを用意しておくと、困った場合に安心です。Mac App StoreからOSインストーラをダウンロードすると/Applicationsインストーラが作られるので、以下のコマンドを実行します。

% sudo /Applications/Install\ macOS\ Big\ Sur.app/Contents/Resources/createinstallmedia --volume /Volumes/Mobile

詳細は、公式ドキュメントのmacOS の起動可能なインストーラを作成する方法を参照してください。

Arch Linuxインストーラを準備

Arch Linuxのイメージをダウンロードから探して、USB インストールメディアの「BIOSUEFI ブータブル USB」に書かれている手順を実行します。

% diskutil unmountDisk /dev/diskX
% sudo dd if=archlinux-2021-04-01-x86_64.iso of=/dev/rdiskX bs=1M

余談ですがこのISOイメージは、ddコマンドを使ってそのままUSBメモリに書き込むだけで使えるように作られています。xorrisoとUEFIブート再び[その1]に詳細が書かれているけれどハック感があって面白いです。

インストール

上で作ったUSBメモリからインストーラを起動して、セットアップしていきます。事前に以下のドキュメントを読んでおくといいでしょう。

MacBook Proのインストール事例だけどこれも参考になりました。

インストーラを起動するとすぐにプロンプトが表示されるので必要なコマンドを入力していけばいいのですが、MacBookの場合はどうやら画面の下数行*1が見切れてしまっているようで、コマンド入力や結果の確認が困難です。なのでカーソルが画面の中央あたりまで到達したときは、次のコマンドを入力する前にCtrl+Lclearなどでカーソルを1行目に戻してから入力すると安全です。

まずインターネットへ接続するためWi-Fiの設定をします。ドキュメントはIwdです。この設定はインストーラにおけるネットワークの設定で、再起動すると消えてしまうので、後でもう一度、同じ設定が必要です。

# iwctl
[iwd]# device list
  wlan0      dc:a9:04:xx:xx:xx       on      phy0      station
[iwd]# station dc:a9:04:xx:xx:xx scan
[iwd]# station dc:a9:04:xx:xx:xx get-networks
[iwd]# station dc:a9:04:xx:xx:xx connect SSID
[iwd]# exit

時刻合わせです。これも再起動すると消えるので、後でもう一度設定します。

# timedatectl set-ntp true

準備ができたらパーティションを分割していきます。GPTで管理するので、fdiskではなくgdiskを使います。分割自体は好みでレイアウトすればいいのですが、最初のEFIパーティションはsystemd-bootで利用するのでそのまま残しておきます。手元では、Linux用のパーティションは特に分割などせず、1つだけ作りました。また、スワップファイルの方が便利そうなので、swapパーティションは作りませんでした。

# gdisk /dev/nvme0n1
...

パーティションの用意ができたら、インストール先となるパーティション/mntにマウントします。

# mkfs.ext4 /dev/nvme0n1p2
# mount /dev/nvme0n1p2 /mnt

そのまま必須パッケージを/mntにインストールします。

# pacstrap /mnt base linux linux-firmware

/etc/fstabを更新します。

# genfstab -U /mnt >>/mnt/etc/fstab

ブートローダはsystemd-bootを使うので、EFIパーティション/mnt/bootにマウントしてからarch-chrootします。

# mount /dev/nvme0n1p1 /mnt/boot
# arch-chroot /mnt

systemd-boot

ブートローダの選択肢はいくつかありますが、Arch Linuxのドキュメントに、MacBookではいちばん簡単な方法と書かれていたのでsystemd-bootを使いました。ドキュメントはsystemd-bootです。

ここでマイクロコードも一緒にロードしておきましょう。12インチMacBookIntelのCPUなので、パッケージはintel-ucodeです。

# pacman -S systemd-boot intel-ucode
# bootctl --path=/boot install

設定ファイルを2つ作ります。/boot/loader/entries/arch.confカーネル/となるパーティションを設定するものです。UUIDの値は、blkidコマンドで/dev/nvme0n1p2の値を調べました。

title Arch Linux
linux /vmlinuz-linux
initrd /intel-ucode.img
initrd /initramfs-linux.img
options root=UUID=ca7846e4-64bc-4086-ae73-524f5aeb546e rw

ブートしたいカーネルが複数あるなら、このファイルを必要なだけ作ります。次に、ローダ自体の設定です。これは/boot/loader/loader.confに書きます。

default arch
timeout 3
console-mode max
editor no

正しく設定ができていれば、bootctlコマンドで設定した内容を読めるはずです。

# bootctl list
Boot Loader Entries:
        title: Arch Linux (default)
           id: arch.conf
       source: /boot/loader/entries/arch.conf
        linux: /vmlinuz-linux
       initrd: /intel-ucode.img
               /initramfs-linux.img
      options: root=UUID=ca7846e4-64bc-4086-ae73-524f5aeb546e rw

(この時点では、インストーラのエントリがいくつかあるけど再起動すると消える)

ネットワークの設定

上で行ったネットワークの設定をもう一度実施します。こちらは再起動した後で使われるものです。インストーラと異なり、ブート後の環境にはまだiwdパッケージがインストールされていないので、ここで追加しておきます。

# pacman -S iwd

設定自体は上のものと同じですが、以下のコマンドを使うと1行で行なえます。

# iwctl station wlan0 connect <SSID>

SSIDごとに、設定は/var/lib/iwd/<SSID>.<enc>へ保存されます。あとは、ホスト名の設定と、ブート時の再接続を入れておきましょう。

/etc/hostname

linux-pc

/etc/systemd/network/20-wlan0.network

[Match]
Name=wlan0

[Network]
DHCP=ipv4

コマンドはこのような。

# systemctl enable iwd.service
# systemctl enable systemd-networkd.service
# systemctl enable systemd-resolved.service
# networkctl list

時間の設定

NTPとタイムゾーンの設定です。

# ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
# hwclock --systohc

# timedatectl set-ntp true
# timedatectl status

言語の追加

en_US.UTF-8ja_JP.UTF-8を追加します。

# echo 'en_US.UTF-8 UTF-8' >>/etc/locale.gen
# echo 'ja_JP.UTF-8 UTF-8' >>/etc/locale.gen
# locale-gen

これ以降、/etc/locale.confLANGを設定すると日本語化されますが、フォントがない状態でされても文字が読めなくて困るだけなので、これは後で設定します。

rootのパスワード

設定しておきます。

# passwd

systemd-homed

ホームディレクトリの管理もsystemd-homedを使うようにしました。ドキュメントはsystemd-homedです。ついでにvisudoも入れておきます。

# systemctl enable systemd-homed.service
# homectl create lufia
# pacman -S vi sudo
# visudo
# gpasswd -a lufia wheel

vimパッケージもありますが、簡単なテキストの編集にしか使わないのでviパッケージで十分でした。

スワップファイルの追加

swapパーティションを作らなかったので、ここでスワップファイルを設定します。ドキュメントはスワップです。

# fallocate -l 2G /swapfile
# chmod 600 /swapfile
# mkswap /swapfile
# swapon /swapfile

/etc/fstabにエントリを追加。

/swapfile    none    swap    defaults    0 0

デスクトップ環境

2021-05-16追加: この後でKDEに切り替えました

blog.lufia.org

Waylandを使おうとしているので、色々と面倒が少なそうなGNOMEをインストールしました。

# pacman -S gnome
# systemctl enable gdm
# pacman -S noto-fonts noto-fonts-cjk

これで特に何もしなくても、再起動すればGNOMEのセッションマネージャが使えます。ログイン後に環境変数XDG_SESSION_TYPEをみると、WaylandかX11のどちらが使われているかを調べられます。XDG_SESSION_TYPEx11の場合、セッションマネージャの右下にある設定ボタンからGNOMEを選んでログインするとwaylandに変わります。

ディスプレイの設定

RetinaディスプレイHiDPIディスプレイとも呼ばれるようです。GNOMEでは、デフォルトでサイズ調整は200%と設定されていたと思いますが、好みに応じてGNOMEの設定アプリから変更しましょう。

言語の設定

上で言語の追加をしましたが、グラフィックスを使えるようになったので、必要ならここで切り替えます。

# echo 'LANG=ja_JP.UTF-8' >/etc/locale.conf

スリープでD3coldへ遷移させない

12インチMacBookでは、D3coldへ遷移すると、うまく復帰できない問題があるそうです。実際に試したところ、スリープから復帰したログイン画面でパスワードを入力しても、先へ進むことができませんでした。そのため、D3coldへの遷移を止めます。

d3cold_allowedファイルへ0を書き込むことで無効となりますが、再起動するたびに入力するのは面倒なので、systemdを使って自動化します。まずはユニットファイルを/etc/systemd/system/d3cold-disable.serviceに作りました。

[Unit]
Description=Disables sleep from stopping nvme hardware on MacBook

[Service]
ExecStart=/sbin/d3cold 0
Type=oneshot
RemainAftrExit=yes

[Install]
WantedBy=multi-user.target

ExecStart=で実行しているコマンドは以下の内容です。

#!/bin/bash

v=${1:-0}
echo $v > /sys/bus/pci/devices/'0000:01:00.0'/d3cold_allowed

これを有効にします。

$ sudo chmod +x /sbin/d3cold
$ sudo systemctl enable d3cold-disable.service

サーマルスロットリング

12インチMacBookファンレスなこともあり、Go自身のコンパイルをしていると突然スリープに落ちたりします。それでは困るので、thermald.serviceを有効にします。

# pacman -S thermald
# systemctl enable thermald.service

thermaldは、基本的に何も設定しなくても適切に動作するらしいですが、MacBookファンレスなこともあって、しばらく負荷をかけると

systemd-logind[299]: Suspend key pressed.

というログがjournaldに記録されてサスペンドします。なので明示的に/etc/thermald/thermal-conf.xmlに設定を書きます。この設定では、CPUパッケージの温度が75℃を越えたら色々な方法で温度を下げます。

<?xml version="1.0"?>
<ThermalConfiguration>
  <Platform>
    <Name>Macbook 2017</Name>
    <ProductName>*</ProductName>
    <Preference>QUIET</Preference>
    <ThermalZones>
      <ThermalZone>
        <Type>x86_pkg_temp</Type>
        <TripPoints>
          <TripPoint>
            <SensorType>x86_pkg_temp</SensorType>
            <Temperature>75000</Temperature>
            <type>passive</type>
            <ControlType>PARALLEL</ControlType>
            <CoolingDevice>
              <index>1</index>
              <type>rapl_controller</type>
              <influence>50</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>2</index>
              <type>intel_pstate</type>
              <influence>40</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>3</index>
              <type>intel_powerclamp</type>
              <influence>30</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>4</index>
              <type>cpufreq</type>
              <influence>20</influence>
            </CoolingDevice>
            <CoolingDevice>
              <index>5</index>
              <type>Processor</type>
              <influence>10</influence>
            </CoolingDevice>
          </TripPoint>
        </TripPoints>
      </ThermalZone>
    </ThermalZones>
  </Platform>
</ThermalConfiguration>

CPU 周波数スケーリングによると、他にも便利なものはあるようですが、今のところ特には困っていないのでいいかな。

/sys/devices/system/cpu/cpu?/cpufreq/scaling_governorを読むと、どのgovernorがセットされているか調べられますし、/sys/class/thermal/cooling_device?/typeでデバイスの種類が分かります。

$ grep . /sys/devices/system/cpu/cpu?/cpufreq/scaling_governor
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu1/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu2/cpufreq/scaling_governor:powersave
/sys/devices/system/cpu/cpu3/cpufreq/scaling_governor:powersave

$ grep . /sys/class/thermal/cooling_device?/type
/sys/class/thermal/cooling_device0/type:Processor
/sys/class/thermal/cooling_device1/type:Processor
/sys/class/thermal/cooling_device2/type:Processor
/sys/class/thermal/cooling_device3/type:Processor
/sys/class/thermal/cooling_device4/type:intel_powerclamp
/sys/class/thermal/cooling_device5/type:LCD

CPUクロックや温度はlscpu/sys/class/thermal/thermal_zone?/tempで読めます。tempファイルの値は、3330033.3℃のようです。

$ lscpu | grep 'CPU MHz'
CPU MHz:                             2084.082

$ grep . /sys/class/thermal/thermal_zone?/temp
/sys/class/thermal/thermal_zone0/temp:33300
/sys/class/thermal/thermal_zone1/temp:43000

再起動

ここで再起動します。ログイン画面が表示されたら、さきほど作成した一般ユーザーでログインしましょう。

ログイン後は主に個人環境に対する設定を行います。

CapsLockをControlにする

実現方法はいくつかありますが、GNOMEの設定で対応しました。もともとlv3:ralt_switchが含まれていたので、そこに追記した形です。

$ gsettings set org.gnome.desktop.input-sources xkb-options "['lv3:ralt_switch', 'ctrl:nocaps']"

サウンドの設定

MacBookでは追加のドライバが必要です。このドライバのインストールスクリプトではsymlinksコマンドが必要なので、先にAURからインストールしておきます。

$ git clone https://aur.archlinux.org/symlinks.git
$ cd symlinks
$ sudo pacman -S fakeroot
$ makepkg -si

GitHubから12インチMacBook用のドライバをインストールします。

$ git clone https://github.com/leifliddy/macbook12-audio-driver.git
$ cd macbook12-audio-driver
$ sudo ./install.cirrus.driver.sh

MacBook Pro用のドライバもありますが、こちらは12インチMacBookでは認識しませんでした。

Bluetoothの設定

12インチMacBook用のBluetoothドライバです。インストール方法はサウンドの時と同じ。

$ git clone https://github.com/leifliddy/macbook12-bluetooth-driver.git
$ cd macbook12-bluetooth-driver
$ sudo ./install.bluetooth.sh

手元にはBluetoothキーボードトラックパッドがあるので、それぞれ接続しました。ドキュメントはBluetoothにあります。

$ sudo pacman -S bluez bluez-utils
$ sudo systemctl enable bluetooth
$ bluetoothctl
[bluetooth] agent on
[bluetooth] scan on
[bluetooth] devices
[bluetooth] pair 00:00:00:00:00:00
[agent] PIN code: xxxxxx
Pairing successful
[bluetooth] connect 00:00:00:00:00:00
Connection successful

[bluetooth] pair 11:11:11:11:11:11
Pairing successful
[bluetooth] connect 11:11:11:11:11:11
Connection successful

再起動した後に自動接続されるように、/etc/bluetooth/main.confへ設定をしておきます。

[Policy]
AutoEnable=true

外部ディスプレイを繋いでいるときはスリープしない

クラムシェルとかリッドクローズドと呼ばれているものです。/etc/systemd/logind.confを以下のように変更しました。とはいえ、GNOMEによって電源管理されているようなので、この設定がどの程度有効かは分かりません。

--- /etc/systemd/logind.orig 2021-03-29 22:36:55.053860982 +0900
+++ /etc/systemd/logind.conf  2021-03-29 22:56:55.439920429 +0900
@@ -24,8 +24,8 @@
 #HandleSuspendKey=suspend
 #HandleHibernateKey=hibernate
 #HandleLidSwitch=suspend
-#HandleLidSwitchExternalPower=suspend
-#HandleLidSwitchDocked=ignore
+HandleLidSwitchExternalPower=suspend
+HandleLidSwitchDocked=ignore
 #HandleRebootKey=reboot
 #PowerKeyIgnoreInhibited=no
 #SuspendKeyIgnoreInhibited=no

トラックパッド

Waylandではlibinputが動作していて、トラックパッドを接続した後は特に何も設定する必要はありませんでしたが、タップでクリックなどいくつか変更しています。ここは完全に好みです。

$ gsettings list-recursively org.gnome.desktop.peripherals.touchpad
org.gnome.desktop.peripherals.touchpad tap-button-map 'default'
org.gnome.desktop.peripherals.touchpad click-method 'fingers'
org.gnome.desktop.peripherals.touchpad edge-scrolling-enabled false
org.gnome.desktop.peripherals.touchpad disable-while-typing true
org.gnome.desktop.peripherals.touchpad two-finger-scrolling-enabled true
org.gnome.desktop.peripherals.touchpad send-events 'enabled'
org.gnome.desktop.peripherals.touchpad speed 0.0
org.gnome.desktop.peripherals.touchpad tap-and-drag true
org.gnome.desktop.peripherals.touchpad natural-scroll true
org.gnome.desktop.peripherals.touchpad middle-click-emulation false
org.gnome.desktop.peripherals.touchpad left-handed 'mouse'
org.gnome.desktop.peripherals.touchpad tap-to-click true
org.gnome.desktop.peripherals.touchpad tap-and-drag-lock true

日本語入力

一般的に、IBusまたはFcitxのうちどちらかと、MozcまたはSKKのどちらかを組み合わせて使うことになると思います。Linuxのかな漢字変換の興亡SKKが勧められていたので、ここではibus-skkを使うようにします。Mozcの方が簡単だろうけれど、どのみちPlan 9ではSKKを使うことになるし、合わせておくのもいいかなと思ったのでした。

$ sudo pacman -S ibus-skk skk-jisyo

Waylandを使っているので、IBusを使うための環境変数~/.pam_environmentに追加します。

GTK_IM_MODULE DEFAULT=ibus
XMODIFIERS DEFAULT=@im=ibus
QT_IM_MODULE DEFAULT=ibus

また、SKKのキーボードレイアウトは日本語配列になっているので、/usr/share/ibus/component/skk.xmllayoutを変更します。

--- /usr/share/ibus/component/skk.orig   2021-03-21 00:03:32.931254445 +0900
+++ /usr/share/ibus/component/skk.xml 2021-03-21 20:26:27.483580937 +0900
@@ -17,7 +17,7 @@
            <license>GPL</license>
            <author>Daiki Ueno &lt;ueno@unixuser.org&gt;</author>
            <icon>/usr/share/ibus-skk/icons/ibus-skk.svg</icon>
-          <layout>ja</layout>
+           <layout>us</layout>
            <longname>SKK</longname>
            <description>SKK Input Method</description>
            <rank>70</rank>

せっかくなので追加の辞書を入れました。

$ git clone https://github.com/tokuhirom/jawiki-kana-kanji-dict.git
$ cd jawiki-kana-kanji-dict
$ echo ';; -*- coding: utf-8 -*-' >~/.config/ibus-skk/SKK-JISHO.jawiki
$ cat SKK-JISHO.jawiki >>~/.config/ibus-skk/SKK-JISHO.jawiki

あとは、ibus-setupの「入力メソッド」にSKKを追加して、そこへSKK-JISHO.jawikiの辞書も追加すれば終わりです。ただし、SKK-JISHO.jawikiは必ずシステム辞書として追加しましょう。ユーザー辞書として追加しても動作はしますが、候補を確定するときすべてのユーザー辞書へ更新が行なわれるらしく、確定する度に2、3秒待たされることになります。

$ ibus-setup

今の設定はこんな雰囲気。

  1. ~/.config/ibus-skk/user.dict (ユーザー辞書)
  2. /usr/share/skk/SKK-JISHO.L (システム辞書)
  3. ~/.config/ibus-skk/SKK-JISHO.jawiki (システム辞書)
  4. localhost:1178 (サーバ)

参考情報

Firefox

Firefoxを使っているので、言語パックも一緒にインストールします。

$ sudo pacman -S firefox firefox-i18n-ja

標準ではCtrl+Hキーに履歴の表示をトグルするショートカットキーが割り当てられていますが、個人的にはCtrl+Hをバックスペースとして使うのでとてもイライラするので変更します。対応するための方法はいくつかありますが、GNOME側でキーテーマをEmacs風なものに変更すると、FirefoxでもCtrl+Hでバックスペースを入力できるようになります。

$ gsettings set org.gnome.desktop.interface gtk-key-theme 'Emacs'

これで、Ctrl+Aで行頭にカーソルを移動させられたりなど好みの挙動になりました。それぞれのキーに何が割り当てられているかは、/usr/share/themes/Emacs/gtk-3.0/gtk-keys.cssを読むとなんとなく分かると思います。

ターミナル

FirefoxCtrl+CでコピーするけどターミナルではCtrl+Shift+Cでコピーするのは、どちらかに統一したいですね。ターミナルはCtrl+Cで割り込みを発生させるので難しいかなと思っていたけれど割り込みはCtrl+Alt+Cでも起こせたので、以下のコマンドでキーマップを変更しました。

$ gsettings set org.gnome.Terminal.Legacy.Keybindings:/org/gnome/terminal/legacy/keybindings/ copy '<Control>c'
$ gsettings set org.gnome.Terminal.Legacy.Keybindings:/org/gnome/terminal/legacy/keybindings/ paste '<Control>v'

これで、どのアプリでも、Ctrl+CでコピーしてCtrl+Vで貼り付けができる。

クリップボード

クリップボードにはPRIMARY*2CLIPBOARDの2種類あるようで、テキストを選択してマウスの中ボタンで扱うような暗黙的なコピーはPRIMARYを参照して、明示的にコピーをする動作はCLIPBOARDを参照するようです。日本語版のWikiより英語版の方が詳しいのでClipboardのSelectionを読みましょう。GNOMEPrimary Selectionにも色々と書かれています。

Waylandでコマンドラインからクリップボードを操作するためにwl-clipboardを入れておきます。

$ sudo pacman -S wl-clipboard

別件で、最後に選択したテキストがマウスの中ボタンでペーストされてしまうのは誤操作の原因になるので止めてしまいます。

$ gsettings set org.gnome.desktop.interface gtk-enable-primary-paste false

Firefoxは別の設定になるようです。about:configmiddlemouse.pastefalseに切り替えます。

バックアップ

rsyncでコピーする方法がRsync によるフルシステムバックアップで紹介されていますが、rsyncは転送先でそのままファイルとして扱えるため便利ではあるものの、転送先のファイルシステムPOSIX準拠でない場合に色々な問題が発生するのであまり使いたくはありません*3。なのでBorg Backupを使ってみることにしました。exFATでフォーマットされているMOBILEディスクにbackupsディレクトリを作成して、それをBorgのリポジトリにします。borg init -e repokeyで暗号化しています。また、紛失しないようにリポジトリの鍵をエクスポートして保管しておきます。

$ sudo pacman -S borg
$ borg init -e repokey /run/media/lufia/MOBILE/backups
$ borg key export /run/media/lufia/MOBILE/backups repo.key

上では手元のディスクを使いましたが、Google Cloudを使っているならCloud Storage FUSEの方が便利かもしれません。

$ git clone https://aur.archlinux.org/gcsfuse.git
$ cd gcsfuse
$ sudo pacman -S fakeroot # なければ入れる
$ makepkg -si

これで後はバックアップを作成するだけです。毎回コマンドを組み立てるのは面倒なので以下のスクリプトを書きました。

#!/bin/bash

set -eu

if [[ $# -ne 1 ]]
then
    echo usage: $(basename $0) repo >&2
    exit 1
fi
h=$(hostnamectl --static)
d=$(date +%Y-%m%d)
sudo borg create --progress "$1::$h-$d" / --exclude-from ~/lib/borg.exclude

コピーする必要のないファイルもあるので、borg.excludeシステムメンテナンスを参考にこのような内容です。

/dev/*
/proc/*
/sys/*
/tmp/*
/run/*
/mnt/*
/media/*
/lost+found
/var/run/*
/swapfile
/home/*.home
/home/*/.cache
/home/*/.local/share/Trash/*
/home/*/Downloads/*

ところで、systemd-homedを使っていると、/home/$user.homeに大きなファイルが作られています。これはホームディレクトリのLUKSイメージらしく、ユーザーがログインするなどでアクティベートされると/home/$userにマウントされます*4。このイメージは、手元では300GBほどあって、毎回のバックアップで対象とするには大きく時間がかかります。上のスクリプトではどうせ一人しか使わないので、必ずホームディレクトリはマウントされているものとして扱うことにしました。ちなみにアクティベートされたユーザはhomectl listすると分かりますし、homectl activateでアクティベートするようです。

$ homectl list
NAME  UID   GID   STATE  REALNAME HOME        SHELL    
lufia 60331 60331 active lufia    /home/lufia /bin/bash

1 home areas listed.

参考情報

日常の利用

感想など

トラックパッドに慣性スクロールが欲しいなど、まだ気になるところはありますが、これならデスクトップ環境として使えると思えるところまで設定しました。購入して電源を入れるだけで快適に使えるmacOSはすごいなと思うものの、一度設定してしまえばLinuxでもそんなに困らないはずだし、癖や好みの話でもあるので、しばらくこのままLinuxデスクトップで生活してみる予定です。

*1:10行程度?

*2:SECONDARYもあるらしい

*3:symlinkの未サポートやファイル名のcase-insensitiveなど

*4:イメージの種類によりファイル名の末尾が.homedirなどに変わる

Plan 9の権利がPlan 9財団に譲渡され、MITライセンスに変わりました

2021年3月23日に、ノキアから、Plan 9著作権Plan 9財団に譲渡する発表がありました。

www.bell-labs.com

翻訳はこちら。

okuranagaimo.blogspot.com

元々、AT&Tの一部門が独立してLucent Technologiesとなっていて、ベル研はそこに含まれていたけれども、いつの間にかノキアに買収されていたらしいですね。

何が変わったのか

以前のPlan 9はLucent Public License 1.02*1でライセンスされ、ベル研(plan9.bell-labs.com)により配布されていました。ユーザーはreplicaコマンドを使ってベル研の中央サーバからアップデートを取得し、必要ならサーバへパッチを送ることが行われていましたが、2015年1月を最後に、Plan 9のメンテナンスを行う人がいなくなり、しばらくしてからは*2ベル研のサービス自体も参照ができなくなっていました。

代わりに、0introさんにより9p.ioでplan9.bell-labs.comのミラーが提供されるようになり、今ではドキュメントの参照など基本的にこちらが使われるようになっています。他に、9legacyというパッチ集や9frontといったフォークが派生して今もそれぞれ活動していますが、9p.ioが配布する公式のソースコード自体は2015年のものから変わっていませんでした。余談ですがGoがサポートしているPlan 9は9legacyのことです。Go DashboardPlan 9イメージは9legacyの最新版を参照しています。

このように、ここ数年は残念な状況でしたが、今回の譲渡でPlan 9の権利はPlan 9財団へ譲渡されて、Foundation Activitiesによると9legacyのパッチが本家に取り込まれる告知が出ていました。

Preparing for a new release, including patches.

今後の運用がどうなるのかはわかりませんが、Goもplan9portもGitHubで運用されているので、Plan 9もそうなったら嬉しいですね。

Plan 9財団

Plan 9財団は2021年2月に、9fansでアナウンスされました。Plan 9界隈でよく名前を聞く人がメンバーになっています。

9fansでのアナウンス

Twitterアカウントはこちら

この財団は、現状ではPlan 9本体(従来はオープンではなかった1st〜3rd editionも公開されている!)のホストと、GSoCへの参加などを行なっているようです。Activityを見る限りでは2020年から活動していたようですが、まあ、2020年のメールにある署名は強い....

Forkはどうなるか

ここは完全に推測ですが、少なくとも9legacyのパッチは取り込まれることが告知されているので、9legacyは本家Plan 9に統合されそうです。9frontは、部分的にはバックポートされるかもしれないけれども、実装的にも思想的にもベル研Plan 9との差が大きくて、完全なマージは無理じゃないかな、と思っています。とはいえ9frontの方が進んでいるのは事実なので、前衛的な9frontと慎重な本家に分かれるんじゃないかな。plan9portは、以前rscさんが9fans/plan9port

Sorry, but plan9port is only aiming for compatibility with the original Plan 9, not the many forks.

と言っていたけど実際はいくつか拡張されているので、互換性を維持しながら変化していくかもしれません。Raspberry Piへの移植(9pi)は分からないけれど、これは9legacyに近いものなので、millerさんが望めばマージされるかもですね。

*1:SCOのゴタゴタがあってアップデートされた記憶がある

*2:2017年頃かな