std::pairをmemcpyすることについて

g++-8で追加された-Wclass-memaccessの警告を有効にしている場合、以下のコードはコンパイル時に警告が出ます。-Wclass-memaccessの警告は、-Wallに含まれるため、単にコンパイラをg++-8に変更しただけでこの警告に出くわすこともあると思います。

#include <utility>
#include <cstring>

std::pair<double, double> p, q;

int main() {
    ::memcpy(&q,&p,sizeof q);
}

これは、どのような警告かというと、「std::pair<T, U>には非自明なコピー代入演算子がある。本当にmemcpyでバイト列をコピーするだけで大丈夫?」という警告です。

ここで、非自明なコピー代入演算子とは、何も書かなかった時の自動定義と= defaultでの定義を除く、ソースコード中に明示的に書かれたコピー代入演算子のことです。

普通に考えるとstd::pair<T, U>のコピー代入演算子は非自明なことをするはずはないのですが、定義されているので仕方がないです。

以下、TUはPlain Old Data (POD)であることを前提として話を進めます。

LLVMコンパイルするとこの警告が出た

どうもg++-8でLLVMコンパイルするとこの警告が出ます。 LLVMC++で書かれており、コンパイル速度が速いことを売りにしているようですが、スピード狂がいるのでしょうか。

警告メッセージによれば、llvm::SmallVector<T, N>Tとしてstd::pairを使ったことが原因のようです。llvm::SmallVector<T, N>TがPODのようなものである場合、具体的にはllvm::isPodLike<T>::valuetrueの時、特殊化されており、memcpyを使った高速実装を用いるようです。

問題のllvm::isPodLike<T>ですが、大体以下のような感じに定義されています。

template<typename T>
struct isPodLike {
  static const bool value = std::is_trivially_copyable<T>::value;
};

llvm/type_traits.h at 425788d8b599aef1e77092228423e0bb641df359 · llvm-mirror/llvm · GitHub

これならばmemcpyを用いても問題は発生しそうにありません。しかし、その下に定義されている特殊化がまずいです。

// std::pair's are pod-like if their elements are. 
template<typename T, typename U> 
struct isPodLike<std::pair<T, U>> { 
  static const bool value = isPodLike<T>::value && isPodLike<U>::value; 
}; 

ちなみに、現在LLVMをg++-8でコンパイルしてもこの警告は出なくなっています。どうやって解決したのかというと、

Fix few g++ 8 warning with non obvious copy object operations · llvm-mirror/llvm@425788d · GitHub

reinterpret_cast<void*>を使って、単に警告を黙らせるという姑息な解決法だったようです。

正しい方法

正しい方法は、以下の二通りです。

1 自明にコピー可能なstruct Pair<T, U>を自分で定義する。

2 std::copyなどを用いてコピーする。

1 の方法を使う場合、std::pairとの互換性のため、std::pair<T1, U1>からの変換コンストラクタ等を定義した方が良いでしょう。= defaultによる定義を用いることで、自明にコピー可能であることと両立可能です。

2 の方法を使う場合、memcpyよりも遅くなってしまうという欠点があります。

std::copymemcpyよりおそいのか

現代の最適化コンパイラ、clang++やg++をもってしても、std::copymemcpyよりも遅いです(x86のように幅広なレジスタを持たないアーキテクチャなら互角になるかもしれません)。

memcpyを使った場合

memcpyを使った場合は限界までチューニングされた関数が呼び出されます。たぶん、一番幅広なレジスタ(私の環境では256bit幅のymmレジスタ)に乗っけてコピーしているのだと思われます。

std::copyを使った場合

以下はstd::pair<int, double>[10000000]std::copyするコードを、clang5.0.1で-O3コンパイルした時のアセンブリです。注意すべき点として、この構造体にはパディングが含まれているという点があげられます。パディングが含まれている構造体をmemcmpで比較するのはまずいですが、memcpyでコピーすること自体は問題ありません(実際はstd::pairは自明にコピー可能なわけではないのでこの議論は根本がまちがっているわけですが)。

.LBB0_3:
  mov eax, [rbx+src]
  mov [rbx+dst], eax
  mov rax, [rbx+src+8]
  mov [rbx+dst+8], rax
  mov eax, [rbx+src+16]
  mov [rbx+dst+16], eax
  mov rax, [rbx+src+24]
  mov [rbx+dst+24], rax
  mov eax, [rbx+src+32]
  mov [rbx+dst+32], eax
  mov rax, [rbx+src+40]
  mov [rbx+dst+40], rax
  mov eax, [rbx+src+48]
  mov [rbx+dst+48], eax
  mov rax, [rbx+src+56]
  mov [rbx+dst+56], rax
  mov eax, [rbx+src+64]
  mov [rbx+dst+64], eax
  mov rax, [rbx+src+72]
  mov [rbx+dst+72], rax
  lea rax, [r15+5]
  and r15, -2
  add rbx, 80
  cmp r15, 4
  mov r15, rax
  jne .LBB0_3

単に五倍ループアンローリングされること以外はごく普通のコードになっています。パディングのところに触らないようにeax(4Byteレジスタ)やrax(8Byteレジスタ)を使っているため、命令数がかさみ、コピーに時間がかかるようになってしまうようです。

パディングがあるのは意地悪な例のようにも思えますが、std::pair<double, double>のようなものの場合でも、movsd命令(16Byteレジスタの下半分だけ使う命令)を使っているため、結局コピーに時間がかかる点は同じです。

g++の場合はもう少し速いコードを生成するようですが、実行速度はmemcpyより有意に一割ほど遅かったです(wandboxを使ったためアセンブリは確認していません)。

おわりに

最適化はコンパイラに任せろと言われて久しいですが、現代のコンパイラをもってしてもstd::copyを自動でmemcpyに変換するような高級なことはできず、いまだにこの手の最適化はプログラマの責任の範囲となってしまっています。 一方で最近のコンパイラは想像を絶するような最適化も行うため、うっかり未定義挙動を踏まないようにするのも、最適化プログラマの責任となってしまっています。

現代のスピード狂プログラマは、速いコードを書くための知識だけではなく、言語仕様の熟知も必要条件となりつつあります。 コンパイラを黙らせるのではなく、コンパイラの有用な指摘をありがたく受け取れるようにしたいものです。

