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.
- checksums.yaml +15 -0
- data/LICENSE +19 -0
- data/README.md +72 -0
- data/Rakefile +10 -0
- data/childprocess-server.gemspec +21 -0
- data/lib/childprocess-server.rb +1 -0
- data/lib/childprocess/server.rb +123 -0
- data/spec/childprocess/server_spec.rb +114 -0
- data/spec/spec_helper.rb +17 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|