childprocess-server 0.1.1

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YjhjOTBjNTljNzBkMjE4ZmJhODAxNTJiOWU5NzQzYmY3Zjc2YTQ5MA==
5
+ data.tar.gz: !binary |-
6
+ ZTQyZTBiNmNlMGNmZGFmY2U4ODgzOTFjYmMyZDk5ZmMxNDE0YjA2ZQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MDgzYWRjYTFkZGJiYjc0NDg1NzlhYjYwMmFkNGFiODEzNWM3OGM2ZmQxNTNh
10
+ ZGZjYTJhM2ZhYzVlOGI1Y2Q2N2E2MTJiMmE5YmM4YmE5YzI2YmZjNGYzNGQ2
11
+ MDhiZWIyNWU3N2VhZTIwNGYzY2UzNDY5NTQxYjFkMWIzMGI1Yjc=
12
+ data.tar.gz: !binary |-
13
+ YzA4Y2YzOTE2MTgxNjdmNDAxM2RmM2UyNGQ5OGI5NjE0ZjFiOTNlNzQzMjkw
14
+ YjEwZGM4MGI5MzdiNWRmZDI2MDc1ZjBmYTY3ZGVhN2Q2MzA3MWU2ZDIxOGJk
15
+ YmNjMTY5ZGZkYWYxYTMwMWJmYTJhOWRlMzBhMGVlMjk5YzM2M2Y=
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2013 WU Jun <quark@zju.edu.cn>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,72 @@
1
+ # childprocess-server
2
+
3
+ Manage and interact with processes, remotely (via dRuby).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ gem install childprocess-server
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Standalone
14
+
15
+ ```ruby
16
+ require 'childprocess-server'
17
+ server = ChildProcess::Server.new
18
+
19
+ # Run a process
20
+ pid = server.launch('sleep', '1000')
21
+ server.alive?(pid) # => true
22
+ server.stop(pid)
23
+ server.alive?(pid) # => false
24
+
25
+ # Run 'echo', get output
26
+ pid = server.launch('echo', 'hello')
27
+ server.read_output(pid) # => "hello\n"
28
+ server.alive?(pid) # => false
29
+
30
+ # Run 'cat', write stdin interactively
31
+ pid = server.launch('cat')
32
+ server.write_input(pid, "foo\n")
33
+ server.read_output(pid) # => "foo\n"
34
+ server.write_input(pid, "bar\n")
35
+ server.read_output(pid) # => "foo\nbar\n"
36
+ server.stop(pid)
37
+
38
+ # List all process ids managed
39
+ # Note: exited processes are also listed.
40
+ server.list_pids # => [ ... ]
41
+
42
+ # Clean up
43
+ server.clean_up
44
+ server.list_pids # => []
45
+ ```
46
+
47
+ ### Client / Server
48
+
49
+ ```ruby
50
+ DRB_URI = 'drbunix://tmp/a.socket'
51
+
52
+ # Server side
53
+ ChildProcess::Server.new.start_service(DRB_URI) # will block by default
54
+
55
+ # Client side
56
+ server = ChildProcess::Server.connect(DRB_URI)
57
+ pid = server.launch('sort', '-n')
58
+ server.write_input(pid, "20\n10\n2\n1\n")
59
+ server.read_output(pid) # => "1\n2\n10\n20\n"
60
+ ```
61
+
62
+ ## Notes
63
+
64
+ * This library is thread-safe.
65
+ * This library does not have authentic feature.
66
+ Set file system permissions or firewall for security.
67
+ * After `stop`, output can still be read using `read_output`,
68
+ because outputs are stored in temporary files.
69
+ Use `clean_up` to delete them immediately.
70
+ Otherwise they are deleted when server exits.
71
+ * If `read_output` or `write_input` encounters errors,
72
+ they just return `nil` instead of raising an error.
@@ -0,0 +1,10 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'yard'
3
+
4
+ task :default => :spec
5
+
6
+ RSpec::Core::RakeTask.new
7
+ YARD::Rake::YardocTask.new
8
+
9
+ task :doc => [:yard]
10
+ task :test => [:spec]
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'childprocess-server'
3
+ s.version = '0.1.1'
4
+ s.date = Date.civil(2013,3,31)
5
+ s.summary = 'Manage and interact with processes, remotely.'
6
+ s.description = 'Manage and interact with CLI processes, remotely via dRuby.'
7
+ s.authors = ["Wu Jun"]
8
+ s.email = 'quark@zju.edu.cn'
9
+ s.homepage = 'https://github.com/quark-zju/childprocess-server'
10
+ s.require_paths = ['lib']
11
+ s.licenses = ['MIT']
12
+ s.has_rdoc = 'yard'
13
+ s.files = %w(LICENSE README.md Rakefile childprocess-server.gemspec)
14
+ s.files += Dir['{lib,spec}/**/*.rb']
15
+ s.add_dependency 'childprocess', '~> 0.3.9'
16
+ s.add_development_dependency 'rake', '~> 10.0'
17
+ s.add_development_dependency 'rspec', '~> 2.13'
18
+ s.add_development_dependency 'yard', '~> 0.8'
19
+ s.test_files = Dir['spec/**/*.rb']
20
+ end
21
+
@@ -0,0 +1 @@
1
+ require 'childprocess/server'
@@ -0,0 +1,123 @@
1
+ require 'drb/drb'
2
+ require 'childprocess'
3
+ require 'thread'
4
+ require 'tempfile'
5
+
6
+ class ChildProcess::Server
7
+
8
+ # Connect to existing DRb service.
9
+ #
10
+ # @param uri [String] drb path
11
+ # @return [DrbObject<Server>] remote server
12
+ def self.connect(uri)
13
+ DRb.start_service
14
+ DRbObject.new_with_uri(uri)
15
+ end
16
+
17
+ # Start DRb service.
18
+ #
19
+ # @param wait [Bool] whether to block and wait for drb service to end
20
+ # @param uri [String] drb path
21
+ # @return [DRb::DRbServer]
22
+ def start_service(uri, wait = true)
23
+ server = DRb.start_service(uri, self)
24
+ DRb.thread.join if wait
25
+ server
26
+ end
27
+
28
+ def initialize
29
+ @processes = {}
30
+ @mutex = Mutex.new
31
+ end
32
+
33
+ # Launch a process in background.
34
+ #
35
+ # @param commands [Array<String>] commands
36
+ # @return [Integer] pid
37
+ def launch(*commands)
38
+ output = Tempfile.new('cps-out')
39
+ output.sync = true
40
+
41
+ process = ChildProcess.build(*commands)
42
+ process.io.stdout = process.io.stderr = output
43
+ process.duplex = true
44
+ process.start
45
+
46
+ pid = process.pid
47
+ access_processes do |processes|
48
+ processes[pid] = process
49
+ end
50
+ pid
51
+ end
52
+
53
+ # Read output, will not block.
54
+ #
55
+ # @param pid [Integer] process id
56
+ # @return [String] output so far, <tt>nil</tt> on error
57
+ def read_output(pid)
58
+ access_processes do |processes|
59
+ File.read(processes[pid].io.stdout.path) rescue nil
60
+ end
61
+ end
62
+
63
+ # Write to input.
64
+ #
65
+ # @param pid [Integer] process id
66
+ def write_input(pid, content)
67
+ access_processes do |processes|
68
+ processes[pid] && processes[pid].io.stdin.write(content) rescue nil
69
+ end
70
+ end
71
+
72
+ # List process ids managed by this server.
73
+ #
74
+ # @return [Array<Integer>] process ids
75
+ def list_pids
76
+ access_processes do |processes|
77
+ processes.keys
78
+ end
79
+ end
80
+
81
+ # Check whether a process managed by this server is alive.
82
+ #
83
+ # @param pid [Integer] process id
84
+ # @return [Bool] whether that process is alive,
85
+ # <tt>nil</tt> if that process is not managed by this server
86
+ def alive?(pid)
87
+ access_processes do |processes|
88
+ processes[pid] && processes[pid].alive?
89
+ end
90
+ end
91
+
92
+ # Stop a process managed by this server.
93
+ #
94
+ # @param pid [Integer] process id
95
+ def stop(pid)
96
+ access_processes do |processes|
97
+ processes[pid] && processes[pid].stop
98
+ end
99
+ end
100
+
101
+ # Clean up exited processes.
102
+ def clean_up
103
+ access_processes do |processes|
104
+ processes.values.select(&:exited?).each do |process|
105
+ process.io.stdout.path.unlink rescue nil
106
+ end
107
+ processes.delete_if { |_, process| process.exited? }
108
+ # Do not leak @processes outside
109
+ # We are using dRuby, keep input/output objects simple
110
+ nil
111
+ end
112
+ end
113
+
114
+ protected
115
+
116
+ # Access processes, exclusively.
117
+ def access_processes
118
+ @mutex.synchronize do
119
+ yield @processes
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+ require 'childprocess/server'
3
+
4
+ shared_examples_for 'common-server-tests' do
5
+ describe 'for external processes' do
6
+ it 'runs "true"' do
7
+ pid = server.launch('true')
8
+ pid.should > 0
9
+ server.list_pids.should include(pid)
10
+ server.read_output(pid).should be_empty
11
+ server.stop(pid)
12
+ server.alive?(pid).should be_false
13
+ server.clean_up.should be_nil
14
+ server.alive?(pid).should be_nil
15
+ server.list_pids.should_not include(pid)
16
+ end
17
+
18
+ it 'runs "cat" interactively' do
19
+ pid = server.launch('stdbuf', '-o0', 'cat')
20
+ server.alive?(pid).should be_true
21
+ (server.read_output(pid) || '').should be_empty
22
+ server.write_input(pid, "a\n")
23
+ sleep 0.1
24
+ server.read_output(pid).should == "a\n"
25
+ server.write_input(pid, "bc\n")
26
+ sleep 0.1
27
+ server.read_output(pid).should == "a\nbc\n"
28
+ server.stop(pid)
29
+ server.alive?(pid).should be_false
30
+ server.clean_up.should be_nil
31
+ end
32
+ end
33
+
34
+ describe 'for processes not managed' do
35
+ it '#read_output' do
36
+ server.read_output($$).should be_nil
37
+ end
38
+
39
+ it '#alive?' do
40
+ server.alive?($$).should be_nil
41
+ end
42
+
43
+ it 'stop' do
44
+ expect do
45
+ server.stop($$)
46
+ end.to_not raise_error
47
+ end
48
+
49
+ it '#write_input' do
50
+ expect do
51
+ server.write_input($$, 'test')
52
+ end.to_not raise_error
53
+ end
54
+ end
55
+ end
56
+
57
+ describe 'standalone' do
58
+ let(:server) { ChildProcess::Server.new }
59
+
60
+ it_should_behave_like 'common-server-tests'
61
+ end
62
+
63
+ describe 'client-server' do
64
+ SOCKET_FILE = File.join(Dir.tmpdir, "csspec-#{$$}.socket")
65
+ DRB_URI = "drbunix://#{SOCKET_FILE}"
66
+ THREAD_COUNT = 10
67
+ CLIENT_COUNT = 4
68
+
69
+ before(:all) do
70
+ server = ChildProcess::Server.new
71
+ @service = server.start_service(DRB_URI, false)
72
+ end
73
+
74
+ after(:all) do
75
+ @service.stop_service
76
+ File.unlink(SOCKET_FILE) rescue nil
77
+ end
78
+
79
+ let(:server) { ChildProcess::Server.connect(DRB_URI) }
80
+
81
+ it_should_behave_like 'common-server-tests'
82
+
83
+ it 'accepts multiple connections' do
84
+ expect do
85
+ clients = CLIENT_COUNT.times.map { ChildProcess::Server.connect(DRB_URI) }
86
+ pids = clients.map { |client| client.launch('sleep', '3') }
87
+ server.list_pids.should include(*pids)
88
+ pids.each { |pid| server.stop(pid) }
89
+ server.clean_up
90
+ end.to_not change { server.list_pids }
91
+ end
92
+
93
+ it 'handle concurrent requests' do
94
+ expect do
95
+ threads = THREAD_COUNT.times.map do
96
+ Thread.new do
97
+ texts = CLIENT_COUNT.times.map { rand.to_s }
98
+ clients = CLIENT_COUNT.times.map { ChildProcess::Server.connect(DRB_URI) }
99
+ pids = clients.map.with_index do |client, i|
100
+ client.launch('echo', texts[i])
101
+ end
102
+ clients[0].list_pids.should include(*pids)
103
+ sleep 0.1
104
+ pids.map.with_index do |pid, i|
105
+ clients[rand(0...clients.size)].read_output(pid).should == "#{texts[i]}\n"
106
+ end
107
+ end
108
+ end
109
+ threads.each(&:join)
110
+ sleep 0.1
111
+ server.clean_up
112
+ end.to_not change { server.list_pids }
113
+ end
114
+ end
@@ -0,0 +1,17 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ RSpec.configure do |config|
8
+ config.treat_symbols_as_metadata_keys_with_true_values = true
9
+ config.run_all_when_everything_filtered = true
10
+ # config.filter_run :focus
11
+
12
+ # Run specs in random order to surface order dependencies. If you find an
13
+ # order dependency and want to debug it, you can fix the order by providing
14
+ # the seed, which is printed after each run.
15
+ # --seed 1234
16
+ config.order = 'random'
17
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: childprocess-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Wu Jun
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: childprocess
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.9
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.9
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '2.13'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '2.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '0.8'
69
+ description: Manage and interact with CLI processes, remotely via dRuby.
70
+ email: quark@zju.edu.cn
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - LICENSE
76
+ - README.md
77
+ - Rakefile
78
+ - childprocess-server.gemspec
79
+ - lib/childprocess/server.rb
80
+ - lib/childprocess-server.rb
81
+ - spec/childprocess/server_spec.rb
82
+ - spec/spec_helper.rb
83
+ homepage: https://github.com/quark-zju/childprocess-server
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.0.3
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Manage and interact with processes, remotely.
107
+ test_files:
108
+ - spec/childprocess/server_spec.rb
109
+ - spec/spec_helper.rb
110
+ has_rdoc: yard