1
0
mirror of https://github.com/nxshock/gron.git synced 2025-04-12 00:15:23 +05:00

Compare commits

...

18 Commits
v0.0.4 ... main

Author SHA1 Message Date
b6e32d3b98 Show console output 2024-04-03 13:47:40 +05:00
0c5aff496b Prevent parallel websocket writes 2024-04-03 13:47:20 +05:00
252141ae01 Update deps 2024-03-28 09:48:04 +05:00
8a4b8e6b50 Allow to set cmd working directory 2024-03-28 09:33:15 +05:00
48370480b1 Update deps 2023-10-20 21:38:03 +05:00
91657772f7 Update go-ora dep 2023-08-26 13:35:33 +05:00
19587fadae Update go-ora version 2023-08-19 14:57:33 +05:00
49805789bb Update deps 2023-08-14 14:29:24 +05:00
ab02e120cc Add default timeout for go-ora library 2023-05-21 15:20:25 +05:00
67d0cf83d8 Update deps 2023-05-15 21:10:30 +05:00
629268a22d Fix dropdown display 2023-02-24 22:41:51 +05:00
9186678563 Left-align button text 2023-02-24 22:41:22 +05:00
ff03ada7fe Update go-ora 2023-02-24 21:59:14 +05:00
f575d71369 Add workaround for spaces in job config file names 2022-11-21 21:37:55 +05:00
b118d18516 Update info about config directory 2022-11-20 14:59:39 +05:00
efa20ae0f0 Move job examples to separate dir 2022-11-20 14:56:40 +05:00
d9d9f0dcf0 Add JS for online data update 2022-11-20 14:44:03 +05:00
733d5f28f1 Show http get/post errors in logs 2022-11-05 14:07:55 +05:00
16 changed files with 293 additions and 120 deletions

View File

