Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions cli/common/unityprocess/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package unityprocess

import (
"context"
"encoding/base64"
"fmt"
"os/exec"
"path/filepath"
Expand Down Expand Up @@ -74,21 +75,32 @@ 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
}

// 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)",
"}",
}
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 {
Expand Down Expand Up @@ -136,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
}
Expand Down
55 changes: 51 additions & 4 deletions cli/common/unityprocess/process_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package unityprocess

import (
"encoding/base64"
"path/filepath"
"runtime"
"strings"
Expand All @@ -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\<USER_NAME>\My Project" -useHub
456|C:\Program Files\Unity\Hub\Editor\6000.0.0f1\Editor\Unity.exe -batchmode -projectPath "C:\Users\<USER_NAME>\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\<USER_NAME>\My Project" -useHub`) + "\r\n" +
"456|" + encode(`C:\Program Files\Unity\Hub\Editor\6000.0.0f1\Editor\Unity.exe -batchmode -projectPath "C:\Users\<USER_NAME>\Batch"`) + "\r\n"

processes := parseWindowsUnityProcesses(output)

Expand All @@ -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\<USER_NAME>\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\<USER_NAME>\` + 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{
Expand Down