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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/console.js +973 -0
  4. package/package.json +32 -0
  5. package/server.js +1547 -0
  6. package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
  7. package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
  8. package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
  9. package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
  10. package/static/fonts/Hack-Bold.ttf +0 -0
  11. package/static/fonts/Hack-BoldItalic.ttf +0 -0
  12. package/static/fonts/Hack-Italic.ttf +0 -0
  13. package/static/fonts/Hack-Regular.ttf +0 -0
  14. package/static/fonts/Lato-Bold.ttf +0 -0
  15. package/static/fonts/Lato-BoldItalic.ttf +0 -0
  16. package/static/fonts/Lato-Italic.ttf +0 -0
  17. package/static/fonts/Lato-Regular.ttf +0 -0
  18. package/static/fonts/mplus-1m-bold.ttf +0 -0
  19. package/static/fonts/mplus-1m-light.ttf +0 -0
  20. package/static/fonts/mplus-1m-medium.ttf +0 -0
  21. package/static/fonts/mplus-1m-regular.ttf +0 -0
  22. package/static/fonts/mplus-1m-thin.ttf +0 -0
  23. package/static/images/access.png +0 -0
  24. package/static/images/actor.png +0 -0
  25. package/static/images/actors.png +0 -0
  26. package/static/images/add-selector.png +0 -0
  27. package/static/images/add.png +0 -0
  28. package/static/images/back.png +0 -0
  29. package/static/images/black-box.png +0 -0
  30. package/static/images/by-sa.svg +74 -0
  31. package/static/images/cancel.png +0 -0
  32. package/static/images/chart.png +0 -0
  33. package/static/images/check-disab.png +0 -0
  34. package/static/images/check-off.png +0 -0
  35. package/static/images/check-on.png +0 -0
  36. package/static/images/check-x.png +0 -0
  37. package/static/images/clone.png +0 -0
  38. package/static/images/close.png +0 -0
  39. package/static/images/cluster.png +0 -0
  40. package/static/images/compare.png +0 -0
  41. package/static/images/compress.png +0 -0
  42. package/static/images/constraint.png +0 -0
  43. package/static/images/copy.png +0 -0
  44. package/static/images/data-to-clpbrd.png +0 -0
  45. package/static/images/dataset.png +0 -0
  46. package/static/images/delete.png +0 -0
  47. package/static/images/diagram.png +0 -0
  48. package/static/images/down.png +0 -0
  49. package/static/images/edit-chart.png +0 -0
  50. package/static/images/edit.png +0 -0
  51. package/static/images/eq.png +0 -0
  52. package/static/images/equation.png +0 -0
  53. package/static/images/experiment.png +0 -0
  54. package/static/images/favicon.ico +0 -0
  55. package/static/images/fewer-dec.png +0 -0
  56. package/static/images/filter.png +0 -0
  57. package/static/images/find.png +0 -0
  58. package/static/images/forward.png +0 -0
  59. package/static/images/host-logo.png +0 -0
  60. package/static/images/icon.png +0 -0
  61. package/static/images/icon.svg +23 -0
  62. package/static/images/ignore.png +0 -0
  63. package/static/images/include.png +0 -0
  64. package/static/images/info-to-clpbrd.png +0 -0
  65. package/static/images/info.png +0 -0
  66. package/static/images/is-black-box.png +0 -0
  67. package/static/images/lbl.png +0 -0
  68. package/static/images/lift.png +0 -0
  69. package/static/images/link.png +0 -0
  70. package/static/images/linny-r.icns +0 -0
  71. package/static/images/linny-r.ico +0 -0
  72. package/static/images/linny-r.png +0 -0
  73. package/static/images/linny-r.svg +21 -0
  74. package/static/images/logo.png +0 -0
  75. package/static/images/model-info.png +0 -0
  76. package/static/images/module.png +0 -0
  77. package/static/images/monitor.png +0 -0
  78. package/static/images/more-dec.png +0 -0
  79. package/static/images/ne.png +0 -0
  80. package/static/images/new.png +0 -0
  81. package/static/images/note.png +0 -0
  82. package/static/images/ok.png +0 -0
  83. package/static/images/open.png +0 -0
  84. package/static/images/outcome.png +0 -0
  85. package/static/images/parent.png +0 -0
  86. package/static/images/paste.png +0 -0
  87. package/static/images/pause.png +0 -0
  88. package/static/images/print-chart.png +0 -0
  89. package/static/images/print.png +0 -0
  90. package/static/images/process.png +0 -0
  91. package/static/images/product.png +0 -0
  92. package/static/images/pwlf.png +0 -0
  93. package/static/images/receiver.png +0 -0
  94. package/static/images/redo.png +0 -0
  95. package/static/images/remove.png +0 -0
  96. package/static/images/rename.png +0 -0
  97. package/static/images/repo-logo.png +0 -0
  98. package/static/images/repository.png +0 -0
  99. package/static/images/reset.png +0 -0
  100. package/static/images/resize.png +0 -0
  101. package/static/images/restore.png +0 -0
  102. package/static/images/save-chart.png +0 -0
  103. package/static/images/save-data.png +0 -0
  104. package/static/images/save-diagram.png +0 -0
  105. package/static/images/save.png +0 -0
  106. package/static/images/sensitivity.png +0 -0
  107. package/static/images/settings.png +0 -0
  108. package/static/images/solve.png +0 -0
  109. package/static/images/solver-logo.png +0 -0
  110. package/static/images/stats-to-clpbrd.png +0 -0
  111. package/static/images/stats.png +0 -0
  112. package/static/images/stop.png +0 -0
  113. package/static/images/store.png +0 -0
  114. package/static/images/stretch.png +0 -0
  115. package/static/images/table-to-clpbrd.png +0 -0
  116. package/static/images/table.png +0 -0
  117. package/static/images/tree.png +0 -0
  118. package/static/images/tudelft.png +0 -0
  119. package/static/images/ubl.png +0 -0
  120. package/static/images/undo.png +0 -0
  121. package/static/images/up.png +0 -0
  122. package/static/images/zoom-in.png +0 -0
  123. package/static/images/zoom-out.png +0 -0
  124. package/static/index.html +3088 -0
  125. package/static/linny-r.css +4722 -0
  126. package/static/scripts/iro.min.js +7 -0
  127. package/static/scripts/linny-r-config.js +105 -0
  128. package/static/scripts/linny-r-ctrl.js +1199 -0
  129. package/static/scripts/linny-r-gui.js +14814 -0
  130. package/static/scripts/linny-r-milp.js +286 -0
  131. package/static/scripts/linny-r-model.js +10405 -0
  132. package/static/scripts/linny-r-utils.js +687 -0
  133. package/static/scripts/linny-r-vm.js +7079 -0
  134. package/static/show-diff.html +84 -0
  135. package/static/show-png.html +113 -0
  136. package/static/sounds/error.wav +0 -0
  137. package/static/sounds/notification.wav +0 -0
  138. 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
+ */