Manage Background Processes: Explicitly Close Projects

by Alex Johnson 55 views

In today's fast-paced development world, developers often juggle multiple projects simultaneously. Each project may involve several terminals, development servers, and other background processes. While multitasking is essential, the accumulation of these background processes can lead to significant resource exhaustion, impacting system performance and overall productivity. This article explores the need for an explicit "Close Project" action to effectively manage background processes, prevent resource leaks, and ensure a smoother development experience. So, let's dive deep into why explicitly closing projects is crucial for managing background processes efficiently.

The Problem: Accumulation of Background Processes

Imagine a scenario where a developer is working on multiple projects throughout the day. Each project may have its own set of terminals and development servers running in the background. When switching between projects, the processes associated with the previous project often remain active, consuming valuable system resources. Over time, this accumulation of background processes can lead to a significant drain on memory and CPU, causing the system to slow down or even crash.

To illustrate this, consider a developer working on five different projects, each with five terminals and a memory footprint of 50MB per terminal. This translates to a total of 1.25GB of RAM being consumed by background processes alone. Without a mechanism to explicitly clean up these unused projects, the system will eventually run out of memory, leading to performance issues and potential data loss. This is a critical problem that needs a robust solution.

The Current Scenario highlights the challenges developers face daily:

  1. A user switches between 10 different projects during the day.
  2. Each project has approximately 3-5 terminals and 1-2 development servers running.
  3. This results in around 40 terminal processes and 15 development servers active simultaneously.
  4. The combined memory usage can range from 2-4GB.
  5. Users lack visibility into resource usage and have no way to selectively close projects.

This situation creates a significant resource leak, where processes accumulate until the application crashes or the user is forced to quit the entire app, losing unsaved work and disrupting their workflow. Therefore, addressing this issue is paramount for improving the developer experience.

The Solution: An Explicit "Close Project" Action

To address the problem of accumulating background processes, an explicit "Close Project" action is essential. This feature allows developers to manually terminate the processes associated with a project they are no longer actively working on, freeing up system resources and preventing performance degradation. By implementing a "Close Project" action, developers gain greater control over their system's resources and can maintain a more efficient workflow. This provides a proactive approach to resource management.

User Story: The Developer's Perspective

  • As a developer juggling multiple projects throughout the day,
  • I want to explicitly close projects I'm done with,
  • So that I can free up memory and CPU without quitting the entire app.

This user story underscores the core need for a "Close Project" action. Developers need a way to manage their resources effectively without disrupting their workflow. An explicit close action provides this control, allowing them to maintain system performance and focus on their tasks.

Architecture: Implementing the "Close Project" Action

The implementation of a "Close Project" action involves several key steps and architectural considerations. The goal is to provide a seamless and intuitive user experience while ensuring that background processes are terminated cleanly and efficiently. The proposed architecture includes the following key components:

  1. Killing Terminals: Terminate all terminal processes associated with the project.
  2. Stopping Development Servers: Stop all development servers running for the project.
  3. Clearing Project State: Remove any persisted state information for the project.
  4. Visual Feedback: Provide visual cues to indicate which projects are running and their resource usage.
  5. Resource Usage Monitoring: Display memory and process count information for each project.

This architecture ensures that when a project is closed, all associated resources are released, and the system's state is cleaned up. The visual feedback and resource monitoring provide developers with the necessary information to make informed decisions about project management and resource allocation. This holistic approach guarantees a smooth and efficient experience.

Deliverables: Code Changes and Implementation Details

The implementation of the "Close Project" action requires modifications to several parts of the application. These changes include UI updates, backend logic, and IPC (Inter-Process Communication) handlers. The following sections detail the specific code changes and implementation details required to deliver this feature.

Code Changes

To implement the "Close Project" feature, the following files will need to be created or modified:

Files to Create:

  • src/components/Project/ProjectStatusIndicator.tsx: Visual indicator for running projects (e.g., a green dot).
  • src/components/Project/ProjectResourceStats.tsx: Display for memory and process count.

