50 Commits

Author SHA1 Message Date
dengsgo
605914d1e1 增加 1.16 2020-08-23 16:22:08 +08:00
dengsgo
86cf40564b 优化 文件扫描性能 2020-08-23 16:17:18 +08:00
dengsgo
819297118c 更新 changelog 2020-07-19 18:10:34 +08:00
dengsgo
a76d4cfe6c 增加 对信号的处理 2020-07-19 18:06:24 +08:00
dengsgo
14ba79c2b3 更新 README 2020-06-27 15:15:29 +08:00
dengsgo
c5f0649ab5 增加 pid文件进程感知 2020-06-27 15:11:55 +08:00
dengsgo
7c6f8f910b 增加 snap config 2020-04-22 15:40:09 +08:00
dengsgo
d07616bde9 rename 2020-03-16 11:19:13 +08:00
dengsgo
4f81692954 typo deamon->daemon 2020-03-16 11:16:14 +08:00
Deng.Liu
588af5143f Update README.md 2020-03-15 21:10:11 +08:00
Deng.Liu
2d8924b7fe Update go.yml 2020-03-15 21:02:32 +08:00
Deng.Liu
08fb7aabf8 Update go.yml 2020-03-15 20:56:56 +08:00
Deng.Liu
9f0a51ce0e Update go.yml 2020-03-15 20:55:31 +08:00
Deng.Liu
8a3a2d570b Update go.yml 2020-03-15 20:53:42 +08:00
Deng.Liu
e5405f4fa9 Update go.yml 2020-03-15 20:46:58 +08:00
Deng.Liu
9d9cce0f31 Create go.yml 2020-03-15 20:43:56 +08:00
dengsgo
c0364a62bc 发行 v1.15 2020-03-08 13:26:38 +08:00
dengsgo
a9758653ba 修改 changelog 2020-03-08 13:24:49 +08:00
dengsgo
b1c5f7da39 修改 readme mod 2020-03-05 15:37:36 +08:00
dengsgo
29fc62de90 更改 mod管理依赖 2020-03-05 15:31:47 +08:00
dengsgo
2d409fbce0 修改 log 2020-03-04 13:41:02 +08:00
dengsgo
550dc01dfb 修改 readme 2020-03-04 11:18:21 +08:00
dengsgo
d0492bad8c 更新 readme 2020-03-03 15:01:08 +08:00
dengsgo
55625d51bc 增加 支持 2020-03-03 14:57:35 +08:00
dengsgo
8ddc124f89 增加 支持 2020-03-03 14:54:12 +08:00
dengsgo
8b7042411c 修改 rekeased 2020-03-03 11:39:15 +08:00
dengsgo
b72caedcbb 重构 指令 2020-03-03 11:37:20 +08:00
dengsgo
f989590d14 修改 文案 2020-03-03 11:12:08 +08:00
dengsgo
f2afa015bd 修改 版本 2020-01-05 14:47:46 +08:00
dengsgo
dc7162b57d 修改 配置 2020-01-05 14:44:56 +08:00
dengsgo
8828bcaed6 修复 错误 2020-01-02 18:32:16 +08:00
dengsgo
7d2e8dd34d 修复 panic 2020-01-02 18:22:16 +08:00
dengsgo
bb80977213 更新 changelog 2020-01-02 17:42:02 +08:00
dengsgo
1b4206a6b3 增加 ignore*指令支持 2020-01-02 17:38:52 +08:00
dengsgo
3351429515 重构 log 2020-01-02 17:25:02 +08:00
dengsgo
00e34193aa 更新 changelog 2020-01-02 16:58:01 +08:00
dengsgo
4e0b9f54f6 增加 指令支持 2020-01-02 16:38:26 +08:00
dengsgo
9defc968ba 优化 taskman 2019-12-27 18:09:56 +08:00
dengsgo
66eee100f2 忽略 pid 2019-12-27 16:46:36 +08:00
dengsgo
9e4642d0c2 更新 logo出现的时机 2019-12-27 16:43:49 +08:00
dengsgo
44bb0dd65f 增加 未知参数提醒 2019-12-27 16:31:00 +08:00
dengsgo
eeaad58d1e 更新 .travis 2019-12-24 11:50:21 +08:00
dengsgo
44051b9184 更新 .travis 2019-12-24 11:47:21 +08:00
dengsgo
a2daa98d08 更新 .travis 2019-12-24 11:35:51 +08:00
dengsgo
97bdcb644c 忽略 pid监听 2019-12-24 11:27:38 +08:00
dengsgo
a0137807f3 update echo PATH 2019-12-23 16:26:20 +08:00
dengsgo
b0a0e296d2 update echo PATH 2019-12-23 16:23:28 +08:00
dengsgo
e63d200382 更新 .travis 2019-12-23 16:18:03 +08:00
dengsgo
cb86b0dbe6 增加 守护进程 2019-12-23 16:13:31 +08:00
dengsgo
bead88c5e2 更新 travis script 2019-12-17 19:28:46 +08:00
19 changed files with 500 additions and 143 deletions

