Skip to content

Commit c16e6fc

Browse files
committed
Extract subprocess management to a reusable module.
1 parent 60ec684 commit c16e6fc

File tree

2 files changed

+171
-68
lines changed

2 files changed

+171
-68
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2014 Selenium committers
2+
// Copyright 2014 Software Freedom Conservancy
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
'use strict';
17+
18+
var childProcess = require('child_process');
19+
20+
var promise = require('..').promise;
21+
22+
23+
/**
24+
* A hash with configuration options for an executed command.
25+
* <ul>
26+
* <li>
27+
* <li>{@code args} - Command line arguments.
28+
* <li>{@code env} - Command environment; will inherit from the current process
29+
* if missing.
30+
* <li>{@code stdio} - IO configuration for the spawned server process. For
31+
* more information, refer to the documentation of
32+
* {@code child_process.spawn}.
33+
* </ul>
34+
*
35+
* @typedef {{
36+
* args: (!Array.<string>|undefined),
37+
* env: (!Object.<string, string>|undefined),
38+
* stdio: (string|!Array.<string|number|!Stream|null|undefined>|undefined)
39+
* }}
40+
*/
41+
var Options;
42+
43+
44+
/**
45+
* Describes a command's termination conditions.
46+
* @param {?number} code The exit code, or {@code null} if the command did not
47+
* exit normally.
48+
* @param {?string} signal The signal used to kill the command, or
49+
* {@code null}.
50+
* @constructor
51+
*/
52+
var Result = function(code, signal) {
53+
/** @type {?number} */
54+
this.code = code;
55+
56+
/** @type {?string} */
57+
this.signal = signal;
58+
};
59+
60+
61+
/**
62+
* Represents a command running in a sub-process.
63+
* @param {!promise.Promise.<!Result>} result The command result.
64+
* @constructor
65+
*/
66+
var Command = function(result, onKill) {
67+
/** @return {boolean} Whether this command is still running. */
68+
this.isRunning = function() {
69+
return result.isPending();
70+
};
71+
72+
/**
73+
* @return {!promise.Promise.<!Result>} A promise for the result of this
74+
* command.
75+
*/
76+
this.result = function() {
77+
return result;
78+
};
79+
80+
/**
81+
* Sends a signal to the underlying process.
82+
* @param {string=} opt_signal The signal to send; defaults to
83+
* {@code SIGTERM}.
84+
*/
85+
this.kill = function(opt_signal) {
86+
onKill(opt_signal || 'SIGTERM');
87+
};
88+
};
89+
90+
91+
// PUBLIC API
92+
93+
94+
/**
95+
* Spawns a child process. The returned {@link Command} may be used to wait
96+
* for the process result or to send signals to the process.
97+
*
98+
* @param {string} command The executable to spawn.
99+
* @param {Options=} opt_options The command options.
100+
* @return {!Command} The launched command.
101+
*/
102+
module.exports = function(command, opt_options) {
103+
var options = opt_options || {};
104+
105+
var proc = childProcess.spawn(command, options.args || [], {
106+
env: options.env || process.env,
107+
stdio: options.stdio || 'ignore'
108+
}).once('exit', onExit);
109+
110+
// This process should not wait on the spawned child, however, we do
111+
// want to ensure the child is killed when this process exits.
112+
proc.unref();
113+
process.once('exit', killCommand);
114+
115+
var result = promise.defer();
116+
var cmd = new Command(result.promise, function(signal) {
117+
if (!result.isPending() || !proc) {
118+
return; // No longer running.
119+
}
120+
proc.kill(signal);
121+
});
122+
return cmd;
123+
124+
function onExit(code, signal) {
125+
proc = null;
126+
process.removeListener('exit', killCommand);
127+
result.fulfill(new Result(code, signal));
128+
}
129+
130+
function killCommand() {
131+
process.removeListener('exit', killCommand);
132+
proc && proc.kill('SIGTERM');
133+
}
134+
};

javascript/node/selenium-webdriver/remote/index.js

Lines changed: 37 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@
1414

1515
'use strict';
1616

17-
var spawn = require('child_process').spawn,
18-
os = require('os'),
19-
path = require('path'),
17+
var path = require('path'),
2018
url = require('url'),
2119
util = require('util');
2220

2321
var promise = require('../').promise,
2422
httpUtil = require('../http/util'),
23+
exec = require('../io/exec'),
2524
net = require('../net'),
2625
portprober = require('../net/portprober');
2726

@@ -95,6 +94,21 @@ function DriverService(executable, options) {
9594

9695
/** @private {(string|!Array.<string|number|!Stream|null|undefined>)} */
9796
this.stdio_ = options.stdio || 'ignore';
97+
98+
/**
99+
* A promise for the managed subprocess, or null if the server has not been
100+
* started yet. This promise will never be rejected.
101+
* @private {promise.Promise.<!exec.Command>}
102+
*/
103+
this.command_ = null;
104+
105+
/**
106+
* Promise that resolves to the server's address or null if the server has
107+
* not been started. This promise will be rejected if the server terminates
108+
* before it starts accepting WebDriver requests.
109+
* @private {promise.Promise.<string>}
110+
*/
111+
this.address_ = null;
98112
}
99113

