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
28 changes: 28 additions & 0 deletions cli/dispatcher/internal/dispatcher/dispatcher_v2_detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"io"
"os"
"path/filepath"
"strings"
"testing"

"github.com/hatayama/unity-cli-loop/common/clicore"
clierrors "github.com/hatayama/unity-cli-loop/common/errors"
"github.com/hatayama/unity-cli-loop/dispatcher/internal/nativepath"
)
Expand Down Expand Up @@ -343,6 +345,32 @@ func TestRunDispatcherDelegatesResolvedV2PackageDespiteStalePin(t *testing.T) {
}
}

// Verifies launch stays in the native dispatcher for a V2 project while live commands still use the V2 CLI.
func TestRunDispatcherKeepsV2LaunchInNativeDispatcher(t *testing.T) {
projectRoot := createDispatcherUnityProject(t)
writeV2PackageManifest(t, projectRoot)
writeV2PackageCachePackageJSON(t, projectRoot, "abc123", "2.2.0")
t.Chdir(projectRoot)

deps := defaultDispatcherRunDeps()
deps.runV2CLI = func(context.Context, string, []string, io.Writer, io.Writer) (int, error) {
t.Fatal("V2 launch must not be delegated to the V2 CLI")
return 0, nil
}
deps.launch.findRunningUnityProcess = func(context.Context, string) (*clicore.UnityProcess, error) {
return nil, nil
}

var stdout bytes.Buffer
code := runDispatcherWithDeps(context.Background(), []string{"launch", "--quit", projectRoot}, &stdout, io.Discard, deps)
if code != 0 {
t.Fatalf("V2 launch quit exit code = %d", code)
}
if !strings.Contains(stdout.String(), `"Quit": true`) {
t.Fatalf("native V2 launch response missing quit result: %s", stdout.String())
}
}

func TestRunDispatcherForwardsResolvedV3PackageToPinnedRunner(t *testing.T) {
// Verifies a resolved V3 lock version keeps using the pinned project runner.
projectRoot := createDispatcherUnityProject(t)
Expand Down
4 changes: 4 additions & 0 deletions cli/dispatcher/internal/dispatcher/dispatcher_v2_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"runtime"

"github.com/hatayama/unity-cli-loop/common/clicore"
clierrors "github.com/hatayama/unity-cli-loop/common/errors"
)

