読者です 読者をやめる 読者になる 読者になる

SORACOM LoRa Space に「接続ポイント」を登録してみた!

SORACOM の共有型 LoRaWAN 接続サービス「SORACOM LoRa Space」ですが、私の手元にもようやく、ようやくデバイスLoRa Arduino 開発シールド AL-050)が届きまして、さてどんな事をして遊ぼうかなと考えていたところ、SORACOM LoRa Space のホームページ が開設され、LoRaWAN でつながった箇所をみんなで教え合うみたいなことができるようになりましたので早速試してみました。

f:id:bearmini:20170511145518p:plain

ログインして「接続ポイントの登録」画面に行き、説明に従ってセットアップしていくと・・・

f:id:bearmini:20170511152836p:plain

こんな感じでプロットされます。 (↑はソラコム二子玉川オフィスの周辺です。)

セットアップは拍子抜けするほど簡単だったので、皆さんもぜひ試してみてはいかがでしょうか。

ちなみに私の使用した Arduino は STEMTera というもので、Arduino とブレッドボードが一体化したというものすごく便利なものです。

Arduino を使っていると、結局配線したりするのにブレッドボードが必要になるので、この組み合わせは最強ですね。 しかも、うれしいことにこの STEMTera、裏面が LEGO 互換・・・!私の MacBrik Book との相性も抜群です。

↓の共立エレショップさんで買うと Amazon より安いです。

eleshop.jp

これに、GPS モジュールを接続します。

こんな感じです。

f:id:bearmini:20170511151145j:plain

そしてモバイルバッテリーを接続して、いざお出かけ。

f:id:bearmini:20170511151029p:plain

ちなみにこの基板やら配線やらむき出しのブツを持って街中をウロウロしてると通報されかねませんのでそのあたりは自己責任でお願いします。私は自宅から持参したタッパーに入れて持ち歩きました。

Go のプログラムで Windows 上で chmod しようとしただけなのになぜか golang.org/x/sys/windows にバグっぽい挙動を見つけたばかりかなぜかアセンブラを読むハメになった話

何を言ってるかわか(ry

ファイルのパーミッションを変更する chmod コマンド相当の関数は、私の知る限り Golang の標準ライブラリには 2 つあって、しかもそのいずれもが Windows 上では期待したような動作にならないって、みなさん知っていましたか?

まずひとつめ、os.Chmod() 、これは内部的に syscall.Chmod() を呼んでいます。

syscall.Chmod()このあたり に実装があって(本記事執筆時点)、ソースコードをご覧頂いてもわかるように Windows APISetFileAttributes() を呼んでファイルの ReadOnly 属性を設定したり落としたりしているだけです。

魚拓: f:id:bearmini:20170427193959p:plain

なので、os.Chmod(path, 0600) とかやっても、もともと他の人が読み書きできる設定になってるファイルはそのまま他の人が読み書きできる状態が維持されてしまいます。

びっくりしますね。

続いてふたつめ、 func (*os.File) Chmod() です。これは内部的に (*poll.FD) Fchmod() を呼んでいて、そこから syscall.Fchmod() が呼び出されます。こちらの実装は常に EWINDOWS という値を返すようになっています。つまり常にエラーです。

魚拓: f:id:bearmini:20170427194850p:plain

なので、var f *os.Filef に対して f.Chmod(0600) とかやっても、エラーになるだけでファイルのパーミッションは何も変わりません。

このあたりで、「Windows には ACL (Access Control Lists) というものがあったな・・・POSIX/UNIX のシンプルなファイルパーミッションとは根本的に仕組みが違うのかな・・・」ということに気づいちゃいます。

ということで、「じゃあ GolangWindowsACL が操作できるライブラリがあればいいじゃないか。きっとあるはずだ。」と思っておもむろにグーグル先生に聞いてみるわけじゃないですか。

するといくつかライブラリが見つかりますが、私がやりたいことができそうだったのが以下のライブラリです。

github.com

しかも chmod.go という名前の、そのものズバリなファイル(そしてその中に Chmod() 関数)まであるじゃありませんか。

ということでこいつを使ってみます。

すると、ほぼ期待したような動作をしてくれるようです。 「いいじゃないか、こいつを採用だ」と思った時に、一つ問題が見つかりました。

この go-aclChmod() は、エラーが起きたとき(たとえば存在しないファイルに対して呼び出したときなど)に、error を返してくれるのは良いのですが、error.Error() でそのエラーメッセージを取り出すと、常に

The operation completed successfully.

という文字列が取得されるのです。

なぜだ、、、

ということで Deep dive が始まります。

go-aclChmod() は内部で同じパッケージの Apply() を呼んでいます。 Apply() は内部で go-ac/api パッケージの GetNamedSecurityInfo()SetNamedSecurityInfo() を呼び出します。

ここでは GetNamedSecurityInfo() を見ていくことにしましょう。

// https://github.com/hectane/go-acl/blob/master/api/secinfo.go#L44

var (
    procGetNamedSecurityInfoW = advapi32.MustFindProc("GetNamedSecurityInfoW")
    procSetNamedSecurityInfoW = advapi32.MustFindProc("SetNamedSecurityInfoW")
)

// https://msdn.microsoft.com/en-us/library/windows/desktop/aa446645.aspx
func GetNamedSecurityInfo(objectName string, objectType int32, secInfo uint32, owner, group **windows.SID, dacl, sacl, secDesc *windows.Handle) error {
    ret, _, err := procGetNamedSecurityInfoW.Call(
        uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(objectName))),
        uintptr(objectType),
        uintptr(secInfo),
        uintptr(unsafe.Pointer(owner)),
        uintptr(unsafe.Pointer(group)),
        uintptr(unsafe.Pointer(dacl)),
        uintptr(unsafe.Pointer(sacl)),
        uintptr(unsafe.Pointer(secDesc)),
    )
    if ret != 0 {
        return err
    }
    return nil
}

advapi32.dll からロードした GetNamedSecurityInfoW() Windows API を呼んでいます。

advapi32 は windows.MustLoadDLL() でロードした windows.DLL 型なので、advapi32.MustFindProc() でロードした procGetNamedSecurityInfoWwindows.Proc 型です。

procGetNamedSecurityInfoW.Call() の 3 番目の戻り値に問題がありそうです。

