d.sunnyone.org
sunnyone.org

ページ

2016-04-13

gtk-rs のチュートリアル的文書を書いた

Gtk+のRustラッパーであるgtk-rsの入門を書いた:RustとGtk+で開発するGUIアプリケーション

ほんとはもう少しやるといいことがあるのだけど(GResource、スレッド対応など)、まぁとりあえずここまで書いてあればGUIアプリケーション書いたことある人であれば感じは掴めるのかなと。ということでRev0.1。

(少なくともいまのところは)ニーズはほとんどないと思うけど、やる気があるうちにある程度形にしておかないと絶対書かないので。自分がこれから書いていくアプリケーションを読み進めるにあたっての基礎知識となれば。

2016-04-10

メタプログラミングについて話してきました

ゆるスタック勉強会 #20 で「メタプログラミングとは」という内容で話してきました。「メタプログラミングって何?イメージがつかない」という話を受けて、イメージをつくようにするのがこの話の目的です。



本当はそれぞれの言語のやりかたについて手順を書けばよかったのですが、力つきたので、実際のライブラリから例を持ってくる方向性にしました。話している間はコードや別の資料も見ながら話していたので、このスライド自体というよりは、各スライドでポイントしているライブラリの実装あるいは類似するライブラリの実装を読んでもらうのがイメージをつかみやすいんじゃないかと思います。

それぞれの言語でニワカなところがあるので、全方位からツッコミを受けてもおかしくない内容だと思っています。何かあれば補足していきたいのでぜひどうぞ。

2016-04-01

Rust と C言語 をコールバックで行き来する(Cブリッジが必要なVer)

Rustは C FFI が強いので、はっきりしたstructとCの関数を呼び出すなんていうケースでは、それっぽいstructとexternでの関数の定義を書けば呼び出せる。コールバックもできる。
他言語関数インターフェイス

しかし、以下のようなケースではちょっとやりにくい。
  • 必要な関数の呼び出しにCマクロが必要
  • opaqueなstructの下のほうのメンバにアクセスが必要
要はC言語を解釈してもらえると楽だよねという話なので、「Cでラッパー書いてautotoolsだMakefileだなんだかんだ、ようやくつなげる」となるかと思いきや、このようなCラッパーが必要な状況についてもRustには補助がある。今回はそのやりかたについて紹介する。

自分が必要だったところがC世界からのコールバック呼び出しだったので、それについても触れる。基本的には、RustのアプリケーションがCライブラリを使うという想定。コールバックなので、行ったり来たりなのだけどね。

やりかたの概要

やりかたは概ね以下の通り。
  1. ラッパー関数をCで用意する
  2. CargoのビルドスクリプトでCソースのコンパイル方法を指示する
  3. ラッパー関数をexternで定義する
  4. ラッパー関数を呼び出すstruct & traitを定義する
  5. 実際に呼び出す
実際のソースはhttps://github.com/sunnyone/rust-bridgesampleに置いてあるので、ソースがあればいいという方はこちらでどうぞ。

やりかた

ラッパー関数の準備

Rustから呼び出したい関数を、普通の関数呼び出しにできる形で定義する。本来は何かのコードのラッパーの想定なのだが、そっち側の知識が必要となると話がややこしくなるので、ここでは単なるCソースを例として使う。



上記がsrc/bridge.cに置かれているとする。

Cargo.toml + build.rsにCソースのコンパイル方法の指示する

ここがこの記事のキモ。cargoに、置いたCソースのコンパイル方法を指示する必要がある。

まず、Cargo.tomlにビルドスクリプトが「build.rs」にあるよ、ということを指示する。またコンパイラの実行の面倒を見てくれる「gcc」crateをbuild dependenciesに入れる(gccと言っているが、MSVCも対応しているらしい。試してないが)。

[package]
(略)
build = "build.rs"

[build-dependencies]
gcc = "0.3"

[dependencies]
libc = "0.2"

dependenciesのlibcは、ビルドスクリプト的には必要ないが、C FFIではほぼ使うのでここで入れておく。

上記で定義した「build.rs」に、コンパイラの呼び出しを定義する。

extern crate gcc;

fn main() {
    gcc::compile_library("libbridgesample.a", &["src/bridge.c"]);
}

このあたりの記述、詳しくはCargoのドキュメント「Build Script Support」を参照。
本来ライブラリのリンクの指示も必要だが、ここでは一旦省く。詳しくは上記ドキュメントを参照(下でもすこし触れる)。

