mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 04:31:27 +08:00
* fix(windows): improve Git Bash detection for portable installations Enhance Git Bash detection on Windows to support portable Git installations and custom installation paths. The previous implementation only checked fixed paths and failed to detect Git when installed to custom locations or added to PATH manually. Key improvements: - Use where.exe to find git.exe in PATH and derive bash.exe location - Support CHERRY_STUDIO_GIT_BASH_PATH environment variable override - Add security check to skip executables in current directory - Implement three-tier fallback strategy (env var -> git derivation -> common paths) - Add detailed logging for troubleshooting This fixes the issue where users with portable Git installations could run git.exe from command line but the app failed to detect Git Bash. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(windows): improve Git Bash detection for portable installations Enhance Git Bash detection on Windows to support portable Git installations and custom installation paths. The previous implementation only checked fixed paths and failed to detect Git when installed to custom locations or added to PATH manually. Key improvements: - Move findExecutable and findGitBash to utils/process.ts for better code organization - Use where.exe to find git.exe in PATH and derive bash.exe location - Add security check to skip executables in current directory - Implement two-tier fallback strategy (git derivation -> common paths) - Add detailed logging for troubleshooting - Remove environment variable override to simplify implementation This fixes the issue where users with portable Git installations could run git.exe from command line but the app failed to detect Git Bash. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(windows): improve Git Bash detection for portable installations Enhance Git Bash detection on Windows to support portable Git installations and custom installation paths. The previous implementation only checked fixed paths and failed to detect Git when installed to custom locations or added to PATH manually. Key improvements: - Move findExecutable and findGitBash to utils/process.ts for better code organization - Use where.exe to find git.exe in PATH and derive bash.exe location - Add security check to skip executables in current directory - Implement two-tier fallback strategy (git derivation -> common paths) - Add detailed logging for troubleshooting - Remove environment variable override to simplify implementation This fixes the issue where users with portable Git installations could run git.exe from command line but the app failed to detect Git Bash. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * update iswin * test: add comprehensive test coverage for findExecutable and findGitBash Add 33 test cases covering: - Git found in common paths (Program Files, Program Files (x86)) - Git found via where.exe in PATH - Windows/Unix line ending handling (CRLF/LF) - Whitespace trimming from where.exe output - Security checks to skip executables in current directory - Multiple Git installation structures (Standard, Portable, MSYS2) - Bash.exe path derivation from git.exe location - Common paths fallback when git.exe not found - LOCALAPPDATA environment variable handling - Priority order (derivation over common paths) - Error scenarios (Git not installed, bash.exe missing) - Real-world scenarios (official installer, portable, Scoop) All tests pass with proper mocking of fs, path, and child_process modules. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: clarify path navigation comments in findGitBash Replace confusing arrow notation showing intermediate directories with clearer descriptions of the navigation intent: - "navigate up 2 levels" instead of showing "-> Git/cmd -> Git ->" - "bash.exe in same directory" for portable installations - Emphasizes the intent rather than the intermediate steps Makes the code more maintainable by clearly stating what each path pattern is checking for. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * test: skip process utility tests on non-Windows platforms Use describe.skipIf to skip all tests when not on Windows since findExecutable and findGitBash have platform guards that return null on non-Windows systems. Remove redundant platform mocking in nested describe blocks since the entire suite is already Windows-only. This fixes test failures on macOS and Linux where all 33 tests were failing because the functions correctly return null on those platforms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * format * fix: improve Git Bash detection error handling and logging - Add try-catch wrapper in IPC handler to handle unexpected errors - Fix inaccurate comment: usr/bin/bash.exe is for MSYS2, not Git 2.x - Change log level from INFO to DEBUG for internal "not found" message - Keep WARN level only in IPC handler for user-facing message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
import { loggerService } from '@logger'
|
|
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
|
import { execFileSync, spawn } from 'child_process'
|
|
import fs from 'fs'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
|
|
import { isWin } from '../constant'
|
|
import { getResourcePath } from '.'
|
|
|
|
const logger = loggerService.withContext('Utils:Process')
|
|
|
|
export function runInstallScript(scriptPath: string): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const installScriptPath = path.join(getResourcePath(), 'scripts', scriptPath)
|
|
logger.info(`Running script at: ${installScriptPath}`)
|
|
|
|
const nodeProcess = spawn(process.execPath, [installScriptPath], {
|
|
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }
|
|
})
|
|
|
|
nodeProcess.stdout.on('data', (data) => {
|
|
logger.debug(`Script output: ${data}`)
|
|
})
|
|
|
|
nodeProcess.stderr.on('data', (data) => {
|
|
logger.error(`Script error: ${data}`)
|
|
})
|
|
|
|
nodeProcess.on('close', (code) => {
|
|
if (code === 0) {
|
|
logger.debug('Script completed successfully')
|
|
resolve()
|
|
} else {
|
|
logger.warn(`Script exited with code ${code}`)
|
|
reject(new Error(`Process exited with code ${code}`))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
export async function getBinaryName(name: string): Promise<string> {
|
|
if (isWin) {
|
|
return `${name}.exe`
|
|
}
|
|
return name
|
|
}
|
|
|
|
export async function getBinaryPath(name?: string): Promise<string> {
|
|
if (!name) {
|
|
return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
|
}
|
|
|
|
const binaryName = await getBinaryName(name)
|
|
const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
|
const binariesDirExists = fs.existsSync(binariesDir)
|
|
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
|
|
}
|
|
|
|
export async function isBinaryExists(name: string): Promise<boolean> {
|
|
const cmd = await getBinaryPath(name)
|
|
return await fs.existsSync(cmd)
|
|
}
|
|
|
|
/**
|
|
* Find executable in common paths or PATH environment variable
|
|
* Based on Claude Code's implementation with security checks
|
|
* @param name - Name of the executable to find (without .exe extension)
|
|
* @returns Full path to the executable or null if not found
|
|
*/
|
|
export function findExecutable(name: string): string | null {
|
|
// This implementation uses where.exe which is Windows-only
|
|
if (!isWin) {
|
|
return null
|
|
}
|
|
|
|
// Special handling for git - check common installation paths first
|
|
if (name === 'git') {
|
|
const commonGitPaths = [
|
|
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Git', 'cmd', 'git.exe'),
|
|
path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'cmd', 'git.exe')
|
|
]
|
|
|
|
for (const gitPath of commonGitPaths) {
|
|
if (fs.existsSync(gitPath)) {
|
|
logger.debug(`Found ${name} at common path`, { path: gitPath })
|
|
return gitPath
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use where.exe to find executable in PATH
|
|
// Use execFileSync to prevent command injection
|
|
try {
|
|
// Add .exe extension for more precise matching on Windows
|
|
const executableName = `${name}.exe`
|
|
const result = execFileSync('where.exe', [executableName], {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
})
|
|
|
|
// Handle both Windows (\r\n) and Unix (\n) line endings
|
|
const paths = result.trim().split(/\r?\n/).filter(Boolean)
|
|
const currentDir = process.cwd().toLowerCase()
|
|
|
|
// Security check: skip executables in current directory
|
|
for (const exePath of paths) {
|
|
// Trim whitespace from where.exe output
|
|
const cleanPath = exePath.trim()
|
|
const resolvedPath = path.resolve(cleanPath).toLowerCase()
|
|
const execDir = path.dirname(resolvedPath).toLowerCase()
|
|
|
|
// Skip if in current directory or subdirectory (potential malware)
|
|
if (execDir === currentDir || execDir.startsWith(currentDir + path.sep)) {
|
|
logger.warn('Skipping potentially malicious executable in current directory', {
|
|
path: cleanPath
|
|
})
|
|
continue
|
|
}
|
|
|
|
logger.debug(`Found ${name} via where.exe`, { path: cleanPath })
|
|
return cleanPath
|
|
}
|
|
|
|
return null
|
|
} catch (error) {
|
|
logger.debug(`where.exe ${name} failed`, { error })
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find Git Bash executable on Windows
|
|
* @returns Full path to bash.exe or null if not found
|
|
*/
|
|
export function findGitBash(): string | null {
|
|
// Git Bash is Windows-only
|
|
if (!isWin) {
|
|
return null
|
|
}
|
|
|
|
// 1. Find git.exe and derive bash.exe path
|
|
const gitPath = findExecutable('git')
|
|
if (gitPath) {
|
|
// Try multiple possible locations for bash.exe relative to git.exe
|
|
// Different Git installations have different directory structures
|
|
const possibleBashPaths = [
|
|
path.join(gitPath, '..', '..', 'bin', 'bash.exe'), // Standard Git: git.exe at Git/cmd/ -> navigate up 2 levels -> then bin/bash.exe
|
|
path.join(gitPath, '..', 'bash.exe'), // Portable Git: git.exe at Git/bin/ -> bash.exe in same directory
|
|
path.join(gitPath, '..', '..', 'usr', 'bin', 'bash.exe') // MSYS2 Git: git.exe at msys64/usr/bin/ -> navigate up 2 levels -> then usr/bin/bash.exe
|
|
]
|
|
|
|
for (const bashPath of possibleBashPaths) {
|
|
const resolvedBashPath = path.resolve(bashPath)
|
|
if (fs.existsSync(resolvedBashPath)) {
|
|
logger.debug('Found bash.exe via git.exe path derivation', { path: resolvedBashPath })
|
|
return resolvedBashPath
|
|
}
|
|
}
|
|
|
|
logger.debug('bash.exe not found at expected locations relative to git.exe', {
|
|
gitPath,
|
|
checkedPaths: possibleBashPaths.map((p) => path.resolve(p))
|
|
})
|
|
}
|
|
|
|
// 2. Fallback: check common Git Bash paths directly
|
|
const commonBashPaths = [
|
|
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Git', 'bin', 'bash.exe'),
|
|
path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Git', 'bin', 'bash.exe'),
|
|
...(process.env.LOCALAPPDATA ? [path.join(process.env.LOCALAPPDATA, 'Programs', 'Git', 'bin', 'bash.exe')] : [])
|
|
]
|
|
|
|
for (const bashPath of commonBashPaths) {
|
|
if (fs.existsSync(bashPath)) {
|
|
logger.debug('Found bash.exe at common path', { path: bashPath })
|
|
return bashPath
|
|
}
|
|
}
|
|
|
|
logger.debug('Git Bash not found - checked git derivation and common paths')
|
|
return null
|
|
}
|