windows.ProcCall() 関数は以下のような定義になっています。

// https://github.com/golang/sys/blob/master/windows/dll_windows.go#L126

func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
    switch len(a) {
    case 0:
        return syscall.Syscall(p.Addr(), uintptr(len(a)), 0, 0, 0)
    case 1:
        return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], 0, 0)
    case 2:
        return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], 0)

  :

syscall.Syscall() を呼んでいますね。

// https://github.com/golang/go/blob/master/src/runtime/syscall_windows.go#L156

//go:linkname syscall_Syscall syscall.Syscall
//go:nosplit
func syscall_Syscall(fn, nargs, a1, a2, a3 uintptr) (r1, r2, err uintptr) {
    c := &getg().m.syscall
    c.fn = fn
    c.n = nargs
    c.args = uintptr(noescape(unsafe.Pointer(&a1)))
    cgocall(asmstdcallAddr, unsafe.Pointer(c))
    return c.r1, c.r2, c.err
}

この 3 番目の戻り値 c.err が怪しいです。 ちなみに、ここでは関数シグネチャでは 3 番目の戻り値の型は uintptr になっていますが、この関数の宣言部では以下のように syscall.Errno になっています。

// https://github.com/golang/go/blob/master/src/syscall/dll_windows.go#L24

// Implemented in ../runtime/syscall_windows.go.
func Syscall(trap, nargs, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)

さて、syscall_Syscall() の中に戻って見ていきます。

この3番目の戻り値 c.err が設定されているのは、おそらく cgocall() の中でしょう。

少し長いですが cgocall() 関数の全体を以下に掲載します。

// https://github.com/golang/go/blob/master/src/runtime/cgocall.go#L92

// Call from Go to C.
//go:nosplit
func cgocall(fn, arg unsafe.Pointer) int32 {
    if !iscgo && GOOS != "solaris" && GOOS != "windows" {
        throw("cgocall unavailable")
    }

    if fn == nil {
        throw("cgocall nil")
    }

    if raceenabled {
        racereleasemerge(unsafe.Pointer(&racecgosync))
    }

    // Lock g to m to ensure we stay on the same stack if we do a
    // cgo callback. In case of panic, unwindm calls endcgo.
    lockOSThread()
    mp := getg().m
    mp.ncgocall++
    mp.ncgo++
    mp.incgo = true

    // Reset traceback.
    mp.cgoCallers[0] = 0

    // Announce we are entering a system call
    // so that the scheduler knows to create another
    // M to run goroutines while we are in the
    // foreign code.
    //
    // The call to asmcgocall is guaranteed not to
    // grow the stack and does not allocate memory,
    // so it is safe to call while "in a system call", outside
    // the $GOMAXPROCS accounting.
    //
    // fn may call back into Go code, in which case we'll exit the
    // "system call", run the Go code (which may grow the stack),
    // and then re-enter the "system call" reusing the PC and SP
    // saved by entersyscall here.
    entersyscall(0)
    errno := asmcgocall(fn, arg)
    exitsyscall(0)

    // From the garbage collector's perspective, time can move
    // backwards in the sequence above. If there's a callback into
    // Go code, GC will see this function at the call to
    // asmcgocall. When the Go call later returns to C, the
    // syscall PC/SP is rolled back and the GC sees this function
    // back at the call to entersyscall. Normally, fn and arg
    // would be live at entersyscall and dead at asmcgocall, so if
    // time moved backwards, GC would see these arguments as dead
    // and then live. Prevent these undead arguments from crashing
    // GC by forcing them to stay live across this time warp.
    KeepAlive(fn)
    KeepAlive(arg)

    endcgo(mp)
    return errno
}

直接 c.err に何か値を設定しているところはないようです。

 errno := asmcgocall(fn, arg)

の行が怪しいですね。 ここで fn は、cgocall() を呼び出す際に第一引数に指定された asmstdcallAddr です。

asmstdcallAddr は以下のように定義されていて、

// https://github.com/golang/go/blob/master/src/runtime/os_windows.go#L157

var asmstdcallAddr unsafe.Pointer

以下のように初期化されています。

