From 8b45960662800406cc7d7ffc2e53ef530c80d040 Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:03:06 +0800 Subject: [PATCH] Fix windows path (#361) --- electron-builder.yml | 2 - electron/utils/openclaw-cli.ts | 82 ++++++- package.json | 2 +- resources/cli/win32/update-user-path.ps1 | 88 +++++++ scripts/bundle-openclaw.mjs | 284 ++++++++++++++++++++++- scripts/installer.nsh | 182 ++------------- 6 files changed, 476 insertions(+), 164 deletions(-) create mode 100644 resources/cli/win32/update-user-path.ps1 diff --git a/electron-builder.yml b/electron-builder.yml index 88899dbc1..a0aee3610 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -118,8 +118,6 @@ win: nsis: oneClick: false perMachine: false - # Avoid NSIS build failure: warning 6010 (_ci_StrContains "not referenced") is emitted when - # building the uninstaller, because that function is only used in the install macro. warningsAsErrors: false allowToChangeInstallationDirectory: true deleteAppDataOnUninstall: false diff --git a/electron/utils/openclaw-cli.ts b/electron/utils/openclaw-cli.ts index 9d9e9f5f6..2537507cc 100644 --- a/electron/utils/openclaw-cli.ts +++ b/electron/utils/openclaw-cli.ts @@ -104,6 +104,11 @@ function getPackagedCliWrapperPath(): string | null { return null; } +function getWindowsPowerShellPath(): string { + const systemRoot = process.env.SystemRoot || 'C:\\Windows'; + return join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); +} + // ── macOS / Linux install ──────────────────────────────────────────────────── function getCliTargetPath(): string { @@ -165,6 +170,71 @@ function isCliInstalled(): boolean { return true; } +function ensureWindowsCliOnPath(): Promise<'updated' | 'already-present'> { + return new Promise((resolve, reject) => { + const cliWrapper = getPackagedCliWrapperPath(); + if (!cliWrapper) { + reject(new Error('CLI wrapper not found in app resources.')); + return; + } + + const cliDir = dirname(cliWrapper); + const helperPath = join(cliDir, 'update-user-path.ps1'); + if (!existsSync(helperPath)) { + reject(new Error(`PATH helper not found at ${helperPath}`)); + return; + } + + const child = spawn( + getWindowsPowerShellPath(), + [ + '-NoProfile', + '-NonInteractive', + '-ExecutionPolicy', + 'Bypass', + '-File', + helperPath, + '-Action', + 'add', + '-CliDir', + cliDir, + ], + { + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }, + ); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('error', reject); + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(stderr.trim() || `PowerShell exited with code ${code}`)); + return; + } + + const status = stdout.trim(); + if (status === 'updated' || status === 'already-present') { + resolve(status); + return; + } + + reject(new Error(`Unexpected PowerShell output: ${status || '(empty)'}`)); + }); + }); +} + function ensureLocalBinInPath(): void { if (process.platform === 'win32') return; @@ -205,7 +275,17 @@ export async function autoInstallCliIfNeeded( notify?: (path: string) => void, ): Promise { if (!app.isPackaged) return; - if (process.platform === 'win32') return; // NSIS handles it + if (process.platform === 'win32') { + try { + const result = await ensureWindowsCliOnPath(); + if (result === 'updated') { + logger.info('Added Windows CLI directory to user PATH.'); + } + } catch (error) { + logger.warn('Failed to ensure Windows CLI is on PATH:', error); + } + return; + } const target = getCliTargetPath(); const wrapperSrc = getPackagedCliWrapperPath(); diff --git a/package.json b/package.json index 7e972cd1d..b2fd0d9e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawx", - "version": "0.1.24-alpha.1", + "version": "0.1.24-alpha.7", "pnpm": { "onlyBuiltDependencies": [ "@whiskeysockets/baileys", diff --git a/resources/cli/win32/update-user-path.ps1 b/resources/cli/win32/update-user-path.ps1 new file mode 100644 index 000000000..99677039f --- /dev/null +++ b/resources/cli/win32/update-user-path.ps1 @@ -0,0 +1,88 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet('add', 'remove')] + [string]$Action, + + [Parameter(Mandatory = $true)] + [string]$CliDir +) + +$ErrorActionPreference = 'Stop' + +function Normalize-PathEntry { + param([string]$Value) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return '' + } + + return $Value.Trim().Trim('"').TrimEnd('\').ToLowerInvariant() +} + +$current = [Environment]::GetEnvironmentVariable('Path', 'User') +$entries = @() +if (-not [string]::IsNullOrWhiteSpace($current)) { + $entries = $current -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } +} + +$target = Normalize-PathEntry $CliDir +$seen = [System.Collections.Generic.HashSet[string]]::new() +$nextEntries = New-Object System.Collections.Generic.List[string] + +foreach ($entry in $entries) { + $normalized = Normalize-PathEntry $entry + if ([string]::IsNullOrWhiteSpace($normalized)) { + continue + } + + if ($normalized -eq $target) { + continue + } + + if ($seen.Add($normalized)) { + $nextEntries.Add($entry.Trim().Trim('"')) + } +} + +$status = 'already-present' +if ($Action -eq 'add') { + if ($seen.Add($target)) { + $nextEntries.Add($CliDir) + $status = 'updated' + } +} elseif ($entries.Count -ne $nextEntries.Count) { + $status = 'updated' +} + +$newPath = if ($nextEntries.Count -eq 0) { $null } else { $nextEntries -join ';' } +[Environment]::SetEnvironmentVariable('Path', $newPath, 'User') + +Add-Type -Namespace OpenClaw -Name NativeMethods -MemberDefinition @" +using System; +using System.Runtime.InteropServices; +public static class NativeMethods { + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr SendMessageTimeout( + IntPtr hWnd, + int Msg, + IntPtr wParam, + string lParam, + int fuFlags, + int uTimeout, + out IntPtr lpdwResult + ); +} +"@ + +$result = [IntPtr]::Zero +[OpenClaw.NativeMethods]::SendMessageTimeout( + [IntPtr]0xffff, + 0x001A, + [IntPtr]::Zero, + 'Environment', + 0x0002, + 5000, + [ref]$result +) | Out-Null + +Write-Output $status diff --git a/scripts/bundle-openclaw.mjs b/scripts/bundle-openclaw.mjs index be4d605ac..030cd43c2 100644 --- a/scripts/bundle-openclaw.mjs +++ b/scripts/bundle-openclaw.mjs @@ -374,9 +374,11 @@ echo` Size: ${formatSize(sizeBefore)} → ${formatSize(sizeAfter)} (saved ${fo // Node.js 22+ ESM interop when the translators try to call hasOwnProperty on // the undefined exports object. // +// We also patch Windows child_process spawn sites in the bundled agent runtime +// so shell/tool execution does not flash a console window for each tool call. // We patch these files in-place after the copy so the bundle is safe to run. function patchBrokenModules(nodeModulesDir) { - const patches = { + const rewritePatches = { // node-domexception@1.0.0: transpiled index.js leaves module.exports = undefined. // Node.js 18+ ships DOMException as a built-in global, so a simple shim works. 'node-domexception/index.js': [ @@ -393,21 +395,299 @@ function patchBrokenModules(nodeModulesDir) { `module.exports.default = dom;`, ].join('\n'), }; + const replacePatches = [ + { + rel: '@mariozechner/pi-coding-agent/dist/core/bash-executor.js', + search: ` const child = spawn(shell, [...args, command], { + detached: true, + env: getShellEnv(), + stdio: ["ignore", "pipe", "pipe"], + });`, + replace: ` const child = spawn(shell, [...args, command], { + detached: true, + env: getShellEnv(), + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + });`, + }, + { + rel: '@mariozechner/pi-coding-agent/dist/core/exec.js', + search: ` const proc = spawn(command, args, { + cwd, + shell: false, + stdio: ["ignore", "pipe", "pipe"], + });`, + replace: ` const proc = spawn(command, args, { + cwd, + shell: false, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + });`, + }, + ]; let count = 0; - for (const [rel, content] of Object.entries(patches)) { + for (const [rel, content] of Object.entries(rewritePatches)) { const target = path.join(nodeModulesDir, rel); if (fs.existsSync(target)) { fs.writeFileSync(target, content + '\n', 'utf8'); count++; } } + for (const { rel, search, replace } of replacePatches) { + const target = path.join(nodeModulesDir, rel); + if (!fs.existsSync(target)) continue; + + const current = fs.readFileSync(target, 'utf8'); + if (!current.includes(search)) { + echo` ⚠️ Skipped patch for ${rel}: expected source snippet not found`; + continue; + } + + const next = current.replace(search, replace); + if (next !== current) { + fs.writeFileSync(target, next, 'utf8'); + count++; + } + } if (count > 0) { echo` 🩹 Patched ${count} broken module(s) in node_modules`; } } +function findFirstFileByName(rootDir, matcher) { + const stack = [rootDir]; + while (stack.length > 0) { + const current = stack.pop(); + let entries = []; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(fullPath); + continue; + } + if (entry.isFile() && matcher.test(entry.name)) { + return fullPath; + } + } + } + return null; +} + +function findFilesByName(rootDir, matcher) { + const matches = []; + const stack = [rootDir]; + while (stack.length > 0) { + const current = stack.pop(); + let entries = []; + try { + entries = fs.readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + const fullPath = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(fullPath); + continue; + } + if (entry.isFile() && matcher.test(entry.name)) { + matches.push(fullPath); + } + } + } + return matches; +} + +function patchBundledRuntime(outputDir) { + const replacePatches = [ + { + label: 'workspace command runner', + target: () => findFirstFileByName(path.join(outputDir, 'dist'), /^workspace-.*\.js$/), + search: `\tconst child = spawn(resolvedCommand, finalArgv.slice(1), { +\t\tstdio, +\t\tcwd, +\t\tenv: resolvedEnv, +\t\twindowsVerbatimArguments, +\t\t...shouldSpawnWithShell({ +\t\t\tresolvedCommand, +\t\t\tplatform: process$1.platform +\t\t}) ? { shell: true } : {} +\t});`, + replace: `\tconst child = spawn(resolvedCommand, finalArgv.slice(1), { +\t\tstdio, +\t\tcwd, +\t\tenv: resolvedEnv, +\t\twindowsVerbatimArguments, +\t\twindowsHide: true, +\t\t...shouldSpawnWithShell({ +\t\t\tresolvedCommand, +\t\t\tplatform: process$1.platform +\t\t}) ? { shell: true } : {} +\t});`, + }, + { + label: 'agent scope command runner', + target: () => findFirstFileByName(path.join(outputDir, 'dist', 'plugin-sdk'), /^agent-scope-.*\.js$/), + search: `\tconst child = spawn(resolvedCommand, finalArgv.slice(1), { +\t\tstdio, +\t\tcwd, +\t\tenv: resolvedEnv, +\t\twindowsVerbatimArguments, +\t\t...shouldSpawnWithShell({ +\t\t\tresolvedCommand, +\t\t\tplatform: process$1.platform +\t\t}) ? { shell: true } : {} +\t});`, + replace: `\tconst child = spawn(resolvedCommand, finalArgv.slice(1), { +\t\tstdio, +\t\tcwd, +\t\tenv: resolvedEnv, +\t\twindowsVerbatimArguments, +\t\twindowsHide: true, +\t\t...shouldSpawnWithShell({ +\t\t\tresolvedCommand, +\t\t\tplatform: process$1.platform +\t\t}) ? { shell: true } : {} +\t});`, + }, + { + label: 'chrome launcher', + target: () => findFirstFileByName(path.join(outputDir, 'dist', 'plugin-sdk'), /^chrome-.*\.js$/), + search: `\t\treturn spawn(exe.path, args, { +\t\t\tstdio: "pipe", +\t\t\tenv: { +\t\t\t\t...process.env, +\t\t\t\tHOME: os.homedir() +\t\t\t} +\t\t});`, + replace: `\t\treturn spawn(exe.path, args, { +\t\t\tstdio: "pipe", +\t\t\twindowsHide: true, +\t\t\tenv: { +\t\t\t\t...process.env, +\t\t\t\tHOME: os.homedir() +\t\t\t} +\t\t});`, + }, + { + label: 'qmd runner', + target: () => findFirstFileByName(path.join(outputDir, 'dist', 'plugin-sdk'), /^qmd-manager-.*\.js$/), + search: `\t\t\tconst child = spawn(resolveWindowsCommandShim(this.qmd.command), args, { +\t\t\t\tenv: this.env, +\t\t\t\tcwd: this.workspaceDir +\t\t\t});`, + replace: `\t\t\tconst child = spawn(resolveWindowsCommandShim(this.qmd.command), args, { +\t\t\t\tenv: this.env, +\t\t\t\tcwd: this.workspaceDir, +\t\t\t\twindowsHide: true +\t\t\t});`, + }, + { + label: 'mcporter runner', + target: () => findFirstFileByName(path.join(outputDir, 'dist', 'plugin-sdk'), /^qmd-manager-.*\.js$/), + search: `\t\t\tconst child = spawn(resolveWindowsCommandShim("mcporter"), args, { +\t\t\t\tenv: this.env, +\t\t\t\tcwd: this.workspaceDir +\t\t\t});`, + replace: `\t\t\tconst child = spawn(resolveWindowsCommandShim("mcporter"), args, { +\t\t\t\tenv: this.env, +\t\t\t\tcwd: this.workspaceDir, +\t\t\t\twindowsHide: true +\t\t\t});`, + }, + ]; + + let count = 0; + for (const patch of replacePatches) { + const target = patch.target(); + if (!target || !fs.existsSync(target)) { + echo` ⚠️ Skipped patch for ${patch.label}: target file not found`; + continue; + } + + const current = fs.readFileSync(target, 'utf8'); + if (!current.includes(patch.search)) { + echo` ⚠️ Skipped patch for ${patch.label}: expected source snippet not found`; + continue; + } + + const next = current.replace(patch.search, patch.replace); + if (next !== current) { + fs.writeFileSync(target, next, 'utf8'); + count++; + } + } + + if (count > 0) { + echo` 🩹 Patched ${count} bundled runtime spawn site(s)`; + } + + const ptyTargets = findFilesByName( + path.join(outputDir, 'dist'), + /^(subagent-registry|reply|pi-embedded)-.*\.js$/, + ); + const ptyPatches = [ + { + label: 'pty launcher windowsHide', + search: `\tconst pty = spawn(params.shell, params.args, { +\t\tcwd: params.cwd, +\t\tenv: params.env ? toStringEnv(params.env) : void 0, +\t\tname: params.name ?? process.env.TERM ?? "xterm-256color", +\t\tcols: params.cols ?? 120, +\t\trows: params.rows ?? 30 +\t});`, + replace: `\tconst pty = spawn(params.shell, params.args, { +\t\tcwd: params.cwd, +\t\tenv: params.env ? toStringEnv(params.env) : void 0, +\t\tname: params.name ?? process.env.TERM ?? "xterm-256color", +\t\tcols: params.cols ?? 120, +\t\trows: params.rows ?? 30, +\t\twindowsHide: true +\t});`, + }, + { + label: 'disable pty on windows', + search: `\t\t\tconst usePty = params.pty === true && !sandbox;`, + replace: `\t\t\tconst usePty = params.pty === true && !sandbox && process.platform !== "win32";`, + }, + { + label: 'disable approval pty on windows', + search: `\t\t\t\t\tpty: params.pty === true && !sandbox,`, + replace: `\t\t\t\t\tpty: params.pty === true && !sandbox && process.platform !== "win32",`, + }, + ]; + + let ptyCount = 0; + for (const patch of ptyPatches) { + let matchedAny = false; + for (const target of ptyTargets) { + const current = fs.readFileSync(target, 'utf8'); + if (!current.includes(patch.search)) continue; + matchedAny = true; + const next = current.replaceAll(patch.search, patch.replace); + if (next !== current) { + fs.writeFileSync(target, next, 'utf8'); + ptyCount++; + } + } + if (!matchedAny) { + echo` ⚠️ Skipped patch for ${patch.label}: expected source snippet not found`; + } + } + + if (ptyCount > 0) { + echo` 🩹 Patched ${ptyCount} bundled PTY site(s)`; + } +} + patchBrokenModules(outputNodeModules); +patchBundledRuntime(OUTPUT); // 8. Verify the bundle const entryExists = fs.existsSync(path.join(OUTPUT, 'openclaw.mjs')); diff --git a/scripts/installer.nsh b/scripts/installer.nsh index d06256de4..d0d4df1ae 100644 --- a/scripts/installer.nsh +++ b/scripts/installer.nsh @@ -79,107 +79,40 @@ ; elevation this call silently fails — no crash, just no key written. WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1 - ; Add resources\cli to the current user's PATH for openclaw CLI. - ; Read current PATH, skip if already present, append otherwise. - ; - ; ReadRegStr silently returns "" when the value exceeds the NSIS string - ; buffer (8192 chars for the electron-builder large-strings build). - ; Without an error-flag check we would overwrite the entire user PATH with - ; only our CLI directory, destroying every other PATH entry. + ; Use PowerShell to update the current user's PATH. + ; This avoids NSIS string-buffer limits and preserves long PATH values. + InitPluginsDir ClearErrors - ReadRegStr $0 HKCU "Environment" "Path" - IfErrors _ci_readFailed - - StrCmp $0 "" _ci_setNew - - ; Check if our CLI dir is already in PATH - Push "$INSTDIR\resources\cli" - Push $0 - Call _ci_StrContains + File "/oname=$PLUGINSDIR\update-user-path.ps1" "${PROJECT_DIR}\resources\cli\win32\update-user-path.ps1" + nsExec::ExecToStack '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\update-user-path.ps1" -Action add -CliDir "$INSTDIR\resources\cli"' + Pop $0 Pop $1 - StrCmp $1 "" 0 _ci_done - - ; Append to existing PATH - StrCpy $0 "$0;$INSTDIR\resources\cli" - Goto _ci_write - - _ci_setNew: - StrCpy $0 "$INSTDIR\resources\cli" - - _ci_write: - WriteRegExpandStr HKCU "Environment" "Path" $0 - ; Broadcast WM_SETTINGCHANGE so running Explorer/terminals pick up the change - SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=500 + StrCmp $0 "error" 0 +2 + DetailPrint "Warning: Failed to launch PowerShell while updating PATH." + StrCmp $0 "timeout" 0 +2 + DetailPrint "Warning: PowerShell PATH update timed out." + StrCmp $0 "0" 0 +2 Goto _ci_done - - _ci_readFailed: - ; PATH value could not be read (likely exceeds NSIS buffer). - ; Skip modification to avoid destroying existing entries. - DetailPrint "Warning: Could not read user PATH (may exceed 8192 chars). Skipping PATH update." + DetailPrint "Warning: PowerShell PATH update exited with code $0." _ci_done: !macroend -; Helper: check if $R0 (needle) is found within $R1 (haystack). -; Pushes needle then haystack before call; pops result (needle if found, "" if not). -Function _ci_StrContains - Exch $R1 ; haystack - Exch - Exch $R0 ; needle - Push $R2 - Push $R3 - Push $R4 - - StrLen $R3 $R0 - StrLen $R4 $R1 - IntOp $R4 $R4 - $R3 - - StrCpy $R2 0 - _ci_loop: - IntCmp $R2 $R4 0 0 _ci_notfound - StrCpy $1 $R1 $R3 $R2 - StrCmp $1 $R0 _ci_found - IntOp $R2 $R2 + 1 - Goto _ci_loop - - _ci_found: - StrCpy $R0 $R0 - Goto _ci_end - - _ci_notfound: - StrCpy $R0 "" - - _ci_end: - Pop $R4 - Pop $R3 - Pop $R2 - Pop $R1 - Exch $R0 -FunctionEnd - !macro customUnInstall - ; Remove resources\cli from user PATH + ; Remove resources\cli from user PATH via PowerShell so long PATH values are handled safely + InitPluginsDir ClearErrors - ReadRegStr $0 HKCU "Environment" "Path" - IfErrors _cu_pathDone - StrCmp $0 "" _cu_pathDone - - ; Remove our entry (with leading or trailing semicolons) - Push $0 - Push "$INSTDIR\resources\cli" - Call un._cu_RemoveFromPath + File "/oname=$PLUGINSDIR\update-user-path.ps1" "${PROJECT_DIR}\resources\cli\win32\update-user-path.ps1" + nsExec::ExecToStack '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\update-user-path.ps1" -Action remove -CliDir "$INSTDIR\resources\cli"' Pop $0 - - ; If PATH is now empty, delete the registry value instead of writing "" - StrCmp $0 "" _cu_deletePath - WriteRegExpandStr HKCU "Environment" "Path" $0 - Goto _cu_pathBroadcast - - _cu_deletePath: - DeleteRegValue HKCU "Environment" "Path" - - _cu_pathBroadcast: - SendMessage ${HWND_BROADCAST} ${WM_SETTINGCHANGE} 0 "STR:Environment" /TIMEOUT=500 + Pop $1 + StrCmp $0 "error" 0 +2 + DetailPrint "Warning: Failed to launch PowerShell while removing PATH entry." + StrCmp $0 "timeout" 0 +2 + DetailPrint "Warning: PowerShell PATH removal timed out." + StrCmp $0 "0" 0 +2 + Goto _cu_pathDone + DetailPrint "Warning: PowerShell PATH removal exited with code $0." _cu_pathDone: @@ -219,70 +152,3 @@ FunctionEnd _cu_skipRemove: !macroend -; Uninstaller helper: remove a substring from a semicolon-delimited PATH string. -; Push haystack, push needle before call; pops cleaned string. -Function un._cu_RemoveFromPath - Exch $R0 ; needle - Exch - Exch $R1 ; haystack - - ; Try removing ";needle" (entry in the middle or end) - Push "$R1" - Push ";$R0" - Call un._ci_StrReplace - Pop $R1 - - ; Try removing "needle;" (entry at the start) - Push "$R1" - Push "$R0;" - Call un._ci_StrReplace - Pop $R1 - - ; Try removing exact match (only entry) - StrCmp $R1 $R0 0 +2 - StrCpy $R1 "" - - Pop $R0 - Exch $R1 -FunctionEnd - -; Uninstaller helper: remove first occurrence of needle from haystack. -; Push haystack, push needle; pops result. -Function un._ci_StrReplace - Exch $R0 ; needle - Exch - Exch $R1 ; haystack - Push $R2 - Push $R3 - Push $R4 - Push $R5 - - StrLen $R3 $R0 - StrLen $R4 $R1 - StrCpy $R5 "" - StrCpy $R2 0 - - _usr_loop: - IntCmp $R2 $R4 _usr_done _usr_done - StrCpy $1 $R1 $R3 $R2 - StrCmp $1 $R0 _usr_found - StrCpy $1 $R1 1 $R2 - StrCpy $R5 "$R5$1" - IntOp $R2 $R2 + 1 - Goto _usr_loop - - _usr_found: - ; Copy the part after the needle - IntOp $R2 $R2 + $R3 - StrCpy $1 $R1 "" $R2 - StrCpy $R5 "$R5$1" - - _usr_done: - StrCpy $R1 $R5 - Pop $R5 - Pop $R4 - Pop $R3 - Pop $R2 - Pop $R0 - Exch $R1 -FunctionEnd