2
0

electron.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. var host = host || {};
  2. const electron = require('electron');
  3. const fs = require('fs');
  4. const http = require('http');
  5. const https = require('https');
  6. const process = require('process');
  7. const path = require('path');
  8. const querystring = require('querystring');
  9. host.ElectronHost = class {
  10. constructor() {
  11. process.on('uncaughtException', (err) => {
  12. this.exception(err, true);
  13. });
  14. this._document = window.document;
  15. this._window = window;
  16. this._window.eval = global.eval = () => {
  17. throw new Error('window.eval() not supported.');
  18. };
  19. this._window.addEventListener('unload', () => {
  20. if (typeof __coverage__ !== 'undefined') {
  21. const file = path.join('.nyc_output', path.basename(window.location.pathname, '.html')) + '.json';
  22. /* eslint-disable no-undef */
  23. fs.writeFileSync(file, JSON.stringify(__coverage__));
  24. /* eslint-enable no-undef */
  25. }
  26. });
  27. this._environment = electron.ipcRenderer.sendSync('get-environment', {});
  28. this._queue = [];
  29. }
  30. get window() {
  31. return this._window;
  32. }
  33. get document() {
  34. return this._document;
  35. }
  36. get version() {
  37. return this._environment.version;
  38. }
  39. get type() {
  40. return 'Electron';
  41. }
  42. get agent() {
  43. return 'any';
  44. }
  45. initialize(view) {
  46. this._view = view;
  47. electron.ipcRenderer.on('open', (_, data) => {
  48. this._openPath(data.path);
  49. });
  50. return new Promise((resolve /*, reject */) => {
  51. const telemetry = () => {
  52. if (this._environment.package) {
  53. this._telemetry = new host.Telemetry('UA-54146-13', this._getConfiguration('userId'), navigator.userAgent, this.type, this.version);
  54. }
  55. resolve();
  56. };
  57. const consent = () => {
  58. this._message('This app uses cookies to report errors and anonymous usage information.', 'Accept', () => {
  59. this._setConfiguration('consent', Date.now());
  60. telemetry();
  61. });
  62. };
  63. const time = this._getConfiguration('consent');
  64. if (time && (Date.now() - time) < 30 * 24 * 60 * 60 * 1000) {
  65. telemetry();
  66. }
  67. else {
  68. this._request('https://ipinfo.io/json', { 'Content-Type': 'application/json' }, 2000).then((text) => {
  69. try {
  70. const json = JSON.parse(text);
  71. const countries = ['AT', 'BE', 'BG', 'HR', 'CZ', 'CY', 'DK', 'EE', 'FI', 'FR', 'DE', 'EL', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'NO', 'PL', 'PT', 'SK', 'ES', 'SE', 'GB', 'UK', 'GR', 'EU', 'RO'];
  72. if (json && json.country && !countries.indexOf(json.country) !== -1) {
  73. this._setConfiguration('consent', Date.now());
  74. telemetry();
  75. }
  76. else {
  77. consent();
  78. }
  79. }
  80. catch (err) {
  81. consent();
  82. }
  83. }).catch(() => {
  84. consent();
  85. });
  86. }
  87. });
  88. }
  89. start() {
  90. if (this._queue) {
  91. const queue = this._queue;
  92. delete this._queue;
  93. if (queue.length > 0) {
  94. const path = queue.pop();
  95. this._openPath(path);
  96. }
  97. }
  98. electron.ipcRenderer.on('export', (_, data) => {
  99. this._view.export(data.file);
  100. });
  101. electron.ipcRenderer.on('cut', () => {
  102. this._view.cut();
  103. });
  104. electron.ipcRenderer.on('copy', () => {
  105. this._view.copy();
  106. });
  107. electron.ipcRenderer.on('paste', () => {
  108. this._view.paste();
  109. });
  110. electron.ipcRenderer.on('selectall', () => {
  111. this._view.selectAll();
  112. });
  113. electron.ipcRenderer.on('toggle', (sender, name) => {
  114. this._view.toggle(name);
  115. this._update(Object.assign({}, this._view.options));
  116. });
  117. electron.ipcRenderer.on('zoom-in', () => {
  118. this.document.getElementById('zoom-in-button').click();
  119. });
  120. electron.ipcRenderer.on('zoom-out', () => {
  121. this.document.getElementById('zoom-out-button').click();
  122. });
  123. electron.ipcRenderer.on('reset-zoom', () => {
  124. this._view.resetZoom();
  125. });
  126. electron.ipcRenderer.on('show-properties', () => {
  127. this.document.getElementById('menu-button').click();
  128. });
  129. electron.ipcRenderer.on('find', () => {
  130. this._view.find();
  131. });
  132. this.document.getElementById('menu-button').addEventListener('click', () => {
  133. this._view.showModelProperties();
  134. });
  135. const openFileButton = this.document.getElementById('open-file-button');
  136. if (openFileButton) {
  137. openFileButton.style.opacity = 1;
  138. openFileButton.addEventListener('click', () => {
  139. electron.ipcRenderer.send('open-file-dialog', {});
  140. });
  141. }
  142. const githubButton = this.document.getElementById('github-button');
  143. const githubLink = this.document.getElementById('logo-github');
  144. if (githubButton && githubLink) {
  145. githubButton.style.opacity = 1;
  146. githubButton.addEventListener('click', () => {
  147. this.openURL(githubLink.href);
  148. });
  149. }
  150. this.document.addEventListener('dragover', (e) => {
  151. e.preventDefault();
  152. });
  153. this.document.addEventListener('drop', (e) => {
  154. e.preventDefault();
  155. });
  156. this.document.body.addEventListener('drop', (e) => {
  157. e.preventDefault();
  158. const paths = Array.from(e.dataTransfer.files).map(((file) => file.path));
  159. if (paths.length > 0) {
  160. electron.ipcRenderer.send('drop-paths', { paths: paths });
  161. }
  162. return false;
  163. });
  164. this._view.show('welcome');
  165. }
  166. environment(name) {
  167. return this._environment[name];
  168. }
  169. error(message, detail) {
  170. electron.ipcRenderer.sendSync('show-message-box', {
  171. type: 'error',
  172. message: message,
  173. detail: detail,
  174. });
  175. }
  176. confirm(message, detail) {
  177. const result = electron.ipcRenderer.sendSync('show-message-box', {
  178. type: 'question',
  179. message: message,
  180. detail: detail,
  181. buttons: ['Yes', 'No'],
  182. defaultId: 0,
  183. cancelId: 1
  184. });
  185. return result === 0;
  186. }
  187. require(id) {
  188. try {
  189. return Promise.resolve(require(id));
  190. }
  191. catch (error) {
  192. return Promise.reject(error);
  193. }
  194. }
  195. save(name, extension, defaultPath, callback) {
  196. const selectedFile = electron.ipcRenderer.sendSync('show-save-dialog', {
  197. title: 'Export Tensor',
  198. defaultPath: defaultPath,
  199. buttonLabel: 'Export',
  200. filters: [ { name: name, extensions: [ extension ] } ]
  201. });
  202. if (selectedFile) {
  203. callback(selectedFile);
  204. }
  205. }
  206. export(file, blob) {
  207. const reader = new FileReader();
  208. reader.onload = (e) => {
  209. const data = new Uint8Array(e.target.result);
  210. fs.writeFile(file, data, null, (err) => {
  211. if (err) {
  212. this.exception(err, false);
  213. this.error('Error writing file.', err.message);
  214. }
  215. });
  216. };
  217. let err = null;
  218. if (!blob) {
  219. err = new Error("Export blob is '" + JSON.stringify(blob) + "'.");
  220. }
  221. else if (!(blob instanceof Blob)) {
  222. err = new Error("Export blob type is '" + (typeof blob) + "'.");
  223. }
  224. if (err) {
  225. this.exception(err, false);
  226. this.error('Error exporting image.', err.message);
  227. }
  228. else {
  229. reader.readAsArrayBuffer(blob);
  230. }
  231. }
  232. request(file, encoding, base) {
  233. return new Promise((resolve, reject) => {
  234. const pathname = path.join(base || __dirname, file);
  235. fs.stat(pathname, (err, stat) => {
  236. if (err && err.code === 'ENOENT') {
  237. reject(new Error("The file '" + file + "' does not exist."));
  238. }
  239. else if (err) {
  240. reject(err);
  241. }
  242. else if (!stat.isFile()) {
  243. reject(new Error("The path '" + file + "' is not a file."));
  244. }
  245. else if (stat && stat.size < 0x7ffff000) {
  246. fs.readFile(pathname, encoding, (err, data) => {
  247. if (err) {
  248. reject(err);
  249. }
  250. else {
  251. resolve(encoding ? data : new host.ElectronHost.BinaryStream(data));
  252. }
  253. });
  254. }
  255. else if (encoding) {
  256. reject(new Error("The file '" + file + "' size (" + stat.size.toString() + ") for encoding '" + encoding + "' is greater than 2 GB."));
  257. }
  258. else {
  259. resolve(new host.ElectronHost.FileStream(pathname, 0, stat.size, stat.mtimeMs));
  260. }
  261. });
  262. });
  263. }
  264. openURL(url) {
  265. electron.shell.openExternal(url);
  266. }
  267. exception(error, fatal) {
  268. if (this._telemetry && error && error.telemetry !== false) {
  269. try {
  270. const name = error && error.name ? error.name + ': ' : '';
  271. const message = error && error.message ? error.message : JSON.stringify(error);
  272. const description = [ name + message ];
  273. if (error.stack) {
  274. const format = (file, line, column) => {
  275. return file.split('\\').join('/').split('/').pop() + ':' + line + ':' + column;
  276. };
  277. const match = error.stack.match(/\n {4}at (.*) \((.*):(\d*):(\d*)\)/);
  278. if (match) {
  279. description.push(match[1] + ' (' + format(match[2], match[3], match[4]) + ')');
  280. }
  281. else {
  282. const match = error.stack.match(/\n {4}at (.*):(\d*):(\d*)/);
  283. if (match) {
  284. description.push('(' + format(match[1], match[2], match[3]) + ')');
  285. }
  286. }
  287. }
  288. this._telemetry.exception(description.join(' @ '), fatal);
  289. }
  290. catch (e) {
  291. // continue regardless of error
  292. }
  293. }
  294. }
  295. screen(name) {
  296. if (this._telemetry) {
  297. try {
  298. this._telemetry.screenview(name);
  299. }
  300. catch (e) {
  301. // continue regardless of error
  302. }
  303. }
  304. }
  305. event(category, action, label, value) {
  306. if (this._telemetry) {
  307. try {
  308. this._telemetry.event(category, action, label, value);
  309. }
  310. catch (e) {
  311. // continue regardless of error
  312. }
  313. }
  314. }
  315. _context(location) {
  316. const basename = path.basename(location);
  317. const stat = fs.statSync(location);
  318. if (stat.isFile()) {
  319. const dirname = path.dirname(location);
  320. return this.request(basename, null, dirname).then((stream) => {
  321. return new host.ElectronHost.ElectronContext(this, dirname, basename, stream);
  322. });
  323. }
  324. else if (stat.isDirectory()) {
  325. const entries = new Map();
  326. const walk = (dir) => {
  327. for (const item of fs.readdirSync(dir)) {
  328. const pathname = path.join(dir, item);
  329. const stat = fs.statSync(pathname);
  330. if (stat.isDirectory()) {
  331. walk(pathname);
  332. }
  333. else if (stat.isFile()) {
  334. const stream = new host.ElectronHost.FileStream(pathname, 0, stat.size, stat.mtimeMs);
  335. const name = pathname.split(path.sep).join(path.posix.sep);
  336. entries.set(name, stream);
  337. }
  338. }
  339. };
  340. walk(location);
  341. return Promise.resolve(new host.ElectronHost.ElectronContext(this, location, basename, null, entries));
  342. }
  343. throw new Error("Unsupported path stat '" + JSON.stringify(stat) + "'.");
  344. }
  345. _openPath(path) {
  346. if (this._queue) {
  347. this._queue.push(path);
  348. return;
  349. }
  350. if (path && this._view.accept(path)) {
  351. this._view.show('welcome spinner');
  352. this._context(path).then((context) => {
  353. this._view.open(context).then((model) => {
  354. this._view.show(null);
  355. const options = Object.assign({}, this._view.options);
  356. if (model) {
  357. options.path = path;
  358. }
  359. this._update(options);
  360. }).catch((error) => {
  361. const options = Object.assign({}, this._view.options);
  362. if (error) {
  363. this._view.error(error, null, null);
  364. options.path = null;
  365. }
  366. this._update(options);
  367. });
  368. }).catch((error) => {
  369. this._view.error(error, 'Error while reading file.', null);
  370. this._update({ path: null });
  371. });
  372. }
  373. }
  374. _request(location, headers, timeout) {
  375. return new Promise((resolve, reject) => {
  376. const url = new URL(location);
  377. const protocol = url.protocol === 'https:' ? https : http;
  378. const options = {};
  379. options.headers = headers;
  380. if (timeout) {
  381. options.timeout = timeout;
  382. }
  383. const request = protocol.request(location, options, (response) => {
  384. if (response.statusCode !== 200) {
  385. const err = new Error("The web request failed with status code " + response.statusCode + " at '" + location + "'.");
  386. err.type = 'error';
  387. err.url = location;
  388. err.status = response.statusCode;
  389. reject(err);
  390. }
  391. else {
  392. let data = '';
  393. response.on('data', (chunk) => {
  394. data += chunk;
  395. });
  396. response.on('err', (err) => {
  397. reject(err);
  398. });
  399. response.on('end', () => {
  400. resolve(data);
  401. });
  402. }
  403. });
  404. request.on("error", (err) => {
  405. reject(err);
  406. });
  407. request.on("timeout", () => {
  408. request.destroy();
  409. const error = new Error("The web request timed out at '" + location + "'.");
  410. error.type = 'timeout';
  411. error.url = url;
  412. reject(error);
  413. });
  414. request.end();
  415. });
  416. }
  417. _getConfiguration(name) {
  418. return electron.ipcRenderer.sendSync('get-configuration', { name: name });
  419. }
  420. _setConfiguration(name, value) {
  421. electron.ipcRenderer.sendSync('set-configuration', { name: name, value: value });
  422. }
  423. _update(data) {
  424. electron.ipcRenderer.send('update', data);
  425. }
  426. _message(message, button, callback) {
  427. const messageText = this.document.getElementById('message');
  428. if (messageText) {
  429. messageText.innerText = message;
  430. }
  431. const messageButton = this.document.getElementById('message-button');
  432. if (messageButton) {
  433. if (button && callback) {
  434. messageButton.style.removeProperty('display');
  435. messageButton.innerText = button;
  436. messageButton.onclick = () => {
  437. messageButton.onclick = null;
  438. callback();
  439. };
  440. }
  441. else {
  442. messageButton.style.display = 'none';
  443. messageButton.onclick = null;
  444. }
  445. }
  446. this._view.show('welcome message');
  447. }
  448. };
  449. host.Telemetry = class {
  450. constructor(trackingId, clientId, userAgent, applicationName, applicationVersion) {
  451. this._params = {
  452. aip: '1', // anonymizeIp
  453. tid: trackingId,
  454. cid: clientId,
  455. ua: userAgent,
  456. an: applicationName,
  457. av: applicationVersion
  458. };
  459. }
  460. screenview(screenName) {
  461. const params = Object.assign({}, this._params);
  462. params.cd = screenName;
  463. this._send('screenview', params);
  464. }
  465. event(category, action, label, value) {
  466. const params = Object.assign({}, this._params);
  467. params.ec = category;
  468. params.ea = action;
  469. params.el = label;
  470. params.ev = value;
  471. this._send('event', params);
  472. }
  473. exception(description, fatal) {
  474. const params = Object.assign({}, this._params);
  475. params.exd = description;
  476. if (fatal) {
  477. params.exf = '1';
  478. }
  479. this._send('exception', params);
  480. }
  481. _send(type, params) {
  482. params.t = type;
  483. params.v = '1';
  484. for (const param in params) {
  485. if (params[param] === null || params[param] === undefined) {
  486. delete params[param];
  487. }
  488. }
  489. const body = querystring.stringify(params);
  490. const options = {
  491. method: 'POST',
  492. host: 'www.google-analytics.com',
  493. path: '/collect',
  494. headers: { 'Content-Length': Buffer.byteLength(body) }
  495. };
  496. const request = https.request(options, (response) => {
  497. response.on('error', (/* error */) => {});
  498. });
  499. request.setTimeout(5000, () => {
  500. request.destroy();
  501. });
  502. request.on('error', (/* error */) => {});
  503. request.write(body);
  504. request.end();
  505. }
  506. };
  507. host.ElectronHost.BinaryStream = class {
  508. constructor(buffer) {
  509. this._buffer = buffer;
  510. this._length = buffer.length;
  511. this._position = 0;
  512. }
  513. get position() {
  514. return this._position;
  515. }
  516. get length() {
  517. return this._length;
  518. }
  519. stream(length) {
  520. const buffer = this.read(length);
  521. return new host.ElectronHost.BinaryStream(buffer.slice(0));
  522. }
  523. seek(position) {
  524. this._position = position >= 0 ? position : this._length + position;
  525. if (this._position > this._buffer.length) {
  526. throw new Error('Expected ' + (this._position - this._buffer.length) + ' more bytes. The file might be corrupted. Unexpected end of file.');
  527. }
  528. }
  529. skip(offset) {
  530. this._position += offset;
  531. if (this._position > this._buffer.length) {
  532. throw new Error('Expected ' + (this._position - this._buffer.length) + ' more bytes. The file might be corrupted. Unexpected end of file.');
  533. }
  534. }
  535. peek(length) {
  536. if (this._position === 0 && length === undefined) {
  537. return this._buffer;
  538. }
  539. const position = this._position;
  540. this.skip(length !== undefined ? length : this._length - this._position);
  541. const end = this._position;
  542. this.seek(position);
  543. return this._buffer.subarray(position, end);
  544. }
  545. read(length) {
  546. if (this._position === 0 && length === undefined) {
  547. this._position = this._length;
  548. return this._buffer;
  549. }
  550. const position = this._position;
  551. this.skip(length !== undefined ? length : this._length - this._position);
  552. return this._buffer.subarray(position, this._position);
  553. }
  554. byte() {
  555. const position = this._position;
  556. this.skip(1);
  557. return this._buffer[position];
  558. }
  559. };
  560. host.ElectronHost.FileStream = class {
  561. constructor(file, start, length, mtime) {
  562. this._file = file;
  563. this._start = start;
  564. this._length = length;
  565. this._position = 0;
  566. this._mtime = mtime;
  567. }
  568. get position() {
  569. return this._position;
  570. }
  571. get length() {
  572. return this._length;
  573. }
  574. stream(length) {
  575. const file = new host.ElectronHost.FileStream(this._file, this._position, length, this._mtime);
  576. this.skip(length);
  577. return file;
  578. }
  579. seek(position) {
  580. this._position = position >= 0 ? position : this._length + position;
  581. }
  582. skip(offset) {
  583. this._position += offset;
  584. if (this._position > this._length) {
  585. throw new Error('Expected ' + (this._position - this._length) + ' more bytes. The file might be corrupted. Unexpected end of file.');
  586. }
  587. }
  588. peek(length) {
  589. length = length !== undefined ? length : this._length - this._position;
  590. if (length < 0x10000000) {
  591. const position = this._fill(length);
  592. this._position -= length;
  593. return this._buffer.subarray(position, position + length);
  594. }
  595. const position = this._position;
  596. this.skip(length);
  597. this.seek(position);
  598. const buffer = new Uint8Array(length);
  599. this._read(buffer, position);
  600. return buffer;
  601. }
  602. read(length) {
  603. length = length !== undefined ? length : this._length - this._position;
  604. if (length < 0x10000000) {
  605. const position = this._fill(length);
  606. return this._buffer.subarray(position, position + length);
  607. }
  608. const position = this._position;
  609. this.skip(length);
  610. const buffer = new Uint8Array(length);
  611. this._read(buffer, position);
  612. return buffer;
  613. }
  614. byte() {
  615. const position = this._fill(1);
  616. return this._buffer[position];
  617. }
  618. _fill(length) {
  619. if (this._position + length > this._length) {
  620. throw new Error('Expected ' + (this._position + length - this._length) + ' more bytes. The file might be corrupted. Unexpected end of file.');
  621. }
  622. if (!this._buffer || this._position < this._offset || this._position + length > this._offset + this._buffer.length) {
  623. this._offset = this._position;
  624. this._buffer = new Uint8Array(Math.min(0x10000000, this._length - this._offset));
  625. this._read(this._buffer, this._offset);
  626. }
  627. const position = this._position;
  628. this._position += length;
  629. return position - this._offset;
  630. }
  631. _read(buffer, offset) {
  632. const descriptor = fs.openSync(this._file, 'r');
  633. const stat = fs.statSync(this._file);
  634. if (stat.mtimeMs != this._mtime) {
  635. throw new Error("File '" + this._file + "' last modified time changed.");
  636. }
  637. try {
  638. fs.readSync(descriptor, buffer, 0, buffer.length, offset + this._start);
  639. }
  640. finally {
  641. fs.closeSync(descriptor);
  642. }
  643. }
  644. };
  645. host.ElectronHost.ElectronContext = class {
  646. constructor(host, folder, identifier, stream, entries) {
  647. this._host = host;
  648. this._folder = folder;
  649. this._identifier = identifier;
  650. this._stream = stream;
  651. this._entries = entries || new Map();
  652. }
  653. get identifier() {
  654. return this._identifier;
  655. }
  656. get stream() {
  657. return this._stream;
  658. }
  659. get entries() {
  660. return this._entries;
  661. }
  662. request(file, encoding, base) {
  663. return this._host.request(file, encoding, base === undefined ? this._folder : base);
  664. }
  665. require(id) {
  666. return this._host.require(id);
  667. }
  668. exception(error, fatal) {
  669. this._host.exception(error, fatal);
  670. }
  671. };
  672. window.addEventListener('load', () => {
  673. global.protobuf = require('./protobuf');
  674. global.flatbuffers = require('./flatbuffers');
  675. const view = require('./view');
  676. window.__host__ = new host.ElectronHost();
  677. window.__view__ = new view.View(window.__host__);
  678. });