Alpine Linuxでデスクトップ

ようこそ、私のチャンネルへ

師走が近くなって参りましたが、皆さんはいかがお過ごしでしょうか。

冬の到来も近いので、OSを変更します。移行先はAlpine Linux、musl libcでBusyBoxな小さめのナウでヤンクな人達に大人気のディストリビューションです。古事記にもそう記されている。

まともなディストリビューションなので、基本的な事(X11の入れ方やら)はWikiに書いてある事もあり、書いていない事もある。という訳で、忘れちゃならないティップス集です。

Installation

聖典はこれなんですが、Arch Linuxに比べると簡単ですね、まるでDebianみたい。

快適なwifiアクセス

他のディストリでよく採用されているDHCPクライアント、dhcpcdはwpa_supplicantとよしなに連携してくれますが、BusyBox同梱のudhcpcはそんな事をしてくれません。アクセスポイントが変わる度にudhcpc -iwlan0やらそんな感じのことを叩きたくないので、どうにかシュッとさせる必要がある。

という訳で、wpa_supplicantに同梱されたwpa_cliのフック機能を使って手動でudhcpcをよしなにします。例えばこんな感じで。

/etc/network/interfaces

auto lo
iface lo inet loopback

auto wlan0
iface wlan0 inet manual
# wpa_supplicant calls udhcpc

/etc/wpa_supplicant/wpa_cli.sh

#!/bin/sh

UDHCPC='/sbin/udhcpc'

if [[ -x "${UDHCPC}" ]]; then
	logger -t wpa_cli "${UDHCPC} doesn't exist"
	exit 1
elif [[ -z "${1}" -o -z "${2}" ]]; then
	logger -t wpa_cli "Usage: ${0} IFNAME ACTION"
	exit 1
fi

IFNAME="${1}"
ACTION="${2}"
PIDFILE="/var/run/udhcpc.${IFNAME}.pid"

case "${ACTION}" in
	CONNECTED)
		if [[ -f "${PIDFILE}" ]]; then
			kill -s USR1 `cat "${PIDFILE}"`
		else
			${UDHCPC} -R -n -p "${PIDFILE}" -i${IFNAME}
		fi
		;;
	DISCONNECTED)
		[[ -f "${PIDFILE}" ]] && \
			${KILL} `cat "${PIDFILE}"`
		;;
	*)
		logger -t wpa_cli "unknown action '${ACTION}'"
		exit 1
esac

/etc/init.d/wpa_cli

#!/sbin/openrc-run

description='wpa_cli action daemon'

name='wpa_cli'
command='/sbin/wpa_cli'
pidfile='/var/run/wpa_cli.pid'
command_args="-P ${pidfile} -B -a ${wpa_cli_action:-/etc/wpa_supplicant/wpa_cli.sh}"

depend () {
	need wpa_supplicant
	after wpa_supplicant
}

あとはrc-update add wpa_cliすれば、次回起動時から、wpa_cliがudhcpcをdhcpcd並みによしなにシュッしてくれるようになります。OpenRCはsystemd並みにinitファイルが書きやすくてキモティーですね。

Naze kaNihongo gaUTenai

日本語が打てない。何故でしょうか。

$ apk search ibus
live-media-2016.04.21-r0
libusb-1.0.20-r0
usbredir-0.7-r2
libusb-compat-0.1.5-r3
libusb-dev-1.0.20-r0
usbredir-dev-0.7-r2
libusb-compat-dev-0.1.5-r3
$ apk search fcitx
$ apk search uim
$ apk search scim

はい。という訳で、IMEを自分でコンパイルして入れましょう。今回は、skkが組み込まれているuimを採用しました。

# apk add \
    libtool gtk+-dev libx11-dev gcc make \
    g++ gtk+2.0-dev libxext-dev gettext \
    fortify-headers freetype-dev \
    libintl
$ wget https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/uim/uim-1.8.6.tar.gz
$ ./configure \
    --disable-nls --disable-gnome-applet \
    --enable-gnome3-applet=no --enable-qt4-qt3support=no \
    --disable-fep --disable-emacs --enable-kde-applet=no \
    --enable-kde4-applet=no --without-m17nlib --without-anthy \
    --with-mana=no --with-prime=no --with-gtk3=no  --with-x
$ make
# make install
# apk del -r --purge \
    automake autoconf libtool gtk+-dev libx11-dev gcc \
    g++ gtk+2.0-dev libxext-dev gettext \
    fortify-headers freetype-dev

