-
Notifications
You must be signed in to change notification settings - Fork 57
/
index.js
316 lines (273 loc) · 8.36 KB
/
index.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
const { spawn } = require('child_process');
const express = require('express');
const fs = require('fs');
const helmet = require('helmet');
const morgan = require('morgan');
const uuid = require('uuid');
const config = require('./config.js');
const rfs = require('rotating-file-stream');
// App and loaded modules.
const app = express();
const HTTP_OK_CODE = 200;
const HTTP_ERROR_CODE = 400;
const HTTP_TOOLARGE_CODE = 413;
const HTTP_INTERNALERROR_CODE = 500;
// Enable cross-origin ressource sharing.
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
res.setHeader('Content-Type', 'application/json');
next();
});
const args = config.cliArgs;
app.use(express.json({limit: args.limit}));
app.use(express.urlencoded({extended: true, limit: args.limit}));
const accessLogStream = rfs.createStream(args.logdir + '/access.log', {
compress: 'gzip',
size: args.logsize,
});
app.use(morgan('combined', {stream: accessLogStream}));
app.use(helmet());
app.use((err, req, res, next) => {
if (
err instanceof SyntaxError &&
err.status === HTTP_ERROR_CODE &&
'body' in err
) {
const message =
'Invalid JSON object in request, please add vehicles and jobs or shipments to the object body';
console.log(now() + ': ' + JSON.stringify(message));
res.status(HTTP_ERROR_CODE);
res.send({
code: config.vroomErrorCodes.input,
error: message,
});
}
});
// Simple date generator for console output.
function now() {
const date = new Date();
return date.toUTCString();
};
function fileExists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch (err) {
return false;
}
};
// Callback for size and some input validity checks.
function sizeCheckCallback(maxLocationNumber, maxVehicleNumber) {
return function (req, res, next) {
const hasJobs = 'jobs' in req.body;
const hasShipments = 'shipments' in req.body;
const correctInput = (hasJobs || hasShipments) && 'vehicles' in req.body;
if (!correctInput) {
const message =
'Invalid JSON object in request, please add vehicles and jobs or shipments to the object body';
console.error(now() + ': ' + JSON.stringify(message));
res.status(HTTP_ERROR_CODE);
res.send({
code: config.vroomErrorCodes.input,
error: message,
});
return;
}
let nbLocations = 0;
if (hasJobs) {
nbLocations += req.body.jobs.length;
}
if (hasShipments) {
nbLocations += 2 * req.body.shipments.length;
}
if (nbLocations > maxLocationNumber) {
const message = [
'Too many locations (',
nbLocations,
') in query, maximum is set to',
maxLocationNumber,
].join(' ');
console.error(now() + ': ' + JSON.stringify(message));
res.status(HTTP_TOOLARGE_CODE);
res.send({
code: config.vroomErrorCodes.tooLarge,
error: message,
});
return;
}
if (req.body.vehicles.length > maxVehicleNumber) {
const vehicles = req.body.vehicles.length;
const message = [
'Too many vehicles (',
vehicles,
') in query, maximum is set to',
maxVehicleNumber,
].join(' ');
console.error(now() + ': ' + JSON.stringify(message));
res.status(HTTP_TOOLARGE_CODE);
res.send({
code: config.vroomErrorCodes.tooLarge,
error: message,
});
return;
}
next();
};
};
const vroomCommand = args.path + 'vroom';
const options = [];
options.push('-r', args.router);
if (args.router !== 'libosrm') {
const routingServers = config.routingServers;
for (const profileName in routingServers[args.router]) {
const profile = routingServers[args.router][profileName];
if ('host' in profile && 'port' in profile) {
options.push('-a', profileName + ':' + profile.host);
options.push('-p', profileName + ':' + profile.port);
} else {
console.error(
"Incomplete configuration: profile '" +
profileName +
"' requires 'host' and 'port'."
);
}
}
}
if (args.geometry) {
options.push('-g');
}
if (args.planmode) {
options.push('-c');
}
function execCallback(req, res) {
const reqOptions = options.slice();
// Default command-line values.
let nbThreads = args.threads;
let explorationLevel = args.explore;
if (args.override && 'options' in req.body) {
// Optionally override defaults.
// Retrieve route geometry.
if (!args.geometry && 'g' in req.body.options && req.body.options.g) {
reqOptions.push('-g');
}
// Set plan mode.
if (!args.planmode && 'c' in req.body.options && req.body.options.c) {
reqOptions.push('-c');
}
// Adjust number of threads.
if ('t' in req.body.options && typeof req.body.options.t == 'number') {
nbThreads = req.body.options.t;
}
// Adjust exploration level.
if ('x' in req.body.options && typeof req.body.options.x == 'number') {
explorationLevel = req.body.options.x;
}
if ('l' in req.body.options && typeof req.body.options.l == 'number') {
reqOptions.push('-l ' + req.body.options.l);
}
}
reqOptions.push('-t ' + nbThreads);
reqOptions.push('-x ' + explorationLevel);
const timestamp = Math.floor(Date.now() / 1000); //eslint-disable-line
const fileName = args.logdir + '/' + timestamp + '_' + uuid.v1() + '.json';
try {
fs.writeFileSync(fileName, JSON.stringify(req.body));
} catch (err) {
console.error(now() + ': ' + err);
res.status(HTTP_INTERNALERROR_CODE);
res.send({
code: config.vroomErrorCodes.internal,
error: 'Internal error',
});
return;
}
reqOptions.push('-i ' + fileName);
const vroom = spawn(vroomCommand, reqOptions, {shell: true});
// Handle errors.
vroom.on('error', err => {
const message = ['Unknown internal error', err].join(': ');
console.error(now() + ': ' + JSON.stringify(message));
res.status(HTTP_INTERNALERROR_CODE);
res.send({
code: config.vroomErrorCodes.internal,
error: message,
});
});
vroom.stderr.on('data', data => {
console.error(now() + ': ' + data.toString());
});
// Handle solution. The temporary solution variable is required as
// we also want to adjust the status that is only retrieved with
// 'exit', after data is written in stdout.
let solution = '';
vroom.stdout.on('data', data => {
solution += data.toString();
});
vroom.on('close', (code, signal) => {
switch (code) {
case config.vroomErrorCodes.ok:
res.status(HTTP_OK_CODE);
break;
case config.vroomErrorCodes.internal:
// Internal error.
res.status(HTTP_INTERNALERROR_CODE);
break;
case config.vroomErrorCodes.input:
// Input error.
res.status(HTTP_ERROR_CODE);
break;
case config.vroomErrorCodes.routing:
// Routing error.
res.status(HTTP_INTERNALERROR_CODE);
break;
default:
// Required for e.g. vroom crash or missing command in $PATH.
res.status(HTTP_INTERNALERROR_CODE);
solution = {
code: config.vroomErrorCodes.internal,
error: 'Internal error',
};
}
res.send(solution);
if (fileExists(fileName)) {
fs.unlinkSync(fileName);
}
});
};
app.post(args.baseurl, [
sizeCheckCallback(args.maxlocations, args.maxvehicles),
execCallback,
]);
// set the health endpoint with some small problem
app.get(args.baseurl + 'health', (req, res) => {
const vroom = spawn(
vroomCommand,
['-i', './healthchecks/vroom_custom_matrix.json'],
{shell: true}
);
let msg = 'healthy';
let status = HTTP_OK_CODE;
vroom.on('error', () => {
// only called when vroom not in cliArgs.path or PATH
msg = 'vroom is not in $PATH, check cliArgs.path in config.yml';
status = HTTP_INTERNALERROR_CODE;
});
vroom.stderr.on('data', err => {
// called when vroom throws an error and sends the error message back
msg = err.toString();
status = HTTP_INTERNALERROR_CODE;
});
vroom.on('close', code => {
if (code !== config.vroomErrorCodes.ok) {
console.error(`${now()}: ${msg}`);
}
res.status(status).send();
});
});
const server = app.listen(args.port, () => {
console.log('vroom-express listening on port ' + args.port + '!');
});
server.setTimeout(args.timeout);