<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[てくにかるむ]]></title><description><![CDATA[「エラーをなくすことは非常に有益で時には新しい真実や事実を作り上げるよりも勝る」
ー チャールズ・ダーウィン]]></description><link>http://multix.jp/</link><generator>Ghost 0.6</generator><lastBuildDate>Mon, 06 Apr 2026 23:51:51 GMT</lastBuildDate><atom:link href="http://multix.jp/tag/tecnicalroom/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[WebBrowserコンポーネントいろいろ]]></title><description><![CDATA[<p>WebBrowserコンポーネントを .NET Framework であれこれ料理したときのいろいろ。</p>

<ul class="index"></ul>

<hr>

<h3 id="">レンダリングバージョンを変更する</h3>

<p>WebBrowserコンポーネント<sup id="fnref:1"><a href="http://multix.jp/webbrowser-component-etc/#fn:1" rel="footnote">1</a></sup>は、何もせずにそのまま使用すると必ず IE7相当のレンダリングエンジンと JavaScriptエンジンになってしまう。これは <code>&lt;meta http-equiv="x-ua-compatible" content="IE=Edge" /&gt;</code> などを食わせても変わらない。<sup id="fnref:2"><a href="http://multix.jp/webbrowser-component-etc/#fn:2" rel="footnote">2</a></sup> この問題を解決するには、レジストリを触るしか方法がない。</p>

<pre><code class="language-brush:vb ">Try  
    Dim RendererVersion As Integer = 11000
    Dim ExecName As String = Path.GetFileName(Environment.GetCommandLineArgs()(0))
    Dim Regkey As RegistryKey = _
        Registry.CurrentUser.CreateSubKey( _
            "SOFTWARE\Microsoft\Internet Explorer\</code></pre>]]></description><link>http://multix.jp/webbrowser-component-etc/</link><guid isPermaLink="false">54302723-107c-4a26-93fd-27768651728a</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[VB.NET]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 17 Apr 2017 09:22:12 GMT</pubDate><content:encoded><![CDATA[<p>WebBrowserコンポーネントを .NET Framework であれこれ料理したときのいろいろ。</p>

<ul class="index"></ul>

<hr>

<h3 id="">レンダリングバージョンを変更する</h3>

<p>WebBrowserコンポーネント<sup id="fnref:1"><a href="http://multix.jp/webbrowser-component-etc/#fn:1" rel="footnote">1</a></sup>は、何もせずにそのまま使用すると必ず IE7相当のレンダリングエンジンと JavaScriptエンジンになってしまう。これは <code>&lt;meta http-equiv="x-ua-compatible" content="IE=Edge" /&gt;</code> などを食わせても変わらない。<sup id="fnref:2"><a href="http://multix.jp/webbrowser-component-etc/#fn:2" rel="footnote">2</a></sup> この問題を解決するには、レジストリを触るしか方法がない。</p>

<pre><code class="language-brush:vb ">Try  
    Dim RendererVersion As Integer = 11000
    Dim ExecName As String = Path.GetFileName(Environment.GetCommandLineArgs()(0))
    Dim Regkey As RegistryKey = _
        Registry.CurrentUser.CreateSubKey( _
            "SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BROWSER_EMULATION")
    Regkey.SetValue(ExecName, RendererVersion)
    Regkey.Close()
Catch ex As Exception  
    Console.WriteLine("CreateSubKey: " &amp; ex.StackTrace)
End Try

WebBrowser1 = New WebBrowser()  
WebBrowser1.ObjectForScripting = Me  
</code></pre>

<p>レジストリには、キー＝実行ファイル(EXE)名、値＝IEバージョンの1000倍のDWORD値 を登録する。存在しない時の既定値は 7000 と見做される。またこのとき Visual Studio のデバッグRunでは .EXE名が <code>〜.vhost.exe</code> になるのを考慮する必要がある。結局のところ、プログラム実行毎に毎回自分自身の .EXE名を新規／上書き登録してしまうのが手堅い。またレジストリの登録タイミングは初めて <code>New WebBrowser()</code> する以前でなければならない。</p>

<hr>

<h3 id="vbnet">マウスイベントを VB.NET側で使いたい</h3>

<p>WebBrowserコンポーネント内で発生したイベントはほぼすべて JavaScript内で完結して外部（Windows.Forms）に伝播しない。必要なら JavaScriptから ObjectForScripting機能経由で Formクラスを呼んで RaiseEvent しろということになっている。だがそういう毎回使うような定形処理はサブクラス化してまとめといたほうが良いに決まっている。</p>

<pre><code class="language-brush:vb title: WebBrowserEx.vb">Imports System.Windows.Forms  
Public Class WebBrowserEx  
    Inherits WebBrowser

    Private WithEvents Body As HtmlElement

    Public Shadows Event MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs)
    Public Shadows Event MouseLeave(ByVal sender As Object, ByVal e As MouseEventArgs)

    Protected Overrides Sub OnDocumentCompleted(ByVal e As WebBrowserDocumentCompletedEventArgs)
        Me.Body = Me.Document.Body
        MyBase.OnDocumentCompleted(e)
    End Sub

    Private Sub Body_MouseDown(ByVal sender As Object, _
                               ByVal e As HtmlElementEventArgs) _
                           Handles Body.MouseDown
        RaiseEvent MouseDown(sender, New MouseEventArgs(e.MouseButtonsPressed, 1, _
                                                        e.ClientMousePosition.X, _
                                                        e.ClientMousePosition.Y, 0))
    End Sub

    Private Sub Body_MouseLeave(ByVal sender As Object, _
                                ByVal e As HtmlElementEventArgs) _
                            Handles Body.MouseLeave
        RaiseEvent MouseLeave(sender, New MouseEventArgs(MouseButtons.None, 0, _
                                                         e.ClientMousePosition.X, _
                                                         e.ClientMousePosition.Y, 0))
End Class

' WebBrowser1 = New WebBrowserEx()  
' WebBrowser1.Navigate("about:blank")  
</code></pre>

<p>ここでは一部しか書いてないが、Up/Enter/Move/Over も同様に実装<sup id="fnref:3"><a href="http://multix.jp/webbrowser-component-etc/#fn:3" rel="footnote">3</a></sup>できる。</p>

<p>ただこの実装においては、WebBrowserコンポーネント内で Refresh() する、あるいはF5(Ctrl+R)キーを押してのページリロードに対応できない。というのも .NET版の WebBrowserコンポーネントはリロード時に一切イベントを発火しないからだ。ページ内のJavaScriptでは通常通り onload等が発火するので ObjectForScriptingで補完する（Me.Document.Body を読み直す）必要がある。</p>

<hr>

<h3 id="jscripttypeinfonet">JScriptTypeInfo と .NET オブジェクトの相互変換</h3>

<p>ObjectForScriptingと InvokeScriptを使えば .NETと JavaScriptのユーザ関数を相互に呼び出し合うことが出来る。この時確実に引き渡せる引数は、数値・文字列・真偽値・null の4種類しかない。配列や連想配列（ハッシュ）はシリアライズする必要がある。だが実際には次のコードを試せば解るが、JavaScriptで生成された複雑なデータ構造は .NETのなかを素通りさせることができる。</p>

<pre><code class="language-brush:javascript title:JavaScript">var r = window.external.foo({a:[123, 456], h:{s:789}, s:"Test"});  
function bar (data) {  
    return {b:data.a[0] + data.h.s};
}
console.log(r.b); // 912  
</code></pre>

<pre><code class="language-brush:vb title:VB.NET">Public Function foo (ByVal d As Object) As Object  
    Console.log(d.s)    ' Test
    Console.log(d.a(1)) ' 456
    Console.log(d.h.s)  ' 789
    Return WebBrowser1.Document.InvokeScript("bar", d)
End Function  
</code></pre>

<p>WebBrowserコンポーネントにはこのJavaScriptネイティブのデータ構造＝JScriptTypeInfo型オブジェクトを解釈する機能（オブジェクト定義）がない。単純な文字列や数値は ToString()や ToInt32()メソッドで変換できるし、ハッシュの場合は Item() メソッドこそ使えるものの Count() も Containes() もないので「静的な構造体」以外は直接扱えないのだ。</p>

<pre><code class="language-brush:vb">Console.log(d.s)               ' sメンバーがあれば読み出せるがなければ例外発生  
Console.log(d.Item("s"))       ' これもメンバーがなければ例外発生  
Console.log(d.Conatines("s"))  ' これもメンバーがなければ例外発生  
</code></pre>

<p>.NETのほうでは3.5以降で JSON文字列をシリアライズ／デシリアライズ出来るため、引数も返値も JSON文字列化する取り決めにすれば良い話なのだが、ネイティブにデータ構造を受け渡しできたほうが（JavaScript側の視点では）利便性が良い。もっぱら融通が効かないのは静的型言語である .NETの側なので、こちらをもう少し頑張ってみよう。</p>

<p>取り敢えず、JScriptTypeInfoを読んで比較的扱いやすい Dictionary型にまるごと変換するなら次のようなコードが書ける。いったん IExpando型にキャストすればメンバーのプロパティリストを得られるので、CallByName()で実体を得ることができる。これを再帰的に適用すればいちおう全体にアクセスすることが出来る。ただし配列については手を抜いてハッシュ化してしまっているが。</p>

<pre><code class="language-brush:vb title:JScriptTypeInfo型を Dictionary型に変換する（暫定）">' Imports System.Reflection  
' Imports System.Runtime.InteropServices.Expando

Private Function JscInfoToDict(ByVal jscinfo As Object) As Dictionary(Of String, Object)  
    Dim dict = New Dictionary(Of String, Object)
    Dim keys() As String = DirectCast(jscinfo, IExpando).GetProperties( _
        BindingFlags.Default).Select(Function(p) p.Name).ToArray()
    For Each Key In keys
        Dim Var As Object = CallByName(jscinfo, Key, CallType.Get)
        ' Console.WriteLine("Key:" &amp; Key &amp; ", Var:" &amp; Var.ToString &amp; ", Type:" &amp; Var.GetType.ToString)
        If Var.GetType.ToString = "System.__ComObject" Then  ' これだけでは配列とハッシュの区別がつけられない
            dict.Add(Key, JscInfoToDict(Var))
        Else
            dict.Add(Key, Var)
        End If
    Next
    Return dict
End Function  
</code></pre>

<p>これとは逆に、Dictionary型を JScriptTypeInfo型に変換するのは、JScriptTypeInfo型の定義がないので不可能だ。ならば「JScriptTypeInfo型を知っている」言語に変換を委託してしまえば良い。そもそも相互変換をしたい場面において WebBrowserコンポーネントを使っていないということは滅多にないだろうから、InvokeScriptで JSONクラスメソッドを呼び出してしまえば良い。<sup id="fnref:4"><a href="http://multix.jp/webbrowser-component-etc/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:vb title:相互変換">' Imports System.Web.Script.Serialization

' Friend WithEvents Converter As WebBrowser  
' Converter = New WebBrowser()  
' Converter.Navigate(New Uri("about:blank"))

Private Sub Converter_DocumentCompleted(ByVal sender As Object, _  
                                        ByVal e As EventArgs) _
                                    Handles Converter.DocumentCompleted
    sender.Document.InvokeScript("eval", New Object() {"function __j2s(j){return JSON.stringify(j)};"})
    sender.Document.InvokeScript("eval", New Object() {"function __s2j(j){return JSON.parse(j)};"})
End Sub

Public Function jobject_decoder(ByVal jscinfo As Object) As Dictionary(Of String, Object)  
    Dim jss As JavaScriptSerializer = New JavaScriptSerializer()
    Dim jstring As String = Converter.Document.InvokeScript("__j2s", New Object() {jscinfo})
    Return jss.Deserialize(Of Dictionary(Of String, Object))(If(jstring, "{}"))
End Function

Public Function jobject_encoder(ByVal dict As Dictionary(Of String, Object)) As Object  
    Dim jss As JavaScriptSerializer = New JavaScriptSerializer()
    Dim jstring As String = jss.Serialize(dict)
    Return Converter.Document.InvokeScript("__s2j", New Object() {jstring})
End Function  
</code></pre>

<p>.NET版の InvokeScript() は COM版のそれとは異なり、ルート要素のユーザ関数しか呼び出せず、クラスメソッドを直接叩くことが出来ない。本来なら <code>JSON.stringify</code> <code>JSON.parse</code> を直接使いたいが使えないので、DocumentCompleted を待ってから変換用ユーザ関数を eval で登録する方法を取った。なお JSONクラスは IE7ではそもそも存在しないので、前述した IEバージョン変更のレジストリ登録が事前に必要になる。<sup id="fnref:5"><a href="http://multix.jp/webbrowser-component-etc/#fn:5" rel="footnote">5</a></sup></p>

<hr>

<h3 id="cscriptexejson">余談：cscript.exeでの JSON変換</h3>

<p>Windows標準でかつ標準入出力が使える汎用インタプリタとして、cscript.exe は貴重な存在だ。うまく使えば grep/sed/awk あたりの代用にもなる。だが標準では JSON変換機能を持っていないなど基本的な言語仕様が少々古い。しかし COMオブジェクトとして HTMLファイルにアクセスすると、その中の最新 JavaScriptエンジンから欲しい機能をアドオン的に持ってくることが出来る。</p>

<pre><code class="language-brush:javascript title:jconvert.js">// Ex) cscript.exe //Nologo //U jconvert.js &lt;STDIN &gt;STDOUT
var htmlfile = WScript.CreateObject('htmlfile'), JSON;  
htmlfile.write('&lt;meta http-equiv="x-ua-compatible" content="IE=9" /&gt;');  
htmlfile.close(JSON = htmlfile.parentWindow.JSON);

while (!WScript.StdIn.AtEndOfStream) {  
    var json = JSON.parse(WScript.StdIn.ReadLine()),
    // user code
    WScript.StdOut.WriteLine(JSON.stringify(json));
}
</code></pre>

<p>このテクニックは色々と応用が効く。同じようにして（役に立つかどうかはともかく）jQueryをインポートすることが出来るし、include／require文代わりに使うことも出来る。</p>

<p>なお cscript.exe の標準入出力は、素で起動すると ANSI すなわち日本語では CP932 になる。このため JSON文字列中のマルチバイト文字はエスケープされていなければ正しく扱えない。そこで //U オプション付きで起動すると UTF-16LE でマルチバイト文字を扱えるようになるが、改行コードも2バイト文字（ワード幅）にしなければならない罠がある。更に余談となるが、cscript.exe のスクリプトコードは、ANSI(CP932)または UTF-16LE（BOM付き）で記述されていなければならない。一般的な UTF-8N では文字化けが発生する。<sup id="fnref:6"><a href="http://multix.jp/webbrowser-component-etc/#fn:6" rel="footnote">6</a></sup></p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>ここでのサンプルコードは VB.NET だが、C#NETでも JScript.NET でも基本は変わらない。.NETではどの言語でコードを記述しようと、結果的に生成されるのは同一の共通言語オブジェクトだ。なお .NET Frameworkのバージョンは 4.0以降を前提とする。いまさら Vista 以前を動作対象にすることもないし、Win7でも 4.0 Runtime を入れれば済むことだ。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:1" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:2"><p>x-ua-compatible が実装されたのはIE8以降なのだから当然なのだが。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:2" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:3"><p>UpはDownと、Enter/Move/OverはLeaveとおなじ MouseEventArgs(...) 宣言を書く。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:3" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:4"><p>ここで使っている Converterはコントロール表示には使わないので Windows.Forms.Controls に Add する必要はない。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:4" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:5"><p>IEバージョンを変えないならば、Navigate() で読み込むページのなかのほうに、JSON変換関数を JavaScriptで自作して記述することになる。もっともそこまでの手間を掛けるなら、.NET側で頑張るよりも JavaScriptの方で普通に JSON変換して受け渡したほうがコード記述量は増えるものの、単純だろう。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:5" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:6"><p>これに気付いておらず「wscript／cscript は日本語が扱えない」とする発言や書籍がままあるので注意を要する。Windows本来のネイティブ文字コードは結構な昔から「ANSIおよびUnicode(16)」と決まっている。「メモ帳」が UTF-8Nファイルをそのまま保存せず、おせっかいにもBOMを挿入したりする所以もまたこれである。 <a href="http://multix.jp/webbrowser-component-etc/#fnref:6" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[ActivePerlのオフライン構築]]></title><description><![CDATA[<p>ActivePerl 本体は実行インストーラで何時でもインストールできるが、ppm や cpan で組み込む追加モジュールはオンライン構築が前提になっている。だが世間から断絶された秘匿ネットワークや、そもそもインターネットに接続するという概念のない PCやサーバに納品したり、機材リプレースで開発環境を再構築しなければならない場面では、USBメモリや DVD-Rからのオフラインインストールが出来なければ話にならない。そもそもバージョンが数世代古くなるとダウンロード元の ppmレポジトリが有料プラン専用に切り替えられて、いつもで気軽に再ダウンロードすることができなくなってしまう。それでは10年以上メンテ契約が続くような案件では、開発当時の状況保存は結構重要な問題になるので予防線を張っておくことは必要なのだ。</p>

<hr>

<h3 id="ppmx">.ppmxファイルの保存</h3>

<p>ppm install を実行すると、レポジトリから .ppmxファイルが tempディレクトリにダウンロードされてインストールされる。だがこのファイルはコマンド終了時に全削除されるため、普段目にすることがない。.ppmxファイル自体は ppm install の引数に渡すことでオフラインインストールできるため、消される前にファイルのコピーを別の場所に取っておけば、あとでおなじ環境を再構築することが容易くなる。</p>

<pre><code class="language-brush:perl gutter:true title:Client.patch">--- C:/Perl64/lib/ActivePerl/PPM/Client.pm      Tue Feb 16 06:02:11 2016
+++ D:/Perl64/lib/</code></pre>]]></description><link>http://multix.jp/activeperl-offline-installation/</link><guid isPermaLink="false">db8a724c-e2cd-49a8-9217-2a6cf62cc337</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[ActivePerl]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 10 Mar 2017 04:08:26 GMT</pubDate><content:encoded><![CDATA[<p>ActivePerl 本体は実行インストーラで何時でもインストールできるが、ppm や cpan で組み込む追加モジュールはオンライン構築が前提になっている。だが世間から断絶された秘匿ネットワークや、そもそもインターネットに接続するという概念のない PCやサーバに納品したり、機材リプレースで開発環境を再構築しなければならない場面では、USBメモリや DVD-Rからのオフラインインストールが出来なければ話にならない。そもそもバージョンが数世代古くなるとダウンロード元の ppmレポジトリが有料プラン専用に切り替えられて、いつもで気軽に再ダウンロードすることができなくなってしまう。それでは10年以上メンテ契約が続くような案件では、開発当時の状況保存は結構重要な問題になるので予防線を張っておくことは必要なのだ。</p>

<hr>

<h3 id="ppmx">.ppmxファイルの保存</h3>

<p>ppm install を実行すると、レポジトリから .ppmxファイルが tempディレクトリにダウンロードされてインストールされる。だがこのファイルはコマンド終了時に全削除されるため、普段目にすることがない。.ppmxファイル自体は ppm install の引数に渡すことでオフラインインストールできるため、消される前にファイルのコピーを別の場所に取っておけば、あとでおなじ環境を再構築することが容易くなる。</p>

<pre><code class="language-brush:perl gutter:true title:Client.patch">--- C:/Perl64/lib/ActivePerl/PPM/Client.pm      Tue Feb 16 06:02:11 2016
+++ D:/Perl64/lib/ActivePerl/PPM/Client.pm      Fri Mar 10 12:12:03 2017
@@ -1328,6 +1328,9 @@
                    if ($save_len != $len) {
                        die "Aborted download ($len bytes expected, got $save_len).\n";
                    }
+use File::Copy;
+my $File = File::Basename::basename($save, '.tgz');
+File::Copy::copy($save, $File.'.ppmx');
                }
                # XXX An MD5 checksum for the tarball would be a good thing
            }
</code></pre>

<p>5.18以降に通用するこのパッチを当てると、カレントディレクトリにダウンロードされた .ppmxファイルが残されるようになる。依存関係で複数のモジュールが取得された場合はその全てが残される。このコードを見れば解るが、 .ppmxファイルの実態は単なる .tgzアーカイブだ。わざわざ拡張子を代えているのは、そうしないと ppmコマンドがオフラインインストール用ファイル名として認識しないからである。</p>

<p>なお ppmコマンドは自前のデータベースとキャッシュでダウンロード履歴を管理しており、毎回必ずしもファイルダウンロードが発生するわけではない。情報は以下のフォルダに保存されているので、作業前にこれを空にしてやればよい。</p>

<pre><code>%LOCALAPPDATA%\ActiveState\ActivePerl
</code></pre>

<p>保存しておいた .ppmxファイルは、ppm install でインストールすることが出来るが、依存関係にあるサブモジュールを先にインストールしておかないと、オンラインで取りに行こうとしてしまう。そこで複数の .ppmxファイルから .ppdファイルを取り出し、そこに書かれた依存関係リストを読み出し、優先順位を付けて順次処理することになる。</p>

<pre><code class="language-brush:perl gutter:true title:LocalUpdate.pl">#!C:/Perl/bin/perl.exe
use utf8;  
use strict;  
use Archive::Tar;  
use File::Basename;  
use File::Find;  
use Cwd;