そもそも、今回の問題はstd::pairの設計に問題があることが原因のような気がしますが。

パック展開でリスト操作

先週はパック展開を使うとconstexpr関数が高速化するみたいなことを書きましたが、具体的な使い方をあまり書けなかったので、具体例を書いておきます。

map

template<class T, std::size_t N, std::size_t... Indeces>
constexpr auto map_f_impl( const T (&arr)[N], std::index_sequence<Indeces...> ) {
    return std::array { f( arr[Indeces] )... };
}
template<class T, std::size_t N>
constexpr auto map_f( const T (&arr)[N] ) {
    return map_f_impl( arr, std::make_index_sequence<N>() );
}

パック展開をそのまま使えばよいです。 C++17では推論ガイドのおかげでstd::arrayのテンプレート実引数を指定しなくてよくなりました。

fold

template<class T, std::size_t N, std::size_t... Indeces>
constexpr auto fold_impl( const T (&arr)[N], std::index_sequence<Indeces...> ) {
    return (arr[Indeces] + ...);
}
template<class T, std::size_t N>
constexpr auto fold( const T (&arr)[N] ) {
    return fold_impl( arr, std::make_index_sequence<N>() );
}

畳み込み式をそのまま使えばよいです。

scan (prefix sum)

template<class T>
constexpr auto sum( T& acc, T val ) {
    return acc += val;
}

template<class T, std::size_t N, std::size_t... Indeces>
constexpr auto prefix_sum_impl( const T (&arr)[N], T init, std::index_sequence<Indeces...> ) {
    return std::array { sum( init, arr[Indeces] )... };
}
template<class T, std::size_t N>
constexpr auto prefix_sum( const T (&arr)[N], T init = T() ) {
    return prefix_sum_impl( arr, init, std::make_index_sequence<N>() );
}

副作用を持つ関数を使ってパック展開すると、scanが作れます。あまり知られていないような気がします。

実践例

最小値の検索

