cline 0.3.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -1
- data/README.md +77 -42
- data/bin/cline +7 -13
- data/cline.gemspec +32 -20
- data/lib/cline.rb +60 -38
- data/lib/cline/client.rb +28 -0
- data/lib/cline/collectors.rb +7 -3
- data/lib/cline/collectors/feed.rb +28 -22
- data/lib/cline/command.rb +93 -17
- data/lib/cline/configure.rb +4 -2
- data/lib/cline/monkey.rb +7 -0
- data/lib/cline/notification.rb +12 -12
- data/lib/cline/notify_io.rb +24 -0
- data/lib/cline/scheduled_job.rb +26 -0
- data/lib/cline/server.rb +102 -0
- data/lib/cline/version.rb +1 -1
- data/spec/lib/notification_spec.rb +7 -15
- data/spec/lib/server_spec.rb +22 -0
- data/spec/spec_helper.rb +3 -3
- data/spec/support/example_group_helper.rb +8 -0
- metadata +65 -93
- data/lib/cline/out_streams.rb +0 -34
data/lib/cline/collectors.rb
CHANGED
@@ -4,6 +4,8 @@ module Cline::Collectors
|
|
4
4
|
class Base
|
5
5
|
class << self
|
6
6
|
def create_or_pass(message, notified_at)
|
7
|
+
return unless notified_at
|
8
|
+
|
7
9
|
message = message.encode(Encoding::UTF_8)
|
8
10
|
notified_at = parse_time_string_if_needed(notified_at)
|
9
11
|
|
@@ -13,7 +15,10 @@ module Cline::Collectors
|
|
13
15
|
create!(message: message, notified_at: notified_at) unless find_by_message_and_notified_at(message, notified_at)
|
14
16
|
end
|
15
17
|
rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordInvalid => e
|
16
|
-
|
18
|
+
error = [e.class, e.message].join(' ')
|
19
|
+
|
20
|
+
puts error
|
21
|
+
Cline.logger.error error
|
17
22
|
end
|
18
23
|
|
19
24
|
private
|
@@ -27,8 +32,7 @@ module Cline::Collectors
|
|
27
32
|
end
|
28
33
|
|
29
34
|
def oldest_notification
|
30
|
-
@oldest_notification ||=
|
31
|
-
Cline::Notification.order(:notified_at).limit(1).first
|
35
|
+
@oldest_notification ||= Cline::Notification.order(:notified_at).limit(1).first
|
32
36
|
end
|
33
37
|
|
34
38
|
def reset_oldest_notification
|
@@ -1,11 +1,14 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
+
require 'pathname'
|
4
|
+
|
3
5
|
module Cline::Collectors
|
4
6
|
class Feed < Base
|
5
7
|
class << self
|
6
8
|
def collect
|
7
9
|
new(opml_path.read).entries.each do |entry|
|
8
10
|
message = Cline::Notification.normalize_message("#{entry.title} #{entry.url}")
|
11
|
+
|
9
12
|
create_or_pass message, entry.published
|
10
13
|
end
|
11
14
|
end
|
@@ -17,47 +20,50 @@ module Cline::Collectors
|
|
17
20
|
|
18
21
|
def initialize(opml_str)
|
19
22
|
require 'rexml/document'
|
23
|
+
require 'active_support/deprecation'
|
20
24
|
require 'feedzirra'
|
21
25
|
|
22
26
|
@opml = REXML::Document.new(opml_str)
|
23
|
-
@feeds = parse_opml(@opml.elements['opml/body'])
|
24
27
|
end
|
25
28
|
|
26
29
|
def entries
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
rescue
|
38
|
-
puts $!.class, $!.message
|
39
|
-
ensure
|
40
|
-
Thread.pass
|
41
|
-
end
|
42
|
-
}
|
43
|
-
}.map(&:join)
|
30
|
+
feed_urls = parse_opml(@opml.elements['opml/body'])
|
31
|
+
entries = []
|
32
|
+
|
33
|
+
3.times.map { Thread.fork {
|
34
|
+
while url = feed_urls.pop
|
35
|
+
entries += fetch_entries(url)
|
36
|
+
end
|
37
|
+
|
38
|
+
Thread.pass
|
39
|
+
} }.map(&:join)
|
44
40
|
|
45
41
|
entries
|
46
42
|
end
|
47
43
|
|
44
|
+
private
|
45
|
+
|
48
46
|
def parse_opml(opml_node)
|
49
|
-
|
47
|
+
urls = []
|
50
48
|
|
51
49
|
opml_node.elements.each('outline') do |el|
|
52
50
|
unless el.elements.size.zero?
|
53
|
-
|
51
|
+
urls += parse_opml(el)
|
54
52
|
else
|
55
53
|
url = el.attributes['xmlUrl']
|
56
|
-
|
54
|
+
urls << url if url
|
57
55
|
end
|
58
56
|
end
|
59
57
|
|
60
|
-
|
58
|
+
urls
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch_entries(feed_url)
|
62
|
+
feed = Feedzirra::Feed.fetch_and_parse(feed_url)
|
63
|
+
|
64
|
+
feed.is_a?(Feedzirra::FeedUtilities) ? feed.entries : []
|
65
|
+
rescue => e
|
66
|
+
Cline.logger.error [e.class, e.message].join(' ')
|
61
67
|
end
|
62
68
|
end
|
63
69
|
end
|
data/lib/cline/command.rb
CHANGED
@@ -1,12 +1,35 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'thor'
|
4
4
|
|
5
5
|
module Cline
|
6
6
|
class Command < Thor
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
class << self
|
8
|
+
def start(args = ARGV, *)
|
9
|
+
return super unless server_available?(args)
|
10
|
+
|
11
|
+
Cline::Client.start args
|
12
|
+
rescue => e
|
13
|
+
Cline.logger.fatal :cline do
|
14
|
+
%(#{e.class} #{e.message}\n#{e.backtrace.join($/)})
|
15
|
+
end
|
16
|
+
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def server_available?(args)
|
23
|
+
return false if client_command?(args)
|
24
|
+
return false unless Cline::Server.running?
|
25
|
+
return false unless Cline::Server.client_process?
|
26
|
+
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def client_command?(args)
|
31
|
+
%w(collect open).include? args.first
|
32
|
+
end
|
10
33
|
end
|
11
34
|
|
12
35
|
map '-s' => :show,
|
@@ -15,20 +38,22 @@ module Cline
|
|
15
38
|
'-st' => :status,
|
16
39
|
'-c' => :collect,
|
17
40
|
'-i' => :init,
|
41
|
+
'-d' => :server,
|
18
42
|
'-v' => :version
|
19
43
|
|
20
44
|
desc :show, 'Show a latest message'
|
21
45
|
method_options offset: :integer
|
22
46
|
def show(offset = options[:offset] || 0)
|
23
|
-
Notification.display
|
47
|
+
notify Notification.display!(offset)
|
24
48
|
end
|
25
49
|
|
26
50
|
desc :tick, 'Rotate message'
|
27
51
|
method_options offset: :integer, interval: :integer
|
28
|
-
def tick(
|
52
|
+
def tick(interval = options[:interval] || 5, offset = options[:offset] || 0)
|
29
53
|
loop do
|
30
54
|
show offset
|
31
|
-
|
55
|
+
|
56
|
+
sleep Integer(interval)
|
32
57
|
end
|
33
58
|
end
|
34
59
|
|
@@ -36,13 +61,15 @@ module Cline
|
|
36
61
|
method_options query: :string
|
37
62
|
def search(keyword = optoins[:query])
|
38
63
|
Notification.by_keyword(keyword).each do |notification|
|
39
|
-
|
64
|
+
puts notification.display_message
|
40
65
|
end
|
41
66
|
end
|
42
67
|
|
43
68
|
desc :open, 'Open the URL in the message if exists'
|
44
69
|
method_options hint: :string
|
45
70
|
def open(alias_string = options[:hint])
|
71
|
+
require 'launchy'
|
72
|
+
|
46
73
|
notification = Notification.by_alias_string(alias_string).last
|
47
74
|
|
48
75
|
if notification && url = notification.detect_url
|
@@ -54,19 +81,27 @@ module Cline
|
|
54
81
|
|
55
82
|
desc :status, 'Show status'
|
56
83
|
def status
|
57
|
-
|
58
|
-
|
84
|
+
puts "displayed : #{Notification.displayed.count}"
|
85
|
+
puts "total : #{Notification.count}"
|
86
|
+
|
87
|
+
server :status
|
59
88
|
end
|
60
89
|
|
61
90
|
desc :collect, 'Collect sources'
|
62
91
|
def collect
|
63
|
-
|
92
|
+
pid = Process.fork {
|
93
|
+
Cline.collectors.each &:collect
|
64
94
|
|
65
|
-
|
95
|
+
Notification.clean(Cline.notifications_limit) if Cline.notifications_limit
|
96
|
+
}
|
97
|
+
|
98
|
+
Process.waitpid pid
|
66
99
|
end
|
67
100
|
|
68
101
|
desc :init, 'Init database'
|
69
102
|
def init
|
103
|
+
Cline.establish_database_connection
|
104
|
+
|
70
105
|
ActiveRecord::Base.connection.create_table(:notifications) do |t|
|
71
106
|
t.text :message, null: false, default: ''
|
72
107
|
t.integer :display_count, null: false, default: 0
|
@@ -78,19 +113,60 @@ module Cline
|
|
78
113
|
method_options limit: :integer
|
79
114
|
def recent(limit = options[:limit] || 1)
|
80
115
|
Notification.recent_notified.limit(limit).each do |notification|
|
81
|
-
|
116
|
+
puts notification.display_message
|
82
117
|
end
|
83
118
|
end
|
84
119
|
|
85
|
-
desc :version, 'Show version
|
120
|
+
desc :version, 'Show version'
|
86
121
|
def version
|
87
|
-
|
122
|
+
puts "cline version #{Cline::VERSION}"
|
123
|
+
end
|
124
|
+
|
125
|
+
desc :server, 'start or stop server'
|
126
|
+
def server(command = :start)
|
127
|
+
case command.intern
|
128
|
+
when :start
|
129
|
+
puts 'starting cline server'
|
130
|
+
|
131
|
+
Server.start
|
132
|
+
when :stop
|
133
|
+
puts 'stopping cline server'
|
134
|
+
|
135
|
+
Server.stop
|
136
|
+
when :reload
|
137
|
+
puts 'reloading configuration'
|
138
|
+
|
139
|
+
Cline.tap do |c|
|
140
|
+
c.load_config_if_exists
|
141
|
+
c.load_default_config
|
142
|
+
end
|
143
|
+
when :status
|
144
|
+
if Server.running?
|
145
|
+
puts "Socket file exists"
|
146
|
+
puts "But server isn't responding" if Server.client_process?
|
147
|
+
else
|
148
|
+
puts "Server isn't running"
|
149
|
+
end
|
150
|
+
else
|
151
|
+
puts 'Usage: cline server (start|stop)'
|
152
|
+
end
|
88
153
|
end
|
89
154
|
|
90
155
|
private
|
91
156
|
|
92
|
-
def
|
93
|
-
|
157
|
+
def notify(str)
|
158
|
+
Cline.notify_io.tap do |io|
|
159
|
+
io.puts str
|
160
|
+
io.flush if io.respond_to?(:flush)
|
161
|
+
end
|
162
|
+
|
163
|
+
Thread.current[:stdout].tap do |stdout|
|
164
|
+
stdout.puts str if stdout
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def puts(str)
|
169
|
+
Cline.stdout.puts str
|
94
170
|
end
|
95
171
|
end
|
96
172
|
end
|
data/lib/cline/configure.rb
CHANGED
@@ -5,13 +5,15 @@ require 'forwardable'
|
|
5
5
|
module Cline
|
6
6
|
def self.configure(&block)
|
7
7
|
configure = Configure.new
|
8
|
-
block ? block
|
8
|
+
block ? block[configure] : configure
|
9
9
|
end
|
10
10
|
|
11
11
|
class Configure
|
12
12
|
extend Forwardable
|
13
13
|
|
14
|
-
def_delegators Cline,
|
14
|
+
def_delegators Cline,
|
15
|
+
:logger, :logger=, :notify_io, :notify_io=, :notifications_limit, :notifications_limit=, :collectors, :collectors=, :jobs, :jobs=,
|
16
|
+
:pool_size=, :out_stream= # obsoletes
|
15
17
|
|
16
18
|
def notification
|
17
19
|
Cline::Notification
|
data/lib/cline/monkey.rb
ADDED
data/lib/cline/notification.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
3
|
require 'uri'
|
4
|
+
require 'sqlite3'
|
5
|
+
require 'active_record'
|
4
6
|
|
5
7
|
module Cline
|
8
|
+
establish_database_connection
|
9
|
+
|
6
10
|
class Notification < ActiveRecord::Base
|
7
11
|
validate :notified_at, presence: true
|
8
12
|
validate :message, presence: true, uniqueness: true
|
@@ -37,30 +41,26 @@ module Cline
|
|
37
41
|
end
|
38
42
|
|
39
43
|
class << self
|
40
|
-
def display(offset = 0)
|
41
|
-
earliest(1, offset).first.display
|
44
|
+
def display!(offset = 0)
|
45
|
+
earliest(1, offset).first.display!
|
42
46
|
end
|
43
47
|
|
44
48
|
def normalize_message(m)
|
45
49
|
m.gsub(/[\r\n]/, '')
|
46
50
|
end
|
47
51
|
|
48
|
-
def clean(
|
52
|
+
def clean(limit)
|
49
53
|
order('notified_at DESC').
|
50
54
|
order(:display_count).
|
51
|
-
offset(
|
55
|
+
offset(limit).
|
52
56
|
destroy_all
|
53
57
|
end
|
54
58
|
end
|
55
59
|
|
56
|
-
def display
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
out.flush if out.respond_to?(:flush)
|
61
|
-
end
|
62
|
-
|
63
|
-
increment! :display_count
|
60
|
+
def display!
|
61
|
+
display_message.tap {
|
62
|
+
increment! :display_count
|
63
|
+
}
|
64
64
|
end
|
65
65
|
|
66
66
|
def display_message
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
autoload :Notify, 'notify'
|
4
|
+
|
5
|
+
module Cline
|
6
|
+
module NotifyIO
|
7
|
+
class WithNotify
|
8
|
+
def initialize(io = $stdout)
|
9
|
+
@io = io
|
10
|
+
end
|
11
|
+
|
12
|
+
def puts(str)
|
13
|
+
@io.puts str
|
14
|
+
|
15
|
+
Notify.notify 'cline', str
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
WithGrowl = WithNotify
|
20
|
+
end
|
21
|
+
|
22
|
+
OutStreams = NotifyIO
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Cline
|
2
|
+
class ScheduledJob
|
3
|
+
INTERVAL = 60
|
4
|
+
|
5
|
+
def initialize(trigger, &block)
|
6
|
+
@trigger = trigger
|
7
|
+
@job = block
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
loop do
|
12
|
+
invoke_if_needed
|
13
|
+
|
14
|
+
sleep INTERVAL
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def invoke_if_needed
|
21
|
+
Thread.fork { Command.new.instance_eval(&@job) } if @trigger.()
|
22
|
+
rescue Exception => e
|
23
|
+
Cline.logger.error [e.class, e.message].join(' ')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/cline/server.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'socket'
|
5
|
+
require 'json'
|
6
|
+
require 'cline/monkey'
|
7
|
+
|
8
|
+
module Cline
|
9
|
+
class Server
|
10
|
+
class << self
|
11
|
+
def start
|
12
|
+
raise %(Socket file #{socket_file} already exists.) if running?
|
13
|
+
|
14
|
+
Process.daemon
|
15
|
+
|
16
|
+
pid_file.open 'w' do |pid|
|
17
|
+
pid.write Process.pid
|
18
|
+
end
|
19
|
+
|
20
|
+
Signal.trap(:KILL) { Server.clean }
|
21
|
+
|
22
|
+
new(socket_file).run
|
23
|
+
end
|
24
|
+
|
25
|
+
def stop
|
26
|
+
raise %(Server isn't running) unless running?
|
27
|
+
|
28
|
+
Process.kill :TERM, pid
|
29
|
+
end
|
30
|
+
|
31
|
+
def clean
|
32
|
+
File.unlink pid_file
|
33
|
+
File.unlink socket_file
|
34
|
+
end
|
35
|
+
|
36
|
+
def running?
|
37
|
+
socket_file.exist?
|
38
|
+
end
|
39
|
+
|
40
|
+
def client_process?
|
41
|
+
Process.pid != pid
|
42
|
+
end
|
43
|
+
|
44
|
+
def pid
|
45
|
+
Integer(pid_file.read)
|
46
|
+
end
|
47
|
+
|
48
|
+
def pid_file
|
49
|
+
Pathname.new(Cline.cline_dir).join('cline.pid')
|
50
|
+
end
|
51
|
+
|
52
|
+
def socket_file
|
53
|
+
Pathname.new(Cline.cline_dir).join('cline.sock')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(socket_file)
|
58
|
+
@socket_file = socket_file
|
59
|
+
@server = UNIXServer.new(@socket_file.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
def run
|
63
|
+
invoke_jobs
|
64
|
+
|
65
|
+
loop do
|
66
|
+
Thread.fork @server.accept do |socket|
|
67
|
+
request = socket.recv(120)
|
68
|
+
|
69
|
+
process socket, JSON.parse(request)
|
70
|
+
|
71
|
+
socket.close
|
72
|
+
|
73
|
+
GC.start
|
74
|
+
end
|
75
|
+
end
|
76
|
+
ensure
|
77
|
+
Server.clean
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def process(io, args)
|
83
|
+
replace_current_io io
|
84
|
+
|
85
|
+
Command.start args
|
86
|
+
rescue Exception => e
|
87
|
+
warn %(#{e.class} #{e.message}\n#{e.backtrace.join($/)})
|
88
|
+
end
|
89
|
+
|
90
|
+
def replace_current_io(io)
|
91
|
+
io.sync = true
|
92
|
+
|
93
|
+
Thread.current[:stdout] = Thread.current[:stderr] = io
|
94
|
+
end
|
95
|
+
|
96
|
+
def invoke_jobs
|
97
|
+
Cline.jobs.each do |job|
|
98
|
+
Thread.fork(job, &:run)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|