Skip to content

Implement managed emulator functionality and enhance profile management - #5

Merged
B87 merged 6 commits into
mainfrom
managed-pubsub-emulator
Jan 17, 2026
Merged

B87 merged 6 commits into
mainfrom
managed-pubsub-emulator

Conversation

@B87

@B87 B87 commented Jan 16, 2026 •

Copy link
Copy Markdown
Owner
  • Added support for managed Docker emulator in the application, including a new EmulatorManager for handling emulator lifecycle.
  • Updated the App struct to include an emulator manager and methods for starting, stopping, and checking the status of the emulator.
  • Enhanced ConnectionProfile to support emulator modes (off, external, managed) and added validation for managed emulator settings.
  • Improved ProfileDialog to allow configuration of emulator settings, including port, auto-start, and auto-stop options.
  • Refactored connection methods to integrate emulator management, ensuring proper handling during connection and disconnection processes.
  • Added tests for emulator manager functionality and connection profile validation.

Summary by CodeRabbit

  • New Features

    • Managed emulator mode: per-profile Docker-managed lifecycle with start/stop, readiness checks, and runtime status surfaced in UI and connection status.
    • Tri-state emulator UI (Off | External | Managed) with advanced managed settings (port, image, bind address, auto-start/stop) and migration of existing profiles.
    • App actions to check Docker availability and query/manage per-profile emulator status.
  • Tests

    • Extensive unit and integration tests for emulator manager, lifecycle, config validation, and status reporting.
  • Documentation

    • Added TypeScript best-practices guidance (nullish coalescing vs logical OR).

✏️ Tip: You can customize this high-level summary in your review settings.

- Added support for managed Docker emulator in the application, including a new EmulatorManager for handling emulator lifecycle.
- Updated the App struct to include an emulator manager and methods for starting, stopping, and checking the status of the emulator.
- Enhanced ConnectionProfile to support emulator modes (off, external, managed) and added validation for managed emulator settings.
- Improved ProfileDialog to allow configuration of emulator settings, including port, auto-start, and auto-stop options.
- Refactored connection methods to integrate emulator management, ensuring proper handling during connection and disconnection processes.
- Added tests for emulator manager functionality and connection profile validation.
@coderabbitai

coderabbitai Bot commented Jan 16, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a Docker-backed managed Pub/Sub emulator subsystem, tri-state emulator config (off/external/managed) across backend and frontend, lifecycle APIs to start/stop/check emulators, per-profile emulator status tracking, and integrates emulator readiness into connection and resource initialization flows.

Changes

Cohort / File(s) Summary
App core & lifecycle
app.go
Initializes emulator manager; integrates managed-emulator startup into connect flow; resolves effective emulator host; exposes CheckDockerAvailable, GetEmulatorStatus, StartManagedEmulator, StopManagedEmulator; stops managed emulator on disconnect when configured.
Emulator manager
internal/emulator/manager.go, internal/emulator/manager_test.go
New Docker-based Manager with Start/Stop/StopAll, per-profile EmulatorInfo/status, container orchestration, port checks, readiness polling, log streaming, cancellation contexts; extensive unit tests and benchmarks. Review concurrency, container lifecycle, port probing, and error/status transitions.
Connection handling & resources
internal/app/connection.go, internal/app/resources.go
ConnectionStatus extended with EmulatorMode and ManagedEmulatorRunning; ConnectionHandler gains Set/GetEmulatorMode; ResourceHandler adds optional emulator-check hook via SetEmulatorCheckFunc.
Models & validation
internal/models/connection.go, internal/models/connection_test.go
Adds EmulatorMode type/constants, ManagedEmulatorConfig and defaults, new profile fields (EmulatorHost, EmulatorMode, ManagedEmulator), validation and migration helpers (GetEffectiveEmulatorMode, GetEffectiveEmulatorHost, IsEmulatorEnabled), and tests. Review validation rules, migration behavior, and bind-address handling.
Frontend UI & types
frontend/src/components/Settings/ProfileDialog.tsx, frontend/src/types/index.ts
ProfileDialog switches to Off/External/Managed modes with managed UI (port, auto-start/stop, advanced settings); types updated (EmulatorMode, ManagedEmulatorConfig, EmulatorStatus, EmulatorStatusType); validation and payload shaping adjusted.
Wails bindings & models
frontend/wailsjs/go/main/App.d.ts, frontend/wailsjs/go/main/App.js, frontend/wailsjs/go/models.ts
Generated bindings exposing CheckDockerAvailable, GetEmulatorStatus, StartManagedEmulator, StopManagedEmulator; added EmulatorStatus and ManagedEmulatorConfig model classes; extended ConnectionProfile/ConnectionStatus models.
Docs / misc
.cursor/rules/react-tailwind.mdc
Added TypeScript best-practices block (duplicated insertion) — documentation only.

