Skip to content

Commit 403fccf

Browse files
authored
Merge pull request #182 from yuja201/fix/181-worker-node-path
[fix] disable RunAsNode fuse and use utilityProcess for worker execution
2 parents b36f2cc + 8bb9df5 commit 403fccf

5 files changed

Lines changed: 152 additions & 152 deletions

File tree

package-lock.json

Lines changed: 14 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: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"build:unpack": "npm run build && electron-builder --dir",
2121
"build:win": "npm run build && electron-builder --win",
2222
"build:mac": "electron-vite build && electron-builder --mac",
23-
"build:linux": "electron-vite build && electron-builder --linux"
23+
"build:linux": "electron-vite build && electron-builder --linux",
24+
"postpack": "node scripts/fuse.mjs"
2425
},
2526
"dependencies": {
2627
"@anthropic-ai/sdk": "^0.68.0",
@@ -51,6 +52,7 @@
5152
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
5253
"@electron-toolkit/eslint-config-ts": "^3.1.0",
5354
"@electron-toolkit/tsconfig": "^2.0.0",
55+
"@electron/fuses": "^2.1.0",
5456
"@electron/rebuild": "^4.0.1",
5557
"@types/better-sqlite3": "^7.6.13",
5658
"@types/node": "^22.19.0",
@@ -83,10 +85,6 @@
8385
"filter": [
8486
"*.sql"
8587
]
86-
},
87-
{
88-
"from": "build/node/node.exe",
89-
"to": "bin/node.exe"
9088
}
9189
],
9290
"directories": {

scripts/fuse.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { flipFuses, FuseVersion, FuseV1Options } from '@electron/fuses'
2+
import { createRequire } from 'node:module'
3+
4+
const require = createRequire(import.meta.url)
5+
6+
await flipFuses(require.resolve('electron'), {
7+
version: FuseVersion.V1,
8+
[FuseV1Options.RunAsNode]: false
9+
})

src/main/services/data-generator/data-generator-service.ts

Lines changed: 71 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from 'node:path'
22
import os from 'node:os'
3-
import { spawn } from 'node:child_process'
4-
import { app, BrowserWindow } from 'electron'
3+
import { app, BrowserWindow, utilityProcess } from 'electron'
54
import type { WorkerTask, WorkerResult, GenerateRequest, GenerationResult } from '@shared/types'
65
import { getDatabaseByProjectId } from '../../database/databases'
76
import { getDBMSById } from '../../database/dbms'
@@ -83,108 +82,90 @@ export async function runDataGenerator(
8382
: undefined
8483

8584
const queue = [...tables]
86-
const running = new Set<ReturnType<typeof spawn>>()
85+
const running = new Set<ReturnType<typeof utilityProcess.fork>>()
8786
const results: WorkerResult[] = []
8887
const cacheRoot = getFileCacheRoot()
8988

9089
const startNext = async (): Promise<void> => {
91-
if (queue.length === 0) return
92-
const table = queue.shift()!
93-
94-
const task: WorkerTask = {
95-
projectId,
96-
dbType: dbTypeKey,
97-
table,
98-
schema,
99-
database: databaseInfo,
100-
rules,
101-
mode,
102-
connection: connectionInfo,
103-
skipInvalidRows: payload.skipInvalidRows ?? true
104-
}
105-
const isPackaged = app.isPackaged
106-
const baseDir = isPackaged
107-
? fs.existsSync(path.join(process.resourcesPath, 'app.asar.unpacked'))
108-
? path.join(process.resourcesPath, 'app.asar.unpacked')
109-
: path.join(process.resourcesPath, 'app')
110-
: app.getAppPath()
111-
112-
const workerPath = path.join(baseDir, 'out', 'main', 'worker-runner.js')
113-
114-
const nodeBinary = path.join(process.resourcesPath, 'bin', 'node.exe')
115-
116-
const child = spawn(nodeBinary, [workerPath], {
117-
stdio: ['pipe', 'pipe', 'pipe'],
118-
env: {
119-
...process.env,
120-
TASK: JSON.stringify(task),
121-
HERESDUMMY_CACHE_DIR: cacheRoot
90+
try {
91+
if (queue.length === 0) return
92+
const table = queue.shift()!
93+
94+
const task: WorkerTask = {
95+
projectId,
96+
dbType: dbTypeKey,
97+
table,
98+
schema,
99+
database: databaseInfo,
100+
rules,
101+
mode,
102+
connection: connectionInfo,
103+
skipInvalidRows: payload.skipInvalidRows ?? true
122104
}
123-
})
124-
running.add(child)
125-
126-
let stdout = ''
127-
let stdoutBuffer = ''
128-
let stderr = ''
129-
130-
child.stdout.on('data', (data) => {
131-
const chunk = data.toString()
132-
process.stdout.write(chunk)
133-
stdout += chunk
134-
135-
stdoutBuffer += chunk
136-
let newlineIndex: number
137-
while ((newlineIndex = stdoutBuffer.indexOf('\n')) !== -1) {
138-
const line = stdoutBuffer.slice(0, newlineIndex).trim()
139-
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1)
140-
if (!line) continue
141-
142-
if (line.startsWith('{') && line.endsWith('}')) {
143-
try {
144-
const msg = JSON.parse(line)
145-
if (msg.type) {
146-
mainWindow.webContents.send('data-generator:progress', msg)
147-
}
148-
} catch (err) {
149-
void err
105+
const isPackaged = app.isPackaged
106+
const baseDir = isPackaged
107+
? fs.existsSync(path.join(process.resourcesPath, 'app.asar.unpacked'))
108+
? path.join(process.resourcesPath, 'app.asar.unpacked')
109+
: path.join(process.resourcesPath, 'app')
110+
: app.getAppPath()
111+
112+
const workerPath = path.join(baseDir, 'out', 'main', 'worker-runner.js')
113+
114+
const child = utilityProcess.fork(workerPath, [], {
115+
stdio: 'pipe',
116+
env: {
117+
...process.env,
118+
HERESDUMMY_CACHE_DIR: cacheRoot
119+
},
120+
serviceName: 'Data Generator Worker'
121+
})
122+
running.add(child)
123+
124+
child.postMessage({
125+
type: 'start',
126+
task
127+
})
128+
129+
let stderr = ''
130+
131+
// 완료 행 수 받아서 진행 상황 프론트로 보내기
132+
child.on('message', (data) => {
133+
if (data.type) {
134+
if (data.type === 'worker-result') {
135+
results.push(data.result)
136+
} else {
137+
mainWindow.webContents.send('data-generator:progress', data)
150138
}
151139
}
152-
}
153-
})
154-
155-
child.stderr.on('data', (data) => {
156-
stderr += data.toString()
157-
console.error('[worker] stderr:', data.toString())
158-
})
159-
160-
child.on('close', (code) => {
161-
running.delete(child)
162-
if (code === 0) {
163-
// stdout 안에서 success 포함 JSON 객체 전부 추출
164-
const matches = stdout.match(/\{[^}]*"success"[^}]*\}/g)
165-
166-
if (matches && matches.length > 0) {
167-
// 가장 마지막 JSON이 최종 결과 JSON
168-
const finalJson = matches[matches.length - 1]
169-
results.push(JSON.parse(finalJson))
170-
} else {
140+
})
141+
142+
child.stderr?.on('data', (data) => {
143+
stderr += data.toString()
144+
console.error('[worker] stderr:', data.toString())
145+
})
146+
147+
child.on('exit', (code) => {
148+
running.delete(child)
149+
if (code !== 0) {
171150
results.push({
172151
success: false,
173152
tableName: table.tableName,
174153
sqlPath: '',
175-
error: 'Worker finished without a valid result JSON.'
154+
error: stderr || `Worker exited with code ${code}`
176155
})
177156
}
178-
} else {
179-
results.push({
180-
success: false,
181-
tableName: table.tableName,
182-
sqlPath: '',
183-
error: stderr || `Worker exited with code ${code}`
184-
})
185-
}
157+
startNext()
158+
})
159+
} catch (err) {
160+
logger.error('startNext 실패', err)
161+
results.push({
162+
success: false,
163+
tableName: 'unknown',
164+
sqlPath: '',
165+
error: (err as Error).message
166+
})
186167
startNext()
187-
})
168+
}
188169
}
189170

190171
for (let i = 0; i < Math.min(MAX_PARALLEL, queue.length); i++) {

0 commit comments

Comments
 (0)