// https://github.com/golang/go/blob/master/src/runtime/os_windows.go#L263
func osinit() {
    asmstdcallAddr = unsafe.Pointer(funcPC(asmstdcall))

asmstdcall という関数のアドレスのようです。

asmstdcall

// https://github.com/golang/go/blob/master/src/runtime/os_windows.go#L153

// Call a Windows function with stdcall conventions,
// and switch to os stack during the call.
func asmstdcall(fn unsafe.Pointer)

というように宣言だけされていますが、実体はアセンブラで記述されています。 名前の通り、stdcall 呼び出し規約に則って関数を呼び出す処理がアセンブラで記述されているようです。

以下は 64bit windows 環境用のアセンブリコードです。

// https://github.com/golang/go/blob/master/src/runtime/sys_windows_amd64.s#L13

// void runtime·asmstdcall(void *c);
TEXT runtime·asmstdcall(SB),NOSPLIT|NOFRAME,$0
    // asmcgocall will put first argument into CX.
    PUSHQ   CX          // save for later
    MOVQ    libcall_fn(CX), AX
    MOVQ    libcall_args(CX), SI
    MOVQ    libcall_n(CX), CX

    // SetLastError(0).
    // https://en.wikipedia.org/wiki/Win32_Thread_Information_Block
    // http://www.geoffchappell.com/studies/windows/win32/ntdll/structs/teb/index.htm
    // http://shitwefoundout.com/wiki/Win32_Thread_Environment_Block
    MOVQ    0x30(GS), DI   //* DI = _NT_TIB.Self (where GS == TIB: Thread Information Block)
    MOVL    $0, 0x68(DI)   //* _TEB.LastErrorValue = 0

    SUBQ    $(maxargs*8), SP    // room for args

    // Fast version, do not store args on the stack.
    CMPL    CX, $4
    JLE loadregs

    // Check we have enough room for args.
    CMPL    CX, $maxargs
    JLE 2(PC)
    INT $3          // not enough room -> crash

    // Copy args to the stack.
    MOVQ    SP, DI
    CLD
    REP; MOVSQ
    MOVQ    SP, SI

loadregs:
    // Load first 4 args into correspondent registers.
    MOVQ    0(SI), CX
    MOVQ    8(SI), DX
    MOVQ    16(SI), R8
    MOVQ    24(SI), R9
    // Floating point arguments are passed in the XMM
    // registers. Set them here in case any of the arguments
    // are floating point values. For details see
    //  https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx
    MOVQ    CX, X0
    MOVQ    DX, X1
    MOVQ    R8, X2
    MOVQ    R9, X3

    // Call stdcall function.
    CALL    AX

    ADDQ    $(maxargs*8), SP

    // Return result.
    POPQ    CX
    MOVQ    AX, libcall_r1(CX)

    // GetLastError().
    MOVQ    0x30(GS), DI          //* DI = _NT_TIB.Self
    MOVL    0x68(DI), AX          //* AX = _TEB.LastErrorValue
    MOVQ    AX, libcall_err(CX)

    RET

このアセンブラは Go の処理系特有の記法で、この文法などについての最も詳しい説明は以下のリンク先のようです。

A Manual for the Plan 9 assembler

Rob Pike 先生が自ら書かれた説明のようですね。

多少特殊な記法はありますが、Intel 系のアセンブラに慣れた人なら特に問題なく読めるでしょう。

この最後の

 // GetLastError().
    MOVQ    0x30(GS), DI          //* DI = _NT_TIB.Self
    MOVL    0x68(DI), AX          //* AX = _TEB.LastErrorValue
    MOVQ    AX, libcall_err(CX)

の部分です。

//* で始まるコメントは私が追加したものです。

早い話が、Thread Information Block (TIB) またの名を Thread Environment Block (TEB) の LastErrorValue を取り出して、libcall.err に入れています。

libcall というのは↓のことで、

// https://github.com/golang/go/blob/master/src/runtime/runtime2.go#L295

type libcall struct {
    fn   uintptr
    n    uintptr // number of parameters
    args uintptr // parameters
    r1   uintptr // return values
    r2   uintptr
    err  uintptr // error number
}

これは m のメンバです。

type m struct {
  :
  
    syscall   libcall // stores syscall parameters on windows

  :
}

mg のメンバです。

// https://github.com/golang/go/blob/master/src/runtime/runtime2.go#L320

type g struct {
  :
  
    m              *m      // current m; offset known to arm liblink

  :

m とか g とかについては ↓ このあたりに良い説明があります。

niconegoto.hatenadiary.jp

要は、

// https://github.com/golang/go/blob/master/src/runtime/syscall_windows.go#L156

//go:linkname syscall_Syscall syscall.Syscall
//go:nosplit
func syscall_Syscall(fn, nargs, a1, a2, a3 uintptr) (r1, r2, err uintptr) {
    c := &getg().m.syscall

この cgmsyscall すなわち libcall 型なので先程のアセンブラ

 // GetLastError().
    MOVQ    0x30(GS), DI          //* DI = _NT_TIB.Self
    MOVL    0x68(DI), AX          //* AX = _TEB.LastErrorValue
    MOVQ    AX, libcall_err(CX)

では c.errLastError を入れてますよ、ということになります。

ちなみに TIB のメモリレイアウトに関しては以下のリンク先を参考にしました。

Win32 Thread Environment Block - Shit we found out

さて、そういうわけで procGetNamedSecurityInfoW.Call() の第 3 戻り値には LastError が入っていそうなものです。

Errno 型なので、以下の関数によって string に変換されるはずです。

// https://github.com/golang/go/blob/master/src/syscall/syscall_windows.go#L90

func (e Errno) Error() string {
    // deal with special go errors
    idx := int(e - APPLICATION_ERROR)
    if 0 <= idx && idx < len(errors) {
        return errors[idx]
    }
    // ask windows for the remaining errors
    var flags uint32 = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY | FORMAT_MESSAGE_IGNORE_INSERTS
    b := make([]uint16, 300)
    n, err := formatMessage(flags, 0, uint32(e), langid(LANG_ENGLISH, SUBLANG_ENGLISH_US), b, nil)
    if err != nil {
        n, err = formatMessage(flags, 0, uint32(e), 0, b, nil)
        if err != nil {
            return "winapi error #" + itoa(int(e))
        }
    }
    // trim terminating \r and \n
    for ; n > 0 && (b[n-1] == '\n' || b[n-1] == '\r'); n-- {
    }
    return string(utf16.Decode(b[:n]))
}

事前に定義済みの errors の中には “The operation completed successfully” という文字列は見つかりませんでしたので、おそらく formatMessage() 関数がその文言を常に返してきてしまっているという状況なのでしょう。

formatMessage()

// https://github.com/golang/go/blob/master/src/syscall/syscall_windows.go#L144

//sys   formatMessage(flags uint32, msgsrc uintptr, msgid uint32, langid uint32, buf []uint16, args *byte) (n uint32, err error) = FormatMessageW

という感じで Windows APIFormatMessageW を呼び出しているだけです。

ということは e が 0 なのか、、、

第 1 戻り値はちゃんとエラーの理由を表す値になっているようなので、もしかしたら GetNamedSecurityInfoW() Windows API は LastError を設定しない君なのでは?という可能性に気づき、やる気が萎えたのでここでおしまい。

ということでタイトルでは「バグっぽい」と煽り気味に書きましたが、Golang のライブラリのバグではなくて、Windows の仕様なのかもしれません。

リモートホスト上のパケットキャプチャをリアルタイムに手元のマシンの Wireshark で見る方法

ワンライナーで行けちゃいます😁

wireshark -k -i <(ssh user@host "tcpdump -U -n -w - -i eth0 'not port 22'")

しかもリモートホスト側で何かしら特別な準備とかが不要なので非常にお気楽・お手軽です。

前提条件

解説

上記ワンライナーを実行すると、以下の図のように ssh で接続したホスト上で tcpdump コマンドを実行してパケットをキャプチャし、そのパケットキャプチャを手元の Wireshark でリアルタイムに見ることができるようになります。

f:id:bearmini:20170216154859p:plain Icons for a laptop and a server are designed by Gregor Cresnar from Flaticon

ワンライナーは比較的短いので難しくないと思いますが念のため解説します。

まず wireshark コマンドで Wireshark を起動しています。

オプションの -k はすぐにキャプチャを開始する、という意味です。 -i は入力インターフェースの指定ですね。この場合は <( ... ) で指定されるコマンドの出力を Wireshark が読み込むという動きになります。 <( ... )bash の記法です。bash を用いていない場合は -i - と pipe を組み合わせて

ssh user@host "tcpdump -U -n -w - -i eth0 'not port 22'" | wireshark -k -i -

という感じにすればよいでしょう。(末尾の - に注意)

続いて ssh 部分に入っていきます。

user@host はキャプチャを取りたい目的のリモートホスト(とそれにログインするためのユーザー)の指定です。 公開鍵認証をする場合や踏み台サーバー経由でのログインをする場合などはここに引数を追加するか、~/.ssh/config に指定しておくとよいでしょう。

"" で囲まれた部分がリモートホスト上で実行されます。 つまり、tcpdump がいろいろ引数を指定された状態で起動されます。

tcpdump コマンドに渡されている引数を一つずつ見てみましょう。

-U は 1 パケットごとに出力ファイルに出力するように指定するオプションです。(これを指定しないと tcpdump にバッファリングされてしまい、リアルタイム性が損なわれると思われます。)

-n は IP アドレスをホスト名に逆引きする処理を抑制するオプションです。リモートホスト側で名前解決をしてしまうとパケットキャプチャにそれが混じってしまうので DNS クエリをしないようにこれを指定しています。

-w - は標準出力へ出力することを指定するオプションです。リモートホスト上で tcpdump が標準出力にパケットの情報を出力すると、それが ssh 経由でローカルホストの標準出力に出力されます。そしてその標準出力を wireshark に食わせているというわけです。

-i eth0tcpdump がパケットをキャプチャするインターフェース名の指定です。適宜他の名前に変えましょう。

'not port 22'tcpdump でキャプチャするパケットを選別するためのフィルタの指定。ここでは、パケットキャプチャの転送に使われている 22 番ポートのパケットはキャプチャしないように指定しています。

bash スクリプトでヒアドキュメントを変数に格納したいけどヒアドキュメントの中にカッコがあってエラーになってしまう場合の対策

最近ブログの記事タイトルが長い気がするのでなんとか短めに抑えたいのですがなんとかならないものでしょうかね。

さて、昨年あたりから shellcheck というツールを使ってシェルスクリプトの lint をするようにしたので自分の shell 力が少し上がった気がしているのですが、以下のようなありがちなコードの解決策が覚えられそうになかったのでメモしておきます。

やりたいことは、read コマンドを使ってユーザーに複数の選択肢を提示して選んでもらうというとてもシンプルな、よくありがちなことだと思います。

read コマンドには -p オプションがあり、ユーザーに対して何かのプロンプトを提示することができます。

そこで

read -r -p "$prompt" num

という感じでプロンプトを表示しつつ num という名前の変数に選択されたの番号を取得することを考えます。

prompt 変数にはメッセージを入れておきます。

たとえば、以下のようなプロンプトを表示させたい場合:

1) hoge
2) fuga
3) piyo

