<?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>Tue, 07 Apr 2026 01:25:17 GMT</lastBuildDate><atom:link href="http://multix.jp/tag/ghost-blog/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><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[Node.jsのサービス実行]]></title><description><![CDATA[<p>このブログはNode.jsでGhost BlogをCentOSやWindows上で動かしているが、開発環境はともかく本番用やステージング用はサーバ起動時に一緒にデーモン起動していて欲しい。ところがコレには色々と考えるべきところがあるのだ。</p>

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

<hr>

<h4 id="">デーモンプロセス化を助けるツール</h4>

<p>Node.jsのデーモン化実装にはいくつかあるが、以下が代表的なものだろう。</p>

<ul>
<li>supervisor</li>
<li>forever</li>
<li>pm2 </li>
</ul>

<p>いずれも<code>npm install -g</code>で導入できる。おまけにLinuxでもWindowsでもそれなりに動く。それぞれに特徴がありセールスポイントがあり使い勝手も異なるが、一番リッチなのがpm2で、一番シンプルなのがforeverだろうか。</p>

<p>だがこれらはどういうワケか<strong>suid/sugidをサポートしてない</strong>のである。いやNode.js自体にはPOSIX準拠の<code>process.setuid</code>と<code>process.setgid</code>が一応実装されているのだがそれをこれらで使っているのを見たことがない<sup id="fnref:1"><a href="http://multix.jp/daemon-nodejs/#fn:1" rel="footnote">1</a></sup>。なので上記のこれらを普通にroot権限で起動するとそのままroot権限プロセスで動き続けることになり、実行権限を落として運用したい場合は別の工夫が必要になる。</p>

<hr>

<h4 id="">手っ取り早くデーモンにしてしまう</h4>

<p>WindowsにはWindowsの事情があるから置いておくとして、まずCentOS6ではどうするか。6のinitプロセスにはupstartとsysvinitの2種類<sup id="fnref:2"><a href="http://multix.jp/daemon-nodejs/#fn:2" rel="footnote">2</a></sup>が使われており、特段の事情がなければ使い慣れた後者に従うのが普通だろう。とりあえずroot権限でGhost（に限らずNode.jsで実装した各種サービス）を手っ取り早く動かしてしまうには以下のようにする。なおここではforeverを使用する。また一連の作業はすべてrootユーザで行う。</p>

<p>まずyumでredhat-lsb-coreを導入する。</p>]]></description><link>http://multix.jp/daemon-nodejs/</link><guid isPermaLink="false">2207ab43-c467-411c-b83f-c73635252b8f</guid><category><![CDATA[Linux]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Ghost Blog]]></category><dc:creator><![CDATA[朝日薫]]></dc:creator><pubDate>Thu, 07 May 2015 08:17:54 GMT</pubDate><content:encoded><![CDATA[<p>このブログはNode.jsでGhost BlogをCentOSやWindows上で動かしているが、開発環境はともかく本番用やステージング用はサーバ起動時に一緒にデーモン起動していて欲しい。ところがコレには色々と考えるべきところがあるのだ。</p>

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

<hr>

<h4 id="">デーモンプロセス化を助けるツール</h4>

<p>Node.jsのデーモン化実装にはいくつかあるが、以下が代表的なものだろう。</p>

<ul>
<li>supervisor</li>
<li>forever</li>
<li>pm2 </li>
</ul>

<p>いずれも<code>npm install -g</code>で導入できる。おまけにLinuxでもWindowsでもそれなりに動く。それぞれに特徴がありセールスポイントがあり使い勝手も異なるが、一番リッチなのがpm2で、一番シンプルなのがforeverだろうか。</p>

<p>だがこれらはどういうワケか<strong>suid/sugidをサポートしてない</strong>のである。いやNode.js自体にはPOSIX準拠の<code>process.setuid</code>と<code>process.setgid</code>が一応実装されているのだがそれをこれらで使っているのを見たことがない<sup id="fnref:1"><a href="http://multix.jp/daemon-nodejs/#fn:1" rel="footnote">1</a></sup>。なので上記のこれらを普通にroot権限で起動するとそのままroot権限プロセスで動き続けることになり、実行権限を落として運用したい場合は別の工夫が必要になる。</p>

