main.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. // Modules to control application life and create native browser window
  2. const {app, BrowserWindow, dialog, ipcMain, screen, session} = require('electron');
  3. app.commandLine.appendSwitch("--disable-http-cache");
  4. const {Builder, By, Key, until} = require("selenium-webdriver");
  5. const chrome = require('selenium-webdriver/chrome');
  6. const {ServiceBuilder} = require('selenium-webdriver/chrome');
  7. const {rootCertificates} = require('tls');
  8. const {exit} = require('process');
  9. const path = require('path');
  10. const fs = require('fs');
  11. const {exec, spawn} = require('child_process');
  12. const iconPath = path.join(__dirname, 'favicon.ico');
  13. const task_server = require(path.join(__dirname, 'server.js'));
  14. const util = require('util');
  15. let config = fs.readFileSync(path.join(task_server.getDir(), `config.json`), 'utf8');
  16. config = JSON.parse(config);
  17. if(config.debug){
  18. let logPath = 'info.log'
  19. let logFile = fs.createWriteStream(logPath, { flags: 'a' })
  20. console.log = function() {
  21. logFile.write(util.format.apply(null, arguments) + '\n')
  22. process.stdout.write(util.format.apply(null, arguments) + '\n')
  23. }
  24. console.error = function() {
  25. logFile.write(util.format.apply(null, arguments) + '\n')
  26. process.stderr.write(util.format.apply(null, arguments) + '\n')
  27. }
  28. }
  29. let allWindowSockets = [];
  30. let allWindowScoketNames = [];
  31. task_server.start(config.webserver_port); //start local server
  32. let server_address = `${config.webserver_address}:${config.webserver_port}`;
  33. const websocket_port = 8084; //目前只支持8084端口,写死,因为扩展里面写死了
  34. console.log("server_address: " + server_address);
  35. let driverPath = "";
  36. let chromeBinaryPath = "";
  37. let execute_path = "";
  38. console.log(process.arch);
  39. if (process.platform === 'win32' && process.arch === 'ia32') {
  40. driverPath = path.join(__dirname, "chrome_win32/chromedriver_win32.exe");
  41. chromeBinaryPath = path.join(__dirname, "chrome_win32/chrome.exe");
  42. execute_path = path.join(__dirname, "chrome_win32/execute.bat");
  43. } else if (process.platform === 'win32' && process.arch === 'x64') {
  44. driverPath = path.join(__dirname, "chrome_win64/chromedriver_win64.exe");
  45. chromeBinaryPath = path.join(__dirname, "chrome_win64/chrome.exe");
  46. execute_path = path.join(__dirname, "chrome_win64/execute.bat");
  47. } else if (process.platform === 'darwin') {
  48. driverPath = path.join(__dirname, "chromedriver_mac64");
  49. chromeBinaryPath = path.join(__dirname, "chrome_mac64.app/Contents/MacOS/Google Chrome");
  50. execute_path = path.join(__dirname, "");
  51. } else if (process.platform === 'linux') {
  52. driverPath = path.join(__dirname, "chrome_linux64/chromedriver_linux64");
  53. chromeBinaryPath = path.join(__dirname, "chrome_linux64/chrome");
  54. execute_path = path.join(__dirname, "chrome_linux64/execute.sh");
  55. }
  56. console.log(driverPath, chromeBinaryPath, execute_path);
  57. let language = "en";
  58. let driver = null;
  59. let mainWindow = null;
  60. let flowchart_window = null;
  61. let current_handle = null;
  62. let old_handles = [];
  63. let handle_pairs = {};
  64. // var ffi = require('ffi-napi');
  65. // var libm = ffi.Library('libm', {
  66. // 'ceil': [ 'double', [ 'double' ] ]
  67. // });
  68. // libm.ceil(1.5); // 2
  69. // const {user32FindWindowEx,
  70. // winspoolGetDefaultPrinter,} = require('win32-api/fun');
  71. // async function testt(){
  72. // // 获取当前电脑当前用户默认打印机名
  73. // const printerName = await winspoolGetDefaultPrinter()
  74. // console.log(printerName);
  75. // }
  76. // testt();
  77. function createWindow() {
  78. // Create the browser window.
  79. mainWindow = new BrowserWindow({
  80. width: 520,
  81. height: 750,
  82. webPreferences: {
  83. preload: path.join(__dirname, 'src/js/preload.js')
  84. },
  85. icon: iconPath,
  86. // frame: false, //取消window自带的关闭最小化等
  87. resizable: false //禁止改变主窗口尺寸
  88. })
  89. // and load the index.html of the app.
  90. // mainWindow.loadFile('src/index.html');
  91. mainWindow.loadURL(server_address + '/index.html?user_data_folder=' + config.user_data_folder, { extraHeaders: 'pragma: no-cache\n' });
  92. // 隐藏菜单栏
  93. const {Menu} = require('electron');
  94. Menu.setApplicationMenu(null);
  95. mainWindow.on('close', function (e) {
  96. if (process.platform !== 'darwin') {
  97. app.quit();
  98. }
  99. });
  100. // mainWindow.webContents.openDevTools();
  101. // Open the DevTools.
  102. // mainWindow.webContents.openDevTools()
  103. }
  104. async function beginInvoke(msg, ws) {
  105. if (msg.type == 1) {
  106. if (msg.message.id != -1) {
  107. let url = "";
  108. if (language == "zh") {
  109. url = server_address + `/taskGrid/FlowChart_CN.html?id=${msg.message.id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address;
  110. } else if (language == "en") {
  111. url = server_address + `/taskGrid/FlowChart.html?id=${msg.message.id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address;
  112. }
  113. console.log(url);
  114. flowchart_window.loadURL(url, { extraHeaders: 'pragma: no-cache\n' });
  115. }
  116. mainWindow.hide();
  117. // Prints the currently focused window bounds.
  118. // This method has to be called on macOS before changing the window's bounds, otherwise it will throw an error.
  119. // It will prompt an accessibility permission request dialog, if needed.
  120. if(process.platform != "linux" && process.platform != "darwin"){
  121. const {windowManager} = require("node-window-manager");
  122. const window = windowManager.getActiveWindow();
  123. console.log(window);
  124. windowManager.requestAccessibility();
  125. // Sets the active window's bounds.
  126. let size = screen.getPrimaryDisplay().workAreaSize
  127. let width = parseInt(size.width)
  128. let height = parseInt(size.height * 0.6)
  129. window.setBounds({x: 0, y: size.height * 0.4, height: height, width: width});
  130. }
  131. flowchart_window.show();
  132. // flowchart_window.openDevTools();
  133. } else if (msg.type == 2) {
  134. // 键盘输入事件
  135. // const robot = require("@jitsi/robotjs");
  136. let keyInfo = msg.message.keyboardStr;
  137. let handles = await driver.getAllWindowHandles();
  138. console.log("handles", handles);
  139. let exit = false;
  140. let content_handle = handle_pairs[msg.message.id];
  141. console.log(msg.message.id, content_handle);
  142. let order = [...handles.filter(handle => handle != current_handle && handle != content_handle), current_handle, content_handle]; //搜索顺序
  143. let len = order.length;
  144. while (true) {
  145. // console.log("handles");
  146. try{
  147. let iframe = msg.message.iframe;
  148. let enter = false;
  149. if (/<enter>/i.test(keyInfo)) {
  150. keyInfo = keyInfo.replace(/<enter>/gi, '');
  151. enter = true;
  152. }
  153. let h = order[len - 1];
  154. console.log("current_handle", current_handle);
  155. if(h != null && handles.includes(h)){
  156. await driver.switchTo().window(h);
  157. current_handle = h;
  158. console.log("switch to handle: ", h);
  159. }
  160. // await driver.executeScript("window.stop();");
  161. // console.log("executeScript");
  162. if(!iframe){
  163. let element = await driver.findElement(By.xpath(msg.message.xpath));
  164. console.log("Find Element at handle: ", current_handle);
  165. // 使用正则表达式匹配 '<enter>',不论大小写
  166. await element.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), keyInfo);
  167. if(enter){
  168. await element.sendKeys(Key.ENTER);
  169. }
  170. console.log("send key");
  171. break;
  172. } else {
  173. let iframes = await driver.findElements(By.tagName('iframe'));
  174. // 遍历所有的 iframe 并点击里面的元素
  175. for(let i = 0; i < iframes.length; i++) {
  176. let iframe = iframes[i];
  177. // 切换到 iframe
  178. await driver.switchTo().frame(iframe);
  179. // 在 iframe 中查找并点击元素
  180. let element;
  181. try {
  182. element = await driver.findElement(By.xpath(msg.message.xpath));
  183. } catch (error) {
  184. console.log('No such element found in the iframe');
  185. }
  186. if (element) {
  187. await element.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), keyInfo);
  188. if(enter){
  189. await element.sendKeys(Key.ENTER);
  190. }
  191. }
  192. // 完成操作后切回主文档
  193. await driver.switchTo().defaultContent();
  194. }
  195. break;
  196. }
  197. } catch (error) {
  198. console.log("len", len);
  199. len = len - 1;
  200. if (len == 0) {
  201. break;
  202. }
  203. }
  204. // .then(function (element) {
  205. // console.log("element", element, handles);
  206. // element.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), keyInfo);
  207. // exit = true;
  208. // }, function (error) {
  209. // console.log("error", error);
  210. // len = len - 1;
  211. // if (len == 0) {
  212. // exit = true;
  213. // }
  214. // }
  215. // );
  216. }
  217. // let handles = driver.getAllWindowHandles();
  218. // driver.switchTo().window(handles[handles.length - 1]);
  219. // driver.findElement(By.xpath(msg.message.xpath)).sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), keyInfo);
  220. // robot.keyTap("a", "control");
  221. // robot.keyTap("backspace");
  222. // robot.typeString(keyInfo);
  223. // robot.keyTap("shift");
  224. // robot.keyTap("shift");
  225. } else if (msg.type == 3) {
  226. try {
  227. if (msg.from == 0) {
  228. socket_flowchart.send(msg.message.pipe); //直接把消息转接
  229. let message = JSON.parse(msg.message.pipe);
  230. let type = message.type;
  231. console.log("FROM Browser: ", message);
  232. console.log("Iframe:", message.iframe);
  233. if(type.indexOf("Click")>=0){
  234. // 鼠标点击事件
  235. let iframe = message.iframe;
  236. let handles = await driver.getAllWindowHandles();
  237. console.log("handles", handles);
  238. let exit = false;
  239. let content_handle = handle_pairs[message.id];
  240. console.log(message.id, content_handle);
  241. let order = [...handles.filter(handle => handle != current_handle && handle != content_handle), current_handle, content_handle]; //搜索顺序
  242. let len = order.length;
  243. while(true) {
  244. try{
  245. let h = order[len - 1];
  246. console.log("current_handle", current_handle);
  247. if(h != null && handles.includes(h)){
  248. await driver.switchTo().window(h); //执行失败会抛出异常
  249. current_handle = h;
  250. console.log("switch to handle: ", h);
  251. }
  252. //下面是找到窗口的情况下
  253. if(!iframe){
  254. let element = await driver.findElement(By.xpath(message.xpath));
  255. await element.click();
  256. break;
  257. } else {
  258. let iframes = await driver.findElements(By.tagName('iframe'));
  259. // 遍历所有的 iframe 并点击里面的元素
  260. for(let i = 0; i < iframes.length; i++) {
  261. let iframe = iframes[i];
  262. // 切换到 iframe
  263. await driver.switchTo().frame(iframe);
  264. // 在 iframe 中查找并点击元素
  265. let element;
  266. try {
  267. element = await driver.findElement(By.xpath(message.xpath));
  268. } catch (error) {
  269. console.log('No such element found in the iframe');
  270. }
  271. if (element) {
  272. await element.click();
  273. }
  274. // 完成操作后切回主文档
  275. await driver.switchTo().defaultContent();
  276. }
  277. break;
  278. }
  279. } catch (error) {
  280. console.log("len", len); //如果没有找到元素,就切换到下一个窗口
  281. len = len - 1;
  282. if (len == 0) {
  283. break;
  284. }
  285. }
  286. }
  287. }
  288. } else {
  289. socket_window.send(msg.message.pipe);
  290. for(let i in allWindowSockets){
  291. try{
  292. allWindowSockets[i].send(msg.message.pipe);
  293. } catch {
  294. console.log("Cannot send to socket with id: ", allWindowScoketNames[i]);
  295. }
  296. }
  297. console.log("FROM Flowchart: ", JSON.parse(msg.message.pipe));
  298. }
  299. } catch (e) {
  300. console.log(e);
  301. }
  302. } else if (msg.type == 5) {
  303. let child = require('child_process').execFile;
  304. // 参数顺序: 1. task id 2. server address 3. saved_file_name 4. "remote" or "local" 5. user_data_folder
  305. // var parameters = [msg.message.id, server_address];
  306. let parameters = [];
  307. console.log(msg.message)
  308. if (msg.message.user_data_folder == null || msg.message.user_data_folder == undefined || msg.message.user_data_folder == "") {
  309. parameters = ["--id", "[" + msg.message.id + "]", "--server_address", server_address, "--user_data", 0];
  310. } else {
  311. let user_data_folder_path = path.join(task_server.getDir(), msg.message.user_data_folder);
  312. parameters = ["--id", "[" + msg.message.id + "]", "--server_address", server_address, "--user_data", 1];
  313. config.user_data_folder = msg.message.user_data_folder;
  314. config.absolute_user_data_folder = user_data_folder_path;
  315. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  316. }
  317. if(msg.message.mysql_config_path != "-1"){
  318. config.mysql_config_path = msg.message.mysql_config_path;
  319. }
  320. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  321. // child('Chrome/easyspider_executestage.exe', parameters, function(err,stdout, stderr) {
  322. // console.log(stdout);
  323. // });
  324. let spawn = require("child_process").spawn;
  325. if (process.platform != "darwin" && msg.message.execute_type == 1 && msg.message.id != -1) {
  326. let child_process = spawn(execute_path, parameters);
  327. child_process.stdout.on('data', function (data) {
  328. console.log(data.toString());
  329. });
  330. }
  331. ws.send(JSON.stringify({"config_folder": task_server.getDir() + "/", "easyspider_location": task_server.getEasySpiderLocation()}));
  332. } else if (msg.type == 6) {
  333. try{
  334. flowchart_window.openDevTools();
  335. } catch {
  336. console.log("open devtools error");
  337. }
  338. } else if (msg.type == 7) {
  339. // 获得当前页面Cookies
  340. try{
  341. let cookies = await driver.manage().getCookies();
  342. console.log("Cookies: ", cookies);
  343. let cookiesText = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('\n');
  344. socket_flowchart.send(JSON.stringify({"type": "GetCookies", "message": cookiesText}));
  345. } catch {
  346. console.log("Cannot get Cookies");
  347. }
  348. }
  349. }
  350. const WebSocket = require('ws');
  351. const {all} = require("express/lib/application");
  352. let socket_window = null;
  353. let socket_start = null;
  354. let socket_flowchart = null;
  355. let wss = new WebSocket.Server({port: websocket_port});
  356. wss.on('connection', function (ws) {
  357. ws.on('message', async function (message, isBinary) {
  358. let msg = JSON.parse(message.toString());
  359. console.log("\n\nGET A MESSAGE: ", msg);
  360. // console.log(msg, msg.type, msg.message);
  361. if (msg.type == 0) {
  362. if (msg.message.id == 0) {
  363. socket_window = ws;
  364. console.log("set socket_window")
  365. } else if (msg.message.id == 1) {
  366. socket_start = ws;
  367. console.log("set socket_start")
  368. } else if (msg.message.id == 2) {
  369. socket_flowchart = ws;
  370. console.log("set socket_flowchart");
  371. } else { //其他的ID是用来标识不同的浏览器标签页的
  372. await new Promise(resolve => setTimeout(resolve, 2300));
  373. let handles = await driver.getAllWindowHandles();
  374. if(arrayDifference(handles, old_handles).length > 0){
  375. old_handles = handles;
  376. current_handle = handles[handles.length - 1];
  377. console.log("New tab opened, change current_handle to: ", current_handle);
  378. }
  379. handle_pairs[msg.message.id] = current_handle;
  380. console.log("Set handle_pair for id: ", msg.message.id, " to ", current_handle, ", title is: ", msg.message.title);
  381. socket_flowchart.send(JSON.stringify({"type": "title", "data": {"title":msg.message.title}}));
  382. allWindowSockets.push(ws);
  383. allWindowScoketNames.push(msg.message.id);
  384. // console.log("handle_pairs: ", handle_pairs);
  385. }
  386. } else if (msg.type == 10) {
  387. let leave_handle = handle_pairs[msg.message.id];
  388. if (leave_handle!=null && leave_handle!=undefined && leave_handle!="")
  389. {
  390. await driver.switchTo().window(leave_handle);
  391. console.log("Switch to handle: ", leave_handle);
  392. current_handle = leave_handle;
  393. }
  394. }
  395. else {
  396. await beginInvoke(msg, ws);
  397. }
  398. });
  399. });
  400. console.log(process.platform);
  401. async function runBrowser(lang = "en", user_data_folder = '', mobile = false) {
  402. const serviceBuilder = new ServiceBuilder(driverPath);
  403. let options = new chrome.Options();
  404. options.addArguments('--disable-blink-features=AutomationControlled');
  405. language = lang;
  406. if (lang == "en") {
  407. options.addExtensions(path.join(__dirname, "EasySpider_en.crx"));
  408. } else if (lang == "zh") {
  409. options.addExtensions(path.join(__dirname, "EasySpider_zh.crx"));
  410. }
  411. options.addExtensions(path.join(__dirname, "XPathHelper.crx"));
  412. options.setChromeBinaryPath(chromeBinaryPath);
  413. if (user_data_folder != "") {
  414. let dir = path.join(task_server.getDir(), user_data_folder);
  415. console.log(dir);
  416. options.addArguments("--user-data-dir=" + dir);
  417. config.user_data_folder = user_data_folder;
  418. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  419. }
  420. if (mobile) {
  421. const mobileEmulation = {
  422. deviceName: 'iPhone XR'
  423. };
  424. options.addArguments(`--user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 13_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"`);
  425. options.setMobileEmulation(mobileEmulation);
  426. }
  427. driver = new Builder()
  428. .forBrowser('chrome')
  429. .setChromeOptions(options)
  430. .setChromeService(serviceBuilder)
  431. .build();
  432. await driver.manage().setTimeouts({implicit: 10000, pageLoad: 10000, script: 10000});
  433. await driver.executeScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})");
  434. // await driver.executeScript("localStorage.clear();"); //重置参数数量
  435. const cdpConnection = await driver.createCDPConnection("page");
  436. let stealth_path = path.join(__dirname, "stealth.min.js");
  437. let stealth = fs.readFileSync(stealth_path, 'utf8');
  438. await cdpConnection.execute('Page.addScriptToEvaluateOnNewDocument', {
  439. source: stealth,
  440. });
  441. try {
  442. if(mobile){
  443. await driver.get(server_address + "/taskGrid/taskList.html?wsport=" + websocket_port + "&backEndAddressServiceWrapper=" + server_address + "&mobile=1&lang=" + lang);
  444. } else {
  445. await driver.get(server_address + "/taskGrid/taskList.html?wsport=" + websocket_port + "&backEndAddressServiceWrapper=" + server_address + "&lang=" + lang);
  446. }
  447. old_handles = await driver.getAllWindowHandles();
  448. current_handle = old_handles[old_handles.length - 1];
  449. } finally {
  450. // await driver.quit(); // 退出浏览器
  451. }
  452. }
  453. function handleOpenBrowser(event, lang = "en", user_data_folder = "", mobile = false) {
  454. const webContents = event.sender;
  455. const win = BrowserWindow.fromWebContents(webContents);
  456. runBrowser(lang, user_data_folder, mobile);
  457. let size = screen.getPrimaryDisplay().workAreaSize;
  458. let width = parseInt(size.width);
  459. let height = parseInt(size.height * 0.6);
  460. flowchart_window = new BrowserWindow({
  461. x: 0,
  462. y: 0,
  463. width: width,
  464. height: height,
  465. icon: iconPath,
  466. });
  467. let url = "";
  468. let id = -1;
  469. if (lang == "en") {
  470. url = server_address + `/taskGrid/FlowChart.html?id=${id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address + "&mobile=" + mobile.toString();
  471. } else if (lang == "zh") {
  472. url = server_address + `/taskGrid/FlowChart_CN.html?id=${id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address+ "&mobile=" + mobile.toString();
  473. }
  474. // and load the index.html of the app.
  475. flowchart_window.loadURL(url, { extraHeaders: 'pragma: no-cache\n' });
  476. if(process.platform != "darwin"){
  477. flowchart_window.hide();
  478. }
  479. flowchart_window.on('close', function (event) {
  480. mainWindow.show();
  481. driver.quit();
  482. });
  483. }
  484. function handleOpenInvoke(event, lang = "en") {
  485. const window = new BrowserWindow({icon: iconPath});
  486. let url = "";
  487. language = lang;
  488. if (lang == "en") {
  489. url = server_address + `/taskGrid/taskList.html?type=1&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address;
  490. } else if (lang == "zh") {
  491. url = server_address + `/taskGrid/taskList.html?type=1&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address + "&lang=zh";
  492. }
  493. // and load the index.html of the app.
  494. window.loadURL(url, { extraHeaders: 'pragma: no-cache\n' });
  495. window.maximize();
  496. mainWindow.hide();
  497. window.on('close', function (event) {
  498. mainWindow.show();
  499. });
  500. }
  501. // This method will be called when Electron has finished
  502. // initialization and is ready to create browser windows.
  503. // Some APIs can only be used after this event occurs.
  504. app.whenReady().then(() => {
  505. session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
  506. details.requestHeaders['Accept-Language'] = 'zh'
  507. callback({ cancel: false, requestHeaders: details.requestHeaders })
  508. })
  509. ipcMain.on('start-design', handleOpenBrowser);
  510. ipcMain.on('start-invoke', handleOpenInvoke);
  511. createWindow();
  512. app.on('activate', function () {
  513. // On macOS it's common to re-create a window in the app when the
  514. // dock icon is clicked and there are no other windows open.
  515. if (BrowserWindow.getAllWindows().length === 0) {
  516. createWindow();
  517. }
  518. })
  519. })
  520. // Quit when all windows are closed, except on macOS. There, it's common
  521. // for applications and their menu bar to stay active until the user quits
  522. // explicitly with Cmd + Q.
  523. app.on('window-all-closed', function () {
  524. if (process.platform !== 'darwin') {
  525. app.quit();
  526. }
  527. })
  528. // In this file you can include the rest of your app's specific main process
  529. // code. You can also put them in separate files and require them here.
  530. function arrayDifference(arr1, arr2) {
  531. return arr1.filter(item => !arr2.includes(item));
  532. }