my $cwd = $ARGV[0] // getcwd;  
print "Search directory: ", $cwd, "\n";  
my(@ppmx_list, %ppmx_hash, %ppmx_path);  
find(sub {  
    if (m/\.ppmx$/io) {
        $ppmx_path{$_} = $File::Find::name;
        push @ppmx_list, $_;
    }
}, $cwd);
foreach my $ppmx (@ppmx_list) {  
    $ppmx_hash{$ppmx} = 9999;
}
foreach my $ppmx (@ppmx_list) {  
    my $tar = Archive::Tar-&gt;new();
    $tar-&gt;read($ppmx_path{$ppmx}, 1);
    foreach my $file ($tar-&gt;get_files()) {
        if ($file-&gt;{name} =~ /\.ppd$/io) {
            foreach my $line (split /\n/, $file-&gt;{data}) {
                if (my $require = ($line =~ m/REQUIRE NAME="([^\"]+)"/o)[0]) {
                    $require =~ s/::/-/go;
                    foreach my $hash (keys %ppmx_hash) {
                        $ppmx_hash{$hash}-- if 0 == index lc($hash), lc($require);
                    }
                }
            }
        }
    }
}
undef @ppmx_list;  
while (my($key, $var) = each %ppmx_hash) {  
    push @ppmx_list, sprintf "%04u %s", $var, $key;
}
foreach my $ppmx (sort {lc $a cmp lc $b} @ppmx_list) {  
    $ppmx =~ s/^\d+ //o;
    my $command = "ppm install ".$ppmx_path{$ppmx}." --nodeps";
    print $cwd, "&gt; ", $command, "\n";
    system $command and die "$!";
    print "\n";
}

1;  
__END__  
</code></pre>

<p>このコードは .ppmxファイルを含む親ディレクトリを引数に取る。引数は複数でもよく、省略されればカレントディレクトリを起点にする。-nオプションは ppmコマンドに --nodeps を付加する。-n オプションなしで起動すると依存解決に失敗した場合（依存する .ppmxが見つからない場合）レポジトリからのダウンロードが生じる。</p>

<hr>

<h3 id="ppmx">.ppmxファイルの作成</h3>

<p>.ppmxファイルの実態は .tgzで固められた .ppdファイルと blibディレクトリだ。cpanでソースからビルドしたものを .ppmxファイルにして保存しておくと、再ビルドする手間を減らせる。cpanの作業ワークは <code>C:\Perl64\cpan\build</code> または <code>C:\Perl\cpan\build</code> にある。</p>

<p>例えば既に cpan install に成功した <code>Foo::Bar</code> モジュールに対応する <code>Foo-Bar-0.1010-4UofsT</code> というディレクトリがあったなら、まずそこに cd して <code>dmake ppd</code> を実行<sup id="fnref:1"><a href="http://multix.jp/activeperl-offline-installation/#fn:1" rel="footnote">1</a></sup>する。すると <code>Foo-Bar.ppd</code> というファイルが作成されるだろう。</p>

<pre><code class="language-brush:bash">&gt; cd C:\Perl64\cpan\build\Foo-Bar-0.1010-4UofsT
&gt; dmake ppd
&gt; type Foo-Bar.ppd
&lt;SOFTPKG NAME="Foo-Bar" VERSION="0.1010"&gt;  
    &lt;ABSTRACT&gt;foobar foobar&lt;/ABSTRACT&gt;
    &lt;AUTHOR&gt;Charlie Root &amp;lt;foobar@example.com&amp;gt;&lt;/AUTHOR&gt;
    &lt;IMPLEMENTATION&gt;
        &lt;PERLCORE VERSION="5,006,0,0" /&gt;
        &lt;REQUIRE NAME="Carp::" /&gt;
        &lt;REQUIRE NAME="Cwd::" VERSION="3.16" /&gt;
        &lt;REQUIRE NAME="Exporter::" /&gt;
        &lt;REQUIRE NAME="strict::" /&gt;
        &lt;REQUIRE NAME="vars::" /&gt;
        &lt;ARCHITECTURE NAME="MSWin32-x64-multi-thread-5.24" /&gt;
        &lt;CODEBASE HREF="" /&gt;
    &lt;/IMPLEMENTATION&gt;
&lt;/SOFTPKG&gt;  
</code></pre>

<p>REQUIRE 行は依存をあらわす。ARCHITECTURE 行はインストール先のプラットフォームを限定するものだが、ピュアPerlで書かれた環境非依存モジュールの場合は <code>noarch</code> に書き換えることでインストール先を選ばないようにすることもできる。</p>

<pre><code class="language-brush:xml ">&lt;ARCHITECTURE NAME="noarch" /&gt;  
</code></pre>

<p>.ppmx(.tgz)ファイルを作るための tar+gzアーカイバは、C:\Perl64\bin に <code>ptar.bat</code>としてインストールされている。すでにパスは通っているので次のようにすれば .ppmxファイルができあがる。</p>

<pre><code class="language-brush:bash ">&gt; ptar -v -c -C -z -f foo-bar.ppmx blib foo-bar.ppd
</code></pre>

<p>この際注意が必要なのは、.ppmxと .ppdのファイル名は一致させておかなければならない点だ。.ppmxファイル名にバージョン番号を付加する一般的なルールに従う場合は、.ppdファイル名にもバージョン番号を付加しておく。もちろんそのバージョン番号は SOFTPKG行の VERSIONと一致しているべきだ。</p>