configureやらは趣味でなんたらやって下さい。Alpine Linuxのレポジトリにあるfirefoxは、XIMサポートが切られているようなので、gtkのサポートを入れておくと時間を無駄にせずにすみます。これは豆知識なのですが、gettextが無くてもuimは動く。

あと、下のような感じでuimを自動起動させると良いです。

$ cat << _EOF_ >> ~/.xinitrc
export XMODIFIERS='@im=uim'
export GTK_IM_MODULE='uim'
export GTK_IM_MODULE_FILE='/usr/local/etc/gtk-2.0/gtk.immodules'
uim-xim &
_EOF_

省電力

powertopがレポジトリにありますが、debugfsにアクセスできないとか言って起動できません。頑張りが重要です。こんな感じで。

/etc/local.d/00-powersave.start

#!/bin/sh

# Enable Laptop-Mode disk writing
echo 5 > /proc/sys/vm/laptop_mode

# NMI watchdog should be turned on
for i in /proc/sys/kernel/nmi_watchdog
do
	echo 0 > $i
done

# Set SATA channel to power saving
for i in /sys/class/scsi_host/host*/link_power_management_policy
do
	echo min_power > $i
done

# Select Ondemand CPU Governor
for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
do
	echo ondemand > $i
done

# Activate USB autosuspend
for i in /sys/bus/usb/devices/*/power/level
do
	echo auto > $i
done

# Activate PCI autosuspend
for i in /sys/bus/pci/devices/*/power/control
do
	echo auto > $i
done

# Activate audio card power saving
# (sounds shorter than 5 seconds will not be played)
echo 5 > /sys/module/snd_hda_intel/parameters/power_save
echo 1 > /sys/module/snd_hda_intel/parameters/power_save_controller

どこかから教授した謎のスクリプト(modifired)なのですが、どこから引っ張ってきたか忘れた。

bluetooth mouse が動かない

# addgroup MYUSER lp
# cat << _EOF_ >> /etc/modules-load.d/uhid.conf
uhid
_EOF_

にゃん。

muslとはいうが、やっぱりFlashしたい

flashpluginはバイナリでしか配布されていません。そのせいで、glibcの入っていないAlpine Linuxでは動きません。フッラシュのためだけに仮想マシンブッチ上げしたり、glibcをブチ込むのは悲しすぎるので、firefox on Debian on Dockerで素早くコンテナーしましょう。

取り敢えずDockerを入れる

# apk add docker
# addgroup USER docker

で終わり……と行きたい所ですが、Dockerがchrootして色々しているせいで、Grsecurityが怒ります。

# cat << _EOF_ >> /etc/sysctl.conf
kernel.grsecurity.chroot_deny_chmod = 0
kernel.grsecurity.chroot_deny_mknod = 0
_EOF_
# rc-update add sysctl
# rc-service start sysctl

こうすれば、さすがのGrsecurityクンもニッコリ。

なお、Docker v1.12.0からchrootの代わりにpivot_rootを使うようになったので、このようなワーカアランウドは必要無くなりました。しかし2016年ポッキーデイ現在、レポジトリ上にあるDockerはv1.11.2です。

画面を転送するには

X11はネットワーク透過性があり、この点については非常に簡単に解決できます。/tmp/.X11-unix/DISPLAY環境変数をコンテナに露出させるだけです。こんな感じですね。

$ docker run \
    --volume=/tmp/.X11-unix:/tmp/.X11-unix \
    --env=DISPLAY
    [IMAGE] [ENTRYPOINT]

ただソケットをexposeするだけだと、X11の認証機構を通過できないので、事前にxhost local:やら打って認証機構を殺しましょう。

${HOME}/.Xauthorityを上手にexposeしたり、hostnameをちゃんと設定したり、xauthをよしなに弄ったりすれば、認証機構を殺さずによしなにできるっぽいですが、面倒なのでやりません。

音を転送するには

ALSAにネットワーク透過性がありません。aserverなる、謎のコマンドラインツールがあるっぽいのですが、使い方が分かりません。なんといってもまず、ドキュメントが無い。

こんな時、一般的にはPulseAudioを使うらしいのですが、素晴らしい事に、レポジトリにそんな物はありません。そこでJACKを使う。これで解決、皆満足。いまずはJACKを入れます。

# apk add jack jack-example-clients

次に、全ての音がJACKを経由するようによしなに設定します。ココには全てが書いてあります。下にはその一部が具体的に記されています。

/etc/modules-load.d/alsa.conf

snd-aloop

ALSAでLoopbackデバイスを使えるようにして……

