rust
encoding
programming
memmap

rustでtailモドキを作ってみた

概要

初投稿になります。rustは初心者なので至らない部分があったらご指摘いただけると嬉しいです。
仕事でログファイルを眺めたりざっくり確認する機会が割と頻繁にあります。
今まではpowershellのコマンドを使っていたのですが、あまりに遅くてストレスが溜まっていました。
で、最近rustを始めていい機会、題材なので自分で実装してみようと思い立ったわけです。

追記 2018/07/12 コードを修正して最下段に記載しました。

環境とか諸々

windowsで動かすことを想定しています。使ったCrateはこんな感じです。

Cargo.toml
[dependencies]
memmap = "0.6.2"
encoding = "0.2.33"

memmapはファイルを後ろから読むのに使用しました。(普通にseekでもよいのかも。)
encodingはshift-jisのファイル読み込みのためです。

処理フロー

  1. 起動時にファイル名、初期表示行数、監視時間間隔を受け取る。
  2. memmapでメモリマップドファイルを作成しファイルの終端から改行の数を数えながら 初期表示行数分を出力する。同時に終了時のインデックスを覚えておく。
  3. 無限ループしながら監視時間間隔分スレッドをスリープし再度ファイルを読み込んで、 新しい行が増えていたら差分を出力する。

メモリマップドファイル

main.rs
//ファイルを開いてmemmap生成
fn get_memmap(file_path: &str) -> memmap::Mmap {
    let file = match File::open(file_path) {
        Ok(file) => file,
        Err(e) => {
            println!("file open error {:?}", e);
            std::process::exit(-1);
        }
    };

    let mmap = unsafe { 
        match MmapOptions::new().map(&file) {
            Ok(map) => map,
            Err(e) => {
                println!("memmap error {:?}", e);
                std::process::exit(-1);
            }
        }
    };

    mmap
}

ログが追記されたのがmmapに反映されていく想定でいたのですが、
ファイルがロックされるため諦めました。
MMapOptionsで制御できるのかもですが、ドキュメントを見ても判別できませんでした。
なので、現状は(この呼び出し元で)読み込むたびに生成、破棄を繰り返しています。

読み込み(初回)

main.rs
//初回に開いた際に出力開始インデックスを取得する。
fn get_read_start_pos(mmap: &memmap::Mmap, length: usize, disp_rows: i32) -> usize {
    let mut index: usize = length - 1;
    let mut counter = disp_rows;

    loop {
        if index <= 1 { 
            return 0;
        }

        let first_byte = index - 1;
        let second_byte = index;
        if &mmap[first_byte..first_byte + 1] == b"\r" && &mmap[second_byte..second_byte + 1] == b"\n" {
            index = index - 1;
            counter = counter - 1;
            if counter < 0 { 
                //最後に見つかった改行分は出力不要
                index = index + 2;
                break; 
            }
        } else {
            index = index - 1;
        }
    }

    index
}

改行コードの判定がすごくイケてない…けど、とりあえず動かすことを優先しました。
(これのせいでWindowsでしか動かないです。)

出力

main.rs
fn print_vec(buffer: Vec<u8>) {
    let mut index = 0;
    let mut output_vec: Vec<u8> = Vec::new();
    let slice_len = buffer.len();

    while index < slice_len  {
        if (index + 1) < slice_len && &buffer[index] == &b"\r"[0] && &buffer[index + 1] == &b"\n"[0] {
            //とりあえずUTF8でデコードし失敗したらshit-jis
            let cnv_string = if let Ok(output) = String::from_utf8(output_vec.clone()) {
                output
            } else {
                let mut chars = String::new();
                match WINDOWS_31J.decode_to(&output_vec, DecoderTrap::Replace, &mut chars) {
                    Ok(_) => {},
                    Err(e) => {
                        println!("parse error {:?}", e);
                        std::process::exit(-1);
                    }
                };

                chars
            };

            println!("{}", cnv_string);
            output_vec.clear();
            //最後に見つかった改行分は出力不要
            index = index + 2;
        } else {
            output_vec.push(buffer[index]);
            index = index + 1;
        }
    }
}

UTF-8でデコードしてエラーだったらshift-jisに決め打ちしました。
また、改行コード見てるのはご愛嬌で…

最後に

rustは書いていて面白いというのはよく聞きますし、実際面白いと思いました。
もう少し自由自在に扱えるように精進すればより一層楽しめそうです。
こんなコードでもpowershellと比べると比較にならない程、速かったです。

コードはここに置きました。良かったらご覧ください。

追記 2018/07/12

初回読み込み時の処理と出力処理を書き直しました。

main.rs
//初回に開いた際に出力開始インデックスを取得する。
fn get_read_start_pos(mmap: &memmap::Mmap, length: usize, disp_rows: i32) -> usize {
    let mut index: usize = length - 1;
    let mut counter = disp_rows;
    loop {
        if index <= 1 { return 0; }
        if &mmap[index..(index + 1)] == b"\n" {
            counter -= 1;
            //最後に見つかった改行分は出力不要
            if counter < 0 { 
                break; 
            } else {
                index -= 1;
            }
        } else {
            index -= 1;
        }
    }
    index
}

不要な変数を削除したのと\r\nで判定するのではなくて\nだけを見るように。
これでwindows以外でも動くはず(未確認)

main.rs
//バッファしたデータを出力する。
fn print_vec(buffer: Vec<u8>) {
    let lines: _ = buffer.split(|b| *b == b'\n');
    for line in lines {
        if line.len() == 0 { continue; }
        let cnv_result = if let Some(cnv_result) = encode(line) {
            cnv_result
        } else  {
            println!("parse error.");
            std::process::exit(-1);
        };

        println!("{}", cnv_result);
    }
}

Vec<u8>を\nでsplitしてその結果をデコードしながら出力します。
相当、すっきりしました。

main.rs
fn encode(buffer: &[u8]) -> Option<String> {
    if let Some(utf) = encode_utf8(buffer) {
        return Some(utf);
    } else {
        if let Some(sjis) = encode_shit_jis(buffer) {
            return Some(sjis);
        } else {
            return None;
        }
    }
}

fn encode_shit_jis(buffer: &[u8]) -> Option<String> {
    let mut chars = String::new();
    match WINDOWS_31J.decode_to(&buffer.to_vec(), DecoderTrap::Replace, &mut chars) {
        Ok(_) => Some(chars),
        Err(e) => {
            println!("Shift Jis parse error.{:?}", e);
            None
        },
    }
}

fn encode_utf8(buffer: &[u8]) -> Option<String> {
    if let Ok(output) = String::from_utf8(buffer.to_vec()) {
        Some(output)
    }  else {
        None
    }
}

出力部分、これはみたまんまです。