ラッパー関数をexternで定義する(いわゆる-sys部分)

ここからはもうRust onlyでのFFIと一緒。externでfnとstructを定義する。opaqueなstructをenumを定義するのがイディオムだそうだ。

Cargoの流儀的には、FFIのためのRustらしくない定義はなんちゃら-sysというcrateに定義するほうがそれらしい。この部分だけの差し替えがしやすいから、とのこと。

ラッパー用のstructを用意する

ffi層を叩く、Rust 的構造にあわせた呼び出し部を用意する。今回は、コールバックをクロージャでもらい、それを実際のCコールバックから呼ぶ形にした。

「外側」と「内側」の2つのstructを作る

Rust世界のコード用のデータを入れておく「内側」と、その「内側」とC関数からもらったポインタを格納しておく「外側」を用意する。


必ずしもこうでなくてよいが、こうしておくことで「内側」をBox(ヒープ)で持つことが出来、ポインタに変換してC関数のポインタに渡すことができる。

git2-rsのsrc/transport.rsでやっていたので参考にした。

Drop traitを実装しポインタが自動で捨てられるようにする

後始末大事。


コールバックとしてC側に渡す関数をexternで定義する

今回はライフタイムをRustが持っているので、キャストするだけ。ここでは、Rust側がライフタイムを握っているのでポインタを渡しているが、もしC側がライフタイムを握るようであれば、Box::into_rawBox::from_rawをうまく使うといいと思う。ライフタイムの制御から外せる。mem::forget というのもあるかな。


ラッパーを実装

あとはffi関数を呼び出すようMyStructにimplする。


呼び出しコードを書く

実際呼ぶコードはこんなかんじになる。 結果はこんな感じ。
mystruct_new
Hello, World: 11
Hello Result: 12345
mystruct_free
これでだいたい使えるはず。

おまけ:pkg-configでライブラリを参照させる

本来ラップする対象があるはずなので、リンク対象やincludeパスを指定する必要がある。基本的には、println!()で、cargoが必要な情報を渡してあげればよいが、build-dependenciesでpkg-config crateを使うと、pkg-configの結果を使える。例えばこんな感じ。 with-glib branchで使ってみてある。実際のところは、panicはやっつけなのでちゃんと分岐させたり、pkg-configに関わらず環境変数で設定できるようにしたりと、工夫がされていることが多いので、詳しくはそこらのライブラリのコードを参照。例えば、glib-sys crateはこんな感じ

2016-03-15

Rust の開発環境を作る (Ubuntu + Visual Studio Code)

今回はRustとその開発環境のセットアップの仕方について。Ubuntu 14.04環境に、Visual Studio Codeをインストールして、コード補完ができるようになるまで。Ubuntu Makeのところ以外は、Macでも大体同じじゃないかなと思う。


Rust コンパイラまわり

multirustのインストール

multirustは、stableやnightlyなどrust環境を切り替えられるようにするツール。普通にrustcをインストールしてもよいが、便利なのでmultirustで入れておく。

$ curl -sf https://raw.githubusercontent.com/brson/multirust/master/blastoff.sh | sh
(略)
$ rustc --version
rustc 1.7.0 (a5d1e7a59 2016-02-29)

rustc的にはこれでおしまい。

Visual Studio Codeまわり

アーカイブを展開するだけでも良いのだけど、便利なUbuntu Makeツールを使う。

Ubuntu Makeのインストール

$ sudo add-apt-repository ppa:ubuntu-desktop/ubuntu-make
$ sudo apt-get update
$ sudo apt-get install ubuntu-make
新しいUbuntuではubuntu-makeのppaはいらないかもしれない。

Visual Studio Codeのインストール

$ umake ide visual-studio-code
(インストールパスを聞かれるけどデフォルトでこんな感じに。)
Choose installation path: /home/yoichi/.local/share/umake/ide/visual-studio-code 

便利なので本来のコマンド名codeでシンボリックリンクを貼っておく。
$ ln -s ~/.local/share/umake/ide/visual-studio-code/code ~/.local/share/umake/bin/code

visual-studio-codeのリンクが壊れているようだけど、Gitでは直っているようなので、少しするとvisual-studio-codeでも起動できるようになると思う。

RustyCode extensionのインストール