@ -4,46 +4,8 @@
## Usage
1. Create `gron.d` directory
2. Create job config in `gron.d/job1.conf` ([TOML](https://en.wikipedia.org/wiki/TOML) format):
```toml
Type = "cmd" # command execution
Category = "Test jobs" # jobs category name
Cron = "* * * * *" # cron instructions
1. Create `gron.d` directory (`JobConfigsPath` variable from `gron.conf` variable).
2. Create job config in `gron.d/job1.conf` ([TOML](https://en.wikipedia.org/wiki/TOML) format). See examples in [_jobExamples](_jobExamples).
Command = "echo Hello" # command to execute
```
SQL job:
```toml
Type = "sql" # sql execution
Cron = "* * * * *" # cron instructions
Description = "execute procedure every minute" # job description
Driver = "pgx" # "pgx" for Postgresql, "oracle" for Oracle, "sqlserver" for Microsoft SQL Server
ConnectionString = "postgres://login:password@host:port/database?sslmode=disable" # each driver has different syntax
SqlText = "CALL procedure" # command to execute
```
Add other options if needed:
```toml
Description = "print Hello every minute" # job description
NumberOfRestartAttemts = 3 # number of restart attemts
RestartSec = 5 # the time to sleep before restarting a job (seconds)
RestartRule = "on-error" # Configures whether the job shall be restarted when the job process exits
OnSuccessCmd = "echo 'Job finished.'" # execute cmd on job success
OnErrorCmd = "echo 'Error occurred: {{.Error}}'" # execute cmd on job error
OnSuccessHttpGetUrl = ""
OnErrorHttpGetUrl = "http://127.0.0.1/alerts?title={{.JobName}}%20failed&message={{.Error}}&tags=warning"
OnSuccessHttpPostUrl = "http://127.0.0.1/alerts"
OnSuccessMessageFmt = "Job {{.JobName}} finished."
OnErrorHttpPostUrl = "http://127.0.0.1/alerts"
OnErrorMessageFmt = "Job {{.JobName}} failed:\n\n{{.Error}}"
```
3. Launch `gron` binary
4. HTTP interface available on http://127.0.0.1:9876
3. Launch `gron` binary.
4. HTTP interface available on http://127.0.0.1:9876.

View File

@ -0,0 +1,24 @@
Type = "cmd" # command execution
Category = "Test jobs" # jobs category name
Description = "print 'Hello' every minute" # job description
Cron = "* * * * *" # cron instructions
Command = "echo Hello" # command to execute
WorkingDir = "/tmp" # working directory
NumberOfRestartAttemts = 3 # number of restart attemts
RestartSec = 5 # the time to sleep before restarting a job (seconds)
RestartRule = "on-error" # Configures whether the job shall be restarted when the job process exits
OnSuccessCmd = "echo 'Job finished.'" # execute cmd on job success
OnErrorCmd = "echo 'Error occurred: {{.Error}}'" # execute cmd on job error
# HTTP client callbacks
OnSuccessHttpGetUrl = "http://127.0.0.1/alerts?title={{.JobName}}%20finished"
OnErrorHttpGetUrl = "http://127.0.0.1/alerts?title={{.JobName}}%20failed&message={{.Error}}&tags=warning"
OnSuccessHttpPostUrl = "http://127.0.0.1/alerts"
OnSuccessMessageFmt = "Job {{.JobName}} finished."
OnErrorHttpPostUrl = "http://127.0.0.1/alerts"
OnErrorMessageFmt = "Job {{.JobName}} failed:\n\n{{.Error}}"

6
_jobExamples/basic.conf Normal file
View File

@ -0,0 +1,6 @@
Type = "cmd" # command execution
Category = "Test jobs" # jobs category name
Description = "print 'Hello' every minute" # job description
Cron = "* * * * *" # cron instructions
Command = "echo Hello" # command to execute

View File

@ -0,0 +1,7 @@
Type = "sql" # sql execution
Cron = "* * * * *" # cron instructions
Description = "execute procedure every minute" # job description
Driver = "pgx" # "pgx" for Postgresql, "oracle" for Oracle, "sqlserver" for Microsoft SQL Server
ConnectionString = "postgres://login:password@host:port/database?sslmode=disable" # each driver has different syntax
SqlText = "CALL procedure" # command to execute

View File

@ -1,10 +1,12 @@
package main
const (
programName = "gron"
import "time"
const (
defaultConfigFileName = "gron.conf"
defaultDbTimeout = 24 * time.Hour
defaultOnSuccessMessageFmt = "Job {{.JobName}} finished."
defaultOnErrorMessageFmt = "Job {{.JobName}} failed:\n\n{{.Error}}"
)

17
go.mod
View File

@ -3,18 +3,21 @@ module github.com/nxshock/gron
go 1.18
require (
github.com/BurntSushi/toml v1.2.0
github.com/BurntSushi/toml v1.3.2
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/creasty/defaults v1.6.0
github.com/creasty/defaults v1.7.0
github.com/denisenkom/go-mssqldb v0.12.3
github.com/gorilla/websocket v1.5.1
github.com/jackc/pgx v3.6.2+incompatible
github.com/nxshock/logwriter v0.0.0-20220514172136-b1385d4106de
github.com/robfig/cron/v3 v3.0.1
github.com/sijms/go-ora/v2 v2.5.3
github.com/sirupsen/logrus v1.9.0
github.com/sijms/go-ora/v2 v2.8.10
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.0
)
require golang.org/x/net v0.22.0 // indirect
require (
github.com/cockroachdb/apd v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@ -28,9 +31,9 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

45
go.sum
View File

@ -1,29 +1,27 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc=
github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA=
github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
@ -31,13 +29,10 @@ github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX
github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/nxshock/logwriter v0.0.0-20220514172136-b1385d4106de h1:z3mN9UkoAech2jk3bYNkxy+5wn2+M6tLAbXIiSJsffI=
@ -45,32 +40,30 @@ github.com/nxshock/logwriter v0.0.0-20220514172136-b1385d4106de/go.mod h1:V24Kcz
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sijms/go-ora/v2 v2.5.3 h1:klGKmhqRONVTtIzTdfYTvrW94kdJkdmZl93u2A3vchI=
github.com/sijms/go-ora/v2 v2.5.3/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sijms/go-ora/v2 v2.8.10 h1:Ekhx0I+A9qVBy1eOLa2eIhHWWYwVTa0MM78KS6h+5fg=
github.com/sijms/go-ora/v2 v2.8.10/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -78,22 +71,20 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -61,7 +61,7 @@ func httpGet(addrFmt, jobName, text string) error {
if err != nil {
return err
}
defer resp.Body.Close() // TODO: нужно ли закрывать Body при наличии ошибки?
defer resp.Body.Close()
return nil
}

View File

@ -8,11 +8,54 @@ import (
"net"
"net/http"
"sort"
"sync"
"time"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
type WsConnections struct {
connections map[*WsConnection]struct{}
mutex sync.Mutex
}
func (wc *WsConnections) Add(c *websocket.Conn) {
wc.mutex.Lock()
defer wc.mutex.Unlock()
wc.connections[NewWsConnection(c)] = struct{}{}
}
func (wc *WsConnections) Delete(c *websocket.Conn) {
wc.mutex.Lock()
defer wc.mutex.Unlock()
for k := range wc.connections {
if k.w == c {
delete(wc.connections, k)
break
}
}
}
func (wc *WsConnections) Send(message interface{}) {
for conn := range wc.connections {
go func(conn *WsConnection) { _ = conn.Send(message) }(conn)
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
var wsConnections = &WsConnections{
connections: make(map[*WsConnection]struct{})}
func httpServer(listenAddress string) {
if listenAddress == "none" {
return
@ -23,6 +66,7 @@ func httpServer(listenAddress string) {
http.HandleFunc("/shutdown", handleShutdown)
http.HandleFunc("/start", handleForceStart)
http.HandleFunc("/details", handleDetails)
http.HandleFunc("/ws", handleWebSocket)
log.WithField("job", "http_server").Fatal(http.ListenAndServe(listenAddress, nil))
}
@ -39,10 +83,9 @@ func handler(w http.ResponseWriter, r *http.Request) {
globalMutex.RLock()
buf := new(bytes.Buffer)
jobEntries := kernel.c.Entries()
jobs := make(map[string][]*Job)
for _, jobEntry := range jobEntries {
for _, jobEntry := range kernel.c.Entries() {
job := jobEntry.Job.(*Job)
job.NextLaunch = jobEntry.Next.Format(config.TimeFormat)
jobs[job.JobConfig.Category] = append(jobs[job.JobConfig.Category], job)
@ -73,6 +116,41 @@ func handler(w http.ResponseWriter, r *http.Request) {
_, _ = buf.WriteTo(w)
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
wsConnections.Add(conn)
defer wsConnections.Delete(conn)
var startMessage struct {
JobName string
}
for {
err := conn.ReadJSON(&startMessage)
if err != nil {
log.Println(err)
break
}
for _, jobEntry := range kernel.c.Entries() {
job := jobEntry.Job.(*Job)
if job.Name == startMessage.JobName {
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
host = r.RemoteAddr
}
log.WithField("job", "http_server").Printf("Forced start %s from %s.", job.Name, host)
go job.Run()
break
}
}
}
}
func handleForceStart(w http.ResponseWriter, r *http.Request) {
jobName := r.FormValue("jobName")
if jobName == "" {
@ -80,9 +158,7 @@ func handleForceStart(w http.ResponseWriter, r *http.Request) {
return
}
jobEntries := kernel.c.Entries()
for _, jobEntry := range jobEntries {
for _, jobEntry := range kernel.c.Entries() {
job := jobEntry.Job.(*Job)
if job.Name == jobName {
host, _, err := net.SplitHostPort(r.RemoteAddr)

85
job.go
View File

@ -1,6 +1,7 @@
package main
import (
"context"
"database/sql"
"errors"
"fmt"
@ -24,7 +25,8 @@ type JobConfig struct {
Category string
// JobType = Cmd
Command string // command for execution
Command string // command for execution
WorkingDir string // working directory
// JobType = Sql
Driver string
@ -84,8 +86,13 @@ func readJob(filePath string) (*Job, error) {
return nil, fmt.Errorf("unknown job type id: %v", int(jobConfig.Type)) // TODO: add job name to log
}
jobName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
jobName = strings.ReplaceAll(jobName, " ", "")
jobName = strings.ReplaceAll(jobName, "_", "")
jobName = strings.ReplaceAll(jobName, "#", "")
job := &Job{
Name: strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)),
Name: jobName,
Status: Inactive,
JobConfig: jobConfig}
@ -111,7 +118,7 @@ func splitCommandAndParams(s string) (command string, params []string) {
// jobLogFile - job log file with needs to be closed after job is done
func (j *Job) openAndMergeLog() (logEntry *log.Entry, jobLogFile *os.File) {
jobLogFile, _ = os.OpenFile(filepath.Join(config.LogFilesPath, j.Name+".log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) // TODO: handle error
jobLogFile.WriteString("\n")
_, _ = jobLogFile.WriteString("\n")
logWriter := io.MultiWriter(mainLogFile, jobLogFile)
@ -128,6 +135,16 @@ func (j *Job) openAndMergeLog() (logEntry *log.Entry, jobLogFile *os.File) {
}
func (j *Job) Run() {
// TODO: переписать неоптимальный цикл
for _, jobEntry := range kernel.c.Entries() {
if jobEntry.Job.(*Job) != j {
continue
}
j.NextLaunch = jobEntry.Next.Format(config.TimeFormat)
break
}
log, jobLogFile := j.openAndMergeLog()
defer jobLogFile.Close()
@ -165,6 +182,8 @@ func (j *Job) runTry(log *log.Entry, jobLogFile *os.File) error {
j.LastStartTime = startTime.Format(config.TimeFormat)
globalMutex.Unlock()
wsConnections.Send(j)
var err error
switch j.JobConfig.Type {
case Cmd:
@ -185,7 +204,7 @@ func (j *Job) runTry(log *log.Entry, jobLogFile *os.File) error {
j.LastError = ""
globalMutex.Unlock()
}
go j.runFinishCallback(err)
go j.runFinishCallback(log, err)
endTime := time.Now()
log.Infof("Finished (%s).", endTime.Sub(startTime).Truncate(time.Second).String())
@ -196,6 +215,8 @@ func (j *Job) runTry(log *log.Entry, jobLogFile *os.File) error {
j.LastExecutionDuration = endTime.Sub(startTime).Truncate(time.Second).String()
globalMutex.Unlock()
wsConnections.Send(j)
return err
}
@ -205,6 +226,7 @@ func (j *Job) runCmd(jobLogFile *os.File) error {
cmd := exec.Command(command, params...)
cmd.Stdout = jobLogFile
cmd.Stderr = jobLogFile
cmd.Dir = j.JobConfig.WorkingDir
return cmd.Run()
}
@ -226,7 +248,10 @@ func (j *Job) runSql(jobLogFile *os.File) error {
}
defer db.Close()
_, err = db.Exec(j.JobConfig.SqlText)
ctx, cancel := context.WithTimeout(context.Background(), defaultDbTimeout)
defer cancel()
_, err = db.ExecContext(ctx, j.JobConfig.SqlText)
if err != nil {
return err
}
@ -234,44 +259,53 @@ func (j *Job) runSql(jobLogFile *os.File) error {
return nil
}
func (j *Job) runFinishCallback(err error) error {
func (j *Job) runFinishCallback(log *log.Entry, jobErr error) error {
s := j.JobConfig.OnSuccessCmd
// Fill variables
errStr := ""
if err != nil {
errStr = err.Error()
if jobErr != nil {
errStr = jobErr.Error()
}
if err != nil {
if jobErr != nil {
s = format(s, struct{ Error string }{Error: errStr})
}
if err == nil && j.JobConfig.OnSuccessCmd != "" {
if jobErr == nil && j.JobConfig.OnSuccessCmd != "" {
cmd, params := splitCommandAndParams(s)
return runSimpleCmd(cmd, params...)
}
if err == nil && j.JobConfig.OnSuccessHttpPostUrl != "" {
httpPost(j.JobConfig.OnSuccessHttpPostUrl, j.successMessage()) // TODO: обработать ошибку
if jobErr == nil && j.JobConfig.OnSuccessHttpPostUrl != "" {
err := httpPost(j.JobConfig.OnSuccessHttpPostUrl, j.successMessage())
if err != nil {
log.Error(err)
}
}
if err == nil && j.JobConfig.OnSuccessHttpGetUrl != "" {
httpGet(j.JobConfig.OnSuccessHttpPostUrl, j.Name, j.successMessage()) // TODO: обработать ошибку
if jobErr == nil && j.JobConfig.OnSuccessHttpGetUrl != "" {
err := httpGet(j.JobConfig.OnSuccessHttpPostUrl, j.Name, j.successMessage())
if err != nil {
log.Error(err)
}
}
if err != nil && j.JobConfig.OnErrorCmd != "" {
if jobErr != nil && j.JobConfig.OnErrorCmd != "" {
cmd, params := splitCommandAndParams(s)
return runSimpleCmd(cmd, params...)
}
if err != nil && j.JobConfig.OnErrorHttpPostUrl != "" {
httpPost(j.JobConfig.OnErrorHttpPostUrl, j.errorMessage(err)) // TODO: обработать ошибку
if jobErr != nil && j.JobConfig.OnErrorHttpPostUrl != "" {
err := httpPost(j.JobConfig.OnErrorHttpPostUrl, j.errorMessage(jobErr))
if err != nil {
log.Error(err)
}
}
if err != nil && j.JobConfig.OnErrorHttpGetUrl != "" {
err2 := httpGet(j.JobConfig.OnErrorHttpGetUrl, j.Name, err.Error())
if err2 != nil {
log.Errorf("OnErrorHttpGetUrl error: %v", err2) // TODO: сделать формат сообщения по стандарту
if jobErr != nil && j.JobConfig.OnErrorHttpGetUrl != "" {
err := httpGet(j.JobConfig.OnErrorHttpGetUrl, j.Name, jobErr.Error())
if err != nil {
log.Errorf("OnErrorHttpGetUrl error: %v", err) // TODO: сделать формат сообщения по стандарту
}
}
@ -309,12 +343,5 @@ func (j *Job) errorMessage(err error) string {
}
func runSimpleCmd(command string, args ...string) error {
log.Println(command)
log.Println(args)
err := exec.Command(command, args...).Run()
if err != nil {
log.Println(">>", err)
}
return err
return exec.Command(command, args...).Run()
}

View File

@ -94,6 +94,6 @@ func main() {
err = s.Run()
if err != nil {
logger.Error(err)
_ = logger.Error(err)
}
}

View File

@ -1 +1 @@
go build -ldflags "-s -w -H windowsgui" -buildmode=pie -trimpath
go build -ldflags "-s -w" -buildmode=pie -trimpath

View File

@ -8,6 +8,6 @@ import (
func format(fmt string, v interface{}) string {
t := new(template.Template)
b := new(strings.Builder)
template.Must(t.Parse(fmt)).Execute(b, v) // TODO: обработать возможные ошибки
_ = template.Must(t.Parse(fmt)).Execute(b, v) // TODO: обработать возможные ошибки
return b.String()
}

23
wconn.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"sync"
"github.com/gorilla/websocket"
)
type WsConnection struct {
w *websocket.Conn
mu sync.Mutex
}
func NewWsConnection(w *websocket.Conn) *WsConnection {
return &WsConnection{w: w}
}
func (w *WsConnection) Send(message any) error {
w.mu.Lock()
defer w.mu.Unlock()
return w.w.WriteJSON(message)
}

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>gron</title>
<link rel="stylesheet" href="/style.css">
<link rel="icon" href="data:,">
</head>
<body>
@ -35,10 +36,9 @@
<th>Details</th>
</tr>
{{range (index $.Jobs .)}}
<tr>
<tr id="{{.Name}}">
<td class="no-padding">
<form action="/start" method="get" id="form-{{.Name}}"></form>
<button{{if gt .CurrentRunningCount 0}} class="runningbg" {{else}}{{if .LastError}} class="errorbg" {{end}}{{end}} type="submit" form="form-{{.Name}}" name="jobName" value="{{.Name}}" {{if gt .CurrentRunningCount 0}} disabled{{end}}>{{.Name}}</button>
<button{{if gt .CurrentRunningCount 0}} class="runningbg" {{else}}{{if .LastError}} class="errorbg" {{end}}{{end}} name="jobName" value="{{.Name}}" {{if gt .CurrentRunningCount 0}} disabled{{end}} onclick='startJob("{{.Name}}")'>{{.Name}}</button>
</td>
<td class="smaller">{{.JobConfig.Description}}</td>
<td class="nowrap" align="right">
@ -54,5 +54,56 @@
</table>{{end}}
</main>
</body>
<script>
let socket = new WebSocket("ws://" + window.location.host + "/ws")
socket.onerror = function(error) {
console.log("WebSocket error: " + JSON.stringify(error))
socket.close()
}
socket.onmessage = function(event) {
message = JSON.parse(event.data);
html4 = "unknown"
if (message.Status == 0) {
html4 = "&#x2bc0; inactive"
} else if (message.Status == 1) {
html4 = '<span class="green">&#x2bc8; running</span>'
} else if (message.Status == 2) {
html4 = '<span class="red">&#x2bc1; error</span>'
} else if (message.Status == 3) {
html4 = '<span class="orange">&#x27f3; restarting</span>'
}
elementId = encodeURIComponent(message.Name)
if (message.CurrentRunningCount > 0) {
document.querySelector("#" + elementId + " > td:nth-child(1) > button").className = "runningbg"
} else if (message.LastError != "") {
document.querySelector("#" + elementId + " > td:nth-child(1) > button").className = "errorbg"
} else {
document.querySelector("#" + elementId + " > td:nth-child(1) > button").removeAttribute("class")
}
if (message.CurrentRunningCount > 0) {
document.querySelector("#" + elementId + " > td:nth-child(1) > button").setAttribute("disabled", "true")
} else {
document.querySelector("#" + elementId + " > td:nth-child(1) > button").removeAttribute("disabled")
}
document.querySelector("#" + elementId + " > td:nth-child(4)").innerHTML = html4
document.querySelector("#" + elementId + " > td:nth-child(5)").innerHTML = message.LastStartTime
document.querySelector("#" + elementId + " > td:nth-child(6)").innerHTML = message.LastEndTime
document.querySelector("#" + elementId + " > td:nth-child(7)").innerHTML = message.LastExecutionDuration
document.querySelector("#" + elementId + " > td:nth-child(8)").innerHTML = message.NextLaunch
}
function startJob(jobName) {
socket.send(JSON.stringify({
jobName
}));
}
</script>
</html>

View File

@ -106,6 +106,7 @@ th {
table button {
width: 100%;
text-align: left;
color: #fff;
background-color: var(--base01);
border: 0;
@ -158,7 +159,7 @@ pre {
.dropdown {
position: relative;
display: inline-block;
display: block;
float: right;
}