Files to Modify:

  • src/components/Project/ProjectSwitcher.tsx: Add "Close Project" action, visual indicators, and resource stats.
  • src/store/projectStore.ts: Add closeProject(projectId) action and getProjectStats(projectId) for resource monitoring.
  • electron/ipc/handlers/project.ts: Add project:close and project:getStats handlers.
  • electron/services/PtyManager.ts: Add killByProject(projectId) and getProjectStats(projectId) methods.
  • electron/services/DevServerManager.ts: Add stopByProject(projectId) method.
  • electron/services/ProjectStore.ts: Add clearProjectState(projectId) method to delete state files.

Implementation Details

The following code snippets illustrate some of the key implementation details for the "Close Project" action.

Project Switcher UI Updates:

The ProjectSwitcher.tsx component will be updated to include a "Close Project" action in the dropdown menu, along with visual indicators (such as a green dot) for projects with active processes. A resource stats tooltip will also be added to display memory and process count information.

// src/components/Project/ProjectSwitcher.tsx
import { Circle, XCircle } from "lucide-react";

export function ProjectSwitcher() {
  const [projectStats, setProjectStats] = useState<Map<string, ProjectStats>>(new Map());
  
  useEffect(() => {
    // Poll for project stats every 5 seconds
    const interval = setInterval(async () => {
      const stats = new Map();
      for (const project of projects) {
        const stat = await projectClient.getStats(project.id);
        stats.set(project.id, stat);
      }
      setProjectStats(stats);
    }, 5000);
    
    return () => clearInterval(interval);
  }, [projects]);
  
  const handleCloseProject = async (projectId: string) => {
    if (projectId === currentProject?.id) {
      addNotification({
        type: "error",
        title: "Cannot close active project",
        message: "Switch to another project first",
      });
      return;
    }
    
    const stats = projectStats.get(projectId);
    if (!stats || stats.processCount === 0) {
      addNotification({
        type: "info",
        title: "No processes to close",
        message: "Project has no running processes",
      });
      return;
    }
    
    const confirmed = await window.confirm(
      `Close ${stats.processCount} process(es) for this project?`
    );
    
    if (confirmed) {
      await projectClient.close(projectId);
      addNotification({
        type: "success",
        title: "Project closed",
        message: `Killed ${stats.processCount} process(es)`,
      });
    }
  };
  
  return (
    <DropdownMenu>
      <DropdownMenuContent>
        {projects.map((project) => {
          const stats = projectStats.get(project.id);
          const isRunning = stats && stats.processCount > 0;
          const isActive = project.id === currentProject?.id;
          
          return (
            <DropdownMenuItem key={project.id} asChild>
              <div className="flex items-center justify-between w-full">
                <div className="flex items-center gap-2">
                  {isRunning && (
                    <Circle 
                      className="h-2 w-2 fill-green-500 text-green-500" 
                      title={`${stats.processCount} processes running`}
                    />
                  )}
                  <span onClick={() => handleProjectSwitch(project.id)}>
                    {project.name}
                  </span>
                </div>
                
                {isRunning && !isActive && (
                  <Button
                    variant="ghost"
                    size="sm"
                    onClick={(e) => {
                      e.stopPropagation();
                      handleCloseProject(project.id);
                    }}
                    title="Close project and kill processes"
                  >
                    <XCircle className="h-4 w-4 text-destructive" />
                  </Button>
                )}
              </div>
            </DropdownMenuItem>
          );
        })}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Backend IPC Handler:

The electron/ipc/handlers/project.ts file will include handlers for project:close and project:getStats IPC calls. The project:close handler will call the appropriate methods to kill terminals, stop development servers, and clear project state. The project:getStats handler will query the PtyManager and DevServerManager to gather resource usage statistics.

// electron/ipc/handlers/project.ts
ipcMain.handle("project:close", async (_event, projectId: string) => {
  console.log(`[IPC] Closing project: ${projectId}`);
  
  const ptyManager = getPtyManager();
  const devServerManager = getDevServerManager();
  const projectStore = getProjectStore();
  
  // Count processes before killing
  const terminals = ptyManager.getAll().filter(t => t.projectId === projectId);
  const servers = Array.from(devServerManager.getAllStates().values())
    .filter(s => s.projectId === projectId);
  
  const totalProcesses = terminals.length + servers.length;
  
  // Kill all terminals for this project
  await ptyManager.killByProject(projectId);
  
  // Stop all dev servers for this project
  await devServerManager.stopByProject(projectId);
  
  // Clear persisted state
  await projectStore.clearProjectState(projectId);
  
  console.log(`[IPC] Closed project ${projectId}: killed ${totalProcesses} processes`);
  
  return { processesKilled: totalProcesses };
});

ipcMain.handle("project:getStats", async (_event, projectId: string) => {
  const ptyManager = getPtyManager();
  const devServerManager = getDevServerManager();
  
  const terminals = ptyManager.getAll().filter(t => t.projectId === projectId);
  const servers = Array.from(devServerManager.getAllStates().values())
    .filter(s => s.projectId === projectId && s.status === "running");
  
  // Estimate memory usage (rough approximation)
  const memoryPerTerminal = 50; // MB
  const memoryPerServer = 100; // MB
  const estimatedMemory = 
    (terminals.length * memoryPerTerminal) + 
    (servers.length * memoryPerServer);
  
  return {
    processCount: terminals.length + servers.length,
    terminalCount: terminals.length,
    serverCount: servers.length,
    estimatedMemoryMB: estimatedMemory,
  };
});

PtyManager killByProject:

The PtyManager.ts file will include a killByProject(projectId) method to terminate all terminal processes associated with a specific project. This method will filter the terminals by projectId and kill each terminal with a "project-closed" reason.

// electron/services/PtyManager.ts
/**
 * Kill all terminals belonging to a specific project.
 * Used when explicitly closing a project to free resources.
 */
async killByProject(projectId: string): Promise<number> {
  const terminalsToKill = Array.from(this.terminals.values())
    .filter(t => t.projectId === projectId);
  
  console.log(`[PtyManager] Killing ${terminalsToKill.length} terminals for project ${projectId}`);
  
  for (const terminal of terminalsToKill) {
    try {
      this.kill(terminal.id, "project-closed");
    } catch (error) {
      console.warn(`[PtyManager] Failed to kill terminal ${terminal.id}:`, error);
    }
  }
  
  return terminalsToKill.length;
}

/**
 * Get statistics about terminals for a specific project.
 */
getProjectStats(projectId: string): { terminalCount: number; processIds: number[] } {
  const projectTerminals = Array.from(this.terminals.values())
    .filter(t => t.projectId === projectId);
  
  return {
    terminalCount: projectTerminals.length,
    processIds: projectTerminals
      .map(t => t.ptyProcess.pid)
      .filter((pid): pid is number => pid !== undefined),
  };
}

DevServerManager stopByProject:

The DevServerManager.ts file will include a stopByProject(projectId) method to stop all development servers associated with a specific project. This method will filter the servers by projectId and stop each server.

// electron/services/DevServerManager.ts
/**
 * Stop all dev servers for a specific project.
 */
async stopByProject(projectId: string): Promise<number> {
  const serversToStop: string[] = [];
  
  for (const [worktreeId, state] of this.states) {
    if (state.projectId === projectId && this.servers.has(worktreeId)) {
      serversToStop.push(worktreeId);
    }
  }
  
  console.log(`[DevServerManager] Stopping ${serversToStop.length} servers for project ${projectId}`);
  
  await Promise.all(serversToStop.map(id => this.stop(id)));
  
  return serversToStop.length;
}

ProjectStore clearProjectState:

The ProjectStore.ts file will include a clearProjectState(projectId) method to delete the state file associated with a specific project. This method will ensure that any persisted state information is removed when a project is closed.

// electron/services/ProjectStore.ts
async clearProjectState(projectId: string): Promise<void> {
  const stateFilePath = this.getStateFilePath(projectId);
  if (stateFilePath && existsSync(stateFilePath)) {
    try {
      await fs.unlink(stateFilePath);
      console.log(`[ProjectStore] Cleared state for project ${projectId}`);
    } catch (error) {
      console.error(`[ProjectStore] Failed to clear state for ${projectId}:`, error);
      throw error;
    }
  }
}

These code snippets provide a glimpse into the implementation details required for the "Close Project" action. The actual implementation will involve additional considerations such as error handling, UI updates, and testing.

Testing: Ensuring Functionality and Reliability

Comprehensive testing is crucial to ensure the functionality and reliability of the "Close Project" action. The following tests should be performed to validate the implementation:

  • Closing a project with 3 terminals should kill all 3 processes.
  • Closing a project with a development server running should stop the server.
  • The state file should be deleted after closing the project.
  • Attempting to close the active project should display an error message.
  • Closing a project with no processes should be a no-op, and a notification should be shown.
  • A green dot should appear for projects with running processes.
  • The green dot should disappear after closing the project.
  • Stats should update every 5 seconds.
  • Memory estimates should be reasonably accurate.

These tests cover various scenarios and edge cases to ensure that the "Close Project" action functions correctly and does not introduce any unexpected issues.

Documentation: Guiding Users and Developers

Clear and concise documentation is essential for guiding users and developers on how to use the "Close Project" action effectively. The documentation should include:

  • Updates to the docs/multi-project-support.md file to describe the close project workflow.
  • A troubleshooting section for resource management issues.

Comprehensive documentation ensures that users and developers can understand and utilize the new feature effectively.

Technical Specifications: Performance and Footprint

The technical specifications for the "Close Project" action should address the footprint, performance, and resource monitoring aspects of the feature.

Footprint:

  • UI: Add a close button and status indicator to ProjectSwitcher.
  • Backend: Add kill-by-project methods to PtyManager and DevServerManager.
  • IPC: Add project:close and project:getStats handlers.

Performance:

  • Closing a project should take less than 500ms for 10 processes.
  • Stats polling should take less than 10ms per project.
  • Memory freed should be approximately 50MB per terminal and 100MB per development server.

Resource Monitoring:

  • Poll stats every 5 seconds (configurable).
  • Cache stats to reduce IPC overhead.
  • Only poll when ProjectSwitcher is open (optimization).

These specifications provide a clear benchmark for the performance and resource usage of the "Close Project" action.

Dependencies: Addressing Blocking Issues

The implementation of the "Close Project" action may depend on other features or issues being resolved. The following dependencies should be considered:

Blocking:

  • #466 (Phase 1): Requires projectId tracking.
  • #467 (Phase 2): Requires filtered events.
  • #468 (Phase 3): Requires state persistence.

Related:

  • Future: Add a "Close All Background Projects" bulk action.
  • Future: Auto-close projects inactive for N hours.

Addressing these dependencies ensures that the "Close Project" action can be implemented smoothly and effectively.

Tasks: Breaking Down the Implementation

To implement the "Close Project" action, the following tasks need to be completed:

  • Add killByProject(projectId) to PtyManager:
    • Filter terminals by projectId.
    • Kill each terminal with the "project-closed" reason.
    • Return the count of killed processes.
  • Add getProjectStats(projectId) to PtyManager:
    • Count terminals by project.
    • Collect process IDs for resource monitoring.
  • Add stopByProject(projectId) to DevServerManager:
    • Filter servers by projectId.
    • Stop each server.
    • Return the count of stopped servers.
  • Add clearProjectState(projectId) to ProjectStore:
    • Delete the state.json file for the project.
    • Handle missing files gracefully.
  • Add project:close IPC handler:
    • Call killByProject and stopByProject.
    • Clear project state.
    • Return the process count.
  • Add project:getStats IPC handler:
    • Query PtyManager and DevServerManager.
    • Calculate estimated memory usage.
    • Return the stats object.
  • Update ProjectSwitcher UI:
    • Add a green dot indicator for running projects.
    • Add a close button (X icon) for inactive projects.
    • Add a stats tooltip showing process count.
    • Poll for stats every 5 seconds.
    • Handle the close project click with confirmation.
  • Add closeProject() to projectStore:
    • Call projectClient.close().
    • Show a notification with the process count.
    • Refresh project stats.
  • Add a confirmation dialog before closing:
    • Show the process count in the confirmation.
    • Don't allow closing the active project.
  • Add error handling for close failures:
    • Graceful degradation if processes fail to kill.
    • Show user-friendly error messages.

Breaking down the implementation into these tasks provides a clear roadmap for the development process.

Acceptance Criteria: Validating the Implementation

The acceptance criteria define the conditions that must be met for the "Close Project" action to be considered complete and successful. The following acceptance criteria should be used to validate the implementation:

  • A green dot appears next to projects with running processes.
  • No green dot appears for projects with zero processes.
  • Hovering over the green dot shows "X processes running."
  • Clicking the close button on an inactive project displays a confirmation dialog.
  • Confirming the close action kills all processes.
  • The process count in the notification matches the actual killed count.
  • The state file is deleted after closing.
  • Switching back to a closed project spawns fresh terminals.
  • Attempting to close the active project shows an error notification.
  • Stats update every 5 seconds while the switcher is open.
  • Closing a project with 5 terminals and 2 development servers kills 7 processes.
  • Memory is freed after closing (verify in Activity Monitor).

Meeting these acceptance criteria ensures that the "Close Project" action functions as expected and provides a valuable user experience.

Edge Cases & Risks: Addressing Potential Issues

Edge cases and risks should be carefully considered to ensure the robustness and reliability of the "Close Project" action. The following edge cases and risks should be addressed:

Edge Cases:

  • A user closes a project while a terminal is running a command (e.g., git push).
  • A development server is in the middle of a build when closed.
  • The project has orphaned processes (not tracked by managers).
  • Users rapidly close multiple projects.
  • Close a project that has already been closed (no processes).

Risks:

  • A process doesn't die cleanly (zombie process).
  • Data loss if a terminal has unsaved output.
  • Users accidentally close a project with important work.
  • Stats polling overhead with 20+ projects.

Mitigation:

  • Force kill processes after a 5s timeout (existing logic).
  • A confirmation dialog warns users about active processes.
  • Add an "Undo close" feature (future) to reopen within 30 seconds.
  • Only poll stats when ProjectSwitcher is open.
  • Cache stats with a 5s TTL to reduce IPC calls.

Addressing these edge cases and risks ensures that the "Close Project" action is robust and user-friendly.

Additional Context: UI Mockup and Future Enhancements

To provide additional context, a UI mockup and a discussion of future enhancements are included.

UI Mockup:

┌─────────────────────────────────┐
│ Project Switcher                │
├─────────────────────────────────┤
│ ● Project A (active)       ✓    │  ← Green dot, checkmark
│ ● Project B            [X]      │  ← Green dot, close button
│   Project C            [X]      │  ← No dot (no processes)
│ ● Project D            [X]      │  ← Green dot, close button
│ + Add Project                   │
└─────────────────────────────────┘

Hover tooltip on green dot:
"3 terminals, 1 dev server running (est. 250MB)"

Resource Stats Display:

| Project | Terminals | Dev Servers | Est. Memory |
|---------|-----------|-------------|-------------|
| A       | 5         | 2           | 450 MB      |
| B       | 3         | 0           | 150 MB      |
| C       | 0         | 0           | 0 MB        |
| D       | 8         | 1           | 500 MB      |
| **Total** | **16** | **3**       | **1.1 GB**  |

Future Enhancements (Phase 5+):

  • Auto-cleanup: Close projects inactive for N hours.
  • Bulk actions: A "Close All Background Projects" button.
  • Resource alerts: A warning when total memory > 2GB.
  • Undo close: Restore a closed project within 30 seconds.
  • Advanced stats: CPU usage, network traffic per project.
  • Project templates: "Dev" vs "Build" process presets.

These UI mockups and future enhancements provide a vision for the evolution of the "Close Project" action.

Conclusion

Implementing an explicit "Close Project" action is crucial for managing background processes, preventing resource leaks, and ensuring a smoother development experience. This article has explored the problem statement, user story, architecture, deliverables, testing, documentation, technical specifications, dependencies, tasks, acceptance criteria, edge cases, risks, and future enhancements related to this feature. By addressing these aspects, developers can gain greater control over their system's resources and maintain a more efficient workflow. Embracing this approach not only enhances individual productivity but also contributes to the overall stability and performance of the development environment. Let's prioritize resource management for a better development journey.

For more information on process management, visit Process Management Concepts - Linux Documentation.