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.
@@ -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
- puts e.class, e.message
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
- entries = []
28
-
29
- @feeds.map { |feed_url|
30
- Thread.fork {
31
- begin
32
- feed = Feedzirra::Feed.fetch_and_parse(feed_url)
33
-
34
- if feed.is_a?(Feedzirra::FeedUtilities)
35
- feed.entries.each { |entry| entries << entry }
36
- end
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
- feeds = []
47
+ urls = []
50
48
 
51
49
  opml_node.elements.each('outline') do |el|
52
50
  unless el.elements.size.zero?
53
- feeds += parse_opml(el)
51
+ urls += parse_opml(el)
54
52
  else
55
53
  url = el.attributes['xmlUrl']
56
- feeds << url if url
54
+ urls << url if url
57
55
  end
58
56
  end
59
57
 
60
- feeds
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
@@ -1,12 +1,35 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'launchy'
3
+ require 'thor'
4
4
 
5
5
  module Cline
6
6
  class Command < Thor
7
- def self.start(*)
8
- Cline.boot
9
- super
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 offset
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(offset = options[:offset] || 0, interval = options[:interval] || 60)
52
+ def tick(interval = options[:interval] || 5, offset = options[:offset] || 0)
29
53
  loop do
30
54
  show offset
31
- sleep interval.to_i
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
- say notification.display_message
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
- say "displayed : #{Notification.displayed.count}", :green
58
- say "total : #{Notification.count}", :cyan
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
- Cline.collectors.each &:collect
92
+ pid = Process.fork {
93
+ Cline.collectors.each &:collect
64
94
 
65
- clean_obsoletes
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
- say notification.display_message
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
- say "cline version #{Cline::VERSION}"
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 clean_obsoletes
93
- Notification.clean(Cline.pool_size) if Cline.pool_size
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
@@ -5,13 +5,15 @@ require 'forwardable'
5
5
  module Cline
6
6
  def self.configure(&block)
7
7
  configure = Configure.new
8
- block ? block.(configure) : configure
8
+ block ? block[configure] : configure
9
9
  end
10
10
 
11
11
  class Configure
12
12
  extend Forwardable
13
13
 
14
- def_delegators Cline, :pool_size=, :out_stream=
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
@@ -0,0 +1,7 @@
1
+ require 'thor'
2
+
3
+ class Thor::Shell::Basic
4
+ extend Forwardable
5
+
6
+ def_delegators Cline, :stdout, :stderr
7
+ end
@@ -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(pool_size)
52
+ def clean(limit)
49
53
  order('notified_at DESC').
50
54
  order(:display_count).
51
- offset(pool_size).
55
+ offset(limit).
52
56
  destroy_all
53
57
  end
54
58
  end
55
59
 
56
- def display
57
- Cline.out_stream.tap do |out|
58
- out.puts display_message
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
@@ -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