<hr>

<h4 id="">手っ取り早くデーモンにしてしまう</h4>

<p>WindowsにはWindowsの事情があるから置いておくとして、まずCentOS6ではどうするか。6のinitプロセスにはupstartとsysvinitの2種類<sup id="fnref:2"><a href="http://multix.jp/daemon-nodejs/#fn:2" rel="footnote">2</a></sup>が使われており、特段の事情がなければ使い慣れた後者に従うのが普通だろう。とりあえずroot権限でGhost（に限らずNode.jsで実装した各種サービス）を手っ取り早く動かしてしまうには以下のようにする。なおここではforeverを使用する。また一連の作業はすべてrootユーザで行う。</p>

<p>まずyumでredhat-lsb-coreを導入する。そしてforeverとinitd-foreverをnpmで導入する。</p>

<pre><code class="language-brush:plain">$ sudo -s
# yum install redhat-lsb-core
# npm install forever -g
# npm install initd-forever -g

# initd-forever --help

  Usage: initd-forever [options]

  Options:

    -h, --help             output usage information
    -V, --version          output the version number
    -a, --app [path]       Path to node.js main file
    -c, --command [value]  Command to execute on main file
    -e, --env [value]      Export NODE_ENV with value
    -l, --logfile [path]   Logs the daemon output to LOGFILE
    -n, --name [value]     Application name
    -p, --pidfile [path]   The pid file
    -m, --monit [value]    Generate the monit script file with the listen port number
    -f, --forever [value]  The location of forever
</code></pre>

<p>次いでGhostディレクトリに移動してinitd-foreverでinitdスクリプトを生成する。最低限出力ファイル名＝サービス名を決定する<strong>-n</strong>指定は必要だ。</p>

<pre><code class="language-brush:plain"># cd ~user/ghost
# initd-forever -n ghost 
Script daemon file saved to ghost  
</code></pre>

<p>出来上がったファイルに実行権限を付与して<code>/etc/init.d</code>へ移動し、chkconfigで使用可能にしてserviceで起動する。</p>

<pre><code class="language-brush:plain"># chmod +x ghost
# mv ghost /etc/init.d
# chkconfig ghost --add
# chkconfig ghost on
# chkconfig ghost --list
ghost              0:off   1:off   2:on    3:on    4:on    5:on    6:off  
# service ghost start
</code></pre>

<p>initd-foreverが生成したファイルは次のようなものだ。通常の用途ではまず修正するところはないだろう。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/init.d/ghost">#!/bin/bash
### BEGIN INIT INFO
# Provides:          /home/user/ghost/core/index
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: forever running /home/user/ghost/core/index
# Description:       /home/user/ghost/core/index
### END INIT INFO
#
# initd a node app
# Based on a script posted by https://gist.github.com/jinze at https://gist.github.com/3748766
#

# Source function library.
. /lib/lsb/init-functions

pidFile="/var/run/ghost.pid"  
logFile="/var/run/ghost.log"

command="node"  
nodeApp="/home/user/ghost/core/index"  
foreverApp="forever"

start() {  
    echo "Starting $nodeApp"

    # Notice that we change the PATH because on reboot
    # the PATH does not include the path to node.
    # Launching forever with a full path
    # does not work unless we set the PATH.
    PATH=/usr/local/bin:$PATH
    export NODE_ENV=production
    #PORT=80
    $foreverApp start --pidFile $pidFile -l $logFile -a -d -c "$command" $nodeApp
    RETVAL=$?
}

restart() {  
    echo -n "Restarting $nodeApp"
    $foreverApp restart $nodeApp
    RETVAL=$?
}

stop() {  
    echo -n "Shutting down $nodeApp"
    $foreverApp stop $nodeApp
    RETVAL=$?
}

status() {  
    echo -n "Status $nodeApp"
    $foreverApp list
    RETVAL=$?
}

