restartable 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MWQ5YTBhNmJmNjQxOTdkZGRmZDlmOThhYWZmZmQ5MDY4OGJiMWM1Yw==
4
+ OGZhZTQzNTI5MDQxODAwNjA5YmM1NGYxMDhkODU3ZTczMWQ2ZGFhOQ==
5
5
  data.tar.gz: !binary |-
6
- ZjMwMzEyMzBhYzQ0NzMyZjgyZjM3YTBhMmMzZDU5OWNhNjU2MGQ0ZA==
6
+ MTAwNzEyYWVjZjA2ODM4MWVhMTc3NGRjOTcwYTIwZWI0ZjQ0ZjdmMQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NzAwZmI4M2VkMzU4ZTE1ZmE5MjJiYWFmNTFjMTI3MGMyM2UzZjYxMWMxYzI1
10
- ZDdmN2U4M2IwODdmYjYyMjJmMDUzZjc2OTkwZDk5ODY0ZWFjY2I2ZWU3NWY2
11
- MDk0MDBiMjFkNGM0MGQ2YmUwZmYwZmZjYTNhMDk4MGE2NDM4MjM=
9
+ YTE4NjVlMTJiNjhiMzU1NzQ4ZjY3ZWJiOWQ4MjI5YzIxMTVlNGNmMDAwMTdh
10
+ NDI3YjIxYmYwMTlhYWQyMTcyYTU1ZWU1OWY1YTlkMmU2YTRhOGU2ZmViY2Nj
11
+ ZDJhZWFiNWFlYjdiZjI1MTM4OTAxNDJjM2MxNzA3MzZlZWM0Nzg=
12
12
  data.tar.gz: !binary |-
13
- ZTczZDEwNzY1MDg3Y2VhOTI4OTVkNzA0NzlhM2FlMjYyN2VlYzhiODc4Yjlm
14
- YmQ5NzFjYmMxNjA2N2ZjZTkzNzFkZTc0ODY4MTNlNGIzZGE5MDM3YjI2ZWVm
15
- ZTM2ZDVhOGQxZmE2ZmZjODA4YTJiZDdiODVlMDcyZjY0MTE5ZTU=
13
+ YzQ2Zjg5YzhiNTA4ZTBkNTRjNjUwNDEwYzQ5ZjQxM2FiMzA1MzJjOTA1YzYw
14
+ MWQ1YTIyNWMzOWQzNTY5ZjJiYzI2YWUxMzc2NDFhZDg2ZWEwYTFhZWRmYmFi
15
+ OGVmMmYxOGYyODZkZTAyNzM5NTVmN2MxMGViNjk5NWViMmM3M2I=
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /.yardoc/
7
7
  /coverage/
8
8
 
9
+ Gemfile.lock
9
10
  Makefile
10
11
  *.o
11
12
  *.bundle
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - rbx-18mode
8
+ - rbx-19mode
9
+ - ree
10
+ script: bundle exec cucumber
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/README.markdown CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Run code, Ctrl-C to restart, two Ctrl-C to stop.
4
4
 