choose a number (1-3) ?  

変数 prompt には定石パターンとして以下のように $( ) と cat とヒアドキュメントを使って、

prompt=$(cat <<EOD
1) hoge
2) fuga
3) piyo

choose a number (1-3) ?
EOD
)

という感じにすると思います。しかしこれは実は問題があります。 ヒアドキュメントの中に半角閉じカッコが入っていると、そこで $( が閉じられてしまって、その後シンタックスエラーなどが起こってしまうのです。

なお、` 〜 ` を使うと閉じカッコの問題は起こらないかもしれませんが、

prompt=`cat <<EOD
       ^-- SC2006: Use $(..) instead of legacy `..`.

というふうに shellcheck に警告されてしまいます。 shellcheck の説明 を読むと、` 〜 ` には POSIX で未定義動作があったり、独自のエスケープモードに入ってびっくりするような結果になったり、ネストさせるのが難しかったり、といったような弊害があるとのことです。

というわけで $( ) を使って以下のように書いてみるとやはり実行時にエラーになってしまいます。

$ cat paren.sh
#!/bin/bash

prompt=$(cat <<EOD
1) hoge
2) fuga
3) piyo

choose a number (1-3) ?
EOD
)

read -r -p "$prompt" num

echo "You chose $num"

$ bash paren.sh
paren.sh: line 4: hoge: command not found
paren.sh: line 5: syntax error near unexpected token `)'
paren.sh: line 5: `2) fuga'

これを回避するには、関数 を使って以下のようにします。

function get-msg() {
    cat <<EOD
1) hoge
2) fuga
3) piyo

choose a number (1-3) ?
EOD
}

prompt=$( get-msg )

read -r -p "$prompt" num

echo "You chose $num"

get-msg 関数(名前はなんでも良いです)が cat とヒアドキュメントを使って問題となる閉じカッコを含む文字列を表示します。 それを $( ) の中で 呼び出すことで文字列を変数に格納します。 これにより、$( ) の中に閉じカッコが現れなくなるので問題が起こらなくなるというわけです。

[改訂新版] シェルスクリプト基本リファレンス  ??#!/bin/shで、ここまでできる (WEB+DB PRESS plus)

[改訂新版] シェルスクリプト基本リファレンス  ??#!/bin/shで、ここまでできる (WEB+DB PRESS plus)

入門bash 第3版

入門bash 第3版

bashクックブック

bashクックブック

ssh の ProxyCommand を .ssh/config に書かずにコマンドライン引数で指定する方法

ssh で踏み台ホスト経由で目的のサーバーに到達したいということはよくあると思います。

たくさんの先人たちが ~/.ssh/config に書くべき内容について書き残してくれていますのでここでは特に詳しくは触れません。

基本的には ProxyCommand を使って、踏み台サーバーから目的のサーバーへと接続するような設定をすればよいのですが、この設定を ~/.ssh/config に書かずに、コマンドライン引数のみで実現できないかと思って調べてみました。

なぜそのように思ったかというと、たとえばあるサーバーに踏み台経由でログインするという処理をスクリプトにしてチームのメンバーに配布して使ってもらうような場合に、「まず .ssh/config を編集して・・・」というステップが入ってくるとそれだけで敷居が高くなってしまったり、間違いが入り込んでしまう余地が多くなってしまいます。 また、接続先の数が増えるとそのたびに編集してもらう必要が出てきてしまいますし、クラウドのようにホストのアドレスが動的に変わるような環境だと別途 API などによって IP アドレスを取得してくる必要があり、そもそも .ssh/config に static な設定を書きづらいということもあるかもしれません。

「単にスクリプトを実行すれば目的のサーバーに到達できる」というシンプルさを保つためには、可能であればスクリプトだけで完結する方法が望ましいわけです。

スクリプトで ~/.ssh/config を書き換えるという少々強引な手法も考えられますが、これは余計な問題を引き起こしそうな気がするのでちょっと避けたいところです。

そんなわけで man ssh を眺めていたところ、-o というオプションを発見しました。これは、~/.ssh/config に書くことができる内容をそのままコマンドライン引数として渡すことができるというオプションです。つまり、-o ProxyCommand=〜〜〜 というようなオプションを ssh コマンドに渡すことができそうです。

たとえば接続元ホスト(local) から踏み台サーバー (gateway) 経由で接続できるようなリモートサーバー (remote) があって、以下のように ~/.ssh/config に書いた場合、

Host gateway
  Hostname gateway.example.com
  User gateway-user
  Port 8022
  IdentityFile ~/.ssh/id_rsa_gateway

Host remote
  Hostname 10.11.12.13
  User remote-user
  Port 1022
  IdentityFile ~/.ssh/id_rsa_remote
  ProxyCommand ssh gateway -W %h:%p

ssh remote とするだけでリモートサーバーにログインできます。 (IdentityFile はどちらも接続元のホスト (local) に置いておきます)

これと同じことをコマンドライン引数だけで実現すると以下のようになります。

ssh -o ProxyCommand='ssh -i ~/.ssh/id_rsa_gateway -p 8022 gateway-user@gateway.example.com -W 10.11.12.13:1022' -i ~/.ssh/id_rsa_remote -p 1022 remote-user@10.11.12.13

ProxyCommand オプションには、gateway を介して remote ホストの ssh のポートまで到達するための設定を書き、その後 remote ホストにログインするための設定を書くようなイメージですね。 (もちろんオプションの順番はこの通りに並べる必要はありませんが、このようにすることで見通しが良くなるような気がします)

上記のコマンド例は私の実際のスクリプトから抜き出してそれをここに貼ることのできるように書き換えていますのでその途中で間違いなどが入り込んでしまっているかもしれません。何かお気づきになったらお知らせください。

実用SSH 第2版―セキュアシェル徹底活用ガイド

実用SSH 第2版―セキュアシェル徹底活用ガイド

OpenSSH[実践]入門 (Software Design plus)

OpenSSH[実践]入門 (Software Design plus)

git コマンドを alias してるときにも補完を効かせる方法

プログラマなら呼吸をするように git コマンドを使うと思うので、git っていう 3 文字でさえ長過ぎますよね。
ということでみなさま .bashrc などで

alias g='git'

みたいなことをしてると思いますが、この場合 git-completion.bash による補完が効かなくなってしまうという弊害がありました。

とはいえ、~/.gitconfig にサブコマンドの短縮形を大量に登録していると思いますし、ファイル名の補完は標準で効くので、不便なのはブランチ名の補完や滅多に使わないサブコマンドの補完など一部の作業に限られていました。

でもこれを解決する方法がありました。

.bashrc などで git-completion.bash を source した後に以下のように __git_complete 関数を実行しましょう。

source /Applications/Xcode.app/Contents/Developer/usr/share/git-core/git-completion.bash
__git_complete g __git_main

私は Mac を使っていて Xcode に含まれている git-completion.bash を使っていますが、環境によって git-completion.bash のパスは異なると思います。

また、git のバージョンが変わったりすると関数名が変わったりするかもしれませんがその際は頑張って git-completion.bash を解読して、どの関数を呼べばよいか探しましょう。

これで .bashrc を source し直したら g コマンドでも補完が効くようになっています。

以上、自分用メモでした。

Mac で SORACOM Air で接続している時に特定のルートだけ SORACOM 経由にしてそれ以外は Wi-Fi 経由にする方法

タイトルが若干長いですが、やりたいことは以下のとおりです。

1. 通常は Wi-Fi などでインターネットに接続している
2. SORACOM Air 入りの USB 3G/LTE モデムを Mac に挿した時、SORACOM が提供するサービスには SORACOM Air 経由でアクセスしたいが、それ以外のネットワーク通信は通常通り Wi-Fi 側で行いたい

私の場合は soracom-sdk-go のテストの中で metadata.soracom.io へのアクセスがあり、そこだけ SORACOM Air 経由にしたいという個人的な事情がありました。

他にも SORACOM Beam や SORACOM Endorse、SORACOM Funnel などのサービスへのアクセスも同様にルーティングしたい場合があるかもしれませんし、SORACOM Canal や SORACOM Direct で接続したプライベート環境へのルーティングも同様に行いたい場合があるかもしれません。

設定手順はおおまかに以下の2ステップです。

1. システム環境設定 > ネットワーク で Wi-Fi と USB モデムの優先順位を設定
2. ダイアルアップ時に実行されるスクリプトを書いてルーティング設定を追加


ではスクリーンショットなどを交えながら具体的に説明していきます。

1. ネットワークインターフェースの優先順位を設定

システム環境設定からネットワークの設定を開きます。
左側のインターフェースのリストの下部に歯車アイコンがあり、それを押すとメニューが現れます。
その中から「サービスの順序」を選択します。
f:id:bearmini:20160328161022j:plain
(画像はメニューなどが英語になっていますが日本語でも同様の項目があるはずです)

サービスの順序のリストが出てきますので、Wi-Fi などの通常使いたいものの方が上になるように設定します。(ドラッグアンドドロップで項目を移動させます)
f:id:bearmini:20160328161434j:plain

並び替えたら OK を押して閉じます。

ちなみに上図の私の環境では FOMA L05A xxx というのが 3G モデムです。

以下のサイトの説明にしたがってセットアップしたものになります。

oshiire.to

余談になるかもしれませんが、以下のサイトで説明されている手順が L-05A にも適用できて、CD を毎回 Eject する手間が省けます。

trtr.hatenablog.jp

2. ダイアルアップ時のルーティング設定を追加

/etc/ppp/ip-up という名前のファイルを以下の内容で作成し、所有者: root 、ファイルのパーミッション:755 とします。
(ファイル名が if-up ではなく ip-up であることに注意してください)

#!/bin/sh

/sbin/route add -net 100.64.0.0/10 10.64.64.64

この route コマンドにより、100.64.0.0/10 宛てのパケットを全て SORACOM Airゲートウェイ(10.64.64.64)に向ける設定が行われます。
(ネットワークのアドレスは 100、ゲートウェイのアドレスは 10 で始まることに注意してください)

SORACOM の各サービスは 100.x.x.x というアドレス(ISP Shared Address)にて提供されています。
例えば SORACOM Beam(beam.soracom.io)は 100.127.127.100 というアドレスを持っていますし、SORACOM Airメタデータサービスは 100.127.100.127 というアドレスでアクセスできます。

なお、ここで追加したルーティング設定は、disconnect 時に自動的に消えるようなので明示的に消さなくても良いようです。

これで beam.soracom.io などに ping が通れば成功です。

$ ping beam.soracom.io
PING beam.soracom.io (100.127.127.100): 56 data bytes
64 bytes from 100.127.127.100: icmp_seq=0 ttl=64 time=399.464 ms
64 bytes from 100.127.127.100: icmp_seq=1 ttl=64 time=435.335 ms
64 bytes from 100.127.127.100: icmp_seq=2 ttl=64 time=394.338 ms
  :

万が一設定を失敗していると、SORACOM 宛てのパケットだけでなく全てのトラフィックが SORACOM 経由になってしまって課金が大変なことになってしまう恐れがありますので、ユーザーコンソールを開いて想定外のトラフィックが流れていないか目視で確認したり、イベントハンドラーを設定するなどして通信量を監視するとよいでしょう。

高度な設定としては、SORACOM Endorse へのトラフィックも SORACOM Air 経由にするために、上記スクリプト(/etc/ppp/ip-up)に以下のような設定を入れておくと良いでしょう。

endorse_ip=$( dig +noall +answer endorse.soracom.io | \grep -oE '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' )
/sbin/route add -host $endorse_ip 10.64.64.64

これは、endorse.soracom.io は 100.64.0.0/10 に属するアドレスを持っていないためです。dig コマンドで endorse.soracom.io のアドレスを解決して、そのアドレス宛てのルートを 10.64.64.64 へ向けています。

長時間かかったコマンドの実行完了を bash でも terminal-notifier でお知らせしたい

[2016.02.19 追記]
スクリプトにバグがあったので修正しました
[追記ここまで]

世の中には CLI から OS X の通知を出せる terminal-notifier という素晴らしいツールがありまして、

github.com

これを brew でインストールしたりなんかすると

$ terminal-notifier -message test

とかやるだけで

f:id:bearmini:20160216215515p:plain

こんな感じにお知らせが出ます。

一方、何かコマンドを実行したら結果が出るまでに10分かかるとか1時間かかるとかそういうことってたまにありますよね。

「ビルドを開始したらちょっと休憩しよう」とか「デプロイを開始したらお昼ごはん行ってこよう」とか「これが終わるまで Twitter でも眺めてるか」ってこと、よくあると思います。

休憩したりお昼ごはんに行ってくるだけならだいたい目論見通りの時間で帰ってくることができますが、Twitter やら Facebook やらを見に行ったりしちゃったが最後、コマンドの実行はとっくに終わってるのになかなか帰ってこれなくて気づいたらすごい時間が経ってしまっていた・・・ってこともよくあると思います。

コマンドの実行が完了したらわかりやすく通知してくれればいいのに、と何度も思いました。

調べてみると、zsh では terminal-notifier を使ってそういうことを実現している方がたくさんいらっしゃるようです。

僕も zsh に改宗・・・と一瞬心が揺らぎましたが、bash でもできないかどうか調べてみました。

すると、こんなものを発見。

github.com

説明に書いてあるとおり、zsh の preexec と precmd をエミュレートするものです。
これがあれば、
preexec でタイムスタンプを保存
precmd でそのタイムスタンプと現在時刻の差を調べ、一定以上の時間が経っていたら terminal-notifier で通知を出す
という処理をすれば望んだ処理が実現できそうです。

ということで ~/.bashrc の末尾あたりに以下のようなコードを書いてみました。

[[ -f ~/.bash-preexec.sh ]] && source ~/.bash-preexec.sh
_tn_timestamp=`date +%s`
_tn_cmd=''
preexec() {
    _tn_timestamp=`date +%s`
    _tn_cmd=$1
}
precmd() {
    now=`date +%s`
    dur=$(( $now - $_tn_timestamp ))
    if [[ $_tn_cmd == "" ]]; then
        return
    fi
    if [[ $dur -gt 60 ]]; then
        terminal-notifier -message "Finished: $_tn_cmd"
        echo elapsed time: $dur seconds
    fi
    _tn_cmd=''
}

preexec はコマンド実行直前に呼ばれる関数で、precmd はコマンド完了後(より厳密には、プロンプト表示直前)に呼ばれる関数です。
プロンプトで何も入力せずに Enter だけ押すと preexec が呼ばれずに precmd が呼ばれたりしますのでそれを回避するコードが入ってたりします。

これで、実行開始して 60 秒以上経ってから実行が完了したら通知が表示されるようになりました。

しかし問題もあって、たとえば vim で何かのファイルを編集して 1 分以上経過して戻ってきたら通知が出ちゃったり、ssh で他のホストにログインして 1 分以上作業してからログアウトして戻ってきたりすると通知が出ちゃったりします。
ブラックリスト的に vim とか ssh だったら通知を出さないというような処理も入れる必要があるかもしれません。

今のところは vimssh の後に通知が表示されてもそれほど困ってないので放置してありますが、何か良い解決策をお持ちの方・思いついた方はお知らせいただければと思います。

SORACOM Air が使える IoT 向け基板のまとめ

さて、前回の記事では SORACOM Air が使える USB 3G/LTE モデムや Wi-Fi ルータをまとめましたが、今回はより IoT に特化した基板で SORACOM Air が使えるもの(動作報告がネット上で見つかったもの)をまとめてみたいと思います。例によって動作保証ではありませんのでそのあたりはご理解ください。

また記載した情報が間違っている可能性もあります。お気づきのことがありましたらコメント等でお知らせください。

続きを読む

SORACOM Air が使える USB 3G/LTE モデム、WiFi ルーターのまとめ

SORACOM Air とは、ひとことで言うと IoT (Internet of Things) 向けの SIM です。
API で通信速度のコントロールができたり、データ通信量の情報を取得できたりする特徴があります。

SORACOM Airdocomo の MVNO の SIM なので、docomo 端末もしくは SIM フリー端末で使えるはずです。
スマホでも使えるみたいですが、ここでは IoT の実験をするときの本命として Raspberry Pi との組み合わせで使いやすい、USB モデム(USB ドングル)、WiFi ルーターをメインにまとめてみたいと思います。

以下、ネット上で動作報告があったデバイスを紹介していきますが、動作保証ではありませんのでそのあたりはご理解ください。
また記載した情報が間違っている可能性もあります。お気づきのことがありましたらコメント等でお知らせください。

文中で記載している価格は 2015年12月30日時点のものです。

続きを読む

Go で DynamoDB Local を使った時にいろいろハマったのでメモ

AWS 公式の Go 言語用 SDK がありますが、Developer Preview ということもあってかドキュメントがちょっと不足しているように思います。

github.com


私は Go のプログラムから DynamoDB を使いたかったのですが、検索するときのキーの指定の仕方とかがだいぶ特殊な感じだったので最初は苦労しましたが、とはいえ、AWS のマネージドサービスとしての DynamoDB を使う分には検索したりすれば多少情報が出てきますのでなんとかなります。

しかし DynamoDB Local を使うとなるとまったく情報がなく、だいぶつまづきました。

以下、いくつかハマったところと解決方法をメモしておきます。

1. 接続先として Endpoint を指定する必要がある

var ac aws.Config
ac.Endpoint = aws.String("hostname:port") 

という感じで DynamoDB Local が動いているアドレスを指定します。
これを指定しないと AWS の DynamoDB に接続しに行ってしまいます。

2. Region を指定する必要がある

これはたしか指定しないとエラーが起きていたはずなので、エラーメッセージにしたがって Region を指定しましょう。

DynamoDB Local ではリージョンとか関係ないはずなのですが、なんでも良いので指定する必要があります。

ac.Region = aws.String("ap-northeast-1")

という感じです。

3. SSL を Disable する必要がある

これはよくよく考えたら当然なのですが、DynamoDB Local には証明書とかを入れてないので SSL なしで接続します。

ac.DisableSSL = aws.Bool(true)

を設定します。


ここまできて、ようやく DynamoDB Local に接続できるようになったはずですが、もう一つ注意点があります。

4. 正しい Credentials を指定する必要がある

私の場合は、DynamoDB Local を Docker コンテナ上で実行しており、その DynamoDB Local に aws コマンドを使ってテスト用のデータを populate する際に、ダミーでもよいので AWS の Credentials 情報(~/.aws/credentials)が存在していないといけなかったので

[default]
aws_access_key_id = dummy
aws_secret_access_key = dummy

という内容のファイルを作成していました。

そしてその populate したデータを Go のプログラムから参照しようとすると、これと同じ内容を aws.Config.Credentials に設定しておかないといけませんでした。
(さもなければ、作ったはずのテーブルが存在しないというエラーになったりします)

ac.Credentials = credentials.NewStaticCredentials("dummy", "dummy", "dummy")

(第3引数の token は指定しなくても良さそうでしたが入れてみました)

これは DynamoDB Local には接続できているのにデータが見えないという挙動になるので、すごく悩みました。

なお、AWS 上の DynamoDB に接続するときは EC2 Instance Role を使ったので Credentials を指定する必要がありませんでした。


Amazon Web Services クラウドデザインパターン設計ガイド 改訂版

Amazon Web Services クラウドデザインパターン設計ガイド 改訂版

最近のCygwinでbashのプロンプトにgitのブランチ名を表示する方法

以前はgit-completionパッケージをインストールして/etc/bash_completionをsourceすればよかっただけのような気がするのですが、最近はこれだとgitコマンドの補完は効くけどプロンプトに表示する__git_ps1が定義されていないようです。

解決策としては、GitHub上のgitのリポジトリにある/contrib/completion/git-prompt.shを使う必要があるようです。

GitHub - git/git: Git Source Code Mirror - This is a publish-only repository and all pull requests are ignored. Please follow Documentation/SubmittingPatches procedure for any of your improvements.

リポジトリごとcloneして定期的に最新に追随するもよし、Rawファイルをダウンロードしてきてどこか適当なところにおいて使うもよしです。

git-prompt.shの使い方はファイルの中を見るとコメントで懇切丁寧に書かれていますので難しくないと思います。
zshでも同じファイルが使えるようです。


15時間でわかるGit集中講座

15時間でわかるGit集中講座

ドライブレコーダー Transcend D200 をコストコで購入

前から欲しかったドライブレコーダー
Transcendの製品が安い割に評判が良かったようなので買うならTranscendにしようと思っていたところ、コストコで税込み11,800円で売られているのを発見。当時の価格.comの最安値よりも1,000円ほど安かったので購入を決断。

取り付けは簡単にできましたが、最初に取り付けた位置がルームミラーに近すぎて干渉してしまったので、強力な両面テープを頑張って剥がして再度位置をちゃんと調節して取り付けし直すハメになりました。(製品は何も悪くありません。私が間抜けだっただけです)

私の車は比較的大きい方ですが、電源配線はAピラーやダッシュボードの下あたりを通してコンソールパネル付近のシガーソケットまで充分に届く長さのものが付属していました。むしろ小さい車だと長すぎてだいぶ余るかもしれません。

車のシガーソケットにもともと FM トランスミッタを装着していたので、それも同時に使うにはシガーソケットの分配器が必要でした。
分配器ももともと持っていたのですが使っていなかったので、これを期に引っ張り出してきて有効活用しました。
(↓は私の持ってるのとは違うけど、イマドキな感じならスマホの充電などに便利な USB ポート付きを買うといいと思います。)

D200 のセットアップは簡単にできました。
D200 自体が WiFi アクセスポイントになってスマホと接続してスマホに映像をダウンロードできたりします。WiFiSSID とパスワードはデフォルトのままだと恥ずかしいのでさっそく変更しました。

私の車ではシガーソケットの電源はエンジンと連動しているので、エンジンを掛けると録画が開始され、エンジンを切ると録画終了です。説明書にも書いてありましたが、車種によってはここが連動してないこともあるらしいのでバッテリ上がりにご注意。

スマホに専用アプリをダウンロードすると、ドライブレコーダーのカメラ映像をライブビューをしたりできます。

少しドライブして、ちゃんと録画出来ているかどうか確認します。

録画した動画を先ほどインストールしたアプリでスマホにダウンロードできたりしますが、WiFI 経由でのダウンロードは結構時間がかかります。それにスマホの記憶領域を消費します。しかも D200 に電源を供給した状態でそういったことをしないといけないので、エンジンを掛けたままだとガソリンがもったいないし、エンジンを切ったままアクセサリーだけONにして使っているとなんとなくバッテリーが心配です。大したことないのかもしれないけど。そんなわけで、ちょっとだけスマホ連携を試したら D200 から SD カードを抜いて自宅の PC で動画を再生することにしました。

動画ファイルは1分ごとに別ファイルとなっており、SD カードの領域がいっぱいになったら古い方のファイルから順番に消されていくようです。
最初から付属していた SD カードは 16 GB だったと思いますが、1分で大体 100MB 以上消費するようなので録画できる時間は2時間強といったところでしょうか。
画質のモードを変えたりしたらもっと長時間撮れるかもしれません。

ファイルが1分ごとに細切れになっているので、連続再生するには VLC を使うと便利です。
VLC はあるフォルダに入っている動画を連続再生する機能を持っていますのでそれを利用して再生します。

ドライブレコーダーの映像が必要になるような事故などのシーンには出会いたくありませんが、近頃はドライブレコーダーでたまたま撮れたおもしろ映像とか貴重なシーンなどがたくさんあるようなので、そういう映像が撮れるといいなと思います。

$0書き換えが流行っているようなので一つ流行に乗っかってみるか

以下の記事では $0 での偽装を見破るため ps コマンドに c オプションを指定して /proc/<pid>/commを表示させればよいというようなことが書いてあると思いますが、/proc/<pid>/commも容易に書き換え可能みたいですのであまりあてにしないほうが良いでしょう。(偽装されていないかどうか知りたい場合)


細かすぎて伝わらないPerlと$0変数 - コマンド名偽装 - ろば電子が詰まっている


/procファイルシステムについて解説した以下のドキュメントを読むと、
https://www.kernel.org/doc/Documentation/filesystems/proc.txt

3.6	/proc/<pid>/comm  & /proc/<pid>/task/<tid>/comm
--------------------------------------------------------
These files provide a method to access a tasks comm value. It also allows for
a task to set its own or one of its thread siblings comm value. The comm value
is limited in size compared to the cmdline value, so writing anything longer
then the kernel's TASK_COMM_LEN (currently 16 chars) will result in a truncated
comm value.

だそうです。 It also allows for a task to set its own or one of its thread siblings comm value. つまり、/proc/<pid>/commは書き込み可能だということ。

実際、

$ echo $$
8837
$ ls -l /proc/$$/comm
-rw-r--r-- 1 xxxx xxxx 0  Jan 16 14:58 /proc/8837/comm  # <- ownerは書き込み可能
$ cat /proc/$$/comm
bash
$ ps c
  PID TTY      STAT   TIME COMMAND
 8837 pts/1    Ss     0:00 bash
 8906 pts/1    R+     0:00 ps
$ ps x | \grep $$
 8837 pts/1    Ss     0:00 bash
 8912 pts/1    S+     0:00 grep 8837
$ echo -n 'hoge' > /proc/$$/comm        # <- 書き換えてみる
$ cat /proc/$$/comm
hoge                                    # <- はい書き換わったー
$ ps c
  PID TTY      STAT   TIME COMMAND
 8837 pts/1    Ss     0:00 hoge         # <- 偽装されている (/proc/<pid>/commに設定した値が表示されている)
 8926 pts/1    R+     0:00 ps
$ ps x | \grep $$
 8837 pts/1    Ss     0:00 bash         # <- こちらは $0 で偽装可能なほう
 8932 pts/1    S+     0:00 grep 8837

ということで、/proc/<pid>/comm/proc/<pid>/cmdline も偽装されるので良い子のみんなは使っちゃダメだよ、ということになる。



上記ブログでは pstree を使えば /proc/<pid>/statを見るので安全って書いてたけど

$ cat /proc/$$/stat
8059 (hoge) S 5118 8059 8059 34847 8549 4218880 4300 26818 0 5 6 4 27 40 20 0 1 0 27809766 29687808 1731 18446744073709551615 4194304 5184116 140734649026160 140734649024840 139834224170140 0 65536 3670020 1266777851 18446744071579312278 0 0 17 0 0 0 0 0 0 7282160 7319112 34164736 140734649034557 140734649034562 140734649034562 140734649036782 0
$ pstree | \grep hoge
     |         |         |         |                |-hoge-+-grep

どう見ても書き換えられてます。

じゃあ解決策はないのかというと、ぱっと思いついたところだと

$ file /proc/$$/exe
/proc/8059/exe: symbolic link to `/bin/bash'