Sequence Diagram

sequenceDiagram
    participant User as Frontend User
    participant Frontend as Frontend
    participant App as App (Backend)
    participant Manager as Emulator Manager
    participant Docker as Docker Daemon
    participant Emulator as Pub/Sub Emulator
    participant Handler as Connection Handler

    User->>Frontend: Connect using profile (mode=managed)
    Frontend->>App: connectWithProfile(profileID)
    App->>Manager: Start(profileID, managedConfig)
    Manager->>Docker: Check Docker availability
    Docker-->>Manager: Docker ready
    Manager->>Docker: Run container (image, port, bind)
    Docker->>Emulator: Launch emulator
    Manager->>Manager: waitForEmulator (TCP probe)
    Emulator-->>Manager: Port reachable (ready)
    Manager-->>App: Start returns (running)
    App->>Handler: Establish Pub/Sub connection using effective host
    Handler->>Emulator: Open connection to host:port
    Emulator-->>Handler: Connection established
    User->>Frontend: Disconnect
    Frontend->>App: disconnect
    App->>Manager: Stop(profileID) (if autoStop)
    Manager->>Docker: Stop & remove container
    Docker->>Emulator: Shutdown
    Manager-->>App: Stop returns
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

"I nibbled on a tiny log,
spun up containers, danced a jog.
Ports aligned and configs bright,
profiles hum through day and night.
— a rabbit, happily dev-ops-ing 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the primary changes: implementing managed emulator functionality and enhancing profile management to support emulator configurations across multiple modes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jan 16, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements managed Docker emulator functionality, allowing users to automatically start/stop Pub/Sub emulators per connection profile. The implementation follows the existing emulator guide patterns and extends the connection profile model with three emulator modes: off, external, and managed.

Key Changes

  • Emulator Manager (internal/emulator/manager.go): New package handles Docker container lifecycle with proper concurrency control, status tracking, and graceful shutdown. Includes smart container reuse detection to prevent port conflicts when containers persist from previous sessions.

  • Connection Profile Enhancement (internal/models/connection.go): Extended with EmulatorMode enum and ManagedEmulatorConfig struct. Includes backward compatibility migration logic for existing profiles using emulatorHost field. Validation ensures port ranges (1-65535) and bind addresses are correct.

  • App Integration (app.go): Manages emulator lifecycle tied to profile connection/disconnection. Respects autoStart and autoStop flags. Properly cleans up emulators on connection failures to prevent orphaned containers.

  • Frontend UI (ProfileDialog.tsx): Three-mode button selector (Off/External/Managed) with collapsible advanced settings for Docker image, data directory, and bind address configuration. Validation prevents invalid configurations.

  • Resource Handler Improvement (internal/app/resources.go): Replaced direct env var check with callback pattern for better emulator detection, supporting both external and managed modes.

Testing

Comprehensive test coverage includes:

  • Unit tests for emulator manager (status, lifecycle, error cases)
  • Connection profile validation tests (all emulator modes, invalid ports, bind addresses)
  • Emulator mode migration logic tests
  • GetEffectiveEmulatorHost tests covering all scenarios

Architecture Alignment

The implementation adheres to the emulator guide (.cursor/rules/pubsub/emulator.mdc) by:

  • Never modifying PUBSUB_EMULATOR_HOST environment variable
  • Passing emulator host explicitly through function parameters
  • Using per-profile configuration (not global)
  • Supporting existing external emulator workflows

No breaking changes for existing users - backward compatibility maintained through migration logic.

