Skip to content

Commit aee40f5

Browse files
authored
Merge pull request #11 from timonmdy/rolling/5
Technical Rework
2 parents ccdac65 + 6eb5823 commit aee40f5

60 files changed

Lines changed: 4000 additions & 1728 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/format-check.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Code Formatting
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
check:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-node@v4
11+
with:
12+
node-version: 20
13+
cache: npm
14+
15+
- run: npm ci
16+
17+
- name: Prettier check
18+
run: npm run format:check

.prettierignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
dist
2+
node_modules
3+
.github
4+
profile-templates
5+
6+
README.md
7+
package.json
8+
package-lock.json

.prettierrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"trailingComma": "es5",
5+
"printWidth": 100,
6+
"tabWidth": 2,
7+
"endOfLine": "lf"
8+
}

package-lock.json

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

package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "java-runner-client",
3-
"version": "2.1.2",
3+
"version": "2.1.3",
44
"description": "Run and manage Java processes with profiles, console I/O, and system tray support",
55
"main": "dist/main/main.js",
66
"scripts": {
@@ -11,16 +11,17 @@
1111
"build:renderer": "vite build",
1212
"build:main": "tsc -p tsconfig.main.json",
1313
"dist": "rimraf dist && npm run build && electron-builder",
14-
"lint": "eslint src --ext .ts,.tsx"
14+
"format": "prettier --write .",
15+
"format:check": "prettier --check ."
1516
},
1617
"dependencies": {
1718
"electron-store": "^8.1.0",
18-
"uuid": "^9.0.0",
1919
"framer-motion": "^12.38.0",
2020
"react": "^18.2.0",
2121
"react-dom": "^18.2.0",
2222
"react-icons": "^5.2.0",
23-
"react-router-dom": "^6.22.0"
23+
"react-router-dom": "^6.22.0",
24+
"uuid": "^9.0.0"
2425
},
2526
"devDependencies": {
2627
"@types/node": "^20.0.0",
@@ -33,6 +34,7 @@
3334
"electron": "^28.0.0",
3435
"electron-builder": "^24.9.0",
3536
"postcss": "^8.4.0",
37+
"prettier": "^3.8.1",
3638
"rimraf": "^5.0.0",
3739
"tailwindcss": "^3.4.0",
3840
"terser": "^5.28.0",
@@ -63,5 +65,14 @@
6365
"oneClick": false,
6466
"allowToChangeInstallationDirectory": true
6567
}
68+
},
69+
"lint-staged": {
70+
"*.{ts,tsx,js,jsx}": [
71+
"prettier --write",
72+
"eslint --fix"
73+
],
74+
"*.{json,css,md}": [
75+
"prettier --write"
76+
]
6677
}
6778
}

