main.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  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, Select, StaleElementReferenceException} = 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. let config_context = JSON.parse(fs.readFileSync(path.join(task_server.getDir(), `config.json`), 'utf8')); //仅在当前进程中使用,不会写入文件
  18. if (config.debug) {
  19. let logPath = 'info.log'
  20. let logFile = fs.createWriteStream(logPath, {flags: 'a'})
  21. console.log = function () {
  22. logFile.write(util.format.apply(null, arguments) + '\n')
  23. process.stdout.write(util.format.apply(null, arguments) + '\n')
  24. }
  25. console.error = function () {
  26. logFile.write(util.format.apply(null, arguments) + '\n')
  27. process.stderr.write(util.format.apply(null, arguments) + '\n')
  28. }
  29. }
  30. let allWindowSockets = [];
  31. let allWindowScoketNames = [];
  32. task_server.start(config.webserver_port); //start local server
  33. let server_address = `${config.webserver_address}:${config.webserver_port}`;
  34. const websocket_port = 8084; //目前只支持8084端口,写死,因为扩展里面写死了
  35. console.log("server_address: " + server_address);
  36. let driverPath = "";
  37. let chromeBinaryPath = "";
  38. let execute_path = "";
  39. console.log(process.arch);
  40. exec(`wmic os get Caption`, function (error, stdout, stderr) {
  41. if (error) {
  42. console.error(`执行的错误: ${error}`);
  43. return;
  44. }
  45. if (stdout.includes('Windows 7')) {
  46. console.log('Windows 7');
  47. let sys_arch = config.sys_arch;
  48. if (sys_arch === 'x64') {
  49. dialog.showMessageBoxSync({
  50. type: 'error',
  51. title: 'Error',
  52. message: 'Windows 7系统请下载使用x32版本的软件,不论Win 7系统为x64还是x32版本。\nFor Windows 7, please download and use the x32 version of the software, regardless of whether the Win 7 system is x64 or x32 version.',
  53. });
  54. }
  55. } else {
  56. console.log('Not Windows 7');
  57. }
  58. });
  59. if (process.platform === 'win32' && process.arch === 'ia32') {
  60. driverPath = path.join(__dirname, "chrome_win32/chromedriver_win32.exe");
  61. chromeBinaryPath = path.join(__dirname, "chrome_win32/chrome.exe");
  62. execute_path = path.join(__dirname, "chrome_win32/execute.bat");
  63. } else if (process.platform === 'win32' && process.arch === 'x64') {
  64. driverPath = path.join(__dirname, "chrome_win64/chromedriver_win64.exe");
  65. chromeBinaryPath = path.join(__dirname, "chrome_win64/chrome.exe");
  66. execute_path = path.join(__dirname, "chrome_win64/execute.bat");
  67. } else if (process.platform === 'darwin') {
  68. driverPath = path.join(__dirname, "chromedriver_mac64");
  69. chromeBinaryPath = path.join(__dirname, "chrome_mac64.app/Contents/MacOS/Google Chrome");
  70. execute_path = path.join(__dirname, "");
  71. } else if (process.platform === 'linux') {
  72. driverPath = path.join(__dirname, "chrome_linux64/chromedriver_linux64");
  73. chromeBinaryPath = path.join(__dirname, "chrome_linux64/chrome");
  74. execute_path = path.join(__dirname, "chrome_linux64/execute.sh");
  75. }
  76. console.log(driverPath, chromeBinaryPath, execute_path);
  77. let language = "en";
  78. let driver = null;
  79. let mainWindow = null;
  80. let flowchart_window = null;
  81. let current_handle = null;
  82. let old_handles = [];
  83. let handle_pairs = {};
  84. let socket_window = null;
  85. let socket_start = null;
  86. let socket_flowchart = null;
  87. let invoke_window = null;
  88. // var ffi = require('ffi-napi');
  89. // var libm = ffi.Library('libm', {
  90. // 'ceil': [ 'double', [ 'double' ] ]
  91. // });
  92. // libm.ceil(1.5); // 2
  93. // const {user32FindWindowEx,
  94. // winspoolGetDefaultPrinter,} = require('win32-api/fun');
  95. // async function testt(){
  96. // // 获取当前电脑当前用户默认打印机名
  97. // const printerName = await winspoolGetDefaultPrinter()
  98. // console.log(printerName);
  99. // }
  100. // testt();
  101. function createWindow() {
  102. // Create the browser window.
  103. mainWindow = new BrowserWindow({
  104. width: 600,
  105. height: 800,
  106. webPreferences: {
  107. preload: path.join(__dirname, 'src/js/preload.js')
  108. },
  109. icon: iconPath,
  110. // frame: false, //取消window自带的关闭最小化等
  111. resizable: false //禁止改变主窗口尺寸
  112. })
  113. // and load the index.html of the app.
  114. // mainWindow.loadFile('src/index.html');
  115. mainWindow.loadURL(server_address + '/index.html?user_data_folder=' + config.user_data_folder + "&copyright=" + config.copyright, {extraHeaders: 'pragma: no-cache\n'});
  116. // 隐藏菜单栏
  117. const {Menu} = require('electron');
  118. Menu.setApplicationMenu(null);
  119. mainWindow.on('close', function (e) {
  120. if (process.platform !== 'darwin') {
  121. app.quit();
  122. }
  123. });
  124. // mainWindow.webContents.openDevTools();
  125. // Open the DevTools.
  126. // mainWindow.webContents.openDevTools()
  127. }
  128. async function findElementRecursive(driver, by, value, frames) {
  129. for (const frame of frames) {
  130. try {
  131. // Try to switch to the frame
  132. try {
  133. await driver.switchTo().frame(frame);
  134. } catch (error) {
  135. if (error.name.indexOf('StaleElement') >= 0) {
  136. // If the frame is stale, switch to the parent frame and then retry switching to the frame
  137. await driver.switchTo().parentFrame();
  138. await driver.switchTo().frame(frame);
  139. } else {
  140. // If it is another exception rethrow it
  141. throw error;
  142. }
  143. }
  144. let element;
  145. try {
  146. // Attempt to find the element in this frame
  147. element = await driver.findElement(by(value));
  148. return element;
  149. } catch (error) {
  150. if (error.name.indexOf('NoSuchElement') >= 0) {
  151. // The element was not found in this frame, recurse into nested iframes
  152. const nestedFrames = await driver.findElements(By.tagName("iframe"));
  153. if (nestedFrames.length > 0) {
  154. element = await findElementRecursive(driver, by, value, nestedFrames);
  155. if (element) {
  156. return element;
  157. }
  158. }
  159. } else {
  160. // If it is another exception, log it
  161. console.error(`Exception while processing frame: ${error}`);
  162. }
  163. }
  164. } catch (error) {
  165. console.error(`Exception while processing frame: ${error}`);
  166. }
  167. }
  168. throw new Error(`Element ${value} not found in any frame or iframe`);
  169. }
  170. async function findElement(driver, by, value, iframe = false) {
  171. // Switch back to the main document
  172. await driver.switchTo().defaultContent();
  173. if (iframe) {
  174. const frames = await driver.findElements(By.tagName("iframe"));
  175. if (frames.length === 0) {
  176. throw new Error(`No iframes found in the current page while searching for ${value}`);
  177. }
  178. const element = await findElementRecursive(driver, by, value, frames);
  179. return element;
  180. } else {
  181. // Find element in the main document as normal
  182. let element = await driver.findElement(by(value));
  183. return element;
  184. }
  185. }
  186. async function findElementAcrossAllWindows(msg, notifyBrowser = true, scrollIntoView = true) {
  187. let handles = await driver.getAllWindowHandles();
  188. // console.log("handles", handles);
  189. let content_handle = current_handle;
  190. let id = -1;
  191. try {
  192. id = msg.message.id;
  193. } catch {
  194. id = msg.id;
  195. }
  196. if (id == -1) { //如果是-1,从当前窗口开始搜索
  197. content_handle = current_handle;
  198. } else {
  199. content_handle = handle_pairs[id];
  200. }
  201. // console.log(msg.message.id, content_handle);
  202. let order = [...handles.filter(handle => handle != current_handle && handle != content_handle), current_handle, content_handle]; //搜索顺序
  203. let len = order.length;
  204. let element = null;
  205. let iframe = false;
  206. try {
  207. iframe = msg.message.iframe;
  208. } catch {
  209. iframe = msg.iframe;
  210. }
  211. // if (iframe) {
  212. // notify_browser("在IFrame中执行操作可能需要较长时间,请耐心等待。", "Executing operations in IFrame may take a long time, please wait patiently.", "info");
  213. // }
  214. let xpath = "";
  215. try {
  216. xpath = msg.message.xpath;
  217. } catch {
  218. //如果msg.pathList存在,说明是循环中的元素
  219. if (msg.pathList != undefined && msg.pathList != null && msg.pathList != "") {
  220. xpath = msg.pathList[0].trim();
  221. } else {
  222. xpath = msg.xpath;
  223. }
  224. }
  225. if (xpath.indexOf("Field(") >= 0 || xpath.indexOf("eval(") >= 0) {
  226. //两秒后通知浏览器
  227. await new Promise(resolve => setTimeout(resolve, 2000));
  228. notify_browser("检测到XPath中包含Field(\"\")或eval(\"\"),试运行时无法正常定位到包含此两项表达式的元素,请在任务正式运行阶段测试是否有效。", "Field(\"\") or eval(\"\") is detected in xpath, and the element containing these two expressions cannot be located normally during trial operation. Please test whether it is valid in the formal call stage.", "warning");
  229. return null;
  230. }
  231. let notify = false;
  232. while (true) {
  233. // console.log("handles");
  234. try {
  235. let h = order[len - 1];
  236. console.log("current_handle", current_handle);
  237. if (h != null && handles.includes(h)) {
  238. await driver.switchTo().window(h);
  239. current_handle = h;
  240. console.log("switch to handle: ", h);
  241. }
  242. element = await findElement(driver, By.xpath, xpath, iframe);
  243. break;
  244. } catch (error) {
  245. console.log("len", len);
  246. len = len - 1;
  247. if (!notify) {
  248. notify = true;
  249. // notify_browser("正在尝试在其他窗口中查找元素,请耐心等待。", "Trying to find elements in other windows, please wait patiently.", "info");
  250. }
  251. if (len == 0) {
  252. break;
  253. }
  254. }
  255. }
  256. if (element == null && notifyBrowser) {
  257. notify_browser("无法找到元素,请检查XPath是否正确:" + xpath, "Cannot find the element, please check if the XPath is correct: " + xpath, "warning");
  258. }
  259. if (element != null && scrollIntoView) {
  260. // 浏览器切换到元素位置稍微靠上的位置
  261. try {
  262. // let script = `arguments[0].scrollIntoView(true);`;
  263. let script = `arguments[0].scrollIntoView({block: "center", inline: "center"});`;
  264. await driver.executeScript(script, element);
  265. } catch (e) {
  266. console.log("Cannot scrollIntoView");
  267. }
  268. }
  269. return element;
  270. }
  271. async function beginInvoke(msg, ws) {
  272. if (msg.type == 1) {
  273. if (msg.message.id != -1) {
  274. let url = "";
  275. if (language == "zh") {
  276. url = server_address + `/taskGrid/FlowChart_CN.html?id=${msg.message.id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address;
  277. } else if (language == "en") {
  278. url = server_address + `/taskGrid/FlowChart.html?id=${msg.message.id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address;
  279. }
  280. console.log(url);
  281. flowchart_window.loadURL(url, {extraHeaders: 'pragma: no-cache\n'});
  282. }
  283. mainWindow.hide();
  284. // Prints the currently focused window bounds.
  285. // This method has to be called on macOS before changing the window's bounds, otherwise it will throw an error.
  286. // It will prompt an accessibility permission request dialog, if needed.
  287. if (process.platform != "linux" && process.platform != "darwin") {
  288. // 非用户信息模式下,设置窗口位置
  289. if (config_context.user_data_folder == null || config_context.user_data_folder == undefined || config_context.user_data_folder == "") {
  290. const {windowManager} = require("node-window-manager");
  291. const window = windowManager.getActiveWindow();
  292. console.log(window);
  293. windowManager.requestAccessibility();
  294. // Sets the active window's bounds.
  295. let size = screen.getPrimaryDisplay().workAreaSize
  296. let width = parseInt(size.width)
  297. let height = parseInt(size.height * 0.6)
  298. window.setBounds({x: 0, y: size.height * 0.4, height: height, width: width});
  299. }
  300. }
  301. flowchart_window.show();
  302. // flowchart_window.openDevTools();
  303. } else if (msg.type == 2) {
  304. // 键盘输入事件
  305. // const robot = require("@jitsi/robotjs");
  306. let keyInfo = msg.message.keyboardStr;
  307. let enter = false;
  308. if (/<enter>/i.test(keyInfo)) {
  309. keyInfo = keyInfo.replace(/<enter>/gi, '');
  310. enter = true;
  311. }
  312. let element = await findElementAcrossAllWindows(msg, notifyBrowser = true, scrollIntoView = false);
  313. await element.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), keyInfo);
  314. if (enter) {
  315. await element.sendKeys(Key.ENTER);
  316. }
  317. } else if (msg.type == 3) {
  318. try {
  319. if (msg.from == 0) {
  320. socket_flowchart.send(msg.message.pipe); //直接把消息转接
  321. let message = JSON.parse(msg.message.pipe);
  322. let type = message.type;
  323. console.log("FROM Browser: ", message);
  324. if (type.indexOf("Click") >= 0 || type.indexOf("Move") >= 0) {
  325. let element = await findElementAcrossAllWindows(message, notifyBrowser = true, scrollIntoView = false);
  326. if (type.indexOf("Click") >= 0) {
  327. await click_element(element, type);
  328. } else if (type.indexOf("Move") >= 0) {
  329. await driver.actions().move({origin: element}).perform();
  330. }
  331. }
  332. } else {
  333. send_message_to_browser(msg.message.pipe);
  334. console.log("FROM Flowchart: ", JSON.parse(msg.message.pipe));
  335. }
  336. } catch (e) {
  337. console.log(e);
  338. }
  339. } else if (msg.type == 4) { //标记元素和试运行功能
  340. let node = JSON.parse(msg.message.node);
  341. let type = msg.message.type;
  342. if (type == 0) { //标记元素
  343. let option = node.option;
  344. let parameters = node.parameters;
  345. //下面是让浏览器自动滚动到元素位置
  346. if (option == 2 || option == 4 || option == 6 || option == 7) {
  347. let xpath = parameters.xpath;
  348. let parent_node = JSON.parse(msg.message.parentNode);
  349. if (parameters.useLoop && option != 4 && option != 6) {
  350. let parent_xpath = parent_node.parameters.xpath;
  351. if (parent_node.parameters.loopType == 2) {
  352. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  353. }
  354. xpath = parent_xpath + xpath;
  355. }
  356. if (xpath.includes("point(")) {
  357. xpath = "//body";
  358. }
  359. let elementInfo = {"iframe": parameters.iframe, "xpath": xpath, "id": -1};
  360. //用于跳转到元素位置
  361. let element = await findElementAcrossAllWindows(elementInfo);
  362. } else if (option == 3) {
  363. let params = parameters.params; //所有的提取数据参数
  364. let param = params[0];
  365. let xpath = param.relativeXPath;
  366. if (param.relative) {
  367. let parent_node = JSON.parse(msg.message.parentNode);
  368. let parent_xpath = parent_node.parameters.xpath;
  369. if (parent_node.parameters.loopType == 2) {
  370. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  371. }
  372. xpath = parent_xpath + xpath;
  373. }
  374. let elementInfo = {"iframe": param.iframe, "xpath": xpath, "id": -1};
  375. let element = await findElementAcrossAllWindows(elementInfo);
  376. } else if (option == 11) {
  377. let params = parameters.params; //所有的提取数据参数
  378. let i = parameters.index;
  379. let param = params[i];
  380. let xpath = param.relativeXPath;
  381. if (param.relative) {
  382. let parent_node = JSON.parse(msg.message.parentNode);
  383. let parent_xpath = parent_node.parameters.xpath;
  384. if (parent_node.parameters.loopType == 2) {
  385. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  386. }
  387. xpath = parent_xpath + xpath;
  388. }
  389. let elementInfo = {"iframe": param.iframe, "xpath": xpath, "id": -1};
  390. let element = await findElementAcrossAllWindows(elementInfo);
  391. } else if (option == 8) {
  392. let loopType = parameters.loopType;
  393. if (loopType <= 2) {
  394. let xpath = "";
  395. if (loopType <= 1) {
  396. xpath = parameters.xpath;
  397. } else if (loopType == 2) {
  398. xpath = parameters.pathList.split("\n")[0].trim();
  399. }
  400. let elementInfo = {"iframe": parameters.iframe, "xpath": xpath, "id": -1};
  401. let element = await findElementAcrossAllWindows(elementInfo);
  402. } else if (loopType == 5) { //JavaScript命令返回值
  403. let code = parameters.code;
  404. let waitTime = parameters.waitTime;
  405. let element = await driver.findElement(By.tagName("body"));
  406. let outcome = await execute_js(code, element, waitTime);
  407. if (!outcome || outcome == -1) {
  408. notify_browser("目前页面中,设置的循环“" + node.title + "”的JavaScript条件不成立", "The condition of the loop " + node.title + " is not met, skip this loop.", "warning");
  409. } else {
  410. notify_browser("目前页面中,设置的循环“" + node.title + "”的JavaScript条件成立", "The condition of the loop " + node.title + " is met, continue this loop.", "success");
  411. }
  412. }
  413. } else if (option == 10) { //条件分支
  414. let condition = parameters.class; //条件类型
  415. let result = -1;
  416. let additionalInfo = "";
  417. if (condition == 5 || condition == 7) { //JavaScript命令返回值
  418. let code = parameters.code;
  419. let waitTime = parameters.waitTime;
  420. let element = await driver.findElement(By.tagName("body"));
  421. if (condition == 7) {
  422. let parent_node = JSON.parse(msg.message.parentNode);
  423. let parent_xpath = parent_node.parameters.xpath;
  424. if (parent_node.parameters.loopType == 2) {
  425. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  426. }
  427. let elementInfo = {"iframe": parent_node.parameters.iframe, "xpath": parent_xpath, "id": -1};
  428. element = await findElementAcrossAllWindows(elementInfo);
  429. }
  430. let outcome = await execute_js(code, element, waitTime);
  431. if (!outcome) {
  432. msg.message.result = 0; //条件不成立传入扩展
  433. } else if (outcome == -1) {
  434. msg.message.result = -1; //JS执行出错
  435. } else {
  436. msg.message.result = 1; //条件成立传入扩展
  437. }
  438. }
  439. }
  440. send_message_to_browser(JSON.stringify({"type": "trial", "message": msg}));
  441. } else { //试运行
  442. try {
  443. let flowchart_url = flowchart_window.webContents.getURL();
  444. } catch {
  445. flowchart_window = null;
  446. }
  447. if (flowchart_window == null) {
  448. notify_flowchart("试运行功能只能在任务设计阶段,Chrome浏览器打开时使用!", "The trial run function can only be used when designing tasks and opening in Chrome browser!", "error");
  449. } else {
  450. notify_browser("正在试运行操作:" + node.title, "Trying to run the operation: " + node.title, "info");
  451. let option = node.option;
  452. let parameters = node.parameters;
  453. let beforeJS = "";
  454. let beforeJSWaitTime = 0;
  455. let afterJS = "";
  456. let afterJSWaitTime = 0;
  457. try {
  458. beforeJS = parameters.beforeJS;
  459. beforeJSWaitTime = parameters.beforeJSWaitTime;
  460. afterJS = parameters.afterJS;
  461. afterJSWaitTime = parameters.afterJSWaitTime;
  462. } catch (e) {
  463. console.log(e);
  464. }
  465. if (option == 1) {
  466. let url = parameters.links.split("\n")[0].trim();
  467. if (parameters.useLoop) {
  468. let parent_node = JSON.parse(msg.message.parentNode);
  469. url = parent_node["parameters"]["textList"].split("\n")[0];
  470. }
  471. try {
  472. await driver.get(url);
  473. } catch (e) {
  474. try {
  475. await driver.switchTo().window(current_handle);
  476. await driver.get(url);
  477. } catch (e) {
  478. let all_handles = await driver.getAllWindowHandles();
  479. let handle = all_handles[all_handles.length - 1];
  480. await driver.switchTo().window(handle);
  481. await driver.get(url);
  482. }
  483. }
  484. } else if (option == 2 || option == 7) { //点击事件
  485. let xpath = parameters.xpath;
  486. let point = parameters.xpath;
  487. if (xpath.includes("point(")) {
  488. xpath = "//body"
  489. }
  490. let elementInfo = {"iframe": parameters.iframe, "xpath": xpath, "id": -1};
  491. if (parameters.useLoop && !parameters.xpath.includes("point(")) {
  492. let parent_node = JSON.parse(msg.message.parentNode);
  493. let parent_xpath = parent_node.parameters.xpath;
  494. if (parent_node.parameters.loopType == 2) {
  495. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  496. }
  497. elementInfo.xpath = parent_xpath + elementInfo.xpath;
  498. }
  499. let element = await findElementAcrossAllWindows(elementInfo, notifyBrowser = false); //通过此函数找到元素并切换到对应的窗口
  500. await execute_js(parameters.beforeJS, element, parameters.beforeJSWaitTime);
  501. if (option == 2) {
  502. if (parameters.xpath.includes("point(")) {
  503. await click_element(element, point);
  504. } else {
  505. await click_element(element);
  506. }
  507. let alertHandleType = parameters.alertHandleType;
  508. if (alertHandleType == 1) {
  509. try {
  510. await driver.switchTo().alert().accept();
  511. } catch (e) {
  512. console.log("No alert");
  513. }
  514. } else if (alertHandleType == 2) {
  515. try {
  516. await driver.switchTo().alert().dismiss();
  517. } catch (e) {
  518. console.log("No alert");
  519. }
  520. }
  521. } else if (option == 7) {
  522. await driver.actions().move({origin: element}).perform();
  523. }
  524. await execute_js(parameters.afterJS, element, parameters.afterJSWaitTime);
  525. send_message_to_browser(JSON.stringify({"type": "cancelSelection"}));
  526. } else if (option == 3) { //提取数据
  527. notify_browser("提示:提取数据操作只能试运行设置的JavaScript语句,且只针对第一个匹配的元素。", "Hint: can only test JavaScript statement set in the data extraction operation, and only for the first matching element.", "info");
  528. let params = parameters.params; //所有的提取数据参数
  529. let not_found_xpaths = [];
  530. for (let i = 0; i < params.length; i++) {
  531. let param = params[i];
  532. let xpath = param.relativeXPath;
  533. if (param.relative) {
  534. let parent_node = JSON.parse(msg.message.parentNode);
  535. let parent_xpath = parent_node.parameters.xpath;
  536. if (parent_node.parameters.loopType == 2) {
  537. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  538. }
  539. xpath = parent_xpath + xpath;
  540. }
  541. let elementInfo = {"iframe": param.iframe, "xpath": xpath, "id": -1};
  542. let element = await findElementAcrossAllWindows(elementInfo, notifyBrowser = false);
  543. if (element != null) {
  544. await execute_js(param.beforeJS, element, param.beforeJSWaitTime);
  545. await execute_js(param.afterJS, element, param.afterJSWaitTime);
  546. } else {
  547. not_found_xpaths.push(xpath);
  548. }
  549. }
  550. if (not_found_xpaths.length > 0) {
  551. notify_browser("无法找到以下元素,请检查XPath是否正确:" + not_found_xpaths.join("\n"), "Cannot find the element, please check if the XPath is correct: " + not_found_xpaths.join("\n"), "warning");
  552. }
  553. } else if (option == 4) { //键盘输入事件
  554. let elementInfo = {"iframe": parameters.iframe, "xpath": parameters.xpath, "id": -1};
  555. let value = node.parameters.value;
  556. if (node.parameters.useLoop) {
  557. let parent_node = JSON.parse(msg.message.parentNode);
  558. value = parent_node["parameters"]["textList"].split("\n")[0];
  559. let index = node.parameters.index;
  560. if (index > 0) {
  561. value = value.split("~")[index - 1];
  562. }
  563. }
  564. let keyInfo = value
  565. let enter = false;
  566. if (/<enter>/i.test(keyInfo)) {
  567. keyInfo = keyInfo.replace(/<enter>/gi, '');
  568. enter = true;
  569. }
  570. if (keyInfo.indexOf("Field(") >= 0 || keyInfo.indexOf("eval(") >= 0) {
  571. //两秒后通知浏览器
  572. await new Promise(resolve => setTimeout(resolve, 2000));
  573. notify_browser("检测到文字中包含Field(\"\")或eval(\"\"),试运行时无法输入两项表达式的替换值,请在任务正式运行阶段测试是否有效。", "Field(\"\") or eval(\"\") is detected in the text, and the replacement value of the two expressions cannot be entered during trial operation. Please test whether it is valid in the formal call stage.", "warning");
  574. }
  575. let element = await findElementAcrossAllWindows(elementInfo, notifyBrowser = false);
  576. await execute_js(beforeJS, element, beforeJSWaitTime);
  577. await element.sendKeys(Key.HOME, Key.chord(Key.SHIFT, Key.END), keyInfo);
  578. if (enter) {
  579. await element.sendKeys(Key.ENTER);
  580. }
  581. await execute_js(afterJS, element, afterJSWaitTime);
  582. } else if (option == 5) { //自定义操作的JS代码
  583. let code = parameters.code;
  584. let codeMode = parameters.codeMode;
  585. let waitTime = parameters.waitTime;
  586. let element = await driver.findElement(By.tagName("body"));
  587. if (codeMode == 0) {
  588. await execute_js(code, element, waitTime);
  589. } else if (codeMode == 8) {
  590. //刷新页面
  591. try {
  592. await driver.navigate().refresh();
  593. } catch (e) {
  594. try {
  595. await driver.switchTo().window(current_handle);
  596. await driver.navigate().refresh();
  597. } catch (e) {
  598. let all_handles = await driver.getAllWindowHandles();
  599. let handle = all_handles[all_handles.length - 1];
  600. await driver.switchTo().window(handle);
  601. await driver.navigate().refresh();
  602. }
  603. }
  604. }
  605. } else if (option == 6) { //切换下拉选项
  606. let optionMode = parseInt(parameters.optionMode);
  607. let optionValue = parameters.optionValue;
  608. if (node.parameters.useLoop) {
  609. let parent_node = JSON.parse(msg.message.parentNode);
  610. optionValue = parent_node["parameters"]["textList"].split("\n")[0];
  611. let index = node.parameters.index;
  612. if (index > 0) {
  613. optionValue = optionValue.split("~")[index - 1];
  614. }
  615. }
  616. let elementInfo = {"iframe": parameters.iframe, "xpath": parameters.xpath, "id": -1};
  617. let element = await findElementAcrossAllWindows(elementInfo, notifyBrowser = false);
  618. execute_js(beforeJS, element, beforeJSWaitTime);
  619. let dropdown = new Select(element);
  620. // Interacting with dropdown element based on optionMode
  621. switch (optionMode) {
  622. case 0: //切换到下一个选项
  623. let script = `var options = arguments[0].options;
  624. for (var i = 0; i < options.length; i++) {
  625. if (options[i].selected) {
  626. options[i].selected = false;
  627. if (i == options.length - 1) {
  628. options[0].selected = true;
  629. } else {
  630. options[i + 1].selected = true;
  631. }
  632. break;
  633. }
  634. }`;
  635. await driver.executeScript(script, element);
  636. break;
  637. case 1:
  638. await dropdown.selectByIndex(parseInt(optionValue));
  639. break;
  640. case 2:
  641. await dropdown.selectByValue(optionValue);
  642. break;
  643. case 3:
  644. await dropdown.selectByVisibleText(optionValue);
  645. break;
  646. default:
  647. throw new Error('Invalid option mode');
  648. }
  649. execute_js(afterJS, element, afterJSWaitTime);
  650. } else if (option == 11) { //单个提取数据参数
  651. notify_browser("提示:提取数据操作只能试运行设置的JavaScript语句,且只针对第一个匹配的元素。", "Hint: can only test JavaScript statement set in the data extraction operation, and only for the first matching element.", "info");
  652. let params = parameters.params; //所有的提取数据参数
  653. let i = parameters.index;
  654. let param = params[i];
  655. let xpath = param.relativeXPath;
  656. if (param.relative) {
  657. let parent_node = JSON.parse(msg.message.parentNode);
  658. let parent_xpath = parent_node.parameters.xpath;
  659. if (parent_node.parameters.loopType == 2) {
  660. parent_xpath = parent_node.parameters.pathList.split("\n")[0].trim();
  661. }
  662. xpath = parent_xpath + xpath;
  663. }
  664. let elementInfo = {"iframe": param.iframe, "xpath": xpath, "id": -1};
  665. let element = await findElementAcrossAllWindows(elementInfo, notifyBrowser = false);
  666. if (element != null) {
  667. await execute_js(param.beforeJS, element, param.beforeJSWaitTime);
  668. await execute_js(param.afterJS, element, param.afterJSWaitTime);
  669. }
  670. }
  671. }
  672. }
  673. } else if (msg.type == 5) {
  674. let child = require('child_process').execFile;
  675. // 参数顺序: 1. task id 2. server address 3. saved_file_name 4. "remote" or "local" 5. user_data_folder
  676. // var parameters = [msg.message.id, server_address];
  677. let parameters = [];
  678. console.log(msg.message)
  679. if (msg.message.user_data_folder == null || msg.message.user_data_folder == undefined || msg.message.user_data_folder == "") {
  680. parameters = ["--ids", "[" + msg.message.id + "]", "--server_address", server_address, "--user_data", 0];
  681. } else {
  682. let user_data_folder_path = path.join(task_server.getDir(), msg.message.user_data_folder);
  683. parameters = ["--ids", "[" + msg.message.id + "]", "--server_address", server_address, "--user_data", 1];
  684. config.user_data_folder = msg.message.user_data_folder;
  685. config.absolute_user_data_folder = user_data_folder_path;
  686. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  687. }
  688. if (msg.message.mysql_config_path != "-1") {
  689. config.mysql_config_path = msg.message.mysql_config_path;
  690. }
  691. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  692. // child('Chrome/easyspider_executestage.exe', parameters, function(err,stdout, stderr) {
  693. // console.log(stdout);
  694. // });
  695. let spawn = require("child_process").spawn;
  696. if (process.platform != "darwin" && msg.message.execute_type == 1 && msg.message.id != -1) {
  697. let child_process = spawn(execute_path, parameters);
  698. child_process.stdout.on('data', function (data) {
  699. console.log(data.toString());
  700. });
  701. }
  702. ws.send(JSON.stringify({
  703. "config_folder": task_server.getDir() + "/",
  704. "easyspider_location": task_server.getEasySpiderLocation()
  705. }));
  706. } else if (msg.type == 6) {
  707. try {
  708. flowchart_window.openDevTools();
  709. } catch {
  710. console.log("open devtools error");
  711. }
  712. try {
  713. invoke_window.openDevTools();
  714. } catch {
  715. console.log("open devtools error");
  716. }
  717. } else if (msg.type == 7) {
  718. // 获得当前页面Cookies
  719. try {
  720. let cookies = await driver.manage().getCookies();
  721. console.log("Cookies: ", cookies);
  722. let cookiesText = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('\n');
  723. socket_flowchart.send(JSON.stringify({"type": "GetCookies", "message": cookiesText}));
  724. } catch {
  725. console.log("Cannot get Cookies");
  726. }
  727. }
  728. }
  729. async function click_element(element, type = "click") {
  730. try {
  731. if (type == "loopClickEvery") {
  732. await driver.actions().keyDown(Key.CONTROL).click(element).keyUp(Key.CONTROL).perform();
  733. } else if (type.includes("point(")) {
  734. //point(10, 20)表示点击坐标为(10, 20)的位置
  735. let point = type.substring(6, type.length - 1).split(",");
  736. let x = parseInt(point[0]);
  737. let y = parseInt(point[1]);
  738. // let actions = driver.actions();
  739. // await actions.move({origin: element}).perform();
  740. // await actions.move({x: x, y: y}).perform();
  741. // await actions.click().perform();
  742. let script = `document.elementFromPoint(${x}, ${y}).click();`;
  743. await driver.executeScript(script);
  744. } else {
  745. await element.click();
  746. }
  747. } catch (e) {
  748. console.log(e);
  749. await driver.executeScript("arguments[0].click();", element);
  750. }
  751. }
  752. async function execute_js(js, element, wait_time = 3) {
  753. let outcome = 0;
  754. if (js.length != 0) {
  755. try {
  756. outcome = await driver.executeScript(js, element);
  757. if (wait_time == 0) {
  758. wait_time = 30000;
  759. }
  760. // await new Promise(resolve => setTimeout(resolve, wait_time));
  761. } catch (e) {
  762. // await new Promise(resolve => setTimeout(resolve, 2000));
  763. notify_browser("执行JavaScript出错,请检查JavaScript语句是否正确:" + js + "\n错误信息:" + e, "Error executing JavaScript, please check if the JavaScript statement is correct: " + js + "\nError message: " + e, "error");
  764. outcome = -1;
  765. }
  766. if (js.indexOf("Field(") >= 0 || js.indexOf("eval(") >= 0) {
  767. //两秒后通知浏览器
  768. await new Promise(resolve => setTimeout(resolve, 2000));
  769. notify_browser("检测到JavaScript中包含Field(\"\")或eval(\"\"),试运行时无法执行两项表达式,请在任务正式运行阶段测试是否有效。", "Field(\"\") or eval(\"\") is detected in JavaScript, and the two expressions cannot be executed during trial operation. Please test whether it is valid in the formal call stage.", "warning");
  770. }
  771. }
  772. return outcome;
  773. }
  774. function notify_flowchart(msg_zh, msg_en, level = "info") {
  775. socket_flowchart.send(JSON.stringify({"type": "notify", "level": level, "msg_zh": msg_zh, "msg_en": msg_en}));
  776. }
  777. function notify_browser(msg_zh, msg_en, level = "info") {
  778. send_message_to_browser(JSON.stringify({"type": "notify", "level": level, "msg_zh": msg_zh, "msg_en": msg_en}));
  779. }
  780. function send_message_to_browser(message) {
  781. socket_window.send(message);
  782. for (let i in allWindowSockets) {
  783. try {
  784. allWindowSockets[i].send(message);
  785. } catch {
  786. console.log("Cannot send to socket with id: ", allWindowScoketNames[i]);
  787. }
  788. }
  789. }
  790. const WebSocket = require('ws');
  791. const {all} = require("express/lib/application");
  792. const {copy} = require("selenium-webdriver/io");
  793. let wss = new WebSocket.Server({port: websocket_port});
  794. wss.on('connection', function (ws) {
  795. ws.on('message', async function (message, isBinary) {
  796. let msg = JSON.parse(message.toString());
  797. // console.log("\n\nGET A MESSAGE: ", msg);
  798. // console.log(msg, msg.type, msg.message);
  799. if (msg.type == 0) {
  800. if (msg.message.id == 0) {
  801. socket_window = ws;
  802. // socket_window.on('close', function (event) {
  803. // socket_window = null;
  804. // console.log("socket_window closed");
  805. // });
  806. // console.log("set socket_window at time: ", new Date());
  807. } else if (msg.message.id == 1) {
  808. socket_start = ws;
  809. console.log("set socket_start at time: ", new Date());
  810. } else if (msg.message.id == 2) {
  811. socket_flowchart = ws;
  812. // socket_flowchart.on('close', function (event) {
  813. // socket_flowchart = null;
  814. // console.log("socket_flowchart closed");
  815. // });
  816. console.log("set socket_flowchart at time: ", new Date());
  817. } else { //其他的ID是用来标识不同的浏览器标签页的
  818. // await new Promise(resolve => setTimeout(resolve, 200));
  819. let handles = await driver.getAllWindowHandles();
  820. if (arrayDifference(handles, old_handles).length > 0) {
  821. old_handles = handles;
  822. current_handle = handles[handles.length - 1];
  823. await driver.switchTo().window(current_handle);
  824. console.log("New tab opened, change current_handle to: ", current_handle);
  825. // 调整浏览器窗口大小,不然扩展会白屏
  826. let size = await driver.manage().window().getRect();
  827. let width = size.width;
  828. let height = size.height;
  829. await driver.manage().window().setRect({width: width, height: height + 10});
  830. // height = height - 1;
  831. await driver.manage().window().setRect({width: width, height: height});
  832. }
  833. await new Promise(resolve => setTimeout(resolve, 2000));
  834. handle_pairs[msg.message.id] = current_handle;
  835. console.log("Set handle_pair for id: ", msg.message.id, " to ", current_handle, ", title is: ", msg.message.title);
  836. socket_flowchart.send(JSON.stringify({"type": "title", "data": {"title": msg.message.title}}));
  837. allWindowSockets.push(ws);
  838. allWindowScoketNames.push(msg.message.id);
  839. console.log("set socket for id: ", msg.message.id, " at time: ", new Date());
  840. ws.on('close', async function (event) {
  841. let index = allWindowSockets.indexOf(ws);
  842. if (index > -1) {
  843. allWindowSockets.splice(index, 1);
  844. allWindowScoketNames.splice(index, 1);
  845. }
  846. let handles = await driver.getAllWindowHandles();
  847. if (handles.length < old_handles.length) {
  848. old_handles = handles;
  849. current_handle = handles[handles.length - 1];
  850. await driver.switchTo().window(current_handle);
  851. console.log("Current tab closed, change current_handle to: ", current_handle);
  852. }
  853. console.log("socket for id: ", msg.message.id, " closed at time: ", new Date());
  854. });
  855. // console.log("handle_pairs: ", handle_pairs);
  856. }
  857. } else if (msg.type == 10) {
  858. let leave_handle = handle_pairs[msg.message.id];
  859. if (leave_handle != null && leave_handle != undefined && leave_handle != "") {
  860. await driver.switchTo().window(leave_handle);
  861. console.log("Switch to handle: ", leave_handle);
  862. current_handle = leave_handle;
  863. }
  864. } else {
  865. await beginInvoke(msg, ws);
  866. }
  867. });
  868. });
  869. console.log(process.platform);
  870. async function runBrowser(lang = "en", user_data_folder = '', mobile = false) {
  871. const serviceBuilder = new ServiceBuilder(driverPath);
  872. let options = new chrome.Options();
  873. options.addArguments('--disable-blink-features=AutomationControlled');
  874. options.addArguments('--disable-infobars');
  875. // 添加实验性选项以排除'enable-automation'开关
  876. options.set('excludeSwitches', ['enable-automation']);
  877. options.excludeSwitches("enable-automation")
  878. // 添加实验性选项来禁用自动化扩展
  879. options.set('useAutomationExtension', false);
  880. // options.addArguments('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36');
  881. options.set
  882. language = lang;
  883. if (lang == "en") {
  884. options.addExtensions(path.join(__dirname, "EasySpider_en.crx"));
  885. } else if (lang == "zh") {
  886. options.addExtensions(path.join(__dirname, "EasySpider_zh.crx"));
  887. }
  888. options.addExtensions(path.join(__dirname, "XPathHelper.crx"));
  889. options.setChromeBinaryPath(chromeBinaryPath);
  890. if (user_data_folder != "") {
  891. let dir = path.join(task_server.getDir(), user_data_folder);
  892. console.log(dir);
  893. options.addArguments("--user-data-dir=" + dir);
  894. config.user_data_folder = user_data_folder;
  895. config_context.user_data_folder = user_data_folder;
  896. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  897. } else {
  898. config_context.user_data_folder = "";
  899. }
  900. if (mobile) {
  901. const mobileEmulation = {
  902. deviceName: 'iPhone XR'
  903. };
  904. 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"`);
  905. options.setMobileEmulation(mobileEmulation);
  906. }
  907. driver = new Builder()
  908. .forBrowser('chrome')
  909. .setChromeOptions(options)
  910. .setChromeService(serviceBuilder)
  911. .build();
  912. await driver.manage().setTimeouts({implicit: 3, pageLoad: 10000, script: 10000});
  913. await driver.executeScript("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})");
  914. // await driver.executeScript("localStorage.clear();"); //重置参数数量
  915. const cdpConnection = await driver.createCDPConnection("page");
  916. let stealth_path = path.join(__dirname, "stealth.min.js");
  917. let stealth = fs.readFileSync(stealth_path, 'utf8');
  918. await cdpConnection.execute('Page.addScriptToEvaluateOnNewDocument', {
  919. source: stealth,
  920. });
  921. try {
  922. if (mobile) {
  923. await driver.get(server_address + "/taskGrid/taskList.html?wsport=" + websocket_port + "&backEndAddressServiceWrapper=" + server_address + "&mobile=1&lang=" + lang);
  924. } else {
  925. await driver.get(server_address + "/taskGrid/taskList.html?wsport=" + websocket_port + "&backEndAddressServiceWrapper=" + server_address + "&lang=" + lang);
  926. }
  927. old_handles = await driver.getAllWindowHandles();
  928. current_handle = old_handles[old_handles.length - 1];
  929. } finally {
  930. // await driver.quit(); // 退出浏览器
  931. }
  932. }
  933. function handleOpenBrowser(event, lang = "en", user_data_folder = "", mobile = false) {
  934. const webContents = event.sender;
  935. const win = BrowserWindow.fromWebContents(webContents);
  936. runBrowser(lang, user_data_folder, mobile);
  937. let size = screen.getPrimaryDisplay().workAreaSize;
  938. let width = parseInt(size.width);
  939. let height = parseInt(size.height * 0.5);
  940. flowchart_window = new BrowserWindow({
  941. x: 0,
  942. y: 0,
  943. width: width,
  944. height: height,
  945. icon: iconPath,
  946. maximizable: true,
  947. resizable: true,
  948. });
  949. let url = "";
  950. let id = -1;
  951. if (lang == "en") {
  952. url = server_address + `/taskGrid/FlowChart.html?id=${id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address + "&mobile=" + mobile.toString();
  953. } else if (lang == "zh") {
  954. url = server_address + `/taskGrid/FlowChart_CN.html?id=${id}&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address + "&mobile=" + mobile.toString();
  955. }
  956. // and load the index.html of the app.
  957. flowchart_window.loadURL(url, {extraHeaders: 'pragma: no-cache\n'});
  958. if (process.platform != "darwin") {
  959. flowchart_window.hide();
  960. }
  961. flowchart_window.on('close', function (event) {
  962. mainWindow.show();
  963. driver.quit();
  964. });
  965. }
  966. function handleOpenInvoke(event, lang = "en") {
  967. invoke_window = new BrowserWindow({icon: iconPath});
  968. let url = "";
  969. language = lang;
  970. if (lang == "en") {
  971. url = server_address + `/taskGrid/taskList.html?type=1&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address;
  972. } else if (lang == "zh") {
  973. url = server_address + `/taskGrid/taskList.html?type=1&wsport=${websocket_port}&backEndAddressServiceWrapper=` + server_address + "&lang=zh";
  974. }
  975. // and load the index.html of the app.
  976. invoke_window.loadURL(url, {extraHeaders: 'pragma: no-cache\n'});
  977. invoke_window.maximize();
  978. mainWindow.hide();
  979. invoke_window.on('close', function (event) {
  980. mainWindow.show();
  981. });
  982. }
  983. // This method will be called when Electron has finished
  984. // initialization and is ready to create browser windows.
  985. // Some APIs can only be used after this event occurs.
  986. app.whenReady().then(() => {
  987. session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
  988. details.requestHeaders['Accept-Language'] = 'zh'
  989. callback({cancel: false, requestHeaders: details.requestHeaders})
  990. })
  991. ipcMain.on('start-design', handleOpenBrowser);
  992. ipcMain.on('start-invoke', handleOpenInvoke);
  993. ipcMain.on('accept-agreement', function (event, arg) {
  994. config.copyright = 1;
  995. fs.writeFileSync(path.join(task_server.getDir(), "config.json"), JSON.stringify(config));
  996. });
  997. createWindow();
  998. app.on('activate', function () {
  999. // On MacOS it's common to re-create a window in the app when the
  1000. // dock icon is clicked and there are no other windows open.
  1001. if (BrowserWindow.getAllWindows().length === 0) {
  1002. createWindow();
  1003. }
  1004. })
  1005. })
  1006. // Quit when all windows are closed, except on macOS. There, it's common
  1007. // for applications and their menu bar to stay active until the user quits
  1008. // explicitly with Cmd + Q.
  1009. app.on('window-all-closed', function () {
  1010. if (process.platform !== 'darwin') {
  1011. app.quit();
  1012. }
  1013. })
  1014. // In this file you can include the rest of your app's specific main process
  1015. // code. You can also put them in separate files and require them here.
  1016. function arrayDifference(arr1, arr2) {
  1017. return arr1.filter(item => !arr2.includes(item));
  1018. }