feat: enhance Python environment setup with improved detection and verification methods
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
|
||||
let mainWindow;
|
||||
@@ -42,14 +42,195 @@ app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
let cachedSystemPython = null;
|
||||
|
||||
function getResourceRoot() {
|
||||
if (app.isPackaged) {
|
||||
const unpacked = path.join(process.resourcesPath, 'app.asar.unpacked');
|
||||
if (fs.existsSync(unpacked)) return unpacked;
|
||||
return process.resourcesPath;
|
||||
}
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
function getDataRoot() {
|
||||
return app.isPackaged ? app.getPath('userData') : __dirname;
|
||||
}
|
||||
|
||||
function getBackendPath(...segments) {
|
||||
return path.join(getResourceRoot(), 'backend', ...segments);
|
||||
}
|
||||
|
||||
function readRegistryValue(key, valueName) {
|
||||
try {
|
||||
const result = spawnSync('reg', ['query', key, '/v', valueName], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 5000,
|
||||
});
|
||||
if (result.status !== 0) return '';
|
||||
const match = result.stdout.match(new RegExp(`${valueName}\\s+REG_(?:EXPAND_)?SZ\\s+(.*)`, 'i'));
|
||||
return match ? match[1].trim() : '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function getSpawnEnv() {
|
||||
const env = { ...process.env };
|
||||
if (process.platform !== 'win32') return env;
|
||||
|
||||
const pathEntries = new Set();
|
||||
const addPaths = (value) => {
|
||||
if (!value) return;
|
||||
value.split(';').forEach((entry) => {
|
||||
const trimmed = entry.trim();
|
||||
if (trimmed) pathEntries.add(trimmed);
|
||||
});
|
||||
};
|
||||
|
||||
addPaths(process.env.PATH);
|
||||
addPaths(readRegistryValue('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment', 'Path'));
|
||||
addPaths(readRegistryValue('HKCU\\Environment', 'Path'));
|
||||
|
||||
const localAppData = process.env.LOCALAPPDATA
|
||||
|| (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
||||
if (localAppData) {
|
||||
pathEntries.add(path.join(localAppData, 'Programs', 'Python', 'Launcher'));
|
||||
for (const version of ['312', '311', '310']) {
|
||||
pathEntries.add(path.join(localAppData, 'Programs', 'Python', `Python${version}`));
|
||||
pathEntries.add(path.join(localAppData, 'Programs', 'Python', `Python${version}`, 'Scripts'));
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.ProgramFiles) {
|
||||
for (const version of ['312', '311', '310']) {
|
||||
pathEntries.add(path.join(process.env.ProgramFiles, `Python${version}`));
|
||||
pathEntries.add(path.join(process.env.ProgramFiles, `Python${version}`, 'Scripts'));
|
||||
}
|
||||
}
|
||||
|
||||
env.PATH = [...pathEntries].join(';');
|
||||
return env;
|
||||
}
|
||||
|
||||
function getWindowsPythonCandidates() {
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (command, args = []) => {
|
||||
const key = `${command}::${args.join(' ')}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
candidates.push({ command, args });
|
||||
};
|
||||
|
||||
const localAppData = process.env.LOCALAPPDATA
|
||||
|| (process.env.USERPROFILE ? path.join(process.env.USERPROFILE, 'AppData', 'Local') : null);
|
||||
|
||||
if (localAppData) {
|
||||
const pyLauncher = path.join(localAppData, 'Programs', 'Python', 'Launcher', 'py.exe');
|
||||
if (fs.existsSync(pyLauncher)) {
|
||||
for (const flag of ['-3.12', '-3.11', '-3.10', '-3']) {
|
||||
addCandidate(pyLauncher, [flag]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const version of ['312', '311', '310']) {
|
||||
const pythonExe = path.join(localAppData, 'Programs', 'Python', `Python${version}`, 'python.exe');
|
||||
if (fs.existsSync(pythonExe)) addCandidate(pythonExe, []);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.ProgramFiles) {
|
||||
for (const version of ['312', '311', '310']) {
|
||||
const pythonExe = path.join(process.env.ProgramFiles, `Python${version}`, 'python.exe');
|
||||
if (fs.existsSync(pythonExe)) addCandidate(pythonExe, []);
|
||||
}
|
||||
}
|
||||
|
||||
addCandidate('py', ['-3.12']);
|
||||
addCandidate('py', ['-3.11']);
|
||||
addCandidate('py', ['-3.10']);
|
||||
addCandidate('py', ['-3']);
|
||||
addCandidate('python', []);
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function verifyPython(command, args, env) {
|
||||
try {
|
||||
const result = spawnSync(command, [...args, '--version'], {
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
timeout: 6000,
|
||||
env,
|
||||
});
|
||||
return result.status === 0 && !result.error;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSystemPython() {
|
||||
if (cachedSystemPython) return cachedSystemPython;
|
||||
|
||||
const env = getSpawnEnv();
|
||||
const candidates = process.platform === 'win32'
|
||||
? getWindowsPythonCandidates()
|
||||
: [
|
||||
{ command: 'python3', args: [] },
|
||||
{ command: 'python', args: [] },
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (verifyPython(candidate.command, candidate.args, env)) {
|
||||
cachedSystemPython = candidate;
|
||||
console.log(`[Python Resolver] Selected: ${candidate.command} ${candidate.args.join(' ')}`);
|
||||
return cachedSystemPython;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('[Python Resolver] No working Python found; falling back to "python".');
|
||||
cachedSystemPython = { command: 'python', args: [], unverified: true };
|
||||
return cachedSystemPython;
|
||||
}
|
||||
|
||||
function spawnPython(command, args, options = {}) {
|
||||
return spawn(command, args, {
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
env: getSpawnEnv(),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function attachProcessListeners(child, { onLine, onClose, onError }) {
|
||||
child.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
for (const line of output.split('\n')) {
|
||||
if (line.trim()) onLine(line.trim());
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
const errStr = data.toString();
|
||||
for (const line of errStr.split('\n')) {
|
||||
if (line.trim()) onLine(`[ERROR] ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => onError(err));
|
||||
child.on('close', (code) => onClose(code));
|
||||
}
|
||||
|
||||
// Helper: Get Python Path
|
||||
function getPythonPaths() {
|
||||
const isWindows = process.platform === 'win32';
|
||||
const venvPython = isWindows
|
||||
? path.join(__dirname, '.venv', 'Scripts', 'python.exe')
|
||||
: path.join(__dirname, '.venv', 'bin', 'python');
|
||||
|
||||
const systemPython = 'python';
|
||||
const venvPython = isWindows
|
||||
? path.join(getDataRoot(), '.venv', 'Scripts', 'python.exe')
|
||||
: path.join(getDataRoot(), '.venv', 'bin', 'python');
|
||||
|
||||
const systemPython = resolveSystemPython();
|
||||
const hasVenv = fs.existsSync(venvPython);
|
||||
|
||||
return { venvPython, systemPython, hasVenv };
|
||||
@@ -64,36 +245,41 @@ ipcMain.handle('check-python-setup', async () => {
|
||||
// IPC: Run Setup
|
||||
ipcMain.handle('run-python-setup', async (event) => {
|
||||
const { systemPython } = getPythonPaths();
|
||||
const setupScript = path.join(__dirname, 'backend', 'setup.py');
|
||||
const setupScript = getBackendPath('setup.py');
|
||||
const dataDir = getDataRoot();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[Main Process] Spawning setup: ${systemPython} ${setupScript}`);
|
||||
// Spawn host python to run setup.py
|
||||
const process = spawn(systemPython, [setupScript], { shell: true });
|
||||
const { command, args: pythonArgs } = systemPython;
|
||||
const spawnArgs = [...pythonArgs, setupScript, '--data-dir', dataDir];
|
||||
console.log(`[Main Process] Spawning setup: ${command} ${spawnArgs.join(' ')}`);
|
||||
|
||||
let stderrData = '';
|
||||
let settled = false;
|
||||
const settle = (payload) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`[Setup Output] ${output.trim()}`);
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
event.sender.send('setup-progress', line.trim());
|
||||
}
|
||||
}
|
||||
});
|
||||
const setupProcess = spawnPython(command, spawnArgs, { cwd: dataDir });
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
const errStr = data.toString();
|
||||
stderrData += errStr;
|
||||
console.error(`[Setup Error] ${errStr.trim()}`);
|
||||
event.sender.send('setup-progress', `[ERROR] ${errStr.trim()}`);
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`[Main Process] Setup completed with exit code ${code}`);
|
||||
resolve({ code, error: stderrData });
|
||||
attachProcessListeners(setupProcess, {
|
||||
onLine: (line) => {
|
||||
console.log(`[Setup Output] ${line}`);
|
||||
event.sender.send('setup-progress', line);
|
||||
if (line.startsWith('[ERROR]')) stderrData += `${line}\n`;
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(`[Setup Error] Failed to start process: ${err.message}`);
|
||||
settle({
|
||||
code: 1,
|
||||
error: `Python baslatilamadi: ${err.message}. Python 3.10-3.12 kurulu oldugundan emin olun.`,
|
||||
});
|
||||
},
|
||||
onClose: (code) => {
|
||||
console.log(`[Main Process] Setup completed with exit code ${code}`);
|
||||
settle({ code, error: stderrData });
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -106,41 +292,38 @@ ipcMain.handle('remove-background', async (event, { inputPath, outputPath }) =>
|
||||
return { success: false, error: 'Python virtual environment not set up. Please run setup first.' };
|
||||
}
|
||||
|
||||
const processScript = path.join(__dirname, 'backend', 'process.py');
|
||||
const processScript = getBackendPath('process.py');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
console.log(`[Main Process] Spawning background removal: ${venvPython} ${processScript}`);
|
||||
// Spawn venv python to run process.py
|
||||
const args = ['--input', inputPath, '--output', outputPath];
|
||||
const process = spawn(venvPython, [processScript, ...args], { shell: false });
|
||||
|
||||
let stderrData = '';
|
||||
let settled = false;
|
||||
const settle = (payload) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(payload);
|
||||
};
|
||||
|
||||
process.stdout.on('data', (data) => {
|
||||
const output = data.toString();
|
||||
console.log(`[Python Output] ${output.trim()}`);
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
event.sender.send('process-progress', line.trim());
|
||||
const bgProcess = spawnPython(venvPython, [processScript, ...args], { cwd: getDataRoot() });
|
||||
|
||||
attachProcessListeners(bgProcess, {
|
||||
onLine: (line) => {
|
||||
console.log(`[Python Output] ${line}`);
|
||||
event.sender.send('process-progress', line);
|
||||
if (line.startsWith('[ERROR]')) stderrData += `${line}\n`;
|
||||
},
|
||||
onError: (err) => {
|
||||
settle({ success: false, error: `Python baslatilamadi: ${err.message}` });
|
||||
},
|
||||
onClose: (code) => {
|
||||
console.log(`[Main Process] Process completed with exit code ${code}`);
|
||||
if (code === 0) {
|
||||
settle({ success: true, outputPath });
|
||||
} else {
|
||||
settle({ success: false, error: stderrData || `Process failed with exit code ${code}` });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
process.stderr.on('data', (data) => {
|
||||
const errStr = data.toString();
|
||||
stderrData += errStr;
|
||||
console.error(`[Python Error] ${errStr.trim()}`);
|
||||
event.sender.send('process-progress', `[ERROR] ${errStr.trim()}`);
|
||||
});
|
||||
|
||||
process.on('close', (code) => {
|
||||
console.log(`[Main Process] Process completed with exit code ${code}`);
|
||||
if (code === 0) {
|
||||
resolve({ success: true, outputPath });
|
||||
} else {
|
||||
resolve({ success: false, error: stderrData || `Process failed with exit code ${code}` });
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user