From 804544f63755b503b2c54ad50bbce9648b3e7027 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 17 Jul 2026 02:39:09 +0900 Subject: [PATCH 1/2] refactor: extract Windows process list script into a function Expose the PowerShell script that lists Unity processes as windowsUnityProcessListScript() so its content can be unit tested, mirroring the existing focus script builders. No behavior change. --- cli/common/unityprocess/process.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cli/common/unityprocess/process.go b/cli/common/unityprocess/process.go index dc023eacd8..cef0d76931 100644 --- a/cli/common/unityprocess/process.go +++ b/cli/common/unityprocess/process.go @@ -74,6 +74,16 @@ func listUnityProcessesMac(ctx context.Context) ([]UnityProcess, error) { } func listUnityProcessesWindows(ctx context.Context) ([]UnityProcess, error) { + commandContext, cancel := withCommandTimeout(ctx, ProcessListCommandTimeout) + defer cancel() + output, err := exec.CommandContext(commandContext, windowsPowerShellCommand, "-NoProfile", "-Command", windowsUnityProcessListScript()).Output() + if err != nil { + return nil, fmt.Errorf("failed to retrieve Unity process list on Windows: %w", err) + } + return parseWindowsUnityProcesses(string(output)), nil +} + +func windowsUnityProcessListScript() string { scriptLines := []string{ "$ErrorActionPreference = 'Stop'", "$processes = Get-CimInstance Win32_Process -Filter \"Name = 'Unity.exe'\" | Where-Object { $_.CommandLine }", @@ -82,13 +92,7 @@ func listUnityProcessesWindows(ctx context.Context) ([]UnityProcess, error) { " Write-Output (\"{0}|{1}\" -f $process.ProcessId, $commandLine)", "}", } - commandContext, cancel := withCommandTimeout(ctx, ProcessListCommandTimeout) - defer cancel() - output, err := exec.CommandContext(commandContext, windowsPowerShellCommand, "-NoProfile", "-Command", strings.Join(scriptLines, "\n")).Output() - if err != nil { - return nil, fmt.Errorf("failed to retrieve Unity process list on Windows: %w", err) - } - return parseWindowsUnityProcesses(string(output)), nil + return strings.Join(scriptLines, "\n") } func parseMacUnityProcesses(output string) []UnityProcess { From 0ec9d6769f04dd9e2700ca3f58e29639c9a85a8e Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 17 Jul 2026 02:47:43 +0900 Subject: [PATCH 2/2] fix: transport Windows process command lines as UTF-8 Base64 Windows PowerShell 5.1 encodes redirected stdout with the OEM code page (CP932 on Japanese Windows), so the Unity process list script delivered non-ASCII project paths as corrupted bytes that never matched the UTF-8 target path. FindRunningUnityProcess then missed running editors, breaking launch -q / launch -r / AlreadyRunning detection and focus targeting for any non-ASCII project path, with launch -q silently reporting success while doing nothing. Encode each command line as Base64 over UTF-8 bytes inside the PowerShell script and decode it in the Go parser, keeping the stdout stream ASCII-only regardless of the console code page. This follows the existing -EncodedCommand precedent in the installer scripts. --- cli/common/unityprocess/process.go | 17 +++++++- cli/common/unityprocess/process_test.go | 55 +++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/cli/common/unityprocess/process.go b/cli/common/unityprocess/process.go index cef0d76931..6a1d83c4d0 100644 --- a/cli/common/unityprocess/process.go +++ b/cli/common/unityprocess/process.go @@ -2,6 +2,7 @@ package unityprocess import ( "context" + "encoding/base64" "fmt" "os/exec" "path/filepath" @@ -83,13 +84,20 @@ func listUnityProcessesWindows(ctx context.Context) ([]UnityProcess, error) { return parseWindowsUnityProcesses(string(output)), nil } +// windowsUnityProcessListScript builds the PowerShell script that lists Unity +// processes as "pid|base64(UTF-8 command line)" lines. +// why: Windows PowerShell 5.1 encodes redirected stdout with the OEM code page +// (e.g. CP932 on Japanese Windows), so non-ASCII project paths in the command +// line get corrupted when Go reads the output as UTF-8. Base64 over UTF-8 +// bytes keeps the stream ASCII-only regardless of the console code page. func windowsUnityProcessListScript() string { scriptLines := []string{ "$ErrorActionPreference = 'Stop'", "$processes = Get-CimInstance Win32_Process -Filter \"Name = 'Unity.exe'\" | Where-Object { $_.CommandLine }", "foreach ($process in $processes) {", " $commandLine = $process.CommandLine -replace \"`r\", ' ' -replace \"`n\", ' '", - " Write-Output (\"{0}|{1}\" -f $process.ProcessId, $commandLine)", + " $encodedCommandLine = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($commandLine))", + " Write-Output (\"{0}|{1}\" -f $process.ProcessId, $encodedCommandLine)", "}", } return strings.Join(scriptLines, "\n") @@ -140,7 +148,12 @@ func parseWindowsUnityProcesses(output string) []UnityProcess { continue } - command := strings.TrimSpace(trimmed[delimiterIndex+1:]) + decodedCommand, err := base64.StdEncoding.DecodeString(strings.TrimSpace(trimmed[delimiterIndex+1:])) + if err != nil { + continue + } + + command := strings.TrimSpace(string(decodedCommand)) if !isUnityEditorCommand(command, windowsUnityExecutablePattern) { continue } diff --git a/cli/common/unityprocess/process_test.go b/cli/common/unityprocess/process_test.go index f29976f524..c0fb37b271 100644 --- a/cli/common/unityprocess/process_test.go +++ b/cli/common/unityprocess/process_test.go @@ -1,6 +1,7 @@ package unityprocess import ( + "encoding/base64" "path/filepath" "runtime" "strings" @@ -27,11 +28,13 @@ func TestParseMacUnityProcessesExtractsProjectPath(t *testing.T) { } } -// Verifies Windows Unity process parsing extracts project paths and skips batchmode workers. +// Verifies Windows Unity process parsing decodes Base64 command lines, extracts project paths, and skips batchmode workers. func TestParseWindowsUnityProcessesExtractsProjectPath(t *testing.T) { - output := `123|C:\Program Files\Unity\Hub\Editor\6000.0.0f1\Editor\Unity.exe -projectPath "C:\Users\\My Project" -useHub -456|C:\Program Files\Unity\Hub\Editor\6000.0.0f1\Editor\Unity.exe -batchmode -projectPath "C:\Users\\Batch" -` + encode := func(commandLine string) string { + return base64.StdEncoding.EncodeToString([]byte(commandLine)) + } + output := "123|" + encode(`C:\Program Files\Unity\Hub\Editor\6000.0.0f1\Editor\Unity.exe -projectPath "C:\Users\\My Project" -useHub`) + "\r\n" + + "456|" + encode(`C:\Program Files\Unity\Hub\Editor\6000.0.0f1\Editor\Unity.exe -batchmode -projectPath "C:\Users\\Batch"`) + "\r\n" processes := parseWindowsUnityProcesses(output) @@ -43,6 +46,50 @@ func TestParseWindowsUnityProcessesExtractsProjectPath(t *testing.T) { } } +// Verifies non-ASCII project paths survive the PowerShell boundary because command lines travel as UTF-8 Base64. +func TestParseWindowsUnityProcessesPreservesNonASCIIProjectPath(t *testing.T) { + projectPath := `C:\Users\\test[1] 検証用\proj` + commandLine := `C:\Program Files\Unity\Hub\Editor\2022.3.62f3\Editor\Unity.exe -projectPath "` + projectPath + `" -useHub` + output := "123|" + base64.StdEncoding.EncodeToString([]byte(commandLine)) + "\r\n" + + processes := parseWindowsUnityProcesses(output) + + if len(processes) != 1 { + t.Fatalf("process count mismatch: %#v", processes) + } + if processes[0].projectPath != projectPath { + t.Fatalf("project path mismatch: %q", processes[0].projectPath) + } +} + +// Verifies command fields that are not valid Base64 (e.g. legacy plain-text or OEM code page bytes) are skipped instead of mis-parsed. +func TestParseWindowsUnityProcessesSkipsNonBase64CommandLines(t *testing.T) { + // 0x8C9F 0x8FD8 0x9770 is the measured CP932 byte sequence for "検証用" + // that Windows PowerShell 5.1 emitted before the Base64 contract. + cp932KenshouYou := string([]byte{0x8C, 0x9F, 0x8F, 0xD8, 0x97, 0x70}) + output := "123|" + `C:\Editor\Unity.exe -projectPath "C:\Users\\` + cp932KenshouYou + `\proj"` + "\r\n" + + processes := parseWindowsUnityProcesses(output) + + if len(processes) != 0 { + t.Fatalf("non-Base64 command lines should be skipped: %#v", processes) + } +} + +// Verifies the Windows process list script transports command lines as UTF-8 Base64 so the OEM console code page cannot corrupt non-ASCII paths. +func TestWindowsUnityProcessListScriptEncodesCommandLineAsUTF8Base64(t *testing.T) { + script := windowsUnityProcessListScript() + + for _, expected := range []string{ + "[System.Text.Encoding]::UTF8.GetBytes($commandLine)", + "[Convert]::ToBase64String(", + } { + if !strings.Contains(script, expected) { + t.Fatalf("script missing %q: %s", expected, script) + } + } +} + // Verifies Unity -projectPath extraction supports quoted, unquoted, and equals forms. func TestExtractProjectPathSupportsEqualsAndSpaces(t *testing.T) { cases := map[string]string{