case "$1" in  
    start)
        start
        ;;
    stop)
        stop
        ;;
    status)
        status
        ;;
    restart)
        restart
        ;;
    *)
        echo "Usage: $0 {start|stop|status|restart}"
        exit 1
        ;;
esac  
</code></pre>

<hr>

<h4 id="">もう少し本格的に制御してみる</h4>

<p>実行するNode.jsサービスがひとつやふたつでしかもrootオンリーなら上記の作業を必要なだけ繰り返せば良い。だがGhost Blogの場合これはあまりよろしくない。というのも；</p>

<ul>
<li>Ghost は記事に対して Draft と Publish の2モードを持つが Staging モードは持っていない。つまり投稿記事を一般公開前にレンダリングチェックすることができない。このために結局本番用とステージング用とでは個別のサービスインスタンスを立ち上げて使い分けたいという需要がある。</li>
<li>Ghost はマルチユーザ（グループ）対応だが、ユーザ別にテーマを変更できるわけではなく全体でひとつのテーマに固定される。故にテーマやプラグインを開発するユースケースでもユーザ別インスタンスの需要がある。またこの作業は頻繁にインスタンスを再起動する必要が有るため、start/stopを行うのにいちいちrootへsuするのは現実的ではない。</li>
<li>Ghost は自身のローカルディレクトリ内にアップロードされた画像ファイルと、記事DBファイルを格納<sup id="fnref:3"><a href="http://multix.jp/daemon-nodejs/#fn:3" rel="footnote">3</a></sup>している。当然それらのファイル権限がそのままでは実効rootになってしまうから、非rootの開発ユーザがファイルメンテナンスをするうえで都合が悪い。</li>
</ul>

<p>これらの諸々を助けるためにforeverdというinitdファイルを作成した。これは /etc/forver.d ディレクトリに配置した設定ファイルの数だけNode.jsのインスタンスを起動する。</p>

<pre><code class="language-brush:bash gutter:true title:/etc/forever.d/ghost-stg.conf">NODE_DIR=/home/user/ghost  
NODE_ENV=staging  
NODE_APP=index.js  
NODE_USER=user  
OPTS="--silent"  
</code></pre>

<pre><code class="language-brush:bash gutter:true title:/etc/forever.d/ghost-prd.conf">NODE_DIR=/home/user/ghost  
NODE_ENV=production  
NODE_APP=index.js  
NODE_USER=user  
OPTS="--silent"  
</code></pre>

<pre><code class="language-brush:bash gutter:true title:/etc/init.d/foreverd">#!/bin/sh
#
# chkconfig: 2345 88 14
# description: Description of the Service
#
# Below is the source function library, leave it be
. /etc/init.d/functions
 
# result of whereis node_modules
export NODE_PATH=/usr/lib/node_modules  
 
PATH=/sbin:/bin:/usr/sbin:/usr/bin  
 
