|
1 | 1 | import path from 'node:path' |
2 | 2 | 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' |
5 | 4 | import type { WorkerTask, WorkerResult, GenerateRequest, GenerationResult } from '@shared/types' |
6 | 5 | import { getDatabaseByProjectId } from '../../database/databases' |
7 | 6 | import { getDBMSById } from '../../database/dbms' |
@@ -83,108 +82,90 @@ export async function runDataGenerator( |
83 | 82 | : undefined |
84 | 83 |
|
85 | 84 | const queue = [...tables] |
86 | | - const running = new Set<ReturnType<typeof spawn>>() |
| 85 | + const running = new Set<ReturnType<typeof utilityProcess.fork>>() |
87 | 86 | const results: WorkerResult[] = [] |
88 | 87 | const cacheRoot = getFileCacheRoot() |
89 | 88 |
|
90 | 89 | 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 |
122 | 104 | } |
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) |
150 | 138 | } |
151 | 139 | } |
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) { |
171 | 150 | results.push({ |
172 | 151 | success: false, |
173 | 152 | tableName: table.tableName, |
174 | 153 | sqlPath: '', |
175 | | - error: 'Worker finished without a valid result JSON.' |
| 154 | + error: stderr || `Worker exited with code ${code}` |
176 | 155 | }) |
177 | 156 | } |
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 | + }) |
186 | 167 | startNext() |
187 | | - }) |
| 168 | + } |
188 | 169 | } |
189 | 170 |
|
190 | 171 | for (let i = 0; i < Math.min(MAX_PARALLEL, queue.length); i++) { |
|
0 commit comments