template<class T, std::size_t N, std::size_t... Indeces>
constexpr auto min_impl( const T (&arr)[N], T init, std::index_sequence<Indeces...> ) {
    return ((init = std::min( init, arr[Indeces]), ...);
}
template<class T, std::size_t N>
constexpr auto min( const T (&arr)[N], T init ) {
    return min_impl( arr, init, std::make_index_sequence<N>() );
}

最小値の探索は一種の畳み込みであると考えることができます。 コンマ演算子で畳み込みすることでfor文の代わりにすることができます。 畳み込み式が使えないC++14の場合でも、Swallowの技法を使っても同じことができます。

行列乗算

template<class T, std::size_t N>
struct Matrix { 
    T data[N*N];
};

template<class T, std::size_t N, std::size_t... Indeces>
constexpr auto inner_product( const T (&lhs)[N*N], const T (&rhs)[N*N], std::size_t i, std::size_t k, std::index_sequence<Indeces...> ) {
    return ((lhs[i*N + Indeces] * rhs[Indeces*N + k]) + ...);
}

template<class T, std::size_t N, std::size_t... Indeces>
constexpr auto product_impl( const Matrix<T, N>& lhs, const Matrix<T, N>& rhs, std::index_sequence<Indeces...> ) {
    return Matrix<T, N> { inner_product<T, N>( lhs.data, rhs.data, Indeces / N, Indeces % N, std::make_index_sequence<N>() )... };
}

template<class T, std::size_t N>
constexpr auto product( const Matrix<T, N>& lhs, const Matrix<T, N>& rhs ) {
    return product_impl( lhs, rhs, std::make_index_sequence<4>() );
}

同時に複数のパック展開を行うことはできないので、関数ごとに分けて行います。 constexpr でニューラルネットワークを実装したというのが最近(6月)ありましたが、そこで使われていた行列乗算をこれに置き換えたところ8倍程度高速化した記憶があります。 前回説明した、

  • パック展開で初期化する
  • 生配列を使って関数呼び出しを減らす
  • 畳み込み式を使って変数の書き換えを減らす

をすべて使っています。

constexpr関数を高速化する方法

constexpr関数ではほとんど何でもできてしまうので、可読性を損なうことなく多くの仕事をコンパイル時にこなすことができます。しかし多くのC++コンパイラでは、インタプリタ上で実行する実装となっており、非常に低速です。constexprで多くのことをやりたい場合、速度がネックになることがあります。

そこで、constexpr関数の(コンパイル時の)実行速度を向上させるテクニックを書いてみます。

constexpr関数の良いところとして、実行時にも同じ関数が使えるという点があげられます。しかし、紹介するテクニックの中には実行時の効率を犠牲にしているものがあるので注意が必要です。

コンパイラは、clang++またはcl.exe(MSVC)を使うものとします。g++はconstexpr関数をメモ化するという性質があるため、竹内関数やフィボナッチ数列を計算するのは速いですが、大規模な計算をするためには膨大なメモリが必要になり、不適です。

変数をなるべく書き換えない

C++14からはconstexpr関数内での変数書き換えが可能になりましたが、変数の書き換えは極力しない方が高速になります。

初期化は統一初期化構文とパック展開を使う

constexpr関数内では未初期化変数は使えないので、必ず初期化する必要があります。そのため、以下のようにfor文を使って値を書き込む場合、「最初の無駄な初期化」「値の書き込み」の二回変数を書き換えていることになります(ついでに、ループ変数を書き換えているのも時間がかかる原因になります)。

constexpr auto f() {
    std::array<std::size_t, 100> arr {};
    for( std::size_t i = 0; i < 100; ++i ) {
        arr[i] = i;
    }
    return arr;
}

こうではなく、index_tupleイディオムとパック展開を用いて、必要な値を初期化時にいきなり書き込むことで高速化することができます。

template<std::size_t... Indeces>
constexpr auto f_impl( std::index_sequence<Indeces...> ) {
    return std::array<std::size_t, 100> arr { Indeces... };
}
constexpr auto f() {
    return f_impl( std::make_index_sequence<100>() );
}

とてもC++11らしいコードになりますが、C++14の新機能を使っても速くならないのでしょうがないです。

このようにした場合、コンパイル時の実行速度は稼げますが、実行時のコードはループアンローリングしたものになるため、プログラムサイズが肥大化します。今回の例はそれほどひどいことにはなりませんが、複雑なコードをアンローリングすることは性能低下につながります。

constexprで行いたい重たい処理のうち、大部分は配列的処理だと思います。関数型言語的に言ってmap関数にあたる処理を行う場合はindex_tupleイディオムとパック展開で一気に処理するのが最も高速です。Indexを扱えるので、ランダムアクセス的な処理も一部可能です。たとえば、並列ソートであるバイトニックソートはこの方法で実装できます。

関数呼び出しを避ける

関数呼び出しには一定のオーバーヘッドがあるため、関数呼び出しを減らすほど高速になります(constexpr関数を使うという点では本末転倒ですが)。可読性を保つこととはトレードオフとなりますが、プログラム高速化の原則「一番実行されるところだけ最適化する」を守ってやればそれほどひどいことにはならないでしょう。

畳み込み式を活用する

map関数の適用のような、完全に並列な処理は先ほど書いたパック展開のテクニックによって高速化が望めますが、「全部足す」のような処理はfor文を使うか、再帰関数呼び出しを行うかのどちらかとなり、いずれも低速です。ここはC++17で導入された畳み込み式を活用する以外ありません。

注意点として、パック展開と同様ループアンローリングをしたことになるので、実行時のコードは肥大化します。

生配列を使う

C++で固定長配列を扱いたいときは、std::array<T, N>を使うのが良いとされ、C言語由来の生配列を使うことは時代遅れとされます。実際、最適化をかければ実行時のパフォーマンスには全く差が出ないのですが、constexpr関数のコンパイル時実行の際には生配列を使った方が高速となります。これは、配列アクセスをする際、std::array<T, N>::operator[](std::size_t)関数(またはat関数)を呼び出していることによるオーバーヘッドが主要因です。一方生配列の場合は単純なポインタ演算により配列アクセスが可能であり、高速になります。

とはいっても配列を関数の返り値として返却することはできないので、返り値の部分にはstd::array<T, N>のような、配列をメンバに持つ構造体を返す必要があります。そこから生配列を取り出すことを一回だけ行えば、それ以降は高速な配列アクセスを利用できるようになります。ただし、std::array<T, N>::data()関数はなぜかconstexpr指定されていないので、自作のArrayクラスなどを利用する必要があります。

ノートパソコンが二台いっぺんに起動しなくなってびっくりした

WindowsUpdateしたからなのか使っているノートパソコンが二台いっぺんに起動しなくなってびっくりしました。

他の同じ症状で困っているのが助かればと思って症状と直した手順を書いておきます。

Lenovo Miix 720

症状

電源ボタンを押すといつも通りLenovoのロゴが出る。しかしその後●がくるくる回るアニメーションが出てこない。そのまま待ってても一時間くらいそのまま。

BIOSを出すためにFn+F2キーを押すとLenovoのロゴが消えて画面遷移する。でもそのあと何も起こらない。 Fn+F12キーの場合も同様。

ブートメディアから起動できないかと思って試してみたけれど、そもそもBIOS画面にたどり着けないので意味がなかった。

直し方

サポートに問い合わせたら、「音量アップボタンを押しながら電源キーを押す」とNovoメニューが出てくるとのことで、実際にやってみるとNovoメニューが出てくる。

4種類あるメニューのうち、「System Recovery」をタッチパッドか方向キーで選択し、エンターキー(左クリックではダメ)を押すと、Windows10の復元画面が出てくる。 その画面で「Windowsを通常起動する」を選ぶと普通に起動した。再起動してみたりしても普通に起動できるので、おそらくこれで解決?何が原因だったのだろう。

「System Recovery」以外のメニューを選ぶとやっぱり画面が真っ暗になってそのあと何も起こらない。

HP Spectre x360

症状

パスワードを入力すると、「User Profile Serviceサービスによるサインインの処理に失敗しました」と出てくる。何回か試しても同じ。 パスワードが間違っている場合は、普通に間違っていますって出る。

直し方

とりあえず再起動したら直った。WindowsUpdateでどっかのファイルのロックがかかりっぱなしになったとかかな?

マクロレス型安全printf

もうみんなconstexprに飽きてしまったのか、ほとんど文献がないのでメモ程度に。 私が考えたわけではなく、 constexpr で テンプレートメタプログラミング - TXT.TXT に書いてあったことを試してみたというだけです。 紹介する実装は、コンセプトの確認にとどまっており、完全な実装というわけではありません。 また、静的型付けにこだわらなければ、boost.printfは変な型が来た時に実行時エラーにしてくれるらしいですが、せっかくC++でやっているのにと感じてしまうかもしれません。実際、静的型付けを行うiostreamでは実行時型エラーは起きないため、利便性と引き換えに安全性が失われてしまっているという感じがあります。

Cプリプロセッサマクロは基本的にC++erから嫌われているので、それを使わないで純粋なC++の文法のみで書けるのは一定の成果なのですが、特に読みやすくなるわけではありません。

本題と関係ない話

Twitterとかマストドンとかってすごく検索性が低いなぁと感じます。特に、空リプ(空中リプライ、話題の引用がない会話)みたいなものは運よく検索で見つかってもほとんど情報を得られないです。 マストドンに至っては、この前に流行った時に立った草の根インスタンスはほとんど消滅してしまっています(おそらく、AWSなどを使うと最初の一年間は無料でできるので、それが終わるタイミングで消滅したのでしょう)。

そういうところに技術的な話をうずめてしまうのが悲しいので、ブログみたいな少しは検索性のあるところに書き留めておこうと思って書いています。

ltmpcの時にいろいろなブログ記事にお世話になったから……。

実際のコード

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ユーザー側では、

print( []{return"%d\n";}, a );

のように書けます。型が正しくないコードを書くと、TMPでちゃんとコンパイルエラーになってくれるはずです(ただし、実装されているのは%d%c%p%%くらいなので、それ以外の書式指定文字列だと書式指定文字列として認識されず、「引数が多いよ!」というコンパイルエラーになると思います)。

少し不思議な点

この方法の肝心な点は、「ラムダ式でくるめば値(項)を型に変換できる」ということです。書式指定文字列は全てconst char*の型になってしまい、内容によって型が変わるということはありません。 しかし型安全なprintfを実現するためには、書式指定文字列ごとに別の型になってくれないと型検査ではじけません。逆に書式指定文字列ごとに別の型になっていれば、TMPでどうにかできるはずです。

ここでラムダ式の、「それぞれ固有の型になる」という特性が活用できます。単にconst char*を返すだけのラムダ式であっても、書式指定文字列が違えば別の型になってくれて目的が達せられます(もっとも、書式指定文字列が全く同一でも別の場所に書くだけで異なる型になってしまいますが……)。

ここで不思議なのは、

template<class Str>
auto ToFormatString( Str str ) -> decltype( ToFormatString_emptyCheck<str()[0]>{}( str ) );

のようなコードで、仮引数strメンバ関数呼び出しをした結果をToFormatString_emptyCheck構造体の非型テンプレート引数にしても普通にコンパイルできる点です。 実際のところ、Str型のどんなオブジェクトからも同じ結果が返ってくるのでこれは問題にはなりませんが、strにアクセスした時点ではread of non-constexpr variable 'str' is not allowed in a constant expressionというコンパイルエラーにならないのはちょっと不思議です。 ちなみに、ラムダ式や自作関数オブジェクトにして、内部状態を持たせ、operator()内部でその内部状態に依存する分岐を行わせようとすると確かにread of non-constexpr variable 'str' is not allowed in a constant expressionというコンパイルエラーが発生します。

つまり、this->operator()()を行った時点ではthisに触ったことにはならず、this->xみたいなメンバ変数にアクセスした時点でread of non-constexpr variable 'str' is not allowed in a constant expressionコンパイルエラーになるのですね。

知識なしでMastodonを改造する

先週の記事でも言ったけれど、私はこの分野の知識はほぼゼロなので、余り役には立たないかも。

Mastodonオープンソースです。オープンソースのよいところはいろいろありますが、そのうちの一つに「気に入らなければ勝手に改造できる」というのがあります。

Mastodonのタイムラインは(Twitterと違って)時系列順に並んでいますが、(Twitterと同じく)相対時間表示になっています。これは気に入らないので改造していきます。

相対時間表示の代わりに絶対時間表示にする

Mastodonの規模はそれほど大きいわけではありませんが、ソースコードを一から読んでいくというのはそれなりに厳しそうです。 そもそも、この分野の知識はほとんどないので、その勉強から始めるとなると気が遠くなります。 そこで、いろいろ眺めてみて、どこが変更に必要そうなところかを探っていきます。

htmlのソースコードを見ていくと、datetime="2018-08-17T22:42:51.543Z"のようなものが入っていることが分かります。それから、5分前みたいなのは普通に書かれていることが分かります。少し待っていると6分前に変わったので、きっとJavascriptでこの辺が操作されているということが推測できます。

そのJavascriptファイルを探し当てたいところですが、どうやら難読化(というより短縮化)がかかっているようで、元のファイルを探す必要がありそうです。

datetimegrepをかけてみれば、きっとその辺のソースコードが見つかるはずです。……とやってみると、かなりの数が引っかかってしまいました。ダメもとでtime datetimeで検索してみると……うまくいきました。app/javascript/mastodon/components/relative_timestamp.jsといういかにもそれらしいファイルが見つかります。実際、中を見てみるとまさにこれが求めるものだということがすぐにわかります。

改造すべき場所はわかりましたが、どうやれば絶対時間を表示できるかの問題を解決していかないといけません。そもそも、文字列をどのように扱うのかがいまいちわからない……。

幸いヒントがあります。七日より前の場合、相対時間表示ではなく絶対時間表示にするようなコードが書かれているので、そこをまねすればよさそうです。

{ ...obj, tag: val }という構文が見慣れないですが、使われ方から見て、「objの要素はそのまま、tag: valを追加する」くらいの意味合いで使われていそうです。よくわかりませんがまねして書いてみます。

(どうやらECMAScript 2018で追加された新機能で、スプレッド構文というらしい。参考文献: Mastodon フロントエンド改造入門 #thebossblog こういう構文に関する情報を無から探し当てるのは初学者には無理があるし、やはり一から学ぶしかないのでしょうか……。)

とはいっても相対時間表示はそれなりに役立つので併記することにしましょう。できあがったコミットがこちら→

Add absolute time · lpha-z/mastodon@b6e2638 · GitHub

(8月18日)のように無駄にかっこがついてしまう問題がありますが、大きな問題ではないし、文字列の扱い方がよくわからないのでしょうがない。今度勉強して直しておきましょう。

コンパイル

ブラウザに流れ込んできているJavascriptコンパイル済みのものになっているので、再コンパイルが必要です。インスタンスを立ち上げた時のウィザードをもう一度やり直せばOKです。そんな遠回りをしなくても、RAILS_ENV=production bundle exec rails assets:precompileを行えばよいです。これをどう見つけるかという話ですが、ウィザードに書いてあります。また、デーモンを再起動(stopしてからstart)する必要があります。

相対時間表示を詳細化する

先々週くらいに言っていた、「1時間前、って書かれても幅が広すぎてわかりづらい!」という文句を改善していきたいと思います。

ソースファイルは、先と同じrelative_timestamp.jsです。そもそも、このソースファイルの中には、5分前という表示ができるだけの情報がないように思われます。「分前」という文字列がどこにもないです。 きっと多言語対応のために別のファイルに定義があるのだと推測します。実際、分前grepすると、app/javascript/mastodon/locales/ja.jsonが引っかかります。どうやらさっきの推測は正しかったようです。

あとは簡単で、適当に修正すればうまくいくはずです。できあがったコミットがこちら↓

Detail relative time (Japanese only) · lpha-z/mastodon@42be44b · GitHub

実はこれはダメ

これでうまくいったと思っていましたが、相対時間表示が1時間0分で止まってしまいました。実は相対時間部分の更新を行うJavascriptは常に動いているわけではなくて、定期的に活動しているようです。で、その更新頻度が1時間に一回になっているため、1時間0分で止まってしまっているようです。UnitDelayって最初何のことかと思っていましたが更新頻度のことだったようです。というか、後から見てみれば_scheduleNextUpdateの中で使用されているし、明らかに更新頻度の定義です(この関数は意味がよくわからず最初読み飛ばしていました……)。

というわけで修正したコミットがこちら↓

Increase frequency of updating relative time · lpha-z/mastodon@a317ef9 · GitHub

reblog(ブースト)の時刻を表示する

これも重要な情報だと私は思うのですが、どうやら表示されていないようです。これも追加したいところですが、今までの手法だけでは太刀打ちできなさそうです。 つまり、今までの二つの変更は情報の表示方法の問題だったのに対し、今回行いたい変更は、新たに情報を持ってくる必要があります。 とはいえ、まずはどこを改造するかを探し当てるところからです。

まず、htmlを眺めているとstatus__wrapperがキーワードになっているようなので、これでgrepします。運よくapp/javascript/mastodon/components/status.jsの一件だけが見つかったのでこれでまず間違いないでしょう。

しかし、肝心のreblogされた時刻はこのソースファイル中では使う予定がないため、適当な変数に代入されている様子がありません。おそらくデータとしてはあるはずですが(そうでないと時系列順にならばないはずです)、オブジェクトのどこに格納されているかが分かりません。 型なし言語はこういう時に困ります……。

このソースファイルのthis.props(なのかな?)に渡されているオブジェクトを構築している部分を探し当てる必要があります。 あるいは、ブラウザでデバッグコンソールにダンプしてもよさそうです。

私の場合は、上記手段のやり方がよくわからなかったため、このソースコードをじっくり眺めました。その結果、

  • status.get('reblog')はreblogだったときに限って、そのreblogしたトゥートが入っている
  • status.get('account')は、
    • 普通のトゥートの時は、そのトゥートをしたアカウントの情報が入っている
    • reblogの時は、reblogしたアカウントの情報が入っている(reblogしたトゥートをしたアカウントの情報ではない)

ということが読み取れます(92, 93行目で変数の上書きを行っているところ)。

また、トゥートを行った時刻は、<RelativeTimestamp timestamp={status.get('created_at')} />という形でRelativeTimestampに渡されていることも読み取れます。 つまり、reblog元のトゥートの作成時間は、status.get('reblog').get('created_at')に入っています。

時刻を格納する変数は一つしかなくて、そこにはトゥート時刻が収まっているためreblog時刻はオブジェクトの中には入っていない、といったパターンも考えられましたが、そうなっている可能性は低そうであることが分かりました。

そこで、status.get('created_at')にreblog時刻が入っていると信じてやってみると……。うまくいきました。

できあがったコミットはこちら↓

Show reblog time (Japanese only) · lpha-z/mastodon@696fafd · GitHub

ふぁぼられた時間を表示する

Twitterにもあるし、これくらいの表示はしたいところです。

app/javascript/mastodon/locales/ja.jsonから逆算して、notification.favouriteなどが含まれているソースファイルを調べてみると、app/javascript/mastodon/features/notifications/components/notification.jsが該当することが分かります。

まず先ほどと同じように<RelativeTimestamp timestamp={notification.get('created_at')} />などとやってみると、コンパイルでエラーになります。 これは当然で、import RelativeTimestamp from '../../../components/relative_timestamp';がないといけないということはすぐにわかります。

この修正を行えばコンパイル自体は通るのですが、ブラウザ側でエラーになります。やはりcreated_at要素がないようです。

やはりこのソースファイルにわたってくるオブジェクトを作成している、上位のソースファイルを探す必要がありそうです。

さんざんいろんなソースファイルを探し回った挙句、app/javascript/mastodon/reducers/notifications.jsを発見しました。

加える変更はたった一行ですが探すのが非常に大変でした……。

できあがったコミットはこちら↓

Show notification time (Japanese only) · lpha-z/mastodon@a74d261 · GitHub

おわりに

さっぱり知識がない状態で、いかに改造すべきポイントを探し当てて改造していくのかという過程を紹介してみました。

みんなもMastodonインスタンスを立てて改造してみましょう。

知識ゼロからマストドンインスタンスを立てた

自分用にマストドンインスタンスを立てました。→ https://radon.lpha-z.net

いろいろとさびしいので、遊びに来てくれるとうれしいです。 Twitterの方を動かさなくなるというわけではないです。

さて、マストドンインスタンスを立てたい誰かの助けになればと思って、やったことをメモしておきます。 注意点として、私はネットワーク方面の知識を全く持っていないので、結構まずいことをやっているかもしれないです。 そういう点の指摘があれば、コメント欄に書いていただけると助かります(大歓迎です)。

基本的には知識ゼロでできるはずですが、Linux系のコマンドを全く叩いたことがないという場合は少し難しいかもしれません(もちろん、手順通りやればいいのですが、やってることの意味が全くわからないと、間違った時に解決方法がわからないかもしれません)。

要約

  • サーバーは、AWSを使う(一年間は無料でできます)
  • ロードバランサーとかデータベース用サーバーとかは使わないで、EC2のインスタンス一個でやる(私がよくわからないのと、小規模ならその方がお金もかからないし……)
  • 自分用のドメインが必要です(一年間で1000円とか2000円とかそこらです)

手順

AWSに入門する

まずはAWSのアカウントを作りましょう。クレジットカードの登録が必要ですが、無料の範囲内でやっていきます。

次に、以下の記事の3.までを読んでAWSに関する知識を得ながら構築を進めます。注意としては、4.以降にかかれている情報は古い(前にマストドンが流行った2017年4月ごろのものにもとづいている)ので、そこは行いません。あと、ドメインをRoute53で入手したり、Amazon Certificate ManagerでSSL証明書を入手したり*1、Hosted Zoneの設定をしたりは今回はやりません。

AWSでMastodonインスタンスを作るまで。自分まとめ

上の記事ではAMIの選択とインスタンスタイプが説明されていません。立てるスタンスは、Ubuntu Server 18.04LTSが良いようなのですが、無料の範囲内にそういったのがないので、.NET Core 2.1 with Ubuntu Server 18.04 - Version 1.0 (ami-6ccaa781)を選びました。本当にこれでいいのかな?インスタンスタイプは、無料になっているt2.microを選びました。 上の記事と異なる点として、セキュリティグループの設定では、HTTPとHTTPSも許可します。HTTPはなくてもよいような気がします。

ドメインを入手し、DNSを設定する

自分用のドメイン(なんとか.comとか、そういうやつです。私のだとlpha-z.net)を手に入れます。私はお名前.comで取得しました。特にこれを推奨しているわけではありませんが、以下の説明はお名前.com特有の説明になります。DNSについての最低限の知識があれば、他のところで取得しても、以下の手順を適当に自分の取得したところ用の手順に読み替えて行えるはずです。そういったサービスがないのなら、Route53でやればよいような気もします。0.5ドルかかるらしいです。

ドメインを入手した後、ログインします。DNSのタブを開きます。DNSレコード指定で、自分のマストドンインスタンスのホスト名(私のだとradon.lpha-z.net。.lpha-z.netはすでに入っているのでradonとだけ入力)を入力します。TYPEはAとして、TTLはなんでもよいのでとりあえずデフォルトのままにしておきます。VALUEは、EC2のインスタンスに割り当てられたIPv4 パブリックIPを入力します。

ところで、ここで入力するホスト名は、マストドンインスタンスを立ち上げてから変えようと思うと面倒なことになるので、慎重に決めた方が良いです。マストドンアカウントは、それ込みになっているからです。

マストドンインスタンスを立ち上げる

ネット上にある記事は、ほとんど2017年4月ごろのものなので(大事なことなので二回言いました)、参考にすると痛い目にあいます。 公式のドキュメント documentation/Production-guide.md at master · tootsuite/documentation · GitHub を眺めながら行いましょう。

以下、手順を詳しく説明します。上のドキュメントに、私の理解での補足説明を加えていますが、怪しい部分が多いと思います。Linuxの知識が少しでもあり、英語が全く読めないというわけではなければ、上のドキュメントを見れば十分だと思われます。

ちなみに、大慌てでやっても一時間くらいかかります。

スワップファイルの設定をする

最後の方の「コンパイル」のところでメモリが足りなくなると困るので、念のためスワップファイルの設定をしておきます。t2.microのメモリ量だと本当にぎりぎりのようです。

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

依存しているソフトウェアをインストールする

この部分はrootユーザーでやってくださいと書いてあります。これは、ubuntuであれば、sudoをつけて実行してくださいという風に読み替えれば十分です。以下の説明では必要なところにsudoを加えて説明していますから、その通りに入力すれば大丈夫なはずです。

node.jsを入れる

node.jsとは、サーバーサイドJavaScript環境だそうです(Wikipedia)。 まず、

sudo apt -y install curl

curlというアプリケーションをインストールします。おそらく、これは更新不要と出るはずです。 次に、

curl -sL https://deb.nodesource.com/setup_8.x | sudo bash -

と入力してnode.jsをインストールします。sudoの位置はここです。これは、このURLから入手したスクリプトを実行するというコマンドになっています。 何が送り付けられてくるのかはよくわかりませんが、とにかくよしなにやってくれるようです。

yarnを入れる

yarnはJavascript用のパッケージマネージャーだそうです。以下のコマンドを入力し手インストールします。

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update

入力ミスに気を付けます。入力ミスをしてもOKなどと表示されるのですが、以降の手順でエラーになってしまうという罠が存在します(私は引っかかりました)。 こちらは得体のしれないスクリプトを実行するのではなく、aptというパッケージマネージャーを通して行っているようです。

その他の依存関係を入れる

そのほかの、aptコマンドで入れられるものをたくさん入れます。

sudo apt -y install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev file git-core g++ libprotobuf-dev protobuf-compiler pkg-config nodejs gcc autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev nginx redis-server redis-tools postgresql postgresql-contrib certbot yarn libidn11-dev libicu-dev

以上のコマンドは長いですが、間違いなくコピペして入力し、実行します。 私は、コピペの際に後ろが途切れてしまい、以下の手順でrubyコンパイルできないだのpostgresqlなんてアカウントはないだのと怒られるドジを踏みました……。

たくさんインストールするので、二分ちょっと時間がかかります。

特権ユーザーじゃなくてもできる依存関係を入れる

準備

今までの手順は、Ubuntu本体に(?)いろいろインストールしていました。マストドンでは、Rubyの特定のバージョンを使っています。こういう時に、Ubuntu本体にその特定のバージョンのRubyを入れてしまうと、ほかのバージョンのRubyを使いたいという場合、困ってしまいます。ここで、特定のバージョンのRubyを、自分でコンパイルして自分だけが使えるようにしてしまえば、その問題は発生しません。そんなことを実現するのがrbenvだそうです。

こういうとき、ユーザーを分けて行えば、何も指定しなかった時のrubyコマンドが自前コンパイルしたrubyになるようにそのユーザーの環境変数に指定しておくことで、rbenvを使っていることを意識せずに行うことができます。

というわけでユーザーを作ります。

sudo adduser mastodon

ドキュメントには書いていませんが、ここでパスワードを決める必要があります。以降使わないので、適当に決めます。住所とかわけのわからないことを聞かれますが、単にエンターを押すだけでスキップできますので、飛ばします。 ユーザーmastodonとしてログインします。

sudo su - mastodon

ログインできました。続いて、次の手順を行います。ここからはsudoは必要ありません(むしろ使えません)。

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv
src/configure
make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec bash
type rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

rbenvをgithubから持ってきて、rubyをビルドする準備を整えました。 type rbenvを実行すると関数みたいなものが出てくれば大丈夫なのだと思います(答えが載っていないのでわからないのですが、私がやったらそうなりました)。

Ruby2.5.1を使えるようにする

Rubyコンパイルします。

rbenv install 2.5.1

コンパイルには五分ほど時間がかかります。止まっているように見えますが、ゆっくり待ちます。

rbenv global 2.5.1

どこでもruby2.5.1を使えるようにするという意味っぽいです(?)

マストドンの依存関係をインストールする

まず、マストドンリポジトリを持ってきます。ホームディレクトリの下のliveというディレクトリに入れることにします。

cd ~
git clone https://github.com/tootsuite/mastodon.git live
cd ~/live

マストドンのmasterブランチは、開発中でいろいろバグが含まれているので、安定板を使います。以下のコマンドは、自動的に最新のバージョンを判定してそれを使えるようにするワンライナーになっています。

git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)

長いですね。今だと最新版はv2.4.3なので、実質的にはgit checkout v2.4.3と同じになります。

以下のコマンドを入力して、依存関係をいろいろインストールします。gemとかbundleが何をやっているのかは知りませんが、まぁなんかうまい具合に依存関係を解決してくれているようです。

gem install bundler
bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without development test

-j$(getconf _NPROCESSORS_ONLN)の部分は、並列化オプションだと思われますが、t2.microのインスタンスだと1coreしか使えないので、指定する意味ないような気がします。 大体三分くらいかかりました。

yarn install --pure-lockfile

これには一分くらいかかりました。ところで、以下のように表示される場合、yarnのインストールをミスっています(先ほど書いた、私の踏んだドジです……)。

Usage: yarn [options]

yarn: error: no such option: --pure-lockfile
mastodonユーザーからubuntuユーザーに戻る

この後の作業は、また特権が必要な作業になります。そこで、以下のコマンドでubuntuユーザーに戻りましょう。

exit

サーバーのいろいろな設定

PostgreSQLの設定

PostgreSQLというのは、データベースを管理するソフトウェアです。マストドンのいろいろなデータをうまいこと保存してくれるようです。その設定を行っていきます。

sudo -u postgres psql

postgresというユーザーとして、psqlコマンドを実行するという意味っぽいです。すると、いつもと違ったプロンプト(入力待ってますマーク)が出ますので、

CREATE USER mastodon CREATEDB;

と入力します。これで仕事は終わりで、このプロンプトから脱出するために

\q

と入力してもとのbashに戻ってきます。

nginxの設定

nginx(えんじんえっくす、と読むらしい)は、webサーバーのソフトウェアです。HTTPSアクセスが来たら何を返すべきか、とかそういったことの設定を行います。

sudo nano /etc/nginx/sites-available/example.com.conf

でその設定ファイルを開きます。そのあと、以下のテキストを入力します。ただし、 example.comになっている部分は、全て自分のホスト名に置き換えてください。四か所あります。

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  root /home/mastodon/live/public;
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/ { allow all; }
  location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com;

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 8m;

  root /home/mastodon/live/public;

  gzip on;
  gzip_disable "msie6";
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

  add_header Strict-Transport-Security "max-age=31536000";

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri @proxy;
  }
  
  location /sw.js {
    add_header Cache-Control "public, max-age=0";
    try_files $uri @proxy;
  }

  location @proxy {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  location /api/v1/streaming {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";

    proxy_pass http://127.0.0.1:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;
}

保存してからエディタを終了したいので、下の説明を見ます。^OでWriteOutだと書いてありますね。これは、コントロールキーとOキーを同時に押してくださいという意味です。

cd /etc/nginx/sites-enabled
sudo ln -s ../sites-available/example.com.conf

よくわかりませんがシンボリックリンクを作成して、完了です。

Let's encryptを利用してSSL証明書を取得する

HTTPSでセキュアな通信をするためには、SSL証明書が必要です。Let's encryptを使うと手軽にSSL証明書を取得できます。 まず、nginxが動いている状態では行えない(止めてくださいと怒られる)ので、以下のコマンドで停止させます。

sudo systemctl stop nginx

次に、以下のコマンドでSSL証明書を取得します。もちろん、example.comの部分は作ろうとしているマストドンインスタンスのホスト名に書き換えてください。

sudo certbot certonly --standalone -d example.com

まず、メールアドレスを聞かれるので、マストドン用に使いたいメールアドレスを入力します(私はgmailアカウントを一つ作成しました)。規約に同意するかを聞かれますから、同意する場合はAを入力します(同意しないとSSL証明書は手に入りません)。宣伝メール(?)を送っていいか聞かれますが、ここはYでもNでも問題なさそうです。これでSSL証明書が手に入り、適切な位置に配置されます。

sudo systemctl start nginx

さっき停止させたnginxを再開させます。もし、ここで変なエラーが出る場合、先ほど作ったnginxの設定ファイルが間違っている可能性があります(たとえば、example.comのままだったりするとそうなります)。

sudo certbot certonly --webroot -d example.com -w /home/mastodon/live/public/

証明書を更新するか聞かれます。公式ドキュメントによれば、ここは更新する(2を入力する)を選んでくださいと書いてありますが、更新しない(1を入力する)を選んでもうまく動きました。ちなみに、更新するのは一週間に五回くらいしか行えないらしいので、変なことになったからまた一からやり直そうみたいなことを繰り返しやっているとエラーになります。

SSL証明書の更新を自動化する

Let's encryptで手に入れたSSL証明書は、90日で失効してしまい更新が必要になります。いちいち手作業でやるのは面倒ですので自動化させましょう。定期的にコマンドを実行してくれるcronを使います。まず以下のように設定ファイルを編集します。

sudo nano /etc/cron.daily/letsencrypt-renew

ここに以下に示したような、自動実行すべきスクリプトを入力し……

#!/usr/bin/env bash
certbot renew
systemctl reload nginx

さっきと同じように保存してエディタを終了させます。ところで、このスクリプトの中にはsudoが書かれていません。これで大丈夫なのかという感じですが、crontabではなく直に作成しているので大丈夫なんだと思います(たぶん……)。crontabを使った場合はそのユーザーの権限で実行されるため、sudoが必要です。

sudo chmod +x /etc/cron.daily/letsencrypt-renew
sudo systemctl restart cron

先のスクリプトファイルに実行権限を与え、cronを再起動しておきます。

マストドンの設定を行う

またmastodonユーザーになりましょう。

sudo su - mastodon
全体の設定

以下のコマンドで、いろいろな設定を行ってくれるウィザードが出てきます。

cd ~/live
RAILS_ENV=production bundle exec rake mastodon:setup

Domain nameのところは、ホスト名を入れます。

Do you want to enable single user mode?のところは、「おひとり様インスタンス」にしたいかを聞かれていますので、Y/Nで答えます。私はNを選んだので、radon.lpha-z.netは私以外でも登録できます。

Are you using Docker to run Mastodon?のところは、Docker上で動かすかを聞かれています。どちらがいいのかわからないので、Docker上で動かさない(N)を選びました。

以下、PostgresSQL(データベース)とRedis(キャッシュらしい)の設定が始まりますが、デフォルトの設定で問題ありません。エンターを押せばデフォルトの設定になります。合計7回エンターを押します。

Do you want to store uploaded files on the cloud?の部分では、画像などを外部サーバーに保存するかを聞かれていますが、設定も面倒なのでローカルに保存することにします。Nを入力しました。

Do you want to send e-mails from localhost?の部分では、メールをこのサーバーから送るかを聞かれています。以下の説明はかなり怪しいです。

どうもEC2のサーバーからメールを送信するのは、スパムメールの問題から面倒事が多いらしいです。そこで、自前のgmailアカウントから送信することにします。

  • SMTP server: smtp.gmail.com
  • SMTP port: 587 (デフォルトでOK)
  • SMTP username: example@gmail.com
  • SMTP password: (見えませんが、入力します)
  • SMTP authentication: plain
  • SMTP OpenSSL verify mode: none
  • Email address to send e-mails "from": (デフォルトでOK)

上のような設定でうまくいきました。ただし、gmailの設定で「安全性の低いアプリからのアカウントへのアクセスを許可する」をオンにしておく必要があります。また、Let's encryptの証明書をとった時のメールアドレスでないと、テストメールは送信できても本番のメールが送信できないようなトラブルが発生しました(原因はよくわかりませんが)。そもそも、plain, noneになっているのって本当はやばい設定になっているんでしょうか……?あと、メールのパスワードが設定ファイルに平文で書かれるのはちょっと気持ち悪い……。

この設定をした後、テストメールを送るか聞かれますので、適当なメールアドレスを入力して、送信できるかと受け取れるかを確認します。パスワードの入力があっているかを二回入力で確かめたりしていないので、これはやった方が良いです。

Save configuration?とこの設定を保存するか聞かれますから、Yと答えます。ここでNと答えると、今まで入力したのが全部破棄されて一からやり直しになるので注意しましょう。

データベースの初期化

Prepare the database now?と聞かれるので、Yと答えてデータベースを初期化しましょう。ところで、いったんデータベースを作った後、諸事情があって一からやりなおしたばあい、既にデータベースがあるということで、上書きされるのではなく、エラーが出ます。その場合は、RAILS_ENV=production bundle exec rake mastodon:setupではなく、RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake mastodon:setupとすることで、エラーにならないで上書きすることができます。

JavascriptCSSコンパイル

手で書いたJavascriptファイルは変数名が長く処理が遅いらしいです。そのため(?)、コンパイルを行って必要最低限のJavascriptファイルにするようです(?)。あるいは、Javascriptではない言語で書かれたコードをJavascriptに変換することをコンパイルと呼んでいるのかな?いずれにせよ、コンパイルをやるらしいです。これには五分くらいかかります。メモリがたくさん必要になります。t2.microだと最初のスワップファイル作成を忘れているとコンパイルできないかも。

Adminアカウントの作成

マストドン上でのAdminアカウントを作成します。アカウント名とメールアドレスを入力すると、仮のパスワードが出てくるので、これを使ってログインすることができます。メールアドレスはログインするときに必要になります(ログインするときに必要なのはアカウント名ではないです)。仮のパスワードは、初回ログイン後に変えておきましょう。

これで設定はおしまいです。特権ユーザーに戻りましょう。

exit

マストドンのサービス用のデーモンを作る

マストドンのウェブページにアクセスしたときには、単に静的なページを表示するだけではなく、いろいろな仕事をする必要があります。たとえば、トゥートしたからそれをデータベースに書き込んで、とかいう要求が一例です。そういった要求を待ち構えているプロセス(デーモン)を作る必要があります。

まず、

sudo nano /etc/systemd/system/mastodon-web.service

で設定ファイルをエディタで開き、以下の内容を書き込んで保存。

[Unit]
Description=mastodon-web
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="PORT=3000"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -SIGUSR1 $MAINPID
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

同様に、

sudo nano /etc/systemd/system/mastodon-sidekiq.service

で以下を入力して、

[Unit]
Description=mastodon-sidekiq
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=5"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

さらに

sudo nano /etc/systemd/system/mastodon-streaming.service

で以下を入力して

[Unit]
Description=mastodon-streaming
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
ExecStart=/usr/bin/npm run start
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

以上の三つの設定ファイルを作りました。

これらを以下のコマンドで有効にします。

sudo systemctl enable /etc/systemd/system/mastodon-*.service

で、これらを開始します。

sudo systemctl start mastodon-*.service

以下のコマンドで、動いているかを確認します。

sudo systemctl status mastodon-*.service

なぜか何も出てきません。

これはUnixの「何も出てこなければ正常」ではありません。実際に動いていません。

これはよくわからないのですが、以下のように個別に開始することで、動くようになりました。なんで?

sudo systemctl start mastodon-web.service
sudo systemctl start mastodon-sidekiq.service
sudo systemctl start mastodon-streaming.service

古い画像とかのキャッシュを消す

他のインスタンスから流れてきたトゥートに添付されている画像や動画は、自分のサーバーにキャッシュされますが、延々たまっていくと邪魔なので、それを定期的に削除するcronを書いておきましょう。 crontabで作ったスクリプトは、設定を作ったユーザーの権限で行われます。

sudo su - mastodon
crontab -e

でエディタの選択肢が出るので好きなエディタを選んで

RAILS_ENV=production
@daily cd /home/mastodon/live && /home/mastodon/.rbenv/shims/bundle exec rake mastodon:media:remove_remote

を入力して保存して終了します。


以上の手順で自分のマストドンインスタンスを立てることができるはずです。

公式のドキュメントがもれなく書いてあるので、それを読めばネットワークなどの知識がなくても動かすことができます。

マストドンを使った雑感とか、改造した部分とかについても書こうと思ったのですがまた別のブログ記事で書くことにします。

あとさみしいので私のマストドンインスタンスにも遊びに来てください……。

radon.lpha-z.net

*1:Amazon Certificate Manager(ACM)で証明書を発行するとき、Aレコードがないといつまでも証明書が発行されなくて、悩みました