45
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.13
uses: actions/setup-go@v1
with:
go-version: 1.13
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: |
export GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct
go get
- name: Build
run: go build
- name: Use
run: |
export PATH=/home/runner/work/fileboy/fileboy:$PATH
fileboy version
fileboy help
fileboy init
cat filegirl.yaml
fileboy exec
fileboy daemon
ls -al .fileboy.pid
ps aux | grep fileboy
fileboy stop

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ fileboy-darwin-amd64.bin
fileboy-linux-amd64.bin
fileboy-windows-amd64.exe
./bin/*
.fileboy.pid

View File

@@ -11,11 +11,17 @@ dist: bionic # ubuntu 18.04
script:
- make init
- make build-linux
- ls bin
- ./bin/fileboy-linux-amd64.bin version
- ./bin/fileboy-linux-amd64.bin help
- ./bin/fileboy-linux-amd64.bin init
- make build-all
- ls -al bin
- echo $PATH
- cp bin/fileboy-linux-amd64.bin /home/travis/bin/fileboy
- fileboy version
- fileboy help
- fileboy init
- cat filegirl.yaml
- ./bin/fileboy-linux-amd64.bin exec
- fileboy exec
- fileboy daemon
- ls -al .fileboy.pid
- ps aux | grep fileboy
- fileboy stop

View File

@@ -1,3 +1,45 @@
### Release v1.16
2020.08.23
- 优化 文件扫描性能
2020.07.19
- 增加 pid 文件处理
- 增加 信息处理
2020.03.16
- typo deamon->daemon
### Release v1.15
2020.03.08
- 优化 指令模式
- 使用 mod 管理依赖
- go version >= 1.13
- 优化 一些细节
2020.01.02
- 增加 指令配置项 `instruction`, 可以通过预定义的指令来控制 command 的行为
- 增加 `should-finish` 指令
- 增加 `exec-when-start` 指令
- 增加 `ignore-warn` 指令
- 增加 `ignore-info` 指令
- 增加 `ignore-stdout` 指令
- 增加 `ignore-exec-error` 指令
2019.12.28
- 增加 `deamon`命令,支持以守护进程的方式运行在后台 **Unix only**
- 增加 `stop`命令,用来停止 deamon 进程 **Unix only**
- 优化 exec stdout
### Release v1.12
2019.12.18

View File

@@ -10,8 +10,7 @@
# make build-start-mac 编译+启动
init:
go get -u gopkg.in/yaml.v2
go get -u gopkg.in/fsnotify/fsnotify.v1
go get -u
build-mac: ;@echo "编译-mac版";
CGO_ENABLED=0 GOARCH=amd64 GOOS=darwin go build -ldflags "-s -w" -o ./bin/fileboy-darwin-amd64.bin

View File

@@ -1,10 +1,10 @@
## 项目说明
[![Build Status](https://travis-ci.org/dengsgo/fileboy.svg?branch=master)](https://travis-ci.org/dengsgo/fileboy) [![Go Report Card](https://goreportcard.com/badge/github.com/dengsgo/fileboy)](https://goreportcard.com/report/github.com/dengsgo/fileboy)
![Go](https://github.com/dengsgo/fileboy/workflows/Go/badge.svg?branch=master) [![Build Status](https://travis-ci.org/dengsgo/fileboy.svg?branch=master)](https://travis-ci.org/dengsgo/fileboy) [![Go Report Card](https://goreportcard.com/badge/github.com/dengsgo/fileboy)](https://goreportcard.com/report/github.com/dengsgo/fileboy)
[简体中文](README.md) | [ENGLISH](README_EN.md)
fileboy文件变更监听通知系统,使用 Go 编写。
fileboy文件变更监听通知工具,使用 Go 编写。
适用于 Hot Reload 典型的如开发go项目无需每次手动执行 go build又比如前端 node 打包) 或者 系统监控的场景。
## 特性
@@ -17,11 +17,12 @@ fileboy文件变更监听通知系统使用 Go 编写。
- 命令支持变量占位符
- 支持冗余任务丢弃,自定义冗余任务范围
- 支持 http 通知
- 高级指令用法
- 更多...
## 编译环境
go version 1.13
Go >= 1.13
## 更新日志
@@ -31,18 +32,20 @@ go version 1.13
### 下载二进制文件
Github: [download v1.12](https://github.com/dengsgo/fileboy/releases)
Gitee: [dowmload v1.12](https://gitee.com/dengsgo/fileboy/releases)
Github: [download v1.15](https://github.com/dengsgo/fileboy/releases)
Gitee: [dowmload v1.15](https://gitee.com/dengsgo/fileboy/releases)
下载已经编译好的对应平台二进制文件,重命名为`fileboy`, 加入系统 Path 中即可。
### 源码编译
clone 该项目,进入主目录,运行命令:
```shell
```bash
## 确保本地 Go 启用 modules
export GO111MODULE=on
go env -w GOPROXY=https://goproxy.io,direct
## 安装依赖
go get -u gopkg.in/fsnotify/fsnotify.v1
go get -u gopkg.in/yaml.v2
go get -u
## 编译
go build
## 运行
@@ -137,45 +140,49 @@ notifier:
# 请求超时 15 秒
# POST 格式:
# Content-Type: application/json;charset=UTF-8
# User-Agent: FileBoy Net Notifier v1.12
# User-Agent: FileBoy Net Notifier v1.15
# Body: {"project_folder":"/project/path","file":"main.go","changed":1576567861913824940,"ext":".go","event":"write"}
# 例: http://example.com/notifier/fileboy-listener
# 不启用通知,请留空 ""
callUrl: ""
# 特殊指令
instruction:
# 可以通过特殊的指令选项来控制 command 的行为,指令可以有多个
# 指令选项解释:
# exec-when-start fileboy启动就绪后自动执行一次 'exec' 定义的命令
# should-finish 触发执行 'exec' 时(C),如果上一次的命令(L)未退出(还在执行),会等待 L 退出(而不是强制 kill ),直到 L 有明确 exit code 才会开始执行本次命令。
# 在等待 L 退出时,又有新事件触发了命令执行(N),则 C 执行取消,只会保留最后一次的 N 执行
# ignore-stdout 执行 'exec' 产生的 stdout 会被丢弃
# ignore-warn fileboy 自身的 warn 信息会被丢弃
# ignore-info fileboy 自身的 info 信息会被丢弃
# ignore-exec-error 执行 'exec' 出错仍继续执行下面的命令而不退出
#- should-finish
#- exec-when-start
- ignore-warn
```
### TODO
- [x] 命令支持变量占位符
- [x] 支持多命令
- [x] 支持监听指定文件夹
- [x] 支持不监听指定文件夹
- [x] 支持监听指定后缀文件
- [x] 支持自定事件监听
- [x] 支持 http 通知
- [x] 支持冗余任务丢弃
- [ ] 支持 http 合并任务的通知
## QA
#### 很多框架都自带了 hot reload 的功能,为什么还要单独写个 fileboy 呢?
### 很多框架都自带了 hot reload 的功能,为什么还要单独写个 fileboy 呢?
这个是一款通用的 hot reload 的软件,理论上适用于任何需要 hot reload 的场景,并不局限于语言层面上。只要灵活的配置 `filegirl.yaml`文件就行了。
#### fileboy 可以应用在那些具体的场景?
### fileboy 可以应用在那些具体的场景?
在开发中,我们很需要一款可以帮助我们自动打包编译的工具,那 fileboy 就非常适合这样的场景。比如 go 项目的热编译,让我们可以边修改代码边运行得到反馈。又比如 PHP Swoole 框架由于常驻进程的原因无法更改代码立即reload使用 fileboy 就可以辅助做到传统 PHP 开发的体验。
对于一些需要监控文件日志或者配置变动的场景, fileboy 同样适合。你可以事先编写好相应的通知报警脚本,然后定义`filegirl.yaml`中的`command`命令,交由 fileboy 自动运行监控报警。
#### 通知器在什么时候会发送 http 请求 ?
### 通知器在什么时候会发送 http 请求 ?
通知器发送 http 通知的前提是在配置文件中设置了 `callUrl` 参数(不为空即为已设置)。触发请求的时机和执行 command 命令是一致的,`command -> delayMillSecond` 参数对于触发器同样有效。请求超时默认15秒.
#### idea 下更改文件,为什么会执行两次或者多次 command ?
### idea 下更改文件,为什么会执行两次或者多次 command ?
由于 idea 系列软件特殊的文件保存策略他会自动创建一些临时文件并且在需要时多次重写文件所以有时反映在文件上就是有多次的更改所以会出现这种情况。1.5之后的版本增加了 `delayMillSecond` 参数,可以解决这个问题。
#### filegirl.yaml 里面的 command 如何配置复杂命令?
### filegirl.yaml 里面的 command 如何配置复杂命令?
fileboy 目前支持 `命令 + 参数`这种形式的 command而且 参数中不能有""符号或者有空格。如:
`go build`:支持;
@@ -184,19 +191,21 @@ fileboy 目前支持 `命令 + 参数`这种形式的 command而且 参数中
`cat a.txt | grep "q" | wc -l`:不支持
对于不支持的命令,可以把它写到一个文件里,然后在 command 中执行这个文件来解决。
#### 为什么起名为 fileboy又把配置名叫做 filegirl
### 为什么起名为 fileboy又把配置名叫做 filegirl
因为爱情~~ (◡ᴗ◡✿)
### 贡献者
## 贡献者
> 排名不分先后
| | | |
| ------------ | ------------ | ------------ |
| <a href="https://github.com/dengsgo"><img src="https://avatars1.githubusercontent.com/u/7929002?s=460&v=4" width=64 style="border-radius:45px;" /></a> | <a href="https://github.com/jason-gao"><img src="https://avatars1.githubusercontent.com/u/9896574?s=460&v=4" width=64 style="border-radius:45px;" /></a> | <a href="https://github.com/itwesley"><img src="https://avatars1.githubusercontent.com/u/1928721?s=460&v=4" width=64 style="border-radius:45px;" /></a> |
[@dengsgo](https://www.yoytang.com) <dengsgo@gmail.com>
## 感谢支持
[@itwesley](https://github.com/itwesley) <wcshen1126@gmail.com>
[@jason-gao](https://github.com/jason-gao) <3048789891@qq.com>
| |
| ------------ |
| <a href="https://www.jetbrains.com/?from=fileboy"><img src="./resources/jetbrains.png" width=140 /></a> |

View File

@@ -4,7 +4,7 @@
[简体中文](README.md) | [ENGLISH](README_EN.md)
Fileboy, File Change Monitoring Notification System, written with Go.
Fileboy, File Change Monitoring Notification Tool Software, written with Go.
For Hot Reload scenarios (typically for developing go projects without having to perform go build manually every time; for example, front-end node packaging) or system monitoring.
## FEATURES
@@ -17,11 +17,12 @@ For Hot Reload scenarios (typically for developing go projects without having to
- Command support variable placeholders
- Supporting redundant task discarding and customizing redundant task scope
- Supporting HTTP notifications
- Advanced instruction usage
- more...
## COMPILE
go version 1.13
Go >= 1.13
## CHANGELOG
@@ -32,8 +33,8 @@ go version 1.13
### BINARIES
Github: [download v1.12](https://github.com/dengsgo/fileboy/releases)
Gitee: [dowmload v1.12](https://gitee.com/dengsgo/fileboy/releases)
Github: [download v1.15](https://github.com/dengsgo/fileboy/releases)
Gitee: [dowmload v1.15](https://gitee.com/dengsgo/fileboy/releases)
Download the compiled binary file of the corresponding platform, rename it `fileboy`, and add it to the system Path.
@@ -41,9 +42,9 @@ Download the compiled binary file of the corresponding platform, rename it `file
Clone project, enter the project directory, run the command:
```shell
export GO111MODULE=on
## installation dependency
go get-u gopkg.in/fsnotify/fsnotify.v1
go get-u gopkg.in/yaml.v2
go get-u
## compile
go build
## run
@@ -133,18 +134,40 @@ notifier:
# timeout 15 second
# POST :
# Content-Type: application/json;charset=UTF-8
# User-Agent: FileBoy Net Notifier v1.12
# User-Agent: FileBoy Net Notifier v1.15
# Body: {"project_folder":"/project/path","file":"main.go","changed":1576567861913824940,"ext":".go","event":"write"}
# e.g: http://example.com/notifier/fileboy-listener
# no notice is enabled. Please leave it blank. ""
callUrl: ""
instruction:
# command behavior can be controlled by special command options. there can be multiple instructions
# options:
# exec-when-start when fileboy is ready to start, execute the command defined by 'exec' once automatically
# should-finish when the execution of 'exec' is triggered (C), if the last command (L) does not exit (still executing),
# it will wait for L to exit (instead of forcing kill), and the execution of this command will not start until L has an explicit exit code.
# when waiting for L to exit, and a new event triggers command execution (n), C execution is cancelled, and only the last N execution is retained
# ignore-stdout stdout generated by executing 'exec' will be discarded
# ignore-warn the warn information of fileboy itself will be discarded
# ignore-info the info information of fileboy itself will be discarded
# ignore-exec-error error executing 'exec' continue to execute the following command without exiting
#- should-finish
#- exec-when-start
- ignore-warn
```
### CONTRIBUTOR
## CONTRIBUTOR
[@dengsgo](https://www.yoytang.com) <dengsgo@gmail.com>
| | | |
| ------------ | ------------ | ------------ |
| <a href="https://github.com/dengsgo"><img src="https://avatars1.githubusercontent.com/u/7929002?s=460&v=4" width=64 style="border-radius:45px;" /></a> | <a href="https://github.com/jason-gao"><img src="https://avatars1.githubusercontent.com/u/9896574?s=460&v=4" width=64 style="border-radius:45px;" /></a> | <a href="https://github.com/itwesley"><img src="https://avatars1.githubusercontent.com/u/1928721?s=460&v=4" width=64 style="border-radius:45px;" /></a> |
[@itwesley](https://github.com/itwesley) <wcshen1126@gmail.com>
[@jason-gao](https://github.com/jason-gao) <3048789891@qq.com>
## THANKS
| |
| ------------ |
| <a href="https://www.jetbrains.com/?from=fileboy"><img src="./resources/jetbrains.png" width=140></a> |

56
daemon.go Normal file
View File

@@ -0,0 +1,56 @@
package main
import (
"io/ioutil"
"os"
"os/exec"
"runtime"
"strconv"
)
func getPidFile() string {
return projectFolder + "/.fileboy.pid"
}
func runAsDaemon() (int, error) {
if runtime.GOOS == "windows" {
logAndExit("daemons mode cannot run on windows.")
}
err := stopDaemon()
if err != nil {
logAndExit(err)
}
_, err = exec.LookPath("fileboy")
if err != nil {
logAndExit("cannot found `fileboy` command in the PATH")
}
daemon := exec.Command("fileboy")
daemon.Dir = projectFolder
daemon.Env = os.Environ()
daemon.Stdout = os.Stdout
err = daemon.Start()
if err != nil {
logAndExit(err)
}
pid := daemon.Process.Pid
if pid != 0 {
ioutil.WriteFile(getPidFile(), []byte(strconv.Itoa(pid)), 0644)
}
return pid, nil
}
func stopDaemon() error {
bs, err := ioutil.ReadFile(getPidFile())
if err != nil {
return nil
}
_ = exec.Command("kill", string(bs)).Run()
os.Remove(getPidFile())
return nil
}
func stopSelf() {
pid := os.Getpid()
os.Remove(getPidFile())
_ = exec.Command("kill", strconv.Itoa(pid)).Run()
}

View File

@@ -2,23 +2,30 @@ package main
import (
"fmt"
"gopkg.in/fsnotify/fsnotify.v1"
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
"math/rand"
"os"
"os/signal"
"path"
"strconv"
"strings"
"syscall"
"time"
"gopkg.in/fsnotify/fsnotify.v1"
"gopkg.in/yaml.v2"
)
const (
Version = 1
PreError = "ERROR:"
PreWarn = "Warn:"
InstExecWhenStart = "exec-when-start"
InstShouldFinish = "should-finish"
InstIgnoreWarn = "ignore-warn"
InstIgnoreInfo = "ignore-info"
InstIgnoreStdout = "ignore-stdout"
InstIgnoreExecError = "ignore-exec-error"
)
var (
@@ -52,30 +59,37 @@ func parseConfig() {
cfg = new(FileGirl)
fc, err := ioutil.ReadFile(getFileGirlPath())
if err != nil {
log.Println(PreError, "The filegirl.yaml file in", projectFolder, "is not exist! ", err)
logError("the filegirl.yaml file in", projectFolder, "is not exist! ", err)
fmt.Print(firstRunHelp)
logAndExit("Fileboy unable to run.")
logAndExit("fileboy unable to run.")
}
err = yaml.Unmarshal(fc, cfg)
if err != nil {
logAndExit(PreError, "Parsed filegirl.yaml failed: ", err)
logAndExit("parsed filegirl.yaml failed: ", err)
}
if cfg.Core.Version > Version {
logAndExit(PreError, "Current fileboy support max version : ", Version)
logAndExit("current fileboy support max version : ", Version)
}
// init map
cfg.Monitor.TypesMap = map[string]bool{}
cfg.Monitor.IncludeDirsMap = map[string]bool{}
cfg.Monitor.ExceptDirsMap = map[string]bool{}
cfg.Monitor.IncludeDirsRec = map[string]bool{}
cfg.InstructionMap = map[string]bool{}
// convert to map
for _, v := range cfg.Monitor.Types {
cfg.Monitor.TypesMap[v] = true
}
log.Println(cfg)
for _, v := range cfg.Instruction {
cfg.InstructionMap[v] = true
}
log.Printf("%+v", cfg)
}
func eventDispatcher(event fsnotify.Event) {
if event.Name == getPidFile() {
return
}
ext := path.Ext(event.Name)
if len(cfg.Monitor.Types) > 0 &&
!keyInMonitorTypesMap(".*", cfg) &&
@@ -97,15 +111,20 @@ func eventDispatcher(event fsnotify.Event) {
}
func addWatcher() {
log.Println("collecting directory information...")
logInfo("collecting directory information...")
dirsMap := map[string]bool{}
for _, dir := range cfg.Monitor.ExceptDirs {
if dir == "." {
logAndExit("exceptDirs must is not project root path ! err path:", dir)
}
}
for _, dir := range cfg.Monitor.IncludeDirs {
darr := dirParse2Array(dir)
if len(darr) < 1 || len(darr) > 2 {
logAndExit(PreError, "filegirl section monitor dirs is error. ", dir)
logAndExit("filegirl section monitor dirs is error. ", dir)
}
if strings.HasPrefix(darr[0], "/") {
logAndExit(PreError, "dirs must be relative paths ! err path:", dir)
logAndExit("dirs must be relative paths ! err path:", dir)
}
if darr[0] == "." {
if len(darr) == 2 && darr[1] == "*" {
@@ -133,25 +152,16 @@ func addWatcher() {
}
}
for _, dir := range cfg.Monitor.ExceptDirs {
if dir == "." {
logAndExit(PreError, "exceptDirs must is not project root path ! err path:", dir)
}
p := projectFolder + "/" + dir
delete(dirsMap, p)
listFile(p, func(d string) {
delete(dirsMap, d)
})
}
for dir := range dirsMap {
log.Println("watcher add -> ", dir)
logInfo("watcher add -> ", dir)
err := watcher.Add(dir)
if err != nil {
logAndExit(PreError, err)
logAndExit(err)
}
}
log.Println("total monitored dirs: " + strconv.Itoa(len(dirsMap)))
log.Println("fileboy is ready.")
logInfo("total monitored dirs: " + strconv.Itoa(len(dirsMap)))
logInfo("fileboy is ready.")
cfg.Monitor.DirsMap = dirsMap
}
@@ -180,7 +190,7 @@ func initWatcher() {
if !ok {
return
}
log.Println(PreError, err)
logError(err)
}
}
}()
@@ -188,6 +198,15 @@ func initWatcher() {
}
func watchChangeHandler(event fsnotify.Event) {
// stop the fileboy daemon process when the .fileboy.pid file is changed
if event.Name == getPidFile() &&
(event.Op == fsnotify.Remove ||
event.Op == fsnotify.Write ||
event.Op == fsnotify.Rename) {
logUInfo("exit daemon process")
stopSelf()
return
}
if event.Op != fsnotify.Create && event.Op != fsnotify.Rename {
return
}
@@ -201,13 +220,7 @@ func watchChangeHandler(event fsnotify.Event) {
continue
}
// check exceptDirs
has := false
for _, v := range cfg.Monitor.ExceptDirs {
if strings.HasPrefix(event.Name, projectFolder+"/"+v) {
has = true
}
}
if has {
if hitDirs(event.Name, &cfg.Monitor.ExceptDirs) {
continue
}
@@ -215,9 +228,9 @@ func watchChangeHandler(event fsnotify.Event) {
err := watcher.Add(event.Name)
if err == nil {
do = true
log.Println("watcher add -> ", event.Name)
logInfo("watcher add -> ", event.Name)
} else {
log.Println(PreWarn, "watcher add faild:", event.Name, err)
logWarn("watcher add faild:", event.Name, err)
}
}
@@ -230,37 +243,56 @@ func watchChangeHandler(event fsnotify.Event) {
_ = watcher.Remove(event.Name)
err := watcher.Add(event.Name)
if err == nil {
log.Println("watcher add -> ", event.Name)
logInfo("watcher add -> ", event.Name)
} else {
log.Println(PreWarn, "watcher add faild:", event.Name, err)
logWarn("watcher add faild:", event.Name, err)
}
}
}
func parseArgs() {
switch len(os.Args) {
case 1:
switch {
case len(os.Args) == 1:
show()
parseConfig()
done := make(chan bool)
initWatcher()
defer watcher.Close()
if keyInInstruction(InstExecWhenStart) {
taskMan.run(new(changedFile))
}
<-done
return
case 2:
case len(os.Args) > 1:
c := os.Args[1]
switch c {
case "deamon", "daemon":
pid, err := runAsDaemon()
if err != nil {
logAndExit(err)
}
logUInfo("PID:", pid)
logUInfo("fileboy is ready. the main process will run as a daemons")
return
case "stop":
err := stopDaemon()
if err != nil {
logAndExit(err)
}
logUInfo("fileboy daemon is stoped.")
return
case "init":
_, err := ioutil.ReadFile(getFileGirlPath())
if err == nil {
log.Println(PreError, "Profile filegirl.yaml already exists.")
logAndExit("If you want to regenerate filegirl.yaml, delete it first")
logError("profile filegirl.yaml already exists.")
logAndExit("delete it first when you want to regenerate filegirl.yaml")
}
err = ioutil.WriteFile(getFileGirlPath(), []byte(exampleFileGirl), 0644)
if err != nil {
log.Println(PreError, "Profile filegirl.yaml create failed! ", err)
logError("profile filegirl.yaml create failed! ", err)
return
}
log.Println("Profile filegirl.yaml created ok")
logUInfo("profile filegirl.yaml created ok")
return
case "exec":
parseConfig()
@@ -268,15 +300,31 @@ func parseArgs() {
return
case "version", "v", "-v", "--version":
fmt.Println(versionDesc)
default:
case "help", "--help", "--h", "-h":
fmt.Print(helpStr)
default:
logAndExit("unknown parameter, use 'fileboy help' to view available commands")
}
return
default:
logAndExit("Unknown parameters, use `fileboy help` show help info.")
logAndExit("unknown parameters, use `fileboy help` show help info.")
}
}
func signalHandler() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
if taskMan != nil && taskMan.cmd != nil && taskMan.cmd.Process != nil {
if err := taskMan.cmd.Process.Kill(); err != nil {
logError("stopping the process failed: PID:", taskMan.cmd.ProcessState.Pid(), ":", err)
}
}
os.Exit(0)
}()
}
func getFileGirlPath() string {
return projectFolder + "/" + filegirlYamlName
}
@@ -293,11 +341,12 @@ func main() {
log.SetPrefix("[FileBoy]: ")
log.SetFlags(2)
log.SetOutput(os.Stdout)
show()
// show()
var err error
projectFolder, err = os.Getwd()
if err != nil {
logAndExit(err)
}
signalHandler()
parseArgs()
}

View File

@@ -24,4 +24,8 @@ type FileGirl struct {
Notifier struct {
CallUrl string `yaml:"callUrl"`
}
Instruction []string `yaml:"instruction"`
// convert to
InstructionMap map[string]bool `yaml:"-"`
}

9
go.mod Normal file
View File

@@ -0,0 +1,9 @@
module fileboy
go 1.13
require (
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7
gopkg.in/yaml.v2 v2.2.8
)

9
go.sum Normal file
View File

@@ -0,0 +1,9 @@
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b h1:h03Ur1RlPrGTjua4koYdpGl8W0eYo8p1uI9w7RPlkdk=
golang.org/x/sys v0.0.0-20200408040146-ea54a3c99b9b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 h1:XNNYLJHt73EyYiCZi6+xjupS9CpvmiDgjPTAjrBlQbo=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -3,7 +3,6 @@ package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"strings"
"time"
@@ -35,7 +34,7 @@ func newNetNotifier(callUrl string) *NetNotifier {
func (n *NetNotifier) Put(cf *changedFile) {
if !n.CanPost {
log.Println(PreWarn, "notifier call url ignore. ", n.CallUrl)
logWarn("notifier call url ignore. ", n.CallUrl)
return
}
n.dispatch(&postParams{
@@ -50,7 +49,7 @@ func (n *NetNotifier) Put(cf *changedFile) {
func (n *NetNotifier) dispatch(params *postParams) {
b, err := json.Marshal(params)
if err != nil {
log.Println(PreError, "json.Marshal n.params. ", err)
logError("json.Marshal n.params. ", err)
return
}
client := &http.Client{
@@ -58,14 +57,14 @@ func (n *NetNotifier) dispatch(params *postParams) {
}
req, err := http.NewRequest("POST", n.CallUrl, bytes.NewBuffer(b))
if err != nil {
log.Println(PreError, "http.NewRequest. ", err)
logError("http.NewRequest. ", err)
return
}
req.Header.Set("Content-Type", "application/json;charset=UTF-8")
req.Header.Set("User-Agent", "FileBoy Net Notifier v1.12")
req.Header.Set("User-Agent", "FileBoy Net Notifier v1.15")
resp, err := client.Do(req)
if err != nil {
log.Println(PreError, "notifier call failed. err:", err)
logError("notifier call failed. err:", err)
return
}
defer func() {
@@ -76,5 +75,5 @@ func (n *NetNotifier) dispatch(params *postParams) {
if resp.StatusCode >= 300 {
// todo retry???
}
log.Println("notifier done .")
logInfo("notifier done .")
}

28
raw.go
View File

@@ -76,11 +76,27 @@ notifier:
# 请求超时 15 秒
# POST 格式:
# Content-Type: application/json;charset=UTF-8
# User-Agent: FileBoy Net Notifier v1.12
# User-Agent: FileBoy Net Notifier v1.15
# Body: {"project_folder":"/project/path","file":"main.go","changed":1576567861913824940,"ext":".go","event":"write"}
# 例: http://example.com/notifier/fileboy-listener
# 不启用通知,请留空 ""
callUrl: ""
# 特殊指令
instruction:
# 可以通过特殊的指令选项来控制 command 的行为,指令可以有多个
# 指令选项解释:
# exec-when-start fileboy启动就绪后自动执行一次 'exec' 定义的命令
# should-finish 触发执行 'exec' 时(C),如果上一次的命令(L)未退出(还在执行),会等待 L 退出(而不是强制 kill ),直到 L 有明确 exit code 才会开始执行本次命令。
# 在等待 L 退出时,又有新事件触发了命令执行(N),则 C 执行取消,只会保留最后一次的 N 执行
# ignore-stdout 执行 'exec' 产生的 stdout 会被丢弃
# ignore-warn fileboy 自身的 warn 信息会被丢弃
# ignore-info fileboy 自身的 info 信息会被丢弃
# ignore-exec-error 执行 'exec' 出错仍继续执行下面的命令而不退出
#- should-finish
#- exec-when-start
- ignore-warn
`
var firstRunHelp = `第一次运行 fileboy ?
@@ -96,6 +112,10 @@ Usage of fileboy:
初始化 fileboy, 在当前目录生成 filegirl.yaml 配置文件
exec
尝试运行定义的 command 命令
daemon
读取当前目录下的 filegirl.yaml 配置,以守护进程的方式运行在后台
stop
停止守护进程
version
查看当前版本信息
`
@@ -119,13 +139,13 @@ var logo = `
_____ _ | | _____ ____) ) | | | |___| |
| ___) | | | | | ___) | __ (| | | |\_____/
| | _| |_| |_____| |_____| |__) ) |___| | ___
|_| (_____)_______)_______)______/ \_____/ (___) V1.12
|_| (_____)_______)_______)______/ \_____/ (___) V1.15
`
var statement = `Dengsgo [dengsgo@gmail.com] Open Source with MIT License`
var versionDesc = `
Version fileboy: v1.12 filegirl: v` + strconv.Itoa(Version) + `
Released 2019.12.18
Version fileboy: v1.15 filegirl: v` + strconv.Itoa(Version) + `
Released 2020.03.08
Licence MIT
Author dengsgo [dengsgo@gmail.com]
Website https://github.com/dengsgo/fileboy

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
resources/jetbrains.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

8
snapcraft.yaml Normal file
View File

@@ -0,0 +1,8 @@
name: fileboy
vendor: dengsgo, https://github.com/dengsgo, <dengsgo@yoytang.com>
summary: File Change Monitoring Notification Tools.
description: File Change Monitoring Notification Tools. Please Visit https://github.com/dengsgo/fileboy
version: 1.15
icon: resources/icon.png
base: core18
grade: stable

View File

@@ -1,10 +1,6 @@
package main
import (
"bufio"
"fmt"
"io"
"log"
"os"
"os/exec"
"sync"
@@ -18,18 +14,40 @@ type TaskMan struct {
notifier *NetNotifier
putLock sync.Mutex
runLock sync.Mutex
waitChan chan bool
waitQueue []*changedFile
}
func newTaskMan(delay int, callUrl string) *TaskMan {
return &TaskMan{
delay: delay,
notifier: newNetNotifier(callUrl),
t := &TaskMan{
delay: delay,
notifier: newNetNotifier(callUrl),
waitChan: make(chan bool, 1),
waitQueue: []*changedFile{},
}
if keyInInstruction(InstShouldFinish) {
go func() {
for {
<-t.waitChan
if len(t.waitQueue) > 0 {
cf := t.waitQueue[len(t.waitQueue)-1]
if len(t.waitQueue) > 1 {
logInfo("redundant tasks dropped:", len(t.waitQueue)-1)
}
t.waitQueue = []*changedFile{}
go t.preRun(cf)
}
}
}()
}
return t
}
func (t *TaskMan) Put(cf *changedFile) {
if t.delay < 1 {
t.preRun(cf)
t.dispatcher(cf)
return
}
t.putLock.Lock()
@@ -40,15 +58,29 @@ func (t *TaskMan) Put(cf *changedFile) {
if t.lastTaskId > cf.Changed {
return
}
t.preRun(cf)
t.dispatcher(cf)
}()
}
func (t *TaskMan) dispatcher(cf *changedFile) {
if keyInInstruction(InstShouldFinish) {
t.waitQueue = append(t.waitQueue, cf)
if t.cmd == nil {
t.waitChan <- true
return
}
logInfo("waitting for the last task to finish")
logInfo("waiting tasks:", len(t.waitQueue))
} else {
t.preRun(cf)
}
}
func (t *TaskMan) preRun(cf *changedFile) {
if t.cmd != nil && t.cmd.Process != nil {
log.Println("stop old process ")
if err := t.cmd.Process.Kill(); err != nil {
log.Println(PreWarn, "stopped err, reason:", err)
logInfo("stop old process ")
logWarn("stopped err, reason:", err)
}
}
go t.run(cf)
@@ -60,42 +92,44 @@ func (t *TaskMan) run(cf *changedFile) {
defer t.runLock.Unlock()
for i := 0; i < len(cfg.Command.Exec); i++ {
carr := cmdParse2Array(cfg.Command.Exec[i], cf)
log.Println("EXEC", carr)
logInfo("EXEC", carr)
t.cmd = exec.Command(carr[0], carr[1:]...)
//cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_UNICODE_ENVIRONMENT}
t.cmd.Stdin = os.Stdin
//cmd.Stdout = os.Stdout
t.cmd.Stdout = os.Stdout
if keyInInstruction(InstIgnoreStdout) {
t.cmd.Stdout = nil
}
t.cmd.Stderr = os.Stderr
t.cmd.Dir = projectFolder
t.cmd.Env = os.Environ()
stdout, err := t.cmd.StdoutPipe()
err := t.cmd.Start()
if err != nil {
log.Println(PreError, err.Error())
return
}
err = t.cmd.Start()
if err != nil {
log.Println(PreError, "run command", carr, "error. ", err)
}
reader := bufio.NewReader(stdout)
for {
line, err2 := reader.ReadString('\n')
if err2 != nil || io.EOF == err2 {
break
logError("run command", carr, "error. ", err)
if keyInInstruction(InstIgnoreExecError) {
continue
}
fmt.Print(line)
break
}
err = t.cmd.Wait()
if err != nil {
log.Println(PreWarn, "cmd wait err ", err)
logError("command exec failed:", carr, err)
if keyInInstruction(InstIgnoreExecError) {
continue
}
break
}
if t.cmd.Process != nil {
if err = t.cmd.Process.Kill(); err != nil {
log.Println(PreWarn, "cmd cannot kill, reason:", err)
err := t.cmd.Process.Kill()
logInfo(t.cmd.ProcessState)
if t.cmd.ProcessState != nil && !t.cmd.ProcessState.Exited() {
logError("command cannot stop!", carr, err)
}
}
}
log.Println("end ")
if keyInInstruction(InstShouldFinish) {
t.cmd = nil
t.waitChan <- true
}
logInfo("EXEC end")
}

44
util.go
View File

@@ -13,6 +13,11 @@ func keyInMonitorTypesMap(k string, cfg *FileGirl) bool {
return ok
}
func keyInInstruction(k string) bool {
_, ok := cfg.InstructionMap[k]
return ok
}
func cmdParse2Array(s string, cf *changedFile) []string {
a := strings.Split(s, " ")
r := make([]string, 0)
@@ -48,11 +53,24 @@ func dirParse2Array(s string) []string {
return r
}
func hitDirs(d string, dirs *[]string) bool {
d += "/"
for _, v := range *dirs {
if strings.HasPrefix(d, projectFolder+"/"+v+"/") {
return true
}
}
return false
}
func listFile(folder string, fun func(string)) {
files, _ := ioutil.ReadDir(folder)
for _, file := range files {
if file.IsDir() {
d := folder + "/" + file.Name()
if hitDirs(d, &cfg.Monitor.ExceptDirs) {
continue
}
fun(d)
listFile(d, fun)
}
@@ -76,7 +94,33 @@ func inStrArray(s string, arr []string) bool {
return false
}
func logInfo(v ...interface{}) {
if keyInInstruction(InstIgnoreInfo) {
return
}
logUInfo(v...)
}
func logUInfo(v ...interface{}) {
v = append([]interface{}{"I:"}, v...)
log.Println(v...)
}
func logWarn(v ...interface{}) {
if keyInInstruction(InstIgnoreWarn) {
return
}
v = append([]interface{}{"W:"}, v...)
log.Println(v...)
}
func logError(v ...interface{}) {
v = append([]interface{}{"E:"}, v...)
log.Println(v...)
}
func logAndExit(v ...interface{}) {
v = append([]interface{}{"O:"}, v...)
log.Println(v...)
os.Exit(15)
}