VSCodeのRust言語用拡張のRustyCodeをインストールする。codeで起動後、Ctrl+Shift+pでコマンドパレットを起動し、install RustyCodeでインストール。

開発支援ツールまわり(racer, rustfmt)

Codeを起動してRustyCodeで書こうとすると、以下のようにエラーが出るので、必要なツールを入れる。
The "racer" command is not available. Make sure it is installed.

racer (コード補完ツール) のインストール

$ cargo install racer
(略)
  Installing /home/yoichi/.multirust/toolchains/stable/cargo/bin/racer
be sure to add `/home/yoichi/.multirust/toolchains/stable/cargo/bin` to your 
PATH to be able to run the installed binaries
cargoに言われる通りPATHを~/.bashrcなどに設定する

export PATH=$PATH:~/.multirust/toolchains/stable/cargo/bin

その後racerを実行するとわかるが、racerには参照用にRustのソースが必要なので、ダウンロードして適当なところに置いておく。

$ racer
RUST_SRC_PATH environment variable must be set to point to the src directory of 
a rust checkout. E.g. "/home/foouser/src/rust/src"
$ mkdir ~/src
$ wget https://static.rust-lang.org/dist/rustc-1.7.0-src.tar.gz
$ tar zxvf rustc-1.7.0-src.tar.gz

racer用にソースの配置場所を環境変数に設定する(~/.bashrcなどに書く)
export RUST_SRC_PATH=~/src/rustc-1.7.0/src

racerコマンドを叩いてusageが出ればOK。

rustfmt (ソースコードフォーマッター) のインストール

なくてもエラーは出ないが、入れておくとCtrl+Shift+I でコードフォーマットできる。cargo fmtでもOK.

インストールはcargoで。
$ cargo install rustfmt

Hello Worldプロジェクトの作成

試すには、適当にプロジェクトを作成してCodeで開く。
$ cargo new hello --bin
$ code hello &

src/main.rsを適当に編集したら、Ctrl+Shift+Rか、コマンドパレット(Ctrl+Shift+p)からRun Debugで実行できる。

2016-02-08

PHPが出力するsyslogの「ool www」の謎を追う

PHPからerror_log()でsyslogを吐いたときに出力される「ool www」という謎の文字列が一体なんなのか調べた。いいから回避策が知りたいという人はこちらへ。おまけが本編という人もいるかもしれない。

発端

とあるPHPアプリケーションがエラーログをPHP標準関数error_log()で吐く。それをフィルタしたくなったのだが、error_log()にはたいしたフックポイントもなく、アプリケーション自体には手を入れたくないので、とりあえずsyslogで受けてSyslogサーバーがなんとかするのがよいかと思って「error_log」の設定を「syslog」にしてみると、なぜかこんな感じに……

Feb  7 22:02:36 phptest ool www: LOG CONTENT

……この「ool www」は一体なんなんだ? Out-of-L...うーん。ここの文字列だけなら実害はないが、なんでこんなことになっているのかわからないので調べることにした。

テストファイル

当該のPHPアプリケーションは置いといて、1ファイルでも再現するので、以下のPHPプログラムでテストすることにする。
<?php error_log("ERRORTEST"); ?>

環境はUbuntu 14.04にnginx + php5-fpm。設定値「error_log」を「syslog」にしておく。あまりバージョンに拘っていないので、リンクが適当だったりするが気になるかたは必要なバージョンのソースを見に行ってほしい。

ソースを追う

error_log()関数の流れ

くだんのerror_log()関数をPHPから呼び出すと、こんな感じで動く。

まず、error_logのPHP_FUNCTIONの記述がext/standard/basic_functions.cにある。ここが実体である_php_error_log_ex()を呼び出す。

ext/standard/basic_functions.cの_php_error_log_ex()は、switchで引数に応じたログ出力(メールなど)を行う。今回のmessageのみの呼び出しではdefault: にたどり着き、php_log_err()を呼び出す。

php_log_err()は、main/main.cにあり、ここで「error_log」変数のチェックを行い、値が「syslog」のときはphp_syslog()関数でsyslogに出力している。

if (!strcmp(PG(error_log), "syslog")) {
 php_syslog(LOG_NOTICE, "%s", log_message);
 PG(in_error_log) = 0;
 return;
}

基本的にメッセージを引き回しているだけで、ここまでに変わった文字列処理は見受けられない。