100114

@@ -106,26 +120,6 @@ function DriverService(executable, options) {
106120
DriverService.DEFAULT_START_TIMEOUT_MS = 30 * 1000;
107121

108122

109-
/** @private {child_process.ChildProcess} */
110-
DriverService.prototype.process_ = null;
111-
112-
113-
/**
114-
* Promise that resolves to the server's address or null if the server has not
115-
* been started.
116-
* @private {webdriver.promise.Promise.<string>}
117-
*/
118-
DriverService.prototype.address_ = null;
119-
120-
121-
/**
122-
* Promise that tracks the status of shutting down the server, or null if the
123-
* server is not currently shutting down.
124-
* @private {webdriver.promise.Promise}
125-
*/
126-
DriverService.prototype.shutdownHook_ = null;
127-
128-
129123
/**
130124
* @return {!webdriver.promise.Promise.<string>} A promise that resolves to
131125
* the server's address.
@@ -140,6 +134,8 @@ DriverService.prototype.address = function() {
140134

141135

142136
/**
137+
* Returns whether the underlying process is still running. This does not take
138+
* into account whether the process is in the process of shutting down.
143139
* @return {boolean} Whether the underlying service process is running.
144140
*/
145141
DriverService.prototype.isRunning = function() {
@@ -151,7 +147,7 @@ DriverService.prototype.isRunning = function() {
151147
* Starts the server if it is not already running.
152148
* @param {number=} opt_timeoutMs How long to wait, in milliseconds, for the
153149
* server to start accepting requests. Defaults to 30 seconds.
154-
* @return {!webdriver.promise.Promise.<string>} A promise that will resolve
150+
* @return {!promise.Promise.<string>} A promise that will resolve
155151
* to the server's base URL when it has started accepting requests. If the
156152
* timeout expires before the server has started, the promise will be
157153
* rejected.
@@ -164,21 +160,28 @@ DriverService.prototype.start = function(opt_timeoutMs) {
164160
var timeout = opt_timeoutMs || DriverService.DEFAULT_START_TIMEOUT_MS;
165161

166162
var self = this;
163+
this.command_ = promise.defer();
167164
this.address_ = promise.defer();
168165
this.address_.fulfill(promise.when(this.port_, function(port) {
169166
if (port <= 0) {
170167
throw Error('Port must be > 0: ' + port);
171168
}
172169
return promise.when(self.args_, function(args) {
173-
self.process_ = spawn(self.executable_, args, {
170+
var command = exec(self.executable_, {
171+
args: args,
174172
env: self.env_,
175173
stdio: self.stdio_
176-
}).once('exit', onServerExit);
174+
});
175+
176+
self.command_.fulfill(command);
177177

178-
// This process should not wait on the spawned child, however, we do
179-
// want to ensure the child is killed when this process exits.
180-
self.process_.unref();
181-
process.once('exit', killServer);
178+
command.result().then(function(result) {
179+
self.address_.reject(result.code == null ?
180+
Error('Server was killed with ' + result.signal) :
181+
Error('Server exited with ' + result.code));
182+
self.address_ = null;
183+
self.command_ = null;
184+
});
182185

183186
var serverUrl = url.format({
184187
protocol: 'http',
@@ -195,26 +198,6 @@ DriverService.prototype.start = function(opt_timeoutMs) {
195198
}));
196199

197200
return this.address_;
198-
199-
function onServerExit(code, signal) {
200-
self.address_.reject(code == null ?
201-
Error('Server was killed with ' + signal) :
202-
Error('Server exited with ' + code));
203-
204-
if (self.shutdownHook_) {
205-
self.shutdownHook_.fulfill();
206-
}
207-
208-
self.shutdownHook_ = null;
209-
self.address_ = null;
210-
self.process_ = null;
211-
process.removeListener('exit', killServer);
212-
}
213-
214-
function killServer() {
215-
process.removeListener('exit', killServer);
216-
self.process_ && self.process_.kill('SIGTERM');
217-
}
218201
};
219202

220203

@@ -226,26 +209,12 @@ DriverService.prototype.start = function(opt_timeoutMs) {
226209
* the server has been stopped.
227210
*/
228211
DriverService.prototype.kill = function() {
229-
if (!this.address_) {
212+
if (!this.address_ || !this.command_) {
230213
return promise.fulfilled(); // Not currently running.
231214
}
232-
233-
if (!this.shutdownHook_) {
234-
// No process: still starting; wait on address.
235-
// Otherwise, kill the process now. Exit handler will resolve the
236-
// shutdown hook.
237-
if (this.process_) {
238-
this.shutdownHook_ = promise.defer();
239-
this.process_.kill('SIGTERM');
240-
} else {
241-
var self = this;
242-
this.shutdownHook_ = this.address_.thenFinally(function() {
243-
self.process_ && self.process_.kill('SIGTERM');
244-
});
245-
}
246-
}
247-
248-
return this.shutdownHook_;
215+
return this.command_.then(function(command) {
216+
command.kill('SIGTERM');
217+
});
249218
};
250219

251220

0 commit comments

Comments
 (0)