<pre><code class="language-brush:bash ">&gt; ren foo-bar.ppd foo-bar-0.1010.ppd
&gt; ptar -v -c -C -z -f foo-bar-0.1010.ppmx blib foo-bar-0.1010.ppd
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>dmake.exe は cpan初期化時に C:\Perl64\sit\bin にインストールされているはずだ。 <a href="http://multix.jp/activeperl-offline-installation/#fnref:1" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[windowsで名前付きパイプを作る]]></title><description><![CDATA[<p>ActivePerl で Win32::Pipe が正常動作しなかった（AutoLoaderが実行時に異常終了する）ので Win32::API を直接叩いてみた。</p>

<hr>

<p>クライアント側は普通に <em>open/close</em> で読み書きできるので、サーバ側で namedpipe を作る部分を書いてみる。</p>

<pre><code class="language-brush:perl gutter:true title:win32pipeserver.pl">use strict;  
use warnings;  
use Win32::API;  
use Fcntl;  
use constant {  
    PIPE_ACCESS_INBOUND     =&gt; 0x00000001,  # O_RDONLY相当
    PIPE_ACCESS_OUTBOUND    =&gt; 0x00000002,  # O_WRONLY相当
    PIPE_ACCESS_DUPLEX      =&gt; 0x00000003,  # O_RDWR相当

    PIPE_</code></pre>]]></description><link>http://multix.jp/win32-create-namedpipe/</link><guid isPermaLink="false">50f30e5a-4293-4576-84e3-6753b5ef8ec4</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Mon, 20 Feb 2017 09:33:48 GMT</pubDate><content:encoded><![CDATA[<p>ActivePerl で Win32::Pipe が正常動作しなかった（AutoLoaderが実行時に異常終了する）ので Win32::API を直接叩いてみた。</p>

<hr>

<p>クライアント側は普通に <em>open/close</em> で読み書きできるので、サーバ側で namedpipe を作る部分を書いてみる。</p>

<pre><code class="language-brush:perl gutter:true title:win32pipeserver.pl">use strict;  
use warnings;  
use Win32::API;  
use Fcntl;  
use constant {  
    PIPE_ACCESS_INBOUND     =&gt; 0x00000001,  # O_RDONLY相当
    PIPE_ACCESS_OUTBOUND    =&gt; 0x00000002,  # O_WRONLY相当
    PIPE_ACCESS_DUPLEX      =&gt; 0x00000003,  # O_RDWR相当

    PIPE_WAIT               =&gt; 0x00000000,
    FILE_FLAG_OVERLAPPED    =&gt; 0x40000000,
};

*STDOUT-&gt;autoflush;

my $PIPE_NAME = "\\\\.\\pipe\\pipesv";

my $CreateNamedPipe = Win32::API-&gt;new( "kernel32", "CreateNamedPipe", "PNNNNNNP", "N" ) or die;  
my $PIPE = $CreateNamedPipe-&gt;Call(  
    $PIPE_NAME,                #  LPCTSTR lpName,                             // パイプ名
    PIPE_ACCESS_DUPLEX,     #  DWORD dwOpenMode,                           // パイプを開くモード
    PIPE_WAIT,              #  DWORD dwPipeMode,                           // パイプ固有のモード
    255,                    #  DWORD nMaxInstances,                        // インスタンスの最大数
    0,                      #  DWORD nOutBufferSize,                       // 出力バッファのサイズ
    0,                      #  DWORD nInBufferSize,                        // 入力バッファのサイズ
    10_000,                 #  DWORD nDefaultTimeOut,                      // タイムアウト(msec)の間隔
    0,                      #  LPSECURITY_ATTRIBUTES lpSecurityAttributes  // セキュリティ記述子
);
my $CloseHandle = Win32::API-&gt;new( "kernel32", "CloseHandle", "N", "N" ) or die;

my $ReadFile = Win32::API-&gt;new( "kernel32", "ReadFile", "NPNPP", "N" ) or die;  
sub ReadFile {  
    my $hFile = shift;
    my $lpBuffer = "\0" x 512;
    my $nNumberOfBytesToRead = 512;
    my $lpNumberOfBytesRead = "\0" x 8;
    my $result = $ReadFile-&gt;Call(
        $hFile,
        $lpBuffer,
        $nNumberOfBytesToRead,
        $lpNumberOfBytesRead,
        0);
    return undef unless $result;
    my $length = unpack "V", $lpNumberOfBytesRead;
    return undef unless $length;
    return substr $lpBuffer, 0, $length;
}

my $WriteFile = Win32::API-&gt;new( "kernel32", "WriteFile", "NPNPP", "N" ) or die;  
sub WriteFile {  
    my $hFile = shift;
    my $lpBuffer = shift;
    while (my $nNumberOfBytesToWrite = defined $lpBuffer &amp;&amp; length $lpBuffer) {
        my $lpNumberOfBytesWritten = "\0" x 8;
        my $result = $WriteFile-&gt;Call(
            $hFile,
            $lpBuffer,
            $nNumberOfBytesToWrite,
            $lpNumberOfBytesWritten,
            0);
        return undef unless $result;
        my $length = unpack "V", $lpNumberOfBytesWritten;
        $lpBuffer = substr $lpBuffer, $length;
    }
    return undef;
}

my $ConnectNamedPipe = Win32::API-&gt;new( "kernel32", "ConnectNamedPipe", "NP", "N" ) or die;  
my $DisconnectNamedPipe = Win32::API-&gt;new( "kernel32", "DisconnectNamedPipe", "N", "N" ) or die;

while ($ConnectNamedPipe-&gt;Call($PIPE, 0)) {  
    WriteFile($PIPE, "Welcome\n");
    print ReadFile($PIPE);         # client from "Hello"
    $DisconnectNamedPipe-&gt;Call($PIPE);
    #last;      # no loop
}
$CloseHandle-&gt;Call($PIPE);

1;  
__END__  
</code></pre>

<p>本来はセキュリティ記述子を指定すべきだが、本件は動作サンプルなので省略<sup id="fnref:1"><a href="http://multix.jp/win32-create-namedpipe/#fn:1" rel="footnote">1</a></sup>する。バッファサイズについてはダミーなので 0 と書いて良い。
こうして出来た PIPEハンドル（単なるint値）は、PerlIOの感知するところではないため<sup id="fnref:2"><a href="http://multix.jp/win32-create-namedpipe/#fn:2" rel="footnote">2</a></sup> 通常の sysread/syswrite では扱えない。従って CloseHandle/WriteFile/ReadFile についても Win32API で実装する。</p>

<p><code>ConnectNamedPipe()</code> はソケット通信の accept() に相当する関数で、クライアント側が同名パイプを開くまでI/Oブロックする。<code>DisconnectNamedPipe()</code>はこのセッションを閉じてクライアントを切断する。</p>

<p>いっぽう、クライアント側は普通のファイルアクセスとなんら変わるところはない。</p>

<pre><code class="language-brush:perl gutter:true title:win32pipeclient.pl">use strict;  
use warnings;

*STDOUT-&gt;autoflush;

my $PIPE_NAME = "\\\\.\\pipe\\pipesv";

open my $FH, "+&lt;", $PIPE_NAME or die "$!";  
$FH-&gt;binmode(":raw");
print scalar &lt;$FH&gt;;                # server from "Welcome"  
$FH-&gt;write("hello\n");
$FH-&gt;close;

1;  
__END__  
</code></pre>

<p>マルチクライアントサーバを実装する場合、基本的には ConnectNamedPipe のあとで CreateThread すればよいが、CreateNamedPipe に FILE_FLAG_OVERLAPPED の指定と、ConnectNamedPipe に lpOverlapped の指定が必要になる。<sup id="fnref:3"><a href="http://multix.jp/win32-create-namedpipe/#fn:3" rel="footnote">3</a></sup></p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>従って誰でもどこからでも無制限に読み書きできてしまう。その場合何が起こり得るかについては<a href="http://eternalwindows.jp/windevelop/service/service06.html">こちら</a>が詳しい。 <a href="http://multix.jp/win32-create-namedpipe/#fnref:1" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:2"><p>open($DUP, "&lt;&amp;=", $fd) といった構文では認識できない。 <a href="http://multix.jp/win32-create-namedpipe/#fnref:2" title="return to article">↩</a></p></li>
<li class="footnote" id="fn:3"><p><a href="https://msdn.microsoft.com/ja-jp/library/cc429611.aspx">https://msdn.microsoft.com/ja-jp/library/cc429611.aspx</a> <a href="http://multix.jp/win32-create-namedpipe/#fnref:3" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[ヘッドレス用memtest86+の作成]]></title><description><![CDATA[<p>memtest86+はふつうは公式サイト<sup id="fnref:1"><a href="http://multix.jp/buildup-memtest-headless/#fn:1" rel="footnote">1</a></sup>等から出来合いのバイナリを入手してくれば充分だが、通常は VGA出力必須のためシリアルコンソールでのヘッドレス環境では実行できない。しかしソースコードには必要最小限ではあるがシリアルコンソール対応コードが含まれているため、ヘッダを修正して makeしなおせばヘッドレス環境で使えるmemtestバイナリを手に入れることができる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">注意点</h4>

<p>memtestのシリアルコンソール対応は、既に充分にはメンテナンスされていないのか若干の不具合がある。例えば configurationメニューから抜けたあと画面が乱れるので起動後のテストオプションが事実上変更できないとか、ESCキーで終了したとき rebootではなく system haltで止まってしまうなどだ。とは言え VGAデバイスを持たない機材のメモリ健全性テストができるだけで充分な利用価値があったりする。</p>

<p>なお memtestバイナリは 32bit実行ファイルとなるので、64bit環境で buildするには <code>/usr/include/gnu/stubs-32.h</code>が要求される。</p>

<hr>

<h4 id="centos">CentOSの場合</h4>

<p>まず必要なパッケージをインストールする。基本的な gccやライブラリの類は <code>yum groupinstall "Development Tools"</code>で揃う。また memtestを makeするために 32bitバージョンの glibc-develもインストールする。</p>

<pre><code class="language-brush:bash">sudo yum groupinstall -y "Development</code></pre>]]></description><link>http://multix.jp/buildup-memtest-headless/</link><guid isPermaLink="false">dadf31ad-7b3a-4598-85e2-3dab8a594530</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Thu, 02 Jul 2015 05:54:24 GMT</pubDate><content:encoded><![CDATA[<p>memtest86+はふつうは公式サイト<sup id="fnref:1"><a href="http://multix.jp/buildup-memtest-headless/#fn:1" rel="footnote">1</a></sup>等から出来合いのバイナリを入手してくれば充分だが、通常は VGA出力必須のためシリアルコンソールでのヘッドレス環境では実行できない。しかしソースコードには必要最小限ではあるがシリアルコンソール対応コードが含まれているため、ヘッダを修正して makeしなおせばヘッドレス環境で使えるmemtestバイナリを手に入れることができる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">注意点</h4>

<p>memtestのシリアルコンソール対応は、既に充分にはメンテナンスされていないのか若干の不具合がある。例えば configurationメニューから抜けたあと画面が乱れるので起動後のテストオプションが事実上変更できないとか、ESCキーで終了したとき rebootではなく system haltで止まってしまうなどだ。とは言え VGAデバイスを持たない機材のメモリ健全性テストができるだけで充分な利用価値があったりする。</p>

<p>なお memtestバイナリは 32bit実行ファイルとなるので、64bit環境で buildするには <code>/usr/include/gnu/stubs-32.h</code>が要求される。</p>

<hr>

<h4 id="centos">CentOSの場合</h4>

<p>まず必要なパッケージをインストールする。基本的な gccやライブラリの類は <code>yum groupinstall "Development Tools"</code>で揃う。また memtestを makeするために 32bitバージョンの glibc-develもインストールする。</p>

<pre><code class="language-brush:bash">sudo yum groupinstall -y "Development Tools"

# 64bit環境の場合
sudo yum install -y glibc-devel-2.17-78.el7.i686  
</code></pre>

<p>memtestの SRPMをダウンロードし、rpmコマンドで展開する。展開結果は <code>~/rpmbuild</code>ディレクトリ以下に現れる。</p>

<pre><code class="language-brush:bash">rpm -iv ftp://ftp.pbone.net/mirror/vault.centos.org/7.1.1503/os/Source/SPackages/memtest86+-4.20-14.el7.src.rpm  
</code></pre>

<p>シリアルポートの設定は、memtestの condfig.hで行うので、これに対する patchファイルを用意する。<strong>SERIAL_CONSOLE_DEFAULT を1にするだけ</strong>だが、これによって起動オプションとして <code>console=CONFIGURATION</code>が記述できるようになる。</p>

<pre><code class="language-brush:plain gutter:true highlight:[8] title:~/rpmbuild/SOURCES/memtest86+-4.20-serial.patch">--- memtest86+-4.20/config.h        2011-01-24 03:11:04.000000000 +0900
+++ memtest86+-4.20/config.h.serial 2015-07-03 13:15:26.716441211 +0900
@@ -13,7 +13,7 @@
 /* SERIAL_CONSOLE_DEFAULT -  The default state of the serial console. */
 /*     This is normally off since it slows down testing.  Change to a 1 */
 /*     to enable. */
-#define SERIAL_CONSOLE_DEFAULT 0
+#define SERIAL_CONSOLE_DEFAULT 1

 /* SERIAL_TTY - The default serial port to use. 0=ttyS0, 1=ttyS1 */ 
 #define SERIAL_TTY 0
</code></pre>

<p>次いで specファイルを修正する。元の memtest86+.specをコピーして、これらを適切な場所に追加する。</p>

<pre><code class="language-brush:bash">cp ~/rpmbuild/SPECS/memtest86+.spec ~/rpmbuild/SPECS/memtest86+-serial.spec  
</code></pre>

<pre><code class="language-brush:bash title:~/rpmbuild/SPECS/memtest86+-serial.spec fix"># Patch1指示を Patch0指示行の直後に追加
Patch1:   memtest86+-4.20-serial.patch

# %patch1コマンドを %patch0コマンド行の直後に追加
%patch1 -p1 -b .serial
</code></pre>

<p>あとは <code>rpmbuild</code>コマンドを叩けば、RPMパッケージが作成される。これをインストールすると /bootに展開されるが、LiveBootの場合必要なのは memtestバイナリ本体だけなので（LiveBoot環境で buildした場合は）出来上がった memtest.binを <code>/run/initramfs/live/syslinux/mt86plus</code>にコピーして、syslinux.cfgを編集すれば事足りる。<sup id="fnref:2"><a href="http://multix.jp/buildup-memtest-headless/#fn:2" rel="footnote">2</a></sup></p>

<pre><code class="language-brush:bash highlight:[4,7]">rpmbuild -bb rpmbuild/SPECS/memtest86+-serial.spec

ls -l ~/rpmbuild/RPMS/x86_64/memtest86+-4.20-14.el7.centos.x86_64.rpm  
-rw-rw-r-- 1 liveuser liveuser 80616 Jul  2 18:31 rpmbuild/RPMS/x86_64/memtest86+-4.20-14.el7.centos.x86_64.rpm

ls -l ~/rpmbuild/BUILD/memtest86+-4.20/memtest.bin  
-rwxr-xr-x 1 liveuser liveuser 176500 Jul  2 18:31 rpmbuild/BUILD/memtest86+-4.20/memtest.bin

cp ~/rpmbuild/BUILD/memtest86+-4.20/memtest.bin /run/initramfs/live/syslinux/mt86plus  
</code></pre>

<pre><code class="language-brush:plain title:/run/initramfs/live/syslinux/syslinux.cfg fix"># Troubleshootingメニューに追加
label memtest  
  menu label Test ^memory
  kernel mt86plus
  append console=ttyS0,115200n8
</code></pre>

<p>memtest.binは FDDブートイメージなので、FDDへはそのまま ddするだけでも使える。ただしそれでは今回のようなシリアルコンソール設定オプションを渡すすべがないので、syslinux等の汎用ブートローダーを利用して起動するほうが一般的だろう。</p>

<hr>

<h4 id="ubuntu">Ubuntuの場合</h4>

<p>まず build環境を整える。<code>apt-get build-dep PACKAGE</code>で指定パッケージを buildするのに必要なツール類がまとめてインストールされる。</p>

<pre><code class="language-brush:bash">sudo apt-get update  
sudo apt-get install devscripts  
sudo apt-get build-dep memtest86+  
apt-get source memtest86+  
</code></pre>

<p>memtestのシリアルポート設定は CentOSの場合と同様だが（memtest86+-4.20/debian/rulesを修正するのも面倒なので）config.hは直接修正する。その後はソースディレクトリの中で <code>debuild -us -uc</code>を実行すれば memtest.binバイナリと、親ディレクトリに .debファイルが作成される。</p>

<pre><code class="language-brush:bash highlight:[8]">cd memtest86+-4.20/

vi config.h

debuild -us -uc

ls -l memtest.bin  
-rwxr-xr-x 1 ubuntu ubuntu 164216 Jul  3 02:57 memtest.bin
</code></pre>

<p>memtest.binは LiveBootフラッシュドライブの mt86plusと差し替えて使用する。</p>

<pre><code class="language-brush:bash highlight:[2]">ls -l /cdrom/install/mt86plus  
-rwxr-xr-x 1 root root 150024 Jun 22 16:11 /cdrom/install/mt86plus

cp memtest.bin /cdrom/install/mt86plus  
</code></pre>

<pre><code class="language-brush:plain title:isolinux/txt.cfg fix">label memtest  
  menu label Test ^memory
  kernel /install/mt86plus
  append console=ttyS0,115200n8
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p><a href="http://www.memtest.org">http://www.memtest.org</a> <a href="http://multix.jp/buildup-memtest-headless/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>syslinuxのバージョンにもよるが kernel/appendコマンドに渡すファイル名に拡張子が付いていると正常認識されないことがあるので、CentOSでは慣習的に8文字以下の拡張子なしファイル名にすることが多い。 <a href="http://multix.jp/buildup-memtest-headless/#fnref:2" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[LinuxヘッドレスLiveBoot（CentOS編）]]></title><description><![CDATA[<p>本稿は<a href="http://multix.jp/headless-liveboot-ubuntu/">前稿</a>の CentOS版だ。要件定義はおよそ同じだが、LiveBootの実現方法の違いにより、構築手順は大きく異なる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する CentOSはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされているものとする。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-centos/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物は ISOイメージとし、CD-R/DVD-Rに焼いて USB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成の USBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>yum一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>liveuser</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的な</li></ol>]]></description><link>http://multix.jp/headless-liveboot-centos/</link><guid isPermaLink="false">cdc0b430-1ee4-4d3b-b6b8-e9407ad9c019</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[Linux]]></category><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Wed, 24 Jun 2015 02:31:27 GMT</pubDate><content:encoded><![CDATA[<p>本稿は<a href="http://multix.jp/headless-liveboot-ubuntu/">前稿</a>の CentOS版だ。要件定義はおよそ同じだが、LiveBootの実現方法の違いにより、構築手順は大きく異なる。</p>

<ul class="index"></ul>

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する CentOSはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされているものとする。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-centos/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物は ISOイメージとし、CD-R/DVD-Rに焼いて USB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成の USBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>yum一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>liveuser</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的な LiveBootの流儀）  </li>
<li>既定のネットワーク設定・接続はオフとする。  </li>
<li>これらのパスワードやネットワーク設定は persistent有効時には自由に設定変更して永続化することができ、再起動しても設定内容が失われてはならない。</li>
</ol>

<p>以後は CentOS-7-x86_64-LiveCD-1503.iso をベースにして話を進める。32bit版や CentOS-6でも同様の手順で構築できる。<sup id="fnref:2"><a href="http://multix.jp/headless-liveboot-centos/#fn:2" rel="footnote">2</a></sup></p>

<p>Ubuntuでは最終的な rootfsを作るのにオンメモリ（2GiB程度）で作業できたが、CentOS用の rootfsを作成するには作業領域が足りないため、起動用 USBメモリの他に Ext4でフォーマットできるフラッシュドライブや HDDを別途用意しなければならない。なお最終的に出来上がるブータブルイメージは CD-Rに充分格納できるサイズ（640MiB以下）になる。</p>

<p>なお製作時のリファレンス機材には Riava RS670A を使用した。<sup id="fnref:3"><a href="http://multix.jp/headless-liveboot-centos/#fn:3" rel="footnote">3</a></sup></p>

<hr>

<h4 id="usb">シリアルコンソール対応のインストーラUSBメモリを作る</h4>

<p>まずヘッドレス環境で使用できる CentOSインストーラを LiveCD ISOイメージから作成する。Windows上でこれを行うには Fedora LiveUSB Creator<sup id="fnref:4"><a href="http://multix.jp/headless-liveboot-centos/#fn:4" rel="footnote">4</a></sup>を使用する。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-24-11-56-53.png" alt=""></p>

<p>次に起動メニューを書き換える。LiveUSB Creatorが作成したベースファイルからの相違点はシリアルコンソール設定の追加が主だ。</p>

<pre><code class="language-brush:plain gutter:true title:/syslinux/syslinux.cfg （フラッシュドライブ起動用）">#serial 0 115200
#console 0

default vesamenu.c32  
timeout 100  
menu background  
menu autoboot Starting CentOS Linux 7 in # second{,s}. Press any key to interrupt.

menu clear  
menu title CentOS Linux 7  
menu vshift 4  
menu rows 12  
menu margin 8  
menu helpmsgrow 15  
menu tabmsgrow 13

menu tabmsg Press Tab for full configuration options on menu items.  
menu separator  
menu separator  
label linux0  
  menu label ^Start CentOS Linux 7 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=LIVE rootfstype=vfat ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 console=tty0 console=ttyS0,115200n8
  menu default
menu separator  
menu begin ^Troubleshooting  
  menu title Troubleshooting
label check0  
  menu label ^Test this media &amp; start CentOS Linux 7 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=LIVE rootfstype=vfat ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 rd.live.check console=tty0 console=ttyS0,115200n8
menu separator  
label local  
  menu label Boot from ^local drive
  localboot 0xffff
menu separator  
label returntomain  
  menu label Return to ^main menu.
  menu exit
menu end  
</code></pre>

<p>このフラッシュドライブで、Troubleshootingサブメニュー内の <strong>Test this media &amp; start CentOS Linux 7 Live</strong> を選んで起動しよう。しばらく待っているとログインプロンプトが出現する。ここからは <code>root[ENTER]</code>で rootシェルに入ることが出来る。</p>

<hr>

<h4 id="rootfs">rootfsを作成する</h4>

<p>この時点で NetworkManager は起動しているので DHCPでのIP取得は既に済んでいるだろう。作業に必要なパッケージを yumで追加する。</p>

<pre><code class="language-brush:bash">LANG=C  
yum install -y squashfs-tools  
</code></pre>

<p>作業用の追加ドライブを接続してパーテションをフォーマットし、<code>/mnt</code>にマウントしてそこに移動する。ここでは <code>/dev/sdb1</code>とする。</p>

<pre><code class="language-brush:bash">mkfs.ext4 /dev/sdb1  
mount /dev/sdb1 /mnt  
cd /mnt  
</code></pre>

<p>起動時のフラッシュドライブは <code>/run/initramfs/live</code>に ReadOnlyでマウントされているので、これを rwオプションでリマウントして書き換え可能にする。</p>

<pre><code class="language-brush:bash">mount -o rw,remount /run/initramfs/live  
cd /run/initramfs/live  
</code></pre>

<p>現在起動している rootfsの実体は <code>/run/initramfs/live/LiveOS/squashfs.img</code>だ。このファイルを差し替えるのが最終目的だが、これは <code>LiveOS/ext3fs.img</code>という loopbackファイルを squashfsで圧縮した構造になっている。そこでこれを以下のようにして再現する。rootfs容量はここでは8GiBとするが、適宜任意のサイズに変えても良い。</p>

<pre><code class="language-brush:bash">mkdir -p squashfs-root/LiveOS  
truncate -s $((2**33)) squashfs-root/LiveOS/ext3fs.img  
mkfs.ext4 -L _CentOS-7-livecd squashfs-root/LiveOS/ext3fs.img  
</code></pre>

<p>このファイルを loopbackマウントして /mnt/chrootでアクセスできるようにする。</p>

<pre><code class="language-brush:bash">mkdir /mnt/chroot  
mount -o loop squashfs-root/LiveOS/ext3fs.img /mnt/chroot/  
</code></pre>

<p>後は yumのグループインストールコマンド一発で、任意の新しい rootfs環境が構築できる。</p>

<pre><code class="language-brush:bash">yum --installroot=/mnt/chroot --releasever=7 -y groupinstall Core  
</code></pre>

<p>releaseverオプションに指定する CentOSのバージョンは、<code>/etc/os-release</code>ファイルを参照すると良い。</p>

<hr>

<h4 id="rootfs">rootfsの環境設定</h4>

<p>まず各システム領域を chroot内に bindマウントする。そしてダミーの <code>/etc/fstab</code>（中身は空でも良い）を作成しておく。</p>

<pre><code class="language-brush:bash">mount -o bind /dev/ chroot/dev/  
mount -o bind /proc/ chroot/proc/  
mount -o bind /tmp/ chroot/tmp/  
mount -o bind /run/ chroot/run/  
touch chroot/etc/fstab  
</code></pre>

<p>initramfsの構築設定ファイルを追加する。これは LiveBoot可能な initramfsを作成するのに必須の設定だ。</p>

<pre><code class="language-brush:plain title:chroot/etc/dracut.conf.d/01-liveos.conf">hostonly="no"  
add_dracutmodules+="dmsquash-live"  
compress="xz"  
</code></pre>

<p>現在起動している LiveCDシステムの、livesysサービスファイルを chroot内にコピーしてサービス登録する。このサービスは初回起動時に <em>liveuser</em>アカウントの作成や rootパスワードの削除処理を行うようになっている。<sup id="fnref:5"><a href="http://multix.jp/headless-liveboot-centos/#fn:5" rel="footnote">5</a></sup></p>

<pre><code class="language-brush:bash">cp -p /etc/init.d/livesys* chroot/etc/init.d/  
chroot chroot/ chkconfig --add livesys  
chroot chroot/ chkconfig --add livesys-late  
</code></pre>

<p>タイムゾーンとロケール情報を設定し、NetworkManagerが初回起動時は起動しないように修正する。</p>

<pre><code class="language-brush:bash">ln -sf ../usr/share/zoneinfo/Asia/Tokyo chroot/etc/localtime

echo "LANG=en_US.utf8" &gt; chroot/etc/locale.conf

chroot chroot /usr/bin/systemctl disable NetworkManager  
</code></pre>

<hr>

<h4 id="linuxkernel">Linux kernelモジュールの組み込み</h4>

<p>kernelと、その他の追加パッケージをインストールする。</p>

<pre><code class="language-brush:bash">yum --installroot=/mnt/chroot -y install kernel squashfs-tools dump isomd5sum epel-release  
</code></pre>

<p>kernelと作成された initramfsを、フラッシュドライブに移動する。rescue関係のファイルは必要ないので削除する。</p>

<pre><code class="language-brush:bash">rm -f chroot/boot/*rescue*  
mv chroot/boot/vmlinuz-* /run/initramfs/live/syslinux/vmlinuz0.new  
mv chroot/boot/initramfs-* /run/initramfs/live/syslinux/initrd0.img.new  
</code></pre>

<p>最後にクリーンアップを行ってアンマウントする。rootfsの umount後は fsckで損傷がないかチェックする。</p>

<pre><code class="language-brush:bash highlight:[9]">yum --installroot=/mnt/chroot clean all  
find chroot/var/log -type f -exec /bin/truncate -s0 {} \;  
umount chroot/proc/  
umount chroot/dev/  
umount chroot/run/  
umount chroot/tmp/

du -sh chroot/  
720M    chroot/

umount chroot/  
fsck.ext4 squashfs-root/LiveOS/ext3fs.img  
</code></pre>

<hr>

<h4 id="squashfsimg">squashfs.img ファイルの作成</h4>

<p>こうして出来た rootfsを mksquashfsで圧縮すれば、LiveBoot可能な新しいシステムファイルになる。ただし Ext4フォーマットした loopbackファイルをそのまま圧縮するため、インストール過程で生じた削除ファイルの痕跡などもそのまま残っていて圧縮率が低下してしまう。</p>

<pre><code class="language-brush:bash highlight:[4]">mksquashfs squashfs-root squashfs.img.new

ls -lh squashfs.img.new  
-rw-r--r--. 1 root root 474M Jun 24 08:21 squashfs.img.new
</code></pre>

<p>これを可能な限り取り除いて圧縮するには、dump/restoreコマンドを経由してファイルシステムを完全に作り直すとよい。ただし dumpコマンドはブロックデバイスしか扱えないので、losetupコマンドを用いて /dev/loopX に loopbackファイルをバインドして指定する。手順は面倒だが、比較するとその効果は絶大だ。</p>

<pre><code class="language-brush:bash highlight:[11,26,27]">mv squashfs.img.new squashfs.img.nev

yum install -y dump

loopdev=$(losetup -f)  
losetup $loopdev squashfs-root/LiveOS/ext3fs.img  
dump -0z9 -f rootdump $loopdev  
losetup -d $loopdev

ls -lh rootdump  
-rw-r--r--. 1 root root 277M Jun 24 08:30 rootdump

rm -f squashfs-root/LiveOS/ext3fs.img  
truncate -s $((2**33)) squashfs-root/LiveOS/ext3fs.img  
mkfs.ext4 -L _CentOS-7-livecd squashfs-root/LiveOS/ext3fs.img

mount -o loop squashfs-root/LiveOS/ext3fs.img chroot  
(cd chroot/; restore -r -f ../rootdump)
rm -fr chroot/lost+found restoresymtable  
umount chroot

fsck.ext4 squashfs-root/LiveOS/ext3fs.img  
mksquashfs squashfs-root squashfs.img.new

ls -lh squashfs.img.ne?  
-rw-r--r--. 1 root root 474M Jun 24 08:21 squashfs.img.nev
-rw-r--r--. 1 root root 250M Jun 24 08:38 squashfs.img.new

cp squashfs.img.new /run/initramfs/live/LiveOS/  
</code></pre>

<hr>

<h4 id="rootfs">新 rootfsでの起動テスト</h4>

<p>こうして作成した rootfsファイルをブートUSBメモリにコピーして shutdownし、Windows（あるいは他のLinux機）で以下の起動用ファイルを差し替える。</p>

<ul>
<li>vmlinuz0.new を syslinux/vmlinuz0 へ</li>
<li>initrd0.img.new を syslinux/initrd0.img へ</li>
<li>squashfs.img.new を LiveOS/squashfs.img へ</li>
</ul>

<p>syslinux/syslinux.cfg は前述のそのままでよいが、今度の起動は通常の <strong>Start CentOS Linux</strong>ラベルを選択する。ログイン・プロンプトは <em>root</em>で直接、あるいは <em>liveuser</em>（いずれもパスワード無し）でシェルに入れる。<em>liveuser</em>アカウントは sudoで <em>root</em>アカウントになれる。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-24-16-47-35.png" alt=""></p>

<p>NetworkManagerは停止状態なので、ネットワークに接続するには dhclientで Link Upするか、nmcli/nmtuiでインタフェース設定を行って NetworkManagerを起動する。</p>

<pre><code class="language-brush:bash">sudo -s

systemctl enable NetworkManager  
systemctl start NetworkManager

nmcli connection modify enp2s0 ipv4.method manual ipv4.addresses 192.168.240.101/24 ipv4.gateway 192.168.240.1 ipv4.dns 8.8.8.8

systemctl restart network.service

# cat /etc/sysconfig/network-scripts/ifcfg-enp2s0
</code></pre>

<hr>

<h4 id="iso">ISOイメージを作成する</h4>

<p>元の ISOの squashfs.img等を差し替えた、新しいISOイメージを作成する。まず作業に必要なパッケージをインストールし、作業領域に ISO内のファイルを展開して ReadOnlyを外す。</p>

<pre><code class="language-brush:bash">mount /dev/sdb1 /mnt  
cd /mnt

# install mkisofs tools
yum install -y mkisofs isomd5sum

# mount image: CentOS-7-x86_64-LiveCD-1503.iso
mkdir loop  
mount /dev/cdrom loop

# copy iso files and modify attributes
cp -rf loop files  
chmod -R +w files/  
umount loop  
</code></pre>

<p>フラッシュドライブから先に作成した新しいシステムファイルをコピーする。このときフラッシュドライブ上では <code>/syslinux</code>に vmlinuz0と initrd0.img ファイルが置かれているが、ISOイメージでは <code>/isolinux</code>ディレクトリになる。<sup id="fnref:6"><a href="http://multix.jp/headless-liveboot-centos/#fn:6" rel="footnote">6</a></sup></p>

<pre><code class="language-brush:bash"># copy kernel and filesystem
cp -f /run/initramfs/live/syslinux/vmlinuz0 files/isolinux/  
cp -f /run/initramfs/live/syslinux/initrd0.img files/isolinux/  
cp -f /run/initramfs/live/LiveOS/squashfs.img files/LiveOS/  
</code></pre>

<p>files/isolinux/isolinux.cfg をシリアルコンソール対応に修正する。最初のシリアルポート設定は BIOSコンソールリダイレクトの有無による。また先に作成した rootfsは SELinuxをサポートするように構築してはいないので、これを無効にする。<code>isolinux/mt86plus</code>については標準では存在しないが、これについては<a href="http://multix.jp/buildup-memtest-headless/">別稿を参照</a>されたい。</p>

<pre><code class="language-brush:plain gutter:true title:isolinux/isolinux.cfg （ODD起動用）">#serial 0 115200
#console 0

default vesamenu.c32  
timeout 100  
menu background  
menu autoboot Starting CentOS Linux 7 in # second{,s}. Press any key to interrupt.

menu clear  
menu title CentOS Linux 7  
menu vshift 4  
menu rows 12  
menu margin 8  
#menu hidden
menu helpmsgrow 15  
menu tabmsgrow 13

menu tabmsg Press Tab for full configuration options on menu items.  
menu separator  
menu separator  
label linux0  
  menu label ^Start CentOS Linux 7 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=CentOS7-liveboot rootfstype=auto ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 selinux=0 console=tty0 console=ttyS0,115200n8
  menu default
menu separator  
menu begin ^Troubleshooting  
  menu title Troubleshooting
label check0  
  menu label ^Test this media &amp; start CentOS Linux 7 1503 Live
  kernel vmlinuz0
  append initrd=initrd0.img root=live:CDLABEL=CentOS7-liveboot rootfstype=auto ro rd.live.image quiet rd.luks=0 rd.md=0 rd.dm=0 rd.live.check selinux=0 console=tty0 console=ttyS0,115200n8 
label memtest  
  menu label Test ^memory
  kernel mt86plus
  append console=ttyS0,115200n8
menu separator  
label local  
  menu label Boot from ^local drive
  localboot 0xffff
menu separator  
label returntomain  
  menu label Return to ^main menu.
  menu exit
</code></pre>

<p>mkisofsコマンドで ISOイメージを作成する。ここではもっともシンプルなオプション構成として GRUB（gfxboot）や EFI対応、Macintosh対応は行ってはいない。注意が必要なのは -Vオプションに渡すCDラベルで、これは kernel起動オプションに記述したのと同じものでなければならない。またこのラベルは最大16文字なのでこれを超えないようにしよう。</p>

<pre><code class="language-brush:bash">find ./files -print | xargs touch -m {} \;  
LANG=C mkisofs -r -J -l \  
  -V CentOS7-liveboot \
  -cache-inodes \
  -b isolinux/isolinux.bin \n
  -c isolinux/boot.cat \
  -no-emul-boot \
  -boot-load-size 4 \
  -boot-info-table \
  -o CentOS7-liveboot.iso \
  ./files
</code></pre>

<p>こうして出来た ISOイメージに、それ自体の改竄検出 MD5チェックサムを implantisomd5コマンドで埋め込む。Ubuntuではファイル単位で MD5をチェックしていたが、CentOSではメディアそのものをチェックする。従ってフラッシュドライブではこの仕組は動かない。一方で CD-R等に焼いた際に Write Errorが有っても、メディアの完全性を確認することが出来る。<sup id="fnref:7"><a href="http://multix.jp/headless-liveboot-centos/#fn:7" rel="footnote">7</a></sup></p>

<pre><code class="language-brush:bash">LANG=C implantisomd5 CentOS7-liveboot.iso  
</code></pre>

<p>改竄チェックは checkisomd5コマンドで行える。引数には調べたい ISOファイルか、メディアを挿れた ODDのデバイスファイルを指定すればよい。なお implantisomd5と checkisomd5コマンドは、isomd5sumパッケージに含まれている。<sup id="fnref:8"><a href="http://multix.jp/headless-liveboot-centos/#fn:8" rel="footnote">8</a></sup></p>

<pre><code class="language-brush:bash">checkisomd5 CentOS7-liveboot.iso  
checkisomd5 /dev/cdrom  
</code></pre>

<hr>

<h4 id="cdrwdvdrwiso">CD-RW/DVD-RWに ISOイメージを焼く</h4>

<p>光学メディアへのライティングは、Ubuntuとおなじく wodimコマンドで行える。</p>

<pre><code class="language-brush:bash">yum install -y wodim

# erase CD/DVD-RW media
wodim dev=/dev/sr0 blank=fast

# writing image
wodim -sao -eject dev=/dev/sr0 CentOS7-liveboot.iso

# check image
checkisomd5 /dev/sr0  
</code></pre>

<p>こうして作成した光学メディアで ODDブートが正常に行えたら、<em>Test this media &amp; start CentOS</em>ラベルで起動してみよう。検査が正常に終了するとログイン・プロンプトに進み、失敗した場合は System Haltするはずだ。<sup id="fnref:9"><a href="http://multix.jp/headless-liveboot-centos/#fn:9" rel="footnote">9</a></sup></p>

<hr>

<h4 id="persistent">persistentモードを設定する</h4>

<p>前述の ISOイメージ/光学メディアで ODDブートが正常に行えたなら、次はその ISOイメージを Live USB Creatorでもって、今度は persistent領域を設定したフラッシュドライブを作成しよう。</p>

<p>persistentモードは kernelオプションの <code>rd.live.overlay=LABEL=LIVE</code>によって有効になる。initramfsは指定された LABELを blkidコマンドで探し、これを <code>/run/initramfs/live/</code>にマウントする。persistent領域ファイルは <code>LiveOS</code>ディレクトリ内の <code>overlay-${LABEL}-${UUID}</code>というファイル名で探して、Device Mapperによって snapshotとして <code>/</code>に結合される。これ自体は初期状態では（ファイルシステムではないので）単なる Zero Fillファイルである。</p>

<pre><code class="language-brush:bash highlight:[2,5,8,9,10,11]">blkid -L LIVE  
/dev/sda1

blkid /dev/sda1  
/dev/sda1: LABEL="LIVE" UUID="0621-6D9B" TYPE="vfat" 

ls -l /run/initramfs/live/LiveOS/  
total 1299100  
-rwxr-xr-x 1 root root      20480 Jun 26 05:26 osmin.img
-rwxr-xr-x 1 root root 1068498944 Jun 30 12:32 overlay-LIVE-0621-6D9B
-rwxr-xr-x 1 root root  261758976 Jun 30 08:26 squashfs.img
</code></pre>

<p>Ubuntuと異なり、Device Mapperを使用しているためもあって実際にあとどの程度データが書き込めるか（保存できるか）は dfコマンドでは判断することができない。dmsetupコマンドの statusサブコマンドで、live-rwボリュームの snapshot消費ブロック数（512byte単位）を知ることはできるから、これを利用するしかない。</p>

<pre><code class="language-brush:bash highlight:[2,5]">dmsetup status live-rw  
0 16777216 snapshot 14960/2086912 72

echo | awk '{print 14960/2086912}'  
0.00716849  
</code></pre>

<p>また Ubuntuの場合は、persistentファイルは overlayfsで重ねられた独立したファイルシステムであったから、非起動状態のフラッシュドライブから persistent領域内のファイルを抜き出すことは容易だったが、CentOSではこれは容易なことではない。</p>

<p>（現在起動しているのとは別の）フラッシュドライブ内の persistent領域にアクセスするには、以下のようにする。</p>

<pre><code class="language-brush:bash highlight:[15,23,28]">## 作業領域の準備

cd /mnt  
mkdir media squash rootfs

## フラッシュドライブを作業領域にマウントし、その中の squashfs.imgをマウントする

mount /dev/sdc1 media  
mount -o loop media/LiveOS/squashfs.img squash

## ext3fs.img（readonly）と overlayファイルに loopbackデバイスを割り当てる
## 空きデバイス名は losetup -fで得られる

losetup -f  
/dev/loop6

losetup /dev/loop6 squash/LiveOS/ext3fs.img -r  
losetup /dev/loop7 edia/LiveOS/overlay-LIVE-0621-6D9B

## ふたつのデバイスを束ねて rootfsを再生する

blockdev -q --getsz /dev/loop6  
16777216

dmsetup create live-rootfs --table "0 16777216 snapshot /dev/loop6 /dev/loop7 P 8"

dmsetup status live-rootfs  
0 16777216 snapshot 17312/2086912 80

## あとはfsckしたり・・・

fsck -f -y /dev/mapper/live-rootfs

## dumpしたり・・・
## （squashfs.img作成以後分だけを抜き出すのに /etc/dumpdatesを利用する例）

echo "/dev/mapper/live-rootfs 0 $(LANG=C \  
  date +"%a %b %d %X %z" \
    -r media/syslinux/ldlinux.sys)" &gt;&gt; /etc/dumpdates
dump -u1z9 /dev/mapper/live-rootfs -f rootfsdump

## mountしたり・・・

mount /dev/mapper/live-rootfs rootfs  
ls -a rootfs/home/liveuser

## 作業を終えたら後始末してフラッシュドライブを切り離す

umount rootfs  
dmsetup remove live-rootfs  
losetup -d /dev/loop6  
losetup -d /dev/loop7  
umount squash  
umount media  
rmdir media squash rootfs  
</code></pre>

<hr>

<h4 id="homeswap">/homeや swapパーテションを設定する</h4>

<p><code>/etc/init.d/livesys</code>を覗いてみると解る<sup id="fnref:10"><a href="http://multix.jp/headless-liveboot-centos/#fn:10" rel="footnote">10</a></sup>が、フラッシュドライブの <code>/LiveOS</code>ディレクトリに、<code>home.img</code>ファイルがあれば <code>/home</code>に、swap.imgファイルが有れば <code>swap</code>に、それぞれ自動的にマウントするようになっている。これらのボリュームファイルは以下のようにセットアップする。</p>

<pre><code class="language-brush:bash highlight:[22,23,24,25,26,27,28,32,33,34]">## persistentオフの場合はフラッシュドライブを rwリマウントする 
mount -o rw,remount /run/initramfs/live

## 保存領域へcd
cd /run/initramfs/live/LiveOS

## home.img ボリュームの作成（サイズは適宜）
truncate -s $((2**31)) home.img  
mkfs.ext4 -L "home-vol" home.img

## swap.img ボリュームの作成（サイズは適宜）
truncate -s $((2**31)) swap.img  
mkswap -L "swap-vol" swap.img

## あとは特に設定変更もなく再起動するだけ
sync;sync;sync  
reboot

## homeボリュームの有無は losetupで確認できる
## 容量は dfコマンドで把握可能
losetup  
NAME       SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE  
/dev/loop0         0      0         0  1 /osmin.img (deleted)
/dev/loop1         0      0         0  1 /osmin
/dev/loop2         0      0         0  1 /run/initramfs/live/LiveOS/squashfs.img
/dev/loop3         0      0         0  1 /LiveOS/ext3fs.img
/dev/loop4         0      0         0  0 /LiveOS/overlay-LIVE-748B-70C6
/dev/loop5         0      0         0  0 /run/initramfs/live/LiveOS/home.img

## swapは freeコマンドで容量が確認できる
free  
              total        used        free      shared  buff/cache   available
Mem:        8162140      178452     7610852       16720      372836     7767880  
Swap:        262136           0      262136
</code></pre>

<hr>

<h4 id="">ダウンロード</h4>

<p><a href="https://secure.multix.jp/download/SerialLive/CentOS7-liveboot-20150703.iso">CentOS7-liveboot-20150703.iso</a> 313MiB (<a href="https://secure.multix.jp/download/SerialLive/CentOS7-liveboot-20150703.iso.md5">md5sum</a>)</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>USBシリアル変換は FTDI系を前提にしている。PL2303系（特にノーブランドのコピー品）は baud=115.2kではFIFOバッファが足りずに通信不具合が多発してトラブルのもとになる。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p><a href="https://www.centos.org">centos.org</a> <a href="http://multix.jp/headless-liveboot-centos/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p><a href="http://store.shopping.yahoo.co.jp/riava/rs670-s16a.html">Riava RS670A</a> 基本スペックは Intel Gigabit NICx6を持つ Intel/Atom C2758 でメモリ8GiB。基本OSは CentOSまたはUbuntu。SSD内蔵だがこれはここでは使用しない（壊さない）。なおフロントパネルのシリアルコンソールポート（COM0）はCisco互換配線（<a href="http://yost.com/computers/RJ45-serial">Yost Serial Device Wiring Standard</a>）になっている。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p><a href="https://fedorahosted.org/liveusb-creator/">Fedora LiveUSB Creator</a> <a href="http://multix.jp/headless-liveboot-centos/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p>persistentモードで初期化済フラグファイルが書けるようになると、これらの初期化処理はスキップされるようになる。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:5" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:6"><p>Fedora LiveUSB Creatorが、ISOイメージ中の /isolinux を /syslinux へコンバートしている。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:6" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:7"><p>Ubuntuでは USBメモリでも一応改竄チェックが可能だが、ISOイメージ中のブートセクタといった非ファイル領域に生じたライティングエラーを検出できない。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:7" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:8"><p>Dracutは、isomd5sumパッケージがインストール済なら initramfsを作成する際にこれを組み込む。initramfsに組み込まれていない場合、rd.live.checkオプションによるメディア検査は正しく実行できない。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:8" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:9"><p><em>Test this media &amp; start CentOS</em>は光学メディア起動時は chkisomd5が走り、検査に成功しなければログイン・プロンプトに達しないが、USBメモリ起動時はメディア自体が検査不能なので、そのまま進んでログイン・プロンプトに達することができる。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:9" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:10"><p>物理パーテションを /homeにしたり、暗号化したりする起動オプションもある。 <a href="http://multix.jp/headless-liveboot-centos/#fnref:10" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[LinuxヘッドレスLiveBoot（Ubuntu編）]]></title><description><![CDATA[<p>VGAのない産業用ヘッドレスPC類に Linux系OSを組み込むのは割とよく行う。もっぱら CentOSをメインにしているのだが、時には諸事情やら比較検証用途やらで急遽 Ubuntuが必要になったりもする。そこでまあ Ubuntuベースのヘッドレス LiveBootを作っておくことにした。  </p>

<ul class="index"></ul>  

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する Ubuntuはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされている。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物はISOイメージとし、CD-R/DVD-Rに焼いてUSB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成のUSBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>apt-get一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>ubuntu</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的なLiveBootの流儀）  </li>
<li>既定のネットワーク設定・</li></ol>]]></description><link>http://multix.jp/headless-liveboot-ubuntu/</link><guid isPermaLink="false">2367878c-5496-47cb-962a-44089346dca7</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[Linux]]></category><category><![CDATA[めもらんだむ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Tue, 09 Jun 2015 02:27:30 GMT</pubDate><content:encoded><![CDATA[<p>VGAのない産業用ヘッドレスPC類に Linux系OSを組み込むのは割とよく行う。もっぱら CentOSをメインにしているのだが、時には諸事情やら比較検証用途やらで急遽 Ubuntuが必要になったりもする。そこでまあ Ubuntuベースのヘッドレス LiveBootを作っておくことにした。  </p>

<ul class="index"></ul>  

<hr>

<h4 id="">要件定義</h4>

<ol>
<li>対象機は x86_64（amd64）とする。  </li>
<li>VGAデバイスは物理的に搭載されていない。GUIも使用しない。よって起動する Ubuntuはコンソールベースとする。  </li>
<li>BIOSコンソールリダイレクト機能は対象機材でサポートされている。  </li>
<li>シリアルコンソールは baud=115.2kで接続する。<sup id="fnref:1"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:1" rel="footnote">1</a></sup>  </li>
<li>成果物はISOイメージとし、CD-R/DVD-Rに焼いてUSB-ODDブート（USB光学ドライブ起動）できるものとする。通常は、再起動毎にあらゆる設定や痕跡は完全に忘却される。  </li>
<li>他の Linuxマシンを必要とすることなく、一般的な Windowsマシンから既成のUSBインストーラを用いて、USBフラッシュドライブに変換できるものとする。この際 persistent領域を設定できなければならない。  </li>
<li>apt-get一式を備えている。追加したパッケージや設定は、persistent有効時は維持される。  </li>
<li>既定のログインユーザ名は <em>ubuntu</em> とし、パスワードも無しとする。ゆえにパスワード無しで sudoできるものとする。（一般的なLiveBootの流儀）  </li>
<li>既定のネットワーク設定・接続はオフとする。  </li>
<li>これらのパスワードやネットワーク設定は persistent有効時には自由に設定変更して永続化することができ、再起動しても設定内容が失われてはならない。</li>
</ol>

<p>要するに、KVMやDocker等の仮想マシン環境のそれと同様の感覚で、気軽に使用できる使い捨て環境の物理版<sup id="fnref:2"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:2" rel="footnote">2</a></sup>である。その仕様上基本的には RAMシステムとなり Swapは初期設定しない。従ってメインメモリは充分潤沢に搭載されているものとする。<sup id="fnref:3"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:3" rel="footnote">3</a></sup></p>

<p>Ubuntu LiveCD Customization については <strong><a href="https://help.ubuntu.com/community/LiveCDCustomization">こちらを参照</a></strong> のこと。</p>

<p>以後は ubuntu-15.04-desktop-amd64.iso をベースにして話を進める。32bit版でも同様だが、14.xx LTS では本稿で解説している手法は使えない。<sup id="fnref:4"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:4" rel="footnote">4</a></sup></p>

<p>なお製作時のリファレンス機材には Riava RS670A を使用した。<sup id="fnref:5"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:5" rel="footnote">5</a></sup></p>

<hr>

<h4 id="usb">シリアルコンソール対応のインストーラUSBフラッシュドライブを作る</h4>

<p>まずはヘッドレス環境で使用できるUbuntuインストーラを作らなければならない。UbuntuのインストーラISOには Desktop版と Server版があり、後者には CLI（テキスト）インストール機能があるが、本稿では Desktop版のISOイメージをベースにする。</p>

<p>端的には、内蔵ストレージにテキストベースの Ubuntuをクリーンインストールして GRUBで起動するのであれば、Server版をベースにするのが筋だ。だが Server版のISOイメージは LiveBoot機能を持たないため、今回の目的では Desktop版ISOイメージから作り出すほうが楽なのである。</p>

<p>用意するのは Ubuntu Desktop 64-bit Install ISO ファイルと、FAT32でフォーマットした1GiB以上のUSBフラッシュドライブと、Universal USB Installer<sup id="fnref:6"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:6" rel="footnote">6</a></sup>だ。</p>

<p>Windowsマシンで USBインストーラを立ち上げ、ダウンロードしたISOイメージと書き込み先フラッシュドライブを指定して起動可能メディアを作成する。この時 persistent領域は指定しない（永続化しない）。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-09-11-13-58.png" alt=""></p>

<p>次にブートメニューを書き換えてシリアルコンソールが使えるようにする。この作業は原則として TeraPadや MIFESのようなテキストエディタで行うが、Windows Notepadでもできなくはない。フラッシュドライブに書き込まれた元ファイルは LF改行なので Notepadでは全部が1行に繋がってしまうが、ブートローダー（SYSLINUX）は CR+LF改行でも動作するので、Notepadでも以下のテンプレートにそのまま入れ替えてしまってもよい。</p>

<p>isolinux.cfg冒頭のシリアルポート設定<sup id="fnref:7"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:7" rel="footnote">7</a></sup>は、BIOSにコンソールリダイレクト機能が備わっている場合には必要ない。BIOSとブートローダーの両方で設定をしてしまうと、入出力がバッティングして表示が崩れたりキー入力に不具合が生じる。従って今回の場合はコメントアウトで記述しておくだけとする。</p>

<pre><code class="language-brush:plain gutter:true highlight:[1,2] title:/isolinux/isolinux.cfg">#serial 0 115200
#console 0
path  
include menu.cfg  
default vesamenu.c32  
prompt 0  
timeout 50  
</code></pre>

<p>stdmenu.cfg からはスプラッシュ画像や表示色の設定をごっそり削る。またブートメニューが 80x24程度に収まるよう、vshift等を調整する。</p>

<pre><code class="language-brush:plain gutter:true title:/isolinux/stdmenu.cfg">menu vshift 4  
menu rows 10  
menu helpmsgrow 15  
menu cmdlinerow 16  
menu timeoutrow 16  
menu tabmsgrow 18  
menu tabmsg Press ENTER to boot or TAB to edit a menu entry  
</code></pre>

<p>txt.cfgのカーネルオプション行には、BIOSからカーネルに制御が渡った後のシリアルコンソール設定 <code>console=tty0 console=ttyS0,115200n8</code>を追記する。またLiveCD機能による自動ネットワーク構成が勝手に動かないよう <code>ip=frommedia</code>も必ず設定する。そしてこの段階では GUIと Ubuntuインストーラの起動をスキップしてシェルに落ちるための <mark>single</mark>を指定しておく。</p>

<pre><code class="language-brush:plain gutter:true highlight:[6] title:/isolinux/txt.cfg （USBメモリブート用）">menu title Ubuntu Console boot menu  
default live  
label live  
  menu label ^Try Ubuntu Console
  kernel /casper/vmlinuz.efi
  append  file=/cdrom/preseed/ubuntu.seed boot=casper cdrom-detect/try-usb=true noprompt floppy.allowed_drive_mask=0 ignore_uuid initrd=/casper/initrd.lz ip=frommedia console=tty0 console=ttyS0,115200n8 single ---
label memtest  
  menu label Test ^memory
  kernel /install/mt86plus
  append console=ttyS0,115200n8
label hd  
  menu label ^Boot from first hard disk
  localboot 0x80
</code></pre>

<p>追加された kernel起動パラメータは以下の意味を持つ。</p>

<ul>
<li><em>boot=casper</em> <br>
casperによるブートシーケンス（LiveBoot）を実行する。  </li>
<li><em>cdrom-detect/try-usb=true</em> <br>
USBメモリブートデバイスを CDROMと同等に扱う。  </li>
<li><em>floppy.allowed_drive_mask=0</em> <br>
フロッピーデバイスを無視する。</li>
<li><em>ignore_uuid</em> <br>
ブートメディアのUUIDをチェックしない。</li>
<li><em>ip=frommedia</em> <br>
ブートメディアの /etc/network/interfaces 設定を有効にする。</li>
<li><em>console=tty0</em> <br>
tty0をコンソール入出力に使用する。tty0は一般に VGA表示デバイスやフレームバッファである。  </li>
<li><em>console=ttyS0,115200n8</em> <br>
ttyS0をコンソール入出力に使用する。ttyS0は一般に第一シリアルポート。通信要件は baud=115200、パリティなし、8bit幅とする。このふたつのconsole指定は、両方書く場合はこの順番（tty0、ttyS0）で書かなければならない。<sup id="fnref:8"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:8" rel="footnote">8</a></sup>  </li>
<li><em>single</em> <br>
シングルモードで起動する。ここでは Ubuntuインストーラを起動させないために指定する。</li>
</ul>

<p>なおインストールイメージに含まれる memtest86+（mt86plus）は、シリアルコンソールに対応していない。通常のVGA表示に加えてBIOSコンソール表示もするようにリビルドしたバイナリと差し替えておくと便利だ。<sup id="fnref:9"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:9" rel="footnote">9</a></sup></p>

<hr>

<h4 id="rootfs">rootfsを作成する</h4>

<p>上記のUSBメモリで起動し <strong>Try Ubuntu Console</strong>を実行するとシングルユーザモードのbashシェルが起動する。まずは dhclientを起動したり、あるいは ifconfigと routeを叩くなりしてインターネットへ接続できるようにしよう。またロケール変数もセットする。</p>

<pre><code class="language-brush:bash">dhclient eth0

LANG=C  
</code></pre>

<p>現在起動しているこの環境は RAMシステムのため、8GiBの物理メモリを積んでいるなら 4GiB容量の RAMディスクが使える。これから作る rootfsは 2GiBも余裕があれば充分なので、/root以下を作業領域とする。なお /tmpは tmpfsマウント時のオプションのせいでそのままでは使用できない<sup id="fnref:10"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:10" rel="footnote">10</a></sup>。RAM容量が足りない場合には Ext4フォーマットした別の USBメモリや外付けHDD等を作業領域に使おう。</p>

<p>新しい rootfsは debootstrapパッケージを利用して作成する。さらにそれを LiveBootで使えるようにするために squashfs-toolsパッケージを用いるので、これらをインストールする。</p>

<pre><code class="language-brush:bash">apt-get install debootstrap squashfs-tools  
</code></pre>

<p>構築する Ubuntuのコードネームと CPUアーキテクチャは USBメモリから起動したそれと同じものとするが、これらは以下のコマンドで確認できる。</p>

<pre><code class="language-brush:plain highlight:[2,3,4,5,6,9]">lsb_release -a  
No LSB modules are available.  
Distributor ID: Ubuntu  
Description:    Ubuntu 15.04  
Release:        15.04  
Codename:       vivid

dpkg --print-architecture  
amd64  
</code></pre>

<p><em>lsb_release</em>コマンドが参照している実体は<code>/etc/lsb-release</code>なので、これをsourceコマンドで読めば環境変数で参照できるようになる。</p>

<pre><code class="language-brush:bash highlight:[2,3,4,5,10]">cat /etc/lsb-release  
DISTRIB_ID=Ubuntu  
DISTRIB_RELEASE=15.04  
DISTRIB_CODENAME=vivid  
DISTRIB_DESCRIPTION="Ubuntu 15.04"

source /etc/lsb-release

echo $DISTRIB_CODENAME  
vivid  
</code></pre>

<p>これらを debootstrapコマンドの引数に記述してミニマムな新規 rootfsを構築する。</p>

<pre><code class="language-brush:bash">source /etc/lsb-release  
mkdir newroot  
debootstrap --arch $(dpkg --print-architecture) \  
   $DISTRIB_CODENAME newroot http://archive.ubuntu.com/ubuntu
</code></pre>

<hr>

<h4 id="rootfs">rootfsの環境設定</h4>

<p>構築された rootfsは上記のコマンドだけで、すでに chrootして一応 apt-getも実行できる状態になっているが、kernelはまだインストールされていない。そこでこれらを行う前の準備として /proc、/dev、/run を rootfs内にマウントする。またデフォルトロケールとタイムゾーンをセットする。</p>

<pre><code class="language-brush:bash">mount -o bind /proc/ newroot/proc/  
mount -o bind /dev/ newroot/dev/  
mount -o bind /run/ newroot/run/

chroot newroot/ /usr/sbin/locale-gen en_US.UTF-8  
chroot newroot/ /usr/sbin/update-locale LANG=en_US.UTF-8  
chroot newroot/ /usr/sbin/dpkg-reconfigure locales  
chroot newroot/ /usr/bin/timedatectl set-timezone Asia/Tokyo  
</code></pre>

<p>さらに aptが参照する <code>newroot/etc/apt/sources.list</code>には必要最小限の1行しか書かれていないので、これを以下の内容に書き換える。<sup id="fnref:11"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:11" rel="footnote">11</a></sup></p>

<pre><code class="language-brush:plain title:newroot/etc/apt/sources.list for 15.04">deb http://jp.archive.ubuntu.com/ubuntu/ vivid main restricted  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid main restricted  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-updates main restricted  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-updates main restricted  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid universe  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid universe  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-updates universe  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-updates universe  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid multiverse  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid multiverse  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-updates multiverse  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-updates multiverse  
deb http://jp.archive.ubuntu.com/ubuntu/ vivid-backports main restricted universe multiverse  
deb-src http://jp.archive.ubuntu.com/ubuntu/ vivid-backports main restricted universe multiverse  
deb http://security.ubuntu.com/ubuntu vivid-security main restricted  
deb-src http://security.ubuntu.com/ubuntu vivid-security main restricted  
deb http://security.ubuntu.com/ubuntu vivid-security universe  
deb-src http://security.ubuntu.com/ubuntu vivid-security universe  
deb http://security.ubuntu.com/ubuntu vivid-security multiverse  
deb-src http://security.ubuntu.com/ubuntu vivid-security multiverse  
</code></pre>

<p>次に LiveBoot専用の管理者ユーザ <em>ubuntu</em>およびグループを作成し、パスワードなしでログイン出来るようにする。さらにrootアカウントでは直接ログイン出来ないようにロックしておく。このユーザ名は LiveBoot機能を提供している casperモジュールの設定に合わせたものだ。むろんそれぞれに任意のパスワードを設定しても構わない。（それを忘れたりしなければ）</p>

<pre><code class="language-brush:bash">chroot newroot/ /usr/sbin/addgroup --system --gid=999 ubuntu  
chroot newroot/ /usr/sbin/useradd -s /bin/bash -g ubuntu --uid=999 -m -k /dev/null ubuntu  
chroot newroot/ /usr/bin/passwd -d ubuntu  
chroot newroot/ /usr/sbin/usermod -L root  
</code></pre>

<p>内蔵ストレージに直接起動可能な rootfsをインストールする場合は、この時点で<code>/etc/fstab</code>を作成しなければならないが、LiveBootではこれが起動中に動的に作成・設定されるため、必要ない。</p>

<p>以上の環境設定が済んだら、apt-getで update/upgradeを行って chroot環境を更新する。</p>

<pre><code class="language-brush:bash">chroot newroot/ /usr/bin/apt-get update  
chroot newroot/ /usr/bin/apt-get upgrade  
</code></pre>

<hr>

<h4 id="linuxkernel">Linux kernelモジュールの組み込み</h4>

<p>いよいよ rootfs内に kernelモジュールをインストールするのだが、その前に現在起動中の USBメモリ（この例では/dev/sda）の MBRセクタを一応バックアップしておく。kernelインストール中に grub-pcセットアップが走るのだが、このとき MBRが上書きされてリブート不能<sup id="fnref:12"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:12" rel="footnote">12</a></sup>になるのを避けるためだ。なおインストールする kernelパッケージ名は 64bitと 32bitでは異なる。</p>

<pre><code class="language-brush:bash">dd if=/dev/sda of=mbr count=1  
</code></pre>

<pre><code class="language-brush:bash title:64bit kernel for EFI Support">chroot newroot/ /usr/bin/apt-get install linux-signed-image-$(uname -r)  
</code></pre>

<pre><code class="language-brush:bash title:32bit kernel">chroot newroot/ /usr/bin/apt-get install linux-image-$(uname -r)  
</code></pre>

<p>grub-pcセットアップは <em>Continue without installing GRUB?</em>を選んで何もせずに抜ける。そして忘れないうちにバックアップしておいた MBRを書き戻す。</p>

<pre><code class="language-brush:bash">dd if=mbr of=/dev/sda  
</code></pre>

<p>続いてその他のパッケージを追加インストールし、整合性を確認したのち、パッケージキャッシュを削除する。少なくとも <code>plymouth</code>はインストールが必要である。</p>

<pre><code class="language-brush:bash">chroot newroot/ /usr/bin/apt-get install plymouth squashfs-tools openssh-client  
chroot newroot/ /usr/bin/apt-get -f install  
chroot newroot/ /usr/bin/apt-get upgrade  
chroot newroot/ /usr/bin/apt-get autoremove  
chroot newroot/ /usr/bin/apt-get clean  
</code></pre>

<p>最後に newroot/var以下のクリーニング<sup id="fnref:13"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:13" rel="footnote">13</a></sup>と、newroot/bootから <strong>kernelとinitrdの削除</strong>する。LiveBootではブートデバイスの <code>casper</code>ディレクトリにある kernelとinitrdをそのまま使用するのでこれらの容量を節約する。</p>

<pre><code class="language-brush:bash">find newroot/var/log -type f -exec truncate -s 0 {} \;  
find newroot/var/cache -name \*-old -delete  
rm -fr newroot/var/lib/apt/lists/*

rm -f newroot/boot/vmlinuz-* newroot/boot/initrd.img-*  
</code></pre>

<p>以上で rootfsの構築は完了したのだが、作業時にマウントした /proc、/dev、/run がアンマウントできない場合がある。その際は lsofコマンドで掴んでいるプロセス探して killすればよい。正しくアンマウントできていれば duコマンドで使用容量が把握できる。</p>

<pre><code class="language-brush:bash highlight:[10]">lsof | grep newroot

kill &lt;PID&gt;

unmount newroot/run  
unmount newroot/dev  
unmount newroot/proc

du -sh newroot/  
420M    newroot/  
</code></pre>

<hr>

<h4 id="filesystemsquashfs">filesystem.squashfs ファイルの作成</h4>

<p>構築した rootfsを mksquashfsコマンドで圧縮する。このファイルはUSBメモリの /casperディレクトリに格納するが、これは <code>/cdrom/casper</code>として見ることが出来る。ただし <code>/cdrom</code>（USBメモリ）は roマウントされていてそのままでは書き込めないため、rwオプションでリマウントする。またそこにある <code>filesystem.squashfs</code>はいままさに <code>/rofs</code>にマウントして使われているため、直接上書きしてはならない。そこで別のファイル名で保存しておく。またこの rootfs全体のブロックサイズも保存しておく。</p>

<pre><code class="language-brush:bash">mount -o rw,remount /cdrom  
mksquashfs newroot /cdrom/casper/filesystem.squashfs.newroot  
printf $(du -s --block-size=1 newroot | cut -f1) &gt; /cdrom/casper/filesystem.size.newroot  
</code></pre>

<p>ちなみに <code>mksquashfs</code>で作成したファイルは <code>unsquashfs</code>コマンドで展開することが出来る。</p>

<pre><code class="language-brush:bash highlight:[4,5]">unsquashfs -d expand-root /cdrom/casper/filesystem.squashfs

ls expand-root/  
bin   dev  home        lib    media  opt   root  sbin  sys  usr  vmlinuz  
boot  etc  initrd.img  lib64  mnt    proc  run   srv   tmp  var  
</code></pre>

<hr>

<h4 id="rootfs">新 rootfs での起動テスト</h4>

<p>これらの作業を終えたら poweroff（shutdown -h）して、USBメモリを Windowsマシンに戻して以下の修正を行う。</p>

<ul>
<li>既存の /casper/filesystem.squashfs を filesystem.squashfs.org にリネーム</li>
<li>/casper/filesystem.squashfs.newroot を filesystem.squashfs にリネーム</li>
<li>同様に filesystem.size も差し替える。</li>
<li>/isolinux/txt.cfg の append行からキーワード single を削除する。</li>
</ul>

<p>この新たな環境での LiveBoot起動に失敗した場合は、上記の逆の手順でインストーライメージのシングルモードに戻す。そして unsquashfsコマンドで rootfsを展開して修正することを繰り返す。</p>

<p>新たな LiveBootが正しく起動してログイン・プロンプトが現れたら、ユーザ名 <em>ubuntu</em>（パスワード無し）でコンソールに入れるはずだ。また <code>sudo -s</code>で rootシェルになれる。</p>

<p><img src="http://multix.jp/content/images/2015/06/2015-06-22-18-14-35.png" alt=""></p>

<hr>

<h4 id="iso">ISOイメージを作成する</h4>

<p>こうして作成した filesystem.squashfsと、ブートメニューファイルを差し替えた ISOイメージを作成する。</p>

<p>まずベースになる Desktop版ISOイメージをローカルに展開する。これはODD（光学ドライブ）から読みだしても良いし、フラッシュドライブにISOイメージをコピーしておいて loopbackマウントで読みだしても良い。コピーしたファイルは書き込み不可となっているので <code>chmod</code>コマンドでパーミッションを変更する。</p>

<pre><code class="language-brush:bash highlight:[6,13,14]">cd /tmp

mkdir loop

sudo mount -o loop ubuntu-15.04-desktop-amd64.iso loop/  
mount: /dev/loop2 is write-protected, mounting read-only

cp -r loop files  
sudo umount loop  
chmod -R +w ./files

ls files/  
autorun.inf  casper  EFI      isolinux    pics  preseed             ubuntu  
boot         dists   install  md5sum.txt  pool  README.diskdefines  wubi.exe  
</code></pre>

<p>現在起動している filesystem.squashfsは <code>/cdrom/casper/</code>以下に見えているのでこれをコピーする。</p>

<pre><code class="language-brush:bash">cat /cdrom/casper/filesystem.squashfs &gt; files/casper/filesystem.squashfs  
</code></pre>

<p>isolinux/isolinux.cfg<sup id="fnref:14"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:14" rel="footnote">14</a></sup>や stdmenu.cfgについては前に上げたのと同じなので割愛する。新しい ODDブート用の isolinux/txt.cg は以下のように記述する。</p>

<pre><code class="language-brush:plain gutter:true title:isolinux/txt.cfg （ODDブート用）">menu title Ubuntu Console boot menu  
default live  
label live  
  menu label ^Try Ubuntu Console
  kernel /casper/vmlinuz.efi
  append  file=/cdrom/preseed/ubuntu.seed boot=casper initrd=/casper/initrd.lz quiet ip=frommedia console=tty0 console=ttyS0,115200n8 ---
label check  
  menu label ^Check disc for defects
  kernel /casper/vmlinuz.efi
  append  boot=casper integrity-check initrd=/casper/initrd.lz quiet console=tty0 console=ttyS0,115200n8 ---
label memtest  
  menu label Test ^memory
  kernel /install/mt86plus
  append console=ttyS0,115200n8
label hd  
  menu label ^Boot from first hard disk
  localboot 0x80
</code></pre>

<p>md5sum.txtは2番目の起動選択 <strong>Check disc for defects</strong>を選んだ際に参照される改竄チェックファイルだ。単純にこのファイルをゼロバイトに切り詰めれば何もチェックされない。一方この機能を活かしたい場合、チェックファイルを作成した後に md5sumが変わってしまうファイル、すなわち md5sum.txt自身と mkisofsコマンドが作成・更新する boot.cat、isolinux.bin はチェックリストから除外しておかなければならない。</p>

<pre><code class="language-brush:bash">pushd ./files  
rm -f md5sum.txt isolinux/boot.cat  
find . -type f -print | xargs md5sum | grep -v isolinux.bin &gt; ../md5sum.txt  
mv ../md5sum.txt .  
popd  
</code></pre>

<p>ISOイメージを作成する mkisofsコマンドは、同名のパッケージでインストールできる。</p>

<pre><code class="language-brush:bash">sudo apt-get install mkisofs  
</code></pre>

<p>mkisofsコマンドの引数<sup id="fnref:15"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:15" rel="footnote">15</a></sup>のうち -b（ブートローダーファイル）と-c（カタログファイル）は構築対象ディレクトリを基準とした場合のパス指定になるので注意しよう。また出力する ISOファイルは、先に上げた USBインストーラを使用する場合はその制限によりファイル名中に <em>desktop</em>の文字列が入っていなければならない。ともかく指定するオプションが多いのでシェルスクリプトにしておいたほうがコードの再利用と修正はしやすい。</p>

<pre><code class="language-brush:bash highlight:[14,15]">find ./files -print | xargs touch -m {} ¥;  
LANG=C mkisofs -r -J -l \  
  -V UBUNTU64 \
  -cache-inodes \
  -b isolinux/isolinux.bin \
  -c isolinux/boot.cat \
  -no-emul-boot \
  -boot-load-size 4 \
  -boot-info-table \
  -o ubuntu-15.04-desktopless-amd64.iso \
  ./files

ls -la ubuntu-15.04-desktop*  
-rwxr--r-- 1 ubuntu ubuntu 1150844928 Jun 17 16:59 ubuntu-15.04-desktop-amd64.iso
-rw-rw-r-- 1 ubuntu ubuntu  238929920 Jun 17 17:25 ubuntu-15.04-desktopless-amd64.iso
</code></pre>

<p>出来上がった ISOイメージはフラッシュドライブや NASなどにコピーして保存しておこう。</p>

<hr>

<h4 id="cdrwdvdrwiso">CD-RW/DVD-RWに ISOイメージを焼く</h4>

<p>前項で作成したISOイメージを Linuxで CD-RW/DVD-RWメディアに焼くには以下のようにする。</p>

<p>まずライティングソフトとして wodimパッケージをインストールする。</p>

<pre><code class="language-brush:bash">sudo apt-get install wodim  
</code></pre>

<p>USB-ODDは、おそらく /dev/sr0 等に見えるだろう。wodimコマンドの <code>-scanbus</code>オプションは SCSIバス以外には機能しないので、dmesgでデバイスノードを確認する。</p>

<pre><code class="language-brush:bash highlight:[2,3,4,5]">sudo wodim dev=/dev/sr0 --devices  
wodim: Overview of accessible drives (1 found) :  
-------------------------------------------------------------------------
 0  dev='/dev/sr0'      rwrw-- : 'Slimtype' 'eTAU108   1'
-------------------------------------------------------------------------
</code></pre>

<p>CD-RW/DVD-RWメディアをブランク消去するには wodimコマンドの blankオプションを使う。</p>

<pre><code class="language-brush:bash">sudo wodim dev=/dev/sr0 blank=fast  
</code></pre>

<p>wodimコマンドにISOイメージファイルを与えると、それを指定のドライブに焼きこむ。</p>

<pre><code class="language-brush:bash">sudo wodim -sao -eject dev=/dev/sr0 ubuntu-15.04-desktopless-amd64.iso  
</code></pre>

<hr>

<h4 id="persistent">persistentモードを設定する</h4>

<p>前述の ISOイメージ/光学メディアで ODDブートが正常に行えたなら、次はその ISOイメージを USBインストーラでもって、今度は persistentモードを設定した USBメモリを作成しよう。</p>

<p>persistentモードが有効な USBメモリは、kernel起動オプションに <em>persistent</em>が加わり、USBメモリのルートに <code>casper-rw</code>というファイルが作成される。このファイルの実体は overlayfsを用いて <code>/</code>に重ねられた ext4フォーマットの loopbackマウントファイルだ。<code>/rofs</code>にマウントされた filesystem.squashfsとの変更差分は、この <code>casper-rw</code>に記録されるようになる。</p>

<pre><code class="language-brush:plain highlight:[2,3,4,5,8,9,10,11,12]">df -Th  
Filesystem     Type      Size  Used Avail Use% Mounted on  
udev           devtmpfs  3.9G     0  3.9G   0% /dev  
tmpfs          tmpfs     798M  8.6M  789M   2% /run  
/dev/sda       vfat      2.0G  1.5G  468M  76% /cdrom
/dev/loop0     squashfs  182M  182M     0 100% /rofs
/cow           overlay   976M   14M  895M   2% /
tmpfs          tmpfs     3.9G     0  3.9G   0% /dev/shm  
tmpfs          tmpfs     5.0M     0  5.0M   0% /run/lock  
tmpfs          tmpfs     3.9G     0  3.9G   0% /sys/fs/cgroup  
tmpfs          tmpfs     3.9G     0  3.9G   0% /tmp  
tmpfs          tmpfs     798M     0  798M   0% /run/user/999  
</code></pre>

<p>persistentモードが正しく効いているかどうかは、更新したログインパスワードやネットワーク設定が再起動しても正しく機能するか否かで容易にわかる。例えばrootアカウントでログイン可能にし、ubuntuアカウントを無効にして新たにadminユーザを作成し、rootとadminにパスワードを設定してみよう。</p>

<pre><code class="language-brush:plain highlight:[6,7,8,13,14,19,20,21]">sudo -s

usermod -U root

passwd root  
Enter new UNIX password:  
Retype new UNIX password:  
passwd: password updated successfully

usermod -L ubuntu

addgroup --system admin  
Adding group `admin' (GID 112) ...  
Done.

useradd -s /bin/bash -g admin -m -k /dev/null admin

passwd admin  
Enter new UNIX password:  
Retype new UNIX password:  
passwd: password updated successfully  
</code></pre>

<p>/etc/network/interfaces に記述したネットワーク設定も persistentモードでなら起動時に反映される。<sup id="fnref:16"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:16" rel="footnote">16</a></sup></p>

<pre><code class="language-brush:plain gutter:true title:/etc/network/interfaces sample">source-directory /etc/network/interfaces.d

auto lo  
iface lo inet loopback

auto eth0  
iface eth0 inet static  
    address 192.168.240.135
    network 192.168.240.0
    netmask 255.255.255.0
    broadcast 192.168.240.255
    gateway 192.168.240.1
    dns-nameservers 192.168.240.1 8.8.8.8 8.8.4.4

auto eth4  
iface eth4 inet dhcp  
</code></pre>

<p>persistentモードで記録された内容を無視してデフォルト状態で起動するには、ブートメニューの起動ラベル選択時に <code>persistent</code>ラベルを取り除けば良い。あるいは casper-rwファイルをリネームして再起動してしまう。<sup id="fnref:17"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:17" rel="footnote">17</a></sup></p>

<pre><code class="language-brush:bash">cd /cdrom  
mv casper-rw casper-rw.bak  
reboot  
</code></pre>

<p>新たな（まっさらの）1GiBの casper-rwファイルは Linux上では以下の手順で作成できる。また事のついでなので、Windows上からもこのファイルを容易に置き換えられるように、フォーマットしたばかりの casper-rwファイルを zipコマンドで圧縮バックアップしておくと、USBインストーラを使わなくても最初期化できるので、いざというとき何かと便利だ。<sup id="fnref:18"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:18" rel="footnote">18</a></sup></p>

<pre><code class="language-brush:bash highlight:[8]">cd /cdrom

rm -f casper-rw.bak

truncate -s $((2**30)) casper-rw

ls -l casper-rw  
-rwxr-xr-x 1 root root 1073741824 Jun 18 13:11 casper-rw

mkfs.ext4 -L casper-rw casper-rw


apt-get install zip

zip casper-rw.zip casper-rw  
</code></pre>

<p>なお FAT32での最大のファイルサイズは 4GiB-1バイトなので、truncateコマンドでの容量指定は<code>$((2**32-1))</code>となる。<sup id="fnref:19"><a href="http://multix.jp/headless-liveboot-ubuntu/#fn:19" rel="footnote">19</a></sup></p>

<hr>

<h4 id="">ダウンロード</h4>

<p><a href="https://secure.multix.jp/download/SerialLive/ubuntu-15.04-desktopless-amd64-20150703.iso">ubuntu-15.04-desktopless-amd64-20150703.iso</a> 174MiB (<a href="https://secure.multix.jp/download/SerialLive/ubuntu-15.04-desktopless-amd64-20150703.iso.md5">md5sum</a>)</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>USBシリアル変換は FTDI系を前提にしている。PL2303系（特にノーブランドのコピー品）は baud=115.2kでは通信異常が多発してトラブルのもとになる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>ヘッドレスにしてディスクレスでもある。PXEBOOTなら USBメモリすら使わない完全ディスクレスだが、ネットワーク等の周辺環境規模が大きくなるので気軽にお試し出来るものではない。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>メインメモリは最低でも2GiB、標準的には8GiBは乗っているものとする。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p><a href="http://www.ubuntu.com/download/desktop">Ubuntu.com</a> 14.xx以前はシリアルコンソール自体に対応していない。ローカルストレージインストールの場合は /etc/init/ttyS0.confを作成すれば一応使えるようになるが、LiveBootはこの設定を壊してしまうため、15.04とおなじ挙動をさせるにはかなりの修正が必要になるため、本稿では 14.xx対応については割愛した。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p><a href="http://store.shopping.yahoo.co.jp/riava/rs670-s16a.html">Riava RS670A</a> 基本スペックは Intel Gigabit NICx6を持つ Intel/Atom C2758 でメモリ8GiB。基本OSは CentOSまたは Ubuntu。SSD内蔵だがこれはここでは使用しない（壊さない）。なおフロントパネルのシリアルコンソールポート（COM0）は Cisco互換配線（<a href="http://yost.com/computers/RJ45-serial">Yost Serial Device Wiring Standard</a>）になっている。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:5" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:6"><p><a href="http://www.pendrivelinux.com/universal-usb-installer-easy-as-1-2-3/">Universal USB Installer</a> <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:6" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:7"><p><a href="http://www.tldp.org/HOWTO/Remote-Serial-Console-HOWTO/configure-boot-loader-syslinux.html">Remote Serial Console HOWTO</a> <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:7" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:8"><p>最初に書いたほうが主コンソール <em>/dev/console</em>になり、そのデバイスは常に存在することが期待される。物理的にVGAを持たない機器では tty0なしの ttyS0だけでも実は良いのだが、シリアルポート未接続であったりシリアルポートドライバに問題がある場合は起動不能になる事態もありうる。そこで plymothを使うことでフレームバッファを用意し、tty0が必ず存在する状態をとすることで不具合を回避している。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:8" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:9"><p>ただし memtest86+のフル機能がシリアルコンソールで使えるわけではない。configメニュー表示等に不具合があるため、設定変更が難しい。このためECC関係などのオプションはビルド時に修正しておかなければならない。シリアルコンソール対応memtest86+の作成については<a href="http://multix.jp/buildup-memtest-headless/">別項を参照</a>のこと。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:9" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:10"><p>規定値でnosuid,nodev,noexecが指定されている。これらをremountで外しても良いが、最初から制限のかかっていない /rootや /homeを使うほうが手間がない。ただし後述の persistentモードが有効な場合、これらはRAMディスクではないので、<em>mount -t tmpfs none /mnt</em> といった具合に明示的に RAMディスクをマウントして使ったほうが良い。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:10" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:11"><p>デフォルトの1行だけでも必要最小限のことはできるが、docker等いまどきのアプリケーションをフルに動かせるようにするには、これだけの行数が必要になる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:11" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:12"><p>MBRが壊れるのは、起動したUSBメモリ以外の内蔵ストレージがある場合でかつ /dev、/run等のマウントを忘れていた場合に発生する。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:12" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:13"><p>ログファイルは単純な削除でも良いが、wtmpやlastlog等も区別なく初期化するために、ここではtruncateでゼロバイトに切り詰めている。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:13" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:14"><p>ISOからコピーしたオリジナルのisolinux.cfgには最後に<strong>gfxboot</strong>にチェインする行があるが本稿では使わない（ISOLINUXをブートローダーとする）ためこれは必ず削除しなければ起動不能になる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:14" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:15"><p>gfxboot関係については、GRUB設定ごと割愛した。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:15" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:16"><p>ただし kernel起動時に <em>ip=frommedia</em> をつけておかないと、ネットワーク設定は綺麗サッパリ無視されてしまうので気をつけよう。知らないと確実にハマる。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:16" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:17"><p>persistentモードで起動しているときは当然マウントされているので削除したりdd初期化したりするのは論外だが、FAT32フォーマットの USBメモリで起動している場合はマウントしたまま mvコマンドでリネームすることはできてしまう。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:17" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:18"><p>ISOイメージを生成する前に準備しておくとより効果的だろう。 <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:18" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:19"><p>ブランクファイルを用意するのに ddコマンドがよく使われるが、バイト単位でのファイルサイズ指定は不得手なので truncateコマンドを使うほうが楽だし、そもそも動作が早い。あえて ddコマンドのオプションで 4GiB-1を表現するなら <em>bs=$((2**16-1)) count=$((2**16+1))</em> となる。  <a href="http://multix.jp/headless-liveboot-ubuntu/#fnref:19" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[DynDNS互換サービスを自作する]]></title><description><![CDATA[<p>筆者が使っている Allied Telesisの AR560S/AR550SにはダイナミックDNS機能があるが、対応サービスにはStandard DNS(Dyn.com)しか使えない。まあ企業用途なら一口25ドルくらいどうってことはないが、個人用途でこれはちょっと面白くないので、DynDNS互換サービスを自作してみた話。</p>

<ul class="index"></ul>

<hr>

<h4 id="dyndns">DynDNSプロトコル</h4>

<p>まず基本的な情報だが、ARルータ側の設定項目はこう定義されている。</p>

<pre><code class="language-brush:plain">SET DDNS [SERVER=server] [PORT=port] [USER=userid] [PASSWORD=password] [DYNAMICHOST=hostnames] [PRIMARYINT=ipinterface] [SECONDARYINT=ipinterface] [OFFLINE={YES|NO|ON|OFF}] [PERIODICUPDATE={1..60|ON|OFF}]

server: ダイナミックDNSサーバー名。（1～31文字。英数字）  
port: HTTPポート番号。80(</code></pre>]]></description><link>http://multix.jp/create-compatible-dyndns-service/</link><guid isPermaLink="false">05d79948-33cb-4328-b681-cf62acedd527</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 15 May 2015 06:50:57 GMT</pubDate><content:encoded><![CDATA[<p>筆者が使っている Allied Telesisの AR560S/AR550SにはダイナミックDNS機能があるが、対応サービスにはStandard DNS(Dyn.com)しか使えない。まあ企業用途なら一口25ドルくらいどうってことはないが、個人用途でこれはちょっと面白くないので、DynDNS互換サービスを自作してみた話。</p>

<ul class="index"></ul>

<hr>

<h4 id="dyndns">DynDNSプロトコル</h4>

<p>まず基本的な情報だが、ARルータ側の設定項目はこう定義されている。</p>

<pre><code class="language-brush:plain">SET DDNS [SERVER=server] [PORT=port] [USER=userid] [PASSWORD=password] [DYNAMICHOST=hostnames] [PRIMARYINT=ipinterface] [SECONDARYINT=ipinterface] [OFFLINE={YES|NO|ON|OFF}] [PERIODICUPDATE={1..60|ON|OFF}]

server: ダイナミックDNSサーバー名。（1～31文字。英数字）  
port: HTTPポート番号。80(HTTP)または 8245(HTTP Bypass)  
userid: ユーザーID。（1～15文字。英数字。大文字小文字を区別する）  
password: パスワード（1～15文字。英数字。大文字小文字を区別する）  
hostnames: ホスト名（URL形式。カンマ区切りで複数指定可能。カンマを含め128文字まで。）  
ipinterface: インターフェース名。(ppp0など)  
</code></pre>

<p>その結果、DNSサーバには次のようなリクエストが送信される。</p>

<pre><code class="language-brush:plain">GET /nic/update?system=dyndns&amp;hostname=example.dyndns.org&amp;myip=123.45.67.89&amp;wildcard=OFF&amp;offline=OFF HTTP/1.1  
Host: members.dyndns.org  
Authorization: Basic [BASE64-ENCODED-USERNAME:PASSWORD-PAIR]  
User-Agent: Dyn DNS Client/CentreCOM AR560S version 2.9.2-09 21-Aug-2012  
</code></pre>

<p>これに対してサーバレスポンスに<code>good &lt;IP_ADDR&gt;</code>が返るとそれは正常に受け付けられたことになり、<code>show ddns</code>で確認できるステータス表示が更新される。</p>

<p>送信URIとして<code>/nic/update</code>は固定、ユーザ認証はBASIC認証方式、パラメータとして<code>hostname</code>は必須。CGIインタフェースとして実装すべき内容はこれだけなのでそう難しいことではない。</p>

<p>ここでの実装では要件的には必要ないのだがエラーログを残すのにも使うので、good以外のレスポンス文字列も上げておく。<sup id="fnref:1"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:1" rel="footnote">1</a></sup></p>

<pre><code class="language-table title:DynDNS Responce Code">|応答|状態|説明|
|good &lt;IP_ADDR&gt;|OK|リクエストは正常に受け付けた|
|nochg &lt;IP_ADDR&gt;|OK|リクエストは正常だが更新されるステータスはない|
|nohost|NG|hostnameは存在しない|
|badauth|NG|認証に失敗した|
|badagent|NG|この機器は許可されていない|
|!donator|NG|利用料が支払われていない|
|abuse|NG|ユーザはブロックされている・乱用|
|911|NG|重大なエラー|
</code></pre>

<hr>

<h4 id="nginxperlmodule">nginx perl moduleでの実装</h4>

<p>今回はnginxのperl module機能を用いて実装する。CentOS版のnginxはこれに対応しているが、公式サイトで配布しているバイナリは対応していないため要リビルドである。次のようにしてなにも表示されなければ、このビルドオプションが足りていない。</p>

<pre><code class="language-brush:plain">$ nginx -V 2&gt;&amp;1 | tr ' ' '\n' | grep perl
--with-http_perl_module
</code></pre>

<p>perlモジュールが有効なら、レスポンスハンドラをperlで記述することが出来る。そこで以下のコードを<code>/etc/nginx/lib/dyndns.pm</code>に用意した。</p>

<pre><code class="language-brush:perl gutter:true title:/etc/nginx/lib/dyndns.pm">#
# $Id: dyndns.pm 159 2015-05-15 04:18:23Z askn $
#
package dyndns;  
use strict;  
use utf8;  
use 5.010;  
use nginx;  
use Net::DNS;  
use MIME::Base64;  
use Sys::Syslog;

sub update_handler {  
    my $r = shift;

    $r-&gt;send_http_header("text/plain");
    return OK if $r-&gt;header_only;

    eval {
        openlog("ddns_update", "cons,pid", "daemon");

        die "unknown:nothing parameters\n" unless $r-&gt;args;

        my $auth = $r-&gt;header_in("Authorization") // "";

        die "badauth:bad authorization.\n"
            unless $auth =~ m{^Basic ([0-9a-z\+\/\=]+)}i;

        my $user = (split /:/, decode_base64($1))[0];

        my $q = {};
        foreach my $pair (split /&amp;/, $r-&gt;args) {
            my($key, $val) = split /=/, $pair;
            next      unless defined $key;
            $val = "" unless defined $val;
            $q-&gt;{$r-&gt;unescape($key)} = $r-&gt;unescape($val);
        }

        my $hostname = lc($q-&gt;{hostname} // "");
        my $myip     =    $q-&gt;{myip} || $r-&gt;remote_addr;
#        my $system   =    $q-&gt;{system}   // "";
#        my $wildcard =    $q-&gt;{wildcard} // "";
#        my $offline  =    $q-&gt;{offline}  // "";
#        my $mx       = ls($q-&gt;{mx}       // "");

        syslog("info", "USER=".$user." HOSTNAME=".$hostname." MYIP=".$myip);

        die "unknown:badformat myip.\n"
            unless $myip =~ /^(\d{1,3}\.){3}\d{1,3}$/;

        my $resolver = new Net::DNS::Resolver;
        $resolver-&gt;nameservers("127.0.0.1");

        my @fqdn = split /,/, $hostname;

        die "numhosth:too many hostname.\n"
            if 4 &lt; scalar @fqdn;

        foreach my $fqdn (@fqdn) {

        # ZONE name resolv stage

            defined $fqdn and $fqdn =~ s{(^\s+|\s+$)}{}g;

            die "nohost:badformat hostname.\n"
                unless defined $fqdn;

            die "badfqdn:badformat hostname.\n"
                unless $fqdn =~ /^([\w\-]+\.)*[\w\-]+$/;

            my $zone = "";
            my $ns = "";
            my(@stack, @ns);
            foreach my $dotted (reverse(split /\./, $fqdn)) {
                $zone = $dotted.".".$zone;
                unshift @stack, $zone;
            }

            foreach my $test (@stack) {
                my $query = $resolver-&gt;query($test, "NS");
                if ($query) {
                    foreach my $rr (grep {$_-&gt;type eq "NS"} $query-&gt;answer) {
                        push @ns, $rr-&gt;nsdname;
                    }
                    $zone = $test;
                    syslog("info", "ZONE=".$zone." NS=".join(",", @ns));
                    last;
                }
            }
            die "dnserr:Undefined query nameservers.\n"
                unless @ns;

            $resolver-&gt;nameservers(@ns);

        # Update stage

            my $update = new Net::DNS::Update($zone);
            $update-&gt;push(update =&gt; rr_del($fqdn.". IN A"));
            $update-&gt;push(update =&gt; rr_add($fqdn.". 3600 IN A ".$myip));

            my $reply = $resolver-&gt;send($update);

            die "dnserr:reply resolver ".$resolver-&gt;errorstring."\n"
                unless $reply;

            die "dnserr:reply ".$reply-&gt;header-&gt;rcode."\n"
                unless $reply-&gt;header-&gt;rcode eq "NOERROR";
        }

        $r-&gt;print("good " . $myip . "\n");
        syslog("info", "update success.");
    };
    if ($@) {
        chomp $@;
        syslog("info", "ERROR: " . $@);

        my $result = (split /:/, $@)[0];
        $r-&gt;print($result."\n");
    }

    return OK;
}
1;  
__END__  
</code></pre>

<p>コード中の<code>$r</code>はnginxから渡されるリファレンスオブジェクトで、mod_perlのそれと似た表現になっている。<code>$r-&gt;args</code>に送信されたクエリが入っているのでこれをパースして<code>hostname</code>と<code>myip</code>を受け取っているが、<code>myip</code>がなければ<code>$r-&gt;remote_addr</code>を代わりに使うよう<sup id="fnref:2"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:2" rel="footnote">2</a></sup>にもしている。その他のパラメータについてはこのコードでは対応していない。なお処理過程のログに付いては（perlの）<code>Sys::Syslog</code>モジュールを用いてsyslogのdaemonファシリティに送信するようにしている。</p>

<p>DNS Aレコード処理は（perlの）<code>Net::DNS</code>モジュールで行う。まず<code>localhost</code>ネームサーバにFQDNを問い合わせてNSレコード=当該ネームサーバIPを取得し（resolvステージ）そこに対して既存DNS Aレコードの削除および新規DNS Aレコードの登録（updateステージ）を行う。ここでは処理を端折っているが、本格的な汎用サービス目的で実装するなら、updateステージではアクセス認証キーの処理を追加する必要があるだろう。<sup id="fnref:3"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:3" rel="footnote">3</a></sup></p>

<hr>

<h4 id="nginxconf">nginx.conf側の記述</h4>

<p>nginx.confから要点だけを抜き書きすると次のようになる。</p>

<pre><code class="language-brush:plain">http {  
    perl_modules  /etc/nginx/lib;
    perl_require  dyndns.pm;

    server {
        location /nic/update {
            auth_basic            "closed site";
            auth_basic_user_file  /etc/nginx/_htpasswd;
            perl                  dyndns::update_handler;
        }
    }
}
</code></pre>

<p>まずグローバル設定内で<code>perl_modules</code>と<code>perl_require</code>ディレクティブを用いてperlコードをnginx本体にロードする。プロセス起動時にメモリ上に読み込まれるためコードを書き換えた場合は逐一nginxをreloadする必要がある。ここで読み込まれたperlコードのpackage名前空間名とハンドラ関数名をlocaltionブロック内で指定すると、URL（<code>/nic/update</code>）とperlコードとを紐付けることができる。またこの時BASIC認証を行うので、認証用パスワードファイルを<code>auth_basic_user_file</code>ディレクティブで指定する。</p>

<hr>

<h4 id="namedconf">named.conf側の設定</h4>

<p>named.confの設定では、FQDNが属すZONEを有しており、かつ上記のリクエストレコード送信元IPがallow-updateディレクティブで指定されていればよい。</p>

<pre><code class="language-brush:plain">zone "example.org" IN {  
    type master;
    :
    allow-update {
        127.0.0.1;
        192.168.0.0/24;
        key host1;
    }
}
</code></pre>

<hr>

<h4 id="perlcgi">参考：Perl-CGI版</h4>

<p>nginx版の前に作成・使用していたPerl-CGIバージョンを以下に上げておく。こちらは単に<code>nsupdate</code>コマンドを叩くだけのシンプルなものだ。<sup id="fnref:4"><a href="http://multix.jp/create-compatible-dyndns-service/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:perl collapse:true gutter:true title:/var/www/html/nic/update">#!/usr/bin/perl
use strict;  
use CGI;  
use Sys::Syslog;  
use 5.010;

my($q, $hostname);  
my $myip = '0.0.0.0';  
eval {  
    openlog('ddns_update', 'cons,pid', 'daemon');

    $q = new CGI;
    $hostname = $q-&gt;param('hostname');
    $myip     = $q-&gt;param('myip') || $ENV{HTTP_X_FORWARDED_FOR} || $ENV{REMOTE_ADDR};
#    $system   = $q-&gt;param('system');
#    $wildcard = $q-&gt;param('wildcard');
#    $offline  = $q-&gt;param('offline');
#    $mx       = $q-&gt;param('mx');

    syslog('info', 'HOSTNAME='.$hostname.' MYIP='.$myip);

    unless ($hostname =~ /^([\w\-]+\.)*[\w\-]+$/) {
        die "nohost\n";
    }

    unless ($myip =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
        die "unknown\n";
    }

    open my $PIPE, '|-', '/usr/bin/nsupdate' or die "badcall nsupdate.";
    print $PIPE &lt;&lt;__EOF;
server 127.0.0.1  
update delete ${hostname}. IN A  
update add ${hostname}. 3600 IN A ${myip}  
send

__EOF  
    close $PIPE or die "break nsupdate.\n";

    print 'Content-type: text/plain', "\r\n\r\n";
    print 'good ', $myip, "\n";
    syslog('info', 'Success.');
};
if ($@) {  
    print 'Content-type: text/plain', "\r\n\r\n";
    print 'nochg ', $myip, "\n";
    syslog('info', 'ERROR: '.$@);
}
1;  
__END__  
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>911は米国で言う110番のこと。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>通常の用途ではMYIPとremote_addrは常に同一だろう。ルータを多段に組んだ場合でもなければ両者が異なるケースはまずない。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>ここでの実装は簡易的なので、ネームサーバ側の設定でこのサービスサーバのIPに直接update許可を与えている。キー認証を使用する場合、named.confのほうではallow-updateディレクティブにアクセス許可するキーIDを追記する。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>この実装のままnginx/perlに持って行くとfork負荷が問題になったのでNet::DNS実装に置き換えた。 <a href="http://multix.jp/create-compatible-dyndns-service/#fnref:4" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[日本国内限定IPv4アクセス制限の仕込み方]]></title><description><![CDATA[<p>Webサービスとかやってると世界中と繋がっているのだなと思うことは多々ある。だがサービス内容によってはその範囲を狭めたいこともあるし、それ以前にお断りということもある。ではどうやって日本国内からのIPアクセスとそれ以外を区別しよう？</p>

<ul class="index"></ul>

<hr>

<h4 id="ipapnic">IPアドレスリストをAPNICから入手する</h4>

<p>世界中に分配されているIPv4アドレスは総元締めのIANAから、世界を分割支配する5つの団体（ARIN/APNIC/LACNIC/AfriNIC/RIPE NCC）に分配され、そこからさらに各国別の元締め（日本ならJPNIC）に再分配されている。このIP分配リストは各団体のFTPサイト等で公開されているので、これを入手して日本にアサインされたIPリストだけを抽出すればACL（アクセス制限リスト）の元を作ることが出来る。今回の場合はJPNICに割り当てられたIPリストを得るために、APNICのFTPサイトから更新リストを入手する。</p>

<p>この更新リスト<sup id="fnref:1"><a href="http://multix.jp/delegated-apnic-acl/#fn:1" rel="footnote">1</a></sup>はほぼ毎日アップデートされている。ファイルサイズは1.8MBほどで約4万行弱ある。また世界中からダウンロードに来ることもあって、日に何回もアクセスにゆくと回数制限に引っ掛かって数時間ブロックされるということもある。故に基本的には1日1回、cronを使用してダウンロードすることになる。</p>

<p>一方で毎日更新されているとはいえ、日本にアサインされたIPが日々こまめに変化しているというわけでもない。だいたい週に1〜2回程度、僅かな追加と削除<sup id="fnref:2"><a href="http://multix.jp/delegated-apnic-acl/#fn:2" rel="footnote">2</a></sup>が発生している程度だ。そこでダウンロードと共に前回のリストと比較して変化があったらメール通知を行い、さらにACLの更新処理を行うという仕組みを作ることにする。</p>

<p>なお以下の仕掛けはすべてrootユーザが行い、メインのワークエリアとしては<code>/etc/rc.d</code>を使用するものとする。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/cron.daily/apnic.list_update">#!/bin/sh

### /etc/</code></pre>]]></description><link>http://multix.jp/delegated-apnic-acl/</link><guid isPermaLink="false">0bd8198c-10c5-4c3d-af4d-0aed7c19e87c</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Web]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Wed, 13 May 2015 09:08:40 GMT</pubDate><content:encoded><![CDATA[<p>Webサービスとかやってると世界中と繋がっているのだなと思うことは多々ある。だがサービス内容によってはその範囲を狭めたいこともあるし、それ以前にお断りということもある。ではどうやって日本国内からのIPアクセスとそれ以外を区別しよう？</p>

<ul class="index"></ul>

<hr>

<h4 id="ipapnic">IPアドレスリストをAPNICから入手する</h4>

<p>世界中に分配されているIPv4アドレスは総元締めのIANAから、世界を分割支配する5つの団体（ARIN/APNIC/LACNIC/AfriNIC/RIPE NCC）に分配され、そこからさらに各国別の元締め（日本ならJPNIC）に再分配されている。このIP分配リストは各団体のFTPサイト等で公開されているので、これを入手して日本にアサインされたIPリストだけを抽出すればACL（アクセス制限リスト）の元を作ることが出来る。今回の場合はJPNICに割り当てられたIPリストを得るために、APNICのFTPサイトから更新リストを入手する。</p>

<p>この更新リスト<sup id="fnref:1"><a href="http://multix.jp/delegated-apnic-acl/#fn:1" rel="footnote">1</a></sup>はほぼ毎日アップデートされている。ファイルサイズは1.8MBほどで約4万行弱ある。また世界中からダウンロードに来ることもあって、日に何回もアクセスにゆくと回数制限に引っ掛かって数時間ブロックされるということもある。故に基本的には1日1回、cronを使用してダウンロードすることになる。</p>

<p>一方で毎日更新されているとはいえ、日本にアサインされたIPが日々こまめに変化しているというわけでもない。だいたい週に1〜2回程度、僅かな追加と削除<sup id="fnref:2"><a href="http://multix.jp/delegated-apnic-acl/#fn:2" rel="footnote">2</a></sup>が発生している程度だ。そこでダウンロードと共に前回のリストと比較して変化があったらメール通知を行い、さらにACLの更新処理を行うという仕組みを作ることにする。</p>

<p>なお以下の仕掛けはすべてrootユーザが行い、メインのワークエリアとしては<code>/etc/rc.d</code>を使用するものとする。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/cron.daily/apnic.list_update">#!/bin/sh

### /etc/rc.d/apnic.list_update
###
### delegated-apnic-latest を取得して変化があれば
### メール通知およびrc.iptablesの再実行を行うスクリプト
###
### /etc/cron.daily からsymlinkを張るか、そこに配置すること
###

new=`mktemp`  
errors=`mktemp`  
DIR="/etc/rc.d"  
list="$DIR/delegated-apnic-latest"

test -f $list || touch $list  
wget -q -O - "http://ftp.apnic.net/stats/apnic/delegated-apnic-latest" &gt; $new 2&gt; $errors

if [ $? -eq 0 ]; then  
    sort_new=`mktemp`
    sort_old=`mktemp`
    diff_out=`mktemp`

    # 日本国内IPについてのみ比較する
    /bin/grep '^apnic|JP|ipv4|' $new &gt; $sort_new
    /bin/grep '^apnic|JP|ipv4|' $list &gt; $sort_old
    diff --ignore-matching-lines="^#" $sort_new $sort_old &gt; $diff_out
    if [ $? -ne 0 ]; then
        (
         echo '-------------------- old delegated-apnic-latest --------------------'
         grep -P '^\d' $list
         echo
         echo '-------------------- new delegated-apnic-latest --------------------'
         grep -P '^\d' $new
         echo '---------------------- difference ----------------------'
         cat $diff_out

         # メールは rootにだす（ /etc/aliases に従う）
        ) | mail -s "delegated-apnic-latest updated $(hostname)" root
        cp -f $new $list

        # iptables の -s に渡せる形式に変換
        $DIR/jponly.pl &lt; $list &gt; $DIR/jponly.list

#        # rc.iptables を再実行する
#        \time $DIR/rc.iptables

         # iptables-save/restoreで更新する
         $DIR/update_mangle.pl
    fi
    rm -f $sort_new $sort_old $diff_out
else  
    cat $errors | mail -s "delegated-apnic-latest update check error $(hostname)" root
fi  
rm -f $new $errors  
</code></pre>

<p>このスクリプトは1日1回、APNICにお伺いを立てて（日本に割り当てられた）IPv4アドレスリストが変化していたら<code>/etc/rc.d/delegated-apnic-latest</code>ファイルを更新する。通知メールはrootに向けて投げるため、<code>/etc/aliases</code>に実際の通知先メールアドレスを書いておこう。</p>

<p>取得したリストの中身は概ね以下のような行の羅列である。</p>

<pre><code class="language-brush:plain">apnic|CN|ipv4|223.214.0.0|131072|20100803|allocated  
apnic|JP|ipv4|223.216.0.0|262144|20100712|allocated  
apnic|CN|ipv4|223.220.0.0|131072|20100723|allocated  
apnic|KR|ipv4|223.222.0.0|65536|20100721|allocated  
apnic|JP|ipv4|223.223.0.0|32768|20100803|allocated  
apnic|IN|ipv4|223.223.128.0|8192|20100809|allocated  
</code></pre>

<p>バーチカルラインで区切られた各カラムは次の意味を持つ。</p>

<ul>
<li>IP管理組織</li>
<li>2文字の国コード</li>
<li>IP種別</li>
<li>割り当てブロック先頭のIPアドレス</li>
<li>割り当てブロックに含まれるIP数（2の冪乗に一致）</li>
<li>割り当てられた日付</li>
<li>現在のステータス</li>
</ul>

<p>注意するのは5カラム目のIP数がIPマスクでもCIDRでもないという点で、ACLとして使用するにはこれをCIDRに変換する必要がある。これを行っているのが43行目の<code>jponly.pl</code>だ。</p>

<hr>

<h4 id="ipcidr">日本国内IPのCIDRを抽出する</h4>

<p>このフィルタスクリプトは、国コードがJPかつステータスがallocatedの行を抽出し、そのIP数からCIDRを計算して出力する。</p>

<pre><code class="language-brush:perl gutter:true title:/etc/rc.d/jponly.pl">#!/usr/bin/perl
use strict;  
use utf8;

###
### /etc/rc.d/jponly.pl
###
### delegated-apnic-latest から日本国内IPのCIDRを抽出する
###

my %CIDR;  
for my $bit ( 0..32 ) {  
    my $len = 2 ** ( 32 - $bit );
    $CIDR{$len} = $bit;
}
while ( &lt;&gt; ) {  
    chomp;
    if ( m{^apnic\|JP\|ipv4\|(\d+(?:\.\d+){3})\|(\d+)\|\d+\|allocated} ) {
        my $addr = $1;
        my $cidr = $CIDR{ $2 || 32 };
        printf "%s/%u\n", $addr, $cidr;
    }
}
exit 0;  
__END__  
</code></pre>

<p>これを通して生成した<code>/etc/rc.d/jponly.list</code>は1行1ブロックのIP/CIDRリストとなる。</p>

<pre><code class="language-brush:plain">223.216.0.0/14  
223.223.0.0/17  
</code></pre>

<hr>

<h4 id="iptablesacl">iptablesでのACL</h4>

<p>これを用いて例えばiptablesのmangleテーブル・Jponlyチェイン（フィルタ）に書き込むには次のようにする。</p>

<pre><code class="language-brush:bash futter:true">#!/bin/bash
JPONLY=/etc/rc.d/jponly.list

# mangleテーブルでフィルタを作る
iptables -t mangle -N Jponly  
cat $JPONLY | while read; do  
  iptables -t mangle -A Jponly -s $REPLY -j MARK --set-mark 2/2
done

# 上記のフィルタをINPUT/FORWARDに適用してパケットにmarkビットを立てる
iptables -t mangle -A INPUT -j Jponly  
iptables -t mangle -A FORWARD -j Jponly

# 他のチェインの中で、markビットが立っているならACCEPT、なければDROPする例
iptables -A Accept -m mark --mark 2/2 -j ACCEPT  
iptables -A Accept -j DROP  
</code></pre>

<p>mangleとmarkビットマスクを使うことで、このJponlyフィルタを容易に再利用できるようにしている。この他にホワイトリストチェイン・ブラックリストチェインを併用する場合も、markビットマスクを利用することで柔軟なACLフィルタを作り出すことが出来る。</p>

<hr>

<h4 id="">フィルタチェインの高速更新</h4>

<p><code>apnic.list_update</code>の49行目で呼び出している<code>update_mangle.pl</code>は、iptablesで設定した既存のJponlyチェインをアップデートするスクリプトだ。これはiptables-save・iptables-restoreコマンドを使用して高速に更新処理を行う。初回こそ前項のようにiptablesコマンドで丁寧に設定する必要があるが、これは数秒〜数十秒の処理時間が掛かってしまう。そこで2回目以降は該当チェインだけを書き換えることでダウンタイムを実用上問題ない時間（ミリ秒オーダー）まで切り詰める。<sup id="fnref:3"><a href="http://multix.jp/delegated-apnic-acl/#fn:3" rel="footnote">3</a></sup></p>

<pre><code class="language-brush:perl gutter:true title:/etc/rc.d/update_mangle.pl">#!/usr/bin/perl
use strict;

my $JPONLY  = '/etc/rc.d/jponly.list';  
my $UPDATE  = '/etc/rc.d/iptables.bak';  
my $SAVE    = '/sbin/iptables-save -c';  
my @RESTORE = ('/sbin/iptables-restore', '-c');

my $SEARCH   = qr{-A Jponly -s};  
my $TEMPLATE = "[0:0] -A Jponly -s %s -j MARK --set-xmark 0x2/0x2 \n";

my $BEFORE;  
my $AFTER = [];  
my @INSERT;

my $PIPE;  
open $PIPE, '&lt;', $JPONLY or die;  
while (&lt;$PIPE&gt;) {  
    chomp;
    push @INSERT, sprintf $TEMPLATE, $_;
}

close $PIPE;  
open $PIPE, '-|', $SAVE or die;  
while (&lt;$PIPE&gt;) {  
    unless ($BEFORE) {
        if ($_ =~ $SEARCH) {
            $BEFORE = $AFTER;
            push @$BEFORE, @INSERT;
            next;
        }
    }
    elsif ($_ =~ $SEARCH) {
        next;
    }
    push @$AFTER, $_;
}
close $PIPE;

open $PIPE, '&gt;', $UPDATE or die;  
print $PIPE @$BEFORE;  
print $PIPE @$AFTER;  
close $PIPE;

system @RESTORE, $UPDATE;

1;  
__END__  
</code></pre>

<hr>

<h4 id="apacheacl">ApacheでのACL</h4>

<p>前述の<code>jponly.list</code>からApache用のACLを作り出すには次のようにする。</p>

<pre><code class="language-brush:bash">#!/bin/bash
JPONLY=/etc/rc.d/jponly.list

( cat $JPONLY | while read; do
  echo "Allow from $REPLY"
done ) &gt; /etc/httpd/conf/jponly.conf  
</code></pre>

<p>こうして生成したACLを、必要な場所でIncludeディレクティブを使用して読み込む。なおリストが更新されたらhttpdプロセスをreload(あるいはgraceful)しなければ実際には反映されない。<sup id="fnref:4"><a href="http://multix.jp/delegated-apnic-acl/#fn:4" rel="footnote">4</a></sup></p>

<pre><code class="language-brush:plain">Order Allow,Deny  
Allow from localhost  
Allow from 192.168.0.0/16  
Include conf/jponly.conf  
Deny from All  
</code></pre>

<hr>

<h4 id="nginxacl">nginxでのACL</h4>

<p>nginxでACLを掛けるには、geoモジュールを使用する。まずApacheの場合と同様にInclude可能なファイルを作成する。</p>

<pre><code class="language-brush:bash">#!/bin/bash
JPONLY=/etc/rc.d/jponly.list

( cat $JPONLY | while read; do
  echo "$REPLY 2;"
done ) &gt; /etc/nginx/jponly.conf  
</code></pre>

<p>これをhttpブロックのgeoモジュールで読み込み、$acl変数に反映させる。これをlocationブロック内で参照して条件分岐する。なおリスト更新後はnginxプロセスをreloadしなければ実際には反映されない。</p>

<pre><code class="language-brush:plain">http {  
    geo $acl {
        default        0;
        127.0.0.1      1;
        192.168.0.0/16 1;
        include jponly.conf;
    }
    server {
        location /jponly/ {
            root /var/www/html_jponly;
            if ($acl = 0) {
                return 403;
            }
        }
        location / {
            root /var/www/html_public;
        }
    }
}
</code></pre>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>このリストにはIPv6も記載されているし、日本以外のアジア諸国もすべて入っている。特定の国のIPだけ弾きたい、といったニーズならばそれに応じたフィルタを書けば同じように対応できる。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>あるときA国で使われていたIPが、気がついたら何時の間にかB国に再割当てされていたということは割とある。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>差分更新処理は端折っているため、実行すると各IPブロックのカウンターはすべてゼロ初期化される。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>GeoIPモジュール <a href="http://dev.maxmind.com/geoip/">http://dev.maxmind.com/geoip/</a> を使ったほうが手間は少ないし処理も高速だし設定もスッキリするが、DBファイルをアップデートしたらサービスリロードが必要になるのは変わらない。 <a href="http://multix.jp/delegated-apnic-acl/#fnref:4" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[Ghost Blogにヘッドラインを追加する]]></title><description><![CDATA[<p>前項<a href="http://multix.jp/addon-markdown-table">Ghost Blogにテーブル表示を追加する</a>と似た話で、今度は各ポストにヘッドラインを付けてみる。これはそのポスト中の<code>&lt;h2〜6&gt;</code>タグの内容を拾ってリストにするものだ。</p>

<ul class="index"></ul>

<hr>

<h4 id="jqueryexmakeindexjs">jquery.ex-make-index.js</h4>

<p>このスクリプトはjQuery Extensionになっており、ロードが完了するとjQuery.makeIndex()メソッドが使用可能になる。なお上位Extensionとしてjquery-ui.jsを必要（依存）<sup id="fnref:1"><a href="http://multix.jp/insert-ghost-blog-headline/#fn:1" rel="footnote">1</a></sup>する。</p>

<pre><code class="language-brush:js collapse:true gutter:plain title:jquery.ex-make-index.js">//
// jquery.ex-make-index.js
//
// $Id: make-index.js 143 2015-05-08 09:31:06Z askn $
//
(function ($) {
    $.fn.makeIndex = function (config) {
        var serial = 0;
        var defaults = {
            search: 'section.post-content',
            footnotes:</code></pre>]]></description><link>http://multix.jp/insert-ghost-blog-headline/</link><guid isPermaLink="false">cc4700a7-f203-4f2c-9c0a-84f9c462d83d</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Ghost Blog]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 08 May 2015 09:45:22 GMT</pubDate><content:encoded><![CDATA[<p>前項<a href="http://multix.jp/addon-markdown-table">Ghost Blogにテーブル表示を追加する</a>と似た話で、今度は各ポストにヘッドラインを付けてみる。これはそのポスト中の<code>&lt;h2〜6&gt;</code>タグの内容を拾ってリストにするものだ。</p>

<ul class="index"></ul>

<hr>

<h4 id="jqueryexmakeindexjs">jquery.ex-make-index.js</h4>

<p>このスクリプトはjQuery Extensionになっており、ロードが完了するとjQuery.makeIndex()メソッドが使用可能になる。なお上位Extensionとしてjquery-ui.jsを必要（依存）<sup id="fnref:1"><a href="http://multix.jp/insert-ghost-blog-headline/#fn:1" rel="footnote">1</a></sup>する。</p>

<pre><code class="language-brush:js collapse:true gutter:plain title:jquery.ex-make-index.js">//
// jquery.ex-make-index.js
//
// $Id: make-index.js 143 2015-05-08 09:31:06Z askn $
//
(function ($) {
    $.fn.makeIndex = function (config) {
        var serial = 0;
        var defaults = {
            search: 'section.post-content',
            footnotes: 'div.footnotes',
            foottitle: 'Foot Notes',
        }
        var options = $.extend(defaults, config);
        this.each(function (i) {
            var $this = $(this);
            $(options.search).find("h2, h3, h4, h5, h6").each(function (i) {
                var current = serial++;
                var id = "mi_" + current;
                var $self = $(this);
                var title = $self.text();
                $("&lt;a/&gt;")
                    .attr({name:id})
                    .insertBefore($self);
                $("&lt;li/&gt;")
                    .append($("&lt;a/&gt;")
                        .attr({href:"#"+id})
                        .click(function () {
                            var href = $(this).attr("href").replace(/#/,"");
                            var bottom = $("a[name=" + href + "]").offset().top;
                            $('html,body').animate({scrollTop:bottom}, 1000, "easeOutExpo");
                            return false;
                        }).text(title)
                ).appendTo($this);
            });
        });
        return this.each(function (i) {
            var $this = $(this);
            var current = serial++;
            var id = "mi_" + current;
            $("&lt;a/&gt;")
                .attr({name:id})
                .insertBefore($(options.footnotes));
            $("&lt;li/&gt;")
                .append(
                    $("&lt;a/&gt;")
                        .attr({href:"#"+id})
                        .click(function () {
                            var href = $(this).attr("href").replace(/#/,"");
                            var bottom = $("a[name=" + href + "]").offset().top;
                            $('html,body').animate({scrollTop:bottom}, 1000, "easeOutExpo");
                            return false;
                        }).text(options.foottitle)
                ).appendTo($this);
        });
    };
})(jQuery);

// End of Script
</code></pre>

<p>これをindex.jsから次のようにして呼び出す。searchオプションで検索対象のセレクタを指定し、その中で見つかる<code>&lt;h2〜6&gt;</code>のアンカーを作成する。<code>&lt;h1&gt;</code>はポストヘッダなので収集しない。またfootnotesオプションで追加のアンカーをひとつ与えることが出来る。またアンカーはアニメーションスクロールによる画面切り替えでひと呼吸いれ、目で置いやすいようにした。</p>

<pre><code class="language-brush:js">    $("ul.index").makeIndex({
        search: 'section.post-content',
        footnotes: 'div.footnotes',
        foottitle: '注釈',
    });
</code></pre>

<p>こうして作成したアンカーリストは、ポスト中の次の記述の箇所に流し込まれる。</p>

<pre><code class="language-brush:html">  &lt;ul class="index"&gt;&lt;/ul&gt;
</code></pre>

<p>縦方向のスクロール量が数画面分だったりセクション数が数えるほどならこういう機能は必要ないだろうし、長くなるようならポストを分割したほうが良い場合が多い。だが記事構成上どうしてもひとつのポストに押し込みたい場合はこういうExtensionがあったほうがユーザビリティを補えるだろう。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>依存すると言っても animateメソッドのオプションだけなので、ここを修正すればjQuery UIがなくても問題はない。 <a href="http://multix.jp/insert-ghost-blog-headline/#fnref:1" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[Ghost Blogにテーブル表示を追加する]]></title><description><![CDATA[<p>Ghost Blog は MarkdownレンダリングにShowdownモジュールを利用しているが、何故かそれに含まれるtable拡張を利用していない。幾度か要望がでたり議論もあったりしたのだが、一向に進展しないためテーマレベルで代替策を講じることにしてみた。</p>

<hr>

<h4 id="jqueryexmarktablejs">jquery.ex-mark-table.js</h4>

<p>このスクリプトはjQuery Extensionになっていて、ロードすると jQuery.markTable() メソッドが追加される。これはMarkdown形式のtable書式を含むオブジェクトを渡してやるとHTML-Tableにオンデマンドで整形する。</p>

<pre><code class="language-brush:js gutter:true collapse:true title:jquery.ex-mark-table.js">//
// jquery.ex-mark-table.js
//
// $Id: mark-table.js 143 2015-05-08 09:31:06Z askn $
//
(function ($) {
    $.fn.markTable = function (config) {
        var defaults = {
            className: 'mark-table'
        }
        var options = $.extend(defaults, config);
        return this.each( function (i) {
            var title</code></pre>]]></description><link>http://multix.jp/addon-markdown-table/</link><guid isPermaLink="false">b87a82f7-96fe-47a1-a56c-0e2449efd870</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Ghost Blog]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 08 May 2015 05:34:15 GMT</pubDate><content:encoded><![CDATA[<p>Ghost Blog は MarkdownレンダリングにShowdownモジュールを利用しているが、何故かそれに含まれるtable拡張を利用していない。幾度か要望がでたり議論もあったりしたのだが、一向に進展しないためテーマレベルで代替策を講じることにしてみた。</p>

<hr>

<h4 id="jqueryexmarktablejs">jquery.ex-mark-table.js</h4>

<p>このスクリプトはjQuery Extensionになっていて、ロードすると jQuery.markTable() メソッドが追加される。これはMarkdown形式のtable書式を含むオブジェクトを渡してやるとHTML-Tableにオンデマンドで整形する。</p>

<pre><code class="language-brush:js gutter:true collapse:true title:jquery.ex-mark-table.js">//
// jquery.ex-mark-table.js
//
// $Id: mark-table.js 143 2015-05-08 09:31:06Z askn $
//
(function ($) {
    $.fn.markTable = function (config) {
        var defaults = {
            className: 'mark-table'
        }
        var options = $.extend(defaults, config);
        return this.each( function (i) {
            var title = $(this).attr("title");
            var $exported = $("&lt;table/&gt;");
            var $body = $("&lt;tbody/&gt;");
            var table = [], align = [], header = [];
            var imported = $(this).html().split(/([^\n]*\n)/);
            while (imported.length) {
                var line = imported.shift();
                if (line) {
                    if (line.length == 0) continue;
                    var column = line.match(/([^\|]*\|)/g);
                    if (column &amp;&amp; column.length &gt; 0) {
                        column.shift();
                        table.push($.map(column, function (val, index) {
                            return val.replace(/\|$/, "");
                        }));
                    }
                }
            }
            if (title != undefined) {
                $("&lt;caption/&gt;").text(title).appendTo($exported);
            }
            if (table.length &amp;&amp; table[0].length &amp;&amp; !table[0][0].match(/(^\:-|-\:$)/)) {
                header = table.shift();
            }
            if (table.length &amp;&amp; table[0].length &amp;&amp; table[0][0].match(/(^\:-|-\:$)/)) {
                align = $.map(table.shift(), function (val, index) {
                    if (val.match(/^\:\-*\:$/)) {
                        return "center";
                    }
                    else if (val.match(/^\-*\:$/)) {
                        return "right";
                    }
                    else {
                        return "left";
                    }
                });
            }
            if (header.length) {
                var $head = $("&lt;tr/&gt;");
                $.each(header, function (key, val) {
                    var $column = $("&lt;th/&gt;").html(val);
                    if (align[key]) {
                        $column.addClass(align[key]);
                    }
                    $head.append($column);
                });
                $("&lt;thead/&gt;").append($head).appendTo($exported);
            }
            if (table.length) {
                while (table.length) {
                    var $line = $("&lt;tr/&gt;");
                    $.each(table.shift(), function (key, val) {
                        var $column = $("&lt;td/&gt;").html(val);
                        if (align[key]) {
                            $column.addClass(align[key]);
                        }
                        $line.append($column);
                    });
                    $line.appendTo($body);
                }
                $body.appendTo($exported);
            }
            $(this).replaceWith($("&lt;div/&gt;").addClass(options.className).append($exported));
        });
    };
})(jQuery);

// End of Script
</code></pre>

<p>これをさらに次のようにしてindex.js等から呼び出す。<sup id="fnref:1"><a href="http://multix.jp/addon-markdown-table/#fn:1" rel="footnote">1</a></sup></p>

<pre><code class="language-brush:js title:呼び出しサンプル">(function pageInit () {
    $('code[class^="language-"]').each(function () {
        var $this = $(this);
        var attr = $this.attr('class').replace(/^language-?/, '');
        var match = attr.match(/title:(.*)/);
        if (match) {
            $this.parent().attr('title', match[1]);
            attr = attr.replace(/title:.*/, '');
        }
        if (attr != null &amp;&amp; attr.match(/^table/)) {
            // マークダウンテーブル
            $this.parent().html($this.html()).markTable();
        }
    });
})();
</code></pre>

