pcelka 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.
- checksums.yaml +7 -0
- data/app.rb +95 -0
- data/assets/css/styles.css +209 -0
- data/assets/js/app.js +16 -0
- data/assets/js/hotkey.js +27 -0
- data/bin/dev +9 -0
- data/bin/dev-puma +8 -0
- data/bin/pcelka +16 -0
- data/bin/pcelka-console +16 -0
- data/config/datastar.rb +16 -0
- data/config/falcon_init.rb +14 -0
- data/config.ru +30 -0
- data/db/migrations/001_create_logs.rb +11 -0
- data/db.rb +3 -0
- data/examples/procfile/Procfile +7 -0
- data/examples/procfile/Procfile.without_colon +2 -0
- data/examples/procfile/error +7 -0
- data/examples/procfile/log/neverdie.log +4 -0
- data/examples/procfile/oneshot +3 -0
- data/examples/procfile/single_call +10 -0
- data/examples/procfile/spawnee +14 -0
- data/examples/procfile/spawner +7 -0
- data/examples/procfile/ticker +14 -0
- data/examples/procfile/utf8 +11 -0
- data/i18n/en.yml +32 -0
- data/i18n/ru.yml +32 -0
- data/lib/pcelka/async_client.rb +51 -0
- data/lib/pcelka/logs_collector.rb +37 -0
- data/lib/pcelka/program.rb +79 -0
- data/lib/pcelka/server/commandable.rb +57 -0
- data/lib/pcelka/server/controllable.rb +45 -0
- data/lib/pcelka/server/notifiable.rb +25 -0
- data/lib/pcelka/server/reportable.rb +61 -0
- data/lib/pcelka/server/writing.rb +28 -0
- data/lib/pcelka/server.rb +66 -0
- data/lib/pcelka/spec/procfile.rb +46 -0
- data/lib/pcelka/spec/program_spec.rb +3 -0
- data/lib/pcelka/spec.rb +11 -0
- data/lib/pcelka/writer/console_writer.rb +60 -0
- data/lib/pcelka/writer/generic_writer.rb +16 -0
- data/lib/pcelka/writer.rb +25 -0
- data/lib/pcelka.rb +5 -0
- data/models.rb +21 -0
- data/pcelka.rb +22 -0
- data/public/android-chrome-192x192.png +0 -0
- data/public/android-chrome-512x512.png +0 -0
- data/public/apple-touch-icon.png +0 -0
- data/public/favicon-16x16.png +0 -0
- data/public/favicon-32x32.png +0 -0
- data/public/favicon.ico +0 -0
- data/public/site.webmanifest +1 -0
- data/public/sound/bee.ogg +0 -0
- data/public/vendor/datastar.js +9 -0
- data/public/vendor/datastar.js.map +7 -0
- data/views/home/action.erb +10 -0
- data/views/home/report.erb +36 -0
- data/views/home.erb +18 -0
- data/views/layout.erb +20 -0
- data/views/logs/log_line.erb +13 -0
- data/views/logs.erb +16 -0
- data/views/nav/arrow.erb +21 -0
- metadata +282 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "async"
|
|
3
|
+
require "pcelka/server"
|
|
4
|
+
require "pcelka/spec"
|
|
5
|
+
|
|
6
|
+
# Public interface to the Server. Server runs in the background,
|
|
7
|
+
# and Client communicates with it.
|
|
8
|
+
#
|
|
9
|
+
# Example usage:
|
|
10
|
+
#
|
|
11
|
+
# pcelka = Pcelka::AsyncClient.from_procfile procfile
|
|
12
|
+
# pcelka.server.writer.add_console_writer
|
|
13
|
+
# th = Thread.new { pcelka.server.run }
|
|
14
|
+
# pcelka << :start_all
|
|
15
|
+
# th.join
|
|
16
|
+
#
|
|
17
|
+
module Pcelka
|
|
18
|
+
class AsyncClient
|
|
19
|
+
def self.from_procfile(procfile)
|
|
20
|
+
spec = Spec.from_procfile(procfile)
|
|
21
|
+
client_sink = Async::Queue.new
|
|
22
|
+
mailbox = Async::Queue.new
|
|
23
|
+
programs_status_changed_cond = Async::Condition.new
|
|
24
|
+
read, write = IO.pipe
|
|
25
|
+
server = Server.new spec:, client_sink:, wakeup_io: read, mailbox:,
|
|
26
|
+
programs_status_changed_cond:;
|
|
27
|
+
|
|
28
|
+
new server:, client_sink:, wakeup_io: write, server_mailbox: mailbox,
|
|
29
|
+
programs_status_changed_cond:
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :server
|
|
33
|
+
|
|
34
|
+
def initialize(server:, client_sink:, wakeup_io:, server_mailbox:, programs_status_changed_cond:)
|
|
35
|
+
@server, @client_sink, @wakeup_io, @server_mailbox, @programs_status_changed_cond =
|
|
36
|
+
server, client_sink, wakeup_io, server_mailbox, programs_status_changed_cond
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def <<(cmd)
|
|
40
|
+
@server_mailbox << cmd
|
|
41
|
+
@wakeup_io.putc Server::WAKEUP_BYTE
|
|
42
|
+
@client_sink.pop
|
|
43
|
+
rescue Errno::EPIPE
|
|
44
|
+
raise "Failed to write to @runner's pipe"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def programs_status_changed?
|
|
48
|
+
@programs_status_changed_cond.wait
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../../db"
|
|
3
|
+
|
|
4
|
+
module Pcelka
|
|
5
|
+
class LogsCollector
|
|
6
|
+
def initialize(logs_mailbox)
|
|
7
|
+
@logs_ready = Async::Condition.new
|
|
8
|
+
@logs_mailbox = logs_mailbox
|
|
9
|
+
@collect_thread = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def collect
|
|
13
|
+
@collect_thread = Thread.new do
|
|
14
|
+
while l = @logs_mailbox.pop
|
|
15
|
+
new_id = DB[:logs].insert(app: l[0], message: l[1], is_error: l[2])
|
|
16
|
+
@logs_ready.signal new_id
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def retrieve_new_logs(last_id = 0)
|
|
22
|
+
logs = DB[:logs].order(:id).where{id > last_id}.all
|
|
23
|
+
unless logs.empty?
|
|
24
|
+
last_id = logs.last[:id]
|
|
25
|
+
logs.each{ yield it }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
while @logs_ready.wait
|
|
29
|
+
logs = DB[:logs].order(:id).where{id > last_id}.all
|
|
30
|
+
unless logs.empty?
|
|
31
|
+
last_id = logs.last[:id]
|
|
32
|
+
logs.each{ yield it }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
# An executable program with common lifecycle methods. It is expected to
|
|
5
|
+
# print something to standard output and standard error, and this is
|
|
6
|
+
# captured for output by the Server.
|
|
7
|
+
module Pcelka
|
|
8
|
+
class Program
|
|
9
|
+
attr_reader :id
|
|
10
|
+
|
|
11
|
+
def initialize(id:, cmd:, stdin:, stdout:, stderr:, thread:, cwd:)
|
|
12
|
+
@id, @cmd, @stdin, @stdout, @stderr, @thread, @cwd =
|
|
13
|
+
id, cmd, stdin, stdout, stderr, thread, cwd
|
|
14
|
+
@stdin.close
|
|
15
|
+
@stdout_ready = @stderr_ready = false
|
|
16
|
+
@stopping = false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.start_from_spec(spec)
|
|
20
|
+
Dir.chdir(spec.cwd) do
|
|
21
|
+
stdin, stdout, stderr, thread = Open3.popen3(spec.cmd)
|
|
22
|
+
new id: spec.id, cmd: spec.cmd, stdin:, stdout:, stderr:, thread:,
|
|
23
|
+
cwd: spec.cwd
|
|
24
|
+
end
|
|
25
|
+
rescue Errno::ENOENT
|
|
26
|
+
# From Procfile, we get all commands wrapped into `sh` so this error
|
|
27
|
+
# could only happen when `sh` is not found on the system. Could happen
|
|
28
|
+
# more often for other formats.
|
|
29
|
+
raise "Unknown command: '#{spec.cmd}'"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def alive? = !dead?
|
|
33
|
+
def dead? = !@thread.alive?
|
|
34
|
+
def stopping? = @stopping
|
|
35
|
+
|
|
36
|
+
def status
|
|
37
|
+
if alive? then :alive
|
|
38
|
+
elsif dead? then :dead
|
|
39
|
+
elsif stopping? then :stopping
|
|
40
|
+
else :unknown
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_stdout
|
|
45
|
+
if @stdout_ready
|
|
46
|
+
@stdout_ready = false
|
|
47
|
+
@stdout.gets
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_stderr
|
|
52
|
+
if @stderr_ready
|
|
53
|
+
@stderr_ready = false
|
|
54
|
+
@stderr.gets
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ios = [@stdout, @stderr]
|
|
59
|
+
|
|
60
|
+
def mark_ready(io)
|
|
61
|
+
if io == @stdout
|
|
62
|
+
@stdout_ready = true
|
|
63
|
+
elsif io == @stderr
|
|
64
|
+
@stderr_ready = true
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def stop
|
|
69
|
+
if alive? && !@stopping
|
|
70
|
+
@stopping = true
|
|
71
|
+
@stdout.flush
|
|
72
|
+
@stderr.flush
|
|
73
|
+
Process.kill "SIGTERM", @thread.pid
|
|
74
|
+
@stopping = false
|
|
75
|
+
end
|
|
76
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pcelka/server/controllable"
|
|
4
|
+
require "pcelka/server/reportable"
|
|
5
|
+
|
|
6
|
+
# Allow receiving commands and do corresponding actions.
|
|
7
|
+
module Pcelka
|
|
8
|
+
class Server
|
|
9
|
+
module Commandable
|
|
10
|
+
include Reportable
|
|
11
|
+
include Controllable
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
def remove_wakeup_io(ios)
|
|
15
|
+
if ios.reject!{ it == @wakeup_io }
|
|
16
|
+
byte = @wakeup_io.readbyte
|
|
17
|
+
raise "Unexpected byte: #{byte}" unless byte == WAKEUP_BYTE
|
|
18
|
+
@is_pending_cmd = true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def process_cmd
|
|
23
|
+
return unless @is_pending_cmd
|
|
24
|
+
@is_pending_cmd = false
|
|
25
|
+
|
|
26
|
+
response =
|
|
27
|
+
case @mailbox.pop
|
|
28
|
+
in :start_all
|
|
29
|
+
@writer.write app: "_all", message: "_starting_all"
|
|
30
|
+
start_all
|
|
31
|
+
in :stop_all
|
|
32
|
+
@writer.write app: "_all", message: "_stopping_all"
|
|
33
|
+
stop_all
|
|
34
|
+
in [:start, id]
|
|
35
|
+
@writer.write app: id, message: "_starting"
|
|
36
|
+
start(id)
|
|
37
|
+
in [:stop, id]
|
|
38
|
+
@writer.write app: id, message: "_stopping"
|
|
39
|
+
stop(id)
|
|
40
|
+
in [:restart, id]
|
|
41
|
+
@writer.write app: id, message: "_restarting"
|
|
42
|
+
restart(id)
|
|
43
|
+
in :report
|
|
44
|
+
report
|
|
45
|
+
in :shutdown
|
|
46
|
+
warn "Pcelka shutting down..."
|
|
47
|
+
stop_all
|
|
48
|
+
@should_stop = true
|
|
49
|
+
else
|
|
50
|
+
:unknown
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@client_sink << response
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "pcelka/program"
|
|
3
|
+
|
|
4
|
+
# Controls the programs in the spec: start, stop, restart, etc.
|
|
5
|
+
module Pcelka
|
|
6
|
+
class Server
|
|
7
|
+
module Controllable
|
|
8
|
+
def start(id)
|
|
9
|
+
unless @running_programs.find{ it.id == id }
|
|
10
|
+
program = Program.start_from_spec(@spec[id])
|
|
11
|
+
@running_programs << program
|
|
12
|
+
add_to_started_programs program
|
|
13
|
+
id
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start_all
|
|
18
|
+
@spec.filter_map{|id, _| start(id) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stop(id)
|
|
22
|
+
if program = @running_programs.find{ it.id == id }
|
|
23
|
+
@running_programs.reject!{ it.id == id }
|
|
24
|
+
program.stop
|
|
25
|
+
id
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop_all
|
|
30
|
+
@running_programs.filter_map{ stop(it.id) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def restart(id)
|
|
34
|
+
stop(id) && start(id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
def add_to_started_programs(program)
|
|
39
|
+
return unless defined?(@started_programs)
|
|
40
|
+
|
|
41
|
+
@started_programs.delete_if{ it.id == program.id } << program
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Pcelka
|
|
4
|
+
class Server
|
|
5
|
+
module Notifiable
|
|
6
|
+
private
|
|
7
|
+
def init_notifiable(programs_status_changed_cond:)
|
|
8
|
+
@programs_status_changed_cond = programs_status_changed_cond
|
|
9
|
+
@last_programs_statuses = nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def notify_programs_status_changed
|
|
13
|
+
new_statuses = gather_statuses
|
|
14
|
+
if @last_programs_statuses.nil? || @last_programs_statuses != new_statuses
|
|
15
|
+
@programs_status_changed_cond.signal true
|
|
16
|
+
end
|
|
17
|
+
@last_programs_statuses = new_statuses
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def gather_statuses
|
|
21
|
+
@started_programs.map{[it.id, it.status]}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Provides a "state" of the server together with possible actions.
|
|
4
|
+
module Pcelka
|
|
5
|
+
class Server
|
|
6
|
+
module Reportable
|
|
7
|
+
Report = Data.define(:items, :allowed_actions, :possible_actions)
|
|
8
|
+
ReportItem = Data.define(:id, :cmd, :status, :allowed_actions,
|
|
9
|
+
:possible_actions)
|
|
10
|
+
|
|
11
|
+
def report
|
|
12
|
+
items =
|
|
13
|
+
@spec.keys.sort.map do |id|
|
|
14
|
+
program_spec = @spec[id]
|
|
15
|
+
program = @started_programs.find{ it.id == id }
|
|
16
|
+
status = program_status(program)
|
|
17
|
+
ReportItem.new id:, cmd: program_spec.cmd, status:,
|
|
18
|
+
allowed_actions: allowed_program_actions(status),
|
|
19
|
+
possible_actions: PROGRAM_POSSIBLE_ACTIONS
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Report.new items:, allowed_actions: allowed_server_actions(items),
|
|
23
|
+
possible_actions: SERVER_POSSIBLE_ACTIONS
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
def program_status(program)
|
|
28
|
+
program&.status || :not_started
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
SERVER_POSSIBLE_ACTIONS = %i[start_all stop_all].freeze
|
|
32
|
+
PROGRAM_POSSIBLE_ACTIONS = %i[start restart stop].freeze
|
|
33
|
+
NOT_STARTED_ACTIONS = %i[start].freeze
|
|
34
|
+
ALIVE_ACTIONS = %i[stop restart].freeze
|
|
35
|
+
DEAD_ACTIONS = %i[start].freeze
|
|
36
|
+
STOPPING_ACTIONS = [].freeze
|
|
37
|
+
UNKNOWN_ACTIONS = [].freeze
|
|
38
|
+
private_constant :NOT_STARTED_ACTIONS, :ALIVE_ACTIONS, :DEAD_ACTIONS,
|
|
39
|
+
:STOPPING_ACTIONS, :UNKNOWN_ACTIONS, :PROGRAM_POSSIBLE_ACTIONS,
|
|
40
|
+
:SERVER_POSSIBLE_ACTIONS
|
|
41
|
+
|
|
42
|
+
def allowed_program_actions(status)
|
|
43
|
+
case status
|
|
44
|
+
when :not_started then NOT_STARTED_ACTIONS
|
|
45
|
+
when :alive then ALIVE_ACTIONS
|
|
46
|
+
when :dead then DEAD_ACTIONS
|
|
47
|
+
when :stopping then STOPPING_ACTIONS
|
|
48
|
+
when :unknown then UNKNOWN_ACTIONS
|
|
49
|
+
else raise "Unreachable"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def allowed_server_actions(items)
|
|
54
|
+
allowed_actions = []
|
|
55
|
+
allowed_actions << :start_all if items.any?{ it.allowed_actions.include?(:start) }
|
|
56
|
+
allowed_actions << :stop_all if items.any?{ it.allowed_actions.include?(:stop) }
|
|
57
|
+
allowed_actions
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "pcelka/writer"
|
|
3
|
+
|
|
4
|
+
# Use the Writer class.
|
|
5
|
+
module Pcelka
|
|
6
|
+
class Server
|
|
7
|
+
module Writing
|
|
8
|
+
attr_reader :writer
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
def init_writer
|
|
12
|
+
@writer = Writer.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def write_programs_output(programs)
|
|
16
|
+
programs.each do |program|
|
|
17
|
+
if out = program.read_stdout
|
|
18
|
+
@writer.write app: program.id, message: out
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if err = program.read_stderr
|
|
22
|
+
@writer.write app: program.id, message: err, is_error: true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pcelka/server/writing"
|
|
4
|
+
require "pcelka/server/commandable"
|
|
5
|
+
require "pcelka/server/notifiable"
|
|
6
|
+
|
|
7
|
+
# Runs several command line programs.
|
|
8
|
+
module Pcelka
|
|
9
|
+
class Server
|
|
10
|
+
include Writing, Commandable, Notifiable
|
|
11
|
+
|
|
12
|
+
WAKEUP_BYTE = 1
|
|
13
|
+
|
|
14
|
+
# @param spec [Spec]
|
|
15
|
+
# @param client_sink [Object#<<]
|
|
16
|
+
# @param wakeup_io [IO#readbyte]
|
|
17
|
+
# @param mailbox [Object#pop] receives messages from clients.
|
|
18
|
+
# @param programs_status_changed_cond [Async::Condition]
|
|
19
|
+
def initialize(spec:, client_sink:, wakeup_io:, mailbox:, programs_status_changed_cond:)
|
|
20
|
+
@spec, @client_sink, @wakeup_io, @mailbox =
|
|
21
|
+
spec, client_sink, wakeup_io, mailbox
|
|
22
|
+
@running_programs = []
|
|
23
|
+
@started_programs = []
|
|
24
|
+
@watch_ios = []
|
|
25
|
+
@is_pending_cmd = false
|
|
26
|
+
@should_stop = false
|
|
27
|
+
init_writer
|
|
28
|
+
init_notifiable programs_status_changed_cond:
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
until @should_stop
|
|
33
|
+
if ready_io = check_ready_io
|
|
34
|
+
remove_wakeup_io(ready_io[0])
|
|
35
|
+
programs = ready_programs(ready_io[0])
|
|
36
|
+
write_programs_output(programs)
|
|
37
|
+
process_cmd
|
|
38
|
+
end
|
|
39
|
+
notify_programs_status_changed
|
|
40
|
+
prune_dead_programs
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
def check_ready_io
|
|
46
|
+
ios_to_watch = @running_programs.flat_map(&:ios)
|
|
47
|
+
ios_to_watch << @wakeup_io
|
|
48
|
+
IO.select ios_to_watch, nil, nil, 0.1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def ready_programs(ios)
|
|
52
|
+
programs = ios.map{|io| @running_programs.find{ it.mark_ready(io) } }
|
|
53
|
+
programs.uniq!(&:id)
|
|
54
|
+
programs
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def prune_dead_programs
|
|
58
|
+
@running_programs.filter! do |program|
|
|
59
|
+
next true if program.alive?
|
|
60
|
+
@writer.write app: program.id, message: "_died", is_error: true
|
|
61
|
+
program.stop
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "pcelka/spec/program_spec"
|
|
3
|
+
|
|
4
|
+
# Parses Procfile.
|
|
5
|
+
module Pcelka
|
|
6
|
+
class Spec
|
|
7
|
+
class Procfile
|
|
8
|
+
class InvalidFormat < StandardError; end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def parse_from_file(path)
|
|
12
|
+
pathname = Pathname.new(path)
|
|
13
|
+
raise ArgumentError, "No Procfile found at path '#{path}'" unless pathname.exist?
|
|
14
|
+
|
|
15
|
+
cwd = Pathname(path).dirname
|
|
16
|
+
parsed = []
|
|
17
|
+
|
|
18
|
+
pathname.each_line do |line|
|
|
19
|
+
next if line.start_with? "#"
|
|
20
|
+
id, cmd = line.split(":", 2)
|
|
21
|
+
raise InvalidFormat, "Line has invalid format: '#{line}'" if cmd.nil?
|
|
22
|
+
parsed << ProgramSpec.new(id:, cmd: parse_cmd(cmd), cwd:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
parsed
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
def parse_cmd(cmd)
|
|
30
|
+
envs, cmd_parts = cmd.split.partition{ it.include?("=") }
|
|
31
|
+
final_cmd = +""
|
|
32
|
+
|
|
33
|
+
if !envs.empty?
|
|
34
|
+
final_cmd << "env "
|
|
35
|
+
envs.each do |env|
|
|
36
|
+
final_cmd << env
|
|
37
|
+
end
|
|
38
|
+
final_cmd << " "
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
final_cmd << "sh -c '" << cmd_parts.join(" ") << "'"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/pcelka/spec.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Writes possibly colored output to the console. Respencts the NO_COLOR
|
|
4
|
+
# environment variable.
|
|
5
|
+
module Pcelka
|
|
6
|
+
class Writer
|
|
7
|
+
class ConsoleWriter
|
|
8
|
+
ANSI = {
|
|
9
|
+
reset: 0, black: 30, red: 31, green: 32, yellow: 33, blue: 34,
|
|
10
|
+
magenta: 35, cyan: 36, white: 37, bright_black: 30, bright_red: 31,
|
|
11
|
+
bright_green: 32, bright_yellow: 33, bright_blue: 34,
|
|
12
|
+
bright_magenta: 35, bright_cyan: 36, bright_white: 37 }.freeze
|
|
13
|
+
|
|
14
|
+
COLORS = %i[
|
|
15
|
+
cyan yellow green magenta blue bright_cyan bright_yellow
|
|
16
|
+
bright_green bright_magenta bright_red bright_blue].freeze
|
|
17
|
+
|
|
18
|
+
ANSI_RESET = "\e[0m"
|
|
19
|
+
ANSI_ERROR = "\e[31m"
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@colors = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write(app:, message:, is_error:)
|
|
26
|
+
log_line = +""
|
|
27
|
+
if is_error
|
|
28
|
+
log_line << colored{ANSI_ERROR} << "STDERR "
|
|
29
|
+
else
|
|
30
|
+
log_line << colored{ansi_color(app)}
|
|
31
|
+
end
|
|
32
|
+
log_line << app << ": " << colored{ANSI_RESET} << message
|
|
33
|
+
$stdout.write log_line
|
|
34
|
+
$stdout.flush
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
def ansi_color(app)
|
|
39
|
+
@colors[app] ||=
|
|
40
|
+
begin
|
|
41
|
+
color = COLORS[@colors.size % COLORS.size]
|
|
42
|
+
ansi = ANSI[color]
|
|
43
|
+
"\e[#{ansi}m"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def colored
|
|
48
|
+
no_color? ? "" : yield
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def no_color?
|
|
52
|
+
return @no_color if defined?(@no_color)
|
|
53
|
+
return @no_color = true unless $stdout.isatty
|
|
54
|
+
no_color_env = ENV["NO_COLOR"]
|
|
55
|
+
return @no_color = true if no_color_env && !no_color_env.empty?
|
|
56
|
+
@no_color = false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Writes a structured message to an `Object#<<`.
|
|
4
|
+
module Pcelka
|
|
5
|
+
class Writer
|
|
6
|
+
class GenericWriter
|
|
7
|
+
def initialize(sink)
|
|
8
|
+
@sink = sink
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write(app:, message:, is_error:)
|
|
12
|
+
@sink << [app, message, is_error]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "pcelka/writer/console_writer"
|
|
3
|
+
require "pcelka/writer/generic_writer"
|
|
4
|
+
|
|
5
|
+
# Container for different concrete writers. Delegates writing to all
|
|
6
|
+
# the @writers.
|
|
7
|
+
module Pcelka
|
|
8
|
+
class Writer
|
|
9
|
+
def initialize
|
|
10
|
+
@writers = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add_console_writer
|
|
14
|
+
@writers << ConsoleWriter.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def add_generic_writer(port)
|
|
18
|
+
@writers << GenericWriter.new(port)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(app:, message:, is_error: false)
|
|
22
|
+
@writers.each{ it.write(app:, message:, is_error:) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/pcelka.rb
ADDED
data/models.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "db"
|
|
3
|
+
|
|
4
|
+
# if %w"development test".include? ENV["RACK_ENV"]
|
|
5
|
+
# require "logger"
|
|
6
|
+
# LOGGER = Logger.new($stdout)
|
|
7
|
+
# LOGGER.level = Logger::FATAL if ENV["RACK_ENV"] == "test"
|
|
8
|
+
# DB.loggers << LOGGER
|
|
9
|
+
# end
|
|
10
|
+
|
|
11
|
+
if %w[development test].include? ENV["RACK_ENV"]
|
|
12
|
+
require "console"
|
|
13
|
+
Console.logger.fatal! if ENV["RACK_ENV"] == "test"
|
|
14
|
+
DB.loggers << Console
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require "sequel/core"
|
|
18
|
+
Sequel.extension :migration
|
|
19
|
+
Sequel::Migrator.run(DB, "db/migrations")
|
|
20
|
+
|
|
21
|
+
Sequel.extension :fiber_concurrency
|
data/pcelka.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "async"
|
|
3
|
+
require "pcelka/async_client"
|
|
4
|
+
require "pcelka/logs_collector"
|
|
5
|
+
|
|
6
|
+
procfile = Pathname(ENV.fetch("PCELKA_PROCFILE", "Procfile"))
|
|
7
|
+
unless procfile.exist?
|
|
8
|
+
procfile = Pathname(__FILE__).parent.join("examples/procfile/Procfile")
|
|
9
|
+
Console.warn "Running an example proc!"
|
|
10
|
+
Console.warn "Please run in the directory with Procfile or specify it via the PCELKA_PROCFILE environment variable"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
logs_mailbox = Async::Queue.new
|
|
14
|
+
PCELKA = Pcelka::AsyncClient.from_procfile procfile
|
|
15
|
+
PCELKA.server.writer.add_generic_writer logs_mailbox
|
|
16
|
+
LOGS_COLLECTOR = Pcelka::LogsCollector.new logs_mailbox
|
|
17
|
+
LOGS_COLLECTOR.collect
|
|
18
|
+
|
|
19
|
+
Thread.new do
|
|
20
|
+
PCELKA.server.run
|
|
21
|
+
PCELKA << :start_all
|
|
22
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/public/favicon.ico
ADDED
|
Binary file
|