${HOME}/.asoundrc

pcm.amix {
  type dmix
  ipc_key 1024
  slave {
    pcm "hw:Loopback,0,0"
    format S32_LE
  }
}

pcm.to_jack {
  type plug
  slave {
    pcm "hw:Loopback,1,0"
    format S32_LE
  }
}

pcm.!default {
  type plug
  slave {
    pcm "amix"
    format S32_LE
  }
}

ctl.!default {
  type hw
  card 1
}

全ての音がpcm.to_jackに飛ぶようにして……

$ jackd -r -d alsa -P hw:1 &

JACKを起動して……

$ alsa_in -j alsa_in -d to_jack -r 48000 &

pcm.to_jackから音声を拾ってくるalsa_inを起動して……

$ jack_connect alsa_in:capture_1 system:playback_1
$ jack_connect alsa_in:capture_2 system:playback_2

alsa_inから流れてくる音声をsystem:playbackに流すようにして、はい。

お次はドッカーコンテナーを起動します。

container_name=container

init_jacknet () {
    local container_ip=`docker inspect --format='{{ .NetworkSettings.IPAddress }}' $1`

    local jack_cli="netjack-${key}"
    local container_ip=`get_container_ip ${container_id}`

    jack_netsource -N ${jack_cli} -H ${container_ip} -I0 -O0 -i2 -o0 &

    sleep 0.1 # wait for jack_netsource initialized

    jack_connect ${jack_cli}:capture_1 system:playback_1
    jack_connect ${jack_cli}:capture_2 system:playback_2
}

(
sleep 1 # wait for docker container initialized
init_jacknet ${container_name}
) &

# 1秒以内に素早く起動しましょう
docker run \
    --name=${container_name}
    [IMAGE] [ENTRYPOINT]

jack_netsource -H [IP]で、[IP]からの音を取ってくる事ができるようになります。jack_connectでsystem:playbackに繋げておけば、スピーカーから音が聞こえてくるようになり安心。

ここで、ホスト側の作業は終わり、ここからコンテナ内での作業が始まります。

${HOME}/.asoundrc

pcm.jack {
    type jack
    playback_ports {
        0 system:playback_1
        1 system:playback_2
    }
}

pcm.!default {
    type plug
    slave.pcm jack
}

ホストの時と同じく、ALSAを流れる全ての音がJACKを経由するように、.asoundrcを設定します。今回はJACKのALSAプラグインを使ってみました。件のプラグインは、Debianなら、apt install libasound2-pluginsで入るゾイ。

jackd -r -d netone &

後は、JACKを起動すれば完了です。コンテナ側でjack_simple_clientして謎の音が流れてくれば、成功だぞ。

これまでの経験を元に、firefox with flashpluginを起動する

アレをこうすれば終わりなのですが、何をやっているかは上を読んで察してくれ。

${HOME}/flashfox/Dockerfile

FROM debian:jessie

