ファイル更新監視ツールを作ってみた

  • 12
    Like
  • 2
    Comment
More than 1 year has passed since last update.

コメントにあるとおり、通知機能でしたらfsnotifyというものがありますので
機能的に欲しいのであれば、そちらをお使いになった方がよいと思います。
文中にもありますが、オリジナルで実装していて、色々面白い部分があり勉強になりました。
コマンド自体は必要なのでfsnotifyに載せ替えて引き続き作っていきたいと思います。

一応記事の元になったソースはタグで残しておきます。

以下が元記事です

現在開発環境が、Windowsで動作環境がLinuxなので、
WindowsからIDEで自動アップロードして動作確認をしています。

レガシー、、、というか汚いので、綺麗にしながら、少しずつテストを書いているけど、
エラーになったのがわかればいいので、マシン弱いし、Jenkins動かすのもどうかな?って思ったので
既存で存在するかもしれませんが、Go言語の勉強がてらに、
ファイル更新されたらテスト実行できるようにファイル更新を監視するツールを作ってみました。

実はgoconveyっていうBDD風のテストフレームワークの監視部分を抜き出そうと思ってたんだけど、
読むのに疲れて書くことにしました。

思ったより色々ノウハウがあったのでGitHubに登録して
Qiitaに投稿することにしました。もう少し開発しようと思ってます。

指定ディレクトリ以下を検索

ioutil.ReadDir() でファイルの一覧を取得します。

updateFileInfo() でファイルの更新時間とディレクトリだったら再帰処理を行います。

 func listFiles(rootPath, searchPath string) error {

      fis, err := ioutil.ReadDir(searchPath)
      if err != nil {
          return err
      }

      for _, fi := range fis {
          fullPath := filepath.Join(searchPath, fi.Name())
          err := updateFileInfo(rootPath, fullPath)
          if err != nil {
              return nil                                                      
          }
      }
      return nil
  }

ここで情報を蓄えたり、監視を行ったりします。
goroutineで呼び出します。

更新時間の判定

os.Stat() でFileInfoを取得して、
貯めこんでおいた FileInfo.ModTime() と比較しています。

func isSpike(fullPath string, fInfo os.FileInfo) bool {

    if mode {
        return false
    }
    src := targetFiles[fullPath]
    if src != nil {
        if fInfo.ModTime() != src.ModTime() {
            return true
        }   
    } else {                                                                
       return true
    }   

    return false
}

modeは、すでに更新があった場合に
更新情報のみの更新を行うので処理は続けるので用意したものです。

更新判定後

Spikeの判定がtrueだった場合にチャネルで呼び出します。

送信側
    if isSpike(path, fInfo) {
        mode = true
        spike <- path                                                           
    }
受信側
    for {
        select {
        case triger := <-spike:
            match, _ := regexp.MatchString(ignore, triger)
            if !match {
                fmt.Println(triger)
                out, _ := exec.Command(cmds[0], cmds[1:]...).CombinedOutput()
                fmt.Println(string(out))
            }
        }
    }

受け側はfor{}で待ち受けて、ignore外の判定されたら処理を行います。
ignoreはcacheなどを無視する時に使ってます。

待ち受け

引数durationで待ち時間で処理します。ここもチャネルです。
更新の受けと同じ場所でtime.After() で待ち受けます。

        case timer := <-time.After(time.Duration(duration) * time.Second):
            if patrol {
                fmt.Println(timer)
            }
            mode = false
            go search(target)

実行

コマンドの引数処理はflagで行いました。
引数は更新時に実行するコマンドです。

go run main.go "go version"

今のところ引数は

  • -duration=10: 検索までの時間(秒)
  • -ignore="cache": 無視する名称
  • -patrol=false: 検索時のタイムスタンプ表示
  • -target="workdir": 検索するディレクトリ(デフォルト時は実行パス)
  • -version=false: バージョン表示

ですね。

バージョンの指定

go build -ldflags "-X main.version 0.0.0" -o ../bin/watcher main.go

って感じでビルドすればバージョンで配布できます。
ビルド時に自分で書くからあんまり意味ないな。。。なんかうまい方法考えます。

作成してみて

コマンド書いたの初だったので色々勉強になりました。

もともとPHPテスト用に書いたのですが、手元で簡単にgo testもぶん回そうと思ってます。

コマンド実行中に検索しないようにしたり、例外とか、*.goとかの指定もできるようにしようかな?
一応引き続き開発してgo install とかもやってみたいと思っています!