cline 0.3.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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