feat: improve the desktop system control features (#454)
* feat: improve the desktop system control features * add preference support --------- Co-authored-by: rick <linuxsuren@users.noreply.github.com>
This commit is contained in:
parent
ce5ad55216
commit
e7620a4a0f
|
@ -153,4 +153,4 @@ jobs:
|
|||
- name: Build Desktop
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
make desktop-package desktop-make
|
||||
make desktop-package desktop-make desktop-test
|
||||
|
|
|
@ -19,3 +19,4 @@ console/atest-desktop/out
|
|||
console/atest-desktop/node_modules
|
||||
console/atest-desktop/atest
|
||||
console/atest-desktop/atest.exe
|
||||
console/atest-desktop/coverage
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
const path = require('node:path')
|
||||
|
||||
exports.control = function(okCallback, errorCallback) {
|
||||
fetch('http://localhost:' + getPort() + '/healthz').
|
||||
fetch(getHealthzUrl()).
|
||||
then(okCallback).catch(errorCallback)
|
||||
}
|
||||
|
||||
|
@ -12,5 +30,21 @@ function getHomePage() {
|
|||
return 'http://localhost:' + getPort()
|
||||
}
|
||||
|
||||
function getHealthzUrl() {
|
||||
return 'http://localhost:' + getPort() + '/healthz'
|
||||
}
|
||||
|
||||
function getHomeDir() {
|
||||
const homedir = require('os').homedir();
|
||||
return path.join(homedir, ".config", 'atest')
|
||||
}
|
||||
|
||||
function getLogfile() {
|
||||
return path.join(getHomeDir(), 'log.log')
|
||||
}
|
||||
|
||||
exports.getPort = getPort
|
||||
exports.getHomePage = getHomePage
|
||||
exports.getHomeDir = getHomeDir
|
||||
exports.getLogfile = getLogfile
|
||||
exports.getHealthzUrl = getHealthzUrl
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
const { getPort, getHealthzUrl, getHomePage } = require('./api');
|
||||
|
||||
describe('getPort function', () => {
|
||||
test('should return the default port number 7788', () => {
|
||||
const port = getPort();
|
||||
expect(port).toBe(7788);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHealthzUrl function', () => {
|
||||
test('should return the default healthz url', () => {
|
||||
const url = getHealthzUrl();
|
||||
expect(url).toBe('http://localhost:7788/healthz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHomePage function', () => {
|
||||
test('should return the default home page url', () => {
|
||||
const url = getHomePage();
|
||||
expect(url).toBe('http://localhost:7788');
|
||||
});
|
||||
})
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
|
||||
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
|
||||
const path = require('node:path');
|
||||
|
|
|
@ -9,35 +9,63 @@
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<button type="button" id="action">Start</button>
|
||||
<div>
|
||||
Log output
|
||||
</div>
|
||||
<button type="button" id="open-server-page">Open Server Page</button>
|
||||
<div style="margin: 5px">
|
||||
<div>
|
||||
<div>Server Status</div>
|
||||
<button type="button" id="action">Start</button>
|
||||
<button type="button" id="open-server-page">Open Server Page</button>
|
||||
<div>
|
||||
<span>Port:</span><input name="port" id="port" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>Log</div>
|
||||
<button type="button" id="open-log-file">Open Log File</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- You can also require other files to run in this process -->
|
||||
<script src="./renderer.js"></script>
|
||||
<script src="./api.js"></script>
|
||||
<script>
|
||||
const actionBut = document.getElementById('action');
|
||||
actionBut.addEventListener('click', (e) => {
|
||||
const action = actionBut.innerHTML;
|
||||
switch (action) {
|
||||
case 'Stop':
|
||||
stop();
|
||||
actionBut.innerHTML = 'Start';
|
||||
window.electronAPI.stopServer()
|
||||
break;
|
||||
case 'Start':
|
||||
start();
|
||||
actionBut.innerHTML= 'Stop';
|
||||
window.electronAPI.startServer()
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
const openServerBut = document.getElementById('open-server-page');
|
||||
openServerBut.addEventListener('click', (e) => {
|
||||
window.location = getHomePage()
|
||||
openServerBut.addEventListener('click', async (e) => {
|
||||
window.location = await window.electronAPI.getHomePage()
|
||||
})
|
||||
|
||||
const openLogfileBut = document.getElementById('open-log-file')
|
||||
openLogfileBut.addEventListener('click', () => {
|
||||
window.electronAPI.openLogDir()
|
||||
})
|
||||
|
||||
const loadServerStatus = async () => {
|
||||
const healthzUrl = await window.electronAPI.getHealthzUrl()
|
||||
|
||||
fetch(healthzUrl).then(res => {
|
||||
actionBut.innerHTML = 'Stop';
|
||||
}).catch(err => {
|
||||
actionBut.innerHTML = 'Start';
|
||||
})
|
||||
}
|
||||
|
||||
loadServerStatus()
|
||||
window.setInterval(loadServerStatus, 2000)
|
||||
|
||||
const portInput = document.getElementById('port');
|
||||
(async function() {
|
||||
portInput.value = await window.electronAPI.getPort()
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,35 +1,63 @@
|
|||
// main.js
|
||||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
// Modules to control application life and create native browser window
|
||||
const { app, BrowserWindow, Menu, MenuItem } = require('electron')
|
||||
const { app, shell, BrowserWindow, Menu, MenuItem, ipcMain, contextBridge } = require('electron')
|
||||
const log = require('electron-log/main');
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
const server = require('./api')
|
||||
const spawn = require("child_process").spawn;
|
||||
const homedir = require('os').homedir();
|
||||
const atestHome = path.join(homedir, ".config", 'atest')
|
||||
const atestHome = server.getHomeDir()
|
||||
const storage = require('electron-json-storage')
|
||||
|
||||
// setup log output
|
||||
log.initialize();
|
||||
log.transports.file.level = 'info';
|
||||
log.transports.file.resolvePathFn = () => path.join(atestHome, 'log.log');
|
||||
|
||||
log.transports.file.level = getLogLevel()
|
||||
log.transports.file.resolvePathFn = () => server.getLogfile()
|
||||
if (process.platform === 'darwin'){
|
||||
app.dock.setIcon(path.join(__dirname, "api-testing.png"))
|
||||
}
|
||||
|
||||
const windowOptions = {
|
||||
width: 1024,
|
||||
height: 600,
|
||||
frame: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
enableRemoteModule: true
|
||||
},
|
||||
icon: path.join(__dirname, '/api-testing.ico'),
|
||||
}
|
||||
|
||||
const createWindow = () => {
|
||||
var width = storage.getSync('window.width')
|
||||
if (!isNaN(width)) {
|
||||
windowOptions.width = width
|
||||
}
|
||||
var height = storage.getSync('window.height')
|
||||
if (!isNaN(height)) {
|
||||
windowOptions.height = height
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1024,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
enableRemoteModule: true
|
||||
},
|
||||
icon: path.join(__dirname, '/api-testing.ico'),
|
||||
})
|
||||
const mainWindow = new BrowserWindow(windowOptions)
|
||||
|
||||
if (!isNaN(serverProcess.pid)) {
|
||||
// server process started by app
|
||||
|
@ -42,6 +70,12 @@ const createWindow = () => {
|
|||
mainWindow.loadFile('index.html')
|
||||
})
|
||||
}
|
||||
|
||||
mainWindow.on('resize', () => {
|
||||
const size = mainWindow.getSize();
|
||||
storage.set('window.width', size[0])
|
||||
storage.set('window.height', size[1])
|
||||
})
|
||||
}
|
||||
|
||||
const menu = new Menu()
|
||||
|
@ -87,6 +121,32 @@ let serverProcess;
|
|||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
ipcMain.on('openLogDir', () => {
|
||||
shell.openExternal('file://' + server.getLogfile())
|
||||
})
|
||||
ipcMain.on('startServer', startServer)
|
||||
ipcMain.on('stopServer', stopServer)
|
||||
ipcMain.on('control', (e, okCallback, errCallback) => {
|
||||
console.log(e + "==" + okCallback + "==" + errCallback)
|
||||
server.control(okCallback, errCallback)
|
||||
})
|
||||
ipcMain.handle('getHomePage', server.getHomePage)
|
||||
ipcMain.handle('getPort', () => {
|
||||
return server.getPort()
|
||||
})
|
||||
ipcMain.handle('getHealthzUrl', server.getHealthzUrl)
|
||||
|
||||
startServer()
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
const startServer = () => {
|
||||
const homeData = path.join(atestHome, 'data')
|
||||
const homeBin = path.join(atestHome, 'bin')
|
||||
|
||||
|
@ -109,15 +169,14 @@ app.whenReady().then(() => {
|
|||
log.info('start to write file with length %d', data.length)
|
||||
|
||||
try {
|
||||
if (process.platform === "win32") {
|
||||
const file = fs.openSync(atestFromHome, 'w');
|
||||
fs.writeSync(file, data, 0, data.length, 0);
|
||||
fs.closeSync(file);
|
||||
}else{
|
||||
fs.writeFileSync(atestFromHome, data);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
if (process.platform === "win32") {
|
||||
const file = fs.openSync(atestFromHome, 'w');
|
||||
fs.writeSync(file, data, 0, data.length, 0);
|
||||
fs.closeSync(file);
|
||||
}else{
|
||||
fs.writeFileSync(atestFromHome, data);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Error Code: %s', e.code);
|
||||
}
|
||||
}
|
||||
|
@ -127,30 +186,28 @@ app.whenReady().then(() => {
|
|||
"server",
|
||||
"--http-port", server.getPort(),
|
||||
"--local-storage", path.join(homeData, "*.yaml")
|
||||
]);
|
||||
])
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
log.info(data.toString())
|
||||
if (data.toString().indexOf('Server is running') != -1) {
|
||||
BrowserWindow.getFocusedWindow().loadURL(server.getHomePage())
|
||||
}
|
||||
});
|
||||
})
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
log.error(data.toString())
|
||||
});
|
||||
})
|
||||
serverProcess.on('close', (code) => {
|
||||
log.log(`child process exited with code ${code}`);
|
||||
});
|
||||
})
|
||||
log.info('start atest server as pid:', serverProcess.pid)
|
||||
log.info(serverProcess.spawnargs)
|
||||
}
|
||||
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
const stopServer = () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill()
|
||||
}
|
||||
}
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
|
@ -159,16 +216,14 @@ app.on('window-all-closed', () => {
|
|||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
}
|
||||
}
|
||||
})
|
||||
app.on('before-quit', () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
stopServer()
|
||||
}
|
||||
})
|
||||
app.on('before-quit', stopServer)
|
||||
|
||||
function getLogLevel() {
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
|||
"description": "API Testing Desktop Application",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "jest --coverage",
|
||||
"start": "electron-forge start",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
|
@ -25,12 +25,15 @@
|
|||
"@electron-forge/publisher-github": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"electron": "^30.0.4",
|
||||
"electron-wix-msi": "^5.1.3"
|
||||
"electron-wix-msi": "^5.1.3",
|
||||
"jest": "^29.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"child_process": "^1.0.2",
|
||||
"electron-json-storage": "^4.6.0",
|
||||
"electron-log": "^5.1.4",
|
||||
"electron-squirrel-startup": "^1.0.1"
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-store": "^9.0.0"
|
||||
},
|
||||
"build": {
|
||||
"extraResources": [
|
||||
|
|
|
@ -1,4 +1,20 @@
|
|||
// preload.js
|
||||
/*
|
||||
Copyright 2024 API Testing Authors.
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
// All the Node.js APIs are available in the preload process.
|
||||
// It has the same sandbox as a Chrome extension.
|
||||
|
@ -11,4 +27,14 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
for (const dependency of ['chrome', 'node', 'electron']) {
|
||||
replaceText(`${dependency}-version`, process.versions[dependency])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openLogDir: () => ipcRenderer.send('openLogDir'),
|
||||
startServer: () => ipcRenderer.send('startServer'),
|
||||
stopServer: () => ipcRenderer.send('stopServer'),
|
||||
control: (okCallback, errCallback) => ipcRenderer.send('control', okCallback, errCallback),
|
||||
getHomePage: () => ipcRenderer.invoke('getHomePage'),
|
||||
getPort: () => ipcRenderer.invoke('getPort'),
|
||||
getHealthzUrl: () => ipcRenderer.invoke('getHealthzUrl'),
|
||||
})
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
let spawn = require("child_process").spawn;
|
||||
let server = require("./api.js")
|
||||
|
||||
server.control(() => {
|
||||
const actionBut = document.getElementById('action');
|
||||
actionBut.innerHTML = 'Stop';
|
||||
})
|
||||
|
||||
let process;
|
||||
function start() {
|
||||
process = spawn("atest", [
|
||||
"server",
|
||||
"--http-port",
|
||||
server.getPort()
|
||||
]);
|
||||
|
||||
process.stdout.on("data", (data) => {
|
||||
console.log(data.toString());
|
||||
});
|
||||
|
||||
process.stderr.on("data", (err) => {
|
||||
console.log(err.toString());
|
||||
});
|
||||
|
||||
process.on("exit", (code) => {
|
||||
console.log(code);
|
||||
});
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (process) {
|
||||
process.kill();
|
||||
}
|
||||
}
|
|
@ -18,3 +18,6 @@ desktop-make: build.embed.ui ## Make an Electron Desktop
|
|||
|
||||
desktop-publish: build.embed.ui ## Publish the Electron Desktop
|
||||
cd console/atest-desktop && npm i && npm run publish
|
||||
|
||||
desktop-test: ## Run unit tests of the Electron Desktop
|
||||
cd console/atest-desktop && npm i && npm test
|
||||
|
|
Loading…
Reference in New Issue