React 入門 (1) ~ 基本環境構築 ~

2018年度版、React、webpack、Babelのセットアップ手順をアップデートしてみた - Qiita

これをベースに入門してみる。 そんなに引っかかることがなく入門できた。

プロジェクトの初期化

$ npm init -y

必要パッケージのインストール

$ npm install --save react react-dom
$ npm install --save webpack
$ npm install --save webpack-cli

package.json に build タスク追加

{
  "scripts": {
    "build": "webpack --mode=production"
  },
}

babel のインストール

$ npm install --save babel-loader @babel/core
$ npm install --save @babel/preset-env @babel/preset-react

webpack.config.js の追加

module.exports = {
  module: {
    rules: [
      {
       test: /\.m?js$/,
       exclude: /(node_modules|bower_components)/,
       use: {
         loader: 'babel-loader',
         options: {
           presets: ['@babel/preset-env']
         }
       }
      }
    ]
  }
}

webpack-dev-server のインストール

$ npm i --save webpack-dev-server
$ npm i --save html-webpack-plugin html-loader

webpack.config.js の修正

const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
       test: /\.m?js$/,
       exclude: /(node_modules|bower_components)/,
       use: {
         loader: 'babel-loader',
         options: {
           presets: ['@babel/preset-env']
         }
       }
      },
      {
        test: /\.html$/,
        use: [
          {
            loader: "html-loader"
          }
        ]
      }

    ]
  },

  plugins: [
    new HtmlWebPackPlugin({
      template: "./src/index.html",
      filename: "./index.html"
    })
  ]
}

package.json の修正

  "scripts": {
    "build": "webpack --mode=production",
    "start": "webpack-dev-server --open --mode=production"
  },

.babelrc の作成

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

src/index.html の作成

<!DOCTYPE html>
<html lang="ja">
    <head>
       <meta charset="utf-8">
       <title>aaa</title>
   </head>
    <body>
        <div id="root"></div>
    </body>
</html>

src/index.js の作成

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
  <div>Hello React</div>,
  document.getElementById("root")
)

成果はここにアップした GitHub - bamchoh/react-study

ここから拡張する場合は、上記リポジトリをコピーしたあと

> npm install
> npm run build
> npm start

で復元したあと、拡張していけばいいのかなと思っている。

NFCカードリーダー pasori を使って NFCカードのIDを golang で読み出す

Windows 対応も済んだので、一旦記事を書いてまとめておく。

概要

SonyNFCカードリーダー (Pasori) で NFCのID情報を取得できるパッケージを書きました。

NFCカードリーダーは以下のものを使用しました。 www.amazon.co.jp

Github リポジトリは以下。 GitHub - bamchoh/pasori: pasori library for golang

参考にした記事/サイト一覧

Linux

Windows

Golangの他実装 (Windows)

こちらは 上記の felicalib の dll を呼び出して使用されているようです。

私の実装

Linux

github.com/google/gousb を使用しているため、libusb に依存しています。ご使用の際には libusb をインストールしてからご使用ください。

Windows

私のパッケージは ID のみを取得するため、felicalib ほど大がかりなライブラリを必要としていませんでしたので、felicalibで実装されている一部を Go で書き起こしました。ですので、Sony が提供している felica.dll のみに依存しています。Sonyのサイトより、felicaライブラリをインストールしてからご使用ください。このパッケージはC:/Program Files/Common Files/Sony Shared/FeliCaLibrary 以下に felica.dll が存在していることを期待しています。

syscall.Call に 構造体のポインタを渡す方法

ひょんなことから golang で dll を動的に呼び出さなくてならなくて、引数として構造体のポインタを渡さなくちゃならなかったんだけど、調べてもよくわからんかったのでメモしておく。

dll 呼び出し側は以下のようなC言語の実装になっているとする。

#include "stdio.h"

typedef struct struct1 {
    int x;
    int y;
    int ret;
    char name[8];
} struct1;

int add0(int x, int y)
{
    int ret;
    ret = x + y;
    return ret;
}