ENV DEBIAN_FRONTEND noninteractive
RUN echo deb http://deb.debian.org/debian jessie contrib >> /etc/apt/sources.list && \
    apt-get update && \
    apt-get install --no-install-recommends -y \
        flashplugin-nonfree firefox-esr fonts-ipaexfont \
        jackd libasound2-plugins dbus-x11 paxctl && \
    rm -rf /var/lib/apt/lists/* && \

    # disabling MPROTECT is required for flashplugin/firefox
    paxctl -cm /usr/lib/firefox-esr/firefox-bin && \
    paxctl -cm /usr/lib/firefox-esr/plugin-container && \
    apt-get purge -y paxctl && \

    useradd -m firefox

COPY ["start-fx.sh", "/"]

USER firefox
ENTRYPOINT ["/start-fx.sh"]

MPROTECTを無効にしないと、firefoxがGrsecurityに殺されるぞ。

${HOME}/flashfox/start-fx.sh

#!/bin/sh
set -ue
umask 0027
export PATH='/bin:/usr/bin'
export LANG='C'
IFS=' 	
'

# copy profile
printf 'Initializing profile...\n'
mkdir "${HOME}/.mozilla"
cp -r /profile/* "${HOME}/.mozilla"

# create asoundrc
cat << _EOF_ > "${HOME}/.asoundrc"
pcm.jack {
    type jack
    playback_ports {
        0 system:playback_1
        1 system:playback_2
    }
}

pcm.!default {
    type plug
    slave.pcm jack
}
_EOF_

# initialize jack
/usr/bin/jackd -r -d netone &

# start firefox!
LANG=ja_JP.UTF-8 /usr/bin/firefox --no-remote --new-instance $@

プロファイルレポジトリに書き込めないと死ぬのでどうにかしています。

${HOME}/.asoundrc

pcm.amix {
  type dmix
  ipc_key 1024
  slave {
    pcm "hw:Loopback,0,0"
    format S32_LE
  }
}

pcm.to_jack {
  type plug
  slave {
    pcm "hw:Loopback,1,0"
    format S32_LE
  }
}

pcm.!default {
  type plug
  slave {
    pcm "amix"
    format S32_LE
  }
}

ctl.!default {
  type hw
  card 1
}

${HOME}/docker-run-gui.sh

#!/bin/sh
set -ue
umask 0027
export PATH='/bin:/usr/bin'
export LANG='C'
IFS=' 	
'

CONTAINER_KEY_DICTKEY='domestic.graphic.container_key'

die () {
    local status=${1}
    printf 'Usage: %s [-d $HOME dir] [-o opts] [-ph] tag entrypoint\n' "$0"
    exit $status
}

get_container_id () {
    printf '%s' `docker ps -ql --filter="label=${CONTAINER_KEY_DICTKEY}=${1}"`
}

get_container_ip () {
    printf '%s' `docker inspect --format='{{ .NetworkSettings.IPAddress }}' ${1}`
}

initialize_jack () {
    local key=${1}
    local info_fifo=${2}

    local container_id=`get_container_id ${key}`
    test -z "${container_id}" && return

    local jack_cli="netjack-${key}"
    local container_ip=`get_container_ip ${container_id}`

    jack_netsource -N ${jack_cli} -H ${container_ip} -I0 -O0 -i2 -o0 &

    sleep 0.1 # wait for jack_netsource initialized

    jack_connect ${jack_cli}:capture_1 system:playback_1
    jack_connect ${jack_cli}:capture_2 system:playback_2

    echo $! >> "jack_pid:${pid_fifo}"
}

finalize_jack () {
    local fifo_netsrc_pid="${1}"
    local netsrc_pid=`cat "${fifo_netsrc_pid}"`

    test -n "${netsrc_pid}" && kill "${netsrc_pid}"
    rm -f "${fifo_netsrc_pid}"
}

while getopts npho:d: opt; do
    case "${opt}" in
      o) opts="${opts:-} ${OPTARG}" ;;
      d) home="${OPTARG}" ;;
      p) privileged='true' ;;
      n) no_jack='true' ;;
      h) die 0 ;;
    [?]) die 1 ;;
    esac
done
shift `expr ${OPTIND} - 1`

test -z "${1:-}" && die 1

tag=${1:-}
shift 1
entrypoint="${@:-}"

container_key=`cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 16`

if test -z "${no_jack:-}"; then
    fifo_netsrc_pid=`mktemp -u`

    (
    sleep 0.1 # wait for docker container initialized
    mkfifo -m 600 "${fifo_netsrc_pid}"
    initialize_jack ${container_key} "${fifo_netsrc_pid}"
    ) &

    trap "finalize_jack '${fifo_netsrc_pid}'" EXIT
fi

test -z "${privileged:-}" && \
    opts="${opts:-} --user='`id -u`:`id -g`'"

xhost local: # bypass MIT-COOKIE authentication
docker run --rm \
    --volume='/tmp/.X11-unix:/tmp/.X11-unix' \
    --env="DISPLAY" \
    --env="HOME=${home:-/tmp}" \
    --label="${CONTAINER_KEY_DICTKEY}=${container_key}" \
    ${opts:-} ${tag} ${entrypoint}

なんか、異常に壮大なシェルスクリプトがありますが、やってる事は似たような事です。後は、下のように実行すればイナフです。

$ jackd -r -d alsa -P hw:1 > /dev/null &
$ # wait for jack initialized
$ alsa_in -j alsa_in -d to_jack -r 48000 > /dev/null &
$ # wait for alsa_in initialized
$ jack_connect alsa_in:capture_1 system:playback_1
$ jack_connect alsa_in:capture_2 system:playback_2
$ cd ${HOME}/flashfox
$ docker build -t flashfox .
$ ./docker-run-gui.sh -d'/tmp' \
    -o"--volume=${HOME}/.mozilla:/profile:ro" \
    flashfox --private-window

遠回りした感じはありますが、やっと目的地に到着しました。見て分かる通り、非常に面倒なので、.xinitrcをイジるなり、シェルスクリプトにしてゴチョゴチョするなりなんなりしておくといいんじゃないでしょうか。