Expand All @@ -19,6 +20,9 @@ func tryRunDetectedDispatcherV2Project(
stderr io.Writer,
deps dispatcherRunDeps,
) (bool, int) {
if len(args) > 0 && args[0] == clicore.LaunchCommandName {
return false, 0
}
v2Project, err := detectV2DispatcherProject(projectRoot)
if err != nil || !v2Project.IsV2 {
return false, 0
Expand Down
46 changes: 12 additions & 34 deletions cli/dispatcher/internal/dispatcher/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,15 @@ func runLaunchWithDeps(ctx context.Context, options launchOptions, startPath str
if !deleteLaunchRecoveryIfRequested(options, projectRoot, stderr) {
return 1
}
v2Project, _ := detectV2DispatcherProject(projectRoot)

runningProcess, handled, code := findLaunchRunningProcess(ctx, options, projectRoot, stdout, stderr, deps)
if handled {
return code
}

if runningProcess != nil {
if handled, code := handleExistingLaunchProcess(ctx, options, projectRoot, runningProcess, stdout, stderr, deps); handled {
if handled, code := handleExistingLaunchProcess(ctx, options, projectRoot, runningProcess, v2Project.IsV2, stdout, stderr, deps); handled {
return code
}
}
Expand All @@ -121,7 +122,7 @@ func runLaunchWithDeps(ctx context.Context, options launchOptions, startPath str
return writeLaunchQuitResponse(stdout, stderr, projectRoot, nil, launchNoProcessMessage)
}

return startUnityAndWaitForReadiness(ctx, options, projectRoot, runningProcess, stdout, stderr, deps)
return startUnityAndWaitForReadiness(ctx, options, projectRoot, runningProcess, v2Project.IsV2, stdout, stderr, deps)
}

func writeLaunchProjectSearch(stdout io.Writer, options launchOptions, startPath string) {
Expand Down Expand Up @@ -173,6 +174,7 @@ func handleExistingLaunchProcess(
options launchOptions,
projectRoot string,
runningProcess *unityprocess.UnityProcess,
isV2 bool,
stdout io.Writer,
stderr io.Writer,
deps launchDeps,
Expand All @@ -182,6 +184,9 @@ func handleExistingLaunchProcess(
clierrors.WriteClassifiedError(stderr, launchEditorVersionRequiresRestartError(options.editorVersion), clierrors.ErrorContext{ProjectRoot: projectRoot, Command: clicore.LaunchCommandName})
return true, 1
}
if isV2 {
return true, writeExistingV2LaunchOpenedResponse(stdout, stderr, projectRoot, runningProcess.Pid)
}
return true, waitForExistingLaunchReadiness(ctx, projectRoot, runningProcess.Pid, stdout, stderr, deps)
}
if err := deps.killUnityProcess(runningProcess.Pid); err != nil {
Expand Down Expand Up @@ -228,6 +233,7 @@ func startUnityAndWaitForReadiness(
options launchOptions,
projectRoot string,
runningProcess *unityprocess.UnityProcess,
isV2 bool,
stdout io.Writer,
stderr io.Writer,
deps launchDeps,
Expand Down Expand Up @@ -265,6 +271,7 @@ func startUnityAndWaitForReadiness(
clicore.WriteFormat(stdout, "Detected Unity version: %s\n", unityVersion)
clicore.WriteLine(stdout, "Unity Hub launch options: none")

launchStartedAt := deps.now()
command := newUnityLaunchCommand(unityPath, buildUnityLaunchArgs(projectRoot, options))
if err := command.Start(); err != nil {
clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{ProjectRoot: projectRoot, Command: clicore.LaunchCommandName})
Expand All @@ -275,6 +282,9 @@ func startUnityAndWaitForReadiness(
clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{ProjectRoot: projectRoot, Command: clicore.LaunchCommandName})
return 1
}
if isV2 {
return waitForV2ProjectOpened(ctx, projectRoot, runningProcess, currentPid, stdout, stderr, launchStartedAt, deps)
}
if err := deps.waitForUnityStartupMarker(ctx, unityLockfilePath(projectRoot), launchLockfilePoll, launchLockfileTimeout); err != nil {
clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{ProjectRoot: projectRoot, Command: clicore.LaunchCommandName})
return 1
Expand Down Expand Up @@ -432,38 +442,6 @@ func unityExecutableCandidates(version string) []string {
}
}

func windowsUnityExecutableCandidates(version string) []string {
candidates := []string{}
for _, base := range []string{
os.Getenv("ProgramFiles"),
os.Getenv("ProgramFiles(x86)"),
os.Getenv("LOCALAPPDATA"),
`C:\Program Files`,
} {
if base == "" {
continue
}
candidates = append(candidates, filepath.Join(base, "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"))
}
return candidates
}

func readUnityEditorVersion(projectRoot string) (string, error) {
content, err := os.ReadFile(filepath.Join(projectRoot, projectVersionFilePath))
if err != nil {
return "", err
}
matches := editorVersionPattern.FindStringSubmatch(string(content))
if len(matches) != 2 {
return "", fmt.Errorf("unity editor version not found in %s", projectVersionFilePath)
}
version := strings.TrimSpace(matches[1])
if version == "" {
return "", fmt.Errorf("unity editor version is empty in %s", projectVersionFilePath)
}
return version, nil
}

func killUnityProcess(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions cli/dispatcher/internal/dispatcher/launch_deps.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,28 @@ import (
)

type launchDeps struct {
now func() time.Time
findRunningUnityProcess func(context.Context, string) (*unityprocess.UnityProcess, error)
focusUnityProcess func(context.Context, int) error
killUnityProcess func(int) error
resolveUnityExecutablePath func(string) (string, error)
waitForUnityProcessExit func(context.Context, string, int, time.Duration, time.Duration) error
waitForUnityStartupMarker func(context.Context, string, time.Duration, time.Duration) error
waitForFreshUnityLockfile func(context.Context, string, time.Time, time.Duration, time.Duration) error
waitForToolReadiness func(context.Context, string, time.Duration) error
probeProjectIpcFallback func(context.Context, string) error
}

func defaultLaunchDeps() launchDeps {
return launchDeps{
now: time.Now,
findRunningUnityProcess: unityprocess.FindRunningUnityProcess,
focusUnityProcess: unityprocess.FocusUnityProcess,
killUnityProcess: killUnityProcess,
resolveUnityExecutablePath: resolveUnityExecutablePath,
waitForUnityProcessExit: waitForUnityProcessExit,
waitForUnityStartupMarker: waitForUnityStartupMarkerOrTimeout,
waitForFreshUnityLockfile: waitForFreshUnityLockfile,
waitForToolReadiness: clicore.WaitForToolReadinessWithTimeout,
probeProjectIpcFallback: clicore.ProbeToolReadinessSequence,
}
Expand Down
30 changes: 30 additions & 0 deletions cli/dispatcher/internal/dispatcher/launch_ready.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ func writeExistingLaunchReadyResponse(stdout io.Writer, stderr io.Writer, projec
})
}

func writeExistingV2LaunchOpenedResponse(stdout io.Writer, stderr io.Writer, projectRoot string, currentPid int) int {
return writeLaunchResponse(stdout, stderr, launchReadyResponse{
Success: true,
Ready: true,
AlreadyRunning: true,
CurrentProcessId: &currentPid,
ProjectRoot: projectRoot,
Message: "Unity is already running for this V2 project. V2 server readiness was not checked.",
})
}

func writeLaunchedReadyResponse(
stdout io.Writer,
stderr io.Writer,
Expand All @@ -91,6 +102,25 @@ func writeLaunchedReadyResponse(
})
}

func writeLaunchedV2ProjectOpenedResponse(
stdout io.Writer,
stderr io.Writer,
projectRoot string,
previousPid *int,
currentPid int,
) int {
return writeLaunchResponse(stdout, stderr, launchReadyResponse{
Success: true,
Ready: true,
Launched: true,
Restarted: previousPid != nil,
PreviousProcessId: previousPid,
CurrentProcessId: &currentPid,
ProjectRoot: projectRoot,
Message: "Unity started and opened the V2 project. V2 server readiness was not checked.",
})
}

func writeLaunchQuitResponse(
stdout io.Writer,
stderr io.Writer,
Expand Down
29 changes: 29 additions & 0 deletions cli/dispatcher/internal/dispatcher/launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -307,6 +308,34 @@ func TestRunLaunchWritesReadyResponseAfterToolReadiness(t *testing.T) {
}
}

// Verifies a V2 launch confirms only that Unity opened the project and never waits for the V3 named pipe.
func TestRunLaunchForV2ProjectWaitsForFreshLockfileWithoutServerProbe(t *testing.T) {
projectRoot := createLaunchTestProject(t)
writeV2PackageManifest(t, projectRoot)
writeV2PackageCachePackageJSON(t, projectRoot, "abc123", "2.2.0")
deps := defaultLaunchDeps()
deps.findRunningUnityProcess = func(context.Context, string) (*clicore.UnityProcess, error) { return nil, nil }
deps.resolveUnityExecutablePath = func(string) (string, error) { return fakeUnityExecutablePath(t), nil }
deps.waitForFreshUnityLockfile = func(context.Context, string, time.Time, time.Duration, time.Duration) error { return nil }
deps.waitForToolReadiness = func(context.Context, string, time.Duration) error {
t.Fatal("V2 launch must not wait for the V3 server")
return nil
}

var stdout bytes.Buffer
code := runLaunchWithDeps(context.Background(), launchOptions{projectPath: projectRoot, editorVersion: "6000.0.0f1"}, projectRoot, &stdout, io.Discard, deps)
if code != 0 {
t.Fatalf("V2 launch exit code = %d", code)
}
response := decodeLaunchResponseFromOutput(t, stdout.String())
if !response.Success || !response.Ready || response.ServerReady || response.ProjectIpcReady {
t.Fatalf("V2 launch readiness flags = %#v", response)
}
if !strings.Contains(response.Message, "V2 server readiness was not checked") {
t.Fatalf("V2 launch message = %q", response.Message)
}
}

func TestWaitForLaunchReadinessUsesLaunchTimeout(t *testing.T) {
// Verifies launch gets a longer startup window without changing shared readiness defaults.
deps := defaultLaunchDeps()
Expand Down
34 changes: 34 additions & 0 deletions cli/dispatcher/internal/dispatcher/launch_unity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package dispatcher

import (
"fmt"
"os"
"path/filepath"
"strings"
)

func windowsUnityExecutableCandidates(version string) []string {
candidates := []string{}
for _, base := range []string{os.Getenv("ProgramFiles"), os.Getenv("ProgramFiles(x86)"), os.Getenv("LOCALAPPDATA"), `C:\Program Files`} {
if base != "" {
candidates = append(candidates, filepath.Join(base, "Unity", "Hub", "Editor", version, "Editor", "Unity.exe"))
}
}
return candidates
}

func readUnityEditorVersion(projectRoot string) (string, error) {
content, err := os.ReadFile(filepath.Join(projectRoot, projectVersionFilePath))
if err != nil {
return "", err
}
matches := editorVersionPattern.FindStringSubmatch(string(content))
if len(matches) != 2 {
return "", fmt.Errorf("unity editor version not found in %s", projectVersionFilePath)
}
version := strings.TrimSpace(matches[1])
if version == "" {
return "", fmt.Errorf("unity editor version is empty in %s", projectVersionFilePath)
}
return version, nil
}
Loading
Loading