restartable 0.1.3 → 0.2.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 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