src/main/RestAPI.ts

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import http from 'http'
2+
import { v4 as uuidv4 } from 'uuid'
3+
import type { Profile, AppSettings } from './shared/types'
4+
import { getAllProfiles, saveProfile, deleteProfile, getSettings, saveSettings } from './Store'
5+
import { processManager } from './ProcessManager'
6+
import { REST_API_CONFIG } from './shared/config/RestApi.config'
7+
8+
// ─── Primitives ───────────────────────────────────────────────────────────────
9+
10+
type Params = Record<string, string>
11+
12+
interface Context {
13+
req: http.IncomingMessage
14+
res: http.ServerResponse
15+
params: Params
16+
body: unknown
17+
}
18+
19+
type RouteHandler = (ctx: Context) => void | Promise<void>
20+
21+
interface RestRoute {
22+
method: string
23+
path: string
24+
handler: RouteHandler
25+
}
26+
27+
// ─── Helpers ──────────────────────────────────────────────────────────────────
28+
29+
function json(res: http.ServerResponse, data: unknown, status = 200) {
30+
res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' })
31+
res.end(JSON.stringify(data))
32+
}
33+
34+
function ok(res: http.ServerResponse, data: unknown = { ok: true }, status = 200) {
35+
json(res, data, status)
36+
}
37+
38+
function err(res: http.ServerResponse, msg: string, status = 400) {
39+
json(res, { error: msg }, status)
40+
}
41+
42+
function readBody(req: http.IncomingMessage): Promise<unknown> {
43+
return new Promise((resolve) => {
44+
let raw = ''
45+
req.on('data', (c) => {
46+
raw += c
47+
})
48+
req.on('end', () => {
49+
try {
50+
resolve(JSON.parse(raw))
51+
} catch {
52+
resolve({})
53+
}
54+
})
55+
})
56+
}
57+
58+
function parsePattern(path: string): { pattern: RegExp; keys: string[] } {
59+
const keys: string[] = []
60+
const src = path.replace(/:([a-zA-Z]+)/g, (_m, k) => {
61+
keys.push(k)
62+
return '([^/]+)'
63+
})
64+
return { pattern: new RegExp(`^${src}$`), keys }
65+
}
66+
67+
// ─── Route definitions ────────────────────────────────────────────────────────
68+
//
69+
// Add a new endpoint here — nothing else to touch.
70+
//
71+
// ctx.params → path params (e.g. :id)
72+
// ctx.body → parsed JSON body (POST/PUT/PATCH only, otherwise {})
73+
// Use ok() / err() / json() to respond.
74+
75+
const routes: RestRoute[] = [
76+
// ── Status ─────────────────────────────────────────────────────────────────
77+
78+
{
79+
method: 'GET',
80+
path: '/api/status',
81+
handler: ({ res }) =>
82+
ok(res, {
83+
ok: true,
84+
version: process.env.npm_package_version ?? 'unknown',
85+
profiles: getAllProfiles().length,
86+
running: processManager.getStates().filter((s) => s.running).length,
87+
}),
88+
},
89+
90+
// ── Profiles ───────────────────────────────────────────────────────────────
91+
92+
{
93+
method: 'GET',
94+
path: '/api/profiles',
95+
handler: ({ res }) => ok(res, getAllProfiles()),
96+
},
97+
{
98+
method: 'GET',
99+
path: '/api/profiles/:id',
100+
handler: ({ res, params }) => {
101+
const p = getAllProfiles().find((p) => p.id === params.id)
102+
p ? ok(res, p) : err(res, 'Profile not found', 404)
103+
},
104+
},
105+
{
106+
method: 'POST',
107+
path: '/api/profiles',
108+
handler: ({ res, body }) => {
109+
const b = body as Partial<Profile>
110+
const p: Profile = {
111+
id: uuidv4(),
112+
name: b.name ?? 'New Profile',
113+
jarPath: b.jarPath ?? '',
114+
workingDir: b.workingDir ?? '',
115+
jvmArgs: b.jvmArgs ?? [],
116+
systemProperties: b.systemProperties ?? [],
117+
programArgs: b.programArgs ?? [],
118+
javaPath: b.javaPath ?? '',
119+
autoStart: b.autoStart ?? false,
120+
autoRestart: b.autoRestart ?? false,
121+
autoRestartInterval: b.autoRestartInterval ?? 10,
122+
color: b.color ?? '#4ade80',
123+
createdAt: Date.now(),
124+
updatedAt: Date.now(),
125+
}
126+
saveProfile(p)
127+
ok(res, p, 201)
128+
},
129+
},
130+
{
131+
method: 'PUT',
132+
path: '/api/profiles/:id',
133+
handler: ({ res, params, body }) => {
134+
const existing = getAllProfiles().find((p) => p.id === params.id)
135+
if (!existing) return err(res, 'Profile not found', 404)
136+
const updated: Profile = {
137+
...existing,
138+
...(body as Partial<Profile>),
139+
id: params.id,
140+
updatedAt: Date.now(),
141+
}
142+
saveProfile(updated)
143+
processManager.updateProfileSnapshot(updated)
144+
ok(res, updated)
145+
},
146+
},
147+
{
148+
method: 'DELETE',
149+
path: '/api/profiles/:id',
150+
handler: ({ res, params }) => {
151+
if (!getAllProfiles().find((p) => p.id === params.id))
152+
return err(res, 'Profile not found', 404)
153+
deleteProfile(params.id)
154+
ok(res)
155+
},
156+
},
157+
158+
// ── Processes ──────────────────────────────────────────────────────────────
159+
160+
{
161+
method: 'GET',
162+
path: '/api/processes',
163+
handler: ({ res }) => ok(res, processManager.getStates()),
164+
},
165+
{
166+
method: 'GET',
167+
path: '/api/processes/log',
168+
handler: ({ res }) => ok(res, processManager.getActivityLog()),
169+
},
170+
{
171+
method: 'POST',
172+
path: '/api/processes/:id/start',
173+
handler: ({ res, params }) => {
174+
const p = getAllProfiles().find((p) => p.id === params.id)
175+
if (!p) return err(res, 'Profile not found', 404)
176+
ok(res, processManager.start(p))
177+
},
178+
},
179+
{
180+
method: 'POST',
181+
path: '/api/processes/:id/stop',
182+
handler: ({ res, params }) => ok(res, processManager.stop(params.id)),
183+
},
184+
{
185+
method: 'POST',
186+
path: '/api/processes/:id/console/clear',
187+
handler: ({ res, params }) => {
188+
processManager.clearConsoleForProfile(params.id)
189+
ok(res)
190+
},
191+
},
192+
193+
// ── Settings ───────────────────────────────────────────────────────────────
194+
195+
{
196+
method: 'GET',
197+
path: '/api/settings',
198+
handler: ({ res }) => ok(res, getSettings()),
199+
},
200+
{
201+
method: 'PUT',
202+
path: '/api/settings',
203+
handler: ({ res, body }) => {
204+
const updated: AppSettings = { ...getSettings(), ...(body as Partial<AppSettings>) }
205+
saveSettings(updated)
206+
ok(res, updated)
207+
},
208+
},
209+
]
210+
211+
const compiled = routes.map((r) => ({ ...r, ...parsePattern(r.path) }))
212+
213+
class RestApiServer {
214+
private server: http.Server | null = null
215+
216+
start(port: number): void {
217+
if (this.server) return
218+
219+
this.server = http.createServer(async (req, res) => {
220+
if (req.method === 'OPTIONS') {
221+
res.writeHead(204, {
222+
'Access-Control-Allow-Origin': '*',
223+
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
224+
'Access-Control-Allow-Headers': 'Content-Type',
225+
})
226+
return res.end()
227+
}
228+
229+
const url = req.url?.split('?')[0] ?? '/'
230+
const method = req.method ?? 'GET'
231+
const body = ['POST', 'PUT', 'PATCH'].includes(method) ? await readBody(req) : {}
232+
233+
for (const route of compiled) {
234+
if (route.method !== method) continue
235+
const match = url.match(route.pattern)
236+
if (!match) continue
237+
const params: Params = {}
238+
route.keys.forEach((k, i) => {
239+
params[k] = match[i + 1]
240+
})
241+
await route.handler({ req, res, params, body })
242+
return
243+
}
244+
245+
err(res, 'Not found', 404)
246+
})
247+
248+
this.server.listen(port, REST_API_CONFIG.host, () => {
249+
console.log(`[JRC REST] Listening on ${REST_API_CONFIG.host}:${port}`)
250+
})
251+
}
252+
253+
stop(): void {
254+
this.server?.close()
255+
this.server = null
256+
}
257+
}
258+
259+
export const restApiServer = new RestApiServer()

0 commit comments

Comments
 (0)