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/console.js
ADDED
@@ -0,0 +1,973 @@
|
|
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 JavaScript file (console.js) implements the console version of
|
9
|
+
Linny-R that can be run in Node.js without a web browser. It defines
|
10
|
+
console versions of the classes FileManager, Monitor, and
|
11
|
+
RepositoryBrowser, and takes care of the interaction between the
|
12
|
+
Virtual Machine and the solver.
|
13
|
+
|
14
|
+
NOTE: For browser-based Linny-R, this file should NOT be loaded, as it
|
15
|
+
requires Node.js modules.
|
16
|
+
*/
|
17
|
+
|
18
|
+
/*
|
19
|
+
Copyright (c) 2017-2022 Delft University of Technology
|
20
|
+
|
21
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
22
|
+
of this software and associated documentation files (the "Software"), to deal
|
23
|
+
in the Software without restriction, including without limitation the rights to
|
24
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
25
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
26
|
+
so, subject to the following conditions:
|
27
|
+
|
28
|
+
The above copyright notice and this permission notice shall be included in
|
29
|
+
all copies or substantial portions of the Software.
|
30
|
+
|
31
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
32
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
33
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
34
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
35
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
36
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
37
|
+
SOFTWARE.
|
38
|
+
*/
|
39
|
+
|
40
|
+
// Set global flag to indicate that this is a Node.js application
|
41
|
+
// (this will make the "module" files linny-r-xxx.js export their properties)
|
42
|
+
global.NODE = true;
|
43
|
+
|
44
|
+
const VERSION_NUMBER = '1.0.0';
|
45
|
+
|
46
|
+
const MAIN_DIRECTORY = process.cwd();
|
47
|
+
|
48
|
+
// Load the required Node.js modules
|
49
|
+
const
|
50
|
+
fs = require('fs'),
|
51
|
+
os = require('os'),
|
52
|
+
path = require('path'),
|
53
|
+
readline = require('readline');
|
54
|
+
|
55
|
+
// Get the platform name (win32, macOS, linux) of the user's computer
|
56
|
+
const PLATFORM = os.platform();
|
57
|
+
|
58
|
+
// Immediately output some configuration information to the console
|
59
|
+
console.log('\nNode.js Linny-R console version', VERSION_NUMBER);
|
60
|
+
console.log('Node.js version:', process.version);
|
61
|
+
console.log('Platform:', PLATFORM, '(' + os.type() + ')');
|
62
|
+
console.log('Main directory:', MAIN_DIRECTORY);
|
63
|
+
|
64
|
+
// Load the MILP solver (dependent on Node.js: `fs`, `os` and `path`)
|
65
|
+
const MILPSolver = require('./static/scripts/linny-r-milp.js');
|
66
|
+
|
67
|
+
// Load the browser-compatible Linny-R scripts
|
68
|
+
const
|
69
|
+
config = require('./static/scripts/linny-r-config.js'),
|
70
|
+
utils = require('./static/scripts/linny-r-utils.js'),
|
71
|
+
vm = require('./static/scripts/linny-r-vm.js'),
|
72
|
+
model = require('./static/scripts/linny-r-model.js'),
|
73
|
+
ctrl = require('./static/scripts/linny-r-ctrl.js');
|
74
|
+
|
75
|
+
// NOTE: the variables, functions and classes defined in these scripts
|
76
|
+
// must still be "imported" into the global scope of this Node.js script
|
77
|
+
for(let k in config) if(config.hasOwnProperty(k)) global[k] = config[k];
|
78
|
+
for(let k in utils) if(utils.hasOwnProperty(k)) global[k] = utils[k];
|
79
|
+
for(let k in vm) if(vm.hasOwnProperty(k)) global[k] = vm[k];
|
80
|
+
for(let k in model) if(model.hasOwnProperty(k)) global[k] = model[k];
|
81
|
+
for(let k in ctrl) if(ctrl.hasOwnProperty(k)) global[k] = ctrl[k];
|
82
|
+
|
83
|
+
// Default settings are used unless these are overruled by arguments on the
|
84
|
+
// command line
|
85
|
+
const usage = `
|
86
|
+
Usage: node console [options]
|
87
|
+
|
88
|
+
Possible options are:
|
89
|
+
channel=[identifier] will start listening at the specified channel
|
90
|
+
(FUTURE OPTION)
|
91
|
+
model=[path] will load model file in [path]
|
92
|
+
module=[name@repo] will load model [name] from repository [repo]
|
93
|
+
(if @repo is blank, repository "local host" is used)
|
94
|
+
(FUTURE OPTION)
|
95
|
+
run will run the loaded model
|
96
|
+
solver=[name] will select solver [name], or warn if not found
|
97
|
+
(name choices: Gurobi or LP_solve)
|
98
|
+
user=[identifier] user ID will be used to log onto remote servers
|
99
|
+
verbose will output solver messages to the console
|
100
|
+
workspace=[path] will create workspace in [path] instead of (main)/user
|
101
|
+
xrun=[title#list] will perform experiment runs in given range
|
102
|
+
(list is comma-separated sequence of run numbers)
|
103
|
+
(FUTURE OPTION)
|
104
|
+
`;
|
105
|
+
|
106
|
+
const SETTINGS = commandLineSettings();
|
107
|
+
|
108
|
+
// The workspace defines the paths to directories where Linny-R can write files
|
109
|
+
const WORKSPACE = createWorkspace();
|
110
|
+
|
111
|
+
// Only then require the Node.js modules that are not "built-in"
|
112
|
+
// NOTE: the function serves to catch the error in case the module has not
|
113
|
+
// been installed with `npm`
|
114
|
+
const { DOMParser } = checkNodeModule('@xmldom/xmldom');
|
115
|
+
|
116
|
+
function checkNodeModule(name) {
|
117
|
+
// Catches the error if Node.js module `name` is not available
|
118
|
+
try {
|
119
|
+
return require(name);
|
120
|
+
} catch(err) {
|
121
|
+
console.log(`ERROR: Node.js module "${name}" needs to be installed first`);
|
122
|
+
process.exit();
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
// Add the XML parser to the global scope so it can be referenced by the
|
127
|
+
// XML-related functions defined in `linny-r-utils.js`
|
128
|
+
global.XML_PARSER = new DOMParser();
|
129
|
+
|
130
|
+
// @@TO DO: Check for Linny-R software updates
|
131
|
+
// For now, simply set the version number
|
132
|
+
global.LINNY_R_VERSION = '1.1.0';
|
133
|
+
|
134
|
+
///////////////////////////////////////////////////////////////////////////////
|
135
|
+
// Class definitions must precede instatiation of Linny-R components //
|
136
|
+
///////////////////////////////////////////////////////////////////////////////
|
137
|
+
|
138
|
+
// CLASS ConsoleMonitor provides the UI for the Virtual Machine, including the
|
139
|
+
// connection with the solver, directly via the file system and sub-process
|
140
|
+
// functions of Node.js
|
141
|
+
class ConsoleMonitor {
|
142
|
+
constructor() {
|
143
|
+
this.console = true;
|
144
|
+
this.visible = false;
|
145
|
+
// The "show log" flag indicates whether log messages should be output to
|
146
|
+
// the console (will be ignored by the GraphicalMonitor)
|
147
|
+
this.show_log = false;
|
148
|
+
this.block_number = 0;
|
149
|
+
}
|
150
|
+
|
151
|
+
logMessage(block, msg) {
|
152
|
+
// Outputs a solver message to the console if logging is activated
|
153
|
+
if(this.show_log) {
|
154
|
+
if(block > this.block_number) {
|
155
|
+
// Mark advance to nex block with a blank line
|
156
|
+
console.log('\nBlock #', block);
|
157
|
+
this.block_number = block;
|
158
|
+
}
|
159
|
+
console.log(msg);
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
logOnToServer() {
|
164
|
+
VM.solver_user = '';
|
165
|
+
VM.solver_token = 'local host';
|
166
|
+
VM.solver_name = SOLVER.name;
|
167
|
+
}
|
168
|
+
|
169
|
+
connectToServer() {
|
170
|
+
// Console always uses local server => no logon prompt
|
171
|
+
this.logOnToServer();
|
172
|
+
return true;
|
173
|
+
}
|
174
|
+
|
175
|
+
submitBlockToSolver(bcode) {
|
176
|
+
let top = MODEL.timeout_period;
|
177
|
+
if(VM.max_solver_time && top > VM.max_solver_time) {
|
178
|
+
top = VM.max_solver_time;
|
179
|
+
UI.notify('Solver time limit for this server is ' +
|
180
|
+
VM.max_solver_time + ' seconds');
|
181
|
+
}
|
182
|
+
try {
|
183
|
+
const data = SOLVER.solveBlock(
|
184
|
+
new URLSearchParams({
|
185
|
+
action: 'solve',
|
186
|
+
user: VM.solver_user,
|
187
|
+
token: VM.solver_token,
|
188
|
+
block: VM.block_count,
|
189
|
+
round: VM.round_sequence[VM.current_round],
|
190
|
+
data: bcode,
|
191
|
+
timeout: top
|
192
|
+
}));
|
193
|
+
VM.processServerResponse(data);
|
194
|
+
const msg =
|
195
|
+
`Solving block #${VM.blockWithRound} took ${VM.elapsedTime} seconds.`;
|
196
|
+
VM.logMessage(VM.block_count, msg);
|
197
|
+
console.log(msg);
|
198
|
+
// solve next block (if any)
|
199
|
+
// NOTE: use setTimeout so that this calling function returns
|
200
|
+
// and hence frees its local variables
|
201
|
+
setTimeout(() => VM.solveBlocks(), 1);
|
202
|
+
} catch(err) {
|
203
|
+
console.log(err);
|
204
|
+
const msg = 'SOLVER ERROR: ' + ellipsedText(err.toString());
|
205
|
+
VM.logMessage(this.block_count, msg);
|
206
|
+
UI.alert(msg);
|
207
|
+
VM.stopSolving();
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
// Dummy methods called by VM, but meaningful only for the GUI monitor
|
212
|
+
reset() {}
|
213
|
+
updateMonitorTime() {}
|
214
|
+
updateBlockNumber() {}
|
215
|
+
addProgressBlock() {}
|
216
|
+
showBlock() {}
|
217
|
+
updateDialog() {}
|
218
|
+
updateContent() {}
|
219
|
+
showCallStack() {}
|
220
|
+
hideCallStack() {}
|
221
|
+
setRunMessages() {}
|
222
|
+
|
223
|
+
} // END of class ConsoleMonitor
|
224
|
+
|
225
|
+
|
226
|
+
// CLASS ConsoleFileManager allows loading and saving models and diagrams, and
|
227
|
+
// handles the interaction with the MILP solver via `exec` calls and files
|
228
|
+
// stored on the modeler's computer
|
229
|
+
class ConsoleFileManager {
|
230
|
+
|
231
|
+
getRemoteData(dataset, url) {
|
232
|
+
// Gets data from a URL, or from a file on the local host
|
233
|
+
if(url === '') return;
|
234
|
+
// NOTE: add this dataset to the "loading" list...
|
235
|
+
addDistinct(dataset, MODEL.loading_datasets);
|
236
|
+
// ... and allow for 3 more seconds (6 times 500 ms) to complete
|
237
|
+
MODEL.max_time_to_load += 6;
|
238
|
+
// Passed parameter is the URL or full path
|
239
|
+
console.log('Load data from', url);
|
240
|
+
if(!url) {
|
241
|
+
console.log('ERROR: No URL or path');
|
242
|
+
return;
|
243
|
+
}
|
244
|
+
if(url.toLowerCase().startsWith('http')) {
|
245
|
+
// URL => validate it, and then try to download its content as text
|
246
|
+
try {
|
247
|
+
new URL(url); // Will throw an error if URL is not valid
|
248
|
+
getTextFromURL(url,
|
249
|
+
(data) => FILE_MANAGER.setData(dataset, data),
|
250
|
+
(error) => {
|
251
|
+
console.log(error);
|
252
|
+
console.log('ERROR: Failed to get data from', url);
|
253
|
+
}
|
254
|
+
);
|
255
|
+
} catch(err) {
|
256
|
+
console.log(err);
|
257
|
+
console.log('ERROR: Invalid URL', url);
|
258
|
+
}
|
259
|
+
} else {
|
260
|
+
const fp = anyOSpath(url);
|
261
|
+
fs.readFile(fp, 'utf8', (err, data) => {
|
262
|
+
if(err) {
|
263
|
+
console.log(err);
|
264
|
+
return `ERROR: Could not read file <tt>${fp}</tt>`;
|
265
|
+
} else {
|
266
|
+
FILE_MANAGER.setData(dataset, data);
|
267
|
+
}
|
268
|
+
});
|
269
|
+
}
|
270
|
+
}
|
271
|
+
|
272
|
+
setData(dataset, data) {
|
273
|
+
if(data !== '' && UI.postResponseOK(data)) {
|
274
|
+
// Server must return either semicolon-separated or
|
275
|
+
// newline-separated string of numbers
|
276
|
+
if(data.indexOf(';') < 0) {
|
277
|
+
// If no semicolon found, replace newlines by semicolons
|
278
|
+
data = data.trim().split('\n').join(';');
|
279
|
+
}
|
280
|
+
// Remove all white space
|
281
|
+
data = data.replace(/\s+/g, '');
|
282
|
+
dataset.unpackDataString(data);
|
283
|
+
// NOTE: remove dataset from the "loading" list
|
284
|
+
const i = MODEL.loading_datasets.indexOf(dataset);
|
285
|
+
if(i >= 0) MODEL.loading_datasets.splice(i, 1);
|
286
|
+
}
|
287
|
+
}
|
288
|
+
|
289
|
+
decryptIfNeeded(data, callback) {
|
290
|
+
// Checks whether XML is encrypted; if not, processes data "as is",
|
291
|
+
// otherwise decrypt using password specified in command line
|
292
|
+
if(data.indexOf('model latch="') < 0) {
|
293
|
+
setTimeout(callback, 0, data);
|
294
|
+
return;
|
295
|
+
}
|
296
|
+
const xml = XML_PARSER.parseFromString(data, 'text/xml');
|
297
|
+
const de = xml.documentElement;
|
298
|
+
// Linny-R model must contain a model node
|
299
|
+
if(de.nodeName !== 'model') throw 'XML document has no model element';
|
300
|
+
const encr_msg = {
|
301
|
+
encryption: nodeContentByTag(de, 'content'),
|
302
|
+
latch: nodeParameterValue(de, 'latch')
|
303
|
+
};
|
304
|
+
console.log('Decrypting...');
|
305
|
+
// NOTE: function `tryToDecrypt` is defined in linny-r-utils.js
|
306
|
+
setTimeout((msg, pwd, ok, err) => tryToDecrypt(msg, pwd, ok, err), 5,
|
307
|
+
encr_msg, SETTINGS.password,
|
308
|
+
// The on_ok function
|
309
|
+
(data) => {
|
310
|
+
if(data) callback(data);
|
311
|
+
},
|
312
|
+
// The on_error function
|
313
|
+
(err) => {
|
314
|
+
console.log(err);
|
315
|
+
console.log('Failed to load encrypted model');
|
316
|
+
});
|
317
|
+
}
|
318
|
+
|
319
|
+
loadModel(fp, callback) {
|
320
|
+
// Get the XML of the file specified via the command line
|
321
|
+
// NOTE: asynchronous method with callback because decryption is
|
322
|
+
fs.readFile(fp, 'utf8', (err, data) => {
|
323
|
+
if(err) {
|
324
|
+
console.log(err);
|
325
|
+
console.log('ERROR: Could not read file '+ fp);
|
326
|
+
} else {
|
327
|
+
FILE_MANAGER.decryptIfNeeded(data,
|
328
|
+
(data) => { if(MODEL.parseXML(data)) callback(MODEL); });
|
329
|
+
}
|
330
|
+
});
|
331
|
+
}
|
332
|
+
|
333
|
+
} // END of class ConsoleFileManager
|
334
|
+
|
335
|
+
// CLASS ConsoleReceiver defines a listener/interpreter for channel commands
|
336
|
+
class ConsoleReceiver {
|
337
|
+
constructor() {
|
338
|
+
// NOTE: each receiver instance listens to a "channel", being the directory
|
339
|
+
// on the local host specified by the modeler
|
340
|
+
this.channel = '';
|
341
|
+
// The file name is the name of the first Linny-R model file or command file
|
342
|
+
// that was found in the channel directory
|
343
|
+
this.file_name = '';
|
344
|
+
// The name of the experiment to be run can be specified in a command file
|
345
|
+
this.experiment = '';
|
346
|
+
// The call-back script is the path to file with a shell command
|
347
|
+
this.call_back_script = '';
|
348
|
+
this.active = false;
|
349
|
+
this.solving = false;
|
350
|
+
this.interval = 1000;
|
351
|
+
this.error = '';
|
352
|
+
this.log_lines = [];
|
353
|
+
}
|
354
|
+
|
355
|
+
setError(msg) {
|
356
|
+
// Record and display error message, and immediately stop listening
|
357
|
+
this.error = msg;
|
358
|
+
UI.warn(this.error);
|
359
|
+
this.deactivate();
|
360
|
+
}
|
361
|
+
|
362
|
+
log(msg) {
|
363
|
+
// Logs a UI message so it will appear in the log file
|
364
|
+
if(this.active) {
|
365
|
+
if(!msg.startsWith('[')) {
|
366
|
+
const
|
367
|
+
d = new Date(),
|
368
|
+
now = d.getHours() + ':' +
|
369
|
+
d.getMinutes().toString().padStart(2, '0') + ':' +
|
370
|
+
d.getSeconds().toString().padStart(2, '0');
|
371
|
+
msg = `[${now}] ${msg}`;
|
372
|
+
}
|
373
|
+
this.log_lines.push(msg);
|
374
|
+
}
|
375
|
+
}
|
376
|
+
|
377
|
+
get logReport() {
|
378
|
+
// Returns log lines as a single string, and clears the log
|
379
|
+
const report = this.log_lines.join('\n');
|
380
|
+
this.log_lines.length = 0;
|
381
|
+
return report;
|
382
|
+
}
|
383
|
+
|
384
|
+
activate() {
|
385
|
+
// Sets channel path and (optional) call-back script
|
386
|
+
this.channel = SETTINGS.channel;
|
387
|
+
this.call_back_script = SETTINGS.callback;
|
388
|
+
// Clear experiment, error message and log
|
389
|
+
this.experiment = '';
|
390
|
+
this.error = '';
|
391
|
+
this.log_lines.length = 0;
|
392
|
+
this.active = true;
|
393
|
+
this.listen();
|
394
|
+
UI.notify('Started listening at', this.channel);
|
395
|
+
}
|
396
|
+
|
397
|
+
listen() {
|
398
|
+
// If active, checks with local server whether there is a new command
|
399
|
+
if(!this.active) return;
|
400
|
+
const jsr = rcvrListen(this.channel);
|
401
|
+
if(jsr.error) {
|
402
|
+
console.log('Receiver error:', jsr.error);
|
403
|
+
} else if(jsr.stop) {
|
404
|
+
console.log('Receiver deactivated by script');
|
405
|
+
this.deactivate();
|
406
|
+
} else if(jsr.file === '') {
|
407
|
+
// Nothing to do => check again after the set time interval
|
408
|
+
setTimeout(() => RECEIVER.listen(), this.interval);
|
409
|
+
} else if(jsr.file && jsr.model) {
|
410
|
+
// NOTE: model will NOT be encrypted, so it can be parsed
|
411
|
+
this.file_name = jsr.file;
|
412
|
+
let msg = '';
|
413
|
+
if(!MODEL.parseXML(jsr.model)) {
|
414
|
+
msg = 'ERROR: Received model is not valid';
|
415
|
+
} else if(jsr.experiment) {
|
416
|
+
EXPERIMENT_MANAGER.selectExperiment(jsr.experiment);
|
417
|
+
if(!EXPERIMENT_MANAGER.selected_experiment) {
|
418
|
+
msg = `ERROR: Unknown experiment "${jsr.experiment}"`;
|
419
|
+
} else {
|
420
|
+
this.experiment = jsr.experiment;
|
421
|
+
}
|
422
|
+
}
|
423
|
+
if(msg) {
|
424
|
+
this.setError(msg);
|
425
|
+
rcvrReport();
|
426
|
+
// Keep listening, so check again after the time interval
|
427
|
+
setTimeout(() => RECEIVER.listen(), this.interval);
|
428
|
+
} else {
|
429
|
+
this.log('Executing: ' + this.file_name);
|
430
|
+
// NOTE: Virtual Machine will trigger the receiver's reporting
|
431
|
+
// action each time the model has been solved
|
432
|
+
if(this.experiment) {
|
433
|
+
this.log('Starting experiment: ' + this.experiment);
|
434
|
+
EXPERIMENT_MANAGER.startExperiment();
|
435
|
+
} else {
|
436
|
+
VM.solveModel();
|
437
|
+
}
|
438
|
+
}
|
439
|
+
}
|
440
|
+
}
|
441
|
+
|
442
|
+
report() {
|
443
|
+
// Saves the run results in the channel, or signals an error
|
444
|
+
let run = '';
|
445
|
+
// NOTE: Always set `solving` to FALSE
|
446
|
+
this.solving = false;
|
447
|
+
if(this.experiment){
|
448
|
+
if(MODEL.running_experiment) {
|
449
|
+
run = MODEL.running_experiment.active_combination_index;
|
450
|
+
this.log(`Reporting: ${this.file_name} (run #${run})`);
|
451
|
+
}
|
452
|
+
}
|
453
|
+
if(MODEL.solved && !VM.halted) {
|
454
|
+
// Normal execution termination => report results
|
455
|
+
const data = MODEL.outputData;
|
456
|
+
rcvrReport(run, data[0], data[1]);
|
457
|
+
// If execution completed, perform the call-back action
|
458
|
+
// NOTE: for experiments, call-back is performed upon completion by
|
459
|
+
// the Experiment Manager
|
460
|
+
if(!this.experiment) this.callBack();
|
461
|
+
} else {
|
462
|
+
if(!VM.halted && !this.error) {
|
463
|
+
// No apparent cause => log this irregularity
|
464
|
+
this.setError('ERROR: Unknown solver problem');
|
465
|
+
rcvrAbort();
|
466
|
+
}
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
callBack() {
|
471
|
+
// Deletes the file in the channel directory (to prevent executing it again)
|
472
|
+
// and activates the call-back script on the local server
|
473
|
+
fetch('receiver/', postData({
|
474
|
+
path: this.channel,
|
475
|
+
file: this.file_name,
|
476
|
+
action: 'call-back',
|
477
|
+
script: this.call_back_script
|
478
|
+
}))
|
479
|
+
.then((response) => {
|
480
|
+
if(!response.ok) {
|
481
|
+
UI.alert(`ERROR ${response.status}: ${response.statusText}`);
|
482
|
+
}
|
483
|
+
return response.text();
|
484
|
+
})
|
485
|
+
.then((data) => {
|
486
|
+
// Call-back completed => resume listening unless running experiment
|
487
|
+
if(RECEIVER.experiment) {
|
488
|
+
// For experiments, only display server response if warning or error
|
489
|
+
UI.postResponseOK(data);
|
490
|
+
} else {
|
491
|
+
// Always show server response for single runs
|
492
|
+
if(UI.postResponseOK(data, true)) {
|
493
|
+
// NOTE: resume listening only if no error
|
494
|
+
setTimeout(() => RECEIVER.listen(), RECEIVER.interval);
|
495
|
+
} else {
|
496
|
+
RECEIVER.deactivate();
|
497
|
+
}
|
498
|
+
}
|
499
|
+
})
|
500
|
+
.catch(() => UI.warn(UI.WARNING.NO_CONNECTION, err));
|
501
|
+
}
|
502
|
+
|
503
|
+
} // END of class ConsoleReceiver
|
504
|
+
|
505
|
+
// Receiver helper functions
|
506
|
+
// NOTE: these functions are adapted versions of those having the same
|
507
|
+
// name in file `server.js`; the main difference is that those functions
|
508
|
+
// respond to HTTP requests, whereas now they return objects
|
509
|
+
|
510
|
+
function rcvrListen(rpath) {
|
511
|
+
// "Listens" at the channel, i.e., looks for work to do
|
512
|
+
let mdl = '',
|
513
|
+
cmd = '';
|
514
|
+
try {
|
515
|
+
// Look for a model file and/or a command file in the channel directory
|
516
|
+
const flist = fs.readdirSync(rpath);
|
517
|
+
// NOTE: `flist` contains file names relative to the channel path
|
518
|
+
for(let i = 0; i < flist.length; i++) {
|
519
|
+
const f = path.parse(flist[i]);
|
520
|
+
if(f.ext === '.lnr' && !mdl) mdl = flist[i];
|
521
|
+
if(f.ext === '.lnrc' && !cmd) cmd = flist[i];
|
522
|
+
}
|
523
|
+
} catch(err) {
|
524
|
+
console.log(err);
|
525
|
+
return {error: `Failed to get file list from ${rpath}`};
|
526
|
+
}
|
527
|
+
// Model files take precedence over command files
|
528
|
+
if(mdl) {
|
529
|
+
try {
|
530
|
+
const data = fs.readFileSync(path.join(rpath, mdl), 'utf8');
|
531
|
+
return {file: path.parse(mdl).name, model: data};
|
532
|
+
} catch(err) {
|
533
|
+
console.log(err);
|
534
|
+
return {error: `Failed to read model ${mdl}`};
|
535
|
+
}
|
536
|
+
}
|
537
|
+
if(cmd) {
|
538
|
+
try {
|
539
|
+
cmd = fs.readFileSync(path.join(rpath, cmd), 'utf8').trim();
|
540
|
+
} catch(err) {
|
541
|
+
console.log(err);
|
542
|
+
return {error: `Failed to read command file ${cmd}`};
|
543
|
+
}
|
544
|
+
// Special command to deactivate the receiver
|
545
|
+
if(cmd === 'STOP LISTENING') {
|
546
|
+
return {stop: 1};
|
547
|
+
} else {
|
548
|
+
// For now, command can only be
|
549
|
+
// "[experiment name|]module name[@repository name]"
|
550
|
+
let m = '',
|
551
|
+
r = '',
|
552
|
+
x = '';
|
553
|
+
const m_r = cmd.split('@');
|
554
|
+
// Repository `r` is local host unless specified
|
555
|
+
if(m_r.length === 2) {
|
556
|
+
r = m_r[1];
|
557
|
+
} else if(m_r.length === 1) {
|
558
|
+
r = 'local host';
|
559
|
+
} else {
|
560
|
+
// Multiple occurrences of @
|
561
|
+
return {error: `Invalid command "${cmd}"`};
|
562
|
+
}
|
563
|
+
m = m_r[0];
|
564
|
+
// Module `m` can be prefixed by an experiment title
|
565
|
+
const x_m = m.split('|');
|
566
|
+
if(x_m.length === 2) {
|
567
|
+
x = x_m[0];
|
568
|
+
m = x_m[1];
|
569
|
+
}
|
570
|
+
// Call the repository helper function `repoLoad` with its callback
|
571
|
+
// function to get the model XML
|
572
|
+
return {
|
573
|
+
file: path.parse(cmd).name,
|
574
|
+
model: repoLoad(r.trim(), m.trim()),
|
575
|
+
experiment: x.trim()
|
576
|
+
};
|
577
|
+
}
|
578
|
+
} else {
|
579
|
+
// Empty fields will be interpreted as "nothing to do"
|
580
|
+
return {file: '', model: '', experiment: ''};
|
581
|
+
}
|
582
|
+
}
|
583
|
+
|
584
|
+
function rcvrAbort() {
|
585
|
+
const log_path = path.join(this.channel, this.file_name + '-log.txt');
|
586
|
+
fs.writeFile(log_path, this.logReport, (err) => {
|
587
|
+
if(err) {
|
588
|
+
console.log(err);
|
589
|
+
console.log('ERROR: Failed to write event log to file', log_path);
|
590
|
+
} else {
|
591
|
+
console.log('Remote run aborted');
|
592
|
+
}
|
593
|
+
});
|
594
|
+
}
|
595
|
+
|
596
|
+
function rcvrReport(run='', data='no data', stats='no statistics') {
|
597
|
+
try {
|
598
|
+
let fp = path.join(this.channel, this.file_name + run + '-data.txt');
|
599
|
+
fs.writeFileSync(fp, data);
|
600
|
+
} catch(err) {
|
601
|
+
console.log(err);
|
602
|
+
console.log('ERROR: Failed to write data to file', fp);
|
603
|
+
return;
|
604
|
+
}
|
605
|
+
try {
|
606
|
+
fp = path.join(this.channel, this.file_name + run + '-stats.txt');
|
607
|
+
fs.writeFileSync(fp, stats);
|
608
|
+
} catch(err) {
|
609
|
+
console.log(err);
|
610
|
+
console.log('ERROR: Failed to write statistics to file', fp);
|
611
|
+
return;
|
612
|
+
}
|
613
|
+
try {
|
614
|
+
fp = path.join(this.channel, this.file_name + run + '-log.txt');
|
615
|
+
fs.writeFileSync(fp, this.logReport);
|
616
|
+
} catch(err) {
|
617
|
+
console.log(err);
|
618
|
+
console.log('ERROR: Failed to write event log to file', fp);
|
619
|
+
}
|
620
|
+
console.log('Data and statistics reported for', this.file_name);
|
621
|
+
}
|
622
|
+
|
623
|
+
function rcvrCallBack(script) {
|
624
|
+
let file_type = '',
|
625
|
+
cpath = path.join(this.channel, this.file_name + '.lnr');
|
626
|
+
try {
|
627
|
+
fs.accessSync(cpath);
|
628
|
+
file_type = 'model';
|
629
|
+
} catch(err) {
|
630
|
+
cpath = path.join(this.channel, this.file_name + '.lnrc');
|
631
|
+
try {
|
632
|
+
fs.accessSync(cpath);
|
633
|
+
file_type = 'command';
|
634
|
+
} catch(err) {
|
635
|
+
cpath = '';
|
636
|
+
}
|
637
|
+
}
|
638
|
+
if(cpath) {
|
639
|
+
console.log('Deleting', file_type, ' file:', cpath);
|
640
|
+
try {
|
641
|
+
fs.unlinkSync(cpath);
|
642
|
+
} catch(err) {
|
643
|
+
console.log(err);
|
644
|
+
console.log(`ERROR: Failed to delete ${file_type} file ${rfile}`);
|
645
|
+
return;
|
646
|
+
}
|
647
|
+
}
|
648
|
+
if(!script) {
|
649
|
+
console.log('No call-back script to execute');
|
650
|
+
return;
|
651
|
+
}
|
652
|
+
try {
|
653
|
+
cmd = fs.readFileSync(path.join(WORKSPACE.callback, script), 'utf8');
|
654
|
+
console.log(`Executing callback command "${cmd}"`);
|
655
|
+
child_process.exec(cmd, (error, stdout, stderr) => {
|
656
|
+
console.log(stdout);
|
657
|
+
if(error) {
|
658
|
+
console.log(error);
|
659
|
+
console.log(stderr);
|
660
|
+
console.log(`ERROR: Failed to execute script "${script}"`);
|
661
|
+
} else {
|
662
|
+
console.log(`Call-back script "${script}" executed`);
|
663
|
+
}
|
664
|
+
});
|
665
|
+
} catch(err) {
|
666
|
+
console.log(err);
|
667
|
+
console.log(`WARNING: Call-back script "${script}" not found`);
|
668
|
+
}
|
669
|
+
}
|
670
|
+
|
671
|
+
|
672
|
+
//
|
673
|
+
// Console functions
|
674
|
+
//
|
675
|
+
|
676
|
+
function commandLineSettings() {
|
677
|
+
// Sets default settings, and then checks the command line arguments
|
678
|
+
const settings = {
|
679
|
+
cli_name: (PLATFORM.startsWith('win') ? 'Command Prompt' : 'Terminal'),
|
680
|
+
run: false,
|
681
|
+
preferred_solver: '',
|
682
|
+
solver: '',
|
683
|
+
solver_path: '',
|
684
|
+
user_dir: path.join(MAIN_DIRECTORY, 'user')
|
685
|
+
};
|
686
|
+
let show_usage = process.argv.length < 3;
|
687
|
+
for(let i = 2; i < process.argv.length; i++) {
|
688
|
+
const lca = process.argv[i].toLowerCase();
|
689
|
+
if(lca === 'help' || lca === '?' || lca.startsWith('-')) {
|
690
|
+
show_usage = true;
|
691
|
+
} else if(lca === 'run') {
|
692
|
+
settings.run = true;
|
693
|
+
} else if(lca === 'verbose') {
|
694
|
+
settings.verbose = true;
|
695
|
+
} else {
|
696
|
+
const av = lca.split('=');
|
697
|
+
if(av.length === 1) av.push('');
|
698
|
+
if(av[0] === 'solver') {
|
699
|
+
if(av[1] !== 'gurobi' && av[1] !== 'lp_solve') {
|
700
|
+
console.log(`WARNING: Unknown solver "${av[1]}"`);
|
701
|
+
} else {
|
702
|
+
settings.preferred_solver = av[1];
|
703
|
+
}
|
704
|
+
} else if(av[0] === 'workspace') {
|
705
|
+
// User directory must be READ/WRITE-accessible
|
706
|
+
try {
|
707
|
+
fs.accessSync(av[1], fs.constants.R_OK | fs.constants.W_O);
|
708
|
+
} catch(err) {
|
709
|
+
console.log(`ERROR: No access to directory "${av[1]}"`);
|
710
|
+
process.exit();
|
711
|
+
}
|
712
|
+
settings.user_dir = av[1];
|
713
|
+
} else if(av[0] === 'user') {
|
714
|
+
// User identifier (mail adress)
|
715
|
+
settings.user_mail = av[1];
|
716
|
+
} else if(av[0] === 'password') {
|
717
|
+
// Decryption password -- not the user's password!
|
718
|
+
settings.password = av[1];
|
719
|
+
} else if(av[0] === 'model') {
|
720
|
+
// Validate model path
|
721
|
+
try {
|
722
|
+
// Add default extension if no extension specified
|
723
|
+
if(av[1].indexOf('.') < 0) av[1] += '.lnr';
|
724
|
+
mp = path.parse(av[1]);
|
725
|
+
if(mp.ext !== '.lnr') {
|
726
|
+
console.log('WARNING: Model file should have extension .lnr');
|
727
|
+
}
|
728
|
+
fs.accessSync(av[1], fs.constants.R_OK);
|
729
|
+
settings.model_path = av[1];
|
730
|
+
} catch(err) {
|
731
|
+
console.log(`ERROR: File "${av[1]}" not found`);
|
732
|
+
process.exit();
|
733
|
+
}
|
734
|
+
} else if(av[0] === 'module') {
|
735
|
+
// Add default repository is none specified
|
736
|
+
if(av[1].indexOf('@') < 0) av[1] += '@local host';
|
737
|
+
// Check is repository exists, etc.
|
738
|
+
// @@@TO DO!
|
739
|
+
} else if(av[0] === 'xrun') {
|
740
|
+
// NOTE: use original argument to preserve upper/lower case
|
741
|
+
const x = process.argv[i].split('=')[1].split('#');
|
742
|
+
settings.x_title = x[0];
|
743
|
+
settings.x_runs = [];
|
744
|
+
x.splice(0, 1);
|
745
|
+
// In case of multiple #, interpret them as commas
|
746
|
+
const r = x.join(',').split(',');
|
747
|
+
for(let i = 0; i < r.length; i++) {
|
748
|
+
if(/^\d+$/.test(r[i])) {
|
749
|
+
settings.x_runs.push(parseInt(r[i]));
|
750
|
+
} else {
|
751
|
+
console.log(`WARNING: Invalid run number "${r[i]}"`);
|
752
|
+
}
|
753
|
+
}
|
754
|
+
// If only invalid numbers, do not run the experiment at all
|
755
|
+
if(r.length > 0 && settings.x_runs === 0) {
|
756
|
+
console.log(`Experiment "${settings.x_title}" will not be run`);
|
757
|
+
settings.x_title = '';
|
758
|
+
}
|
759
|
+
} else {
|
760
|
+
// Terminate script
|
761
|
+
console.log(
|
762
|
+
`ERROR: Invalid command line argument "${process.argv[i]}"`);
|
763
|
+
show_usage = true;
|
764
|
+
}
|
765
|
+
}
|
766
|
+
}
|
767
|
+
// If help is asked for, or command is invalid, show usage and then quit
|
768
|
+
if(show_usage) {
|
769
|
+
console.log(usage);
|
770
|
+
process.exit();
|
771
|
+
}
|
772
|
+
// Check whether MILP solver(s) and Inkscape have been installed
|
773
|
+
const path_list = process.env.PATH.split(path.delimiter);
|
774
|
+
let gurobi_path = '',
|
775
|
+
match,
|
776
|
+
max_v = -1;
|
777
|
+
for(let i = 0; i < path_list.length; i++) {
|
778
|
+
match = path_list[i].match(/gurobi(\d+)/i);
|
779
|
+
if(match && parseInt(match[1]) > max_v) {
|
780
|
+
gurobi_path = path_list[i];
|
781
|
+
max_v = parseInt(match[1]);
|
782
|
+
}
|
783
|
+
match = path_list[i].match(/inkscape/i);
|
784
|
+
if(match) settings.inkscape = path_list[i];
|
785
|
+
}
|
786
|
+
if(!gurobi_path && !PLATFORM.startsWith('win')) {
|
787
|
+
console.log('Looking for Gurobi in /usr/local/bin');
|
788
|
+
try {
|
789
|
+
// On macOS and Unix, Gurobi is in the user's local binaries
|
790
|
+
const gp = '/usr/local/bin';
|
791
|
+
fs.accessSync(gp + '/gurobi_cl');
|
792
|
+
gurobi_path = gp;
|
793
|
+
} catch(err) {
|
794
|
+
// No real error, so no action needed
|
795
|
+
}
|
796
|
+
}
|
797
|
+
if(gurobi_path) {
|
798
|
+
console.log('Path to Gurobi:', gurobi_path);
|
799
|
+
// Check if command line version is executable
|
800
|
+
const sp = path.join(gurobi_path,
|
801
|
+
'gurobi_cl' + (PLATFORM.startsWith('win') ? '.exe' : ''));
|
802
|
+
try {
|
803
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
804
|
+
if(settings.solver !== 'gurobi')
|
805
|
+
settings.solver = 'gurobi';
|
806
|
+
settings.solver_path = sp;
|
807
|
+
} catch(err) {
|
808
|
+
console.log(err.message);
|
809
|
+
console.log(
|
810
|
+
'WARNING: Failed to access the Gurobi command line application');
|
811
|
+
}
|
812
|
+
}
|
813
|
+
// Check if lp_solve(.exe) exists in main directory
|
814
|
+
const
|
815
|
+
sp = path.join(MAIN_DIRECTORY,
|
816
|
+
'lp_solve' + (PLATFORM.startsWith('win') ? '.exe' : '')),
|
817
|
+
need_lps = !settings.solver || settings.preferred_solver === 'lp_solve';
|
818
|
+
try {
|
819
|
+
fs.accessSync(sp, fs.constants.X_OK);
|
820
|
+
console.log('Path to LP_solve:', sp);
|
821
|
+
if(need_lps) {
|
822
|
+
settings.solver = 'lp_solve';
|
823
|
+
settings.solver_path = sp;
|
824
|
+
}
|
825
|
+
} catch(err) {
|
826
|
+
// Only report error if LP_solve is needed
|
827
|
+
if(need_lps) {
|
828
|
+
console.log(err.message);
|
829
|
+
console.log('WARNING: LP_solve application not found in', sp);
|
830
|
+
}
|
831
|
+
}
|
832
|
+
return settings;
|
833
|
+
}
|
834
|
+
|
835
|
+
function createWorkspace() {
|
836
|
+
// Verifies that Linny-R has write access to the user workspace, defines
|
837
|
+
// paths to sub-directories, and creates them if necessary
|
838
|
+
try {
|
839
|
+
// See whether the user directory already exists
|
840
|
+
try {
|
841
|
+
fs.accessSync(SETTINGS.user_dir, fs.constants.R_OK | fs.constants.W_O);
|
842
|
+
} catch(err) {
|
843
|
+
// If not, try to create it
|
844
|
+
fs.mkdirSync(SETTINGS.user_dir);
|
845
|
+
console.log('Created user directory:', SETTINGS.user_dir);
|
846
|
+
}
|
847
|
+
} catch(err) {
|
848
|
+
console.log(err.message);
|
849
|
+
console.log('FATAL ERROR: Failed to create user workspace in',
|
850
|
+
SETTINGS.user_dir);
|
851
|
+
process.exit();
|
852
|
+
}
|
853
|
+
// Define the sub-directory paths
|
854
|
+
const ws = {
|
855
|
+
channel: path.join(SETTINGS.user_dir, 'channel'),
|
856
|
+
callback: path.join(SETTINGS.user_dir, 'callback'),
|
857
|
+
diagrams: path.join(SETTINGS.user_dir, 'diagrams'),
|
858
|
+
modules: path.join(SETTINGS.user_dir, 'modules'),
|
859
|
+
solver_output: path.join(SETTINGS.user_dir, 'solver'),
|
860
|
+
};
|
861
|
+
// Create these sub-directories if not aready there
|
862
|
+
try {
|
863
|
+
for(let p in ws) if(ws.hasOwnProperty(p)) {
|
864
|
+
try {
|
865
|
+
fs.accessSync(ws[p]);
|
866
|
+
} catch(e) {
|
867
|
+
fs.mkdirSync(ws[p]);
|
868
|
+
console.log('Created workspace sub-directory:', ws[p]);
|
869
|
+
}
|
870
|
+
}
|
871
|
+
} catch(err) {
|
872
|
+
console.log(err.message);
|
873
|
+
console.log('WARNING: No access to workspace directory');
|
874
|
+
}
|
875
|
+
// The file containing name, URL and access token for remote repositories
|
876
|
+
ws.repositories = path.join(SETTINGS.user_dir, 'repositories.cfg');
|
877
|
+
// Return the updated workspace object
|
878
|
+
return ws;
|
879
|
+
}
|
880
|
+
|
881
|
+
// Initialize the solver
|
882
|
+
const SOLVER = new MILPSolver(SETTINGS, WORKSPACE);
|
883
|
+
/*
|
884
|
+
// Initialize the dialog for interaction with the user
|
885
|
+
const PROMPTER = readline.createInterface(
|
886
|
+
{input: process.stdin, output: process.stdout});
|
887
|
+
PROMPTER._writeToOutput = function _writeToOutput(str) {
|
888
|
+
if (PROMPTER.stdoutMuted && !PROMPTER.questionPrompt(str)) {
|
889
|
+
PROMPTER.output.write("*");
|
890
|
+
} else {
|
891
|
+
PROMPTER.output.write(str);
|
892
|
+
}
|
893
|
+
};
|
894
|
+
PROMPTER.prompt_phrases = {
|
895
|
+
access_code: 'Access code: ',
|
896
|
+
password: 'Password: '
|
897
|
+
};
|
898
|
+
PROMPTER.questionPrompt = (str) => {
|
899
|
+
const pp = PROMPTER.prompt_phrases;
|
900
|
+
for(let k in pp) if (pp.hasOwnProperty(k)) {
|
901
|
+
if(str === pp[k]) return true;
|
902
|
+
}
|
903
|
+
return false;
|
904
|
+
};
|
905
|
+
// NOTE: for password prompts, mute the output like so:
|
906
|
+
//PROMPTER.stdoutMuted = true;
|
907
|
+
*/
|
908
|
+
|
909
|
+
// Initialize the Linny-R console components as global variables
|
910
|
+
global.UI = new Controller();
|
911
|
+
global.VM = new VirtualMachine();
|
912
|
+
//global.REPOSITORY_BROWSER = new ConsoleRepositoryBrowser(), // still to re-code
|
913
|
+
global.FILE_MANAGER = new ConsoleFileManager();
|
914
|
+
global.DATASET_MANAGER = new DatasetManager();
|
915
|
+
global.CHART_MANAGER = new ChartManager();
|
916
|
+
global.SENSITIVITY_ANALYSIS = new SensitivityAnalysis();
|
917
|
+
global.EXPERIMENT_MANAGER = new ExperimentManager();
|
918
|
+
global.MONITOR = new ConsoleMonitor();
|
919
|
+
global.RECEIVER = new ConsoleReceiver();
|
920
|
+
global.IO_CONTEXT = null;
|
921
|
+
global.MODEL = new LinnyRModel();
|
922
|
+
|
923
|
+
// Connect the virtual machine (may prompt for password)
|
924
|
+
MONITOR.connectToServer();
|
925
|
+
|
926
|
+
// Load the model if specified
|
927
|
+
if(SETTINGS.model_path) {
|
928
|
+
FILE_MANAGER.loadModel(SETTINGS.model_path, (model) => {
|
929
|
+
// Command `run` takes precedence over `xrun`
|
930
|
+
if(SETTINGS.run) {
|
931
|
+
MONITOR.show_log = SETTINGS.verbose;
|
932
|
+
VM.callback = () => {
|
933
|
+
const od = model.outputData;
|
934
|
+
console.log(od[0]);
|
935
|
+
console.log(od[1]);
|
936
|
+
VM.callback = null;
|
937
|
+
};
|
938
|
+
VM.solveModel();
|
939
|
+
} else if(SETTINGS.x_title) {
|
940
|
+
const xi = MODEL.indexOfExperiment(SETTINGS.x_title);
|
941
|
+
if(xi < 0) {
|
942
|
+
console.log(`WARNING: Unknown experiment "${SETTINGS.x_title}"`);
|
943
|
+
} else {
|
944
|
+
EXPERIMENT_MANAGER.selectExperiment(SETTINGS.x_title);
|
945
|
+
EXPERIMENT_MANAGER.callback = () => {
|
946
|
+
const od = model.outputData;
|
947
|
+
console.log(od[0]);
|
948
|
+
console.log(od[1]);
|
949
|
+
VM.callback = null;
|
950
|
+
};
|
951
|
+
if(SETTINGS.x_runs.length === 0) {
|
952
|
+
// Perform complete experiment
|
953
|
+
EXPERIMENT_MANAGER.startExperiment();
|
954
|
+
} else {
|
955
|
+
// Announce, and then perform, only the selected runs
|
956
|
+
console.log('Experiment:', SETTINGS.x_title,
|
957
|
+
'Runs:', SETTINGS.x_runs);
|
958
|
+
for(let i = 0; i < SETTINGS.x_runs.length; i++) {
|
959
|
+
EXPERIMENT_MANAGER.startExperiment(SETTINGS.x_runs[i]);
|
960
|
+
}
|
961
|
+
}
|
962
|
+
}
|
963
|
+
}
|
964
|
+
});
|
965
|
+
}
|
966
|
+
|
967
|
+
/*
|
968
|
+
console.log('Prompting');
|
969
|
+
PROMPTER.question(PROMPTER.access_code, (code) => {
|
970
|
+
SETTINGS.code = code;
|
971
|
+
PROMPTER.close();
|
972
|
+
});
|
973
|
+
*/
|