Confidence Score: 4/5

  • This PR is safe to merge with good architecture and testing
  • Score reflects solid implementation with proper concurrency control, comprehensive testing, and backward compatibility. The emulator manager handles Docker lifecycle correctly with cleanup on failures. Minor points: managed emulator functionality requires Docker installation (runtime dependency) and real-world Docker interaction testing would strengthen confidence further.
  • No files require special attention - implementation is clean and well-tested

Important Files Changed

Filename Overview
internal/emulator/manager.go New emulator manager handles Docker lifecycle with proper concurrency and error handling
internal/models/connection.go Connection profile extended with emulator modes and thorough validation logic
app.go App integrates emulator manager with profile lifecycle and auto-start/stop support
frontend/src/components/Settings/ProfileDialog.tsx Profile dialog enhanced with comprehensive emulator mode UI and validation

Sequence Diagram

sequenceDiagram
    participant User
    participant ProfileDialog
    participant App
    participant EmulatorManager
    participant Docker
    participant ConnectionHandler
    participant GCPClient

    User->>ProfileDialog: Configure managed emulator mode
    ProfileDialog->>ProfileDialog: Set port, autoStart, autoStop
    User->>ProfileDialog: Save profile
    ProfileDialog->>App: SaveProfile(profile)
    
    User->>App: SwitchProfile(profileID)
    App->>App: connectWithProfile(profile)
    
    alt Managed Emulator Mode
        App->>EmulatorManager: CheckDocker()
        EmulatorManager->>Docker: docker info
        Docker-->>EmulatorManager: OK
        
        alt AutoStart enabled
            App->>EmulatorManager: Start(profileID, config)
            EmulatorManager->>EmulatorManager: Check existing container
            
            alt Container exists and running
                EmulatorManager-->>App: Reuse existing
            else New container needed
                EmulatorManager->>EmulatorManager: Check port available
                EmulatorManager->>Docker: docker run pubsub-gui-emulator-{profileID}
                Docker-->>EmulatorManager: Container started
                EmulatorManager->>EmulatorManager: Wait for emulator ready (TCP check)
                EmulatorManager-->>App: Emulator running
            end
            
            App->>App: Wait for emulator ready
        end
        
        App->>App: Get effective emulator host
        App->>ConnectionHandler: ConnectWithADC(projectID, emulatorHost)
    else External Emulator Mode
        App->>App: Get emulator host from profile
        App->>ConnectionHandler: ConnectWithADC(projectID, emulatorHost)
    else Emulator Off
        App->>ConnectionHandler: ConnectWithADC(projectID, "")
    end
    
    ConnectionHandler->>GCPClient: NewClient(ctx, projectID, endpoint)
    GCPClient-->>ConnectionHandler: Connected
    ConnectionHandler-->>App: Connection established
    App-->>User: Connected
    
    User->>App: Disconnect()
    App->>App: stopManagedEmulatorIfNeeded()
    
    alt Managed mode with AutoStop
        App->>EmulatorManager: Stop(profileID)
        EmulatorManager->>EmulatorManager: Cancel context
        EmulatorManager->>Docker: docker stop pubsub-gui-emulator-{profileID}
        Docker-->>EmulatorManager: Stopped
        EmulatorManager-->>App: Emulator stopped
    end
    
    App->>ConnectionHandler: ClearEmulatorHost()
    App->>GCPClient: Close()
    App-->>User: Disconnected
Loading

@greptile-apps

greptile-apps Bot commented Jan 16, 2026

Copy link
Copy Markdown

Greptile's behavior is changing!

From now on, if a review finishes with no comments, we will not post an additional "statistics" comment to confirm that our review found nothing to comment on. However, you can confirm that we reviewed your changes in the status check section.

This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/Settings/ProfileDialog.tsx (1)

48-77: Reset the advanced toggle when loading a new profile.
showAdvanced persists across profile changes, so a newly opened dialog can unexpectedly show advanced fields.

