1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Laurent Cozic
7e1b38b936 eslint 2022-01-06 20:06:33 +00:00
Laurent Cozic
a11402db6d fix ci 2022-01-06 18:48:55 +00:00
Laurent Cozic
c19d533d93 fix lock 2022-01-06 18:27:18 +00:00
Laurent Cozic
1a4ff7054a Add support for plugin task list 2022-01-06 17:46:45 +00:00
31 changed files with 5698 additions and 54 deletions

View File

@@ -52,6 +52,8 @@ packages/app-mobile/lib/rnInjectedJs/
packages/app-mobile/locales
packages/app-mobile/node_modules
packages/app-mobile/pluginAssets/
packages/electron-process-manager/dist/
packages/electron-process-manager/src/ui/
packages/fork-*
packages/htmlpack/dist/
packages/lib/assets/

View File

@@ -1,5 +1,10 @@
#!/bin/bash
# We want the script to stop as soon as an error is found. Otherwise if there's
# an error during `yarn install` for example, it's also going to throw millions
# of errors in test units, which makes debugging difficult.
set -e
# =============================================================================
# Setup environment variables
# =============================================================================

View File

@@ -9,6 +9,7 @@ const path = require('path');
const { dirname } = require('@joplin/lib/path-utils');
const fs = require('fs-extra');
const { ipcMain } = require('electron');
const { openProcessManager } = require('@joplin/electron-process-manager');
interface RendererProcessQuitReply {
canClose: boolean;
@@ -41,6 +42,10 @@ export default class ElectronAppWrapper {
this.initialCallbackUrl_ = initialCallbackUrl;
}
public openProcessManager() {
openProcessManager(require('@electron/remote/main'));
}
electronApp() {
return this.electronApp_;
}

View File

@@ -30,6 +30,10 @@ export class Bridge {
return !this.electronApp().electronApp().isPackaged;
}
public openProcessManager() {
this.electronApp().openProcessManager();
}
// The build directory contains additional external files that are going to
// be packaged by Electron Builder. This is for files that need to be
// accessed outside of the Electron app (for example the application icon).

View File

@@ -726,6 +726,13 @@ function useMenu(props: Props) {
});
},
},
{
id: 'help:toggleTaskList',
label: _('Open plugin task list'),
click: () => {
bridge().openProcessManager();
},
},
menuItemDic.toggleSafeMode,
menuItemDic.openProfileDirectory,

View File

@@ -134,9 +134,10 @@
"7zip-bin-win": "^2.1.1"
},
"dependencies": {
"@electron/remote": "^2.0.1",
"@electron/remote": "2.0.1",
"@fortawesome/fontawesome-free": "^5.13.0",
"@joeattardi/emoji-button": "^4.6.0",
"@joplin/electron-process-manager": "workspace:^",
"@joplin/lib": "~2.7",
"@joplin/renderer": "~2.7",
"async-mutex": "^0.1.3",

View File

@@ -0,0 +1,4 @@
{
"presets" : ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -0,0 +1 @@
dist/

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<content url="file://$MODULE_DIR$/../electron-process-reporter" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="JSX" />
</component>
<component name="MarkdownProjectSettings" wasCopied="true">
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.5" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="false" showSelectionInPreview="true" openRemoteLinks="true" replaceUnicodeEmoji="false" lastLayoutSetsDefault="false">
<PanelProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
</PanelProvider>
</PreviewSettings>
<ParserSettings gitHubSyntaxChange="false" emojiShortcuts="0" emojiImages="0">
<PegdownExtensions>
<option name="ABBREVIATIONS" value="false" />
<option name="ANCHORLINKS" value="true" />
<option name="ASIDE" value="false" />
<option name="ATXHEADERSPACE" value="true" />
<option name="AUTOLINKS" value="true" />
<option name="DEFINITIONS" value="false" />
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
<option name="FENCED_CODE_BLOCKS" value="true" />
<option name="FOOTNOTES" value="false" />
<option name="HARDWRAPS" value="false" />
<option name="HTML_DEEP_PARSER" value="false" />
<option name="INSERTED" value="false" />
<option name="QUOTES" value="false" />
<option name="RELAXEDHRULES" value="true" />
<option name="SMARTS" value="false" />
<option name="STRIKETHROUGH" value="true" />
<option name="SUBSCRIPT" value="false" />
<option name="SUPERSCRIPT" value="false" />
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
<option name="SUPPRESS_INLINE_HTML" value="false" />
<option name="TABLES" value="true" />
<option name="TASKLISTITEMS" value="true" />
<option name="TOC" value="false" />
<option name="WIKILINKS" value="true" />
</PegdownExtensions>
<ParserOptions>
<option name="ADMONITION_EXT" value="false" />
<option name="ATTRIBUTES_EXT" value="false" />
<option name="COMMONMARK_LISTS" value="true" />
<option name="DUMMY" value="false" />
<option name="EMOJI_SHORTCUTS" value="true" />
<option name="ENUMERATED_REFERENCES_EXT" value="false" />
<option name="FLEXMARK_FRONT_MATTER" value="false" />
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="false" />
<option name="GFM_TABLE_RENDERING" value="true" />
<option name="GITBOOK_URL_ENCODING" value="false" />
<option name="GITHUB_LISTS" value="false" />
<option name="GITHUB_WIKI_LINKS" value="true" />
<option name="HEADER_ID_NO_DUPED_DASHES" value="false" />
<option name="JEKYLL_FRONT_MATTER" value="false" />
<option name="NO_TEXT_ATTRIBUTES" value="false" />
<option name="PARSE_HTML_ANCHOR_ID" value="false" />
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
</ParserOptions>
</ParserSettings>
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true" embedImages="false" embedHttpImages="false" imageUriSerials="false">
<GeneratorProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
</GeneratorProvider>
<headerTop />
<headerBottom />
<bodyTop />
<bodyBottom />
</HtmlSettings>
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssUriSerial="true" isCssTextEnabled="false" isDynamicPageWidth="true">
<StylesheetProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
</StylesheetProvider>
<ScriptProviders />
<cssText />
<cssUriHistory />
</CssSettings>
<HtmlExportSettings updateOnSave="false" parentDir="" targetDir="" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetExt="" useTargetExt="false" noCssNoScripts="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" linkFormatType="HTTP_ABSOLUTE" />
<LinkMapSettings>
<textMaps />
</LinkMapSettings>
</component>
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/electron-process-manager.iml" filepath="$PROJECT_DIR$/.idea/electron-process-manager.iml" />
</modules>
</component>
</project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b935834e-d3d3-420f-991d-e49e232dd71b" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileColors">
<fileColor scope="Non-Project Files (Material Default)" color="2E3C43" />
<fileColor scope="Non-Project Files (Material Darker)" color="323232" />
<fileColor scope="Non-Project Files (Material Lighter)" color="eae8e8" />
<fileColor scope="Non-Project Files (Material Palenight)" color="2f2e43" />
</component>
<component name="Git.Settings">
<option name="PREVIOUS_COMMIT_AUTHORS">
<list>
<option value="Kris Dages &lt;krisdages@git.whiteboxsoftware.net&gt;" />
</list>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="ProjectId" id="1hYhEkXEjWlOUW5DO64AadCwogW" />
<component name="ProjectViewState">
<option name="autoscrollFromSource" value="true" />
<option name="autoscrollToSource" value="true" />
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/../electron-process-reporter" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
<property name="nodejs_package_manager_path" value="yarn" />
<property name="settings.editor.selected.configurable" value="web-ide.project.structure" />
<property name="ts.external.directory.path" value="$APPLICATION_HOME_DIR$/plugins/JavaScriptLanguage/jsLanguageServicesImpl/external" />
<property name="vue.rearranger.settings.migration" value="true" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="b935834e-d3d3-420f-991d-e49e232dd71b" name="Default Changelist" comment="" />
<created>1600193824206</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1600193824206</updated>
<workItem from="1600193825399" duration="438000" />
<workItem from="1606177958157" duration="419000" />
<workItem from="1616558380418" duration="1481000" />
<workItem from="1627318845586" duration="3411000" />
<workItem from="1627323366882" duration="72000" />
<workItem from="1627489266598" duration="1192000" />
</task>
<task id="LOCAL-00001" summary="(fix) Add `enableRemoteModule: true` to ProcessManagerWindow webPreferences&#10;&#10;Fixes broken UI in Electron 10">
<created>1600193995342</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1600193995342</updated>
</task>
<task id="LOCAL-00002" summary="(breaking) Use `@electron/remote` instead of deprecated `remote` module&#10;&#10;Updated min electron version to 10.&#10;Bump version to 2.0.0">
<created>1627322100905</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1627322100905</updated>
</task>
<task id="LOCAL-00003" summary="Update README.md for fork">
<created>1627490446165</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1627490446165</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="(fix) Add `enableRemoteModule: true` to ProcessManagerWindow webPreferences&#10;&#10;Fixes broken UI in Electron 10" />
<MESSAGE value="(breaking) Use `@electron/remote` instead of deprecated `remote` module&#10;&#10;Updated min electron version to 10.&#10;Bump version to 2.0.0" />
<MESSAGE value="Update README.md for fork" />
<option name="LAST_COMMIT_MESSAGE" value="Update README.md for fork" />
</component>
</project>

View File

@@ -0,0 +1,92 @@
# Process Manager UI for Electron Apps
* * *
2022-01-06: Forked from https://github.com/krisdages/electron-process-manager
* * *
## Fork using @electron/remote instead of builtin remote module
* Minimum electron version is `10`
* [@electron/remote](https://github.com/electron/remote) is a peerDependency. It needs to be initialized in the main process. Follow the instructions in the link.
## Original 1.0 Readme
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, <7.x`, use version `0.7.1` of this package.
For versions `>=7.x`, use latest.~~
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

View File

@@ -0,0 +1,37 @@
{
"name": "@joplin/electron-process-manager",
"version": "2.0.1",
"description": "Process manager UI for Electron applications - Fork with support for @electron/remote",
"main": "src/index.js",
"private": true,
"scripts": {
"build": "webpack"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.0",
"bluebird": "^3.7.1",
"bluebird-extra": "^2.0.0",
"electron": "^10.4.7",
"electron-default-menu": "1.0.1",
"filesize": "^5.0.3",
"format-number": "^3.0.0",
"object-path": "^0.11.4",
"prop-types": "^15.5.10",
"react": "^16.11.0",
"react-dom": "^16.11.0",
"spectron": "^12.0.0",
"webpack": "^2.5.1"
},
"peerDependencies": {
"electron": ">= 10"
},
"dependencies": {
"electron-process-reporter": "^1.4.0"
}
}

View File

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

View File

@@ -0,0 +1,64 @@
const { EventEmitter } = require('events');
const process = require('process');
const { webContents } = require('electron');
const ProcessManagerWindow = require('./ProcessManagerWindow.js');
const defaultOptions = { defaultSorting: { path: null, how: null } };
class ProcessManager extends EventEmitter {
constructor() {
super();
// legacy
this.openProcessManager = this.open.bind(this);
}
// in case this isn't already done in the app.
//
// No longer needed because caller should setup electron/remote
//
// initializeElectronRemote() {
// return require('@electron/remote/main').initialize();
// }
// We pass the electron/remote/main instance to the manager to ensure it's
// using the same as the main application.
//
// When using a peer dependency it seems the package ends up using its own
// instance, which doesn't work.
open(electronRemote, options = defaultOptions) {
if (this.window) {
this.window.focus();
}
this.window = new ProcessManagerWindow(electronRemote);
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;

View File

@@ -0,0 +1,78 @@
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const { onExtendedProcessMetrics } = require('electron-process-reporter');
class ProcessManagerWindow extends BrowserWindow {
constructor(electronRemote, options) {
const winOptions = Object.assign({
width: 800,
height: 300,
useContentSize: true,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
nodeIntegrationInWorker: true,
webviewTag: true,
enableRemoteModule: true,
contextIsolation: false,
},
}, options || {});
super(winOptions);
this.options = options;
this.attachProcessReporter();
const indexHtml = `file://${path.join(__dirname, '..', 'process-manager.html')}`;
this.loadURL(indexHtml);
console.info('IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', electronRemote);
electronRemote.enable(this.webContents);
setTimeout(() => {
this.openDevTools();
}, 5000);
}
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;

View File

@@ -0,0 +1,4 @@
const ProcessManager = require('./ProcessManager.js');
// singleton
module.exports = new ProcessManager();

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { ipcRenderer } from 'electron';
import objectPath from 'object-path';
import ProcessTable from './ProcessTable';
import ToolBar from './ToolBar';
export default class ProcessManager extends React.Component {
constructor(props) {
super(props);
this.state = {
processData: null,
selectedPid: null,
sorting: {
path: null,
how: null
}
};
}
UNSAFE_componentWillMount() {
// TODO: disabled for now - the remote package would need to be passed to this script somehow.
//
// 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>
)
}
}

View File

@@ -0,0 +1,75 @@
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,
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.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.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>
)
}
}
}

View File

@@ -0,0 +1,94 @@
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.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>
)
}
}

View File

@@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ProcessTableHeader extends React.Component {
constructor(props) {
super(props);
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>
)
}
}

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>
)
}
}

View File

@@ -0,0 +1,6 @@
import React from 'react';
import { render } from 'react-dom';
import ProcessManager from './ProcessManager';
render(<ProcessManager/>, document.getElementById('app'));

View File

@@ -0,0 +1,30 @@
const Application = require('spectron').Application;
const { join } = require('path');
const assert = require('assert');
const app = new Application({
env: { TEST_PROCESS_MANAGER: 1 },
path: require(join(__dirname, '../node_modules/electron')),
args: [join(__dirname, '../example/main.js')],
});
(async () => {
try {
await app.start();
await app.client.waitUntilWindowLoaded();
await app.electron.ipcRenderer.send('open-process-manager');
// This looks to be incorrect signature for assert.
// assert(app.client.getWindowCount(), 2);
// There are 2 webviews on the index page. They are included in windowCount, so it's 4, not 2.
assert.equal(await app.client.getWindowCount(), 4);
await app.client.switchWindow(/process-manager\.html/);
await (await app.client.$('#app .process-table')).waitForDisplayed({ timeout: 60000 });
await app.stop();
} catch (error) {
console.error('Test failed', error);
if (app && app.isRunning()) {
await app.stop();
process.exit(1);
} else { process.exit(1); }
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
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'),
// mode: 'development',
devtool: 'eval-source-map',
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;

2432
yarn.lock

File diff suppressed because it is too large Load Diff