<p>Markdownの方では、ネイティブにMarkdownテーブル書式を書くことが出来ないので、全体をコード書式で括るようにする。このとき三連バッククォート<sup id="fnref:2"><a href="http://multix.jp/addon-markdown-table/#fn:2" rel="footnote">2</a></sup>の直後に<mark>table</mark>というキーワードを付与する。更に<mark>taitle:</mark>キーワードを足すとここから行末までをテーブルキャプションにすることが出来る。<sup id="fnref:3"><a href="http://multix.jp/addon-markdown-table/#fn:3" rel="footnote">3</a></sup></p>

<pre><code class="language-brush:plain">  ```table title:テーブルキャプション
  |foo |bar   |baz  |
  |:---|:----:|----:|
  |Left|center|right|
  ```
</code></pre>

<p>これを保存するとGhostは次のHTMLコードに変換する。</p>

<pre><code class="language-brush:plain">  

{gfm-js-extract-pre-1}
</code></pre>

<p>付与したキーワードに<mark>language-</mark>前置詞が付いたclass名として埋め込まれるのがミソで、ようはこれをjQueryで検出してクライアントサイドでpreをまるごとtableセットに書き換えてやる。あとはCSSで表示を整形<sup id="fnref:4"><a href="http://multix.jp/addon-markdown-table/#fn:4" rel="footnote">4</a></sup>してやればよい。</p>