という感じで/proc/<pid>/exeを見るしかないかなぁ。
これもなにか回避策がありそうだけど。


ちなみにこちらの環境は

$ uname -s -r -v -m -p -i -o
Linux 3.16.0-28-generic #38-Ubuntu SMP Fri Dec 12 17:37:40 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.10
DISTRIB_CODENAME=utopic
DISTRIB_DESCRIPTION="Ubuntu 14.10"

こんな感じです。

現場からは以上です。

Intel Edison の Ubilinux をシングルユーザモードで起動する方法

Intel EdisonをUbilinuxに入れ替えて遊んでいたら、起動スクリプトの書き方を間違えたせいでEdisonが起動しなくなってしまいました。

Intel Edison Kit for Arduino

Intel Edison Kit for Arduino


正確には、途中までは起動するのですが起動途中でブロックしてしまうようになり、しばらく経つとWatchdogが発動してリセットされてしまうようになりました。

ログインプロンプトも出てこないしsshdが起動する前にブロックしてしまっているようなのでログインすることができません。なので修復もできない・・・!
こういうときは焦らずにシングルユーザモードですね。

しかし、ubootでシングルユーザモードってどうやるんだろう・・・(汗)

以下の手順は、試行錯誤のすえに見つけ出したシングルユーザモードに到達する手順です。

続きを読む