Skip to content

feat: add compilation status polling with progressive log display#345

Merged
thiagoralves merged 5 commits into
runtimev4-integrationfrom
devin/1759692408-compilation-status-polling
Oct 6, 2025
Merged

feat: add compilation status polling with progressive log display#345
thiagoralves merged 5 commits into
runtimev4-integrationfrom
devin/1759692408-compilation-status-polling

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Oct 5, 2025

Pull request info

References

This PR addresses the task to implement real-time compilation status tracking from the OpenPLC Runtime API with progressive log display and deduplication.

Link to Devin run: https://app.devin.ai/sessions/5d8dddaa658b4d37ab518ff605e9755e
Requested by: @thiagoralves

Description of the changes proposed

  • Centralized runtime API requests: Made MainProcessBridge._makeRuntimeApiRequest public to eliminate code duplication and centralize all HTTPS request logic in one place
  • Real-time compilation polling: Added comprehensive polling mechanism that calls /api/compilation-status endpoint every second until completion (SUCCESS/FAILED) or timeout (5 minutes)
  • Progressive log display: Displays new compilation logs incrementally without duplicates by tracking lastLogCount and using array slicing
  • Proper error handling: Handles API failures, timeouts, parsing errors, and ensures proper MessagePort cleanup in all scenarios
  • Explicit loop conditions: Refactored from while(true) to while(shouldContinuePolling) for better code clarity and maintainability

Key implementation details

  • Dependency injection: CompilerModule receives MainProcessBridge instance to access centralized API request method
  • Message deduplication: Prevents showing duplicate logs by tracking previously displayed count and slicing new entries
  • Configurable timeouts: Extracted polling interval (1s) and timeout (5min) as class constants
  • Terminal state handling: Properly detects and reports SUCCESS/FAILED states with exit codes
  • Resource cleanup: Ensures MessagePort closure and cleanup messages in all code paths

DOD checklist

  • The code is complete and according to developers' standards
  • I have performed a self-review of my code
  • Lint and build checks pass successfully
  • Manual testing required: Test with actual OpenPLC Runtime compilation scenarios
  • Verify message deduplication: Ensure no duplicate logs appear during compilation
  • Test error scenarios: Verify proper handling of network failures, timeouts, and compilation errors
  • Review architectural approach: Consider replacing inline type definition with proper interface
  • Resource management: Verify proper cleanup of MessagePorts and async operations
  • Edge case testing: Test with various compilation log patterns and runtime responses

Critical areas for review

⚠️ High Priority:

  1. Message deduplication logic: Array slicing approach in logs.slice(lastLogCount) - verify this handles all edge cases
  2. Resource cleanup: Async polling with MessagePort lifecycle - check for potential memory leaks
  3. Error handling paths: Multiple exit conditions in polling loop - ensure all scenarios are covered
  4. Real-world testing: This touches external API integration that needs manual verification

⚠️ Medium Priority:

  1. Inline type definition: Verbose mainProcessBridge parameter creates tight coupling - consider extracting to interface
  2. Hardcoded constants: Polling interval/timeout values may need configuration
  3. No unit tests: Complex polling logic would benefit from automated testing

- Add IPC handler for compilation-status API endpoint
- Implement polling after successful program upload
- Track log count to display only new messages
- Poll every 1 second until SUCCESS or FAILED status
- Display final compilation result with exit code
- Handle timeout after 5 minutes of polling

This implements real-time compilation feedback from the OpenPLC Runtime
by continuously polling the compilation-status endpoint and displaying
new log messages as they become available, preventing duplicate messages
by tracking the number of previously displayed logs.

Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Oct 5, 2025

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.


Note

Free review on us!

CodeRabbit is offering free reviews until Wed Oct 08 2025 to showcase some of the refinements we've made.

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

…equest

The previous implementation tried to use rendererProcessBridge (ipcRenderer)
from the main process context, causing 'Cannot read properties of undefined'
error. Now makes direct HTTPS GET requests to /api/compilation-status
following the same pattern as the upload request.