5
+ [![Build Status](https://travis-ci.org/toy/restartable.png?branch=master)](https://travis-ci.org/toy/restartable)
6
+
5
7
  ## Copyright
6
8
 
7
9
  Copyright (c) 2012-2013 Ivan Kuchin. See LICENSE.txt for details.
data/bin/restartable CHANGED
@@ -33,5 +33,5 @@ rescue OptionParser::ParseError => e
33
33
  end
34
34
 
35
35
  Restartable.new(options) do
36
- system *ARGV
36
+ exec *ARGV
37
37
  end
@@ -0,0 +1,30 @@
1
+ Feature: Restarting
2
+
3
+ Scenario Outline: Restarting and terminating
4
+ Given I have invoked restartable with `<code>`
5
+
6
+ When I have waited for 1 second
7
+ Then I should see "^C to restart, double ^C to stop" in stderr
8
+ And I should see "Hello world!" in stdout
9
+ And there should be an inner process
10
+ When I interrupt restartable
11
+ Then I should see "Killing children…" and "Waiting ^C 0.5 second than restart…" in stderr
12
+ And inner process should terminate
13
+
14
+ When I have waited for 1 second
15
+ Then I should see "^C to restart, double ^C to stop" in stderr
16
+ And I should see "Hello world!" in stdout
17
+ And there should be inner process
18
+ When I interrupt restartable twice
19
+ Then I should see "Killing children…" and "Don't restart!" in last 3 lines of stderr
20
+ And inner process should terminate
21
+ And restartable should finish
22
+
23
+ Examples:
24
+ | code |
25
+ | $stdout.puts "Hello world!" |
26
+ | $stdout.puts "Hello world!"; sleep 30 |
27
+ | exec 'echo "Hello world!"; sleep 30' |
28
+ | system 'echo "Hello world!"; sleep 30' |
29
+ | fork{ $stdout.puts "Hello world!"; sleep 30 } |
30
+ | fork{ fork{ fork{ $stdout.puts "Hello world!"; sleep 30 } } } |
@@ -0,0 +1,67 @@
1
+ require 'restartable'
2
+
3
+ Given(/^I have invoked restartable with `(.*?)`$/) do |command|
4
+ @stdout = IO.pipe
5
+ @stderr = IO.pipe
6
+
7
+ @pid = fork do
8
+ Process.setpgrp
9
+
10
+ @stdout[0].close
11
+ STDOUT.reopen(@stdout[1])
12
+
13
+ @stderr[0].close
14
+ STDERR.reopen(@stderr[1])
15
+
16
+ Restartable.new do
17
+ Signal.trap('INT', 'EXIT')
18
+ eval(command)
19
+ end
20
+ end
21
+
22
+ @stdout[1].close
23
+ @stderr[1].close
24
+ end
25
+
26
+ When(/^I have waited for (\d+) second$/) do |seconds|
27
+ sleep seconds.to_i
28
+ end
29
+
30
+ Then(/^I should see "(.*?)" in stdout$/) do |string|
31
+ Timeout::timeout(5) do
32
+ @stdout[0].gets.should include(string)
33
+ end
34
+ end
35
+
36
+ Then(/^I should see "(.*?)" in (?:last (\d+) lines of )?stderr$/) do |arg, line_count|
37
+ Timeout::timeout(5) do
38
+ strings = arg.split(/".*?"/)
39
+ line_count = line_count ? line_count.to_i : strings.length
40
+ got = line_count.times.map{ @stderr[0].gets }.join
41
+ strings.each do |string|
42
+ got.should include(string)
43
+ end
44
+ end
45
+ end
46
+
47
+ When(/^I interrupt restartable$/) do
48
+ Process.kill('INT', -@pid)
49
+ end
50
+
51
+ When(/^I interrupt restartable twice$/) do
52
+ Process.kill('INT', -@pid)
53
+ sleep 0.1
54
+ Process.kill('INT', -@pid)
55
+ end
56
+
57
+ Then(/^there should be an inner process$/) do
58
+ Sys::ProcTable.ps.any?{ |pe| pe.ppid == @cpid }
59
+ end
60
+
61
+ Then(/^inner process should terminate$/) do
62
+ Sys::ProcTable.ps.none?{ |pe| pe.ppid == @cpid }
63
+ end
64
+
65
+ Then(/^restartable should finish$/) do
66
+ Process.wait(@pid)
67
+ end
data/lib/restartable.rb CHANGED
@@ -9,7 +9,7 @@ class Restartable
9
9
  Gem.loaded_specs['restartable'].version.to_s rescue 'DEV'
10
10
  end
11
11
 
12
- def initialize(options, &block)
12
+ def initialize(options = {}, &block)
13
13
  @options, @block = options, block
14
14
  run!
15
15
  end
@@ -17,94 +17,86 @@ class Restartable
17
17
  private
18
18
 
19
19
  def run!
20
- @mutex = Mutex.new
21
-
22
- receiver, sender = IO.pipe
23
- @trap_sender = Process.fork do
24
- receiver.close
25
- Signal.trap('PIPE', 'EXIT')
26
- synced_trap('INT'){ Marshal.dump(:int, sender) }
27
- loop{ sleep }
20
+ Signal.trap('INT') do
21
+ interrupt!
22
+ if @cpid
23
+ Process.kill('INT', -@cpid) rescue nil
24
+ end
25
+ end
26
+ Signal.trap('TERM') do
27
+ terminate!
28
28
  end
29
- sender.close
30
-
31
- Signal.trap('INT', 'IGNORE')
32
- synced_trap('TERM'){ terminate! }
33
29
 
34
- int_receiver = Thread.new do
35
- until receiver.eof?
36
- Marshal.load(receiver) && interrupt!
30
+ until @stop
31
+ @interrupted = nil
32
+ $stderr << "^C to restart, double ^C to stop\n".green
33
+ @cpid = fork do
34
+ Process.setpgrp
35
+ Signal.trap('INT', 'DEFAULT')
36
+ Signal.trap('TERM', 'DEFAULT')
37
+ @block.call
38
+ end
39
+ sleep 0.1 until @interrupted
40
+ kill_children!
41
+ unless @stop
42
+ $stderr << "Waiting ^C 0.5 second than restart…\n".yellow.bold
43
+ sleep 0.5
37
44
  end
38
45
  end
39
- cycle
40
46
  end
41
47
 
42
48
  def interrupt!
43
- unless @interrupted
44
- @interrupted = true
45
- (Thread.list - [Thread.current]).each do |thread|
46
- thread.raise SignalException.new('INT')
47
- end
49
+ if @interrupted
50
+ @stop = true
51
+ $stderr << "Don't restart!\n".red.bold
48
52
  else
49
- no_restart!
53
+ @interrupted = true
50
54
  end
51
55
  end
52
56
 
53
57
  def terminate!
54
- no_restart!
55
58
  interrupt!
56
- end
57
-
58
- def no_restart!
59
- @stop = true
60
- puts 'Don\'t restart!'.red.bold
61
- end
62
-
63
- def synced_trap(signal, &block)
64
- Signal.trap(signal) do
65
- Thread.new do
66
- @mutex.synchronize(&block)
67
- end
68
- end
59
+ interrupt!
69
60
  end
70
61
 
71
62
  WAIT_SIGNALS = [[5, 'INT'], [3, 'INT'], [1, 'INT'], [3, 'TERM'], [5, 'KILL']]
72
63
 
73
- def cycle
74
- until @stop
75
- @interrupted = false
76
- puts '^C to restart, double ^C to stop'.green
77
- begin
78
- @block.call
79
- loop{ sleep } # wait ^C even if block finishes
80
- rescue SignalException
81
- kill_children!
82
- end
83
- unless @stop
84
- puts 'Waiting ^C 0.5 second than restart…'.yellow.bold
85
- sleep 0.5
86
- end
87
- end
88
- end
89
-
90
64
  def kill_children!
91
- unless children_pids.empty?
92
- puts 'Killing children…'.yellow.bold
65
+ until (pids = children_pids).empty?
66
+ $stderr << "Killing children…\n".yellow.bold
93
67
  ripper = Thread.new do
94
68
  WAIT_SIGNALS.each do |time, signal|
95
69
  sleep time
96
- puts "…SIG#{signal}".yellow
97
- children_pids.each do |child_pid|
98
- Process.kill(signal, child_pid)
70
+ $stderr << "…SIG#{signal}…\n".yellow
71
+ Process.kill('INT', -@cpid)
72
+ end
73
+ end
74
+ Process.waitall
75
+ pids.each do |pid|
76
+ begin
77
+ loop do
78
+ Process.kill(0, pid)
79
+ sleep 1
99
80
  end
81
+ rescue Errno::ESRCH
100
82
  end
101
83
  end
102
- children_pids.each(&Process.method(:wait))
103
84
  ripper.terminate
104
85
  end
105
86
  end
106
87
 
107
88
  def children_pids
108
- Sys::ProcTable.ps.select{ |pe| $$ == pe.ppid }.map(&:pid) - [@trap_sender]
89
+ Sys::ProcTable.ps.select do |pe|
90
+ @cpid == case
91
+ when pe.respond_to?(:pgid)
92
+ pe.pgid
93
+ when pe.respond_to?(:pgrp)
94
+ pe.pgrp
95
+ when pe.respond_to?(:ppid)
96
+ pe.ppid
97
+ else
98
+ raise 'Can\'t find process group id'
99
+ end
100
+ end.map(&:pid)
109
101
  end
110
102
  end
data/restartable.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'restartable'
5
- s.version = '0.1.3'
5
+ s.version = '0.2.0'
6
6
  s.summary = %q{Run code, Ctrl-C to restart, once more Ctrl-C to stop}
7
7
  s.homepage = "http://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
@@ -17,4 +17,6 @@ Gem::Specification.new do |s|
17
17
 
18
18
  s.add_dependency 'colored', '~> 1.2'
19
19
  s.add_dependency 'sys-proctable', '~> 0.9.3'
20
+ s.add_development_dependency 'cucumber'
21
+ s.add_development_dependency 'rspec'
20
22
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restartable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuchin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-10 00:00:00.000000000 Z
11
+ date: 2013-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colored
@@ -38,6 +38,34 @@ dependencies:
38
38
  - - ~>
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.9.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: cucumber
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
41
69
  description:
42
70
  email:
43
71
  executables:
@@ -46,10 +74,14 @@ extensions: []
46
74
  extra_rdoc_files: []
47
75
  files:
48
76
  - .gitignore
77
+ - .travis.yml
78
+ - Gemfile
49
79
  - LICENSE.txt
50
80
  - README.markdown
51
81
  - TODO
52
82
  - bin/restartable
83
+ - features/restarting.feature
84
+ - features/step_definitions/restartable_steps.rb
53
85
  - lib/restartable.rb
54
86
  - restartable.gemspec
55
87
  homepage: http://github.com/toy/restartable
@@ -76,4 +108,6 @@ rubygems_version: 2.0.3
76
108
  signing_key:
77
109
  specification_version: 4
78
110
  summary: Run code, Ctrl-C to restart, once more Ctrl-C to stop
79
- test_files: []
111
+ test_files:
112
+ - features/restarting.feature
113
+ - features/step_definitions/restartable_steps.rb