<pre><code class="language-table title:テーブルキャプション">|foo |bar   |baz  |
|:---|:----:|----:|
|Left|center|right|
</code></pre>

<p>これの実際のHTMLはつぎのように展開されている。</p>

<pre><code class="language-brush:html">  &lt;div class="mark-table"&gt;
    &lt;table&gt;
      &lt;caption&gt;テーブルキャプション&lt;/caption&gt;
      &lt;thead&gt;
        &lt;tr&gt;
          &lt;th class="left"&gt;foo &lt;/th&gt;
          &lt;th class="center"&gt;bar   &lt;/th&gt;
          &lt;th class="right"&gt;baz  &lt;/th&gt;
        &lt;/tr&gt;
      &lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td class="left"&gt;Left&lt;/td&gt;
          &lt;td class="center"&gt;center&lt;/td&gt;
          &lt;td class="right"&gt;right&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
</code></pre>

<p>今後ネイティブにMarkdownテーブル記法が実装された場合は（本実装との互換性はともかく）三連バッククォートを除去するだけで対応できるはずだ。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>実際のコードではここで同時に SyntaxHighliter の変換処理も行っている。 <a href="http://multix.jp/addon-markdown-table/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>プレビュー内容が改行なしで詰め込み表示になるのを避けるため、コードブロックで括るようにした。現状、これ以外の方法で改行をプレビューに反映させることはできない。 <a href="http://multix.jp/addon-markdown-table/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>class名としてHTMLに埋め込まれる都合上、titleにダブルクォートは記述できない。 <a href="http://multix.jp/addon-markdown-table/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>markTable()のオプションにtableタグのclassNameを指定でき、省略値は<em>table.mark-table</em>になっている。 <a href="http://multix.jp/addon-markdown-table/#fnref:4" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[excerptヘルパーにparagraphを追加する]]></title><description><![CDATA[<p>Ghostのトップページには記事抜粋が並ぶが、本文を要約表示するexcerptヘルパーに備わる<mark>words</mark>と<mark>characters</mark>のどちらのアルゴリズムも日本語相手には適切に機能しないので、段落抽出ができる機能を追加してみた。</p>

