diff --git a/.gitignore b/.gitignore index 7a418e8..f3c99be 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,5 @@ dist_electron src/lib/* !src/lib/mac_service.sh -!src/lib/proxy_conf_helper \ No newline at end of file +!src/lib/proxy_conf_helper +!src/lib/LICENSE \ No newline at end of file diff --git a/package.json b/package.json index d336422..0881076 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electron-ssr", - "version": "0.3.0", + "version": "0.3.0-alpha.3", "description": "Cross platform ShadowsocksR GUI client built with electron", "author": { "name": "The Electron-SSR Authors", @@ -26,7 +26,6 @@ "iview": "^3.5.4", "mousetrap": "^1.6.3", "qr-image": "^3.2.0", - "rxjs": "^6.5.4", "sudo-prompt": "^9.1.1", "urlsafe-base64": "^1.0.0", "vue": "^2.6.10", diff --git a/src/lib/LICENSE b/src/lib/LICENSE new file mode 100644 index 0000000..d8abc2c --- /dev/null +++ b/src/lib/LICENSE @@ -0,0 +1,119 @@ +libsodium.dll + +/* + * ISC License + * + * Copyright (c) 2013-2020 + * Frank Denis + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +socks2http + +The MIT License (MIT) + +Copyright (c) 2020 xVanTuring + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +The MIT License (MIT) + +Copyright (c) 2017 Y. T. CHUNG + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +© 2020 GitHub, Inc. + +windows-kill.exe +MIT License + +Copyright (c) 2017-2018 Alireza Dabiri Nejad + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +sysproxy + +Copyright 2020 xVanTuring + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Copyright 2016-2019 Noisyfox + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/src/main/bootstrap.js b/src/main/bootstrap.js index 46c18ca..5267d56 100644 --- a/src/main/bootstrap.js +++ b/src/main/bootstrap.js @@ -17,13 +17,6 @@ export const readyPromise = new Promise(resolve => { app.once('ready', resolve) } }) -isPythonInstalled.then(async (value) => { - // 检查python是否安装 - if (!value) { - await readyPromise - dialog.showErrorBox($t('NOTI_PYTHON_MISSING_TITLE'), $t('NOTI_PYTHON_MISSING_DETAIL')) - } -}) // 未捕获的rejections process.on('unhandledRejection', (reason) => { @@ -67,6 +60,9 @@ if (isWin) { _s2hPath = path.resolve(__dirname, '../src/lib/socks2http') } else { _s2hPath = path.join(path.dirname(exePath), './3rdparty/socks2http') + if (isMac) { + _s2hPath = path.join(path.dirname(exePath), '../3rdparty/socks2http') + } } } export const winToolPath = _winToolPath @@ -85,10 +81,13 @@ async function init () { // 判断配置文件是否存在,不存在用默认数据写入 const configFileExists = await pathExists(appConfigPath) await ensureDir(path.join(appConfigDir, 'logs')) - - // 初始化确保文件存在, 10.11版本以下不支持该功能 - await readyPromise + isPythonInstalled.then(async (value) => { + // Check Python existence + if (!value) { + dialog.showErrorBox($t('NOTI_PYTHON_MISSING_TITLE'), $t('NOTI_PYTHON_MISSING_DETAIL')) + } + }) if (!configFileExists) { // todo better locale detect let config = null diff --git a/src/main/data.js b/src/main/data.js index 3186e06..b22504e 100644 --- a/src/main/data.js +++ b/src/main/data.js @@ -1,82 +1,70 @@ -import { Observable, Subject } from 'rxjs' - -import { multicast, refCount } from 'rxjs/operators' -import { readJson, writeJson } from 'fs-extra' +import events from 'events' +import fse from 'fs-extra' import bootstrapPromise, { appConfigPath } from './bootstrap' import { sendData } from './window' -import { EVENT_RX_SYNC_MAIN } from '../shared/events' -import { isArray, getUpdatedKeys, configMerge, clone } from '../shared/utils' -import defaultConfig, { mergeConfig } from '../shared/config' - -let promise -// 是因为调用app.quit还是手动点击窗口的叉号引起的关闭事件, true表示app.quit +import { EVENT_RX_SYNC_MAIN } from '@/shared/events' +import defaultConfig, { mergeConfig } from '@/shared/config' +import { isArray, getUpdatedKeys, configMerge, clone } from '@/shared/utils' +import logger from './logger' +let currentConfig = null let _isQuiting = false -// 是否是来自renderer的同步数据 -let isFromRenderer = false -// 当前配置 -export let currentConfig +let _isFromRenderer = false +class Data { + constructor () { + this.emitter = new events.EventEmitter() + } + async init () { + logger.debug('RX DATA Init') + await bootstrapPromise + const stored = await read() + mergeConfig(stored) + currentConfig = stored + this.next([stored, [], null, isProxyStarted(stored), false]) + } + next (data) { + this.emitter.emit('data', data) + } + subscribe (fn) { + this.emitter.on('data', fn) + } +} -// 读取配置 +let appConfig$ = new Data() +function isProxyStarted (appConfig) { + return !!(appConfig.enable && appConfig.configs && appConfig.configs[appConfig.index]) +} async function read () { - try { - return await readJson(appConfigPath) - } catch (e) { + let exist = await fse.pathExists(appConfigPath) + if (exist) { + try { + return fse.readJson(appConfigPath) + } catch (error) { + logger.error(`The config: ${appConfigPath} is corrupted, using the default config now!`) + return Promise.resolve(defaultConfig) + } + } else { return Promise.resolve(defaultConfig) } } -// 应用起步后初始化 -async function init () { - await bootstrapPromise - const stored = await read() - mergeConfig(stored) - return stored +// appConfig$.init() +export { + currentConfig, + appConfig$ } - -// 支持多播 -const subject = new Subject() -let _observe -const source = Observable.create(observe => { - _observe = observe - // 初始化数据 - promise = init().then(data => { - currentConfig = data - isFromRenderer = false - // 第一个参数为当前配置对象,第二个参数为变更的字段数组,第三个参数为旧配置,第四个参数为当前配置对应是否开启了代理,第五个参数为旧配置对应是否开启了代理 - observe.next([data, [], null, isProxyStarted(data), false]) - }) -}) - -// 当前是否已选择某节点,即socks代理是否选中并启用 -export function isProxyStarted (appConfig) { - return !!(appConfig.enable && appConfig.configs && appConfig.configs[appConfig.index]) -} - -/** - * 统一使用该接口从外部更新应用配置 - * @param {Object} targetConfig 要更新的配置 - */ export function updateAppConfig (targetConfig, fromRenderer = false, forceAppendArray = false) { const changedKeys = getUpdatedKeys(currentConfig, targetConfig) // // 只有有数据变更才更新配置 if (changedKeys.length) { const oldConfig = clone(currentConfig, true) configMerge(currentConfig, targetConfig, forceAppendArray) - isFromRenderer = fromRenderer - _observe.next([currentConfig, changedKeys, oldConfig, isProxyStarted(currentConfig), isProxyStarted(oldConfig)]) + _isFromRenderer = fromRenderer + appConfig$.next([currentConfig, changedKeys, oldConfig, isProxyStarted(currentConfig), isProxyStarted(oldConfig)]) } } - -/** - * 新增单/多个配置 - * @param {Array} configs 要添加的配置数组 - */ export function addConfigs (configs) { updateAppConfig({ configs: currentConfig.configs.concat(isArray(configs) ? configs : [configs]) }, false, true) } -export const appConfig$ = source.pipe(multicast(subject), refCount()) - -// 传参用于设定是退出应用还是关闭窗口 不传参表示返回当前状态 export function isQuiting (target) { if (target !== undefined) { _isQuiting = target @@ -84,18 +72,14 @@ export function isQuiting (target) { return _isQuiting } } - -// 配置文件变化时 -appConfig$.subscribe(data => { +appConfig$.subscribe((data) => { const [appConfig, changed] = data if (changed.length) { - // 如果更新则写入配置文件 - writeJson(appConfigPath, appConfig, { spaces: '\t' }) + fse.writeJson(appConfigPath, appConfig, { spaces: '\t' }) // 如果是从renderer同步过来的数据则不再同步回去,避免重复同步 - if (!isFromRenderer) { + // ignore if it came from Renderer + if (!_isFromRenderer) { sendData(EVENT_RX_SYNC_MAIN, appConfig) } } }) - -export default promise diff --git a/src/main/index.js b/src/main/index.js index 647589c..3d74b55 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -41,12 +41,8 @@ if (!isPrimaryInstance) { }) } createWindow() - if (isWin || isMac) { - app.setAsDefaultProtocolClient('ssr') - app.setAsDefaultProtocolClient('ss') - } - - // 开机自启动配置 + // load manually when window created + appConfig$.init() const AutoLauncher = new AutoLaunch({ name: 'Electron SSR', isHidden: true, @@ -54,30 +50,30 @@ if (!isPrimaryInstance) { useLaunchAgent: true } }) - - appConfig$.subscribe(data => { + appConfig$.subscribe(async (data) => { const [appConfig, changed] = data - if (!changed.length) { + if (changed.length === 0) { + // if there is no config, or ssrPath is not set, show window // 初始化时没有配置则打开页面,有配置则不显示主页面 - if (!appConfig.configs.length || !appConfig.ssrPath) { + if (appConfig.configs.length === 0 || !appConfig.ssrPath) { showWindow() } - } - if (!changed.length || changed.indexOf('autoLaunch') > -1) { - // 初始化或者选项变更时 - AutoLauncher.isEnabled().then(enabled => { - // 状态不相同时 + } else if (changed.indexOf('autoLaunch') > -1) { + try { + let enabled = await AutoLauncher.isEnabled() if (appConfig.autoLaunch !== enabled) { - return AutoLauncher[appConfig.autoLaunch ? 'enable' : 'disable']().catch(() => { - logger.error(`${appConfig.autoLaunch ? '执行' : '取消'}开机自启动失败`) - }) + await AutoLauncher[appConfig.autoLaunch ? 'enable' : 'disable']() } - }).catch(() => { - logger.error('获取开机自启状态失败') - }) + } catch (error) { + logger.error('Failed to process auto start') + logger.error(error) + } } }) - + if (isWin || isMac) { + app.setAsDefaultProtocolClient('ssr') + app.setAsDefaultProtocolClient('ss') + } // 电源状态检测 powerMonitor.on('suspend', () => { // 系统挂起时 @@ -92,10 +88,10 @@ if (!isPrimaryInstance) { // startProxy() startTask(currentConfig) }) - }).then(err => { + }).catch(err => { + logger.error('Failed at bootstrapPromise') logger.error(err) }) - app.on('window-all-closed', () => { logger.debug('window-all-closed') if (process.platform !== 'darwin') { @@ -111,10 +107,6 @@ if (!isPrimaryInstance) { e.preventDefault() const reflect = p => p.then(() => ({ status: 'fulfilled' }), e => ({ e, status: 'rejected' })) - stopTask() - destroyTray() - destroyWindow() - clearShortcuts() const asyncTask = [ setProxyToNone(), stopHttpProxyServer(), @@ -128,7 +120,10 @@ if (!isPrimaryInstance) { } } }) - + stopTask() + destroyTray() + clearShortcuts() + destroyWindow() app.exit(0) }) diff --git a/src/main/ipc.js b/src/main/ipc.js index bcada1d..fd4ce15 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -3,6 +3,7 @@ import { readJson } from 'fs-extra' import downloadGitRepo from 'download-git-repo' import * as events from '@/shared/events' import { appConfigPath, defaultSSRDownloadDir } from './bootstrap' +import { saveUpdateTime } from './subscribe' import { updateAppConfig } from './data' import { hideWindow, sendData } from './window' import { importConfigFromClipboard } from './tray-handler' @@ -54,6 +55,9 @@ ipcMain.on(events.EVENT_APP_HIDE_WINDOW, () => { }).on(events.EVENT_APP_OPEN_DIALOG, async (e, params) => { const ret = await dialog.showOpenDialog(params) e.reply(events.EVENT_APP_OPEN_DIALOG, ret.filePaths) +}).on(events.EVENT_SUBSCRIBE_SAVE_TIME, () => { + logger.debug('Saving Subsciption Update Time') + saveUpdateTime() }) /** diff --git a/src/main/menu.js b/src/main/menu.js index d8edb2f..ea84654 100644 --- a/src/main/menu.js +++ b/src/main/menu.js @@ -3,28 +3,73 @@ import { appConfig$ } from './data' import { changeProxy } from './tray' import * as handler from './tray-handler' import * as events from '../shared/events' -import { isMac, isLinux } from '../shared/env' +import { isMac, isOldMacVersion } from '../shared/env' import { sendData } from './window' import * as i18n from './locales' import { readyPromise } from './bootstrap' const $t = i18n.default -/** - * 渲染菜单 - */ +// todo provide a better menu +// do mac need app menu here? async function renderMenu (appConfig) { await readyPromise i18n.setLocal(appConfig.lang || 'en-US') - let template = [] - if (isMac) { - template = [ { + let template = [ + { label: $t('MENU_APPLICATION'), submenu: [ { label: $t('MENU_SUB_ENABLE_APP'), type: 'checkbox', checked: appConfig.enable, click: handler.toggleEnable }, { label: $t('MENU_SUB_COPY_HTTP_PROXY'), click: handler.copyHttpProxyCode }, { role: 'quit' } ] - }] - template.push({ + }, + { + label: $t('MENU_PAC'), + submenu: [ + { label: $t('MENU_SUB_UPDATE_PAC'), click: handler.updatePac }, + { + label: $t('MENU_SUB_PAC_MODE'), + submenu: [ + { label: 'GFW List' }, + { label: 'White List' } + ] + } + ] + }, + { + label: $t('MENU_SETTINGS'), + submenu: [ + { label: $t('MENU_SUB_SETTING_OPTIONS'), click: handler.showOptions }, + { label: $t('MENU_SUB_LOAD_CF'), click: handler.importConfigFromFile }, + { label: $t('MENU_SUB_EXPORT_CF'), click: handler.exportConfigToFile }, + { label: $t('MENU_SUB_OPEN_CF'), click: handler.openConfigFile } + ] + }, + { + label: $t('MENU_SUB_ADD'), + submenu: [ + { label: $t('MENU_SUB_ADD_SUB_LINK'), click: () => { sendData(events.EVENT_SUBSCRIBE_NEW) } }, + { label: $t('MENU_SUB_ADD_NODE'), click: createNewConfig }, + { label: $t('MENU_SUB_ADD_FROM_CB'), click: handler.importConfigFromClipboard }, + { label: $t('MENU_SUB_ADD_FROM_QR_SCAN'), click: handler.scanQRCode } + ] + }, + { + label: $t('MENU_HELP'), + submenu: [ + { + label: $t('MENU_SUB_DEVS'), + submenu: [ + { label: $t('MENU_SUB_DEVS_INSPECT_LOG'), click: handler.openLog }, + { role: 'toggleDevTools' }, + { role: 'reload' }, + { role: 'forceReload' } + ] + } + ] + } + ] + if (isMac) { + template.splice(1, 0, { label: 'Edit', submenu: [ { role: 'undo' }, @@ -38,16 +83,10 @@ async function renderMenu (appConfig) { { role: 'selectall' } ] }) - } else if (isLinux) { - template = [ - { - label: $t('MENU_APPLICATION'), - submenu: [ - { label: $t('MENU_SUB_ENABLE_APP'), type: 'checkbox', checked: appConfig.enable, click: handler.toggleEnable }, - { label: $t('MENU_SUB_COPY_HTTP_PROXY'), click: handler.copyHttpProxyCode }, - { role: 'quit' } - ] - }, + } + let macToolPathExist = appConfig['isMacToolInstalled'] + if (!isMac || (!await isOldMacVersion && macToolPathExist)) { + template.splice(2, 0, { label: $t('MENU_SYS_PROXY_MODE'), submenu: [ @@ -55,61 +94,8 @@ async function renderMenu (appConfig) { { label: $t('MENU_SUB_PAC_PROXY'), type: 'checkbox', checked: appConfig.sysProxyMode === 1, click: e => changeProxy(e, 1, appConfig) }, { label: $t('MENU_SUB_GLOBAL_PROXY'), type: 'checkbox', checked: appConfig.sysProxyMode === 2, click: e => changeProxy(e, 2, appConfig) } ] - }, - { - label: $t('MENU_PAC'), - submenu: [ - { label: $t('MENU_SUB_UPDATE_PAC'), click: handler.updatePac }, - { - label: $t('MENU_SUB_PAC_MODE'), - submenu: [ - { label: 'GFW List' }, - { label: 'White List' } - ] - } - ] - }, - { - label: $t('MENU_SETTINGS'), - submenu: [ - { label: $t('MENU_SUB_SETTING_OPTIONS'), click: handler.showOptions }, - { - label: $t('MENU_SUB_CONFIG_FILE'), - submenu: [ - { label: $t('MENU_SUB_LOAD_CF'), click: handler.importConfigFromFile }, - { label: $t('MENU_SUB_EXPORT_CF'), click: handler.exportConfigToFile }, - { label: $t('MENU_SUB_OPEN_CF'), click: handler.openConfigFile } - ] - }, - { - label: $t('MENU_SUB_ADD'), - submenu: [ - { label: $t('MENU_SUB_ADD_SUB_LINK'), click: () => { sendData(events.EVENT_SUBSCRIBE_NEW) } }, - { label: $t('MENU_SUB_ADD_NODE'), click: createNewConfig }, - { label: $t('MENU_SUB_ADD_FROM_CB'), click: handler.importConfigFromClipboard }, - { label: $t('MENU_SUB_ADD_FROM_QR_SCAN'), click: handler.scanQRCode } - ] - } - ] - }, - { - label: $t('MENU_HELP'), - submenu: [ - { - label: $t('MENU_SUB_DEVS'), - submenu: [ - { label: $t('MENU_SUB_DEVS_INSPECT_LOG'), click: handler.openLog }, - { role: 'toggleDevTools' }, - { role: 'reload' }, - { role: 'forceReload' } - ] - } - ] } - ] - } else { - Menu.setApplicationMenu(null) - return + ) } Menu.setApplicationMenu(Menu.buildFromTemplate(template)) } @@ -122,7 +108,7 @@ appConfig$.subscribe(data => { if (changed.length === 0) { renderMenu(appConfig) } else { - if (['enable', 'sysProxyMode', 'lang'].some(key => changed.indexOf(key) > -1)) { + if (['enable', 'sysProxyMode', 'lang', 'isMacToolInstalled'].some(key => changed.indexOf(key) > -1)) { renderMenu(appConfig) } } diff --git a/src/main/pac.js b/src/main/pac.js index 26a4209..88b1c22 100644 --- a/src/main/pac.js +++ b/src/main/pac.js @@ -23,6 +23,7 @@ httpShutdown.extend() export async function downloadPac (force = false) { await bootstrapPromise const pacExisted = await pathExists(pacRawPath) + logger.debug(`${pacRawPath} pacExisted: ${pacExisted}`) let pac = '' if (force || !pacExisted) { logger.debug('start download pac') @@ -30,7 +31,7 @@ export async function downloadPac (force = false) { await writeFile(pacRawPath, pac) // save raw pac file } else { // always gen pac from raw - pac = await readFile(pacRawPath) + pac = (await readFile(pacRawPath)).toString() } pac = pac.replace(/__PROXY__/g, `SOCKS5 127.0.0.1:${currentConfig.localPort}; SOCKS 127.0.0.1:${currentConfig.localPort}; PROXY 127.0.0.1:${currentConfig.localPort}; ${currentConfig.httpProxyEnable ? 'PROXY 127.0.0.1:' + currentConfig.httpProxyPort + ';' : ''} DIRECT`) @@ -46,7 +47,7 @@ let notified = false /** * pac server */ -export async function serverPac (appConfig, isProxyStarted) { +async function serverPac (appConfig, isProxyStarted) { if (isProxyStarted) { const host = currentConfig.shareOverLan ? '0.0.0.0' : '127.0.0.1' const port = appConfig.pacPort !== undefined ? appConfig.pacPort : currentConfig.pacPort || 1240 diff --git a/src/main/proxy.js b/src/main/proxy.js index e34f355..03958a1 100644 --- a/src/main/proxy.js +++ b/src/main/proxy.js @@ -122,16 +122,14 @@ export function startProxy (mode) { // 监听配置变化 appConfig$.subscribe(data => { const [appConfig, changed, , isProxyStarted] = data - // 必须得有节点选中 if (isProxyStarted) { if (!changed.length) { startProxy(appConfig.sysProxyMode) } else { - if (appConfig.sysProxyMode === 1 && changed.indexOf('pacPort') > -1) { - // pacPort变更时 + if (appConfig.sysProxyMode === 1 && (changed.indexOf('pacPort') > -1 || changed.indexOf('enable') > -1)) { + // pacPort changes or enable changed startProxy(1) - } else if (appConfig.sysProxyMode === 2 && changed.indexOf('localPort') > -1) { - // localPort变更时 + } else if (appConfig.sysProxyMode === 2 && (changed.indexOf('localPort') > -1 || changed.indexOf('enable') > -1)) { startProxy(2) } } diff --git a/src/main/subscribe.js b/src/main/subscribe.js index 211b4f3..e7b644c 100644 --- a/src/main/subscribe.js +++ b/src/main/subscribe.js @@ -1,6 +1,3 @@ -/** - * 订阅服务器 - */ import { readFile, writeFile } from 'fs-extra' import { subscribeUpdateFile } from './bootstrap' import { appConfig$ } from './data' @@ -22,20 +19,22 @@ export async function startTask (appConfig, forceUpdate = false) { stopTask() if (appConfig.autoUpdateSubscribes && appConfig.serverSubscribes.length) { if (forceUpdate) { - await update(appConfig) + await update() } // 单位是 时 const intervalTime = appConfig.subscribeUpdateInterval * 3600000 try { if (!forceUpdate) { const content = await readFile(subscribeUpdateFile, 'utf8') - lastUpdateTime = new Date(content.toString()) + lastUpdateTime = new Date(content) } const nextUpdateTime = new Date(+lastUpdateTime + intervalTime) logger.info('next subscribe update time: %s', nextUpdateTime) timeout(nextUpdateTime, intervalTime, appConfig) } catch (e) { - update(appConfig) + logger.error('Something wrong while starting subscribe task') + logger.error(e) + update() } } } @@ -43,7 +42,7 @@ export async function startTask (appConfig, forceUpdate = false) { // 间隔多久开始下一次更新,用下一次间隔时间减去当前时间 function timeout (nextUpdateTime, intervalTime, appConfig) { _timeout = setTimeout(() => { - update(appConfig) + update() interval(intervalTime, appConfig) }, nextUpdateTime - new Date()) } @@ -51,12 +50,12 @@ function timeout (nextUpdateTime, intervalTime, appConfig) { // 往后的更新都按照interval来进行 function interval (intervalTime, appConfig) { _interval = setInterval(() => { - update(appConfig) + update() }, intervalTime) } // 保存最近一次的更新时间 -async function saveUpdateTime () { +export async function saveUpdateTime () { const date = new Date() lastUpdateTime = date logger.info('last update time: %s', lastUpdateTime) @@ -64,7 +63,7 @@ async function saveUpdateTime () { } // 发起更新 -async function update (appConfig) { +async function update () { await saveUpdateTime() updateSubscribes() } @@ -89,7 +88,7 @@ appConfig$.subscribe(data => { const [appConfig, changed] = data // 初始化 if (changed.length === 0) { - startTask(appConfig, false) + startTask(appConfig) } else { if (['autoUpdateSubscribes', 'subscribeUpdateInterval'].some(key => changed.indexOf(key) > -1)) { startTask(appConfig) diff --git a/src/main/tray-handler.js b/src/main/tray-handler.js index 99cdf4d..49ec847 100644 --- a/src/main/tray-handler.js +++ b/src/main/tray-handler.js @@ -1,6 +1,6 @@ import { app, shell, clipboard } from 'electron' import { readJson, writeJson } from 'fs-extra' -import { join } from 'path' +import path from 'path' import sudo from 'sudo-prompt' import bootstrapPromise, { appConfigPath, exePath, macToolPath } from './bootstrap' import logger, { logPath } from './logger' @@ -67,7 +67,7 @@ export function importConfigFromFile () { export function exportConfigToFile () { const _path = chooseSavePath('选择导出的目录') if (_path) { - writeJson(join(_path, 'gui-config.json'), currentConfig, { spaces: '\t' }) + writeJson(path.join(_path, 'gui-config.json'), currentConfig, { spaces: '\t' }) } } @@ -152,7 +152,7 @@ export async function installMacHelpToolTray () { } export async function installMacHelpTool () { const helperPath = process.env.NODE_ENV === 'development' - ? join(__dirname, '../src/lib/proxy_conf_helper') - : join(exePath, '../../../Contents/proxy_conf_helper') + ? path.join(__dirname, '../src/lib/proxy_conf_helper') + : path.join(path.dirname(exePath), '../3rdparty/proxy_conf_helper') await sudoMacCommand(`cp ${helperPath} "${macToolPath}" && chown root:admin "${macToolPath}" && chmod a+rx "${macToolPath}" && chmod +s "${macToolPath}"`) } diff --git a/src/main/window.js b/src/main/window.js index 9808604..3ce1816 100644 --- a/src/main/window.js +++ b/src/main/window.js @@ -4,6 +4,7 @@ import logger from './logger' import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' +import { isLinux } from '@/shared/env' let mainWindow let readyPromise @@ -18,7 +19,7 @@ export function createWindow () { resizable: false, minimizable: false, maximizable: false, - show: true, + show: isLinux, webPreferences: { webSecurity: process.env.NODE_ENV === 'production', nodeIntegration: true } }) if (process.platform === 'darwin') { app.dock.show() } @@ -34,6 +35,7 @@ export function createWindow () { // hide to tray when window closed mainWindow.on('close', (e) => { // 当前不是退出APP的时候才去隐藏窗口 + // hide the windows if not quiting if (!isQuiting()) { e.preventDefault() mainWindow.hide() diff --git a/src/renderer/ipc.js b/src/renderer/ipc.js index bc1f1b9..93ba291 100644 --- a/src/renderer/ipc.js +++ b/src/renderer/ipc.js @@ -8,50 +8,51 @@ import { loadConfigsFromString } from '../shared/ssr' /** * ipc-render事件 */ -ipcRenderer.on(events.EVENT_APP_NOTIFY_MAIN, (e, { title, body }) => { - // 显示main进程的通知 - showHtmlNotification(body, title) -}).on(events.EVENT_APP_SCAN_DESKTOP, () => { - // 扫描二维码 - scanQrcode((e, result) => { - if (e) { - showNotification('未找到相关二维码', '扫码失败') - } else { - const configs = loadConfigsFromString(result) - if (configs.length) { - store.dispatch('addConfigs', configs) - showNotification(`已成功添加${configs.length}条记录`) +export function register () { + ipcRenderer.on(events.EVENT_APP_NOTIFY_MAIN, (e, { title, body }) => { + // 显示main进程的通知 + showHtmlNotification(body, title) + }).on(events.EVENT_APP_SCAN_DESKTOP, () => { + // 扫描二维码 + scanQrcode((e, result) => { + if (e) { + showNotification('未找到相关二维码', '扫码失败') + } else { + const configs = loadConfigsFromString(result) + if (configs.length) { + store.dispatch('addConfigs', configs) + showNotification(`已成功添加${configs.length}条记录`) + } } - } - }) -}).on(events.EVENT_APP_SHOW_PAGE, (e, targetView) => { - // 显示具体某页面 - console.log('received view update: ', targetView.page, targetView.tab) - store.commit('updateView', { ...targetView, fromMain: true }) -}).on(events.EVENT_APP_ERROR_MAIN, (e, err) => { - // 弹框显示main进程报错内容 - alert(err) -}).on(events.EVENT_SUBSCRIBE_UPDATE_MAIN, (e) => { - // 更新订阅服务器 - store.dispatch('updateSubscribes').then(updatedCount => { - if (updatedCount > 0) { - showNotification(`服务器订阅更新成功,共更新了${updatedCount}个节点`) - } else { - showNotification(`服务器订阅更新完成,没有新节点`) - } - }).catch(() => { - showNotification('服务器订阅更新失败') + }) + }).on(events.EVENT_APP_SHOW_PAGE, (e, targetView) => { + // 显示具体某页面 + console.log('received view update: ', targetView.page, targetView.tab) + store.commit('updateView', { ...targetView, fromMain: true }) + }).on(events.EVENT_APP_ERROR_MAIN, (e, err) => { + // 弹框显示main进程报错内容 + alert(err) + }).on(events.EVENT_SUBSCRIBE_UPDATE_MAIN, (e) => { + // 更新订阅服务器 + store.dispatch('updateSubscribes').then(updatedCount => { + if (updatedCount > 0) { + showNotification(`服务器订阅更新成功,共更新了${updatedCount}个节点`) + } else { + showNotification(`服务器订阅更新完成,没有新节点`) + } + }).catch(() => { + showNotification('服务器订阅更新失败') + }) + }).on(events.EVENT_RX_SYNC_MAIN, (e, appConfig) => { + // 同步数据 + console.log('received sync data: %o', appConfig) + store.commit('updateConfig', [appConfig]) + }).on(events.EVENT_CONFIG_CREATE, () => { + store.dispatch('newConfig') + }).on(events.EVENT_SUBSCRIBE_NEW, () => { + store.dispatch('newSubscription') }) -}).on(events.EVENT_RX_SYNC_MAIN, (e, appConfig) => { - // 同步数据 - console.log('received sync data: %o', appConfig) - store.commit('updateConfig', [appConfig]) -}).on(events.EVENT_CONFIG_CREATE, () => { - store.dispatch('newConfig') -}).on(events.EVENT_SUBSCRIBE_NEW, () => { - store.dispatch('newSubscription') -}) - +} /** * 与main进程同步配置项 * @param {Object} appConfig 用于更新的应用配置 @@ -93,3 +94,6 @@ export function openDialog (options) { }) }) } +export function saveUpdateTime () { + ipcRenderer.send(events.EVENT_SUBSCRIBE_SAVE_TIME) +} diff --git a/src/renderer/locales/en-US.json b/src/renderer/locales/en-US.json index 1b29b5f..9c5f7dd 100644 --- a/src/renderer/locales/en-US.json +++ b/src/renderer/locales/en-US.json @@ -54,7 +54,7 @@ "UI_SUBSCRIPTION_URL":"URL", "UI_ENTER_NEW_SUB_URL":"Please enter new Subscription URL", "UI_GROUP_NAME":"Group name", - "UI_NO_NODE":"There is no node. Please go Setting-Add to add one", + "UI_NO_NODE":"There is no node here. Please go `Add` to add one.", "UI_FEATURE_CROSS_PLATFORM": "Cross Platform", "UI_FEATURE_CP_DETAIL":"Support Windows, MacOs and Linux", diff --git a/src/renderer/main.js b/src/renderer/main.js index bbfa743..d545412 100644 --- a/src/renderer/main.js +++ b/src/renderer/main.js @@ -1,10 +1,10 @@ import Vue from 'vue' import './components' -import { getInitConfig } from './ipc' +import { getInitConfig, register } from './ipc' import store from './store' import App from './App' import i18n from './i18n' -require('./ipc') +register() Vue.config.productionTip = false // 启动应用时获取初始化数据 diff --git a/src/renderer/store/index.js b/src/renderer/store/index.js index 8b422c9..2db6258 100644 --- a/src/renderer/store/index.js +++ b/src/renderer/store/index.js @@ -4,11 +4,9 @@ import defaultConfig from '@/shared/config' import { merge, clone, - request, isSubscribeContentValid, getUpdatedKeys, - isConfigEqual, - somePromise + isConfigEqual } from '@/shared/utils' import { defaultSSRConfig } from '@/shared/ssr' import { syncConfig } from '../ipc' @@ -261,14 +259,19 @@ export default new Vuex.Store({ } }, // 更新所有订阅服务器 - updateSubscribes ({ state, dispatch }, updateSubscribes) { + async updateSubscribes ({ state, dispatch }, updateSubscribes) { // 要更新的订阅服务器 updateSubscribes = updateSubscribes || state.appConfig.serverSubscribes // 累计更新节点数 let updatedCount = 0 - return Promise.all(updateSubscribes.map(subscribe => { - return somePromise([request(subscribe.URL, true), fetch(subscribe.URL).then(res => res.text())]).then(res => { - const [groupCount, groupConfigs] = isSubscribeContentValid(res) + const updatedArr = [] + const failedArr = [] + await Promise.all(updateSubscribes.map(async subscribe => { + try { + const res = await fetch(subscribe.URL) + updatedArr.push(subscribe.URL) + const textContent = await res.text() + const [groupCount, groupConfigs] = isSubscribeContentValid(textContent) if (groupCount > 0) { for (const groupName in groupConfigs) { const configs = groupConfigs[groupName] @@ -302,10 +305,11 @@ export default new Vuex.Store({ } } } - }) - })).then(() => { - return updatedCount - }) + } catch (error) { + failedArr.push(subscribe.URL) + } + })) + return { updatedCount, updatedArr, failedArr } }, removeEditingNode (context) { const clone = context.state.appConfig.configs.slice() diff --git a/src/renderer/views/ManagePanel.vue b/src/renderer/views/ManagePanel.vue index fb8f951..6a42225 100644 --- a/src/renderer/views/ManagePanel.vue +++ b/src/renderer/views/ManagePanel.vue @@ -1,24 +1,18 @@ diff --git a/src/renderer/views/option/Subscribe.vue b/src/renderer/views/option/Subscribe.vue index 525fa0f..df2b601 100644 --- a/src/renderer/views/option/Subscribe.vue +++ b/src/renderer/views/option/Subscribe.vue @@ -7,7 +7,7 @@
+ @keyup.enter.native="saveInput" @keyup.esc.native="cancelInput" @on-blur="cancelInput"/> {{$t('UI_SETTING_UPDATE_AUTO')}}
{{$t('UI_PER')}}  @@ -27,7 +27,8 @@