php_syslog()関数とは

であればポイントはphp_syslog()であろうと、php_syslog()の定義をphp_syslog.hに見つけるも、こうなっていた。

なんとphp_syslog()はsyslog()であった。つまり、PHP内で何か特別な処理をすることなく、そのままsyslog()関数にメッセージを投げているだけである。

syslog関数の使い方

こうなってくるとsyslog(3)が大事になる。manがあり、日本語が古くないようなので日本語のsyslog(3)を見てみる。

プロトタイプとしては、こうなっているようだ。
#include <syslog.h>

void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void); 

呼び出しとあわせると、priorityがLOG_NOTICEで、formatが"%s"、%s部分としてlog_messageが渡ることになる。

説明を読むと
ident で指定した文字列は各メッセージの前に付与される。
とあるので「ool www」の部分はidentのようだ。

処理の中でとくにopenlog()する処理は見受けられなかったので
openlog() は必須ではなく、必要に応じて syslog() から呼び出される。 syslog() が呼び出した場合、 ident のデフォルト値は NULL になる。
の部分に該当し、
通常は ident にはプログラム名が設定される。 ident が NULL の場合、プログラムが ident として使用される
とあるので「ool www」はプログラム名のようだ。しかし「ool www」なんてプログラムはいないぞ?

syslog()関数の実装

こうなると、identがNULLだったときのopenlog()の動きを知りたくなる。そこでglibcを見たところ、openlog()の実体はmisc/syslog.cにある。

openlog()は内部のopenlog_internal()を呼び、ここでidentはLogTag変数に代入される。

LogTagがNULLのときの処理を探してみると、syslog()のログ処理の実体である__vsyslog_chk()に以下の記述があった。

if (LogTag == NULL)
      LogTag = __progname;

__prognameは何かと見ると、頭のほうでexternで宣言されていた。
extern char *__progname; /* Program name, from crt0. */
だいぶあやしい。

「__progname」とは

で、誰が__prognameをセットしているのかと探してみると「misc/init-misc.c」にあった。

void
__init_misc (int argc, char **argv, char **envp)
{
  if (argv && argv[0])
    {
      char *p = strrchr (argv[0], '/');
      if (p == NULL)
         __progname = argv[0];
      else
         __progname = p + 1;
      __progname_full = argv[0];
    }
}
__prognameは「argv[0]の最後の'/'より後」のようだ。なるほどargv[0]の役目を考えると妥当そうに見える。__progname_fullとしてargv[0]も保持されている。

php-fpmのargv[0]ってなんだっけ?

phpが動作するのは、php-fpmプロセス。そういえばphp-fpmは独自設定した文字列がpsで見えてたよなと見てみると、こうなっている。

$ ps -ef | grep '[p]hp-fpm'
root      2664     1  0 22:02 ?        00:00:00 php-fpm: master process (/etc/php5/fpm/php-fpm.conf)                    
www-data  2666  2664  0 22:02 ?        00:00:00 php-fpm: pool www                                                       
www-data  2667  2664  0 22:02 ?        00:00:00 php-fpm: pool www

php-fpmは明らかにargv[0]を書き換えている。勘のいい人ならもうお分かりだろう。一応検証するために、PHPを適当に展開してfpm.cのfpm_init()の頭と末尾(argvが書き換わる前と後)に以下のようにログを仕込んでみる(ヘッダやexternは適宜挿入)と...

zlog(ZLOG_NOTICE, "argv[0]: %s", argv[0]);

zlog(ZLOG_NOTICE, "__progname: %s", __progname);
zlog(ZLOG_NOTICE, "__progname_full: %s", __progname_full);

こんな風になる。

$ sudo ./sapi/fpm/php-fpm -F -y /etc/php5/fpm/php-fpm.conf 
[08-Feb-2016 00:25:23] NOTICE: argv[0]: ./sapi/fpm/php-fpm
[08-Feb-2016 00:25:23] NOTICE: fpm is running, pid 24381
[08-Feb-2016 00:25:23] NOTICE: __progname: ster process (/etc/php5/fpm/php-fpm.conf)
[08-Feb-2016 00:25:23] NOTICE: __progname_full: php-fpm: master process (/etc/php5/fpm/php-fpm.conf)
もう少しわかりやすくすると、こう。