<hr>

<h3 id="excerptpatch">excerpt.patch</h3>

<p>excerptヘルパーが提供するwordsは英単語数を数え、charactersは文字数を数えているようなのだが、何れも半角文字を含まないベタな日本語に適用すると処理結果が安定しない<sup id="fnref:1"><a href="http://multix.jp/excerpt-paragraph-patch/#fn:1" rel="footnote">1</a></sup>。頑張って修正するにも元がそれでは手間が見合わないので、新たに段落を切り出す機能を追加することにしてみた。これなら<strong>n回目の空行までが要約文になる</strong>という見た目で効果範囲が理解しやすい記述ルールも作れることになる。</p>

<p>このpatchがやってることはtruncateOptionsにparagraphオプションを追加し、その指定数だけ<code>&lt;p&gt;</code>または<code>&lt;h1〜6&gt;</code>タグを持つ地の文を抽出して返すようにしているだけだ。</p>

<pre><code class="language-brush:plain gutter:true title:excerpt.patch">--- Ghost-0.6.2/core/server/helpers/excerpt.js    Tue Feb 17 18:07:11 2015
+++ GhostBlog/core/server/helpers/excerpt.</code></pre>]]></description><link>http://multix.jp/excerpt-paragraph-patch/</link><guid isPermaLink="false">bd207c22-7dd0-486a-9749-3dd47c02b15a</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Ghost Blog]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Fri, 08 May 2015 02:47:09 GMT</pubDate><content:encoded><![CDATA[<p>Ghostのトップページには記事抜粋が並ぶが、本文を要約表示するexcerptヘルパーに備わる<mark>words</mark>と<mark>characters</mark>のどちらのアルゴリズムも日本語相手には適切に機能しないので、段落抽出ができる機能を追加してみた。</p>