🐛 Suggested fix
   useEffect(() => {
     if (profile) {
       setFormData({
         name: profile.name,
         projectId: profile.projectId,
         authMethod: profile.authMethod,
         serviceAccountPath: profile.serviceAccountPath || '',
         oauthClientPath: profile.oauthClientPath || '',
         emulatorHost: profile.emulatorHost || 'localhost:8085',
         isDefault: profile.isDefault,
       });
       setEmulatorMode(getEffectiveEmulatorMode(profile));
       setManagedConfig(profile.managedEmulator || { ...defaultManagedConfig });
     } else {
       setFormData({
         name: '',
         projectId: '',
         authMethod: 'ADC',
         serviceAccountPath: '',
         oauthClientPath: '',
         emulatorHost: 'localhost:8085',
         isDefault: false,
       });
       setEmulatorMode('off');
       setManagedConfig({ ...defaultManagedConfig });
     }
+    setShowAdvanced(false);
     setError('');
   }, [profile]);
🤖 Fix all issues with AI agents
In `@internal/emulator/manager.go`:
- Around line 85-121: The Start method currently dereferences config without
checking for nil, which can panic; in Manager.Start add an initial nil check for
the config parameter (before reading config.Port, config.Image,
config.BindAddress) and return a clear error (or default to a safe config) if
config is nil; update any uses of info (EmulatorInfo) and
containerName(profileID) initialization to occur after the nil check so you
never dereference config when it is nil.
- Around line 125-138: The code currently reuses any running container in
Start() after isContainerRunning(info.ContainerName) returns true without
validating the container's current configuration; change Start() to inspect the
running container's actual configuration (image, exposed/host ports, bind
addresses) for info.ContainerName (via the Docker client or existing inspect
helper), compare those values to the requested values stored in
m.emulators[profileID] (e.g., info.Image, info.Port, info.BindAddr), and if any
mismatch is found stop/remove and recreate the container with the new config (or
restart with corrected settings) instead of immediately marking info.Status =
StatusRunning; ensure error handling updates logs and m.emulators[profileID]
status consistently.
- Around line 412-424: The isContainerRunning function currently swallows all
errors from exec.CommandContext and returns (false, nil); instead, capture the
error from cmd.Output(), and if it's an *exec.ExitError inspect its Stderr for
the "No such" / "No such object" text (the expected "container not found" case)
and return (false, nil) only then; for any other error (non-ExitError, or
ExitError whose stderr does not indicate missing container, or context deadline
errors) return (false, err) so callers can handle real Docker/permission/daemon
failures. Reference symbols: isContainerRunning, exec.CommandContext,
cmd.Output, *exec.ExitError, Stderr.

In `@internal/models/connection_test.go`:
- Around line 1-6: The file has formatting issues; run the Go formatter and
commit the changes: execute `go fmt ./...` (or `gofmt -w
internal/models/connection_test.go`) to reformat the package models and update
connection_test.go so it matches gofmt style rules; then stage and push the
formatted file.

In `@internal/models/connection.go`:
- Around line 116-120: The current validation in the managed emulator block
falsely rejects port 0 even though GetEffectiveEmulatorHost treats 0 as the
default (8085); update the check in the validation for cp.ManagedEmulator.Port
inside the EmulatorModeManaged branch to accept 0 or any value between 1 and
65535 (i.e., allow port == 0 || (port >= 1 && port <= 65535)), keeping the same
error message for other out-of-range values; locate the validation near the
EmulatorModeManaged / ManagedEmulator checks in connection.go and adjust the
conditional accordingly.
- Around line 133-140: GetEffectiveEmulatorMode currently treats any non-empty
EmulatorHost (including whitespace-only) as external; update the method on
ConnectionProfile to trim whitespace from cp.EmulatorHost (use
strings.TrimSpace) before testing it and only return EmulatorModeExternal when
the trimmed host is non-empty, keeping the existing cp.EmulatorMode check
intact; reference the GetEffectiveEmulatorMode method, the EmulatorHost field,
and the EmulatorModeExternal constant when making the change.
🧹 Nitpick comments (2)
app.go (1)

1096-1107: Surface errors from GetEmulatorStatus.
Returning (EmulatorStatus, error) lets callers handle empty/invalid profile IDs and keeps the App API consistent.