そう、元々あった最後の/までの文字数分、新しい文字列の開始からスキップされている。ログ出力がだるいので例ではmaster processになっているが、pool wwwでどうなるかを想像するのはたやすいと思う。

結論:syslog tagに使われる__prognameを、プログラムパスであるargv[0]からディレクトリ部分を取り除いた文字列へのポインタとしてlibcが保存したあと、php-fpmがargv[0]を書き換えたため、謎の文字列「ool www」が生まれた。

一応Bug reportは作っておいた。PHPが対処すべき問題のように感じるけど、libcが文字列をコピーしておかないのが悪いって言われるとそうすかって感じなので、正直期待できない:https://bugs.php.net/bug.php?id=71544

回避策:identに妥当な値を設定するには?

暗黙のopenlog()をさせなければいい。つまり、期待のプログラムが動く前にopenlog()すればよい。

具体的には、以下のようなファイルを例えば/etc/php5/fpm/fixsyslog.phpと置いて……

php.iniのauto_prepend_fileで参照する。
auto_prepend_file = /etc/php5/fpm/fixsyslog.php

これで常にopenlog()が呼ばれるので現象は起こらない。

感想

原因に関しては、ソースを追うことですっきりさせることができた。オープンソースのありがたみって、やっぱりこういうところにあると思う。解決策は、もやもやが残るままだけど…

おまけ:php-fpmはどのようにargv[0]を書き換えているか?

php-fpmは見ての通りargv[0]を書き換えるのだけど、argvを全部足しても期待の文字数に足りないなんてこともあるはず。そんなときの処理がfpm_env.cに書かれていた。

ざっくり言うと「environがargvとメモリ空間上に連続で置いてあるから、一応確認しつつenvironにどいてもらって、そこをargv[0]として使いましょう」である。グロい!コメントによると、nginxやpureftpdもそうしているらしい。

この記事も最後なので該当コードを貼り付けておく。


2016-01-25

Ansibleを複数ホストへのコマンド投げツールとしてad-hocに使う

Ansibleは普通「playbook」と呼ばれるファイルを記述し、それを使って動かすのだけど、もっとてきとーに使っても便利なときは便利なので、今日はその使い方の紹介。実行する側は、ansibleがインストールされている必要はあるが(apt-get install ansibleでよい)、対象ホストはsshでアクセスできればOK。

複数ホストにコマンドを投げる

シンプルな例: 適当なファイルをcatする

もっとも理解が簡単なのは、権限だとかは気にせずにただコマンドが実行できればよい、という使い方。サーバがたくさんあるんだけど、ひとつひとつ実行するのはダルい。そんなときは、こんな風に実行すると、実行して結果を並べてくれる。

$ ansible -a "cat /etc/hostname" -i host1,host2 -u yoichi all
host1 | success | rc=0 >>
host1

host2 | success | rc=0 >>
host2

-aにコマンドを指定し、-iで対象ホストをカンマ区切りで並べ、-uにユーザを指定し、allをつける。

ユーザの指定なくsshできる状態なら、-uはなくてもいい。また、playbookを書いていてinventory fileを持っているなら、そのファイルを-iで指定したほうが当然楽。この例ではありがたみが全然ないが、もし1ホストにしたければ「-i hostname,」のように最後に,をつける。

sudoする例: crontabに設定された内容を見る

Ansibleをansible-playbookコマンドで使っている人にはお馴染みの、-sでsudo(-Kでパスワード)、-Sでsuさせることもできる。例えば、crontabに設定された項目を見たい、とか。
$ ansible -s -a "crontab -u root -l" -i host1,host2 all
host1 | FAILED | rc=1 >>
no crontab for root

host2 | success | rc=0 >>
# Edit this file to introduce tasks to be run by cron.
(略)
# m h  dom mon dow   command
30 5 * * * /usr/local/bin/rotate.sh

切り離す例: 一斉シャットダウン

停電準備で一斉シャットダウンしたいです、とかいうこともあると思う。shutdownのケースでは、結果を待てないので-Bオプションを使ってバックグラウンドで切り離す。

$ ansible -s -K -B 1 -a "shutdown -h now" -i host1,host2 all
SUDO password: 
background launch...

host1 | success >> {
    "ansible_job_id": "867312552566.13878",
    "results_file": "/root/.ansible_async/867312552566.13878",
    "started": 1
}

host1 | success >> {
    "ansible_job_id": "867312552566.2418",
    "results_file": "/root/.ansible_async/867312552566.2418",
    "started": 1
}

