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:
Rick 2024-05-25 21:44:22 +08:00 committed by GitHub
parent ce5ad55216
commit e7620a4a0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 3361 additions and 126 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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');
});
})

View File

@ -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');

View File

@ -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>

View File

@ -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

View File

@ -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": [

View File

@ -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'),
})

View File

@ -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();
}
}

View File

@ -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