void add1(int x, int y, int *ret)
{
    *ret = x + y;
    return;
}

void add2(struct1 *args)
{
    args->ret = args->x + args->y;
    sprintf(args->name, "struct1");
    return;
}

golang から dll を呼び出すためにまず dll をLoadDLL でロードする必要がある。そのあと、関数を FindProcgolang の変数にバインドして Call メソッドでその関数を呼び出すんだが、渡す引数をすべて uintptr にしなければならない。Goの型を uintptr にするには変数のポインタを unsafe.Pointer で一度ラップしてからそれをさらに uintptr でラップすることでできる。ただ、配列や構造体は変数のポインタを渡してもちゃんと渡されなくて、第一要素のポインタを渡す必要があるようだ。

例えば var a [10]int のような場合は uintptr(unsafe.Pointer(&a)) ではなく uintptr(unsafe.Pointer(&a[0]) とする必要がある。構造体の場合の例では

var b struct {
  x int
  y int
}

このような構造体があったとしたら、uintptr(unsafe.Pointer(&b.x)) とすることでポインタが渡る。(ただ、複雑な構造体の場合は単純には渡せない可能性があり、その場合はどうするのかはまだ解明不足)

上記を踏まえたうえで、先の dll 内の関数を呼び出す golang のコードは以下になる。

package main

import (
    "C"
    "fmt"
    "log"
    "syscall"
)
import "unsafe"

type struct1 struct {
    x    int32
    y    int32
    ret  int32
    name [8]uint8
}

func main() {
    dll, err := syscall.LoadDLL("a.dll")
    if err != nil {
        log.Fatal("LoadDLL: ", err)
    }
    defer dll.Release()

    add0, err := dll.FindProc("add0")
    if err != nil {
        log.Fatal("FindProc(add2): ", err)
    }

    add1, err := dll.FindProc("add1")
    if err != nil {
        log.Fatal("FindProc(add2): ", err)
    }

    add2, err := dll.FindProc("add2")
    if err != nil {
        log.Fatal("FindProc(add2): ", err)
    }

    ret0, _, _ := add0.Call(1, 2)
    fmt.Println("add0:")
    fmt.Println(" ", ret0)

    var ret1 int
    add1.Call(1, 2, uintptr(unsafe.Pointer(&ret1)))
    fmt.Println("add1:")
    fmt.Println(" ", ret1)

    var ret2 struct1
    ret2.x = 1
    ret2.y = 2
    add2.Call(uintptr(unsafe.Pointer(&ret2.x)))
    fmt.Println("add2:")
    fmt.Println(" ", ret2.x)
    fmt.Println(" ", ret2.y)
    fmt.Println(" ", ret2.ret)
    fmt.Println(" ", string(ret2.name[:]))
}

以上

C#のライブラリ vs-streamjsonrpc とGoの標準添付パッケージ jsonrpc を通信させる方法

前回 Go と C# を名前付きパイプでつなぐ - bamch0h’s diary という記事を書いた。 その記事のまとめにも書いたように、RPCで通信できるのか興味があったので調べてみた 意外と大変だったのでブログに残しておくことにする。

TL;DR

  1. C#側のライブラリとして Microsoft/vs-streamjsonrpc を使ってみた
  2. Go側は標準添付のJSONRPCパッケージだと通信しない。
  3. 理由はvs-streamjsonrpcが送受信にヘッダとしてContent-Lengthを要求するから
  4. なので、JSONRPCのコーデックに渡すnet.Connをラップしてあげるとよい

Go側のパッケージ

Go側は標準のパッケージを使用することにした。参考にしたのは以下の記事

medium.com

Microsoft/go-winio で Go同士をjsonrpcでつながることを確認して次の工程へ

C#側のライブラリ

C# jsonrpc とかで検索すると JSON-RPC.NET がトップに出てくるけど、あまり良い印象がないのと Microsoft 公式のリポジトリということで以下のライブラリを選択した。

github.com

サンプル実装は以下のリポジトリが紹介されていた。

github.com

ただ、サンプルをそのまま使ってもGo言語とは通信できない。理由はいくつかあり、以下の通り。

  1. このライブラリがjsonデータを送信する前にヘッダ情報として Content-Length: <jsonデータ長>\r\n\r\n を送信するから
  2. Goの標準ライブラリはparamsを配列で来ているものとみなして1番目の要素のみを取得するが、送信されてきたデータはmapなのでパースエラーとなってしまう
  3. 1番目の内容とも絡む話だが、Go側からjsonデータを送信する前にContent-Lengthを送信してあげないとC#ライブラリ側でちゃんと受信してくれない

諸問題を解決する

  1. の問題に対応するため、Go側でヘッダ情報をパースする。といっても、今回は vs-streamjsonrpc と通信する。という割り切りの元、Read待ちしているときの初めのパケットを捨てる。という対応で何とかしのぐことにする。

  2. の問題はGo側で対処するよりも、C#側で対処したほうがやりやすい。Go側の引数と同じフィールドを持つクラスを作成し、それを引数として送信してあげることにする。

  3. の問題はGo側でjsonデータを送信する前に割り込んでContent-Lengthを送信する必要がある。だが、jsonデータの受信から送信までの流れをServeCodec()が一手に引き受けているため外側からはどうにもできない。そこで、案が2つあり、一つはServeCodec()に渡す引数のrpc.ServerCodecを新たに作成し、jsonデータを送信する前にContent-Lengthを送信するように修正する。というもの。もう一つは、jsonrpc.NewServerCodec() に渡す io.ReadWriteCloser をラップしてWrite()が呼ばれたときに、実データを送る前にContent-Lengthを送信する。というもの。前者は別の理由(JSONRPCのバージョン情報である、jsonrpc: キーをjson内に含めたいという理由)で修正を行っていたが、標準ライブラリのほとんどをコピーしてこなければならず、あまり筋の良い方法だとは思えなかったので、後者に切り替えた。(こっちもWrite()する前に必ずContent-Lengthを送信するので、それはそれでどうなんだろう? とも思ったりする)

Go側の実装

というわけで以下のように実装した

package main

import (
    "fmt"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "time"

    "github.com/Microsoft/go-winio"
)

// --- ラッパー部分 ----

// Accept() で ラップした net.Conn を返したいのでwinioのListenerもラップする
type localWin32PipeListener struct {
    l net.Listener
}

func (l *localWin32PipeListener) Accept() (net.Conn, error) {
    conn, err := l.l.Accept()
    if err != nil {
        return nil, err
    }
    // Accept()で取得したconnをラップして返す
    return &wrappedConn{conn}, nil
}

func (l *localWin32PipeListener) Close() error {
    return l.l.Close()
}

func (l *localWin32PipeListener) Addr() net.Addr {
    return l.l.Addr()
}

// net.Conn をラップした構造体
type wrappedConn struct {
    c net.Conn
}

func (c *wrappedConn) Read(b []byte) (n int, err error) {
    var m int
    tmp := make([]byte, 65535)
    m, err = c.c.Read(tmp)
    if err != nil {
        return 0, err
    }
    newline := 0
    for i := 0; i < m; i++ {
        // Content-Lengthを受信している前提で読み飛ばす
        if tmp[i] == '\n' {
            newline++
            continue
        }

        // body を buffer に詰め込む。bufferは512バイトなので
        // 受信データがそれ以上あるとコケる。
        // ここら辺は後で実装を詰める必要あり
        if newline >= 2 {
            for j := 0; j < len(tmp[i:m]); j++ {
                b[j] = tmp[i+j]
            }
            return len(tmp[i:m]), nil
        }
    }
    return 0, nil
}

func (c *wrappedConn) Write(b []byte) (n int, err error) {
    // 標準パッケージでは jsonrpc 2.0 用のバージョンが付加されないので
    // 無理やりつける
    // これがないと、クライアント側で受け取ってくれない
    prefix := []byte(fmt.Sprintf("{\"jsonrpc\":\"2.0\","))
    body := append(prefix, b[1:]...)
    // 実データを書き込む前に常にContent-Lengthを送信する
    header := []byte(fmt.Sprintf("Content-Length: %v\r\n\r\n", len(body)))
    fmt.Print(string(header))
    fmt.Print(string(body))
    return c.c.Write(append(header, body...))
}

func (c *wrappedConn) Close() error {
    return c.c.Close()
}

func (c *wrappedConn) LocalAddr() net.Addr {
    return c.c.LocalAddr()
}

func (c *wrappedConn) RemoteAddr() net.Addr {
    return c.c.RemoteAddr()
}

func (c *wrappedConn) SetDeadline(t time.Time) error {
    return c.c.SetDeadline(t)
}

func (c *wrappedConn) SetReadDeadline(t time.Time) error {
    return c.c.SetReadDeadline(t)
}

func (c *wrappedConn) SetWriteDeadline(t time.Time) error {
    return c.c.SetWriteDeadline(t)
}

// ---- 本体 ----

var pipename = `\\.\\pipe\winiotestpipe`

type (
    TestRPC struct{}
    RPCArgs struct {
        A, B int
    }
)

func (t *TestRPC) Add(args *RPCArgs, reply *int) error {
    fmt.Println("args.A", args.A)
    fmt.Println("args.B", args.B)
    *reply = args.A + args.B
    fmt.Println("Result", *reply)
    return nil
}

func main() {
    s := rpc.NewServer()
    t := &TestRPC{}
    s.Register(t)

    l, err := winio.ListenPipe(pipename, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer l.Close()

    // Listener をラップする
    ll := &localWin32PipeListener{l}

    conn, err := ll.Accept()
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()
    s.ServeCodec(jsonrpc.NewServerCodec(conn))
}

C#側の実装

rpc.InvokeAsync() を実行するときの引数の指定に注意。それ以外はデフォルトで通信する。(Go側で頑張ってるからね)

using System;
using System.Threading.Tasks;
using System.IO.Pipes;
using StreamJsonRpc;

namespace StreamJsonRpcServer
{
    // 引数指定用のクラスを定義する
    public class Add
    {
        public int A;
        public int B;
    }

    class Program
    {
        static async Task Main(string[] args)
        {
            await MainAsync();
        }

        static async Task MainAsync()
        {
            using (var stream = new NamedPipeClientStream(".", "winiotestpipe", PipeDirection.InOut, PipeOptions.Asynchronous))
            {
                await stream.ConnectAsync();
                var rpc = JsonRpc.Attach(stream);
                // メソッド名は TestRPC.Add とすること
                // 引数をクラスで指定する
                int sum = await rpc.InvokeAsync<int>("TestRPC.Add", new Add { A = 3, B = 5 });
                Console.WriteLine($"3+5={sum}");
            }
        }
    }
}

まとめ

C#vs-streamjsonrpc と Go の標準ライブラリ jsonrpc をラップしてnet.Conn経由して通信するようにした。 vs-streamjsonrpc がなぜ Content-Length を要求するのかがわからないが、何か理由がるのだろう。ただ、その部分で汎用性が低くなっているのでは?という気がする。使用したvs-streamjsonrpc のバージョンが1.4だったのだが、github上では2.0 betaがあったので、もしかしたらそのバージョンではより簡単に通信できるようになっているかもしれない。また、2.0 beta では 自由にハンドラを変更できるようになっているので、2.0がリリースされたら高速なハンドラを使用して通信できるようにするのも楽しいかもしれない。

Go と C# を名前付きパイプでつなぐ

昨日、UNIXドメインソケットをGoで使うサンプルを作成したがその際にパフォーマンス比較用に作成した名前付きパイプを使ってC#と通信する。

サーバーサイド(Go言語)

特筆するところはない。

package main

import (
    "log"

    winio "github.com/Microsoft/go-winio"
)

var pipename = `\\.\pipe\winiotestpipe`
var ok = []byte("ok")
var bufSize = 65537

func main() {
    l, err := winio.ListenPipe(pipename, nil)
    if err != nil {
        return
    }
    defer l.Close()

    for {
        func() {
            conn, err := l.Accept()
            if err != nil {
                log.Fatal(err)
            }
            defer conn.Close()
            buf := make([]byte, bufSize)
            for {
                for {
                    n, err := conn.Read(buf)
                    if err != nil {
                        return
                    }
                    if buf[n-1] == '\n' {
                        break
                    }
                }
                conn.Write(ok)
            }
        }()
    }
}

クライアントサイド(C#)

注意するところは以下の通り * 名前付きパイプのパスをGo言語側と同じにする必要があるので、\\.\pipe\ 部分を削除しておく。 * UTF8でエンコードしてあげることで、GO側でも扱いやすくする。おそらくRead()したバッファは逆にUTF8からUTF16にエンコードする必要があるのではないかと思っているが、コード簡略化のため実施していない。

using System;
using System.IO.Pipes;
using System.Text;

namespace ConsoleApp1
{
    class Program
    {
        static void PrintBuffer(byte[] buffer)
        {
            for (int i = 0; i < buffer.Length; i++)
            {
                Console.Write(Convert.ToChar(buffer[i]));
            }
        }

        static void PrintlnBuffer(byte[] buffer)
        {
            PrintBuffer(buffer);
            Console.WriteLine();
        }

        static void Main(string[] args)
        {
            var client = new NamedPipeClientStream("winiotestpipe");
            client.Connect();
            var enc = new UTF8Encoding();
            byte[] buffer = enc.GetBytes("a\n");
            int len = buffer.Length;

            PrintBuffer(buffer);
            client.Write(buffer, 0, len);
            client.Flush();

            byte[] rxbuf = new byte[2];
            client.Read(rxbuf, 0, 2);
            PrintlnBuffer(rxbuf);

            Console.ReadLine();
        }
    }
}

まとめ

jsonとかでやり取りしたらRPC通信的なことができてGoをバックエンドとしたデスクトップアプリとか書けるのかなぁ?と思っている。

go 1.12 では Windows にも Unix ドメインソケットが入るので簡単に使ってみた。

参考

qiita.com

go 1.12 beta1 のインストール

ここ(Google グループ) に書いてある通りにダウンロード。

> go get golang.org/dl/go1.12beta1 
> go1.12beta1 download 

簡単にインストールできるし、Windows でもコマンドが同じなのが便利。

ベンチマーク

単純にサーバーとクライアントを作るのもつまらないので、go test -bench で通信のベンチマークをとってみる

サーバーサイド

net.Listen に渡す引数を tcp から unix に変えて、IPアドレスとポートの代わりにUNIXドメインソケット用ファイルをアドレスとして渡すだけ。

package main

import (
    "log"
    "net"
    "os"
    "path/filepath"
)

var ok = []byte("ok")
var bufSize = 65537

func main() {
    proto := "unix"
    addr := filepath.Join(os.TempDir(), "unixdomainsocketsample")
    os.Remove(addr)

    listener, _ := net.Listen(proto, addr)
    defer listener.Close()

    for {
        func() {
            conn, err := listener.Accept()
            if err != nil {
                log.Fatal(err)
            }
            defer conn.Close()
            buf := make([]byte, bufSize)
            for {
                for {
                    n, err := conn.Read(buf)
                    if err != nil {
                        return
                    }
                    // 終端をチェック
                    if buf[n-1] == '\n' {
                        break
                    }
                }
                conn.Write(ok)
            }
        }()
    }
}

クライアントサイド

ベンチマーク用関数を定義。送信サイズごとに計測

package main

import (
    "log"
    "net"
    "os"
    "path/filepath"
    "strings"
    "testing"
)

func setup() net.Conn {
    proto := "unix"
    addr := filepath.Join(os.TempDir(), "unixdomainsocketsample")
    conn, err := net.Dial(proto, addr)
    if err != nil {
        panic(err)
    }
    return conn
}

func sendBase(conn net.Conn, times int, data []byte) {
    buf := make([]byte, 2)
    for i := 0; i < times; i++ {
        _, err := conn.Write(data)
        if err != nil {
            log.Println(err)
            return
        }
        conn.Read(buf)
    }
}

func BenchmarkSend1Char(b *testing.B) {
    conn := setup()
    defer conn.Close()
    benchmarks := []struct {
        name string
        data []byte
    }{
        {"00001-char", []byte(strings.Repeat("a", 1))},
        {"00127-char", []byte(strings.Repeat("a", 127))},
        {"00256-char", []byte(strings.Repeat("a", 256))},
        {"00512-char", []byte(strings.Repeat("a", 512))},
        {"01024-char", []byte(strings.Repeat("a", 1024))},
        {"02048-char", []byte(strings.Repeat("a", 2048))},
        {"04096-char", []byte(strings.Repeat("a", 4096))},
        {"08192-char", []byte(strings.Repeat("a", 8192))},
        {"16384-char", []byte(strings.Repeat("a", 16384))},
        {"32768-char", []byte(strings.Repeat("a", 32768))},
        {"65536-char", []byte(strings.Repeat("a", 65536))},
        {"131072-char", []byte(strings.Repeat("a", 131072))},
        {"262144-char", []byte(strings.Repeat("a", 262144))},
    }
    for _, bm := range benchmarks {
        var bmData []byte
        b.Run(bm.name, func(b *testing.B) {
            bmData = append(bm.data, '\n') // 終端追加
            b.ResetTimer()
            sendBase(conn, b.N, bmData)
        })
    }
}

計測

計測方法は、サーバー側を起動した後、ベンチコマンドをクライアント側で実行する。

サーバー側実行

> go1.12beta1 build -o server.exe server.go & server.exe

クライアント側

> go1.12beta1 test -bench .

結果

TCP、名前付きパイプとの実行結果を合わせて表にしてみた。TCPは標準の netパッケージを使用。名前付きパイプはMicroSoft/winio(https://github.com/Microsoft/go-winio) の実装を使用 () の中はサーバー側のバッファサイズ size は 送信データのサイズ

size Unix(8192) Unix(65537) Tcp(8192) Tcp(65537) Pipe(8192) Pipe(65537)
1 bytes 19900 ns/op 26439 ns/op 74849 ns/op 77649 ns/op 60250 ns/op 58899 ns/op
127 bytes 19230 ns/op 26299 ns/op 74449 ns/op 77105 ns/op 59733 ns/op 58099 ns/op
256 bytes 19319 ns/op 26499 ns/op 75199 ns/op 77009 ns/op 61233 ns/op 56000 ns/op
512 bytes 19790 ns/op 26119 ns/op 76249 ns/op 77757 ns/op 57700 ns/op 58233 ns/op
1024 bytes 21549 ns/op 27120 ns/op 76349 ns/op 77599 ns/op 60200 ns/op 57533 ns/op
2048 bytes 19020 ns/op 33019 ns/op 86399 ns/op 87699 ns/op 60050 ns/op 57549 ns/op
4096 bytes 25670 ns/op 31639 ns/op 93450 ns/op 96149 ns/op 60600 ns/op 59499 ns/op
8192 bytes 39780 ns/op 34859 ns/op 119399 ns/op 119499 ns/op 61300 ns/op 60166 ns/op
16384 bytes 44699 ns/op 33139 ns/op 169699 ns/op 167199 ns/op 65550 ns/op 63899 ns/op
32768 bytes 62300 ns/op 40339 ns/op 270600 ns/op 259000 ns/op 73649 ns/op 64999 ns/op
65536 bytes 84750 ns/op 44933 ns/op 496664 ns/op 493668 ns/op 86299 ns/op 75300 ns/op
131072 bytes 129299 ns/op 80049 ns/op 905476 ns/op 908032 ns/op 113600 ns/op 80750 ns/op
262144 bytes 213400 ns/op 119999 ns/op 1828996 ns/op 1816518 ns/op 188400 ns/op 117900 ns/op

f:id:bamch0h:20181222141600p:plain
ベンチ比較(Unix, Tcp, Pipe)

TCPUNIXドメインソケットの差が大きいのでTcp(8192)Tpc(65536)を除外してグラフ化

f:id:bamch0h:20181222141626p:plain
ベンチ比較(Unix, Pipe)

Unix ドメインソケット同士の比較だと 4096と8192の間ぐらいで速度が反転する。大きいデータを送信する場合はそのあたりを基準にチューニングする必要があるのかな? パイプとUnix ドメインソケットの比較だと、基本的にはUnixドメインソケットのほうが早いようだ。ただ、サイズが大きくなると速度が同等になるので、大きいデータを一括で送信するようなケースの場合はパイプのほうがいいのかもしれない。

まとめ

今回はUNIXドメインソケットをGO言語で使ってみた。 使うだけだとつまらないのでパフォーマンスを測定し、TCP、名前付きパイプとの速度と比較してみた。 UNIXドメインソケットはTCPより速いみたいなので、内部的な通信ではUNIXドメインソケットを使うほうがいいのかもしれない。 また、Unixドメインソケットはパイプより速いが、大きなデータを一括で送信する場合はパイプでも速度的な違いはない。

今後の展開

Go言語とそれ以外の言語で通信できるようなら色々楽しそうなので そういうことにもチャレンジしていきたい。

コンテナに入るのに docker ps してから docker start して docker exec するのめんどくさいから一度にできるコマンド作った

結論

作った。Goで。 github.com

経緯

パソコン起動してから、dockerのコンテナに入るのにコマンドをいくつも叩かないといけないのがめんどくさかったから。 叩くコマンドは以下の3つ * docker ps -a * docker start <コンテナ名> * docker exec -it -e <デフォルト環境変数> <コンテナ名> bash

コマンドの内容としては、単純で docker ps -a --format "{{.Name}}" で出力されたコンテナ名を cho で選択して、それを docker start に渡して実行した後、docker exec をコンテナ名とともに起動しているだけ。

cho に関しては、以下のリンクを参照。パイプで渡された文字列に対して行単位で選択ができるコマンド。今回作ったコマンドを実行するには必要なので、別途 go get https://github.com/mattn/cho でインストールしておく必要がある。 mattn.kaoriya.net

知見

docker ps の書式指定

docker ps--format というのがあり、表示するフォーマットをGoのtemplate書式で指定できる。 docker ps --format "{{.Name}}" とすれば、コンテナ名のみ表示され docker ps --format "table {{.ID}}\t{{.Labels}}"とすれば、ヘッダ行を追加した状態でIDLabel`が表示されるので便利。

go-pipeline の Output で返される文字列

docker pscho を繋ぐために、今回は go-pipeline (GitHub - mattn/go-pipeline) を使用したが、返される文字列をそのまま exec.Command() の引数として使うとエラーとなる。理由としては文字列の最後に改行文字が入っており、コンテナ名が存在しない。となるからだ。解決方法としては、最後の1バイトを取り除いた部分文字列を渡すことで解決した。

docker exec が動かない

上記問題を解決したうえで docker exec が実行時にエラーとなっていた。原因は exec.Command で作成した Cmd 構造体の Stdin と Stdout にちゃんと標準入力と標準出力をセットしていなかったことが原因だった。nil のままだと NULLデバイス(os.DevNull)にアクセスするようだ。(https://golang.org/pkg/os/exec/#Cmd)

今後の課題

  • 今は 環境変数を固定で指定しているが、コマンドを実行するタイミングで指定できるようにもしたい
  • また、デフォルトの環境変数を rcファイルに設定しておいて、それをデフォルトで読み込めるようにもしたい
  • 現状の名前が goto_docker_container と長いので、何か良い名前を付けてやりたい