Ansibleのモジュールを活用する

上述の例はすべてコマンドだったが、当然ながら、Ansibleのモジュールも活用できる。

モジュール活用例1: サービスを再起動させる

例えば、サービス再起動では、serviceコマンドでもいいが、serviceモジュールを使うとこのようになる。

$ ansible -s -m service -a "name=myservice state=restarted" -i host1,host2 all
host1 | success >> {
    "changed": true,
    "name": "myservice",
    "state": "started"
}

host2 | success >> {
    "changed": true,
    "name": "myservice",
    "state": "started"
}

-mでモジュール名を指定し、-aでモジュールの引数を指定する。実は-aはいつでもモジュールの引数なのだが、-mのデフォルトが「command」なので、コマンドを実行できるというわけだ。

モジュール活用例2: copyモジュールでファイルを配置する

ad-hocに使う例ではない気がするが、その気になればファイルを持っていくことも可能。

$ echo hoge > hogefile
$ ansible -m copy -a "src=hogefile dest=/tmp/hogefile1" -i host1,host2 all
host1 | success >> {
    "changed": true,
    (略)
}

host2 | success >> {
    "changed": true,
    (略)
}

注意

あくまで一回限りで使うようなものであって、残したいものだったり、ansibleコマンドを3つも4つも打つようであれば、playbookを作るのがよい。上述のコマンドを羅列すればyaml書かなくても呼べるじゃん?っていうのはナシで。ベストプラクティス構成にしておくのがよいと思うが、1ファイルplaybookでもまぁansibleコマンドがたくさん書いてあるよりは良いと思う。

参考

こういうのをad-hoc commandsと呼ぶらしい→Introduction To Ad-Hoc Commands
他にも例がいくつかあるので、参考にされたい。

おわりに

「ansible」というソフトウェア名がついているコマンドがこの動作であるところから、もともとはこういう風に使うように始まったのではないかと私は勝手に思っている。現時点ではメインではない使い方になった(と思う)ので、知らなくても全然使えないことはないが、ベースの考え方はこのエージェントなしにリモートホストに任意にモジュールを投げ込んで実行するツールだと思うと、理解も応用もしやすいと思うので、触ってみておいて損はないかなと思う。

2016-01-10

avahi-browse で mDNS/DNS-SD のホスト・サービスの探索をする

apt-get install avahi-utilsして「avahi-browse -alr」とすればよい。たいした話ではないけれど、多分忘れるので。

出力例はこんな感じ。
$ avahi-browse -alr
+   eth0 IPv6 sample [00:11:22:33:44:55]                      Workstation          local
+   eth0 IPv4 sample [00:11:22:33:44:55]                      Workstation          local
+   eth0 IPv4 Jenkins                                         Web Site             local
+   eth0 IPv4 jenkins                                         _jenkins._tcp        local
+   eth0 IPv4 jenkins                                         _hudson._tcp         local
=   eth0 IPv6 sample [00:11:22:33:44:55]                      Workstation          local
   hostname = [sample.local]
   address = [fe80::82ee:1234:5678:9999]
   port = [9]
   txt = []
=   eth0 IPv4 sample [00:11:22:33:44:55]                      Workstation          local
   hostname = [sample.local]
   address = [192.168.1.152]
   port = [9]h
   txt = []
=   eth0 IPv4 Jenkins                                       Web Site             local
   hostname = [jenkins.local]
   address = [192.168.1.101]
   port = [8005]
   txt = ["version=1.643" "url=http://jenkins.example.local:8005/" "slave-port=46298" "path=/" "server-id=5ca870f7e5eb41888299af4853b2021b"]
=   eth0 IPv4 jenkins                                       _jenkins._tcp        local
   hostname = [jenkins.local]
   address = [192.168.1.101]
   port = [8005]
   txt = ["version=1.643" "url=http://jenkins.example.local:8005/" "slave-port=46298" "path=/" "server-id=5ca870f7e5eb41888299af4853b2021b"]
=   eth0 IPv4 jenkins                                       _hudson._tcp         local
   hostname = [jenkins.local]
   address = [192.168.1.101]
   port = [8005]
   txt = ["version=1.643" "url=http://jenkins.example.local:8005/" "slave-port=46298" "path=/" "server-id=5ca870f7e5eb41888299af4853b2021b"]