project files added

This commit is contained in:
mhalfmann
2021-06-15 16:00:08 +02:00
parent e156e2f053
commit db46afa351
13928 changed files with 1569902 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets" : ["stage-0", "react"]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

+79
View File
@@ -0,0 +1,79 @@
# Process Manager UI for Electron Apps
This package provides a process manager UI for Electron applications.
It opens a window displaying a table of every processes run by the Electron application with information (type, URL for `webContents`, memory..).
[![npm version](https://badge.fury.io/js/electron-process-manager.svg)](https://badge.fury.io/js/electron-process-manager)
![screenshot](https://github.com/getstation/electron-process-manager/raw/master/.github/screenshots/window.png)
:warning: For `@electron>=3.0.0`
It can be useful to debug performance of an app with several `webview`.
It's inspired from Chrome's task manager.
## Features
- [ ] Memory reporting
- [ ] Link memory data to web-contents (for electron >=1.7.1)
- [x] Kill a process from the UI
- [x] Open developer tools for a given process
- [x] CPU metrics
- [x] Sort by columns
⚠️ Unfortunately, memory info are no longer available in Electron>=4 (see [electron/electron#16179](https://github.com/electron/electron/issues/16179))
## Installation
```bash
$ npm install electron-process-manager
```
## Usage
```js
const { openProcessManager } = require('electron-process-manager');
openProcessManager();
```
## Options
`openProcessManager` function can take options in paramters
#### options.defaultSorting
**defaultSorting.how**: `'ascending' | 'descending'`
**defaultSorting.path**:
| Field name | path |
|--------------------|----------------------------|
| Pid | 'pid' |
| WebContents Domain | 'webContents.0.URLDomain' |
| Process Type | 'webContents.0.type' |
| Private Memory | 'memory.privateBytes' |
| Shared Memory | 'memory.sharedBytes' |
| Working Set Size | 'memory.workingSetSize' |
| % CPU | 'cpu.percentCPUUsage' |
| Idle Wake Ups /s | 'cpu.idleWakeupsPerSecond' |
| WebContents Id | 'webContents.0.id' |
| WebContents Type | 'webContents.0.type' |
| WebContents URL | 'webContents.0.URL' |
example:
```js
const { openProcessManager } = require('electron-process-manager');
openProcessManager({ how: 'descending', path: 'cpu.percentCPUUsage' });
```
## Future
- Add physical memory (noted as "Memory" in Chrome's task manager)
- Add networks metrics
Pull requests welcome :)
## License
MIT License
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
{
"_args": [
[
"electron-process-manager@0.7.1",
"C:\\Daten\\Git\\Tumortisch"
]
],
"_from": "electron-process-manager@0.7.1",
"_id": "electron-process-manager@0.7.1",
"_inBundle": false,
"_integrity": "sha512-GE7Sok7IfObhVPqx7n+wVBpdFxVvR/ySa56sLsTT4rnYKBFDWtdrVMiXgKjhHjxsxO59Y+piXT6QonamrQFWeA==",
"_location": "/electron-process-manager",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "electron-process-manager@0.7.1",
"name": "electron-process-manager",
"escapedName": "electron-process-manager",
"rawSpec": "0.7.1",
"saveSpec": null,
"fetchSpec": "0.7.1"
},
"_requiredBy": [
"/"
],
"_resolved": "https://registry.npmjs.org/electron-process-manager/-/electron-process-manager-0.7.1.tgz",
"_spec": "0.7.1",
"_where": "C:\\Daten\\Git\\Tumortisch",
"author": "",
"bugs": {
"url": "https://github.com/getstation/electron-process-manager/issues"
},
"dependencies": {
"electron-process-reporter": "^1.4.0"
},
"description": "Process manager UI for Electron applications",
"devDependencies": {
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"bluebird": "^3.5.4",
"bluebird-extra": "^2.0.0",
"electron": "^5.0.0",
"electron-default-menu": "1.0.1",
"filesize": "^3.5.11",
"format-number": "^3.0.0",
"object-path": "^0.11.4",
"prop-types": "^15.5.10",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"spectron": "^5.0.0",
"webpack": "^2.5.1"
},
"homepage": "https://github.com/getstation/electron-process-manager#readme",
"license": "MIT",
"main": "src/index.js",
"name": "electron-process-manager",
"peerDependencies": {
"electron": ">=1.7.1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/getstation/electron-process-manager.git"
},
"resolutions": {
"spectron/**/electron-chromedriver": "^4.0.0-beta.1"
},
"scripts": {
"build": "webpack",
"dev": "webpack -d --watch",
"example": "npm run build && electron example",
"prepare": "npm run build",
"test": "npm run build && node tests/test.js"
},
"version": "0.7.1"
}
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Process Manager</title>
<link rel="stylesheet" href="vendor/photon.css">
<style>
.process-table {
/*margin: 10px;*/
}
.process-table-container {
overflow-x: scroll;
flex: 2;
}
</style>
</head>
<body>
<div id="app" class="window"></div>
<script src="dist/ui-bundle.js"></script>
</body>
</html>
@@ -0,0 +1,51 @@
const { EventEmitter } = require('events');
const process = require('process');
const { webContents } = require('electron');
var ProcessManagerWindow = require('./ProcessManagerWindow.js');
const defaultOptions = { defaultSorting: { path: null, how: null } };
class ProcessManager extends EventEmitter {
constructor() {
super();
// legacy
this.openProcessManager = this.open.bind(this);
}
open (options = defaultOptions) {
if (this.window) {
this.window.focus();
}
this.window = new ProcessManagerWindow();
this.window.defaultSorting = options.defaultSorting || {};
this.window.showWhenReady();
this.window.on('kill-process', pid => this.killProcess(pid))
this.window.on('open-dev-tools', webContentsId => this.openDevTools(webContentsId))
this.window.on('closed', () => this.window = null)
this.emit('open-window', this.window);
return this.window;
}
killProcess(pid) {
this.emit('will-kill-process', pid, this.window);
process.kill(pid);
this.emit('killed-process', pid, this.window);
}
openDevTools(webContentsId) {
this.emit('will-open-dev-tools', webContentsId, this.window);
const wc = webContents.fromId(webContentsId);
wc.openDevTools({ mode: 'detach' });
this.emit('did-open-dev-tools', webContentsId, this.window);
}
}
module.exports = ProcessManager;
@@ -0,0 +1,69 @@
const { app, BrowserWindow, shell, ipcMain, webContents } = require('electron');
const path = require('path');
const process = require('process');
const { onExtendedProcessMetrics } = require('electron-process-reporter');
class ProcessManagerWindow extends BrowserWindow {
constructor(options) {
const winOptions = Object.assign({
width: 800,
height: 300,
useContentSize: true,
webPreferences: {
nodeIntegration: true,
webviewTag: true,
}
}, options || {});
super(winOptions);
this.options = options;
this.attachProcessReporter();
const indexHtml = 'file://' + path.join(__dirname, '..', 'process-manager.html');
this.loadURL(indexHtml);
}
showWhenReady() {
this.once('ready-to-show', () => {
this.show();
});
}
sendStatsReport(reportData) {
if (!this.webContents) return;
this.webContents.send('process-manager:data', reportData);
}
openDevTools() {
this.webContents.openDevTools();
}
attachProcessReporter() {
this.subscription = onExtendedProcessMetrics(app)
.subscribe(report => this.sendStatsReport(report))
ipcMain.on('process-manager:kill-process', (e, pid) => {
// ignore if not for us
if (!this || this.isDestroyed()) return;
if (e.sender !== this.webContents) return;
this.emit('kill-process', pid);
});
ipcMain.on('process-manager:open-dev-tools', (e, webContentsId) => {
// ignore if not for us
if (!this || this.isDestroyed()) return;
if (e.sender !== this.webContents) return;
this.emit('open-dev-tools', webContentsId);
});
this.on('closed', () => {
if (this.subscription) this.subscription.unsubscribe()
});
}
}
module.exports = ProcessManagerWindow;
+4
View File
@@ -0,0 +1,4 @@
var ProcessManager = require('./ProcessManager.js');
// singleton
module.exports = new ProcessManager();
@@ -0,0 +1,105 @@
import React from 'react';
import { ipcRenderer, remote } from 'electron';
import objectPath from 'object-path';
import ProcessTable from './ProcessTable';
import ToolBar from './ToolBar';
export default class ProcessManager extends React.Component {
constructor() {
super();
this.state = {
processData: null,
selectedPid: null,
sorting: {
path: null,
how: null
}
};
}
componentWillMount() {
this.setState({ sorting: remote.getCurrentWindow().defaultSorting });
ipcRenderer.on('process-manager:data', (_, data) => {
this.setState({ processData: data });
})
}
canKill() {
if (!this.state.selectedPid) return false;
const pids = this.state.processData.map(p => p.pid);
// verify that select pid is in list of processes
return pids.indexOf(this.state.selectedPid) !== -1;
}
canOpenDevTool() {
return this.canKill() && this.getWebContentsIdForSelectedProcess() !== null;
}
getWebContentsIdForSelectedProcess() {
const { processData, selectedPid } = this.state;
if (!selectedPid) return null;
const process = processData.find(p => p.pid === selectedPid);
if (!process || !process.webContents || process.webContents.length === 0) return null;
return process.webContents[0].id;
}
handleKillProcess() {
const pid = this.state.selectedPid;
if (!pid) return;
ipcRenderer.send('process-manager:kill-process', pid);
}
handleOpenDevTool() {
const webContentsId = this.getWebContentsIdForSelectedProcess();
ipcRenderer.send('process-manager:open-dev-tools', webContentsId);
}
getProcessData() {
const { processData, sorting } = this.state;
if (!sorting.path || !sorting.how) return processData;
return processData.sort((p1, p2) => {
const p1Metric = objectPath.get(p1, sorting.path);
const p2Metric = objectPath.get(p2, sorting.path);
if (p1Metric === p2Metric) return 0;
const comp = p1Metric < p2Metric ? -1 : 1;
return sorting.how == 'ascending' ? comp : -comp;
});
}
render () {
const { processData } = this.state;
if (!processData) return (<span>No data</span>);
return (
<div className="window">
<header className="toolbar toolbar-header">
<ToolBar
disableKill={!this.canKill()}
onKillClick={this.handleKillProcess.bind(this)}
disabelOpenDevTool={!this.canOpenDevTool()}
onOpenDevToolClick={this.handleOpenDevTool.bind(this)}
/>
</header>
<div className="process-table-container">
<ProcessTable
processData={this.getProcessData()}
selectedPid={this.state.selectedPid}
sorting={this.state.sorting}
onSortingChange={sorting => this.setState({ sorting })}
onSelectedPidChange={pid => this.setState({ selectedPid: pid })}
/>
</div>
</div>
)
}
}
@@ -0,0 +1,81 @@
import React from 'react';
import PropTypes from 'prop-types';
import filesize from 'filesize';
import format from 'format-number';
const KB = 1024;
const formatPercentage = format({
round: 1,
padRight: 1
});
export default class ProcessRow extends React.Component {
static propTypes = {
pid: PropTypes.number,
type: PropTypes.string,
memory: PropTypes.shape({
peakWorkingSetSize: PropTypes.number,
privateBytes: PropTypes.number,
sharedBytes: PropTypes.number,
workingSetSize: PropTypes.number
}),
cpu: PropTypes.shape({
percentCPUUsage: PropTypes.number,
idleWakeupsPerSecond: PropTypes.number
}),
webContents: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number,
type: PropTypes.string,
URL: PropTypes.string,
URLDomain: PropTypes.string
})),
selected: PropTypes.bool,
onSelect: PropTypes.func
}
render() {
const { webContents, memory } = this.props;
if (!webContents || webContents.length === 0) {
return (
<tr
className={this.props.selected ? 'selected': ''}
onClick={this.props.onSelect}
>
<td>{this.props.pid}</td>
<td></td>
<td>{this.props.type}</td>
<td>{memory ? filesize(memory.privateBytes*KB) : 'N/A'}</td>
<td>{memory ? filesize(memory.sharedBytes*KB) : 'N/A'}</td>
<td>{memory ? filesize(memory.workingSetSize*KB) : 'N/A'}</td>
<td>{formatPercentage(this.props.cpu.percentCPUUsage)}</td>
<td>{this.props.cpu.idleWakeupsPerSecond}</td>
<td></td>
<td></td>
<td></td>
</tr>
)
} else {
// FIX ME: we consider we have only have 1 webContents per process
const wc = webContents[0];
return (
<tr
className={this.props.selected ? 'selected': ''}
onClick={this.props.onSelect}
>
<td>{this.props.pid}</td>
<td>{wc.URLDomain}</td>
<td>{this.props.type}</td>
<td>{memory ? filesize(memory.privateBytes*KB) : 'N/A'}</td>
<td>{memory ? filesize(memory.sharedBytes*KB) : 'N/A'}</td>
<td>{memory ? filesize(memory.workingSetSize*KB) : 'N/A'}</td>
<td>{formatPercentage(this.props.cpu.percentCPUUsage)}</td>
<td>{this.props.cpu.idleWakeupsPerSecond}</td>
<td>{wc.id}</td>
<td>{wc.type}</td>
<td>{wc.URL}</td>
</tr>
)
}
}
}
@@ -0,0 +1,106 @@
import React from 'react';
import PropTypes from 'prop-types';
import ProcessRow from './ProcessRow';
import ProcessTableHeader from './ProcessTableHeader';
export default class ProcessTable extends React.Component {
static propTypes = {
processData: PropTypes.arrayOf(PropTypes.object),
selectedPid: PropTypes.number,
sorting: PropTypes.PropTypes.shape({
path: PropTypes.string,
how: PropTypes.string
}),
onSortingChange: PropTypes.func,
onSelectedPidChange: PropTypes.func
}
render() {
return (
<table className="process-table table-striped">
<thead>
<tr>
<ProcessTableHeader
path='pid'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>Pid</ProcessTableHeader>
<ProcessTableHeader
path='webContents.0.URLDomain'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>WebContents Domain</ProcessTableHeader>
<ProcessTableHeader
path='webContents.0.type'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>Process Type</ProcessTableHeader>
<ProcessTableHeader
path='memory.privateBytes'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>Private Memory</ProcessTableHeader>
<ProcessTableHeader
path='memory.sharedBytes'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>Shared Memory</ProcessTableHeader>
<ProcessTableHeader
path='memory.workingSetSize'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>Working Set Size</ProcessTableHeader>
<ProcessTableHeader
path='cpu.percentCPUUsage'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>% CPU</ProcessTableHeader>
<ProcessTableHeader
path='cpu.idleWakeupsPerSecond'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>Idle Wake Ups /s</ProcessTableHeader>
<ProcessTableHeader
path='webContents.0.id'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>WebContents Id</ProcessTableHeader>
<ProcessTableHeader
path='webContents.0.type'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>WebContents Type</ProcessTableHeader>
<ProcessTableHeader
path='webContents.0.URL'
sorting={this.props.sorting}
onSortingChange={this.props.onSortingChange}
>WebContents URL</ProcessTableHeader>
</tr>
</thead>
<tbody>
{
this.props.processData.map(p =>
<ProcessRow
key={p.pid}
{...p}
onSelect={() => this.props.onSelectedPidChange(p.pid)}
selected={this.props.selectedPid === p.pid}
/>
)
}
</tbody>
</table>
)
}
}
@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ProcessTableHeader extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
static propTypes = {
children: PropTypes.node,
path: PropTypes.string.isRequired,
sorting: PropTypes.PropTypes.shape({
path: PropTypes.string,
how: PropTypes.string
}),
onSortingChange: PropTypes.func
}
getSortCharacter() {
if (!this.sortHow) return (
<span>&nbsp;</span>
);
return this.sortHow == 'ascending' ? '👆' : '👇'
}
get sortHow() {
if (!this.props.sorting) return null;
if (this.props.sorting.path == this.props.path){
return this.props.sorting.how;
}
return null;
}
handleClick() {
let nextSortHow = null;
if(this.sortHow === null) {
nextSortHow = 'ascending';
} else if (this.sortHow === 'ascending') {
nextSortHow = 'descending';
} else {
nextSortHow = null;
}
this.props.onSortingChange({
path: this.props.path,
how: nextSortHow
});
}
render() {
return (
<th onClick={this.handleClick}>
{this.props.children}
&nbsp;
{this.getSortCharacter()}
</th>
)
}
}
+35
View File
@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ToolBar extends React.Component {
static propTypes = {
onKillClick: PropTypes.func,
disableKill: PropTypes.bool,
onOpenDevToolClick: PropTypes.func,
disabelOpenDevTool: PropTypes.bool
}
render() {
return (
<div className="toolbar-actions">
<div className="btn-group">
<button
className="btn btn-default"
disabled={this.props.disableKill}
onClick={this.props.onKillClick}
>
End process
</button>
<button
className="btn btn-default"
disabled={this.props.disabelOpenDevTool}
onClick={this.props.onOpenDevToolClick}
>
Open Dev Tool
</button>
</div>
</div>
)
}
}
+6
View File
@@ -0,0 +1,6 @@
import React from 'react';
import { render } from 'react-dom';
import ProcessManager from './ProcessManager';
render(<ProcessManager/>, document.getElementById('app'));
+29
View File
@@ -0,0 +1,29 @@
const Application = require('spectron').Application;
const { join } = require('path');
const assert = require('assert')
const { setUpApp } = require('./utils');
const app = new Application({
env: { TEST_PROCESS_MANAGER: 1 },
path: require(join(__dirname, '../node_modules/electron')),
args: [join(__dirname, '../example/main.js')]
});
app.start()
.then(() => {
setUpApp(app);
return app.client.waitUntilWindowLoaded();
})
.then(() => app.electron.ipcRenderer.send('open-process-manager'))
.then(() => app.client.waitUntilWindowLoaded())
.then(() => assert(app.client.getWindowCount(), 2))
.then(() => app.client.changeFocusToMatchingURL(/process-manager\.html/))
.then(() => app.client.waitForVisible('#app .process-table'))
.then(() => app.stop())
.catch(function (error) {
console.error('Test failed', error);
if (app && app.isRunning()) {
app.stop().then(() => process.exit(1))
} else process.exit(1);
})
+15
View File
@@ -0,0 +1,15 @@
const Promise = require('bluebird');
require('bluebird-extra').usePromise(Promise);
const setUpApp = function(app) {
app.client.addCommand('changeFocusToMatchingURL', URLRegexp => {
return app.client.windowHandles()
.then(handles => Promise.eachAny(handles.value, handle => {
return app.client.window(handle)
.then(() => app.client.getUrl())
.then(url => !!url.match(URLRegexp) || undefined)
}))
})
}
module.exports.setUpApp = setUpApp;
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
const webpack = require('webpack');
const path = require('path');
const BUILD_DIR = path.resolve(__dirname, 'dist');
const config = {
entry: path.resolve(__dirname, 'src/ui/index.js'),
output: {
path: BUILD_DIR,
filename: 'ui-bundle.js'
},
module : {
loaders : [
{
test: /\.(jsx|js)?$/,
loader : 'babel-loader',
include: path.resolve(__dirname, 'src/ui')
}
]
},
target: 'electron-renderer'
};
module.exports = config;
File diff suppressed because it is too large Load Diff