forked from open-rmf/rmf-web
-
Notifications
You must be signed in to change notification settings - Fork 0
/
rmf-launcher.js
182 lines (160 loc) · 4.55 KB
/
rmf-launcher.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
const { spawn } = require('child_process');
const { mkdirSync } = require('fs');
const LaunchMode = {
None: 0,
LocalSingleton: 1,
};
/**
* Help to launch and kill all required RMF processes. Assumes required rmf components are installed
* and launches them locally.
*/
exports.LocalLauncher = class {
/**
* Singleton instance of rmfLauncher, signal handlers are installed to cleanup processses spawned
* by this instance.
*
* Note: This installs `SIGINT` and `SIGTERM` handlers, these handlers may conflict with existing
* or future handlers. If there are other signal handlers installed, it is recommended to not use
* this, the instance is lazy created so no handlers will be installed if this is never called.
*/
static get instance() {
if (!this._instance) {
this._instance = new exports.LocalLauncher();
/**
* Make sure spawned processes are killed when the program exits.
*/
const cleanUp = async () => {
await this._instance?.kill();
};
process.once('beforeExit', cleanUp);
process.once('exit', cleanUp);
process.once('SIGINT', cleanUp);
process.once('SIGTERM', cleanUp);
}
return this._instance;
}
static _instance;
async launch(timeout = 30000) {
if (this._launched) {
return;
}
const headless = !process.env.RMF_DASHBOARD_NO_HEADLESS;
const demoPkg = process.env.RMF_DASHBOARD_DEMO_PACKAGE || 'rmf_demos_gz';
const demoMap = process.env.RMF_DASHBOARD_DEMO_MAP || 'office.launch.xml';
const demoArgs = ['launch', demoPkg, demoMap, 'server_uri:=ws://localhost:8000/_internal'];
if (headless) {
demoArgs.push('headless:=true');
}
mkdirSync(`${__dirname}/.rmf`, { recursive: true });
this._rmfDemo = new ManagedProcess('ros2', demoArgs, {
stdio: 'inherit',
cwd: `${__dirname}/.rmf`,
});
const ready = await this._rmfReady(timeout);
if (!ready) {
throw new Error('unable to detect rmf');
}
this._launched = true;
}
async kill() {
await Promise.all([this._rmfDemo?.kill('SIGINT')]);
this._rmfDemo = undefined;
this._launched = false;
}
_launched = false;
_rmfDemo;
async _rmfReady(timeout) {
const ros2Echo = spawn('ros2', [
'topic',
'echo',
'fleet_states',
'rmf_fleet_msgs/msg/FleetState',
]);
if (!ros2Echo) {
return false;
}
return new Promise((res) => {
const timer = setTimeout(() => {
ros2Echo && ros2Echo.kill();
res(false);
}, timeout);
ros2Echo.stdout.once('data', () => {
ros2Echo.kill();
clearTimeout(timer);
res(true);
});
});
}
};
/**
* A wrapper around child process that helps manages spawned process and their subprocesses' life.
* It spawns processes in their own process groups. The `kill` method will kill the process and
* all its childrens.
*
* This is needed especially for `ros2 launch` because newer versions of it no longer propagate
* signals to its children. So killing `ros2 launch` will leave zombie processes.
*/
class ManagedProcess {
get proc() {
return this._proc;
}
get alive() {
return this._procAlive;
}
constructor(command, args, options) {
this._proc = spawn(command, args, {
...options,
detached: true,
});
this._procAlive = !!this._proc;
if (this._procAlive) {
this._proc.once('exit', () => (this._procAlive = false));
}
}
/**
* Kill the child process and all their childrens.
*/
async kill(signal) {
if (!this._procAlive) {
return;
}
return new Promise((res) => {
this._proc.once('exit', res);
process.kill(-this._proc.pid, signal);
});
}
_proc;
_procAlive = false;
}
/**
* Stub launcher that does not do anything, useful in dev cycle when you want to manage the rmf
* processes manually.
*/
exports.StubLauncher = class {
async launch() {}
async kill() {}
};
/**
* Creates a launcher based on the RMF_LAUNCH_MODE environment variable. Value can be one of:
*
* * none
* * local
*
* Defaults to `local`.
*
* If the value is `local`, a singleton instance is used and signal handlers are installed.
*/
exports.makeLauncher = () => {
let launchMode = LaunchMode.LocalSingleton;
if (process.env.RMF_LAUNCH_MODE === 'none') {
launchMode = LaunchMode.None;
}
switch (launchMode) {
case LaunchMode.None:
return new exports.StubLauncher();
case LaunchMode.LocalSingleton:
return exports.LocalLauncher.instance;
default:
throw new Error('unknown launch mode');
}
};