fordeamon(){  
    for conf in /etc/forever.d/*.conf; do
        name=$(basename $conf .conf)
        if [ -n "$2" ]; then
            if [ "$name" != "$2" ]; then
                continue
            fi
        fi
        source $conf
        : ${NODE_USER:=root}
        : ${NODE_APP:=index.js}
        : ${NODE_DIR:=~}
        if [ $(whoami) != $NODE_USER ]; then
            su -c "$0 $1 $name" $NODE_USER
        else
            echo $"Process id is $name"
            case "$1" in  
                start)
                    NODE_ENV=$NODE_ENV \
                        forever $OPTS \
                        --sourceDir="$NODE_DIR" \
                        --workingDir="$NODE_DIR" \
                        --minUptime=1000 \
                        --spinSleepTime=1000 \
                        --uid=$name \
                        -a \
                        start $NODE_APP
                    ;;
                stop)
                    NODE_ENV=$NODE_ENV \
                        forever stop $name
                    ;;
                restart)
                    NODE_ENV=$NODE_ENV \
                        forever restart $name
                    ;;
                list)
                    NODE_ENV=$NODE_ENV \
                        forever list | grep /$name.log
                    ;;
            esac
        fi
    done
}
 
case "$1" in  
    start)
        echo "Start service forever"
        fordeamon start $2
        ;;
    stop)
        echo "Stop service forever"
        fordeamon stop $2
        ;;
    restart)
        echo "Restart service forever"
        fordeamon restart $2
        ;;
    status)
        fordeamon list $2
        ;;
    list)
        fordeamon list $2
        ;;
    *)
        echo "Usage: $0 {start|stop|restart|status|list} [name]"
        exit 1
        ;;
esac  
</code></pre>

<p>これを実行権限を付与してchkconfigでサービス登録すると、サーバ起動時に /etc/forver.d/*.conf に従ってforeverインスタンスを起動するが、その後は一般ユーザ側で普通に（serviceコマンドを通さずに）foreverコマンドでlist/stop/restart<sup id="fnref:4"><a href="http://multix.jp/daemon-nodejs/#fn:4" rel="footnote">4</a></sup>することができるようになる。またroot側からも<code>service foreverd COMMAND NAME</code>でインスタンス個別にユーザ権限でプロセス操作ができる。</p>

<pre><code class="language-brush:plain title:root側の例"># service foreverd start
# service foreverd restart NAME
# service foreverd stop NAME
</code></pre>

<pre><code class="language-brush:plain title:一般ユーザ側の例">$ forever list
$ forever restart NAME
$ forever stop NAME
$ /etc/init.d/foreverd start NAME
$ forever restartall
</code></pre>

<p>Switch Userは27行目のsuコマンドが行っているが、これは自分自身（/etc/init.d/foreverd）をsuidして再帰的に呼び出すようにした。正攻法ならsudoコマンドでforeverを呼びたくなるところだが、エスケープを含むコマンドラインの組み立てが煩雑になるのを避けたいのと、CnetOS6の/etc/sudoersのデフォルト設定では非TTYでのsudo実行を禁止しており、そのままでは rcプロセス（当然非TTY）で動作しない<sup id="fnref:5"><a href="http://multix.jp/daemon-nodejs/#fn:5" rel="footnote">5</a></sup>という事情による。</p>

<p>なお当然だが well-known port を掴むようなインスタンスはrootユーザで起動しなければ意味が無い。また個々のインスタンスが掴むポート番号を重複しないように調整するのもroot管理者の仕事である。</p>

<hr>

<div class="footnotes"><ol><li class="footnote" id="fn:1"><p>比較的新しい機能なので古いNode.jsに配慮するとまだ機能統合できないということか。ゆえにデーモンプロセスツールではなく個々のアプリケーションの中でやってくれという主張らしい。 <a href="http://multix.jp/daemon-nodejs/#fnref:1" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:2"><p>CentOS7になるとsysvinitが非推奨となってsystemctlが標準となる。6/7両方で修正なく動くようにしたいならupstartで実装するのも手だ。 <a href="http://multix.jp/daemon-nodejs/#fnref:2" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:3"><p>DBについてはPostgresql等外部へ逃すことはできるがアップロード画像ファイルはDBに入らないためどうにもならない。ゆえにsupervisorやpm2のファイル更新監視モードは無限再起動に陥るため使うことができず、かえってそれらを使うメリットがない。本項でforeverを主に扱っているのはこういう事情による。 <a href="http://multix.jp/daemon-nodejs/#fnref:3" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:4"><p>ユーザ側でのstartは、root実行時と同じ起動パラメータを指定するようにしないと当然同じ動作にならない。故にサービス起動時はサンプルの4行目のように /etc/init.d/foreverdを直接実行する。 <a href="http://multix.jp/daemon-nodejs/#fnref:4" title="return to article">↩</a></p></li>

<li class="footnote" id="fn:5"><p>visudoで <em>Defaults requiretty</em> を <em>Defaults:system_user !requiretty</em> に修正すれば非TTYプロセス中でもsudoできるようになる。だが今回の場合はrootが非rootプロセスを起動したいのであり、非rootにはrootプロセスを起動させたくないのだから suのほうが理にかなう。 <a href="http://multix.jp/daemon-nodejs/#fnref:5" title="return to article">↩</a></p></li></ol></div>]]></content:encoded></item></channel></rss>