<hr>

<h3 id="excerptpatch">excerpt.patch</h3>

<p>excerptヘルパーが提供するwordsは英単語数を数え、charactersは文字数を数えているようなのだが、何れも半角文字を含まないベタな日本語に適用すると処理結果が安定しない<sup id="fnref:1"><a href="http://multix.jp/excerpt-paragraph-patch/#fn:1" rel="footnote">1</a></sup>。頑張って修正するにも元がそれでは手間が見合わないので、新たに段落を切り出す機能を追加することにしてみた。これなら<strong>n回目の空行までが要約文になる</strong>という見た目で効果範囲が理解しやすい記述ルールも作れることになる。</p>

<p>このpatchがやってることはtruncateOptionsにparagraphオプションを追加し、その指定数だけ<code>&lt;p&gt;</code>または<code>&lt;h1〜6&gt;</code>タグを持つ地の文を抽出して返すようにしているだけだ。</p>

<pre><code class="language-brush:plain gutter:true title:excerpt.patch">--- Ghost-0.6.2/core/server/helpers/excerpt.js    Tue Feb 17 18:07:11 2015
+++ GhostBlog/core/server/helpers/excerpt.js    Fri Apr 24 11:26:53 2015
@@ -14,13 +14,24 @@
     var truncateOptions = (options || {}).hash || {},
         excerpt;

