cline 0.3.2 → 1.0.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.
- 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
|