watchmonkey_cli 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +3 -0
- data/README.md +14 -2
- data/VERSION +1 -1
- data/doc/checker_example.rb +2 -0
- data/doc/config_example.rb +56 -0
- data/lib/watchmonkey_cli/application.rb +24 -2
- data/lib/watchmonkey_cli/application/configuration.rb +21 -1
- data/lib/watchmonkey_cli/application/configuration.tpl +21 -0
- data/lib/watchmonkey_cli/application/core.rb +41 -6
- data/lib/watchmonkey_cli/application/dispatch.rb +5 -1
- data/lib/watchmonkey_cli/application/output_helper.rb +4 -0
- data/lib/watchmonkey_cli/checker.rb +14 -4
- data/lib/watchmonkey_cli/checkers/dev_pry.rb +1 -1
- data/lib/watchmonkey_cli/hooks/platypus.rb +2 -2
- data/lib/watchmonkey_cli/hooks/requeue.rb +12 -1
- data/lib/watchmonkey_cli/hooks/telegram_bot.rb +566 -0
- data/lib/watchmonkey_cli/loopback_connection.rb +7 -0
- data/lib/watchmonkey_cli/ssh_connection.rb +11 -1
- data/lib/watchmonkey_cli/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47f837c69b183c6b2f6f1670358d1bd4eb6745afd4db2335e2f1c95749c17022
|
4
|
+
data.tar.gz: bdc02ab27bbb69a4c15eb67c4bc310fbfc25493abb124684353ccddcf7a320e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: def716ff43830383dd7f095b5c502172ce9ac71f008a9cbee00cd18448aa8d893fdf306e717181e569e70a36facac814433b520674fc0957b27430e48857c6cf
|
7
|
+
data.tar.gz: 1fc92b89105fd547344474405c276661f1383486c5cf0443bc8ee989c991b0fe1a46c003bc88bede5ca3d7e3c91cd0e651e97d82e51d6f984291a5015fe7a63d
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,10 +4,11 @@ Watchmonkey is a very simple tool to monitor resources with Ruby without the nee
|
|
4
4
|
|
5
5
|
Before looking any further you might want to know:
|
6
6
|
|
7
|
-
* There is no escalation or notification system but you may add it yourself
|
8
|
-
* I created this for being used with [Platypus](http://sveinbjorn.org/platypus) hence the [Platypus Hook](https://github.com/2called-chaos/watchmonkey_cli/blob/master/lib/watchmonkey_cli/hooks/platypus.rb)
|
7
|
+
* There is no escalation or notification system (except experimental telegram bot) but you may add it yourself
|
8
|
+
* I originally created this for being used with [Platypus](http://sveinbjorn.org/platypus) hence the [Platypus Hook](https://github.com/2called-chaos/watchmonkey_cli/blob/master/lib/watchmonkey_cli/hooks/platypus.rb)
|
9
9
|
* This is how the text output looks like: [Screenshot](http://imgur.com/8yLYnKb)
|
10
10
|
* This is how the Platypus support looks like: [ProgressBar](http://imgur.com/Vd8ZD7A) [HTML/WebView](http://imgur.com/5FwmWFZ)
|
11
|
+
* This is how Telegram Bot looks for now: [Telegram Screenshot](http://imgur.com/HBONi51)
|
11
12
|
|
12
13
|
---
|
13
14
|
|
@@ -43,6 +44,8 @@ To get a list of available options invoke Watchmonkey with the `--help` or `-h`
|
|
43
44
|
--generate-config [myconfig] Generates a example config in ~/.watchmonkey
|
44
45
|
-l, --log [file] Log to file, defaults to ~/.watchmonkey/logs/watchmonkey.log
|
45
46
|
-t, --threads [NUM] Amount of threads to be used for checking (default: 10)
|
47
|
+
-e, --except tag1,tag2 Don't run tasks tagged with given tags
|
48
|
+
-o, --only tag1,tag2 Only run tasks tagged with given tags
|
46
49
|
-s, --silent Only print errors and infos
|
47
50
|
-q, --quiet Only print errors
|
48
51
|
|
@@ -81,6 +84,15 @@ If you want to monitor something that is not covered by the buildin handlers you
|
|
81
84
|
By default Watchmonkey will run all tests once and then exit. This addon will enable Watchmonkey to run in a loop and run tests on a periodic interval.
|
82
85
|
Since this seems like a core feature it might get included directly into Watchmonkey but for now take a look at the [application configuration file](https://github.com/2called-chaos/watchmonkey_cli/blob/master/doc/config_example.rb) and [ReQueue source code](https://github.com/2called-chaos/watchmonkey_cli/blob/master/lib/watchmonkey_cli/hooks/requeue.rb) for integration examples.
|
83
86
|
|
87
|
+
### Telegram Bot
|
88
|
+
Notify via Telegram. Experimental. Refer to [application configuration file](https://github.com/2called-chaos/watchmonkey_cli/blob/master/doc/config_example.rb) and [TelegramBot source code](https://github.com/2called-chaos/watchmonkey_cli/blob/master/lib/watchmonkey_cli/hooks/telegram_bot.rb) for further information.
|
89
|
+
|
90
|
+
* works with ReQueue (wouldn't make much sense otherwise huh?)
|
91
|
+
* optional per-user message throttling via checker uniqid (checker name + host + arguments)
|
92
|
+
* optional per-user only/except filters based on tags
|
93
|
+
* planned: robust telegram connection failure handling
|
94
|
+
* planned: per-user regex exclusion filters
|
95
|
+
|
84
96
|
### Platypus support
|
85
97
|
[Platypus](http://sveinbjorn.org/platypus) is a MacOS software to create dead simple GUI wrappers for scripts. There is buildin support for the interface types ProgressBar and WebView. For integration examples take a look at the [application configuration file](https://github.com/2called-chaos/watchmonkey_cli/blob/master/doc/config_example.rb) and [Platypus hook source code](https://github.com/2called-chaos/watchmonkey_cli/blob/master/lib/watchmonkey_cli/hooks/platypus.rb).
|
86
98
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.11.0
|
data/doc/checker_example.rb
CHANGED
@@ -23,6 +23,7 @@ module MyWatchmonkeyCheckers
|
|
23
23
|
# Called by configuration defining a check with all the arguments.
|
24
24
|
# e.g. my_checker "http://google.com", some_option: true
|
25
25
|
# Should invoke `app.enqueue` which will by default call `#check!` method with given arguments.
|
26
|
+
# Must have options as last argument!
|
26
27
|
def enqueue host, opts = {}
|
27
28
|
opts = { some_option: false }.merge(opts)
|
28
29
|
|
@@ -36,6 +37,7 @@ module MyWatchmonkeyCheckers
|
|
36
37
|
end
|
37
38
|
|
38
39
|
# First argument is the result object, all other arguments came from `app.enqueue` call.
|
40
|
+
# Must have options as last argument!
|
39
41
|
def check! result, host, opts = {}
|
40
42
|
# Do your checks and modify the result object.
|
41
43
|
# Debug messages will not show if -s/--silent or -q/--quiet argument is passed.
|
data/doc/config_example.rb
CHANGED
@@ -43,3 +43,59 @@ if @argv.delete("--platypus")
|
|
43
43
|
|
44
44
|
@opts[:colorize] = false # doesn't render in platypus
|
45
45
|
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
# Integrate Telegram notifications
|
50
|
+
# For options refer to the source code:
|
51
|
+
# https://github.com/2called-chaos/watchmonkey_cli/blob/master/lib/watchmonkey_cli/hooks/telegram_bot.rb
|
52
|
+
if @argv.delete("--telegram")
|
53
|
+
require "watchmonkey_cli/hooks/telegram_bot"
|
54
|
+
WatchmonkeyCli::TelegramBot.hook!(self, {
|
55
|
+
# to create a bot refer to https://core.telegram.org/bots#6-botfather
|
56
|
+
api_key: "123456789:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
57
|
+
|
58
|
+
# poll timeout, the longer this is (in seconds) the longer it will take to gracefully shut down
|
59
|
+
timeout: 5,
|
60
|
+
|
61
|
+
# optionally log incoming messages
|
62
|
+
#logger: Logger.new(STDOUT),
|
63
|
+
|
64
|
+
# purge old throttle data, default: 30.days
|
65
|
+
#throttle_retention: 30.days,
|
66
|
+
|
67
|
+
# retry sending messages that failed, default: false
|
68
|
+
# Not recommended since on connection failure a HUGE amount of messages will accumulate
|
69
|
+
# and spam you (and reach rate limits) upon connection restore.
|
70
|
+
#retry_on_egress_failure: false,
|
71
|
+
|
72
|
+
# configure your notification targets, if not listed you can't interact with the bot
|
73
|
+
notify: [
|
74
|
+
[
|
75
|
+
# your telegram ID, if you try talking to the bot it will tell you your ID
|
76
|
+
987654321,
|
77
|
+
|
78
|
+
# flags
|
79
|
+
# - :all -- same as :debug, :info, :error (not recommended)
|
80
|
+
# - :debug -- send all debug messages (not recommended)
|
81
|
+
# - :info -- send all info messages (not recommended)
|
82
|
+
# - :error -- send all error messages (RECOMMENDED)
|
83
|
+
# - :admin_flag -- allows access to some commands (/wm_shutdown /stats)
|
84
|
+
[:error, :admin_flag],
|
85
|
+
|
86
|
+
# options (all optional, you can comment them out but leave the {})
|
87
|
+
{
|
88
|
+
# throttle: seconds(int) -- throttle messages by checker uniqid for this long (0/false = no throttle, default)
|
89
|
+
throttle: 15*60,
|
90
|
+
|
91
|
+
# only: Array(string, symbol) -- only notify when tagged with given tags
|
92
|
+
only: %w[production critical],
|
93
|
+
|
94
|
+
# except: Array(string, symbol) -- don't notify when tagged with given tags (runs after only-check)
|
95
|
+
except: %w[database],
|
96
|
+
}
|
97
|
+
],
|
98
|
+
[123456789, [:error], { throttle: 30.minutes }]
|
99
|
+
],
|
100
|
+
})
|
101
|
+
end
|
@@ -21,9 +21,22 @@ module WatchmonkeyCli
|
|
21
21
|
app.haltpoint
|
22
22
|
rescue Interrupt
|
23
23
|
app.abort("Interrupted", 1)
|
24
|
+
rescue SystemExit
|
25
|
+
# silence
|
24
26
|
ensure
|
27
|
+
$wm_runtime_exiting = true
|
25
28
|
app.fire(:wm_shutdown)
|
26
|
-
app.
|
29
|
+
if app.filtered_threads.length > 1
|
30
|
+
app.error "[WARN] #{app.filtered_threads.length} threads remain (should be 1)..."
|
31
|
+
app.filtered_threads.each do |thr|
|
32
|
+
app.debug "[THR] #{Thread.main == thr ? "MAIN" : "THREAD"}\t#{thr.alive? ? "ALIVE" : "DEAD"}\t#{thr.inspect}", 10
|
33
|
+
thr.backtrace.each do |l|
|
34
|
+
app.debug "[THR]\t#{l}", 20
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
app.debug "1 thread remains..."
|
39
|
+
end
|
27
40
|
end
|
28
41
|
end
|
29
42
|
end
|
@@ -52,6 +65,8 @@ module WatchmonkeyCli
|
|
52
65
|
silent: false, # -s flag
|
53
66
|
quiet: false, # -q flag
|
54
67
|
stdout: STDOUT, # (internal) STDOUT redirect
|
68
|
+
tag_only: [], # -o flag
|
69
|
+
tag_except: [], # -e flag
|
55
70
|
}
|
56
71
|
init_params
|
57
72
|
yield(self)
|
@@ -65,6 +80,8 @@ module WatchmonkeyCli
|
|
65
80
|
opts.on("--generate-config [myconfig]", "Generates a example config in ~/.watchmonkey") {|s| @opts[:dispatch] = :generate_config; @opts[:config_name] = s }
|
66
81
|
opts.on("-l", "--log [file]", "Log to file, defaults to ~/.watchmonkey/logs/watchmonkey.log") {|s| @opts[:logfile] = s || logger_filename }
|
67
82
|
opts.on("-t", "--threads [NUM]", Integer, "Amount of threads to be used for checking (default: 10)") {|s| @opts[:threads] = s }
|
83
|
+
opts.on("-e", "--except tag1,tag2", Array, "Don't run tasks tagged with given tags") {|s| @opts[:tag_except] = s.map(&:to_sym) }
|
84
|
+
opts.on("-o", "--only tag1,tag2", Array, "Only run tasks tagged with given tags") {|s| @opts[:tag_only] = s.map(&:to_sym) }
|
68
85
|
opts.on("-s", "--silent", "Only print errors and infos") { @opts[:silent] = true }
|
69
86
|
opts.on("-q", "--quiet", "Only print errors") { @opts[:quiet] = true }
|
70
87
|
|
@@ -76,10 +93,15 @@ module WatchmonkeyCli
|
|
76
93
|
opts.on("-z", "Do not check for updates on GitHub (with -v/--version)") { @opts[:check_for_updates] = false }
|
77
94
|
opts.on("--dump-core", "for developers") { @opts[:dump] = true }
|
78
95
|
end
|
96
|
+
fire(:optparse_init, @optparse)
|
79
97
|
end
|
80
98
|
|
81
99
|
def parse_params
|
82
|
-
@optparse
|
100
|
+
fire(:optparse_parse_before, @optparse)
|
101
|
+
fire(:optparse_parse_around, @optparse) do
|
102
|
+
@optparse.parse!(@argv)
|
103
|
+
end
|
104
|
+
fire(:optparse_parse_after, @optparse)
|
83
105
|
rescue OptionParser::ParseError => e
|
84
106
|
abort(e.message)
|
85
107
|
dispatch(:help)
|
@@ -62,6 +62,7 @@ module WatchmonkeyCli
|
|
62
62
|
def initialize app, file
|
63
63
|
@app = app
|
64
64
|
@file = file
|
65
|
+
@tags = []
|
65
66
|
begin
|
66
67
|
eval File.read(file, encoding: "utf-8"), binding, file
|
67
68
|
rescue
|
@@ -74,9 +75,28 @@ module WatchmonkeyCli
|
|
74
75
|
@app.fetch_connection(:ssh, name, opts, &b)
|
75
76
|
end
|
76
77
|
|
78
|
+
def tag_all! *tags
|
79
|
+
@tags = tags.map(&:to_sym)
|
80
|
+
end
|
81
|
+
|
77
82
|
def method_missing meth, *args, &block
|
78
83
|
if c = @app.checkers[meth.to_s]
|
79
|
-
|
84
|
+
opts = args.extract_options!
|
85
|
+
only = @app.opts[:tag_only]
|
86
|
+
except = @app.opts[:tag_except]
|
87
|
+
tags = (@tags + (opts[:tags] || []).map(&:to_sym)).uniq
|
88
|
+
if only.any?
|
89
|
+
if tags.any?{|t| only.include?(t) }
|
90
|
+
if tags.any?{|t| except.include?(t) }
|
91
|
+
return @app.debug "Skipping #{meth} with #{args} and #{opts} due to tag_except filter..."
|
92
|
+
end
|
93
|
+
else
|
94
|
+
return @app.debug "Skipping #{meth} with #{args} and #{opts} due to tag_only filter..."
|
95
|
+
end
|
96
|
+
elsif tags.any?{|t| except.include?(t) }
|
97
|
+
return @app.debug "Skipping #{meth} with #{args} and #{opts} due to tag_except filter..."
|
98
|
+
end
|
99
|
+
c.enqueue(*args, opts.merge(tags: tags))
|
80
100
|
else
|
81
101
|
super
|
82
102
|
end
|
@@ -1,5 +1,26 @@
|
|
1
1
|
# This is a Ruby file!
|
2
2
|
|
3
|
+
# =========================================
|
4
|
+
# = Step 0: Tag all checkers in this file =
|
5
|
+
# =========================================
|
6
|
+
|
7
|
+
# All checkers you define after this will get these
|
8
|
+
# base tags (you can pass additional ones).
|
9
|
+
# Do note that you can call this method multiple
|
10
|
+
# times and it will replace your base tags for
|
11
|
+
# all checkers to follow, base tags will be cleared
|
12
|
+
# upon reaching the end of the file.
|
13
|
+
|
14
|
+
tag_all! :production, :critical
|
15
|
+
|
16
|
+
# You can use additional tags on any checker by passing
|
17
|
+
# a `tags` option consisting of an Array of strings or symbols.
|
18
|
+
# e.g.:
|
19
|
+
# some_checker :some_target, tags: %w[critical foo bar]
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
|
3
24
|
# =================================
|
4
25
|
# = Step 1: Setup SSH connections =
|
5
26
|
# =================================
|
@@ -1,6 +1,12 @@
|
|
1
1
|
module WatchmonkeyCli
|
2
2
|
class Application
|
3
3
|
module Core
|
4
|
+
def filtered_threads
|
5
|
+
Thread.list.reject do |thr|
|
6
|
+
thr.backtrace[0]["gems/concurrent-ruby"] rescue false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
4
10
|
# ===================
|
5
11
|
# = Signal trapping =
|
6
12
|
# ===================
|
@@ -37,10 +43,31 @@ module WatchmonkeyCli
|
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
40
|
-
def fire which, *args
|
46
|
+
def fire which, *args, &block
|
41
47
|
return if @disable_event_firing
|
42
|
-
sync { debug "[Event] Firing #{which} (#{@hooks[which].try(:length) || 0} handlers) #{args.map(&:class)}", 99 }
|
43
|
-
|
48
|
+
sync { debug "[Event] Firing #{which} (#{@hooks[which].try(:length) || 0} handlers) #{args.map(&:class)}#{" HAS_BLOCK" if block}", 99 }
|
49
|
+
if block && (!@hooks[which] || @hooks[which].empty?)
|
50
|
+
block.call
|
51
|
+
else
|
52
|
+
if @hooks[which] && @hooks[which].any?
|
53
|
+
if block
|
54
|
+
catch :abort do
|
55
|
+
_fire_around(@hooks[which], args, 0, &block)
|
56
|
+
end
|
57
|
+
else
|
58
|
+
@hooks[which].all?{|h| h.call(*args) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def _fire_around hooks, args, index = 0, &block
|
65
|
+
return block.call unless hook = hooks[index]
|
66
|
+
skip = catch(:skip) {
|
67
|
+
hook.call(*args) { _fire_around(hooks, args, index + 1, &block) }
|
68
|
+
nil
|
69
|
+
}
|
70
|
+
_fire_around(hooks, args, index + 1, &block) if skip
|
44
71
|
end
|
45
72
|
|
46
73
|
|
@@ -82,8 +109,10 @@ module WatchmonkeyCli
|
|
82
109
|
def close_connections!
|
83
110
|
@connections.each do |type, clist|
|
84
111
|
clist.each do |id, con|
|
85
|
-
|
86
|
-
|
112
|
+
if con.established?
|
113
|
+
debug "[SHUTDOWN] closing #{type} connection #{id} #{con}"
|
114
|
+
con.close!
|
115
|
+
end
|
87
116
|
end
|
88
117
|
end
|
89
118
|
end
|
@@ -100,8 +129,14 @@ module WatchmonkeyCli
|
|
100
129
|
@queue << [checker, a, ->(*a) {
|
101
130
|
begin
|
102
131
|
result = Checker::Result.new(checker, *a)
|
132
|
+
|
133
|
+
# assign tags
|
134
|
+
taskopts = a.extract_options!
|
135
|
+
result.tags = taskopts[:tags] || []
|
136
|
+
a << taskopts
|
137
|
+
|
103
138
|
checker.debug(result.str_running)
|
104
|
-
checker.
|
139
|
+
checker.rsafe(result) {
|
105
140
|
timeout = checker.class.maxrt.nil? ? @opts[:maxrt] : checker.class.maxrt
|
106
141
|
timeout = timeout.call(self, checker, a) if timeout.respond_to?(:call)
|
107
142
|
begin
|
@@ -3,7 +3,11 @@ module WatchmonkeyCli
|
|
3
3
|
module Dispatch
|
4
4
|
def dispatch action = (@opts[:dispatch] || :help)
|
5
5
|
if respond_to?("dispatch_#{action}")
|
6
|
-
|
6
|
+
fire(:dispatch_before, action)
|
7
|
+
fire(:dispatch_around, action) do
|
8
|
+
send("dispatch_#{action}")
|
9
|
+
end
|
10
|
+
fire(:dispatch_after, action)
|
7
11
|
else
|
8
12
|
abort("unknown action #{action}", 1)
|
9
13
|
end
|
@@ -52,16 +52,24 @@ module WatchmonkeyCli
|
|
52
52
|
|
53
53
|
class Result
|
54
54
|
attr_reader :checker, :type, :args
|
55
|
-
attr_accessor :result, :command, :data
|
55
|
+
attr_accessor :result, :command, :data, :tags
|
56
56
|
|
57
57
|
def initialize checker, *args
|
58
58
|
@checker = checker
|
59
59
|
@args = args
|
60
60
|
@mutex = Monitor.new
|
61
61
|
@type = :info
|
62
|
+
@tags = []
|
62
63
|
@spool = { error: [], info: [], debug: []}
|
63
64
|
end
|
64
65
|
|
66
|
+
def uniqid additional = []
|
67
|
+
([
|
68
|
+
self.class.name,
|
69
|
+
@args.map(&:to_s).to_s,
|
70
|
+
] + additional).join("/")
|
71
|
+
end
|
72
|
+
|
65
73
|
def sync &block
|
66
74
|
@mutex.synchronize(&block)
|
67
75
|
end
|
@@ -197,10 +205,12 @@ module WatchmonkeyCli
|
|
197
205
|
e.backtrace.each{|l| resultobj.debug "\t\t#{l}" }
|
198
206
|
resultobj.dump!
|
199
207
|
end
|
200
|
-
|
201
|
-
|
208
|
+
unless $wm_runtime_exiting
|
209
|
+
sleep 1
|
210
|
+
retry
|
211
|
+
end
|
202
212
|
end
|
203
|
-
resultobj.error! "
|
213
|
+
resultobj.error! "retries exceeded"
|
204
214
|
resultobj.dump!
|
205
215
|
end
|
206
216
|
end
|
@@ -5,7 +5,7 @@ module WatchmonkeyCli
|
|
5
5
|
self.maxrt = false
|
6
6
|
|
7
7
|
def enqueue host, opts = {}
|
8
|
-
|
8
|
+
host = app.fetch_connection(:loopback, :local) if !host || host == :local
|
9
9
|
host = app.fetch_connection(:ssh, host) if host.is_a?(Symbol)
|
10
10
|
app.enqueue(self, host, opts)
|
11
11
|
end
|
@@ -71,7 +71,7 @@ module WatchmonkeyCli
|
|
71
71
|
<dt>Items in Queue</dt><dd class="qlength">#{@queue.length}</dd>
|
72
72
|
<dt>Items in ReQ</dt><dd class="rqlength">#{@requeue.length}</dd>
|
73
73
|
<dt>Workers</dt><dd class="workers">#{@threads.select{|t| t[:working] }.length}/#{@threads.length} working (#{@threads.select(&:alive?).length} alive)</dd>
|
74
|
-
<dt>Threads</dt><dd class="tlength">#{
|
74
|
+
<dt>Threads</dt><dd class="tlength">#{filtered_threads.length}</dd>
|
75
75
|
<dt>Processed entries</dt><dd class="processed">#{@processed}</dd>
|
76
76
|
<dt>Watching since</dt><dd>#{@boot}</dd>
|
77
77
|
<dt>Last draw</dt><dd class="lastdraw">#{Time.current}</dd>
|
@@ -93,7 +93,7 @@ module WatchmonkeyCli
|
|
93
93
|
$("dd.qlength").html("#{@queue.length}");
|
94
94
|
$("dd.rqlength").html("#{@requeue.length}");
|
95
95
|
$("dd.workers").html("#{@threads.select{|t| t[:working] }.length}/#{@threads.length} working#{ti}");
|
96
|
-
$("dd.tlength").html("#{
|
96
|
+
$("dd.tlength").html("#{filtered_threads.length}");
|
97
97
|
$("dd.processed").html("#{@processed}");
|
98
98
|
$("dd.lastdraw").html("#{Time.current}");
|
99
99
|
$("pre.lasterrors").html("#{escape_javascript @platypus_status_cache[:errors].map{|t,e| "#{t}: #{e}" }.join("\n")}");
|
@@ -37,11 +37,12 @@ module WatchmonkeyCli
|
|
37
37
|
puts " Queue: #{@queue.length}"
|
38
38
|
puts " Requeue: #{@requeue.length}"
|
39
39
|
puts " Workers: #{@threads.select{|t| t[:working] }.length}/#{@threads.length} working (#{@threads.select(&:alive?).length} alive)"
|
40
|
-
puts " Threads: #{
|
40
|
+
puts " Threads: #{filtered_threads.length}"
|
41
41
|
# puts " #{@threads.select(&:alive?).length} alive"
|
42
42
|
# puts " #{@threads.select{|t| t.status == "run" }.length} running"
|
43
43
|
# puts " #{@threads.select{|t| t.status == "sleep" }.length} sleeping"
|
44
44
|
puts " Processed: #{@processed}"
|
45
|
+
puts " Promises: #{@telegram_bot_egress_promises.length}" if @telegram_bot_egress_promises
|
45
46
|
puts "========== //STATUS =========="
|
46
47
|
end
|
47
48
|
end
|
@@ -101,6 +102,16 @@ module WatchmonkeyCli
|
|
101
102
|
}]
|
102
103
|
end
|
103
104
|
end
|
105
|
+
|
106
|
+
def requeue_runall
|
107
|
+
return if $wm_runtime_exiting
|
108
|
+
sync do
|
109
|
+
debug "Running all queued tasks immediately!"
|
110
|
+
@requeue.each_with_index do |(run_at, callback), index|
|
111
|
+
@requeue[index][0] = Time.now
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
104
115
|
end
|
105
116
|
end
|
106
117
|
end
|
@@ -0,0 +1,566 @@
|
|
1
|
+
module WatchmonkeyCli
|
2
|
+
class TelegramBot
|
3
|
+
BotProbeError = Class.new(::RuntimeError)
|
4
|
+
UnknownEgressType = Class.new(::RuntimeError)
|
5
|
+
|
6
|
+
class Event
|
7
|
+
attr_reader :app, :bot
|
8
|
+
|
9
|
+
def initialize app, bot, event
|
10
|
+
@app, @bot, @event = app, bot, event
|
11
|
+
end
|
12
|
+
|
13
|
+
def raw
|
14
|
+
@event
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing method, *args, &block
|
18
|
+
@event.__send__(method, *args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def user_data
|
22
|
+
app.telegram_bot_get_udata(raw.from.id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_admin?
|
26
|
+
user_data && user_data[1].include?(:admin_flag)
|
27
|
+
end
|
28
|
+
|
29
|
+
def from_descriptive
|
30
|
+
"".tap do |name|
|
31
|
+
name << "[BOT] " if from.is_bot
|
32
|
+
name << "#{from.first_name} " if from.first_name.present?
|
33
|
+
name << "#{from.last_name} " if from.last_name.present?
|
34
|
+
name << "(#{from.username}) " if from.username.present?
|
35
|
+
name << "[##{from.id}]"
|
36
|
+
end.strip
|
37
|
+
end
|
38
|
+
|
39
|
+
def message?
|
40
|
+
raw.is_a?(Telegram::Bot::Types::Message)
|
41
|
+
end
|
42
|
+
|
43
|
+
def message
|
44
|
+
return false unless message?
|
45
|
+
raw.text.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def chunks
|
49
|
+
@_chunks ||= message.split(" ")
|
50
|
+
end
|
51
|
+
|
52
|
+
def command?
|
53
|
+
raw.is_a?(Telegram::Bot::Types::Message) && message.start_with?("/")
|
54
|
+
end
|
55
|
+
|
56
|
+
def command
|
57
|
+
chunks.first if command?
|
58
|
+
end
|
59
|
+
|
60
|
+
def args
|
61
|
+
chunks[1..-1]
|
62
|
+
end
|
63
|
+
|
64
|
+
def reply msg, msgopts = {}
|
65
|
+
return unless msg.present?
|
66
|
+
msgopts = msgopts.merge(text: msg, chat_id: raw.chat.id)
|
67
|
+
msgopts = msgopts.merge(reply_to_message_id: raw.message_id) unless msgopts[:quote] == false
|
68
|
+
app.telegram_bot_send(msgopts.except(:quote))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class BetterQueue
|
73
|
+
def initialize
|
74
|
+
@stor = []
|
75
|
+
@monitor = Monitor.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def sync &block
|
79
|
+
@monitor.synchronize(&block)
|
80
|
+
end
|
81
|
+
|
82
|
+
[:length, :empty?, :push, :pop, :unshift, :shift, :<<].each do |meth|
|
83
|
+
define_method(meth) do |*args, &block|
|
84
|
+
sync { @stor.send(meth, *args, &block) }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class PromiseHandler < BetterQueue
|
90
|
+
def scrub!
|
91
|
+
@stor.dup.each_with_index do |p, i|
|
92
|
+
@stor.delete(p) if [:fulfilled, :rejected].include?(p.state)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def await_all!
|
97
|
+
while p = shift
|
98
|
+
p.wait
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.hook!(app, bot_opts = {})
|
104
|
+
app.opts[:telegram_bot] = bot_opts.reverse_merge({
|
105
|
+
debug: false,
|
106
|
+
info: true,
|
107
|
+
error: true,
|
108
|
+
throttle_retention: 30.days,
|
109
|
+
retry_on_egress_failure: false,
|
110
|
+
notify: []
|
111
|
+
})
|
112
|
+
|
113
|
+
app.instance_eval do
|
114
|
+
hook :dispatch_around do |action, &act|
|
115
|
+
throw :skip, true unless action == :index
|
116
|
+
begin
|
117
|
+
require "telegram/bot"
|
118
|
+
rescue LoadError
|
119
|
+
abort "[TelegramBot] cannot load telegram/bot, run\n\t# gem install telegram-bot-ruby"
|
120
|
+
end
|
121
|
+
telegram_bot_dispatch(&act)
|
122
|
+
end
|
123
|
+
|
124
|
+
[:debug, :info, :error].each do |level|
|
125
|
+
hook :"on_#{level}" do |msg, robj|
|
126
|
+
telegram_bot_notify(msg, robj)
|
127
|
+
end if app.opts[:telegram_bot][level]
|
128
|
+
end
|
129
|
+
|
130
|
+
hook :wm_shutdown do
|
131
|
+
debug "[TelegramBot] shutting down telegram bot ingress thread"
|
132
|
+
if @telegram_bot_ingress&.alive?
|
133
|
+
@telegram_bot_ingress[:stop] = true
|
134
|
+
@telegram_bot_ingress.join
|
135
|
+
end
|
136
|
+
|
137
|
+
debug "[TelegramBot] shutting down telegram bot egress thread"
|
138
|
+
if @telegram_bot_egress&.alive?
|
139
|
+
telegram_bot_msg_admins "<b>ALERT: Watchmonkey is shutting down!</b>", parse_mode: "HTML" rescue false
|
140
|
+
@telegram_bot_egress_promises.await_all!
|
141
|
+
@telegram_bot_egress[:stop] = true
|
142
|
+
@telegram_bot_egress.join
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# --------------------
|
147
|
+
|
148
|
+
def telegram_bot_state
|
149
|
+
"#{wm_cfg_path}/telegram_bot.wmstate"
|
150
|
+
end
|
151
|
+
|
152
|
+
def telegram_bot_dispatch &act
|
153
|
+
parent_thread = Thread.current
|
154
|
+
@telegram_bot_throttle_locks = {}
|
155
|
+
@telegram_bot_egress_promises = PromiseHandler.new
|
156
|
+
@telegram_bot_egress_queue = BetterQueue.new
|
157
|
+
telegram_bot_throttle # eager load
|
158
|
+
telegram_bot_normalize_notify_udata
|
159
|
+
|
160
|
+
# ==================
|
161
|
+
# = ingress thread =
|
162
|
+
# ==================
|
163
|
+
debug "[TelegramBot] Starting telegram bot ingress thread..."
|
164
|
+
@telegram_bot_ingress = Thread.new do
|
165
|
+
Thread.current.abort_on_exception = true
|
166
|
+
begin
|
167
|
+
until Thread.current[:stop]
|
168
|
+
begin
|
169
|
+
telegram_bot.fetch_updates {|ev| telegram_bot_handle_event(ev) }
|
170
|
+
rescue SocketError, Faraday::ConnectionFailed => ex
|
171
|
+
error "[TelegramBot] Poll failure, retrying in 3 seconds (#{ex.class}: #{ex.message})"
|
172
|
+
sleep 3 unless Thread.current[:stop]
|
173
|
+
retry unless Thread.current[:stop]
|
174
|
+
end
|
175
|
+
end
|
176
|
+
rescue StandardError => ex
|
177
|
+
parent_thread.raise(ex)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# wait for telegram bot to be ready
|
182
|
+
begin
|
183
|
+
Timeout::timeout(10) do
|
184
|
+
loop do
|
185
|
+
break if @telegram_bot_info
|
186
|
+
sleep 0.5
|
187
|
+
end
|
188
|
+
end
|
189
|
+
rescue Timeout::Error
|
190
|
+
abort "[TelegramBot] Failed to start telegram bot within 10 seconds, aborting...", 2
|
191
|
+
end
|
192
|
+
|
193
|
+
# ==================
|
194
|
+
# = egress thread =
|
195
|
+
# ==================
|
196
|
+
debug "[TelegramBot] Starting telegram bot egress thread..."
|
197
|
+
@telegram_bot_egress = Thread.new do
|
198
|
+
Thread.current.abort_on_exception = true
|
199
|
+
begin
|
200
|
+
until Thread.current[:stop] && @telegram_bot_egress_queue.empty?
|
201
|
+
begin
|
202
|
+
promise, item = @telegram_bot_egress_queue.shift
|
203
|
+
if item
|
204
|
+
mode = item.delete(:__mode)
|
205
|
+
case mode
|
206
|
+
when :send then promise.set telegram_bot.api.send_message(item)
|
207
|
+
when :edit then promise.set telegram_bot.api.edit_message_text(item)
|
208
|
+
else raise(UnknownEgressType, "Unknown egress mode `#{mode}'!")
|
209
|
+
end
|
210
|
+
else
|
211
|
+
@telegram_bot_egress_promises.scrub!
|
212
|
+
sleep 0.5
|
213
|
+
end
|
214
|
+
rescue SocketError, Faraday::ConnectionFailed => ex
|
215
|
+
if opts[:telegram_bot][:retry_on_egress_failure]
|
216
|
+
error "[TelegramBot] Push failure, retrying in 3 seconds (#{ex.class}: #{ex.message})"
|
217
|
+
@telegram_bot_egress_queue.unshift([promise, item.merge(__mode: mode)])
|
218
|
+
sleep 3 unless Thread.current[:stop]
|
219
|
+
else
|
220
|
+
error "[TelegramBot] Push failure, discarding message (#{ex.class}: #{ex.message})"
|
221
|
+
sleep 0.5
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
rescue StandardError => ex
|
226
|
+
parent_thread.raise(ex)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
telegram_bot_msg_admins "<b>INFO: Watchmonkey is watching#{" ONCE" unless opts[:loop_forever]}!</b>", disable_notification: true, parse_mode: "HTML"
|
231
|
+
act.call
|
232
|
+
rescue BotProbeError => ex
|
233
|
+
abort ex.message
|
234
|
+
ensure
|
235
|
+
@telegram_bot_egress_promises.await_all!
|
236
|
+
File.open(telegram_bot_state, "w+") do |f|
|
237
|
+
f << telegram_bot_throttle.to_json
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def telegram_bot
|
242
|
+
@telegram_bot ||= begin
|
243
|
+
Telegram::Bot::Client.new(opts[:telegram_bot][:api_key], opts[:telegram_bot].except(:api_key)).tap do |bot|
|
244
|
+
begin
|
245
|
+
me = bot.api.get_me
|
246
|
+
if me["ok"]
|
247
|
+
@telegram_bot_info = me["result"]
|
248
|
+
info "[TelegramBot] Established client [#{@telegram_bot_info["id"]}] #{@telegram_bot_info["first_name"]} (#{@telegram_bot_info["username"]})"
|
249
|
+
else
|
250
|
+
raise BotProbeError, "Failed to get telegram client information, got NOK response (#{me.inspect})"
|
251
|
+
end
|
252
|
+
rescue HTTParty::RedirectionTooDeep, Telegram::Bot::Exceptions::ResponseError => ex
|
253
|
+
raise BotProbeError, "Failed to get telegram client information, API key correct? (#{ex.class})"
|
254
|
+
rescue SocketError, Faraday::ConnectionFailed => ex
|
255
|
+
raise BotProbeError, "Failed to get telegram client information, connection failure? (#{ex.class}: #{ex.message})"
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def telegram_bot_send payload
|
262
|
+
Concurrent::Promise.new.tap do |promise|
|
263
|
+
@telegram_bot_egress_queue.push([promise, payload.merge(__mode: :send)])
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def telegram_bot_edit payload
|
268
|
+
Concurrent::Promise.new.tap do |promise|
|
269
|
+
@telegram_bot_egress_queue.push([promise, payload.merge(__mode: :edit)])
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def telegram_bot_tid_exclusive tid, &block
|
274
|
+
sync do
|
275
|
+
@telegram_bot_throttle_locks[tid] ||= Monitor.new
|
276
|
+
end
|
277
|
+
@telegram_bot_throttle_locks[tid].synchronize(&block)
|
278
|
+
end
|
279
|
+
|
280
|
+
def telegram_bot_throttle
|
281
|
+
@telegram_bot_throttle ||= begin
|
282
|
+
Hash.new {|h,k| h[k] = Hash.new }.tap do |h|
|
283
|
+
if File.exist?(telegram_bot_state)
|
284
|
+
json = File.read(telegram_bot_state)
|
285
|
+
if json.present?
|
286
|
+
JSON.parse(json).each do |k, v|
|
287
|
+
case k
|
288
|
+
when "__mute_until"
|
289
|
+
h[k] = Time.parse(v)
|
290
|
+
else
|
291
|
+
v.each do |k, data|
|
292
|
+
data[0] = Time.parse(data[0])
|
293
|
+
data[1] = Time.parse(data[1])
|
294
|
+
end
|
295
|
+
v.delete_if {|k, data| data[0] < opts[:telegram_bot][:throttle_retention].ago }
|
296
|
+
h[k.to_i] = v
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def telegram_bot_normalize_notify_udata
|
306
|
+
opts[:telegram_bot][:notify].each do |a|
|
307
|
+
a[1] = (a[1] ? [*a[1]] : [:error]).flat_map(&:to_sym).uniq
|
308
|
+
a[2] ||= {}
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def telegram_bot_get_udata lookup_tid
|
313
|
+
opts[:telegram_bot][:notify].detect do |tid, level, topts|
|
314
|
+
tid == lookup_tid
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def telegram_bot_user_muted? tid, notify = true
|
319
|
+
telegram_bot_tid_exclusive(tid) do
|
320
|
+
return false unless mute_until = telegram_bot_throttle[tid]["__mute_until"]
|
321
|
+
if mute_until > Time.current
|
322
|
+
return true
|
323
|
+
else
|
324
|
+
telegram_bot_throttle[tid].delete("__mute_until")
|
325
|
+
telegram_bot_send(text: "You have been unmuted (expired #{mute_until})", chat_id: tid, disable_notification: true) if notify
|
326
|
+
return false
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def telegram_bot_notify msg, robj
|
332
|
+
to_notify = opts[:telegram_bot][:notify].select do |tid, level, topts|
|
333
|
+
level.include?(:all) || level.include?(robj.type)
|
334
|
+
end
|
335
|
+
|
336
|
+
to_notify.each do |tid, level, topts|
|
337
|
+
telegram_bot_tid_exclusive(tid) do
|
338
|
+
next if telegram_bot_user_muted?(tid)
|
339
|
+
if robj
|
340
|
+
# gate only tags
|
341
|
+
next if topts[:only] && topts[:only].any? && !robj.tags.any?{|t| topts[:only].include?(t) }
|
342
|
+
|
343
|
+
# gate except tags
|
344
|
+
next if topts[:except] && topts[:except].any? && robj.tags.any?{|t| topts[:except].include?(t) }
|
345
|
+
|
346
|
+
# send message
|
347
|
+
throttle = telegram_bot_throttle[tid][robj.uniqid] ||= [Time.current, Time.current, 0, nil]
|
348
|
+
throttle[2] += 1
|
349
|
+
if (Time.current - throttle[0]) <= (topts[:throttle] || 0) && throttle[3]
|
350
|
+
throttle[1] = Time.current
|
351
|
+
_telegram_bot_sendmsg(tid, msg, throttle[2], throttle, throttle[3])
|
352
|
+
else
|
353
|
+
throttle[0] = Time.current
|
354
|
+
throttle[2] = 1
|
355
|
+
throttle[3] = nil
|
356
|
+
_telegram_bot_sendmsg(tid, msg, throttle[2], throttle)
|
357
|
+
end
|
358
|
+
else
|
359
|
+
_telegram_bot_sendmsg(tid, msg, 0, [])
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def _telegram_bot_sendmsg tid, msg, repeat, result_to, msgid = nil
|
366
|
+
msg = "<pre>#{ERB::Util.h msg}</pre>"
|
367
|
+
msg = "#{msg}\n<b>(message repeated #{repeat} times)</b> #{" <i>last occurance: #{result_to[1]}</i>" if result_to[1]}" if repeat > 1 && msgid
|
368
|
+
(msgid ?
|
369
|
+
telegram_bot_edit(chat_id: tid, message_id: msgid, text: msg, disable_web_page_preview: true, parse_mode: "html")
|
370
|
+
:
|
371
|
+
telegram_bot_send(chat_id: tid, text: msg, disable_web_page_preview: true, parse_mode: "html")
|
372
|
+
).tap do |promise|
|
373
|
+
await = Concurrent::Promise.new
|
374
|
+
promise.on_success do |m|
|
375
|
+
if msgid = m.dig("result", "message_id")
|
376
|
+
result_to[3] = msgid
|
377
|
+
await.set :success
|
378
|
+
else
|
379
|
+
puts m.inspect
|
380
|
+
await.set :failed
|
381
|
+
end
|
382
|
+
end
|
383
|
+
promise.on_error do |ex|
|
384
|
+
error "[TelegramBot] MessagePromiseError - #{ex.class}:#{ex.message}"
|
385
|
+
await.set :failed
|
386
|
+
end
|
387
|
+
@telegram_bot_egress_promises << await
|
388
|
+
end
|
389
|
+
rescue
|
390
|
+
end
|
391
|
+
|
392
|
+
def _telegram_bot_timestr_parse *chunks
|
393
|
+
time = Time.current
|
394
|
+
chunks.flatten.each do |chunk|
|
395
|
+
if chunk.end_with?("d")
|
396
|
+
time += chunk[0..-2].to_i.days
|
397
|
+
elsif chunk.end_with?("h")
|
398
|
+
time += chunk[0..-2].to_i.hours
|
399
|
+
elsif chunk.end_with?("m")
|
400
|
+
time += chunk[0..-2].to_i.minutes
|
401
|
+
elsif chunk.end_with?("s")
|
402
|
+
time += chunk[0..-2].to_i.seconds
|
403
|
+
else
|
404
|
+
time += chunk.to_i.seconds
|
405
|
+
end
|
406
|
+
end
|
407
|
+
time
|
408
|
+
end
|
409
|
+
|
410
|
+
def telegram_bot_handle_event ev
|
411
|
+
Event.new(self, telegram_bot, ev).tap do |event|
|
412
|
+
begin
|
413
|
+
ctrl = catch :event_control do
|
414
|
+
case event.command
|
415
|
+
when "/ping"
|
416
|
+
event.reply "Pong!"
|
417
|
+
throw :event_control, :done
|
418
|
+
when "/start"
|
419
|
+
if event.user_data
|
420
|
+
event.reply [].tap{|m|
|
421
|
+
m << "<b>Welcome!</b> I will tell you if something is wrong with bmonkeys infrastructure."
|
422
|
+
m << "Your current tags are: #{event.user_data[1].join(", ")}"
|
423
|
+
m << "<b>You have admin permissions!</b>" if event.user_admin?
|
424
|
+
m << "\nInstead of muting me in Telegram you can silence me for a while with <code>/mute 6h 30m</code>."
|
425
|
+
m << "Use <code>/help</code> for more information."
|
426
|
+
}.join("\n"), parse_mode: "HTML"
|
427
|
+
else
|
428
|
+
event.reply %{
|
429
|
+
Hello there, unfortunately I don't recognize your user id (#{event.from.id}).
|
430
|
+
Please ask your admin to add you to the configuration file.
|
431
|
+
}
|
432
|
+
end
|
433
|
+
throw :event_control, :done
|
434
|
+
when "/mute"
|
435
|
+
if event.user_data
|
436
|
+
telegram_bot_tid_exclusive(event.from.id) do
|
437
|
+
telegram_bot_user_muted?(event.from.id) # clears if expired
|
438
|
+
if event.args.any?
|
439
|
+
mute_until = _telegram_bot_timestr_parse(event.args)
|
440
|
+
telegram_bot_throttle[event.from.id]["__mute_until"] = mute_until
|
441
|
+
event.reply "You are muted until #{mute_until}\nUse /unmute to prematurely cancel the mute.", disable_notification: true
|
442
|
+
else
|
443
|
+
msg = "Usage: <code>/mute <1d 2h 3m 4s></code>"
|
444
|
+
if mute_until = telegram_bot_throttle[event.from.id]["__mute_until"]
|
445
|
+
msg << "\n<b>You are currently muted until #{mute_until}</b> /unmute"
|
446
|
+
end
|
447
|
+
event.reply msg, parse_mode: "HTML", disable_notification: true
|
448
|
+
end
|
449
|
+
end
|
450
|
+
throw :event_control, :done
|
451
|
+
else
|
452
|
+
throw :event_control, :access_denied
|
453
|
+
end
|
454
|
+
when "/unmute"
|
455
|
+
if event.user_data
|
456
|
+
telegram_bot_tid_exclusive(event.from.id) do
|
457
|
+
if mute_until = telegram_bot_throttle[event.from.id]["__mute_until"]
|
458
|
+
event.reply "You have been unmuted (prematurely canceled #{mute_until})", disable_notification: true
|
459
|
+
else
|
460
|
+
event.reply "You are not currently muted.", disable_notification: true
|
461
|
+
end
|
462
|
+
end
|
463
|
+
throw :event_control, :done
|
464
|
+
else
|
465
|
+
throw :event_control, :access_denied
|
466
|
+
end
|
467
|
+
when "/scan", "/rescan"
|
468
|
+
if event.user_data
|
469
|
+
if respond_to?(:requeue_runall)
|
470
|
+
event.reply "Triggering all tasks in ReQueue…", disable_notification: true
|
471
|
+
requeue_runall
|
472
|
+
else
|
473
|
+
event.reply "Watchmonkey is not running with ReQueue!", disable_notification: true
|
474
|
+
end
|
475
|
+
throw :event_control, :done
|
476
|
+
else
|
477
|
+
throw :event_control, :access_denied
|
478
|
+
end
|
479
|
+
when "/clearthrottle", "/clearthrottles"
|
480
|
+
if event.user_data
|
481
|
+
telegram_bot_tid_exclusive(event.from.id) do
|
482
|
+
was_muted = telegram_bot_throttle[event.from.id].delete("__mute_until")
|
483
|
+
telegram_bot_throttle.delete(event.from.id)
|
484
|
+
event.reply "Cleared all your throttles!", disable_notification: true
|
485
|
+
if was_muted
|
486
|
+
telegram_bot_throttle[event.from.id]["__mute_until"] = was_muted
|
487
|
+
end
|
488
|
+
end
|
489
|
+
throw :event_control, :done
|
490
|
+
else
|
491
|
+
throw :event_control, :access_denied
|
492
|
+
end
|
493
|
+
when "/status"
|
494
|
+
if event.user_admin?
|
495
|
+
msg = []
|
496
|
+
msg << "<pre>"
|
497
|
+
msg << "========== STATUS =========="
|
498
|
+
msg << " Queue: #{@queue.length}"
|
499
|
+
msg << " Requeue: #{@requeue.length}" if @requeue
|
500
|
+
msg << " Workers: #{@threads.select{|t| t[:working] }.length}/#{@threads.length} working (#{@threads.select(&:alive?).length} alive)"
|
501
|
+
msg << " Threads: #{filtered_threads.length}"
|
502
|
+
msg << " Processed: #{@processed}"
|
503
|
+
msg << " Promises: #{@telegram_bot_egress_promises.length}"
|
504
|
+
msg << "========== //STATUS =========="
|
505
|
+
msg << "</pre>"
|
506
|
+
event.reply msg.join("\n"), parse_mode: "HTML", disable_notification: true
|
507
|
+
throw :event_control, :done
|
508
|
+
else
|
509
|
+
throw :event_control, :access_denied
|
510
|
+
end
|
511
|
+
when "/help"
|
512
|
+
event.reply [].tap{|m|
|
513
|
+
m << "<b>/start</b> Bot API"
|
514
|
+
if event.user_data
|
515
|
+
m << "<b>/mute <time></b> Don't send messages for this long (e.g. <code>/mute 1d 2h 3m 4s</code>)"
|
516
|
+
m << "<b>/unmute</b> Cancel mute"
|
517
|
+
m << "<b>/scan</b> Trigger all queued scans in ReQueue"
|
518
|
+
m << "<b>/clearthrottle</b> Clears your throttled messages"
|
519
|
+
end
|
520
|
+
if event.user_admin?
|
521
|
+
m << "<b>/status</b> Some status information"
|
522
|
+
m << "<b>/wm_shutdown</b> Shutdown watchmonkey process, may respawn if deamonized"
|
523
|
+
end
|
524
|
+
}.join("\n"), parse_mode: "HTML", disable_notification: true
|
525
|
+
throw :event_control, :done
|
526
|
+
when "/wm_shutdown"
|
527
|
+
if event.user_admin?
|
528
|
+
$wm_runtime_exiting = true
|
529
|
+
telegram_bot_msg_admins "ALERT: `#{event.from_descriptive}` invoked shutdown!"
|
530
|
+
throw :event_control, :done
|
531
|
+
else
|
532
|
+
throw :event_control, :access_denied
|
533
|
+
end
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
if ctrl == :access_denied
|
538
|
+
event.reply("You don't have sufficient permissions to execute this command!")
|
539
|
+
elsif ctrl == :bad_request
|
540
|
+
event.reply("Bad Request: Your input is invalid!")
|
541
|
+
end
|
542
|
+
rescue StandardError => ex
|
543
|
+
event.reply("Sorry, encountered an error while processing your request!")
|
544
|
+
|
545
|
+
# notify admins
|
546
|
+
msg = "ERROR: Encountered error while processing a request!\n"
|
547
|
+
msg << "Request: #{ERB::Util.h event.message}\n"
|
548
|
+
msg << "Origin: #{ERB::Util.h event.from.inspect}\n"
|
549
|
+
msg << "<pre>#{ERB::Util.h ex.class}: #{ERB::Util.h ex.message}\n#{ERB::Util.h ex.backtrace.join("\n")}</pre>"
|
550
|
+
telegram_bot_msg_admins(msg, parse_mode: "HTML")
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def telegram_bot_msg_admins msg, msgopts = {}
|
556
|
+
return unless msg.present?
|
557
|
+
opts[:telegram_bot][:notify].each do |tid, level, topts|
|
558
|
+
next unless level.include?(:admin_flag)
|
559
|
+
next if telegram_bot_user_muted?(tid)
|
560
|
+
telegram_bot_send(msgopts.merge(text: msg, chat_id: tid))
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
@@ -1,7 +1,10 @@
|
|
1
1
|
module WatchmonkeyCli
|
2
2
|
class LoopbackConnection
|
3
|
+
attr_reader :opts, :established
|
4
|
+
|
3
5
|
def initialize(id, opts = {}, &initializer)
|
4
6
|
@id = id
|
7
|
+
@established = false
|
5
8
|
@opts = {}.merge(opts)
|
6
9
|
# @mutex = Monitor.new
|
7
10
|
initializer.try(:call, @opts)
|
@@ -15,6 +18,10 @@ module WatchmonkeyCli
|
|
15
18
|
"lo:#{@id}"
|
16
19
|
end
|
17
20
|
|
21
|
+
def established?
|
22
|
+
@established
|
23
|
+
end
|
24
|
+
|
18
25
|
def sync &block
|
19
26
|
# @mutex.synchronize(&block)
|
20
27
|
block.try(:call)
|
@@ -4,6 +4,7 @@ module WatchmonkeyCli
|
|
4
4
|
|
5
5
|
def initialize(id, opts = {}, &initializer)
|
6
6
|
@id = id
|
7
|
+
@established = false
|
7
8
|
|
8
9
|
if opts.is_a?(String)
|
9
10
|
u, h = opts.split("@", 2)
|
@@ -30,6 +31,10 @@ module WatchmonkeyCli
|
|
30
31
|
"ssh:#{@id}"
|
31
32
|
end
|
32
33
|
|
34
|
+
def established?
|
35
|
+
@established
|
36
|
+
end
|
37
|
+
|
33
38
|
def sync &block
|
34
39
|
@mutex.synchronize(&block)
|
35
40
|
end
|
@@ -42,7 +47,12 @@ module WatchmonkeyCli
|
|
42
47
|
end
|
43
48
|
|
44
49
|
def connection
|
45
|
-
sync {
|
50
|
+
sync {
|
51
|
+
@ssh ||= begin
|
52
|
+
@established = true
|
53
|
+
Net::SSH.start(nil, nil, @opts)
|
54
|
+
end
|
55
|
+
}
|
46
56
|
end
|
47
57
|
|
48
58
|
def close!
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: watchmonkey_cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sven Pachnit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -141,6 +141,7 @@ files:
|
|
141
141
|
- lib/watchmonkey_cli/helper.rb
|
142
142
|
- lib/watchmonkey_cli/hooks/platypus.rb
|
143
143
|
- lib/watchmonkey_cli/hooks/requeue.rb
|
144
|
+
- lib/watchmonkey_cli/hooks/telegram_bot.rb
|
144
145
|
- lib/watchmonkey_cli/loopback_connection.rb
|
145
146
|
- lib/watchmonkey_cli/ssh_connection.rb
|
146
147
|
- lib/watchmonkey_cli/version.rb
|