1 | #!/usr/bin/env node
|
2 | // Concepto TI DSL LIVE
|
3 | /*
|
4 | - inicialmente asociado a ti.dsl (hacer agnostico despues)
|
5 |
|
6 | 1. saludar en consola (todo hacer config.json para considerar opciones o usar argumentos)
|
7 | 1.1 se debe ejecutar en la carpeta del proyecto donde estan los archivos 'live_*.json' :: OK!
|
8 | 2. se debe conectar (o intentar cada x segundos) a Concepto en modo Live (usando smart.js->bridge) :: OK!
|
9 | 3. se debe conectar (o intentar cada x segundos) a TiShadow socket (puerto 3000) (usando ts.js)
|
10 | 4. cuando el punto 4 y 5 esten ok:
|
11 | 4.1 debe leer archivo app/live_properties.json de proyecto actual y dejar en memoria. :: OK!
|
12 | 4.2 debe leer archivo app/live_styles.json de proyecto actual y dejar en memoria.
|
13 | 5. leer cada transaccion y llamar funcion procesarTransaccion(nodo) :: OK!
|
14 | 5.1 ver si nodeid existe como llave en live_properties de memoria (4.2). :: OK!
|
15 | 5.2 ve si atributo tiene match en registro identificado de live_properties del punto 5.1 :: OK!
|
16 | 5.3 reemplazar de contenidos texto 'nodeid' por valor de nuestro nodo (getSpy('nodeid').) y dejar string en temporal. :: OK!
|
17 | 5.3 si tiene llave 'before_set', asignar valor de atributo a var temp 'setvalue' y asignar a string a enviar. :: OK!
|
18 | 5.4 si tiene llave 'set' asignar contenido a string a enviar: ej: getSpy('nodeid').setTitle(setvalue).. :: OK!
|
19 | */
|
20 |
|
21 | var bridge = require('./smart');
|
22 | var tis = require('./ts.js');
|
23 |
|
24 | var apple = require('./apple');
|
25 |
|
26 | var notifier = require('node-notifier');
|
27 | var watch = require('node-watch'); // watched file changes.
|
28 |
|
29 | var jsonfile = require('jsonfile')
|
30 | var fs = require('fs');
|
31 | var path = require('path');
|
32 | var cheerio = require('cheerio');
|
33 | var _colors = require('colors');
|
34 | var pkginfo = require('pkginfo')(module);
|
35 |
|
36 | var args = process.argv.slice(2), cwd = '', lprop = '';
|
37 | var nodes = {}, nodes_meta = {}, last_cmd = '';
|
38 | var $;
|
39 | // define here all files that must be read (key)->live_key.json
|
40 | var _live = {
|
41 | properties : {},
|
42 | //style : {}
|
43 | };
|
44 | var _config = {
|
45 | title: 'Concepto DSL Live Mode',
|
46 | dsl: 'ti.dsl',
|
47 | ti_connected: false,
|
48 | live_retry_delay: 2000
|
49 | };
|
50 |
|
51 | if (args.length>0) {
|
52 | // tiene un argumento (directorio del proyecto)
|
53 | cwd = args[0];
|
54 | } else {
|
55 | // no tiene un argumento, intentamos usar directorio en donde estamos
|
56 | cwd = process.cwd();
|
57 | }
|
58 |
|
59 | // SEND WELCOME MESSAGE
|
60 | var _say_app_name = cwd.split(path.sep)[directory.split(path.sep).length-1].split('_').join(' ').split('-').join(' ');
|
61 | consoleTitle('Concepto DSL Live Mode Bridge :: ver '+module.exports.version,'yellow','magenta');
|
62 | apple.say('Laiv iniciado para aplicacion '+_say_app_name);
|
63 |
|
64 | // READ DSL AND JSON FILES
|
65 | loadLive(cwd, function(){
|
66 | // read json files
|
67 | loadJSON(cwd);
|
68 | });
|
69 |
|
70 | var _startAll = function() {
|
71 | if (last_cmd!='_startAll') {
|
72 | console.log('bridge:on'.underline.green);
|
73 | // if live on, execute update on json properties : 14-1-2016 PSB
|
74 | loadJSON(cwd);
|
75 | // TI SHADOW SOCKET
|
76 | tis.disconnect();
|
77 | var socket = tis.connect(
|
78 | function(e) {
|
79 | //console.log("shadow:connected.\n".green);
|
80 | _config.ti_connected = true;
|
81 | // try to start Concepto Live Bridge
|
82 | bridge.init(function(){});
|
83 | //
|
84 | },
|
85 | function(msg) {
|
86 | console.log("shadow:message received:".green,msg);
|
87 | },
|
88 | function() {
|
89 | // on error
|
90 | //console.log("shadow:disconnected.\n".red);
|
91 | _config.ti_connected = false;
|
92 | }
|
93 | );
|
94 | last_cmd = '_startAll';
|
95 | }
|
96 | };
|
97 |
|
98 | var _stopAll = function() {
|
99 | if (last_cmd!='_stopAll') {
|
100 | console.log('bridge:off'.underline.red+'\n\n');
|
101 | if (_config.ti_connected==true) {
|
102 | // desconectamos ti_shadow
|
103 | tis.disconnect();
|
104 | //console.log("shadow:disconnected.\n\n".green);
|
105 | }
|
106 | last_cmd = '_stopAll';
|
107 | }
|
108 | };
|
109 |
|
110 | // MONITOR OPEN PORTS
|
111 | var portastic = require('portastic');
|
112 | var monitor = new portastic.Monitor([3000,9000]);
|
113 | var _live_running = false, _ts_running = false;
|
114 |
|
115 | monitor.on('close', function(port) {
|
116 | // seems monitos close/open are inverted.
|
117 | if (port==9000) {
|
118 | _live_running = true;
|
119 | //console.log('concepto live seems running');
|
120 | } else {
|
121 | // tishadow server is online
|
122 | _ts_running = true;
|
123 | //console.log('tishadow seems running');
|
124 | }
|
125 | // only run if both ports are opened
|
126 | if (_live_running==true && _ts_running==true) {
|
127 | //console.log('both servers seems running: calling startAll');
|
128 | _startAll();
|
129 | }
|
130 | });
|
131 | monitor.on('open', function(port) {
|
132 | if (port==9000) {
|
133 | // concepto mode live is deactivated
|
134 | _live_running = false;
|
135 | //console.log('concepto live seems stop');
|
136 | } else {
|
137 | // tishadow server is offline
|
138 | _ts_running = false;
|
139 | //console.log('tishadow seems stop');
|
140 | }
|
141 | //
|
142 | _stopAll();
|
143 | });
|
144 |
|
145 | // WATCH FILE CHANGES (ti.dsl and live_*.json)
|
146 | var files_to_watch = [path.join(cwd,'ti.dsl')];
|
147 | for (var el in _live) {
|
148 | files_to_watch.push('live_'+el+'.json');
|
149 | var ltmp = path.join(cwd, 'live_'+el+'.json');
|
150 | }
|
151 | watch(files_to_watch, function(file) {
|
152 | // we should call LoadLive(cwd);
|
153 | console.log('watch:file:'.grey+file.yellow+', changed.');
|
154 | if (file.indexOf('.json')!=-1) {
|
155 | loadJSON(cwd);
|
156 | console.log('watch:json reloaded');
|
157 | } else {
|
158 | loadLive(cwd, function(){}, function(){});
|
159 | console.log('watch:dsl reloaded');
|
160 | }
|
161 | });
|
162 |
|
163 |
|
164 | // CONCEPTO DSL LIVE MODE (bridge) events
|
165 | var slow_ping = 0;
|
166 | bridge.on({
|
167 | '_error' : function(error) {
|
168 | // error connecting to concepto - bridge
|
169 | //console.log('live:error connecting:retrying in 3 seconds'.grey);
|
170 | /*setTimeout(function(){
|
171 | bridge.destroy();
|
172 | bridge.init(function(){});
|
173 | }, 3000);*/
|
174 | },
|
175 | 'who_are_you' : function(data) {
|
176 | bridge.send.login('nexo');
|
177 | console.log('live:login sent'.yellow);
|
178 | // Concepto DSL Bridge Ready
|
179 | apple.say('Laiv conectado.');
|
180 | console.log('live:welcome'.green);
|
181 | notifier.notify({
|
182 | 'title': _config.title,
|
183 | 'message': 'Bridge Activated',
|
184 | sound: false
|
185 | });
|
186 | },
|
187 | 'user_information' : function(data) {
|
188 | slow_ping++;
|
189 | if (slow_ping%5==0) {
|
190 | console.log('live:ping ..'.grey);
|
191 | }
|
192 | },
|
193 | 'welcome' : function(data) {
|
194 | //console.log('live:welcome:'.green,data);
|
195 | },
|
196 | 'goodbye' : function(data) {
|
197 | apple.say('laiv desconectado.');
|
198 | console.log('live:goodbye'.yellow);
|
199 | },
|
200 | 'transaction' : function(data) {
|
201 | if (bridge.getLastID()!=data['$'].id) {
|
202 | // solo ejecutamos transaccion si no fuimos nosotros mismos.
|
203 | bridge.parseXml(data['$'].do_action, function(tag, js) {
|
204 | var nodeid = '';
|
205 | if ('$' in js && 'node' in js['$']) nodeid = js['$'].node;
|
206 | if (nodeid!='') {
|
207 | var accion = tag.split('_').join(' ');
|
208 | switch(tag) {
|
209 | case 'insert_attribute_elementary_action':
|
210 | case 'set_attribute_name_elementary_action':
|
211 | var _row = ('row' in js.$)? eval(js.$.row) : 0;
|
212 | //console.log('fila en donde quiero insertar atributo:'+_row);
|
213 | if (nodeid in nodes==false) nodes[nodeid] = [];
|
214 | if (!nodes[nodeid][_row]) nodes[nodeid][_row]={};
|
215 | nodes[nodeid][_row].name = js.$.name;
|
216 | break;
|
217 | case 'edit_node_action':
|
218 | case 'set_attribute_value_elementary_action':
|
219 | if ('text' in js) {
|
220 | // se solicito modificar el nodo: text (usamos nodes_meta)
|
221 | if (nodeid in nodes_meta==false) nodes_meta[nodeid] = { _text:'', _color:'', _bgcolor:'', _icons:'' };
|
222 | if (js.text[0].indexOf('<img')!=-1) {
|
223 | // nodo cambio de imagen: done 14-1-2016
|
224 | var che_img = cheerio.load(js.text[0], { recognizeSelfClosing:true });
|
225 | var just_img = che_img('img[src]').attr('src');
|
226 | var full_img = path.join(cwd,just_img);
|
227 | if (fileExists(full_img)==true) {
|
228 | nodes_meta[nodeid]._image = fs.readFileSync(full_img).toString('base64');
|
229 | procesarTransaccion('nodes_meta',nodeid,nodes_meta[nodeid],'_image', nodes_meta[nodeid]._image);
|
230 | } else {
|
231 | console.log('changed image for nodeid '+nodeid+' was not found on disk ('+full_img+')');
|
232 | }
|
233 |
|
234 | } else {
|
235 | // nodo de texto simple
|
236 | nodes_meta[nodeid]._text = js.text[0];
|
237 | procesarTransaccion('nodes_meta',nodeid,nodes_meta[nodeid],'_text', js.text[0]);
|
238 | //console.log('cambiaron meta datos de nodo: '+nodeid,nodes_meta[nodeid]);
|
239 | }
|
240 | } else {
|
241 | // asigna valor a atributo ya existente: da value
|
242 | var _row = ('row' in js.$)? eval(js.$.row) : 0;
|
243 | //console.log('fila en donde quiero modificar atributo:'+_row);
|
244 | if (nodeid in nodes==false) nodes[nodeid] = [];
|
245 | if (!nodes[nodeid][_row]) nodes[nodeid][_row]={};
|
246 | nodes[nodeid][_row].value = js.value[0];
|
247 | procesarTransaccion('nodes',nodeid,nodes[nodeid][_row],nodes[nodeid][_row].name, js.value[0]);
|
248 | }
|
249 | break;
|
250 | case 'delete_attribute_elementary_action':
|
251 | // eliminamos el atributo row indicado
|
252 | var _row = ('row' in js.$)? eval(js.$.row) : 0;
|
253 | if (nodeid in nodes==false) nodes[nodeid] = [];
|
254 | if (nodes[nodeid][_row]) nodes[nodeid].splice(_row,1);
|
255 | break;
|
256 | case 'node_color_format_action':
|
257 | // nodo cambio de color, lo definimos como un nodes_meta atributo _color
|
258 | if (nodeid in nodes_meta==false) nodes_meta[nodeid] = { _text:'', _color:'', _bgcolor:'', _icons:'' };
|
259 | nodes_meta[nodeid]._color = js.$.color;
|
260 | procesarTransaccion('nodes_meta',nodeid,nodes_meta[nodeid],'_color',js.$.color);
|
261 | //console.log('cambiaron meta datos de nodo: '+nodeid,nodes_meta[nodeid]);
|
262 | break;
|
263 | case 'node_background_color_format_action':
|
264 | // nodo cambio de bgcolor
|
265 | if (nodeid in nodes_meta==false) nodes_meta[nodeid] = { _text:'', _color:'', _bgcolor:'', _icons:'' };
|
266 | nodes_meta[nodeid]._bgcolor = js.$.color;
|
267 | procesarTransaccion('nodes_meta',nodeid,nodes_meta[nodeid],'_bgcolor',js.$.color);
|
268 | //console.log('cambiaron meta datos de nodo: '+nodeid,nodes_meta[nodeid]);
|
269 | break;
|
270 | default:
|
271 | console.log('switch aun no implementado:' + tag.red);
|
272 | logfull('*** ACCION '+tag+' SOLICITADA SOBRE NODE ID: '+nodeid+':',js);
|
273 | break;
|
274 | }
|
275 | //console.log('nodo modificado ('.yellow+nodeid.green+'):'.yellow,nodes[nodeid]);
|
276 | //logfull('*** ACCION '+tag+' SOLICITADA SOBRE NODE ID: '+nodeid+':',js);
|
277 | //apple.say('Accion '+(tag.split('_').join(' '))+' solicitada en nodo.');
|
278 | }
|
279 | });
|
280 | }
|
281 | //console.log('dump de transaction:',data);
|
282 | },
|
283 | '*' : function(data, comando) {
|
284 | console.log('*** EVENTO GENERICO DETECTADO (cmd:'+comando+'): ***', data);
|
285 | }
|
286 | });
|
287 |
|
288 | // *****************
|
289 | // helper methods
|
290 | // *****************
|
291 | function procesarTransaccion(type, nodeid, attributes, changed, value) {
|
292 | // type: nodes, nodes_meta
|
293 | // changed: attributes names that changed
|
294 | console.log('process ('.green+_colors.white(nodeid)+'):'.green+_colors.yellow(type)+'('+_colors.red(changed)+'):'.yellow,attributes);
|
295 | // traducimos a codigo para shadow
|
296 | if (nodeid in _live.properties && changed in _live.properties[nodeid]) {
|
297 | var _live_prop = _live.properties[nodeid][changed];
|
298 | //logfull('dsl.cfc data for node:'.yellow, _live_prop);
|
299 | var _code = '';
|
300 | if (!isNaN(value)) {
|
301 | _code = "var setvalue = "+value+";\n";
|
302 | } else if (value.indexOf("'")!=-1 || value.indexOf('"')!=-1) {
|
303 | // value usa comillas (dejamos tal cual)
|
304 | _code = "var setvalue = "+value+";\n";
|
305 | } else {
|
306 | _code = "var setvalue = '"+value+"';\n";
|
307 | }
|
308 | if (value!='') {
|
309 | // solo procesar si valor no esta completamente en blanco.
|
310 | if ('BEFORE_SET' in _live_prop) _code += _live_prop['BEFORE_SET'];
|
311 | if ('before_set' in _live_prop) _code += _live_prop['before_set'];
|
312 | if ('SET' in _live_prop) _code += 'getSpy("' + nodeid + '").' + _live_prop['SET'] + '(setvalue);';
|
313 | if ('set' in _live_prop) _code += 'getSpy("' + nodeid + '").' + _live_prop['set'] + '(setvalue);';
|
314 | if ('CUSTOMSET' in _live_prop) _code += _live_prop['CUSTOMSET'] + '(setvalue);';
|
315 | if ('customset' in _live_prop) _code += _live_prop['customset'] + '(setvalue);';
|
316 | _code = _code.split('setvalue').join('_setvalue');
|
317 | _code = _code.split('nodeid').join('getSpy("'+nodeid+'")');
|
318 | _code = _code.split('\t').join(''); // eliminamos los tabs
|
319 | console.log('codigo a enviar a tishadow:',_code);
|
320 | _code = _code.split('"').join("'");
|
321 | _code = _code.split("'").join("\'");
|
322 | _code = _code.split("\n").join(" ");
|
323 | var _real = 'Alloy.Globals.live_apply("'+nodeid+'_'+changed+'","'+_code+'");';
|
324 | console.log('real enviando:',_real);
|
325 | if (_config.ti_connected==true) {
|
326 | tis.send(_real);
|
327 | }
|
328 | }
|
329 | } else if (nodeid in _live.properties) {
|
330 | console.log('nodo tiene propiedades, pero atributo no esta dentro de capacidades');
|
331 | } else {
|
332 | //console.log('live properties?',_live.properties);
|
333 | }
|
334 | }
|
335 |
|
336 | function loadJSON(directory) {
|
337 | // loads live_*.json into var '_live.*'
|
338 | console.log('loading json.');
|
339 | for (var el in _live) {
|
340 | var ltmp = path.join(directory, 'live_'+el+'.json');
|
341 | _live[el] = null; // clean the obj first to be overwritten :14/1/2016 PSB
|
342 | if (fileExists(ltmp)==true) {
|
343 | console.log('json file loaded');
|
344 | _live[el] = jsonfile.readFileSync(ltmp,'utf-8');
|
345 | }
|
346 | }
|
347 | }
|
348 |
|
349 | function loadLive(directory, onSuccess, onError) {
|
350 | var dsl_map, padre_id = '', tmp_color = '', tmp_bgcolor = '', tmp_link = '', e_ok = true, _app_name = '';
|
351 | // loads the dsl file, process it into 'nodes' struct var
|
352 | if (fileExists(path.join(directory,_config.dsl))) {
|
353 | _app_name = directory.split(path.sep)[directory.split(path.sep).length-1].toUpperCase();
|
354 | console.log('Processing file '.yellow+_config.dsl.toUpperCase().green + ' ('.white+_app_name.cyan+')'.white);
|
355 | dsl_map = fs.readFileSync(path.join(cwd,_config.dsl),{ encoding:'utf-8' });
|
356 | nodes = {}; // reset nodes file
|
357 | nodes_meta = {}; // reset nodes_meta file
|
358 | // process dsl file into 'nodes'
|
359 | $ = cheerio.load(dsl_map, { recognizeSelfClosing:true });
|
360 | $('node').each(function(index_a,element_a) {
|
361 | padre_id = $(this).attr('id');
|
362 | if (padre_id in nodes==false) nodes[padre_id] = [];
|
363 | if (padre_id in nodes_meta==false) nodes_meta[padre_id] = { _text:'', _color:'', _bgcolor:'', _icons:'' };
|
364 | // assign node metadata (_color, _bgcolor, _link, _text, _icons)
|
365 | if ($(this).attr('text')) nodes_meta[padre_id]._text = $(this).attr('text');
|
366 | if ($(this).attr('color')) nodes_meta[padre_id]._color = $(this).attr('color');
|
367 | if ($(this).attr('background_color')) nodes_meta[padre_id]._bgcolor = $(this).attr('background_color');
|
368 | var _icons = [];
|
369 | $(this).find('icon').each(function(i,e){
|
370 | if ($(e).parent('node').attr('id')==padre_id) {
|
371 | _icons.push($(e).attr('builtin'));
|
372 | }
|
373 | });
|
374 | _icons = _icons.join(',');
|
375 | nodes_meta[padre_id]._icons = _icons;
|
376 | // assign node attributes
|
377 | $(this).find('attribute').each(function(index,element) {
|
378 | if ($(element).parent('node').attr('id')==padre_id) {
|
379 | nodes[padre_id][nodes[padre_id].length] = { name: $(element).attr('name'), value: $(element).attr('value') };
|
380 | }
|
381 | });
|
382 | });
|
383 | if (onSuccess && typeof onSuccess === 'function') {
|
384 | onSuccess();
|
385 | }
|
386 | } else {
|
387 | console.log(_colors.red('FATAL ERROR:\n')+_colors.cyan('NO APP DSL found on ')+_colors.yellow(directory)+'\n\n');
|
388 | process.exit(2);
|
389 | e_ok = false;
|
390 | if (onError && typeof onError === 'function') {
|
391 | onError('file '+_config.dsl+' does not exist on directory '+directory);
|
392 | }
|
393 | }
|
394 | }
|
395 |
|
396 | var searchAttributesOf = function($, nodeid) {
|
397 | var attrs = [], count = 0, node = {};
|
398 | if ($) {
|
399 | $('node[ID='+nodeid+'] attribute').each(function(index,element) {
|
400 | node[index] = { name: $(this).attr('NAME'), value: $(this).attr('VALUE') };
|
401 | });
|
402 | }
|
403 | return node;
|
404 | };
|
405 |
|
406 | var logfull = function(msg, data) {
|
407 | console.log(msg, JSON.stringify(data));
|
408 | };
|
409 |
|
410 | function consoleTitle(message, color, bordercolor) {
|
411 | var max_len = 0, to_show = '', to_show_open = '', to_show_close = '',
|
412 | left_right_chars = '**',
|
413 | left_right_space = 2,
|
414 | left_right_margin = left_right_chars.length+left_right_space;
|
415 | if (message.indexOf('\n')!=-1) {
|
416 | // multiple lines message
|
417 | for (var lines in message.split('\n')) {
|
418 | if (message.split('\n')[lines].length > max_len) {
|
419 | max_len = message.split('\n')[lines].length;
|
420 | }
|
421 | }
|
422 | } else {
|
423 | // single line message
|
424 | max_len = message.length;
|
425 | to_show_open = Array(max_len+(left_right_margin*2)).join('*') + '\n';
|
426 | to_show_open += left_right_chars + Array(left_right_space).join(' ');
|
427 | if (bordercolor) to_show_open = _colors[bordercolor](to_show_open);
|
428 | if (color) {
|
429 | to_show = _colors[color](message);
|
430 | } else {
|
431 | to_show = message;
|
432 | }
|
433 | to_show_close += Array(left_right_space).join(' ') + left_right_chars + '\n';
|
434 | to_show_close += Array(max_len+(left_right_margin*2)).join('*') + '\n';
|
435 | if (bordercolor) to_show_close = _colors[bordercolor](to_show_close);
|
436 | // glue message
|
437 | to_show = to_show_open + to_show + to_show_close;
|
438 | }
|
439 | console.log(to_show);
|
440 | }
|
441 |
|
442 | function fileExists(filePath)
|
443 | {
|
444 | try
|
445 | {
|
446 | return fs.statSync(filePath).isFile();
|
447 | }
|
448 | catch (err)
|
449 | {
|
450 | return false;
|
451 | }
|
452 | }
|
453 |
|
454 | function getHash(str) {
|
455 | var hash = 5381,
|
456 | i = str.length
|
457 |
|
458 | while(i)
|
459 | hash = (hash * 33) ^ str.charCodeAt(--i)
|
460 |
|
461 | /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
|
462 | * integers. Since we want the results to be always positive, convert the
|
463 | * signed int to an unsigned by doing an unsigned bitshift. */
|
464 | return hash >>> 0;
|
465 | } |
\ | No newline at end of file |