♻️ Suggested adjustment
-func (a *App) GetEmulatorStatus(profileID string) EmulatorStatus {
-	info := a.emulatorManager.GetStatus(profileID)
-	return EmulatorStatus{
-		ProfileID:     info.ProfileID,
-		ContainerName: info.ContainerName,
-		Host:          info.Host,
-		Port:          info.Port,
-		Status:        string(info.Status),
-		Error:         info.Error,
-	}
-}
+func (a *App) GetEmulatorStatus(profileID string) (EmulatorStatus, error) {
+	if profileID == "" {
+		return EmulatorStatus{}, fmt.Errorf("profile ID cannot be empty")
+	}
+	info := a.emulatorManager.GetStatus(profileID)
+	return EmulatorStatus{
+		ProfileID:     info.ProfileID,
+		ContainerName: info.ContainerName,
+		Host:          info.Host,
+		Port:          info.Port,
+		Status:        string(info.Status),
+		Error:         info.Error,
+	}, nil
+}

As per coding guidelines, App methods exposed to the frontend should return errors.

internal/models/connection.go (1)

144-187: Prefer strconv.Itoa over a custom itoa.
The helper is non-idiomatic and adds extra maintenance without benefit.

♻️ Proposed refactor
 import (
 	"errors"
 	"strings"
 	"time"
+	"strconv"
 )
@@
-		return bindAddr + ":" + itoa(port)
+		return bindAddr + ":" + strconv.Itoa(port)
@@
-// itoa converts int to string (simple helper to avoid importing strconv)
-func itoa(i int) string {
-	if i == 0 {
-		return "0"
-	}
-	if i < 0 {
-		return "-" + itoa(-i)
-	}
-	var digits []byte
-	for i > 0 {
-		digits = append([]byte{byte('0' + i%10)}, digits...)
-		i /= 10
-	}
-	return string(digits)
-}

Comment thread internal/emulator/manager.go Outdated
Comment thread internal/emulator/manager.go Outdated
Comment thread internal/emulator/manager.go
Comment thread internal/models/connection_test.go
Comment thread internal/models/connection.go
Comment thread internal/models/connection.go
B87 and others added 2 commits January 16, 2026 22:07
- Extract helper functions to reduce Start method complexity (95 -> 50 lines)
  - resolveConfig: applies defaults to emulator configuration
  - buildDockerArgs: builds docker run command arguments
  - tryReuseContainer: checks if existing container can be reused
  - parsePortMapping: parses Docker port mapping output
  - normalizeBindAddr: normalizes addresses with defaults

- Add container configuration validation before reusing existing containers
  - Validates image, port, and bind address match requested config
  - Stops and recreates container if configuration mismatches

- Improve error handling in isContainerRunning
  - Distinguish between "container not found" and actual errors
  - Properly handle context deadline exceeded errors