-    truncateOptions = _.pick(truncateOptions, ['words', 'characters']);
+    truncateOptions = _.pick(truncateOptions, ['words', 'characters', 'paragraph']);
     _.keys(truncateOptions).map(function (key) {
         truncateOptions[key] = parseInt(truncateOptions[key], 10);
     });

     /*jslint regexp:true */
     excerpt = String(this.html);
+
+    if (truncateOptions.paragraph) {
+        var paragraph = excerpt.match(/(.*?&lt;\/(p|h[1-6])&gt;)/ig);
+        if (paragraph) {
+            excerpt = '';
+            for (var i = 0; i &lt; truncateOptions.paragraph; i++) {
+                if (paragraph.length) excerpt = paragraph.shift();
+            }
+        }
+    }
+
     // Strip inline and bottom footnotes
     excerpt = excerpt.replace(/&lt;a href="#fn.*?rel="footnote"&gt;.*?&lt;\/a&gt;/gi, '');
     excerpt = excerpt.replace(/&lt;div class="footnotes"&gt;&lt;ol&gt;.*?&lt;\/ol&gt;&lt;\/div&gt;/, '');
</code></pre>

<p>こうすると<em>themes/&lt;THEME&gt;/loop.hbs</em>中の<code>{{excerpt}}</code>で抽出パラグラフ数を指定できるようになる。処理的にはまずparagraphで本文を切り出したあとにwords/charactersの処理に入るので、出力結果が不用意に長くなりすぎることもない。</p>

<pre><code class="language-brush:plain">{{excerpt words="50" characters="80" paragraph="1"}}
</code></pre>

<p>この実装のミソはGhostが使っているMarkdown実装（Showdown）が処理したセンテンスが<code>&lt;p&gt;&lt;pre&gt;&lt;hr&gt;&lt;h1-6&gt;</code>の何れかで必ず括られていることを利用している。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>UTF文字列もバイナリとみなしその中に現れる7bit文字だけを数えているかのような処理結果になることがある。 <a href="http://multix.jp/excerpt-paragraph-patch/#fnref:1" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[ModefierKeyを取得する]]></title><description><![CDATA[<p><a href="https://ghost.multix.jp/dmm-playing-viewer/">艦板-KanPan-</a>では起動時にShiftキー/Ctrlキーの押し下げ状態を見てキャッシュやCookieを消去しつつ起動できたりする。この動作はもともとOptionキーを押しながらクリックしたり実行したりすると振る舞いが変わるiTunesやAirMac Utilityに触発された小技だ・・・が、ではどうやってこれらのModefierKey状態を取得する？</p>

<ul class="index"></ul>

<hr>

<h4 id="osx">OSXの場合</h4>

<p>OSXの場合、AppKit/Frameworkの<code>NSEvent-&gt;modifierFlags</code>を利用することでModefierKeyのビットフラグ値<sup id="fnref:1"><a href="http://multix.jp/get-modefierkey-propety/#fn:1" rel="footnote">1</a></sup>を取得することが出来る。OSX付属のPerlやPythonにはAppKitのバインディングが標準でインストールされているため、例えばPerlでは以下のようにすればビットフラグ値を標準出力へ返すことが出来る。コメント他を除けば実質5行だ。</p>

<pre><code class="language-brush:perl gutter:true title:modefierkey.pl">#!/usr/bin/perl

=comment=
enum {  
   NSAlphaShiftKeyMask  = 1 &lt;&lt; 16,
   NSShiftKeyMask       = 1 &lt;&lt; 17,
   NSControlKeyMask     = 1 &lt;&lt; 18,
   NSAlternateKeyMask   = 1 &lt;&lt; 19,
   NSCommandKeyMask     = 1 &lt;&lt; 20,</code></pre>]]></description><link>http://multix.jp/get-modefierkey-propety/</link><guid isPermaLink="false">9097cc8e-f724-4a61-8eae-f8c98cafd3f7</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[Windows]]></category><category><![CDATA[Machintosh]]></category><category><![CDATA[Perl]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Thu, 07 May 2015 08:17:04 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://ghost.multix.jp/dmm-playing-viewer/">艦板-KanPan-</a>では起動時にShiftキー/Ctrlキーの押し下げ状態を見てキャッシュやCookieを消去しつつ起動できたりする。この動作はもともとOptionキーを押しながらクリックしたり実行したりすると振る舞いが変わるiTunesやAirMac Utilityに触発された小技だ・・・が、ではどうやってこれらのModefierKey状態を取得する？</p>

<ul class="index"></ul>

<hr>

<h4 id="osx">OSXの場合</h4>

<p>OSXの場合、AppKit/Frameworkの<code>NSEvent-&gt;modifierFlags</code>を利用することでModefierKeyのビットフラグ値<sup id="fnref:1"><a href="http://multix.jp/get-modefierkey-propety/#fn:1" rel="footnote">1</a></sup>を取得することが出来る。OSX付属のPerlやPythonにはAppKitのバインディングが標準でインストールされているため、例えばPerlでは以下のようにすればビットフラグ値を標準出力へ返すことが出来る。コメント他を除けば実質5行だ。</p>

<pre><code class="language-brush:perl gutter:true title:modefierkey.pl">#!/usr/bin/perl

=comment=
enum {  
   NSAlphaShiftKeyMask  = 1 &lt;&lt; 16,
   NSShiftKeyMask       = 1 &lt;&lt; 17,
   NSControlKeyMask     = 1 &lt;&lt; 18,
   NSAlternateKeyMask   = 1 &lt;&lt; 19,
   NSCommandKeyMask     = 1 &lt;&lt; 20,
   NSNumericPadKeyMask  = 1 &lt;&lt; 21,
   NSHelpKeyMask        = 1 &lt;&lt; 22,
   NSFunctionKeyMask    = 1 &lt;&lt; 23,
   NSDeviceIndependentModifierFlagsMask  = 0xffff0000U 
};
=cut=

use strict;  
use warnings;  
use Foundation;

@NSEvent::ISA = qw(PerlObjCBridge);

my $AppKit = '/System/Library/Frameworks/AppKit.framework';  
NSBundle-&gt;bundleWithPath_($AppKit)-&gt;load;

print +(NSEvent-&gt;modifierFlags);  
exit 0;

__END__  
</code></pre>

<p>Bashスクリプト等からModefierKey状態を扱いたい場合はこのスクリプトコマンドを呼び出せば充分用が足りる。</p>

<pre><code class="language-brush:bash title:ビット列を16進数表示する">#/bin/bash
printf '%x¥n' $(modefierkey.pl)  
</code></pre>

<pre><code class="language-brush:bash title:ビットを判定する">#/bin/bash
(($(modefierkey.pl) &amp;&amp; 2**17)) &amp;&amp; echo ShiftDown || echo ShiftUp
</code></pre>

<hr>

<h4 id="windows">Windowsの場合</h4>

<p>前述のコードを Windows Visual Basic<sup id="fnref:2"><a href="http://multix.jp/get-modefierkey-propety/#fn:2" rel="footnote">2</a></sup> に転写すると次のようになる。OSXで言うcommandキー、fnキーに相当するものはないが、代わりにScrollLockキーを取得することが出来る。</p>

<pre><code class="language-brush:vb gutter:true title:modefierkey.vb">Module Module1

    Sub Main()

        'OSX Cocoa
        'enum {
        '   NSAlphaShiftKeyMask  = 1 &lt;&lt; 16,
        '   NSShiftKeyMask       = 1 &lt;&lt; 17,
        '   NSControlKeyMask     = 1 &lt;&lt; 18,
        '   NSAlternateKeyMask   = 1 &lt;&lt; 19,
        '   NSCommandKeyMask     = 1 &lt;&lt; 20,
        '   NSNumericPadKeyMask  = 1 &lt;&lt; 21,
        '   NSHelpKeyMask        = 1 &lt;&lt; 22,
        '   NSFunctionKeyMask    = 1 &lt;&lt; 23,
        '   NSDeviceIndependentModifierFlagsMask  = 0xffff0000U 
        '};

        Dim intMask As Integer = 0

        If My.Computer.Keyboard.CapsLock Then
            intMask += 1 &lt;&lt; 16
        End If
        If My.Computer.Keyboard.ShiftKeyDown Then
            intMask += 1 &lt;&lt; 17
        End If
        If My.Computer.Keyboard.CtrlKeyDown Then
            intMask += 1 &lt;&lt; 18
        End If
        If My.Computer.Keyboard.AltKeyDown Then
            intMask += 1 &lt;&lt; 19
        End If
        If My.Computer.Keyboard.NumLock Then
            intMask += 1 &lt;&lt; 21
        End If
        If My.Computer.Keyboard.ScrollLock Then
            intMask += 1 &lt;&lt; 24
        End If

        Console.Write(intMask)
    End Sub

End Module  
</code></pre>

<hr>

<h4 id="nwjs">Nw.jsの場合</h4>

<p>そもそもコンソールプラットフォームであるNode.jsではModefierKeyを取得する手段は用意されていない<sup id="fnref:3"><a href="http://multix.jp/get-modefierkey-propety/#fn:3" rel="footnote">3</a></sup>が、Nw.jsではGUIウィンドウが存在するのでそちらのwindow.eventからAlt/Ctrl/Shiftの3種類のキー状態真偽値を取得することが出来る。ChromiumベースのGUIプラットフォームなのだからこれは出来て当然だ。</p>

<pre><code class="language-brush:javascript">&amp;lt;script language="javascript"&amp;gt;
switch (true) {  
  case window.event.altKey   : console.log('altKey');   break;
  case window.event.ctrlKey  : console.log('ctrlKey');  break;
  case window.event.shiftKey : console.log('shiftKey'); break;
}
&amp;lt;/script&amp;gt;
</code></pre>

<p>ただし注意が必要なのは、ウィンドウ本体を示すwindowグローバルオブジェクトはGUI内の<code>&lt;script&gt;</code>タグでロードされたコードのほうにしか継承されていない点<sup id="fnref:4"><a href="http://multix.jp/get-modefierkey-propety/#fn:4" rel="footnote">4</a></sup>だ。つまりpackage.jsonの<code>node-main</code>でバックグラウンドロードされたNode.jsネイティブなコードの方からは直接使えない。また当然のことながらウィンドウ表示前・初期化前にはこれらのキー状態を取得出来ないという制約もある。故に「Altキーを押しながらドロップダウンメニューを開いた場合は内容をかえる」といった使いドコロになる。</p>

<hr>

<h4 id="">艦板ではどうしたか</h4>

<p>艦板では結局GUI初期化前にModefierKey状態をしたかったので、外部コマンドを <code>child_process.execSync()</code> で呼び出して結果を得るようになっている。ちなみにこの時の動作はWindows版の場合；</p>

<ul>
<li>ModefierKeyを調べて押されていなければそのまま起動継続する</li>
<li>ModefierKeyが押されていた場合でも、キャッシュクリア済であったならそのまま起動継続する</li>
<li>キャッシュクリアされていなかったら外部キャッシュクリアコードにexecして自分は終了する</li>
<li>外部キャッシュクリアコード<sup id="fnref:5"><a href="http://multix.jp/get-modefierkey-propety/#fn:5" rel="footnote">5</a></sup>は自分でModefierKeyを再確認し、キャッシュフォルダを掃除したあとに艦板をexecして自身は終了する</li>
</ul>

<p>・・・などと存外面倒なことをやっている。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>キーとビット値の対応とcocoaでの名称はコメントを参照して頂きたいが、AlphaShiftKeyはcaps/CapsLockキー、AlternateKeyはalt/option/appleキーのことである。 <a href="http://multix.jp/get-modefierkey-propety/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>要コンパイル。VS Express版だと for Desktopエディションが必要。 <a href="http://multix.jp/get-modefierkey-propety/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>プラットフォーム別に全く異なる実装をビルドする必要があるし、そもそもNode.jsの主戦場はCLIやサーバホスティング用途なので、ModefierKeyを見る要件は殆ど無い。なのでこれが必要になるのはNw.jsのようなGUIフレームワークと組み合わせた場合に概ね限られるといえる。 <a href="http://multix.jp/get-modefierkey-propety/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>親ウィンドウが生成される前から動き出すインスタンスなので継承しようがないとも言える。だが単に継承されていないから使えないだけなので、知ってるほう(GUI)が知らないほう(Native)に module.exports 経由で教えてやれば利用できる。同様にNw.jsのguiオブジェクトも教えてやればバックグラウンド側から各種ウィンドウ操作ができる。 <a href="http://multix.jp/get-modefierkey-propety/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p>Windows版同梱のWinRapper.exeがそれ。OSXのほうは Info.plistに登録された Bashシェル (launch.sh) が modefierkey.pl を呼び出し、必要ならキャッシュ削除処理をしてから Nw.js を起動する段取りだからずっとシンプルだ。 <a href="http://multix.jp/get-modefierkey-propety/#fnref:5" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item><item><title><![CDATA[てくにかるむへようこそ]]></title><description><![CDATA[<hr>

<h4 id="">更新履歴はこちら</h4>

<ul>
<li>2015/11/20 艦版とそのバリアントをアップデート Build 10054</li>
<li>2015/11/09 UWSCヘルプHTMLアップデート (5.2.0)</li>
<li>2015/07/03 ヘッドレスLinux（シリアルコンソール）関係の記事を追加</li>
<li>2015/05/25 艦版とそのバリアントをアップデート Build 10053</li>
<li>2015/05/14 艦版とそのバリアントをアップデート Build 10052</li>
<li>2015/05/13 各ポストのSyntaxHighlighterの実体参照エスケープ不具合を修正</li>
<li>2015/04/28 艦版とそのバリアントをアップデート Build 10051</li>
<li>2015/04/24 UWSCヘルプHTMLアップデート (5.1.1)</li>
<li>2015/</li></ul>]]></description><link>http://multix.jp/technicalroom/</link><guid isPermaLink="false">be02a7eb-c2c3-4cb2-ac58-e7b2b9f2adfd</guid><category><![CDATA[てくにかるむ]]></category><category><![CDATA[おしらせ]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Sun, 15 Feb 2015 06:41:00 GMT</pubDate><content:encoded><![CDATA[<hr>

<h4 id="">更新履歴はこちら</h4>

<ul>
<li>2015/11/20 艦版とそのバリアントをアップデート Build 10054</li>
<li>2015/11/09 UWSCヘルプHTMLアップデート (5.2.0)</li>
<li>2015/07/03 ヘッドレスLinux（シリアルコンソール）関係の記事を追加</li>
<li>2015/05/25 艦版とそのバリアントをアップデート Build 10053</li>
<li>2015/05/14 艦版とそのバリアントをアップデート Build 10052</li>
<li>2015/05/13 各ポストのSyntaxHighlighterの実体参照エスケープ不具合を修正</li>
<li>2015/04/28 艦版とそのバリアントをアップデート Build 10051</li>
<li>2015/04/24 UWSCヘルプHTMLアップデート (5.1.1)</li>
<li>2015/04/02 艦版とそのバリアントをアップデート Build 10050</li>
<li>2015/03/16 艦版とそのバリアントをアップデート Build 10049</li>
<li>2015/03/08 艦版とそのバリアントをアップデート Build 10048</li>
</ul>

<hr>

<h4 id="">このサイトについて</h4>

<p>掲載の記事やリソース・サンプルの扱いについてはすべて自己責任でお願い致します。その使用・引用・転載・流用・改造は自由ですが、それによって生じる利益・不利益・トラブルについて一切の責務は負えません。サポートも一切行いません。何かありましても自力で解決できる地力と鑑定眼・審美眼・忍耐が必要です。ご意見ご要望は随時受付こそしますが、お応えするとは限りません。たいていは無視されるでしょう。気が向けば多少は助言することもありますがコストに見合わぬことは致しかねます。</p>

<hr>

<h4 id="">サイドバー</h4>

<p>右端の緑のタブ当たりにマウスカーソルを持ってくかタッチするとドロワーが開きます。直近の更新記事一覧、タグ一覧、上下前後の表示移動ボタン、サイト内検索窓、ページ共有ボタンがあります。</p>

<hr>

<h4 id="">座右の銘</h4>

<p>サイトロゴの英文は日本語に訳すと；</p>

<blockquote>
  <p>エラーをなくすことは非常に有益で時には新しい真実や事実を作り上げるよりも勝る</p>
  
  <p>ー チャールズ・ダーウィン</p>
</blockquote>

<hr>

<h4 id="">リンクバナー</h4>

<p>いまでは絶滅種のように思いますが200x40px規格のリンクバナーを用意しています。ご利用の際は直リンクでもコピーでも構いません。背景が半透明なので割と何処にでも溶け込みます。</p>

<p><img src="http://multix.jp/banner.png" alt="http://multix.jp/banner.png"></p>

<p>なお旧サイトのバナーも直リンク維持のために現存しておりますが新たにリンクを張る場合の新規利用は遠慮願います。</p>

<p><img src="http://multix.jp/banner.jpg" alt="http://multix.jp/banner.jpg">
<img src="http://multix.jp/mtbanner.gif" alt="http://multix.jp/mtbanner.gif">
<img src="http://multix.jp/multix.gif" alt="http://multix.jp/multix.gif"></p>

<hr>]]></content:encoded></item></channel></rss>