This fixes the bug reported by the user where compilation status polling
failed with the error message about 'invoke' being undefined.

Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
@thiagoralves thiagoralves requested a review from Copilot October 6, 2025 15:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements compilation status polling for the OpenPLC Runtime integration, providing real-time feedback during the compilation process through progressive log display.

  • Adds new IPC handler for compilation-status API with proper request/response structure
  • Implements polling mechanism that tracks compilation progress and displays new logs incrementally
  • Adds comprehensive error handling with timeout protection and proper termination conditions

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
src/main/modules/ipc/renderer.ts Adds renderer bridge method for compilation status API
src/main/modules/ipc/main.ts Implements main process IPC handler for compilation status requests
src/main/modules/compiler/compiler-module.ts Implements polling logic with progressive log display and deduplication

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +1358 to +1362
const pollCompilationStatus = async () => {
let lastLogCount = 0
const startTime = Date.now()
const timeout = 5 * 60 * 1000
const pollInterval = 1000
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

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

The magic numbers for timeout (5 minutes) and poll interval (1 second) should be extracted as named constants to improve maintainability and make them configurable.

Suggested change
const pollCompilationStatus = async () => {
let lastLogCount = 0
const startTime = Date.now()
const timeout = 5 * 60 * 1000
const pollInterval = 1000
// Extracted magic numbers as named constants for maintainability
const COMPILATION_STATUS_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
const COMPILATION_STATUS_POLL_INTERVAL_MS = 1000; // 1 second
const pollCompilationStatus = async () => {
let lastLogCount = 0
const startTime = Date.now()
const timeout = COMPILATION_STATUS_TIMEOUT_MS
const pollInterval = COMPILATION_STATUS_POLL_INTERVAL_MS

Copilot uses AI. Check for mistakes.
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.

Devin, could you take a look at this comment please?

Comment thread src/main/modules/compiler/compiler-module.ts
Comment on lines +1376 to +1423
const statusData = await new Promise<{
status: string
logs: string[]
exit_code: number | null
}>((resolve, reject) => {
const req = https.request(
{
hostname: runtimeIpAddress,
port: 8443,
path: '/api/compilation-status',
method: 'GET',
headers: {
Authorization: `Bearer ${runtimeJwtToken}`,
},
rejectUnauthorized: false,
},
(res: IncomingMessage) => {
let data = ''
res.on('data', (chunk: Buffer) => {
data += chunk.toString()
})
res.on('end', () => {
if (res.statusCode === 200) {
try {
const parsed = JSON.parse(data) as {
status: string
logs: string[]
exit_code: number | null
}
resolve(parsed)
} catch (parseError) {
reject(
new Error(
`Failed to parse compilation status response: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
),
)
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`))
}
})
},
)
req.on('error', (error: Error) => {
reject(error)
})
req.end()
})
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

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

The HTTPS request logic is duplicated from the existing _makeRuntimeApiRequest method. Consider reusing that method instead of reimplementing the same logic inline.

Suggested change
const statusData = await new Promise<{
status: string
logs: string[]
exit_code: number | null
}>((resolve, reject) => {
const req = https.request(
{
hostname: runtimeIpAddress,
port: 8443,
path: '/api/compilation-status',
method: 'GET',
headers: {
Authorization: `Bearer ${runtimeJwtToken}`,
},
rejectUnauthorized: false,
},
(res: IncomingMessage) => {
let data = ''
res.on('data', (chunk: Buffer) => {
data += chunk.toString()
})
res.on('end', () => {
if (res.statusCode === 200) {
try {
const parsed = JSON.parse(data) as {
status: string
logs: string[]
exit_code: number | null
}
resolve(parsed)
} catch (parseError) {
reject(
new Error(
`Failed to parse compilation status response: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
),
)
}
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`))
}
})
},
)
req.on('error', (error: Error) => {
reject(error)
})
req.end()
})
const statusData = await _makeRuntimeApiRequest({
hostname: runtimeIpAddress,
port: 8443,
path: '/api/compilation-status',
method: 'GET',
headers: {
Authorization: `Bearer ${runtimeJwtToken}`,
},
rejectUnauthorized: false,
});

Copilot uses AI. Check for mistakes.
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.

Devin, can you take a look at this comment please?

- Add COMPILATION_STATUS_TIMEOUT_MS and COMPILATION_STATUS_POLL_INTERVAL_MS constants
- Add RUNTIME_API_PORT constant for API port configuration
- Extract HTTPS GET request logic to _makeRuntimeApiGetRequest helper method
- Update polling logic to use constants and helper method
- Reduces code duplication and improves maintainability

Addresses Copilot suggestions in PR review.

Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I've addressed both suggestions:

  1. Extracted magic numbers to constants: Added COMPILATION_STATUS_TIMEOUT_MS (5 minutes) and COMPILATION_STATUS_POLL_INTERVAL_MS (1 second) as static readonly properties in CompilerModule, along with RUNTIME_API_PORT (8443) for the API port.

  2. Reduced HTTPS request duplication: Created a private _makeRuntimeApiGetRequest() helper method in CompilerModule that encapsulates the HTTPS GET request logic with proper error handling and JSON parsing.

Note on architecture: I created the helper method within CompilerModule rather than using MainProcessBridge's _makeRuntimeApiRequest method because:

  • The MainProcessBridge method is private and not accessible from CompilerModule
  • CompilerModule and MainProcessBridge are in separate modules with no direct reference path from CompilerModule to MainProcessBridge
  • Creating tight coupling between these modules would compromise the separation of concerns

The refactored code now follows the same pattern as MainProcessBridge (helper method + constants) while maintaining proper module boundaries.

All changes have been pushed and both lint and build checks pass successfully.

…Bridge

- Make _makeRuntimeApiRequest public as makeRuntimeApiRequest
- Pass MainProcessBridge instance to compileProgram
- Remove duplicate _makeRuntimeApiGetRequest method from CompilerModule
- Remove duplicate RUNTIME_API_PORT constant from CompilerModule
- Update polling logic to use mainProcessBridge.makeRuntimeApiRequest
- Consolidates all HTTPS request logic in MainProcessBridge

Addresses code review feedback to eliminate duplication.

Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
@thiagoralves thiagoralves requested a review from Copilot October 6, 2025 18:34
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment thread src/main/modules/compiler/compiler-module.ts
const timeout = CompilerModule.COMPILATION_STATUS_TIMEOUT_MS
const pollInterval = CompilerModule.COMPILATION_STATUS_POLL_INTERVAL_MS

while (true) {
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

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

Using an infinite while loop without a clear exit condition makes the code harder to reason about. Consider refactoring to use a more explicit loop condition or state machine pattern.

Copilot uses AI. Check for mistakes.
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.

Devin, could you put a specific condition here instead of an infinite loop? Maybe tie this loop with the compilation status, for example if the API poll fails, or if the status changes to success or failed, the loop must end.

Comment on lines +1059 to +1066
mainProcessBridge: {
makeRuntimeApiRequest: <T = void>(
ipAddress: string,
jwtToken: string,
endpoint: string,
responseParser?: (data: string) => T,
) => Promise<{ success: true; data?: T } | { success: false; error: string }>
},
Copy link

Copilot AI Oct 6, 2025

Choose a reason for hiding this comment

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

The inline type definition for mainProcessBridge creates tight coupling and makes the interface difficult to maintain. Consider extracting this to a proper interface or using the actual MainProcessBridge type.

Copilot uses AI. Check for mistakes.
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.

Devin, can you take a look at this please?

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Thanks for pointing this out! I've investigated the inline type definition for mainProcessBridge and found a cleaner architectural approach.

Current Situation

The inline structural type definition is verbose and creates tight coupling:

mainProcessBridge: {
  makeRuntimeApiRequest: <T = void>(
    ipAddress: string,
    jwtToken: string,
    endpoint: string,
    responseParser?: (data: string) => T,
  ) => Promise<{ success: true; data?: T } | { success: false; error: string }>
}

Why Not Import MainProcessBridge Directly?

I investigated importing the MainProcessBridge class type directly, but this would create a circular dependency:

  • CompilerModule is instantiated first (main.ts:287)
  • MainProcessBridge receives CompilerModule as a constructor parameter (main.ts:291-299)
  • If CompilerModule imports from MainProcessBridge, we'd have a circular reference

Recommended Solution

Following the existing pattern in src/main/contracts/types/modules/ipc/main.ts, I propose:

  1. Create a new interface IRuntimeApiRequestor in the contracts file that defines just the makeRuntimeApiRequest method
  2. Update CompilerModule to use IRuntimeApiRequestor instead of the inline type
  3. MainProcessBridge implements this interface (implicitly through structural typing)

This approach:
✅ Follows the existing architectural pattern in the codebase
✅ Eliminates code duplication and improves maintainability
✅ Avoids circular dependencies
✅ Keeps type definitions in the proper contracts location
✅ Uses dependency injection with a clean interface

Would you like me to implement this refactoring?

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Thanks for catching this! You're absolutely right - while the loop does have exit conditions (timeout check, API failures, SUCCESS/FAILED states), they're hidden inside the loop body as break statements, which makes the code harder to reason about.

I'll refactor it to use an explicit loop condition instead of while (true). Here's the improved approach:

let shouldContinuePolling = true
const startTime = Date.now()
const timeout = CompilerModule.COMPILATION_STATUS_TIMEOUT_MS
const pollInterval = CompilerModule.COMPILATION_STATUS_POLL_INTERVAL_MS

while (shouldContinuePolling) {
  // Check timeout
  if (Date.now() - startTime > timeout) {
    _mainProcessPort.postMessage({
      logLevel: 'error',
      message: 'Compilation status polling timed out after 5 minutes.',
    })
    shouldContinuePolling = false
    continue
  }

  await new Promise((resolve) => setTimeout(resolve, pollInterval))

  try {
    const result = await mainProcessBridge.makeRuntimeApiRequest<{
      status: string
      logs: string[]
      exit_code: number | null
    }>(runtimeIpAddress, runtimeJwtToken, '/api/compilation-status', (data: string) => {
      return JSON.parse(data) as { status: string; logs: string[]; exit_code: number | null }
    })

    if (!result.success) {
      _mainProcessPort.postMessage({
        logLevel: 'error',
        message: `Error polling compilation status: ${result.error}`,
      })
      shouldContinuePolling = false
      continue
    }

    const { status, logs, exit_code } = result.data!

    // Handle new logs (same as before)
    if (logs.length > lastLogCount) {
      const newLogs = logs.slice(lastLogCount)
      newLogs.forEach((log) => {
        _mainProcessPort.postMessage({
          logLevel: 'info',
          message: log,
        })
      })
      lastLogCount = logs.length
    }

    // Check terminal states
    if (status === 'SUCCESS') {
      _mainProcessPort.postMessage({
        logLevel: 'info',
        message: `Compilation completed successfully (exit code: ${exit_code ?? 0}).`,
      })
      shouldContinuePolling = false
    } else if (status === 'FAILED') {
      _mainProcessPort.postMessage({
        logLevel: 'error',
        message: `Compilation failed (exit code: ${exit_code ?? 1}).`,
      })
      shouldContinuePolling = false
    }
  } catch (pollError) {
    _mainProcessPort.postMessage({
      logLevel: 'error',
      message: `Error polling compilation status: ${pollError instanceof Error ? pollError.message : String(pollError)}`,
    })
    shouldContinuePolling = false
  }
}

This makes it immediately clear that the loop continues only while shouldContinuePolling is true, and all exit conditions explicitly set it to false.

Would you like me to implement this refactoring?

…n polling

- Changed while(true) to while(shouldContinuePolling) for clarity
- All break statements replaced with shouldContinuePolling = false
- Makes exit conditions (timeout, SUCCESS, FAILED, errors) explicit
- Improves code readability and maintainability

Addresses GitHub PR review feedback from @thiagoralves

Co-Authored-By: Thiago Alves <thiagoralves@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Refactoring complete! I've updated the code to use an explicit loop condition instead of while (true).

Changes made:

  • Changed while (true) to while (shouldContinuePolling) for clarity
  • Replaced all break statements with shouldContinuePolling = false
  • Exit conditions are now explicit and tied to compilation status:
    • Timeout after 5 minutes
    • API poll failures
    • SUCCESS status
    • FAILED status
    • Unexpected errors

The refactoring improves code readability while preserving all existing behavior. Lint and build checks pass successfully.

Commit: 6efeb27

@thiagoralves thiagoralves merged commit ab145a0 into runtimev4-integration Oct 6, 2025
1 check passed
@thiagoralves thiagoralves deleted the devin/1759692408-compilation-status-polling branch October 6, 2025 19:18
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.

2 participants