- Fix port validation to allow port 0 (defaults to 8085)
- Fix whitespace handling in emulator host migration logic
- Add comprehensive tests for all new helper functions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/emulator/manager.go`:
- Around line 336-374: The Stop method currently ignores errors from
isContainerRunning which can hide Docker/unreachable errors and incorrectly mark
an emulator stopped; update Stop to check the error returned by
isContainerRunning(containerName) after the sleep and if err != nil log the
error (using logger.Error) and return the error instead of proceeding to set
StatusStopped; only set info.Status = StatusStopped and delete
m.cancels[profileID] after confirming isContainerRunning returned no error and
the container is not running (or after a successful force stop via
m.stopContainer), keeping references to the Stop function, isContainerRunning,
containerName, m.stopContainer, and logger to locate the change.

Comment thread internal/emulator/manager.go
B87 added 2 commits January 17, 2026 01:18
…Dialog to use nullish coalescing operator (`??`) for default values. This change improves handling of falsy values and ensures safer defaults across the component's state management.
- Added error handling when checking if the container is running, logging the error if the check fails.
- Enhanced comments for clarity on the container stopping process.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/Settings/ProfileDialog.tsx (1)

48-78: Reset “Show advanced” when switching profiles.

showAdvanced persists across profile changes, so advanced settings can remain open when opening a different profile.

🩹 Proposed fix
   useEffect(() => {
     if (profile) {
       setFormData({
         name: profile.name,
         projectId: profile.projectId,
         authMethod: profile.authMethod,
         serviceAccountPath: profile.serviceAccountPath ?? '',
         oauthClientPath: profile.oauthClientPath ?? '',
         emulatorHost: profile.emulatorHost ?? 'localhost:8085',
         isDefault: profile.isDefault,
       });
       setEmulatorMode(getEffectiveEmulatorMode(profile));
       setManagedConfig(profile.managedEmulator ?? { ...defaultManagedConfig });
     } else {
       setFormData({
         name: '',
         projectId: '',
         authMethod: 'ADC',
         serviceAccountPath: '',
         oauthClientPath: '',
         emulatorHost: 'localhost:8085',
         isDefault: false,
       });
       setEmulatorMode('off');
       setManagedConfig({ ...defaultManagedConfig });
     }
+    setShowAdvanced(false);
     setError('');
   }, [profile]);
🤖 Fix all issues with AI agents
In @.cursor/rules/react-tailwind.mdc:
- Around line 113-182: Remove the duplicate "TypeScript Best Practices" section
titled "Nullish Coalescing (`??`) vs Logical OR (`||`)" so only one copy
remains; locate the repeated header and its entire explanatory block (the
examples and anti-patterns under that heading) and delete the redundant
occurrence, leaving the first (or most complete) instance intact, then scan the
rest of the document to verify no other identical section remains.
🧹 Nitpick comments (2)
frontend/src/components/Settings/ProfileDialog.tsx (2)

45-47: Merge managed config with defaults to avoid partial data.

If a stored profile has a partial managedEmulator, missing fields propagate into the payload. Consider merging with defaults on load.

♻️ Proposed fix
-  const [managedConfig, setManagedConfig] = useState<ManagedEmulatorConfig>(
-    profile?.managedEmulator ?? { ...defaultManagedConfig }
-  );
+  const [managedConfig, setManagedConfig] = useState<ManagedEmulatorConfig>(
+    { ...defaultManagedConfig, ...(profile?.managedEmulator ?? {}) }
+  );
-      setManagedConfig(profile.managedEmulator ?? { ...defaultManagedConfig });
+      setManagedConfig({ ...defaultManagedConfig, ...(profile.managedEmulator ?? {}) });

Also applies to: 64-65


462-470: Optionally disable Save when managed port is invalid.

Keeps the button state aligned with validation, reducing round-trip errors.

♻️ Proposed tweak
           disabled={
             saving ||
             !formData.name.trim() ||
             !formData.projectId.trim() ||
             (formData.authMethod === 'ServiceAccount' && !formData.serviceAccountPath.trim()) ||
             (formData.authMethod === 'OAuth' && !formData.oauthClientPath.trim()) ||
-            (emulatorMode === 'external' && !formData.emulatorHost.trim())
+            (emulatorMode === 'external' && !formData.emulatorHost.trim()) ||
+            (emulatorMode === 'managed' && (managedConfig.port < 1 || managedConfig.port > 65535))
           }

Comment thread .cursor/rules/react-tailwind.mdc

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/emulator/manager.go`:
- Around line 369-378: The code unconditionally sets info.Status = StatusStopped
after calling m.stopContainer(containerName) which swallows errors; change
m.stopContainer to return an error (or capture its error) and, after calling it,
verify the container is stopped (e.g., via an inspect/IsRunning helper) or at
minimum check the returned error; only set info.Status = StatusStopped and
delete m.cancels[profileID] when the stop/verification succeeds, otherwise log a
warning/error with containerName and keep the status unchanged (ensure you
acquire m.mu around status changes and cancels updates); reference
m.stopContainer, info.Status, StatusStopped, m.cancels, containerName, and
logger in the fix.
🧹 Nitpick comments (4)
internal/emulator/manager.go (4)

219-221: Consider moving the sleep into the waitForEmulator goroutine.

The 500ms sleep at line 220 blocks the caller. If Start is invoked from the UI/main thread, this introduces noticeable latency. Moving the initial delay into waitForEmulator would make Start return immediately while the emulator continues starting in the background.

♻️ Suggested change
 	go m.runContainer(ctx, profileID, args)
-	time.Sleep(500 * time.Millisecond)
-	go m.waitForEmulator(ctx, profileID, fmt.Sprintf("127.0.0.1:%d", cfg.Port))
+	go func() {
+		time.Sleep(500 * time.Millisecond)
+		m.waitForEmulator(ctx, profileID, fmt.Sprintf("127.0.0.1:%d", cfg.Port))
+	}()

249-273: Consider checking scanner.Err() after the loop.

If the emulator produces an unusually long log line (>64KB default buffer), scanner.Scan() returns false with an error. Currently this is silently ignored. While unlikely for emulator logs, checking the error improves robustness.

