Skip to content

Commit ac190a7

Browse files
committed
2.0.8 Relase - Fix issues with console outputs mixing up
1 parent 7b8919d commit ac190a7

14 files changed

Lines changed: 551 additions & 190 deletions

File tree

ProcessManagement.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Java Runner Client: Process Management Technical Details
2+
3+
## Overview
4+
5+
Java Runner Client (JRC) spawns and manages single Java processes via Node.js `child_process.spawn()`. Each profile corresponds to one independent Java process. The term "managed process" refers to JRC's internal wrapper tracking that process, not a separate secondary process.
6+
7+
## Process Launch Architecture
8+
9+
### Single Java Process Spawning
10+
11+
When you start a Java process, JRC:
12+
13+
1. **Builds Arguments** (`buildArgs()` method):
14+
- Collects all enabled JVM arguments from the profile (e.g., `-Xmx2048m`, `-Xms512m`)
15+
- Adds system properties via `-D` flags (e.g., `-Dapp.name=value`)
16+
- Appends `-jar` with the JAR path
17+
- Adds program arguments passed to the Java application
18+
- Example output: `java -Xmx2048m -Dkey=value -jar /path/to/app.jar arg1 arg2`
19+
20+
2. **Spawns Direct Process**:
21+
```typescript
22+
proc = spawn(cmd, args, {
23+
cwd, env: process.env,
24+
shell: false, // Direct execution, no shell overhead
25+
detached: false,
26+
stdio: ['pipe', 'pipe', 'pipe'] // Captures stdin, stdout, stderr
27+
})
28+
```
29+
- Uses `shell: false` for efficiency (direct JVM launch, no shell wrapper)
30+
- Inherits environment variables from the Electron app
31+
- Pipes all I/O streams so ProcessManager can capture real-time output
32+
33+
### The "Managed Process" Wrapper
34+
35+
The `ManagedProcess` interface is **not a second process** — it's a lightweight wrapper object tracking the spawned process:
36+
37+
```typescript
38+
interface ManagedProcess {
39+
process: ChildProcess // Node.js child process handle
40+
profileId: string // Profile identifier
41+
profileName: string // Human-readable name
42+
jarPath: string // JAR file path
43+
startedAt: number // Start timestamp
44+
intentionallyStopped: boolean // User explicitly stopped it?
45+
}
46+
```
47+
48+
Managed processes are stored in a `Map<string, ManagedProcess>` (keyed by profile ID) to:
49+
- Track which profiles have active processes
50+
- Store execution metadata
51+
- Enable lifecycle management (stop, restart, monitor)
52+
53+
## Real-Time I/O Capturing
54+
55+
ProcessManager captures stdout and stderr in real-time:
56+
57+
```typescript
58+
proc.stdout?.on('data', (chunk: string) => this.pushOutput(profile.id, chunk, 'stdout', managed))
59+
proc.stderr?.on('data', (chunk: string) => this.pushOutput(profile.id, chunk, 'stderr', managed))
60+
```
61+
62+
Key features:
63+
- Each output line receives a **persistent, incrementing counter ID** (survives process restarts)
64+
- IDs are sequential numbers (1, 2, 3...) rather than composite strings
65+
- Duplicate detection throws an error if ID collision occurs
66+
- Line IDs are tracked in a Set per-profile for O(1) lookup
67+
- UI receives output via Electron IPC (`CONSOLE_LINE` message)
68+
69+
### Deduplication & Tracking
70+
71+
The ProcessManager maintains persistent state per-profile:
72+
- **`lineCounters`** Map: Incrementing counter that doesn't reset on restart
73+
- **`seenLineIds`** Map: Set of all seen IDs to detect collisions
74+
- Auto-cleanup triggers at 10k entries to prevent unbounded memory growth
75+
- Each line gets unique sequential ID, ensuring no duplicates across 5000+ entries
76+
77+
## Process Lifecycle Management
78+
79+
### Real-Time Monitoring
80+
81+
```typescript
82+
proc.on('exit', (code, signal) => { /* handling */ })
83+
proc.on('error', (err) => { /* handling */ })
84+
```
85+
86+
- Detects natural termination or crashes
87+
- Records exit code and signal information
88+
- Stores metadata in activity log (max 500 entries, UUID-based)
89+
- Broadcasts state update to UI
90+
91+
### Graceful Shutdown
92+
93+
**Windows** (primary method):
94+
```
95+
taskkill /PID {pid} /T /F
96+
```
97+
- `/T`: Kill process tree (includes child processes)
98+
- `/F`: Force termination if graceful fails
99+
100+
**Unix** (tiered approach):
101+
1. Send `SIGTERM` (graceful shutdown)
102+
2. Wait 5 seconds
103+
3. Send `SIGKILL` if still running (force termination)
104+
105+
### Auto-Restart Logic
106+
107+
If process exits with non-zero code AND `autoRestart` is enabled:
108+
1. Waits `autoRestartInterval` seconds (default: 10s)
109+
2. Uses stored profile snapshot (captures config state at start)
110+
3. Automatically restarts with identical settings
111+
4. Profile snapshots update whenever user saves configuration
112+
113+
## Process Discovery & Scanning
114+
115+
JRC can enumerate all Java processes system-wide (not just managed ones):
116+
117+
**Windows**: PowerShell WMI query
118+
- Retrieves command line for all processes
119+
- Extracts memory usage, thread count, start time
120+
- Identifies Java processes by `java.exe` presence
121+
- Fallback to `tasklist /fo csv` if WMI unavailable
122+
123+
**Unix**: `ps -eo pid,comm,args`
124+
- Parses system process list
125+
- Identifies JAR files via `-jar` flag regex
126+
- Extracts memory and thread info from procfs
127+
128+
Each discovered process is marked as `managed: boolean` (whether JRC started it).
129+
130+
## Technical Efficiency
131+
132+
### Direct Execution
133+
- `shell: false` avoids shell wrapper overhead
134+
- Significant performance improvement for frequent restarts
135+
- Direct JVM launch with minimal context overhead
136+
137+
### Persistent State
138+
- Line counters survive process restarts (not tied to ManagedProcess lifecycle)
139+
- Activity log maintains historical execution data
140+
- Profile snapshots capture exact configuration at start time
141+
142+
### Memory Management
143+
- Activity log auto-pops entries beyond 500
144+
- Line ID tracking Set auto-clears at 10k entries
145+
- Map-based storage with O(1) lookups for process state
146+
147+
### Environment Isolation
148+
- Each process can inherit custom working directories
149+
- System properties configurable via `-D` flags
150+
- Separate stdout/stderr streams prevent log mixing
151+
152+
## IPC Communication Flow
153+
154+
```
155+
┌─────────────────────────┐
156+
│ Java Process │ (stdout/stderr output)
157+
└────────────┬────────────┘
158+
│ [piped to Node.js]
159+
160+
┌─────────────────────────┐
161+
│ ProcessManager │ (captures, formats with line ID)
162+
└────────────┬────────────┘
163+
164+
↓ Electron IPC: CONSOLE_LINE
165+
┌─────────────────────────┐
166+
│ Renderer (React) │ (console component)
167+
└────────────┬────────────┘
168+
169+
170+
┌─────────────────────────┐
171+
│ User UI │ (displays real-time output)
172+
└─────────────────────────┘
173+
```
174+
175+
## Single vs. Multiple Processes
176+
177+
JRC's single-process-per-profile design means:
178+
- Each profile spawns exactly one Java process
179+
- Multiple profiles = multiple independent processes
180+
- No inter-process communication overhead
181+
- Each process gets isolated I/O, environment, and lifecycle management
182+
- Simplifies troubleshooting (one PID per configuration)
183+
184+
## Data Structures
185+
186+
| Map | Purpose | Key | Value | Lifecycle |
187+
|-----|---------|-----|-------|-----------|
188+
| `processes` | Active managed processes | Profile ID | ManagedProcess | Per-process |
189+
| `lineCounters` | Persistent line numbering | Profile ID | Counter number | Per-profile (survives restarts) |
190+
| `seenLineIds` | Duplicate detection | Profile ID | Set of IDs | Per-profile (auto-cleans at 10k) |
191+
| `restartTimers` | Pending auto-restart timers | Profile ID | Timeout handle | Until restart or stop |
192+
| `profileSnapshots` | Saved configurations | Profile ID | Profile object | While process running |
193+
| `activityLog` | Execution history || ProcessLogEntry[] | Global (max 500) |

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "java-runner-client",
3-
"version": "2.0.7",
3+
"version": "2.0.8",
44
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
55
"main": "dist/main/main.js",
66
"scripts": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const FAQ_TOPICS: FaqTopic[] = [
88
{ q: 'What is Java Runner Client?', a: 'Java Runner Client (JRC) lets you run and manage JAR files as persistent background processes. You create a profile for each JAR, configure its arguments, and start/stop it from the Console tab.' },
99
{ q: 'How do I get started quickly?', a: '1. Click "New Profile" in the sidebar.\n2. Go to Configure → Files & Paths and select your .jar.\n3. Go to Console and click Run.' },
1010
{ q: 'Where is the config file stored?', a: 'Windows: %APPDATA%\\java-runner-client\\java-runner-config.json\nLinux: ~/.config/java-runner-client/\nmacOS: ~/Library/Application Support/java-runner-client/' },
11+
{ q: 'What is a "managed" (by JRC) process?', a: 'A "managed" process is a Java process that JRC started and is tracking. Despite the name, it\'s a single Java process per profile, not two processes—JRC spawns one process directly via Node.js and wraps it in a ManagedProcess object that tracks metadata (PID, start time, exit code). JRC captures all stdout/stderr output in real-time, assigns persistent line IDs that survive restarts, and monitors the process lifecycle. If it crashes with a non-zero exit code and auto-restart is enabled, JRC automatically restarts it after a configurable delay. All managed processes are tracked in the Processes map, and you can manually kill any process from the Utilities → Process Scanner and view activity history in the Activity Log.' },
1112
],
1213
},
1314
{

src/main/main.ts

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -65,47 +65,66 @@ function updateTrayMenu(): void {
6565
tray.setContextMenu(Menu.buildFromTemplate([
6666
{ label: 'Open Java Runner Client', click: () => { mainWindow?.show(); mainWindow?.focus() } },
6767
{ type: 'separator' },
68-
...(items.length > 0 ? [...items, { type: 'separator' as const }] : [{ label: 'No processes running', enabled: false }, { type: 'separator' as const }]),
68+
...(items.length > 0
69+
? [...items, { type: 'separator' as const }]
70+
: [{ label: 'No processes running', enabled: false }, { type: 'separator' as const }]),
6971
{ label: 'Quit', click: () => { forceQuit = true; app.quit() } },
7072
]))
7173
}
7274

73-
function registerIpc(): void {
74-
ipcMain.handle(IPC.PROFILES_GET_ALL, () => getAllProfiles())
75-
ipcMain.handle(IPC.PROFILES_SAVE, (_, p) => saveProfile(p))
76-
ipcMain.handle(IPC.PROFILES_DELETE, (_, id) => deleteProfile(id))
77-
ipcMain.handle(IPC.PROCESS_START, (_, p) => processManager.start(p))
78-
ipcMain.handle(IPC.PROCESS_STOP, (_, id) => processManager.stop(id))
79-
ipcMain.handle(IPC.PROCESS_SEND_INPUT, (_, id, input) => processManager.sendInput(id, input))
80-
ipcMain.handle(IPC.PROCESS_GET_STATES, () => processManager.getStates())
81-
ipcMain.handle(IPC.PROCESS_GET_LOG, () => processManager.getActivityLog())
82-
ipcMain.handle(IPC.PROCESS_CLEAR_LOG, () => processManager.clearActivityLog())
83-
ipcMain.handle(IPC.PROCESS_SCAN_ALL, () => processManager.scanAllProcesses())
84-
ipcMain.handle(IPC.PROCESS_KILL_PID, (_, pid)=> processManager.killPid(pid))
85-
ipcMain.handle(IPC.PROCESS_KILL_ALL_JAVA, () => processManager.killAllJava())
86-
ipcMain.handle(IPC.SETTINGS_GET, () => getSettings())
87-
ipcMain.handle(IPC.SETTINGS_SAVE, (_, s) => { saveSettings(s); applyStartupSetting(s.launchOnStartup) })
88-
ipcMain.handle(IPC.DIALOG_PICK_JAR, async () => { const r = await dialog.showOpenDialog({ filters: [{ name: 'JAR', extensions: ['jar'] }], properties: ['openFile'] }); return r.canceled ? null : r.filePaths[0] })
89-
ipcMain.handle(IPC.DIALOG_PICK_DIR, async () => { const r = await dialog.showOpenDialog({ properties: ['openDirectory'] }); return r.canceled ? null : r.filePaths[0] })
90-
ipcMain.handle(IPC.DIALOG_PICK_JAVA, async () => { const r = await dialog.showOpenDialog({ properties: ['openFile'] }); return r.canceled ? null : r.filePaths[0] })
91-
ipcMain.on(IPC.WINDOW_MINIMIZE, () => mainWindow?.minimize())
92-
ipcMain.on(IPC.WINDOW_CLOSE, () => { if (getSettings().minimizeToTray) mainWindow?.hide(); else { forceQuit = true; app.quit() } })
93-
}
94-
95-
function applyStartupSetting(enable: boolean): void {
96-
if (process.platform !== 'win32') return
97-
app.setLoginItemSettings({ openAtLogin: enable, openAsHidden: true, args: ['--hidden'] })
98-
}
99-
10075
app.whenReady().then(() => {
10176
createWindow()
10277
createTray()
103-
registerIpc()
78+
79+
// ── IPC handlers ────────────────────────────────────────────────────────────
80+
81+
ipcMain.handle(IPC.PROFILES_GET_ALL, () => getAllProfiles())
82+
83+
ipcMain.handle(IPC.PROFILES_SAVE, (_e, profile) => {
84+
saveProfile(profile)
85+
// Keep processManager snapshot in sync so auto-restart uses latest config
86+
processManager.updateProfileSnapshot(profile)
87+
})
88+
89+
ipcMain.handle(IPC.PROFILES_DELETE, (_e, id) => deleteProfile(id))
90+
ipcMain.handle(IPC.PROCESS_START, (_e, profile) => processManager.start(profile))
91+
ipcMain.handle(IPC.PROCESS_STOP, (_e, id) => processManager.stop(id))
92+
ipcMain.handle(IPC.PROCESS_SEND_INPUT, (_e, id, inp) => processManager.sendInput(id, inp))
93+
ipcMain.handle(IPC.PROCESS_GET_STATES, () => processManager.getStates())
94+
ipcMain.handle(IPC.PROCESS_GET_LOG, () => processManager.getActivityLog())
95+
ipcMain.handle(IPC.PROCESS_CLEAR_LOG, () => processManager.clearActivityLog())
96+
ipcMain.handle(IPC.PROCESS_SCAN_ALL, () => processManager.scanAllProcesses())
97+
ipcMain.handle(IPC.PROCESS_KILL_PID, (_e, pid) => processManager.killPid(pid))
98+
ipcMain.handle(IPC.PROCESS_KILL_ALL_JAVA,() => processManager.killAllJava())
99+
ipcMain.handle(IPC.SETTINGS_GET, () => getSettings())
100+
ipcMain.handle(IPC.SETTINGS_SAVE, (_e, s) => saveSettings(s))
101+
102+
ipcMain.handle(IPC.DIALOG_PICK_JAR, async () => {
103+
const r = await dialog.showOpenDialog(mainWindow!, { filters: [{ name: 'JAR', extensions: ['jar'] }], properties: ['openFile'] })
104+
return r.canceled ? null : r.filePaths[0]
105+
})
106+
ipcMain.handle(IPC.DIALOG_PICK_DIR, async () => {
107+
const r = await dialog.showOpenDialog(mainWindow!, { properties: ['openDirectory'] })
108+
return r.canceled ? null : r.filePaths[0]
109+
})
110+
ipcMain.handle(IPC.DIALOG_PICK_JAVA, async () => {
111+
const r = await dialog.showOpenDialog(mainWindow!, { filters: [{ name: 'Executable', extensions: ['exe', '*'] }], properties: ['openFile'] })
112+
return r.canceled ? null : r.filePaths[0]
113+
})
114+
115+
ipcMain.on(IPC.WINDOW_MINIMIZE, () => mainWindow?.minimize())
116+
ipcMain.on(IPC.WINDOW_CLOSE, () => { if (getSettings().minimizeToTray) mainWindow?.hide(); else { forceQuit = true; app.quit() } })
117+
118+
// Auto-start profiles
104119
const profiles = getAllProfiles()
105-
for (const p of profiles) if (p.autoStart && p.jarPath) processManager.start(p)
106-
applyStartupSetting(getSettings().launchOnStartup)
107-
setInterval(updateTrayMenu, 5000)
120+
for (const p of profiles) {
121+
if (p.autoStart && p.jarPath) processManager.start(p)
122+
}
123+
124+
// Keep tray menu in sync with process state changes
125+
mainWindow?.webContents.on('did-finish-load', updateTrayMenu)
108126
})
109127

110-
app.on('window-all-closed', () => { /* keep alive via tray */ })
128+
app.on('window-all-closed', () => { /* keep alive in tray */ })
111129
app.on('before-quit', () => { forceQuit = true })
130+
app.on('activate', () => { mainWindow?.show() })

0 commit comments

Comments
 (0)