linny-r 1.1.0
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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/console.js +973 -0
- package/package.json +32 -0
- package/server.js +1547 -0
- package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
- package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
- package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
- package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
- package/static/fonts/Hack-Bold.ttf +0 -0
- package/static/fonts/Hack-BoldItalic.ttf +0 -0
- package/static/fonts/Hack-Italic.ttf +0 -0
- package/static/fonts/Hack-Regular.ttf +0 -0
- package/static/fonts/Lato-Bold.ttf +0 -0
- package/static/fonts/Lato-BoldItalic.ttf +0 -0
- package/static/fonts/Lato-Italic.ttf +0 -0
- package/static/fonts/Lato-Regular.ttf +0 -0
- package/static/fonts/mplus-1m-bold.ttf +0 -0
- package/static/fonts/mplus-1m-light.ttf +0 -0
- package/static/fonts/mplus-1m-medium.ttf +0 -0
- package/static/fonts/mplus-1m-regular.ttf +0 -0
- package/static/fonts/mplus-1m-thin.ttf +0 -0
- package/static/images/access.png +0 -0
- package/static/images/actor.png +0 -0
- package/static/images/actors.png +0 -0
- package/static/images/add-selector.png +0 -0
- package/static/images/add.png +0 -0
- package/static/images/back.png +0 -0
- package/static/images/black-box.png +0 -0
- package/static/images/by-sa.svg +74 -0
- package/static/images/cancel.png +0 -0
- package/static/images/chart.png +0 -0
- package/static/images/check-disab.png +0 -0
- package/static/images/check-off.png +0 -0
- package/static/images/check-on.png +0 -0
- package/static/images/check-x.png +0 -0
- package/static/images/clone.png +0 -0
- package/static/images/close.png +0 -0
- package/static/images/cluster.png +0 -0
- package/static/images/compare.png +0 -0
- package/static/images/compress.png +0 -0
- package/static/images/constraint.png +0 -0
- package/static/images/copy.png +0 -0
- package/static/images/data-to-clpbrd.png +0 -0
- package/static/images/dataset.png +0 -0
- package/static/images/delete.png +0 -0
- package/static/images/diagram.png +0 -0
- package/static/images/down.png +0 -0
- package/static/images/edit-chart.png +0 -0
- package/static/images/edit.png +0 -0
- package/static/images/eq.png +0 -0
- package/static/images/equation.png +0 -0
- package/static/images/experiment.png +0 -0
- package/static/images/favicon.ico +0 -0
- package/static/images/fewer-dec.png +0 -0
- package/static/images/filter.png +0 -0
- package/static/images/find.png +0 -0
- package/static/images/forward.png +0 -0
- package/static/images/host-logo.png +0 -0
- package/static/images/icon.png +0 -0
- package/static/images/icon.svg +23 -0
- package/static/images/ignore.png +0 -0
- package/static/images/include.png +0 -0
- package/static/images/info-to-clpbrd.png +0 -0
- package/static/images/info.png +0 -0
- package/static/images/is-black-box.png +0 -0
- package/static/images/lbl.png +0 -0
- package/static/images/lift.png +0 -0
- package/static/images/link.png +0 -0
- package/static/images/linny-r.icns +0 -0
- package/static/images/linny-r.ico +0 -0
- package/static/images/linny-r.png +0 -0
- package/static/images/linny-r.svg +21 -0
- package/static/images/logo.png +0 -0
- package/static/images/model-info.png +0 -0
- package/static/images/module.png +0 -0
- package/static/images/monitor.png +0 -0
- package/static/images/more-dec.png +0 -0
- package/static/images/ne.png +0 -0
- package/static/images/new.png +0 -0
- package/static/images/note.png +0 -0
- package/static/images/ok.png +0 -0
- package/static/images/open.png +0 -0
- package/static/images/outcome.png +0 -0
- package/static/images/parent.png +0 -0
- package/static/images/paste.png +0 -0
- package/static/images/pause.png +0 -0
- package/static/images/print-chart.png +0 -0
- package/static/images/print.png +0 -0
- package/static/images/process.png +0 -0
- package/static/images/product.png +0 -0
- package/static/images/pwlf.png +0 -0
- package/static/images/receiver.png +0 -0
- package/static/images/redo.png +0 -0
- package/static/images/remove.png +0 -0
- package/static/images/rename.png +0 -0
- package/static/images/repo-logo.png +0 -0
- package/static/images/repository.png +0 -0
- package/static/images/reset.png +0 -0
- package/static/images/resize.png +0 -0
- package/static/images/restore.png +0 -0
- package/static/images/save-chart.png +0 -0
- package/static/images/save-data.png +0 -0
- package/static/images/save-diagram.png +0 -0
- package/static/images/save.png +0 -0
- package/static/images/sensitivity.png +0 -0
- package/static/images/settings.png +0 -0
- package/static/images/solve.png +0 -0
- package/static/images/solver-logo.png +0 -0
- package/static/images/stats-to-clpbrd.png +0 -0
- package/static/images/stats.png +0 -0
- package/static/images/stop.png +0 -0
- package/static/images/store.png +0 -0
- package/static/images/stretch.png +0 -0
- package/static/images/table-to-clpbrd.png +0 -0
- package/static/images/table.png +0 -0
- package/static/images/tree.png +0 -0
- package/static/images/tudelft.png +0 -0
- package/static/images/ubl.png +0 -0
- package/static/images/undo.png +0 -0
- package/static/images/up.png +0 -0
- package/static/images/zoom-in.png +0 -0
- package/static/images/zoom-out.png +0 -0
- package/static/index.html +3088 -0
- package/static/linny-r.css +4722 -0
- package/static/scripts/iro.min.js +7 -0
- package/static/scripts/linny-r-config.js +105 -0
- package/static/scripts/linny-r-ctrl.js +1199 -0
- package/static/scripts/linny-r-gui.js +14814 -0
- package/static/scripts/linny-r-milp.js +286 -0
- package/static/scripts/linny-r-model.js +10405 -0
- package/static/scripts/linny-r-utils.js +687 -0
- package/static/scripts/linny-r-vm.js +7079 -0
- package/static/show-diff.html +84 -0
- package/static/show-png.html +113 -0
- package/static/sounds/error.wav +0 -0
- package/static/sounds/notification.wav +0 -0
- package/static/sounds/warning.wav +0 -0
package/server.js
ADDED
@@ -0,0 +1,1547 @@
|
|
1
|
+
/*
|
2
|
+
Linny-R is an executable graphical specification language for (mixed integer)
|
3
|
+
linear programming (MILP) problems, especially unit commitment problems (UCP).
|
4
|
+
The Linny-R language and tool have been developed by Pieter Bots at Delft
|
5
|
+
University of Technology, starting in 2009. The project to develop a browser-
|
6
|
+
based version started in 2017. See https://linny-r.org for more information.
|
7
|
+
|
8
|
+
This NodeJS script (linny-r-node.js) provides a minimalist local host web server
|
9
|
+
(URL http://127.0.0.1:5050) that will serve the Linny-R GUI (HTML, CSS,
|
10
|
+
and JavaScript files, and images), process the requests from the browser
|
11
|
+
that pass the MILP equation model to the solver, and then return the solution
|
12
|
+
to the Linny-R "virtual machine" that is running in the browser.
|
13
|
+
*/
|
14
|
+
/*
|
15
|
+
Copyright (c) 2020-2022 Delft University of Technology
|
16
|
+
|
17
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
|
+
of this software and associated documentation files (the "Software"), to deal
|
19
|
+
in the Software without restriction, including without limitation the rights
|
20
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
21
|
+
copies of the Software, and to permit persons to whom the Software is
|
22
|
+
furnished to do so, subject to the following conditions:
|
23
|
+
|
24
|
+
The above copyright notice and this permission notice shall be included in
|
25
|
+
all copies or substantial portions of the Software.
|
26
|
+
|
27
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
28
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
29
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
30
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
31
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
32
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
33
|
+
SOFTWARE.
|
34
|
+
*/
|
35
|
+
|
36
|
+
///////////////////////////////////////////////////////////////////////////////
|
37
|
+
// Please do not modify code unless you *really* know what you are doing //
|
38
|
+
///////////////////////////////////////////////////////////////////////////////
|
39
|
+
|
40
|
+
const
|
41
|
+
// The version number of this Linny-R server in Node.js
|
42
|
+
VERSION_NUMBER = '1.0.0',
|
43
|
+
|
44
|
+
// The URL of the official Linny-R website (with the most recent release)
|
45
|
+
PUBLIC_LINNY_R_URL = 'https://sysmod.tbm.tudelft.nl/linny-r',
|
46
|
+
|
47
|
+
// The current working directory (from where Node.js was started) is
|
48
|
+
// assumed to be the main directory
|
49
|
+
MAIN_DIRECTORY = process.cwd(),
|
50
|
+
|
51
|
+
// Get the required built-in Node.js modules
|
52
|
+
child_process = require('child_process'),
|
53
|
+
crypto = require('crypto'),
|
54
|
+
fs = require('fs'),
|
55
|
+
http = require('http'),
|
56
|
+
https = require('https'),
|
57
|
+
os = require('os'),
|
58
|
+
path = require('path'),
|
59
|
+
|
60
|
+
// Get the platform name (win32, macOS, linux) of the user's computer
|
61
|
+
PLATFORM = os.platform();
|
62
|
+
|
63
|
+
// Immediately output some configuration information to the console
|
64
|
+
console.log('\nNode.js server for Linny-R version', VERSION_NUMBER);
|
65
|
+
console.log('Node.js version:', process.version);
|
66
|
+
console.log('Platform:', PLATFORM, '(' + os.type() + ')');
|
67
|
+
console.log('Main directory:', MAIN_DIRECTORY);
|
68
|
+
|
69
|
+
// Only then require the Node.js modules that are not "built-in"
|
70
|
+
|
71
|
+
const
|
72
|
+
{ DOMParser } = checkNodeModule('@xmldom/xmldom');
|
73
|
+
|
74
|
+
function checkNodeModule(name) {
|
75
|
+
// Catches the error if Node.js module `name` is not available
|
76
|
+
try {
|
77
|
+
return require(name);
|
78
|
+
} catch(err) {
|
79
|
+
console.log(`ERROR: Node.js module "${name}" needs to be installed first`);
|
80
|
+
process.exit();
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
// Load class MILPSolver
|
85
|
+
const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
86
|
+
|
87
|
+
///////////////////////////////////////////////////////////////////////////////
|
88
|
+
// Code executed at start-up continues here //
|
89
|
+
///////////////////////////////////////////////////////////////////////////////
|
90
|
+
|
91
|
+
// Default settings are used unless these are overruled by arguments on the
|
92
|
+
// command line. Possible arguments are:
|
93
|
+
// - port=[number] will make the server listen at port [number]
|
94
|
+
// - solver=[name] will select solver [name], or warn if not found
|
95
|
+
// - workspace=[path] will create workspace in [path] instead of (main)/user
|
96
|
+
const SETTINGS = commandLineSettings();
|
97
|
+
|
98
|
+
// The workspace defines the paths to directories where Linny-R can write files
|
99
|
+
const WORKSPACE = createWorkspace();
|
100
|
+
|
101
|
+
// Initialize the solver
|
102
|
+
const SOLVER = new MILPSolver(SETTINGS, WORKSPACE);
|
103
|
+
|
104
|
+
// Create launch script
|
105
|
+
createLaunchScript();
|
106
|
+
|
107
|
+
verifyScriptFiles();
|
108
|
+
|
109
|
+
// Create the HTTP server
|
110
|
+
const SERVER = http.createServer((req, res) => {
|
111
|
+
const u = new URL(req.url, 'http://127.0.0.1:' + SETTINGS.port);
|
112
|
+
// When POST, first get all the full body
|
113
|
+
if(req.method === 'POST') {
|
114
|
+
let body = '';
|
115
|
+
req.on('data', (data) => body += data);
|
116
|
+
req.on('end', () => processRequest(req, res, u.pathname, body));
|
117
|
+
} else if(req.method === 'GET') {
|
118
|
+
processRequest(req, res, u.pathname, u.search);
|
119
|
+
}
|
120
|
+
});
|
121
|
+
|
122
|
+
// Start listening at the specified port number
|
123
|
+
console.log('Listening at: http://127.0.0.1:' + SETTINGS.port);
|
124
|
+
SERVER.listen(SETTINGS.port);
|
125
|
+
|
126
|
+
// Finally, launch the GUI if this command line argument is set
|
127
|
+
if(SETTINGS.launch) {
|
128
|
+
console.log('Launching Linny-R in the default browser');
|
129
|
+
const cmd = (PLATFORM.startsWith('win') ? 'start' : 'open');
|
130
|
+
child_process.exec(cmd + ' http://127.0.0.1:' + SETTINGS.port,
|
131
|
+
(error, stdout, stderr) => {
|
132
|
+
console.log('NOTICE: Failed to launch GUI in browser');
|
133
|
+
console.log(stdout);
|
134
|
+
console.log(stderr);
|
135
|
+
});
|
136
|
+
}
|
137
|
+
|
138
|
+
// Version check & update functionality
|
139
|
+
// ====================================
|
140
|
+
// This section of code implements server responses to requests made by the
|
141
|
+
// browser immediately after loading the GUI page (`index.html`), or when the
|
142
|
+
// user clicks on the link "Version ..." below the Linny-R logo in the upper
|
143
|
+
// left corner of the GUI page.
|
144
|
+
|
145
|
+
const
|
146
|
+
|
147
|
+
VERSION_MESSAGE = `<!DOCTYPE html>
|
148
|
+
<html lang="en-US">
|
149
|
+
<head>
|
150
|
+
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
151
|
+
<title>Linny-R version information</title>
|
152
|
+
<link rel="shortcut icon" type="image/png" href="../images/icon.png">
|
153
|
+
<style>
|
154
|
+
body {
|
155
|
+
font-family: sans-serif;
|
156
|
+
font-size: 16px;
|
157
|
+
}
|
158
|
+
#linny-r-logo {
|
159
|
+
height: 40px;
|
160
|
+
margin-bottom: -10px;
|
161
|
+
}
|
162
|
+
</style>
|
163
|
+
</head>
|
164
|
+
<body>
|
165
|
+
<img id="linny-r-logo" src="../images/logo.png">
|
166
|
+
%1%
|
167
|
+
</body>
|
168
|
+
</html>`,
|
169
|
+
|
170
|
+
NO_INTERNET_MESSAGE = `
|
171
|
+
<h3>Version check failed</h3>
|
172
|
+
<p>
|
173
|
+
No contact with the on-line Linny-R server --
|
174
|
+
please check your internet connection.
|
175
|
+
</p>`,
|
176
|
+
|
177
|
+
UP_TO_DATE_MESSAGE = `
|
178
|
+
<h3>Version JS-%1% is up-to-date</h3>
|
179
|
+
<p>Released on %2%</p>`,
|
180
|
+
|
181
|
+
DOWNLOAD_MESSAGE = `
|
182
|
+
<h3>Latest version is %1%</h3>
|
183
|
+
<p>Released on %2%</p>`,
|
184
|
+
|
185
|
+
SHUTDOWN_MESSAGE = `<!DOCTYPE html>
|
186
|
+
<html lang="en-US">
|
187
|
+
<head>
|
188
|
+
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
189
|
+
<title>Linny-R server shutdown</title>
|
190
|
+
<link rel="shortcut icon" type="image/png" href="../images/icon.png">
|
191
|
+
<style>
|
192
|
+
body {
|
193
|
+
font-family: sans-serif;
|
194
|
+
font-size: 15px;
|
195
|
+
}
|
196
|
+
</style>
|
197
|
+
</head>
|
198
|
+
<body>
|
199
|
+
<h3>Linny-R server (127.0.0.1) is shutting down</h3>
|
200
|
+
<p>To upgrade and/or restart Linny-R, please switch to your
|
201
|
+
${SETTINGS.cli_name} window and there at the prompt:
|
202
|
+
<p>To upgrade to a newer version of Linny-R, first type:</p>
|
203
|
+
<p> <tt>npm update linny-r</tt><p>
|
204
|
+
<p>To restart the server, type:</p>
|
205
|
+
<p> <tt>node server</tt></p>
|
206
|
+
<p>
|
207
|
+
Then switch back to this window, and click
|
208
|
+
<button type="button"
|
209
|
+
onclick="window.location.href = 'http://127.0.0.1:${SETTINGS.port}';">
|
210
|
+
Restart
|
211
|
+
</button>
|
212
|
+
</p>
|
213
|
+
</body>
|
214
|
+
</html>`;
|
215
|
+
|
216
|
+
|
217
|
+
function compareVersions(v1, v2) {
|
218
|
+
// Robust comparison of version numbers
|
219
|
+
nrs1 = (v1 + '.0.0.0').split('.');
|
220
|
+
nrs2 = (v2 + '.0.0.0').split('.');
|
221
|
+
for(i = 0; i < 4; i++) {
|
222
|
+
nrs1[i] = nrs1[i].padStart(6, '0');
|
223
|
+
nrs2[i] = nrs2[i].padStart(6, '0');
|
224
|
+
}
|
225
|
+
v1 = nrs1.slice(0, 4).join('.');
|
226
|
+
v2 = nrs2.slice(0, 4).join('.');
|
227
|
+
if(v1 > v2) return 1;
|
228
|
+
if(v1 < v2) return -1;
|
229
|
+
return 0;
|
230
|
+
}
|
231
|
+
|
232
|
+
function checkVersion(res, version) {
|
233
|
+
// Check whether current version is the most recent
|
234
|
+
console.log('Check version:', version);
|
235
|
+
if(!version) {
|
236
|
+
serveHTML(res, '<h3>No version number specified</h3>');
|
237
|
+
return;
|
238
|
+
}
|
239
|
+
version = version.split('-').pop();
|
240
|
+
getTextFromURL(PUBLIC_LINNY_R_URL + '/check-version/?info',
|
241
|
+
// The `on_ok` function
|
242
|
+
(data, res) => {
|
243
|
+
const
|
244
|
+
info = data.split('|');
|
245
|
+
// Should be [version, release date]
|
246
|
+
if(info.length === 2) {
|
247
|
+
if(compareVersions(version, info[0]) >= 0) {
|
248
|
+
message = UP_TO_DATE_MESSAGE.replace(
|
249
|
+
'%1%', info[0]).replace('%2%', info[1]);
|
250
|
+
} else {
|
251
|
+
message = DOWNLOAD_MESSAGE.replace(
|
252
|
+
'%1%', info[0]).replace('%2%', info[1]);
|
253
|
+
}
|
254
|
+
serveHTML(res, VERSION_MESSAGE.replace('%1%', message));
|
255
|
+
}
|
256
|
+
},
|
257
|
+
// The `on_error` function
|
258
|
+
(error, res) => {
|
259
|
+
console.log(error);
|
260
|
+
serveHTML(res, NO_INTERNET_MESSAGE);
|
261
|
+
},
|
262
|
+
// The response object
|
263
|
+
res);
|
264
|
+
}
|
265
|
+
|
266
|
+
function autoCheck(res) {
|
267
|
+
// Compares the version number in the static file `index.html`
|
268
|
+
// with the version number in the corresponding file on the official
|
269
|
+
// Linny-R website, and serves a status string that indicates whether
|
270
|
+
// a newer release is available
|
271
|
+
const gpath = path.join(MAIN_DIRECTORY, 'static', 'index.html');
|
272
|
+
// Read the globals script
|
273
|
+
fs.readFile(gpath, 'utf8', (err, data) => {
|
274
|
+
let v_match = null;
|
275
|
+
if(err) {
|
276
|
+
console.log('WARNING: Failed to read file', gpath);
|
277
|
+
} else {
|
278
|
+
// Extract the version number
|
279
|
+
v_match = data.match(/LINNY_R_VERSION = '(.+?)'/);
|
280
|
+
if(!v_match) console.log('WARNING: No version number found');
|
281
|
+
}
|
282
|
+
if(!v_match) {
|
283
|
+
servePlainText(res,'no version');
|
284
|
+
return;
|
285
|
+
}
|
286
|
+
let version = v_match[1];
|
287
|
+
// Get the current `index.html` file from the official Linny-R server
|
288
|
+
getTextFromURL(PUBLIC_LINNY_R_URL + '/check-version/?info',
|
289
|
+
// The `on_ok` function: compare versions and return status
|
290
|
+
(data, res) => {
|
291
|
+
let check = 'no match';
|
292
|
+
const
|
293
|
+
info = data.split('|');
|
294
|
+
// Should be [version, release date]
|
295
|
+
if(info.length === 2) {
|
296
|
+
if(compareVersions(version, info[0]) >= 0) {
|
297
|
+
check = 'up-to-date';
|
298
|
+
} else {
|
299
|
+
check = info[0] + '|' + info[1];
|
300
|
+
}
|
301
|
+
}
|
302
|
+
servePlainText(res, check);
|
303
|
+
},
|
304
|
+
// The `on_error` function
|
305
|
+
(error, res) => {
|
306
|
+
console.log(error);
|
307
|
+
servePlainText(res, 'no match');
|
308
|
+
},
|
309
|
+
// The response object
|
310
|
+
res);
|
311
|
+
});
|
312
|
+
}
|
313
|
+
|
314
|
+
// Repository functionality
|
315
|
+
// ========================
|
316
|
+
// For repository services, the Linny-R JavaScript application communicates with
|
317
|
+
// the server via calls to the server like $.post('repo', x) where x is a JSON
|
318
|
+
// object with at least the entry `action`, which can be one of the following:
|
319
|
+
// id return the repository URL (for this script: 'local host')
|
320
|
+
// list return list with names of repositories available on the server
|
321
|
+
// add add repository (name + url) to the repository list (if allowed)
|
322
|
+
// remove remove repository (by name) from the repository list (if allowed)
|
323
|
+
// dir return list with names of modules in the named repository
|
324
|
+
// load return the specified file content from the named repository
|
325
|
+
// access obtain write access for the named repository (requires valid token)
|
326
|
+
// store write XML content to the specified file in the named repository
|
327
|
+
// delete delete the specified module file from the named repository
|
328
|
+
|
329
|
+
function repo(res, sp) {
|
330
|
+
// Processes all repository commands
|
331
|
+
const action = sp.get('action').trim();
|
332
|
+
console.log('Repository action:', action);
|
333
|
+
if(action === 'id') return repoID(res);
|
334
|
+
if(action === 'list') return repoList(res);
|
335
|
+
if(action === 'add') return repoAdd(res, sp);
|
336
|
+
const repo = sp.get('repo').trim();
|
337
|
+
if(action === 'remove') return repoRemove(res, repo);
|
338
|
+
if(action === 'dir') return repoDir(res, repo);
|
339
|
+
if(action === 'access') return repoAccess(res, repo, sp.get('token'));
|
340
|
+
const file = sp.get('file').trim();
|
341
|
+
if(action === 'info') return repoInfo(res, repo, file);
|
342
|
+
if(action === 'load') return repoLoad(res, repo, file);
|
343
|
+
if(action === 'store') return repoStore(res, repo, file, sp.get('xml'));
|
344
|
+
if(action === 'delete') return repoDelete(res, repo, file);
|
345
|
+
// Fall-through: report error
|
346
|
+
servePlainText(res, `ERROR: Invalid action: "${action}"`);
|
347
|
+
}
|
348
|
+
|
349
|
+
function asFileName(s) {
|
350
|
+
// Returns string `s` with whitespace converted to a single dash, and special
|
351
|
+
// characters converted to underscores
|
352
|
+
s = s.trim().replace(/[\s\-]+/g, '-');
|
353
|
+
return s.replace(/[^A-Za-z0-9_\-]/g, '_');
|
354
|
+
}
|
355
|
+
|
356
|
+
function repositoryByName(name) {
|
357
|
+
// Returns array [name, url, token] if `name` found in file `repository.cfg`
|
358
|
+
repo_list = fs.readFileSync(WORKSPACE.repositories, 'utf8').split('\n');
|
359
|
+
for(let i = 0; i < repo_list.length; i++) {
|
360
|
+
rbn = repo_list[i].trim().split('|');
|
361
|
+
while(rbn.length < 2) rbn.push('');
|
362
|
+
if(rbn[0] === name) return rbn;
|
363
|
+
}
|
364
|
+
console.log(`ERROR: Repository "${name}" not found`);
|
365
|
+
return false;
|
366
|
+
}
|
367
|
+
|
368
|
+
function repoId(res) {
|
369
|
+
// Returns the URL of this repository server
|
370
|
+
// NOTE: this local WSGI server should return 'local host'
|
371
|
+
servePlainText(res, 'local host');
|
372
|
+
}
|
373
|
+
|
374
|
+
function repoList(res) {
|
375
|
+
// Returns name list of registered repositories
|
376
|
+
// NOTE: on a local Linny-R server, the first name is always 'local host'
|
377
|
+
let repo_list = 'local host';
|
378
|
+
try {
|
379
|
+
if(!fs.existsSync(WORKSPACE.repositories)) {
|
380
|
+
fs.writeFileSync(WORKSPACE.repositories, repo_list);
|
381
|
+
}
|
382
|
+
repo_list = fs.readFileSync(WORKSPACE.repositories, 'utf8').split('\n');
|
383
|
+
// Return only the names!
|
384
|
+
for(let i = 0; i < repo_list.length; i++) {
|
385
|
+
const r = repo_list[i].trim().split('|');
|
386
|
+
repo_list[i] = r[0];
|
387
|
+
// Add a + to indicate that storing is permitted
|
388
|
+
if(r[0] === 'local host' || (r.length > 2 && r[2])) repo_list[i] += '+';
|
389
|
+
}
|
390
|
+
repo_list = repo_list.join('\n');
|
391
|
+
} catch(err) {
|
392
|
+
console.log('ERROR: Failed to access repository -- ' + err.message);
|
393
|
+
}
|
394
|
+
servePlainText(res, repo_list);
|
395
|
+
}
|
396
|
+
|
397
|
+
function repoAdd(res, sp) {
|
398
|
+
// Registers a remote repository on this local Linny-R server
|
399
|
+
let rname = sp.get('repo');
|
400
|
+
if(rname) rname = rname.trim();
|
401
|
+
if(!rname) return servePlainText(res, 'WARNING: Invalid name');
|
402
|
+
// Get URL without trailing slashes
|
403
|
+
let url = sp.get('url');
|
404
|
+
url = 'https://' + (url ? url.trim() : '');
|
405
|
+
let i = url.length - 1;
|
406
|
+
while(url[i] === '/') i--;
|
407
|
+
url = url.substring(0, i);
|
408
|
+
try {
|
409
|
+
test = new URL(url);
|
410
|
+
} catch(err) {
|
411
|
+
return servePlainText(res, 'WARNING: Invalid URL');
|
412
|
+
}
|
413
|
+
// Error callback function is used twice, so define it here
|
414
|
+
const noConnection = (error, res) => {
|
415
|
+
console.log(error);
|
416
|
+
servePlainText(res, 'ERROR: Failed to connect to ' + url);
|
417
|
+
};
|
418
|
+
// Verify that the URL points to a Linny-R repository
|
419
|
+
postRequest(url, {action: 'id'},
|
420
|
+
// The `on_ok` function
|
421
|
+
(data, res) => {
|
422
|
+
// Response should be the URL of the repository
|
423
|
+
if(data !== url) {
|
424
|
+
servePlainText(res, 'WARNING: Not a Linny-R repository');
|
425
|
+
return;
|
426
|
+
}
|
427
|
+
// If so, append name|url|token to the configuration file
|
428
|
+
// NOTE: token is optional
|
429
|
+
let token = sp.get('token');
|
430
|
+
if(token) token = token.trim();
|
431
|
+
if(token) {
|
432
|
+
postRequest(url, {action: 'access', repo: rname, token: token},
|
433
|
+
// The `on_ok` function
|
434
|
+
(data, res) => {
|
435
|
+
if(data !== 'Authenticated') {
|
436
|
+
servePlainText(res, data);
|
437
|
+
return;
|
438
|
+
}
|
439
|
+
list = fs.readFileSync(
|
440
|
+
WORKSPACE.repositories, 'utf8').split('\n');
|
441
|
+
for(let i = 0; i < list.length; i++) {
|
442
|
+
nu = entry.trim().split('|');
|
443
|
+
if(nu[0] !== 'local host') {
|
444
|
+
if(nu[0] == rname) {
|
445
|
+
servePlainText(res,
|
446
|
+
`WARNING: Repository name "${rname}" already in use`);
|
447
|
+
return;
|
448
|
+
}
|
449
|
+
if(nu[1] === url) {
|
450
|
+
servePlainText(res,
|
451
|
+
`WARNING: Repository already registered as "${nu[0]}"`);
|
452
|
+
return;
|
453
|
+
}
|
454
|
+
}
|
455
|
+
}
|
456
|
+
list.push([rname, url, token].join('|'));
|
457
|
+
fs.writeFileSync(WORKSPACE.repositories, list.join('\n'));
|
458
|
+
servePlainText(res, rname);
|
459
|
+
},
|
460
|
+
// The `on_error` function and the response object
|
461
|
+
noConnection, res);
|
462
|
+
}
|
463
|
+
},
|
464
|
+
// The `on_error` function and the response object
|
465
|
+
noConnection, res);
|
466
|
+
}
|
467
|
+
|
468
|
+
function repoRemove(res, rname) {
|
469
|
+
// Removes a registered repository from the repository configuration file
|
470
|
+
if(rname === 'local host') {
|
471
|
+
servePlainText(res, 'ERROR: Cannot remove local host');
|
472
|
+
return;
|
473
|
+
}
|
474
|
+
try {
|
475
|
+
// Read list of repositories registered on this local host server
|
476
|
+
list = fs.readFileSync(WORKSPACE.repositories, 'utf8').split('\n');
|
477
|
+
// Look for a repository called `rname`
|
478
|
+
let index = -1;
|
479
|
+
for(let i = 0; i < list.length; i++) {
|
480
|
+
const nu = list[i].trim().split('|');
|
481
|
+
if(nu[0] === rname) {
|
482
|
+
index = i;
|
483
|
+
break;
|
484
|
+
}
|
485
|
+
}
|
486
|
+
if(index < 0) {
|
487
|
+
// Not found => cannot remove
|
488
|
+
servePlainText(res, `ERROR: Repository "${rname}" not found`);
|
489
|
+
} else {
|
490
|
+
// Remove from list and save it to file `repository.cfg`
|
491
|
+
list.splice(index, 1);
|
492
|
+
fs.writeFileSync(WORKSPACE.repositories, list.join('\n'));
|
493
|
+
// Return the name to indicate "successfully removed"
|
494
|
+
servePlainText(res, rname);
|
495
|
+
}
|
496
|
+
} catch(err) {
|
497
|
+
console.log(err);
|
498
|
+
servePlainText(res, `ERROR: Failed to remove "${rname}"`);
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
function repoDir(res, rname) {
|
503
|
+
// Returns a newline-separated list of names of the modules stored in the
|
504
|
+
// specified repository
|
505
|
+
const mlist = [];
|
506
|
+
if(rname === 'local host') {
|
507
|
+
// Return list of base filenames of Linny-R models in `modules` directory
|
508
|
+
const flist = fs.readdirSync(WORKSPACE.modules);
|
509
|
+
for(let i = 0; i < flist.length; i++) {
|
510
|
+
const pp = path.parse(flist[i]);
|
511
|
+
// Only add Linny-R model files (.lnr) without this extension
|
512
|
+
if(pp.ext === '.lnr') mlist.push(pp.name);
|
513
|
+
}
|
514
|
+
servePlainText(res, mlist.join('\n'));
|
515
|
+
} else {
|
516
|
+
// Get list from remote server
|
517
|
+
const r = repositoryByName(rname);
|
518
|
+
if(r) {
|
519
|
+
postRequest(r[1], {action: 'dir', repo: r[0]},
|
520
|
+
// The `on_ok` function
|
521
|
+
(data, res) => servePlainText(res, data),
|
522
|
+
// The `on_error` function
|
523
|
+
(error, res) => {
|
524
|
+
console.log(error);
|
525
|
+
servePlainText(res,
|
526
|
+
`ERROR: Failed to access remote repository "${rname}"`);
|
527
|
+
},
|
528
|
+
res);
|
529
|
+
} else {
|
530
|
+
servePlainText(res, `ERROR: Repository "${rname}" not registered`);
|
531
|
+
}
|
532
|
+
}
|
533
|
+
}
|
534
|
+
|
535
|
+
function repoInfo(res, rname, mname) {
|
536
|
+
// Returns the documentation (<notes> in XML) of the requested model file
|
537
|
+
// if found in the specified repository
|
538
|
+
// NOTE: the function `serveNotes` is called when the XML text has been
|
539
|
+
// retrieved either from a local file or from a remote repository URL
|
540
|
+
const serveNotes = (res, xml) => {
|
541
|
+
// Parse XML string
|
542
|
+
try {
|
543
|
+
const parser = new DOMParser();
|
544
|
+
xml = parser.parseFromString(xml, 'text/xml');
|
545
|
+
const de = xml.documentElement;
|
546
|
+
// Linny-R model must contain a model node
|
547
|
+
if(de.nodeName !== 'model') throw 'XML document has no model element';
|
548
|
+
let notes = '';
|
549
|
+
// The XML will contain many "notes" elements; only consider child
|
550
|
+
// nodes of the "model" element
|
551
|
+
for(let i = 0; i < de.childNodes.length; i++) {
|
552
|
+
const ce = de.childNodes[i];
|
553
|
+
// NOTE: node text content is stored as a child node
|
554
|
+
if(ce.nodeName === 'notes' && ce.childNodes.length > 0) {
|
555
|
+
notes = ce.childNodes[0].nodeValue;
|
556
|
+
break;
|
557
|
+
}
|
558
|
+
}
|
559
|
+
servePlainText(res, notes);
|
560
|
+
} catch(err) {
|
561
|
+
console.log(err);
|
562
|
+
console.log('XML', xml);
|
563
|
+
servePlainText(res, 'ERROR: Failed to parse XML of Linny-R model');
|
564
|
+
}
|
565
|
+
};
|
566
|
+
// See where to obtain the XML
|
567
|
+
if(rname === 'local host') {
|
568
|
+
// NOTE: file name includes version number but not the extension
|
569
|
+
fs.readFile(path.join(WORKSPACE.modules, mname + '.lnr'), 'utf8',
|
570
|
+
(err, data) => {
|
571
|
+
if(err) {
|
572
|
+
console.log(err);
|
573
|
+
servePlainText(res, 'ERROR: Failed to read model file');
|
574
|
+
} else {
|
575
|
+
serveNotes(res, data);
|
576
|
+
}
|
577
|
+
});
|
578
|
+
} else {
|
579
|
+
// Get file from remote server
|
580
|
+
r = repositoryByName(rname);
|
581
|
+
if(r) {
|
582
|
+
postRequest(r[1], {action: 'load', repo: r[0], file: mname},
|
583
|
+
// The `on_ok` function
|
584
|
+
(data, res) => serveNotes(res, data.toString()),
|
585
|
+
// The `on_error` function
|
586
|
+
(error, res) => {
|
587
|
+
console.log(error);
|
588
|
+
servePlainText(res, 'ERROR: Failed to download model file');
|
589
|
+
},
|
590
|
+
res);
|
591
|
+
} else {
|
592
|
+
servePlainText(res, `ERROR: Repository "${rname}" not registered`);
|
593
|
+
}
|
594
|
+
}
|
595
|
+
}
|
596
|
+
|
597
|
+
function repoLoad(res, rname, mname, pipe=null) {
|
598
|
+
// Returns the requested model file if found in the specified repository
|
599
|
+
// The optional function pipe(res, xml) allows pass ingon the loaded XML
|
600
|
+
if(rname === 'local host') {
|
601
|
+
// NOTE: file name includes version number but not the extension
|
602
|
+
fs.readFile(path.join(WORKSPACE.modules, mname + '.lnr'), 'utf8',
|
603
|
+
(err, data) => {
|
604
|
+
if(err) {
|
605
|
+
console.log(err);
|
606
|
+
servePlainText(res, 'ERROR: Failed to read model file');
|
607
|
+
} else if(pipe) {
|
608
|
+
pipe(res, data);
|
609
|
+
} else {
|
610
|
+
servePlainText(res, data);
|
611
|
+
}
|
612
|
+
});
|
613
|
+
} else {
|
614
|
+
// Get file from remote server
|
615
|
+
r = repositoryByName(rname);
|
616
|
+
if(r) {
|
617
|
+
postRequest(r[1], {action: 'load', repo: r[0], file: mname},
|
618
|
+
// The `on_ok` function
|
619
|
+
(data, res) => {
|
620
|
+
if(pipe) {
|
621
|
+
pipe(res, data.toString());
|
622
|
+
} else {
|
623
|
+
servePlainText(res, data.toString());
|
624
|
+
}
|
625
|
+
},
|
626
|
+
// The `on_error` function
|
627
|
+
(error, res) => {
|
628
|
+
console.log(error);
|
629
|
+
servePlainText(res, 'ERROR: Failed to download model file');
|
630
|
+
},
|
631
|
+
res);
|
632
|
+
} else {
|
633
|
+
servePlainText(res, `ERROR: Repository "${rname}" not registered`);
|
634
|
+
}
|
635
|
+
}
|
636
|
+
}
|
637
|
+
|
638
|
+
function repoAccess(res, rname, rtoken) {
|
639
|
+
// Requests write access for a remote repository
|
640
|
+
r = repositoryByName(rname);
|
641
|
+
if(!r) {
|
642
|
+
servePlainText(`ERROR: Repository "${rname}" not registered`);
|
643
|
+
return;
|
644
|
+
}
|
645
|
+
postRequest(r[1], {action: 'access', repo: r[0], token: rtoken},
|
646
|
+
// The `on_ok` function
|
647
|
+
(data, res) => {
|
648
|
+
if(data !== 'Authenticated') {
|
649
|
+
servePlainText(res, data);
|
650
|
+
} else {
|
651
|
+
try {
|
652
|
+
// Read the list of repositories from the repository config file
|
653
|
+
const
|
654
|
+
list = fs.readFileSync(
|
655
|
+
WORKSPACE.repositories, 'utf8').split('\n'),
|
656
|
+
new_list = [];
|
657
|
+
for(let i = 0; i < list.length; i++) {
|
658
|
+
const nu = list[i].trim().split('|');
|
659
|
+
if(nu[0] === rname) {
|
660
|
+
// Add or replace the token
|
661
|
+
if(nu.length === 2) {
|
662
|
+
nu.push(rtoken);
|
663
|
+
} else if(nu.length === 3) {
|
664
|
+
nu[2] = rtoken;
|
665
|
+
}
|
666
|
+
}
|
667
|
+
new_list.push(nu.join('|'));
|
668
|
+
}
|
669
|
+
fs.writeFileSync(new_list.join('\n'));
|
670
|
+
servePlainText(res, `Authenticated for <b>${rname}</b>`);
|
671
|
+
} catch(err) {
|
672
|
+
console.log(err);
|
673
|
+
servePlainText(res, `ERROR: Failed to set token for "${rname}"`);
|
674
|
+
}
|
675
|
+
}
|
676
|
+
},
|
677
|
+
// The `on_error` function
|
678
|
+
(error, res) => {
|
679
|
+
console.log(error);
|
680
|
+
servePlainText(res, 'ERROR: Failed to connect to' + r[1]);
|
681
|
+
},
|
682
|
+
res);
|
683
|
+
}
|
684
|
+
|
685
|
+
function repoStore(res, rname, mname, mxml) {
|
686
|
+
// Stores the posted model in the specified repository
|
687
|
+
// NOTE: file name must not contain spaces or special characters
|
688
|
+
mname = asFileName(mname);
|
689
|
+
// Validate XML as a Linny-R model
|
690
|
+
let valid = false;
|
691
|
+
try {
|
692
|
+
const
|
693
|
+
parser = new DOMParser(),
|
694
|
+
doc = parser.parseFromString(mxml, 'text/xml');
|
695
|
+
root = doc.documentElement;
|
696
|
+
// Linny-R model have a model element as root
|
697
|
+
if(root.nodeName !== 'model') throw 'XML document has no model element';
|
698
|
+
valid = true;
|
699
|
+
} catch(err) {
|
700
|
+
console.log(err);
|
701
|
+
servePlainText(res, 'ERROR: Not a Linny-R model');
|
702
|
+
return;
|
703
|
+
}
|
704
|
+
if(rname === 'local host') {
|
705
|
+
// Always allow storing on local host
|
706
|
+
try {
|
707
|
+
// NOTE: first find latest version (if any)
|
708
|
+
const re = new RegExp('^' + mname + '-(\\d+).lnr');
|
709
|
+
// NOTE: Version numbers start at 1
|
710
|
+
let version = 0;
|
711
|
+
const list = fs.readdirSync(WORKSPACE.modules);
|
712
|
+
for(let i = 0; i < list.length; i++) {
|
713
|
+
const match = list[i].match(re);
|
714
|
+
if(match && match.length > 1) {
|
715
|
+
// File name equal to model name plus version number => get the number
|
716
|
+
version = Math.max(version, parseInt(match[1]));
|
717
|
+
}
|
718
|
+
}
|
719
|
+
mname += `-${version + 1}.lnr`;
|
720
|
+
fs.writeFileSync(path.join(WORKSPACE.modules, mname), mxml);
|
721
|
+
servePlainText(res, `Model stored as <tt>${mname}</tt>`);
|
722
|
+
} catch(err) {
|
723
|
+
console.log(err);
|
724
|
+
servePlainText(res, 'ERROR: Failed to write file');
|
725
|
+
}
|
726
|
+
} else {
|
727
|
+
// Otherwise, post file with token
|
728
|
+
r = repositoryByName(rname);
|
729
|
+
if(r) {
|
730
|
+
postRequest(r[1],
|
731
|
+
{action: 'store', repo: rname, file: mname, xml: mxml, token: r[2]},
|
732
|
+
// The `on_ok` function: serve the data sent by the remote server
|
733
|
+
(data, res) => servePlainText(res, data),
|
734
|
+
// The `on_error` function
|
735
|
+
(error, res) => {
|
736
|
+
console.log(error);
|
737
|
+
servePlainText(res, 'ERROR: Failed to connect to' + r[1]);
|
738
|
+
},
|
739
|
+
res);
|
740
|
+
} else {
|
741
|
+
servePlainText(res, `ERROR: Repository "${rname}" not registered`);
|
742
|
+
}
|
743
|
+
}
|
744
|
+
}
|
745
|
+
|
746
|
+
function repoDelete(res, name, file) {
|
747
|
+
// Deletes the specified module from the specified repository
|
748
|
+
// NOTE: this works only on the "local host" repository on this server
|
749
|
+
if(name === 'local host') {
|
750
|
+
// Delete specified model file
|
751
|
+
// NOTE: file name includes version number but not the extension
|
752
|
+
try {
|
753
|
+
fs.unlinkSync(path.join(WORKSPACE.modules, file + '.lnr'));
|
754
|
+
servePlainText(res,
|
755
|
+
`Module <tt>${file}</tt> removed from <strong>${name}</strong>`);
|
756
|
+
} catch(err) {
|
757
|
+
console.log(err);
|
758
|
+
servePlainText(resp, 'ERROR: Failed to delete file');
|
759
|
+
}
|
760
|
+
} else {
|
761
|
+
servePlainText(res, 'Cannot delete modules from a remote repository');
|
762
|
+
}
|
763
|
+
}
|
764
|
+
|
765
|
+
// Remote dataset functionality
|
766
|
+
// ============================
|
767
|
+
// This code section implements the retrieval of time series data from the URL
|
768
|
+
// or file path (on local host) when such a URL or path is specified in the
|
769
|
+
// Dataset dialog
|
770
|
+
|
771
|
+
function anyOSpath(p) {
|
772
|
+
// Helper function that converts Unix path notation (with slashes) to
|
773
|
+
// Windows notation if needed
|
774
|
+
if(p.indexOf('/') < 0) return p;
|
775
|
+
p = p.split('/');
|
776
|
+
// On macOS machines, paths start with a slash, so first substring is empty
|
777
|
+
if(p[0].length === 0) {
|
778
|
+
// In that case, add the leading slash
|
779
|
+
return '/' + path.join(...p);
|
780
|
+
} else if(p[0].endsWith(':') && path.sep === '\\') {
|
781
|
+
// On Windows machines, add a backslash after the disk (if specified)
|
782
|
+
path[0] += path.sep;
|
783
|
+
}
|
784
|
+
// Reassemble path for the OS of this machine
|
785
|
+
return path.join(...p);
|
786
|
+
}
|
787
|
+
|
788
|
+
function loadData(res, url) {
|
789
|
+
// Passed parameter is the URL or full path
|
790
|
+
console.log('Load data from', url);
|
791
|
+
if(!url) servePlainText(res, 'ERROR: No URL or path');
|
792
|
+
if(url.toLowerCase().startsWith('http')) {
|
793
|
+
// URL => validate it, and then try to download its content as text
|
794
|
+
try {
|
795
|
+
new URL(url); // Will throw an error if URL is not valid
|
796
|
+
getTextFromURL(url,
|
797
|
+
(data, res) => servePlainText(res, data),
|
798
|
+
(error, res) => servePlainText(res,
|
799
|
+
`ERROR: Failed to get data from <tt>${url}</tt>`),
|
800
|
+
res);
|
801
|
+
} catch(err) {
|
802
|
+
console.log(err);
|
803
|
+
servePlainText(res, `ERROR: Invalid URL <tt>${url}</tt>`);
|
804
|
+
}
|
805
|
+
} else {
|
806
|
+
const fp = anyOSpath(url);
|
807
|
+
fs.readFile(fp, 'utf8', (err, data) => {
|
808
|
+
if(err) {
|
809
|
+
console.log(err);
|
810
|
+
servePlainText(res, `ERROR: Could not read file <tt>${fp}</tt>`);
|
811
|
+
} else {
|
812
|
+
servePlainText(res, data);
|
813
|
+
}
|
814
|
+
});
|
815
|
+
}
|
816
|
+
}
|
817
|
+
|
818
|
+
// Receiver functionality
|
819
|
+
// ======================
|
820
|
+
// Respond to Linny-R receiver actions:
|
821
|
+
// listen - look for a Linny-r model file in the channel directory, and run it
|
822
|
+
// abort - write message to file <original model file name>-abort.txt
|
823
|
+
// report - write data and statistics on all chart variables as two text files
|
824
|
+
// having names <original model file name>-data.txt and -stats.txt,
|
825
|
+
// respectively
|
826
|
+
// call-back - delete model file (to prevent running it again), and then execute
|
827
|
+
// the call-back Python script specified for the channel
|
828
|
+
|
829
|
+
function receiver(res, sp) {
|
830
|
+
//This function processes all receiver actions
|
831
|
+
let
|
832
|
+
rpath = anyOSpath(sp.get('path') || ''),
|
833
|
+
rfile = anyOSpath(sp.get('file') || '');
|
834
|
+
// Assume that path is relative tochannel directory unless it starts with
|
835
|
+
// a (back)slash or specifiess drive or volume
|
836
|
+
if(!(rpath.startsWith(path.sep) || rpath.indexOf(':') >= 0 ||
|
837
|
+
rpath.startsWith(MAIN_DIRECTORY))) {
|
838
|
+
rpath = path.join(MAIN_DIRECTORY, rpath);
|
839
|
+
}
|
840
|
+
// Verify that the channel path exists
|
841
|
+
try {
|
842
|
+
fs.opendirSync(rpath);
|
843
|
+
} catch(err) {
|
844
|
+
console.log(err);
|
845
|
+
servePlainText(res, `ERROR: No channel path (${rpath})`);
|
846
|
+
return;
|
847
|
+
}
|
848
|
+
// Get the action from the search parameters
|
849
|
+
const action = sp.get('action');
|
850
|
+
console.log('Receiver action:', action, rpath, rfile);
|
851
|
+
if(action === 'listen') {
|
852
|
+
rcvrListen(res, rpath);
|
853
|
+
} else if(action === 'abort') {
|
854
|
+
rcvrAbort(res, rpath, rfile, sp.get('log') || 'NO EVENT LOG');
|
855
|
+
} else if(action === 'report') {
|
856
|
+
let run = sp.get('run');
|
857
|
+
// Zero-pad run number to permit sorting run report file names in sequence
|
858
|
+
run = (run ? '-' + run.padStart(3, '0') : '');
|
859
|
+
let data = sp.get('data') || '',
|
860
|
+
stats = sp.get('stats') || '',
|
861
|
+
log = sp.get('log') || 'NO EVENT LOG';
|
862
|
+
rcvrReport(res, rpath, rfile, run, data, stats, log);
|
863
|
+
} else if(action === 'call-back') {
|
864
|
+
rcvrCallBack(res, rpath, rfile, sp.get('script') || '');
|
865
|
+
} else {
|
866
|
+
servePlainText(res, `ERROR: Invalid action: "${action}"`);
|
867
|
+
}
|
868
|
+
}
|
869
|
+
|
870
|
+
function rcvrListen(res, rpath) {
|
871
|
+
// "Listens" at the channel, i.e., looks for work to do
|
872
|
+
let mdl = '',
|
873
|
+
cmd = '';
|
874
|
+
try {
|
875
|
+
// Look for a model file and/or a command file in the channel directory
|
876
|
+
const flist = fs.readdirSync(rpath);
|
877
|
+
// NOTE: `flist` contains file names relative to `rpath`
|
878
|
+
for(let i = 0; i < flist.length; i++) {
|
879
|
+
const f = path.parse(flist[i]);
|
880
|
+
if(f.ext === '.lnr' && !mdl) mdl = flist[i];
|
881
|
+
if(f.ext === '.lnrc' && !cmd) cmd = flist[i];
|
882
|
+
}
|
883
|
+
} catch(err) {
|
884
|
+
console.log(err);
|
885
|
+
servePlainText(res, `ERROR: Failed to get file list from <tt>${rpath}</tt>`);
|
886
|
+
return;
|
887
|
+
}
|
888
|
+
// Model files take precedence over command files
|
889
|
+
if(mdl) {
|
890
|
+
fs.readFile(path.join(rpath, mdl), 'utf8', (err, data) => {
|
891
|
+
if(err) {
|
892
|
+
console.log(err);
|
893
|
+
servePlainText(res, `ERROR: Failed to read model <tt>${mdl}</tt>`);
|
894
|
+
} else {
|
895
|
+
serveJSON(res, {file: path.parse(mdl).name, model: data});
|
896
|
+
}
|
897
|
+
});
|
898
|
+
return;
|
899
|
+
}
|
900
|
+
if(cmd) {
|
901
|
+
try {
|
902
|
+
cmd = fs.readFileSync(path.join(rpath, cmd), 'utf8').trim();
|
903
|
+
} catch(err) {
|
904
|
+
console.log(err);
|
905
|
+
servePlainText(res, `ERROR: Failed to read command file <tt>${cmd}</tt>`);
|
906
|
+
}
|
907
|
+
// Special command to deactivate the receiver
|
908
|
+
if(cmd === 'STOP LISTENING') {
|
909
|
+
serveJSON(res, {stop: 1});
|
910
|
+
} else {
|
911
|
+
// For now, command can only be
|
912
|
+
// "[experiment name|]module name[@repository name]"
|
913
|
+
let m = '',
|
914
|
+
r = '',
|
915
|
+
x = '';
|
916
|
+
const m_r = cmd.split('@');
|
917
|
+
// Repository `r` is local host unless specified
|
918
|
+
if(m_r.length === 2) {
|
919
|
+
r = m_r[1];
|
920
|
+
} else if(m_r.length === 1) {
|
921
|
+
r = 'local host';
|
922
|
+
} else {
|
923
|
+
// Multiple occurrences of @
|
924
|
+
servePlainText(res, `ERROR: Invalid command <tt>${cmd}</tt>`);
|
925
|
+
return;
|
926
|
+
}
|
927
|
+
m = m_r[0];
|
928
|
+
// Module `m` can be prefixed by an experiment title
|
929
|
+
const x_m = m.split('|');
|
930
|
+
if(x_m.length === 2) {
|
931
|
+
x = x_m[0];
|
932
|
+
m = x_m[1];
|
933
|
+
}
|
934
|
+
// Call repoLoad with its callback function to get the model XML
|
935
|
+
repoLoad(res, r.trim(), m.trim(), (res, xml) => serveJSON(res,
|
936
|
+
{file: path.parse(cmd).name, model: xml, experiment: x.trim()}));
|
937
|
+
}
|
938
|
+
} else {
|
939
|
+
// Empty fields will be interpreted as "nothing to do"
|
940
|
+
serveJSON(res, {file: '', model: '', experiment: ''});
|
941
|
+
}
|
942
|
+
}
|
943
|
+
|
944
|
+
function rcvrAbort(res, rpath, rfile, log) {
|
945
|
+
const log_path = path.join(rpath, rfile + '-log.txt');
|
946
|
+
fs.writeFile(log_path, log, (err) => {
|
947
|
+
if(err) {
|
948
|
+
console.log(err);
|
949
|
+
servePlainText(res,
|
950
|
+
`ERROR: Failed to write event log to file <tt>${log_path}</tt>`);
|
951
|
+
} else {
|
952
|
+
servePlainText(res, 'Remote run aborted');
|
953
|
+
}
|
954
|
+
});
|
955
|
+
}
|
956
|
+
|
957
|
+
function rcvrReport(res, rpath, rfile, run, data, stats, log) {
|
958
|
+
try {
|
959
|
+
let fp = path.join(rpath, rfile + run + '-data.txt');
|
960
|
+
fs.writeFileSync(fp, data);
|
961
|
+
} catch(err) {
|
962
|
+
console.log(err);
|
963
|
+
servePlainText(res,
|
964
|
+
`ERROR: Failed to write data to file <tt>${fp}</tt>`);
|
965
|
+
return;
|
966
|
+
}
|
967
|
+
try {
|
968
|
+
fp = path.join(rpath, rfile + run + '-stats.txt');
|
969
|
+
fs.writeFileSync(fp, stats);
|
970
|
+
} catch(err) {
|
971
|
+
console.log(err);
|
972
|
+
servePlainText(res,
|
973
|
+
`ERROR: Failed to write statistics to file <tt>${fp}</tt>`);
|
974
|
+
return;
|
975
|
+
}
|
976
|
+
try {
|
977
|
+
fp = path.join(rpath, rfile + run + '-log.txt');
|
978
|
+
fs.writeFileSync(fp, log);
|
979
|
+
} catch(err) {
|
980
|
+
console.log(err);
|
981
|
+
servePlainText(res,
|
982
|
+
`ERROR: Failed to write event log to file <tt>${fp}</tt>`);
|
983
|
+
}
|
984
|
+
servePlainText(res, `Data and statistics reported for <tt>${rfile}</tt>`);
|
985
|
+
}
|
986
|
+
|
987
|
+
function rcvrCallBack(res, rpath, rfile, script) {
|
988
|
+
let file_type = '',
|
989
|
+
cpath = path.join(rpath, rfile + '.lnr');
|
990
|
+
try {
|
991
|
+
fs.accessSync(cpath);
|
992
|
+
file_type = 'model';
|
993
|
+
} catch(err) {
|
994
|
+
cpath = path.join(rpath, rfile + '.lnrc');
|
995
|
+
try {
|
996
|
+
fs.accessSync(cpath);
|
997
|
+
file_type = 'command';
|
998
|
+
} catch(err) {
|
999
|
+
cpath = '';
|
1000
|
+
}
|
1001
|
+
}
|
1002
|
+
if(cpath) {
|
1003
|
+
console.log('Deleting', file_type, ' file:', cpath);
|
1004
|
+
try {
|
1005
|
+
fs.unlinkSync(cpath);
|
1006
|
+
} catch(err) {
|
1007
|
+
console.log(err);
|
1008
|
+
servePlainText(res,
|
1009
|
+
`ERROR: Failed to delete ${file_type} file <tt>${rfile}</tt>`);
|
1010
|
+
return;
|
1011
|
+
}
|
1012
|
+
}
|
1013
|
+
if(!script) {
|
1014
|
+
servePlainText(res, 'No call-back script to execute');
|
1015
|
+
return;
|
1016
|
+
}
|
1017
|
+
try {
|
1018
|
+
cmd = fs.readFileSync(path.join(WORKSPACE.callback, script), 'utf8');
|
1019
|
+
console.log(`Executing callback command "${cmd}"`);
|
1020
|
+
child_process.exec(cmd, (error, stdout, stderr) => {
|
1021
|
+
console.log(stdout);
|
1022
|
+
if(error) {
|
1023
|
+
console.log(error);
|
1024
|
+
console.log(stderr);
|
1025
|
+
servePlainText(res,
|
1026
|
+
`ERROR: Failed to execute script <tt>${script}</tt>`);
|
1027
|
+
} else {
|
1028
|
+
servePlainText(res, `Call-back script <tt>${script}</tt> executed`);
|
1029
|
+
}
|
1030
|
+
});
|
1031
|
+
} catch(err) {
|
1032
|
+
console.log(err);
|
1033
|
+
servePlainText(res,
|
1034
|
+
`WARNING: Call-back script <tt>${script}</tt> not found`);
|
1035
|
+
}
|
1036
|
+
}
|
1037
|
+
|
1038
|
+
// Basic server functionality
|
1039
|
+
// ===========================
|
1040
|
+
//
|
1041
|
+
// To provide some minimum of security, the files that will be served
|
1042
|
+
// from the (main)/static directory are restricted to specific MIME
|
1043
|
+
// types, files, and sub-directories of (main)/static
|
1044
|
+
const STATIC_FILES = {
|
1045
|
+
// MIME types of files that can be served
|
1046
|
+
extensions: {
|
1047
|
+
js: 'application/javascript',
|
1048
|
+
xml: 'application/xml',
|
1049
|
+
wav: 'audio/x-wav',
|
1050
|
+
ttc: 'font/collection',
|
1051
|
+
otf: 'font/otf',
|
1052
|
+
ttf: 'font/ttf',
|
1053
|
+
icns: 'image/icns',
|
1054
|
+
png: 'image/png',
|
1055
|
+
svg: 'image/svg+xml',
|
1056
|
+
ico: 'image/x-icon',
|
1057
|
+
css: 'text/css',
|
1058
|
+
html: 'text/html',
|
1059
|
+
txt: 'text/plain'
|
1060
|
+
},
|
1061
|
+
// Subdirectories of (main)/static/ directory from which files with
|
1062
|
+
// accepted MIME types can be served
|
1063
|
+
directories: {
|
1064
|
+
'/scripts': ['js'],
|
1065
|
+
'/images': ['icns', 'ico', 'png', 'svg'],
|
1066
|
+
'/fonts': ['otf', 'ttc', 'ttf'],
|
1067
|
+
'/sounds': ['wav'],
|
1068
|
+
// NOTE: diagrams will actually be served from (main)/user/diagrams/
|
1069
|
+
'/diagrams': ['png', 'svg']
|
1070
|
+
},
|
1071
|
+
// Files that can be served from the (main)/static/ directory itself
|
1072
|
+
files: [
|
1073
|
+
'/index.html',
|
1074
|
+
'/show-png.html',
|
1075
|
+
'/show-diff.html',
|
1076
|
+
'/linny-r.css',
|
1077
|
+
'/favicon.ico',
|
1078
|
+
]
|
1079
|
+
};
|
1080
|
+
|
1081
|
+
function processRequest(req, res, cmd, data) {
|
1082
|
+
// Make correct response to request
|
1083
|
+
// NOTE: `data` is a string of form field1=value1&field2=value2& ... etc.
|
1084
|
+
// regardless of the request method (GET or POST)
|
1085
|
+
if(permittedFile(cmd)) {
|
1086
|
+
// Path contains valid MIME file type extension => serve if allowed
|
1087
|
+
serveStaticFile(res, cmd);
|
1088
|
+
} else if(cmd === '/solver/') {
|
1089
|
+
const
|
1090
|
+
sp = new URLSearchParams(data),
|
1091
|
+
action = sp.get('action');
|
1092
|
+
// NOTE: on remote servers, solver actions require authentication
|
1093
|
+
if(action === 'logon') {
|
1094
|
+
// No authentication -- simply return the passed token, "local host" as
|
1095
|
+
// server name, and the identifier of the solver
|
1096
|
+
serveJSON(res,
|
1097
|
+
{token: 'local host', server: 'local host', solver: SOLVER.id});
|
1098
|
+
} else if(action === 'png') {
|
1099
|
+
convertSVGtoPNG(req, res, sp);
|
1100
|
+
} else if(action === 'solve') {
|
1101
|
+
serveJSON(res, SOLVER.solveBlock(sp));
|
1102
|
+
} else {
|
1103
|
+
// Invalid action => return JSON with error message
|
1104
|
+
const msg = `Invalid action: "${action}"`;
|
1105
|
+
console.log(msg);
|
1106
|
+
serveJSON(res, {error: msg});
|
1107
|
+
}
|
1108
|
+
} else if(cmd === '/shutdown') {
|
1109
|
+
// Shut down this server
|
1110
|
+
serveHTML(res, SHUTDOWN_MESSAGE);
|
1111
|
+
SERVER.close();
|
1112
|
+
} else if(cmd === '/auto-check') {
|
1113
|
+
autoCheck(res);
|
1114
|
+
} else if(cmd === '/auto-update') {
|
1115
|
+
autoUpdate(res);
|
1116
|
+
} else if(cmd === '/check-version') {
|
1117
|
+
checkVersion(res, (new URLSearchParams(data)).get('v'));
|
1118
|
+
} else if(cmd === '/repo/') {
|
1119
|
+
repo(res, new URLSearchParams(data));
|
1120
|
+
} else if(cmd === '/load-data/') {
|
1121
|
+
loadData(res, (new URLSearchParams(data)).get('url'));
|
1122
|
+
} else if(cmd === '/receiver/') {
|
1123
|
+
receiver(res, new URLSearchParams(data));
|
1124
|
+
} else {
|
1125
|
+
serveJSON(res, {error: `Unknown Linny-R request: "${cmd}"`});
|
1126
|
+
}
|
1127
|
+
}
|
1128
|
+
|
1129
|
+
function servePlainText(res, msg) {
|
1130
|
+
// Serve string `msg` as plain text
|
1131
|
+
res.setHeader('Content-Type', 'text/plain');
|
1132
|
+
res.writeHead(200);
|
1133
|
+
res.end(msg);
|
1134
|
+
}
|
1135
|
+
|
1136
|
+
function serveHTML(res, html) {
|
1137
|
+
// Serve HTML string `html`
|
1138
|
+
res.setHeader('Content-Type', 'text/html');
|
1139
|
+
res.writeHead(200);
|
1140
|
+
res.end(html);
|
1141
|
+
}
|
1142
|
+
|
1143
|
+
function serveJSON(res, obj) {
|
1144
|
+
// Serve object `obj` as JSON string
|
1145
|
+
res.setHeader('Content-Type', 'application/json');
|
1146
|
+
res.writeHead(200);
|
1147
|
+
res.end(JSON.stringify(obj));
|
1148
|
+
}
|
1149
|
+
|
1150
|
+
function permittedFile(path) {
|
1151
|
+
// Returns TRUE when file specified by `path` may be served
|
1152
|
+
if(path === '/' || path === '') path = '/index.html';
|
1153
|
+
if(STATIC_FILES.files.indexOf(path) >= 0) return true;
|
1154
|
+
const
|
1155
|
+
parts = path.split('/'),
|
1156
|
+
file = parts.pop(),
|
1157
|
+
dir = STATIC_FILES.directories[parts.join('/')],
|
1158
|
+
ext = file.split('.').pop();
|
1159
|
+
if(dir && dir.indexOf(ext) >= 0) return true;
|
1160
|
+
return false;
|
1161
|
+
}
|
1162
|
+
|
1163
|
+
function serveStaticFile(res, path) {
|
1164
|
+
// Serve the specified path (if permitted: only diagrams and static files)
|
1165
|
+
if(path === '/' || path === '') path = '/index.html';
|
1166
|
+
if(path.startsWith('/diagrams/')) {
|
1167
|
+
// Serve diagrams from the (main)/user/diagrams/ sub-directory
|
1168
|
+
console.log('Diagram:', path);
|
1169
|
+
path = '/user' + path;
|
1170
|
+
} else {
|
1171
|
+
// Other files from the (main)/static/ subdirectory
|
1172
|
+
console.log('Static file:', path);
|
1173
|
+
path = '/static' + path;
|
1174
|
+
}
|
1175
|
+
fs.readFile(MAIN_DIRECTORY + path, (err, data) => {
|
1176
|
+
if(err) {
|
1177
|
+
console.log(err);
|
1178
|
+
res.writeHead(404);
|
1179
|
+
res.end(JSON.stringify(err));
|
1180
|
+
return;
|
1181
|
+
}
|
1182
|
+
const ct = STATIC_FILES.extensions[path.split('.').pop()];
|
1183
|
+
res.setHeader('Content-Type', ct);
|
1184
|
+
res.writeHead(200);
|
1185
|
+
res.end(data);
|
1186
|
+
});
|
1187
|
+
}
|
1188
|
+
|
1189
|
+
function convertSVGtoPNG(req, res, sp) {
|
1190
|
+
// Convert SVG data from browser to PNG image using Inkscape
|
1191
|
+
// NOTE: images can be huge, so send only the file name as response;
|
1192
|
+
// Linny-R will open a new browser window, load the file, and display it
|
1193
|
+
const
|
1194
|
+
svg = decodeURI(atob(sp.get('data'))),
|
1195
|
+
// Use current time as file name
|
1196
|
+
fn = 'diagram-' +
|
1197
|
+
(new Date()).toISOString().slice(0, 19).replace(/[\-\:]/g, ''),
|
1198
|
+
fp = path.join(WORKSPACE.diagrams, fn);
|
1199
|
+
// NOTE: use binary encoding for SVG file
|
1200
|
+
console.log('Saving SVG file:', fp);
|
1201
|
+
try {
|
1202
|
+
fs.writeFileSync(fp + '.svg', svg);
|
1203
|
+
} catch(error) {
|
1204
|
+
console.log('WARNING: Failed to save SVG --', error);
|
1205
|
+
}
|
1206
|
+
// Use Inkscape to convert SVG to the requested format
|
1207
|
+
if(SETTINGS.inkscape) {
|
1208
|
+
console.log('Rendering image');
|
1209
|
+
let
|
1210
|
+
cmd = SETTINGS.inkscape,
|
1211
|
+
svg = fp + '.svg';
|
1212
|
+
// Enclose paths in double quotes if they contain spaces
|
1213
|
+
if(cmd.indexOf(' ') >= 0) cmd = `"${cmd}"`;
|
1214
|
+
if(svg.indexOf(' ') >= 0) svg = `"${svg}"`;
|
1215
|
+
child_process.exec(cmd + ' --export-type=png --export-dpi=' +
|
1216
|
+
SETTINGS.dpi + ' ' + svg,
|
1217
|
+
(error, stdout, stderr) => {
|
1218
|
+
let ext = '.svg';
|
1219
|
+
console.log(stdout);
|
1220
|
+
if(error) {
|
1221
|
+
console.log('WARNING: Failed to run Inkscape --', error);
|
1222
|
+
console.log(stderr);
|
1223
|
+
} else {
|
1224
|
+
ext = '.png';
|
1225
|
+
// Delete the SVG
|
1226
|
+
try {
|
1227
|
+
fs.unlinkSync(fp + '.svg');
|
1228
|
+
} catch(error) {
|
1229
|
+
console.log(`NOTICE: Failed to delete SVG file "${fp}.svg"`);
|
1230
|
+
}
|
1231
|
+
}
|
1232
|
+
// Return the image file name (PNG if successful, otherwise SVG)
|
1233
|
+
servePlainText(res, 'diagrams/' + fn + ext);
|
1234
|
+
}
|
1235
|
+
);
|
1236
|
+
} else {
|
1237
|
+
servePlainText(res, 'diagrams/' + fn + '.svg');
|
1238
|
+
}
|
1239
|
+
}
|
1240
|
+
|
1241
|
+
// Convenience functions to fetch data from external URL
|
1242
|
+
// NOTE: the parameters `on_ok` and `on_error` must be functions with two
|
1243
|
+
// parameters:
|
1244
|
+
// - `result`: either a string with the text obtained from the URL, or an error
|
1245
|
+
// - `response`: a response object (if any) passed by the function that is
|
1246
|
+
// calling `getTextFromURL` so that it may be completed and then passed
|
1247
|
+
// to the browser by the `on_ok` or `on_error` functions
|
1248
|
+
|
1249
|
+
function getTextFromURL(url, on_ok, on_error, response=null) {
|
1250
|
+
// Gets a text string (plain, HTML, or other) from the specified URL,
|
1251
|
+
// and then calls `on_ok`, or `on_error` if the request failed
|
1252
|
+
https.get(url, (res) => {
|
1253
|
+
// Any 2xx status code signals a successful response, but be strict
|
1254
|
+
if (res.statusCode !== 200) {
|
1255
|
+
// Consume response data to free up memory
|
1256
|
+
res.resume();
|
1257
|
+
return on_error(new Error('Get text request failed -- Status code: ' +
|
1258
|
+
res.statusCode), response);
|
1259
|
+
}
|
1260
|
+
// Fetch the complete data string
|
1261
|
+
res.setEncoding('utf8');
|
1262
|
+
let data = '';
|
1263
|
+
res.on('data', (chunk) => { data += chunk; });
|
1264
|
+
res.on('end', () => on_ok(data, response));
|
1265
|
+
}).on('error', (e) => on_error(e, response));
|
1266
|
+
}
|
1267
|
+
|
1268
|
+
function postRequest(url, obj, on_ok, on_error, response=null) {
|
1269
|
+
// Submits `obj` as "POST form" to the remote server specified by `url`
|
1270
|
+
// NOTE: A trailing slash is crucial here, as otherwise the server will
|
1271
|
+
// redirect it as a GET !!!
|
1272
|
+
if(!url.endsWith('/')) url += '/';
|
1273
|
+
const
|
1274
|
+
post_data = formData(obj),
|
1275
|
+
options = {
|
1276
|
+
method: 'POST',
|
1277
|
+
headers: {
|
1278
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
1279
|
+
'Content-Length': Buffer.byteLength(post_data)
|
1280
|
+
}
|
1281
|
+
},
|
1282
|
+
req = https.request(url, options, (res) => {
|
1283
|
+
if (res.statusCode !== 200) {
|
1284
|
+
// Consume response data to free up memory
|
1285
|
+
res.resume();
|
1286
|
+
return on_error(new Error(`POST request (${url}) failed -- ` +
|
1287
|
+
`Status code: ${res.statusCode}`), response);
|
1288
|
+
}
|
1289
|
+
// Fetch the complete data buffer
|
1290
|
+
const chunks = [];
|
1291
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
1292
|
+
res.on('end', () => on_ok(Buffer.concat(chunks), response));
|
1293
|
+
});
|
1294
|
+
req.on('error', (e) => on_error(e, response));
|
1295
|
+
// Add the object as form data to the request body
|
1296
|
+
req.write(post_data);
|
1297
|
+
req.end();
|
1298
|
+
}
|
1299
|
+
|
1300
|
+
function formData(obj) {
|
1301
|
+
// Encodes `obj` as a form that can be POSTed
|
1302
|
+
const fields = [];
|
1303
|
+
for(let k in obj) if(obj.hasOwnProperty(k)) {
|
1304
|
+
fields.push(encodeURIComponent(k) + "=" + encodeURIComponent(obj[k]));
|
1305
|
+
}
|
1306
|
+
return fields.join('&');
|
1307
|
+
}
|
1308
|
+
|
1309
|
+
//
|
1310
|
+
// Functions used during initialization
|
1311
|
+
//
|
1312
|
+
|
1313
|
+
function commandLineSettings() {
|
1314
|
+
// Sets default settings, and then checks the command line arguments
|
1315
|
+
const settings = {
|
1316
|
+
cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
|
1317
|
+
inkscape: '',
|
1318
|
+
dpi: 300,
|
1319
|
+
launch: false,
|
1320
|
+
port: 5050,
|
1321
|
+
preferred_solver: '',
|
1322
|
+
solver: '',
|
1323
|
+
solver_path: '',
|
1324
|
+
user_dir: path.join(MAIN_DIRECTORY, 'user')
|
1325
|
+
};
|
1326
|
+
for(let i = 2; i < process.argv.length; i++) {
|
1327
|
+
const lca = process.argv[i].toLowerCase();
|
1328
|
+
if(lca === 'launch') {
|
1329
|
+
settings.launch = true;
|
1330
|
+
} else {
|
1331
|
+
const av = lca.split('=');
|
1332
|
+
if(av.length === 1) av.push('');
|
1333
|
+
if(av[0] === 'port') {
|
1334
|
+
// Accept any number greater than or equal to 1024
|
1335
|
+
const n = parseInt(av[1]);
|
1336
|
+
if(isNaN(n) || n < 1024) {
|
1337
|
+
console.log(`WARNING: Invalid port number ${av[1]}`);
|
1338
|
+
} else {
|
1339
|
+
settings.port = n;
|
1340
|
+
}
|
1341
|
+
} else if(av[0] === 'solver') {
|
1342
|
+
if(av[1] !== 'gurobi' && av[1] !== 'lp_solve') {
|
1343
|
+
console.log(`WARNING: Unknown solver "${av[1]}"`);
|
1344
|
+
} else {
|
1345
|
+
settings.preferred_solver = av[1];
|
1346
|
+
}
|
1347
|
+
} else if(av[0] === 'dpi') {
|
1348
|
+
// Accept any number greater than or equal to 1024
|
1349
|
+
const n = parseInt(av[1]);
|
1350
|
+
if(isNaN(n) || n > 1200) {
|
1351
|
+
console.log(`WARNING: Invalid resolution ${av[1]} (max. 1200 dpi)`);
|
1352
|
+
} else {
|
1353
|
+
settings.dpi = n;
|
1354
|
+
}
|
1355
|
+
} else if(av[0] === 'workspace') {
|
1356
|
+
// User directory must be READ/WRITE-accessible
|
1357
|
+
try {
|
1358
|
+
fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
|
1359
|
+
} catch(err) {
|
1360
|
+
console.log(`ERROR: No access to directory "${av[1]}"`);
|
1361
|
+
process.exit();
|
1362
|
+
}
|
1363
|
+
settings.user_dir = av[1];
|
1364
|
+
} else {
|
1365
|
+
// Terminate script
|
1366
|
+
console.log(
|
1367
|
+
`ERROR: Invalid command line argument "${process.argv[i]}"`);
|
1368
|
+
process.exit();
|
1369
|
+
}
|
1370
|
+
}
|
1371
|
+
}
|
1372
|
+
// Check whether MILP solver(s) and Inkscape have been installed
|
1373
|
+
const path_list = process.env.PATH.split(path.delimiter);
|
1374
|
+
let gurobi_path = '',
|
1375
|
+
match,
|
1376
|
+
max_v = -1;
|
1377
|
+
for(let i = 0; i < path_list.length; i++) {
|
1378
|
+
match = path_list[i].match(/gurobi(\d+)/i);
|
1379
|
+
if(match && parseInt(match[1]) > max_v) {
|
1380
|
+
gurobi_path = path_list[i];
|
1381
|
+
max_v = parseInt(match[1]);
|
1382
|
+
}
|
1383
|
+
match = path_list[i].match(/inkscape/i);
|
1384
|
+
if(match) settings.inkscape = path_list[i];
|
1385
|
+
}
|
1386
|
+
if(!gurobi_path && !PLATFORM.startsWith('win')) {
|
1387
|
+
console.log('Looking for Gurobi in /usr/local/bin');
|
1388
|
+
try {
|
1389
|
+
// On macOS and Unix, Gurobi is in the user's local binaries
|
1390
|
+
const gp = '/usr/local/bin';
|
1391
|
+
fs.accessSync(gp + '/gurobi_cl');
|
1392
|
+
gurobi_path = gp;
|
1393
|
+
} catch(err) {
|
1394
|
+
// No real error, so no action needed
|
1395
|
+
}
|
1396
|
+
}
|
1397
|
+
if(gurobi_path) {
|
1398
|
+
console.log('Path to Gurobi:', gurobi_path);
|
1399
|
+
// Check if command line version is executable
|
1400
|
+
const sp = path.join(gurobi_path,
|
1401
|
+
'gurobi_cl' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
1402
|
+
try {
|
1403
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
1404
|
+
if(settings.solver !== 'gurobi')
|
1405
|
+
settings.solver = 'gurobi';
|
1406
|
+
settings.solver_path = sp;
|
1407
|
+
} catch(err) {
|
1408
|
+
console.log(err.message);
|
1409
|
+
console.log(
|
1410
|
+
'WARNING: Failed to access the Gurobi command line application');
|
1411
|
+
}
|
1412
|
+
}
|
1413
|
+
// Check if lp_solve(.exe) exists in main directory
|
1414
|
+
const
|
1415
|
+
sp = path.join(MAIN_DIRECTORY,
|
1416
|
+
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : '')),
|
1417
|
+
need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
1418
|
+
try {
|
1419
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
1420
|
+
console.log('Path to LP_solve:', sp);
|
1421
|
+
if(need_lps) {
|
1422
|
+
settings.solver = 'lp_solve';
|
1423
|
+
settings.solver_path = sp;
|
1424
|
+
}
|
1425
|
+
} catch(err) {
|
1426
|
+
// Only report error if LP_solve is needed
|
1427
|
+
if(need_lps) {
|
1428
|
+
console.log(err.message);
|
1429
|
+
console.log('WARNING: LP_solve application not found in', sp);
|
1430
|
+
}
|
1431
|
+
}
|
1432
|
+
// On macOS, Inkscape is not added to the PATH environment variable
|
1433
|
+
if(!settings.inkscape && PLATFORM === 'darwin') {
|
1434
|
+
console.log('Looking for Inkscape in Applications...');
|
1435
|
+
try {
|
1436
|
+
// Look in the default directory
|
1437
|
+
const ip = '/Applications/Inkscape.app/Contents/MacOS';
|
1438
|
+
fs.accessSync(ip);
|
1439
|
+
settings.inkscape = ip;
|
1440
|
+
} catch(err) {
|
1441
|
+
// No real error, so no action needed
|
1442
|
+
}
|
1443
|
+
}
|
1444
|
+
// Verify that Inkscape is installed
|
1445
|
+
if(settings.inkscape) {
|
1446
|
+
// NOTE: on Windows, the command line version is a .com file
|
1447
|
+
const ip = path.join(settings.inkscape,
|
1448
|
+
'inkscape' + (PLATFORM.startsWith('win') ? '.com' : ''));
|
1449
|
+
try {
|
1450
|
+
fs.accessSync(ip, fs.constants.X_OK);
|
1451
|
+
console.log('Path to Inkscape:', settings.inkscape);
|
1452
|
+
settings.inkscape = ip;
|
1453
|
+
console.log(
|
1454
|
+
`SVG will be rendered with ${settings.dpi} dpi resolution`);
|
1455
|
+
} catch(err) {
|
1456
|
+
settings.inkscape = '';
|
1457
|
+
console.log(err.message);
|
1458
|
+
console.log(
|
1459
|
+
'WARNING: Failed to access the Inkscape command line application');
|
1460
|
+
}
|
1461
|
+
} else {
|
1462
|
+
console.log(
|
1463
|
+
'Inkscape not installed, so images will not be rendered as PNG');
|
1464
|
+
}
|
1465
|
+
return settings;
|
1466
|
+
}
|
1467
|
+
|
1468
|
+
function createWorkspace() {
|
1469
|
+
// Verifies that Linny-R has write access to the user workspace, defines
|
1470
|
+
// paths to sub-directories, and creates them if necessary
|
1471
|
+
try {
|
1472
|
+
// See whether the user directory already exists
|
1473
|
+
try {
|
1474
|
+
fs.accessSync(SETTINGS.user_dir, fs.constants.R_OK | fs.constants.W_O);
|
1475
|
+
} catch(err) {
|
1476
|
+
// If not, try to create it
|
1477
|
+
fs.mkdirSync(SETTINGS.user_dir);
|
1478
|
+
console.log('Created user directory:', SETTINGS.user_dir);
|
1479
|
+
}
|
1480
|
+
} catch(err) {
|
1481
|
+
console.log(err.message);
|
1482
|
+
console.log('FATAL ERROR: Failed to create user workspace in',
|
1483
|
+
SETTINGS.user_dir);
|
1484
|
+
process.exit();
|
1485
|
+
}
|
1486
|
+
// Define the sub-directory paths
|
1487
|
+
const ws = {
|
1488
|
+
channel: path.join(SETTINGS.user_dir, 'channel'),
|
1489
|
+
callback: path.join(SETTINGS.user_dir, 'callback'),
|
1490
|
+
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
1491
|
+
modules: path.join(SETTINGS.user_dir, 'modules'),
|
1492
|
+
solver_output: path.join(SETTINGS.user_dir, 'solver'),
|
1493
|
+
};
|
1494
|
+
// Create these sub-directories if not aready there
|
1495
|
+
try {
|
1496
|
+
for(let p in ws) if(ws.hasOwnProperty(p)) {
|
1497
|
+
try {
|
1498
|
+
fs.accessSync(ws[p]);
|
1499
|
+
} catch(e) {
|
1500
|
+
fs.mkdirSync(ws[p]);
|
1501
|
+
console.log('Created workspace sub-directory:', ws[p]);
|
1502
|
+
}
|
1503
|
+
}
|
1504
|
+
} catch(err) {
|
1505
|
+
console.log(err.message);
|
1506
|
+
console.log('WARNING: No access to workspace directory');
|
1507
|
+
}
|
1508
|
+
// The file containing name, URL and access token for remote repositories
|
1509
|
+
ws.repositories = path.join(SETTINGS.user_dir, 'repositories.cfg');
|
1510
|
+
// Return the updated workspace object
|
1511
|
+
return ws;
|
1512
|
+
}
|
1513
|
+
|
1514
|
+
function createLaunchScript() {
|
1515
|
+
// Creates platform-specific script with Linny-R start-up command
|
1516
|
+
const lines = [
|
1517
|
+
'# The first line (without the comment symbol #) should be like this:',
|
1518
|
+
'# cd ',
|
1519
|
+
'',
|
1520
|
+
'# Then this command to launch the Linny-R server should work:',
|
1521
|
+
'node server launch'
|
1522
|
+
];
|
1523
|
+
let sp;
|
1524
|
+
if(PLATFORM.startsWith('win')) {
|
1525
|
+
sp = path.join(MAIN_DIRECTORY, 'linny-r.bat');
|
1526
|
+
lines[1] += 'C:\\path\\to\\main\\Linny-R\\directory';
|
1527
|
+
} else {
|
1528
|
+
sp = path.join(MAIN_DIRECTORY, 'linny-r.command');
|
1529
|
+
lines[1] += '/path/to/main/Linny-R/directory';
|
1530
|
+
}
|
1531
|
+
lines[2] = 'cd ' + MAIN_DIRECTORY;
|
1532
|
+
try {
|
1533
|
+
try {
|
1534
|
+
fs.accessSync(sp);
|
1535
|
+
} catch(err) {
|
1536
|
+
// Only write the script content if the file it does not yet exist
|
1537
|
+
console.log('Creating launch script:', sp);
|
1538
|
+
fs.writeFileSync(sp, lines.join(os.EOL), 'utf8');
|
1539
|
+
}
|
1540
|
+
} catch(err) {
|
1541
|
+
console.log('WARNING: Failed to create launch script');
|
1542
|
+
}
|
1543
|
+
}
|
1544
|
+
|
1545
|
+
/////////////////////////////////////////////////////////////////////////////
|
1546
|
+
// Code ends here //
|
1547
|
+
/////////////////////////////////////////////////////////////////////////////
|