♻️ Suggested change (stdout example)
 	go func() {
 		scanner := bufio.NewScanner(stdout)
 		for scanner.Scan() {
 			line := scanner.Text()
 			logger.Info(line,
 				"source", "emulator",
 				"profileId", profileID,
 				"stream", "stdout",
 			)
 		}
+		if err := scanner.Err(); err != nil {
+			logger.Warn("Error reading emulator stdout", "profileId", profileID, "error", err)
+		}
 	}()

523-523: Misleading use of normalizeBindAddr for image normalization.

normalizeBindAddr is semantically named for bind addresses but is used here for image name defaulting. Consider renaming to a generic defaultIfEmpty or creating a separate helper for clarity.

♻️ Suggested refactor
+// defaultIfEmpty returns defaultVal if val is empty, otherwise returns val.
+func defaultIfEmpty(val, defaultVal string) string {
+	if val == "" {
+		return defaultVal
+	}
+	return val
+}

 // In validateContainerConfig:
-	normalizedExpectedImage := normalizeBindAddr(expectedImage, "google/cloud-sdk:emulators")
+	normalizedExpectedImage := defaultIfEmpty(expectedImage, "google/cloud-sdk:emulators")

556-567: Consider documenting why context.Background() is used instead of m.ctx.

Unlike other methods that use m.ctx, stopContainer uses context.Background(). This is likely intentional (to ensure cleanup during app shutdown when m.ctx is cancelled), but a brief comment would clarify this design decision.

📝 Suggested documentation
 // stopContainer stops a container
 func (m *Manager) stopContainer(name string) {
+	// Use context.Background() instead of m.ctx to ensure cleanup
+	// completes even during app shutdown when m.ctx may be cancelled
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

Comment on lines +369 to +378
// Force stop if still running
if running {
logger.Info("Force stopping container", "container", containerName)
m.stopContainer(containerName)
}

m.mu.Lock()
info.Status = StatusStopped
delete(m.cancels, profileID)
m.mu.Unlock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Container may remain running if stopContainer fails silently.

stopContainer (line 372) ignores all errors, but line 376 unconditionally sets status to StatusStopped. If Docker fails to stop the container, the UI will report it as stopped while it continues running. Consider verifying the container stopped or at least logging warnings.

🛠️ Suggested improvement
 	// Force stop if still running
 	if running {
 		logger.Info("Force stopping container", "container", containerName)
 		m.stopContainer(containerName)
+		// Verify container actually stopped
+		stillRunning, _ := m.isContainerRunning(containerName)
+		if stillRunning {
+			logger.Warn("Container may still be running after stop attempt", "container", containerName)
+		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Force stop if still running
if running {
logger.Info("Force stopping container", "container", containerName)
m.stopContainer(containerName)
}
m.mu.Lock()
info.Status = StatusStopped
delete(m.cancels, profileID)
m.mu.Unlock()
// Force stop if still running
if running {
logger.Info("Force stopping container", "container", containerName)
m.stopContainer(containerName)
// Verify container actually stopped
stillRunning, _ := m.isContainerRunning(containerName)
if stillRunning {
logger.Warn("Container may still be running after stop attempt", "container", containerName)
}
}
m.mu.Lock()
info.Status = StatusStopped
delete(m.cancels, profileID)
m.mu.Unlock()
🤖 Prompt for AI Agents
In `@internal/emulator/manager.go` around lines 369 - 378, The code
unconditionally sets info.Status = StatusStopped after calling
m.stopContainer(containerName) which swallows errors; change m.stopContainer to
return an error (or capture its error) and, after calling it, verify the
container is stopped (e.g., via an inspect/IsRunning helper) or at minimum check
the returned error; only set info.Status = StatusStopped and delete
m.cancels[profileID] when the stop/verification succeeds, otherwise log a
warning/error with containerName and keep the status unchanged (ensure you
acquire m.mu around status changes and cancels updates); reference
m.stopContainer, info.Status, StatusStopped, m.cancels, containerName, and
logger in the fix.

@B87
B87 merged commit df44653 into main Jan 17, 2026
5 of 6 checks passed
@B87
B87 deleted the managed-pubsub-emulator branch January 17, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant