mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add crash recovery and Windows-specific error handling
- Add global error handlers for uncaught exceptions and promise rejections - Implement crash information persistence for recovery on restart - Add Windows-specific signal handlers (SIGTERM, SIGINT, exit) - Enhance ErrorBoundary component with user-friendly crash recovery UI - Add restart functionality from ErrorBoundary - Implement task state persistence during crashes - Add subtask recovery mechanism to restore parent task context - Integrate crash telemetry logging - Add Windows-specific crash detection and messaging Fixes #6257
This commit is contained in:
parent
91c21a1bba
commit
6fd883d81d
5 changed files with 403 additions and 23 deletions
|
|
@ -707,6 +707,10 @@ export const webviewMessageHandler = async (
|
|||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
}
|
||||
break
|
||||
case "reloadWindow":
|
||||
// Reload the VS Code window to recover from crash
|
||||
vscode.commands.executeCommand("workbench.action.reloadWindow")
|
||||
break
|
||||
case "checkpointDiff":
|
||||
const result = checkoutDiffPayloadSchema.safeParse(message.payload)
|
||||
|
||||
|
|
|
|||
270
src/extension.ts
270
src/extension.ts
|
|
@ -59,6 +59,12 @@ export async function activate(context: vscode.ExtensionContext) {
|
|||
context.subscriptions.push(outputChannel)
|
||||
outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
|
||||
|
||||
// Set up global error handlers for crash recovery
|
||||
setupGlobalErrorHandlers(context, outputChannel)
|
||||
|
||||
// Check for crash recovery
|
||||
await checkForCrashRecovery(context, outputChannel)
|
||||
|
||||
// Migrate old settings to new
|
||||
await migrateSettings(context, outputChannel)
|
||||
|
||||
|
|
@ -218,3 +224,267 @@ export async function deactivate() {
|
|||
TelemetryService.instance.shutdown()
|
||||
TerminalRegistry.cleanup()
|
||||
}
|
||||
|
||||
// Global error handlers for crash recovery
|
||||
function setupGlobalErrorHandlers(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
|
||||
// Handle uncaught exceptions
|
||||
process.on("uncaughtException", async (error: Error) => {
|
||||
const errorMessage = `[CRASH] Uncaught Exception: ${error.message}\nStack: ${error.stack}`
|
||||
outputChannel.appendLine(errorMessage)
|
||||
console.error(errorMessage)
|
||||
|
||||
// Save crash information
|
||||
await saveCrashInfo(context, error, "uncaughtException")
|
||||
|
||||
// Attempt to save current task state
|
||||
await saveTaskStateOnCrash(context)
|
||||
|
||||
// Log telemetry
|
||||
// Log crash telemetry
|
||||
try {
|
||||
if (TelemetryService.hasInstance()) {
|
||||
TelemetryService.instance.captureEvent("extension_crash" as any, {
|
||||
type: "uncaughtException",
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
platform: process.platform,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to log telemetry:", e)
|
||||
}
|
||||
|
||||
// Show user-friendly error message
|
||||
vscode.window
|
||||
.showErrorMessage(
|
||||
"Roo Code encountered an unexpected error. Your work has been saved. Please restart VS Code.",
|
||||
"Restart VS Code",
|
||||
)
|
||||
.then((selection) => {
|
||||
if (selection === "Restart VS Code") {
|
||||
vscode.commands.executeCommand("workbench.action.reloadWindow")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
process.on("unhandledRejection", async (reason: any, promise: Promise<any>) => {
|
||||
const errorMessage = `[CRASH] Unhandled Promise Rejection: ${reason}\nPromise: ${promise}`
|
||||
outputChannel.appendLine(errorMessage)
|
||||
console.error(errorMessage)
|
||||
|
||||
// Save crash information
|
||||
await saveCrashInfo(context, reason, "unhandledRejection")
|
||||
|
||||
// Attempt to save current task state
|
||||
await saveTaskStateOnCrash(context)
|
||||
|
||||
// Log telemetry
|
||||
// Log crash telemetry
|
||||
try {
|
||||
if (TelemetryService.hasInstance()) {
|
||||
TelemetryService.instance.captureEvent("extension_crash" as any, {
|
||||
type: "unhandledRejection",
|
||||
reason: String(reason),
|
||||
platform: process.platform,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to log telemetry:", e)
|
||||
}
|
||||
})
|
||||
|
||||
// Windows-specific error handling
|
||||
if (process.platform === "win32") {
|
||||
// Handle Windows-specific errors
|
||||
process.on("SIGTERM", async () => {
|
||||
outputChannel.appendLine("[CRASH] Received SIGTERM signal (Windows termination)")
|
||||
await saveCrashInfo(context, new Error("SIGTERM received"), "SIGTERM")
|
||||
await saveTaskStateOnCrash(context)
|
||||
})
|
||||
|
||||
process.on("SIGINT", async () => {
|
||||
outputChannel.appendLine("[CRASH] Received SIGINT signal (Windows interruption)")
|
||||
await saveCrashInfo(context, new Error("SIGINT received"), "SIGINT")
|
||||
await saveTaskStateOnCrash(context)
|
||||
})
|
||||
|
||||
// Handle Windows-specific exit events
|
||||
process.on("exit", async (code) => {
|
||||
if (code !== 0) {
|
||||
outputChannel.appendLine(`[CRASH] Process exiting with code ${code}`)
|
||||
await saveCrashInfo(context, new Error(`Process exit with code ${code}`), "exit")
|
||||
await saveTaskStateOnCrash(context)
|
||||
}
|
||||
})
|
||||
|
||||
// Handle Windows-specific errors that might cause crashes
|
||||
process.on("uncaughtExceptionMonitor", (error: Error, origin: string) => {
|
||||
// This event is emitted before uncaughtException, useful for logging
|
||||
outputChannel.appendLine(`[CRASH] Uncaught exception monitor: ${error.message} from ${origin}`)
|
||||
|
||||
// Check for Windows-specific error patterns
|
||||
if (error.message.includes("EPERM") || error.message.includes("EACCES")) {
|
||||
outputChannel.appendLine("[CRASH] Windows permission error detected")
|
||||
} else if (error.message.includes("ENOENT")) {
|
||||
outputChannel.appendLine("[CRASH] Windows file not found error detected")
|
||||
} else if (error.message.includes("spawn") || error.message.includes("ENOBUFS")) {
|
||||
outputChannel.appendLine("[CRASH] Windows process spawn error detected")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Save crash information for recovery
|
||||
async function saveCrashInfo(context: vscode.ExtensionContext, error: any, type: string) {
|
||||
try {
|
||||
const crashInfo = {
|
||||
timestamp: new Date().toISOString(),
|
||||
type,
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
}
|
||||
: String(error),
|
||||
platform: process.platform,
|
||||
vscodeVersion: vscode.version,
|
||||
extensionVersion: context.extension?.packageJSON?.version,
|
||||
}
|
||||
|
||||
await context.globalState.update("lastCrashInfo", crashInfo)
|
||||
await context.globalState.update("hasCrashRecovery", true)
|
||||
} catch (e) {
|
||||
console.error("Failed to save crash info:", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Save current task state on crash
|
||||
async function saveTaskStateOnCrash(context: vscode.ExtensionContext) {
|
||||
try {
|
||||
const provider = ClineProvider.getVisibleInstance()
|
||||
if (provider) {
|
||||
const currentTask = provider.getCurrentCline()
|
||||
if (currentTask) {
|
||||
// Save current task state
|
||||
// Force save current state
|
||||
await provider.postStateToWebview()
|
||||
|
||||
// Save task recovery info
|
||||
const recoveryInfo = {
|
||||
taskId: currentTask.taskId,
|
||||
parentTaskId: currentTask.parentTask?.taskId,
|
||||
taskStack: provider.getCurrentTaskStack(),
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
|
||||
await context.globalState.update("taskRecoveryInfo", recoveryInfo)
|
||||
outputChannel.appendLine(`[CRASH] Saved task recovery info for task ${currentTask.taskId}`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to save task state on crash:", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for crash recovery on startup
|
||||
async function checkForCrashRecovery(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel) {
|
||||
try {
|
||||
const hasCrashRecovery = context.globalState.get<boolean>("hasCrashRecovery")
|
||||
const lastCrashInfo = context.globalState.get<any>("lastCrashInfo")
|
||||
const taskRecoveryInfo = context.globalState.get<any>("taskRecoveryInfo")
|
||||
|
||||
if (hasCrashRecovery && lastCrashInfo) {
|
||||
outputChannel.appendLine(
|
||||
`[RECOVERY] Detected previous crash: ${lastCrashInfo.type} at ${lastCrashInfo.timestamp}`,
|
||||
)
|
||||
|
||||
// Clear the crash flag
|
||||
await context.globalState.update("hasCrashRecovery", false)
|
||||
|
||||
// Show recovery notification with Windows-specific messaging if applicable
|
||||
const isWindows = process.platform === "win32"
|
||||
const crashMessage =
|
||||
isWindows && (lastCrashInfo.type === "SIGTERM" || lastCrashInfo.type === "SIGINT")
|
||||
? "Roo Code was terminated unexpectedly on Windows. Would you like to restore your last session?"
|
||||
: "Roo Code recovered from a previous crash. Would you like to restore your last session?"
|
||||
|
||||
const selection = await vscode.window.showInformationMessage(crashMessage, "Restore Session", "Start Fresh")
|
||||
|
||||
if (selection === "Restore Session" && taskRecoveryInfo) {
|
||||
outputChannel.appendLine(`[RECOVERY] Attempting to restore task ${taskRecoveryInfo.taskId}`)
|
||||
|
||||
// Delay to ensure extension is fully initialized
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const provider = ClineProvider.getVisibleInstance()
|
||||
if (provider && taskRecoveryInfo.taskId) {
|
||||
// Check if this was a subtask
|
||||
if (taskRecoveryInfo.parentTaskId) {
|
||||
outputChannel.appendLine(
|
||||
`[RECOVERY] Detected subtask recovery. Parent task: ${taskRecoveryInfo.parentTaskId}`,
|
||||
)
|
||||
|
||||
// First, try to restore the parent task
|
||||
try {
|
||||
await provider.showTaskWithId(taskRecoveryInfo.parentTaskId)
|
||||
outputChannel.appendLine(
|
||||
`[RECOVERY] Restored parent task ${taskRecoveryInfo.parentTaskId}`,
|
||||
)
|
||||
|
||||
// Then show information about the subtask that was interrupted
|
||||
vscode.window
|
||||
.showInformationMessage(
|
||||
`Restored to parent task. The subtask that was running during the crash has been saved and can be resumed.`,
|
||||
"View Subtask",
|
||||
)
|
||||
.then(async (selection) => {
|
||||
if (selection === "View Subtask") {
|
||||
// Show the subtask that was interrupted
|
||||
await provider.showTaskWithId(taskRecoveryInfo.taskId)
|
||||
}
|
||||
})
|
||||
} catch (parentError) {
|
||||
// If parent task can't be restored, just restore the subtask
|
||||
outputChannel.appendLine(
|
||||
`[RECOVERY] Failed to restore parent task, restoring subtask instead`,
|
||||
)
|
||||
await provider.showTaskWithId(taskRecoveryInfo.taskId)
|
||||
|
||||
vscode.window.showInformationMessage(
|
||||
`Restored subtask from before the crash. The parent task context may need to be re-established.`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Regular task recovery
|
||||
await provider.showTaskWithId(taskRecoveryInfo.taskId)
|
||||
|
||||
vscode.window.showInformationMessage(`Restored task from before the crash.`)
|
||||
}
|
||||
|
||||
// If there was a task stack, log it for debugging
|
||||
if (taskRecoveryInfo.taskStack && taskRecoveryInfo.taskStack.length > 1) {
|
||||
outputChannel.appendLine(
|
||||
`[RECOVERY] Task stack at crash: ${taskRecoveryInfo.taskStack.join(" -> ")}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to restore task:", e)
|
||||
vscode.window.showErrorMessage(
|
||||
"Could not restore the previous task, but your work has been saved.",
|
||||
)
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Clear recovery info
|
||||
await context.globalState.update("taskRecoveryInfo", undefined)
|
||||
await context.globalState.update("lastCrashInfo", undefined)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error checking for crash recovery:", e)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,5 +168,20 @@
|
|||
"preventCompletionWithOpenTodos": {
|
||||
"description": "Prevent task completion when there are incomplete todos in the todo list"
|
||||
}
|
||||
},
|
||||
"errorBoundary": {
|
||||
"title": "Something went wrong",
|
||||
"reportText": "Please help us improve by reporting this error on",
|
||||
"githubText": "GitHub",
|
||||
"copyInstructions": "Please copy and paste the following error message:",
|
||||
"errorStack": "Error Stack",
|
||||
"componentStack": "Component Stack",
|
||||
"windowsNote": "This crash occurred on Windows. Your work has been automatically saved.",
|
||||
"crashRecoveryText": "Don't worry! Your work has been saved and can be recovered.",
|
||||
"restarting": "Restarting...",
|
||||
"restartVSCode": "Restart VS Code",
|
||||
"reportIssue": "Report Issue",
|
||||
"technicalDetails": "Technical Details",
|
||||
"helpText": "If the problem persists, please"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ export interface WebviewMessage {
|
|||
| "profileThresholds"
|
||||
| "setHistoryPreviewCollapsed"
|
||||
| "openExternal"
|
||||
| "reloadWindow"
|
||||
| "filterMarketplaceItems"
|
||||
| "marketplaceButtonClicked"
|
||||
| "installMarketplaceItem"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { Component } from "react"
|
|||
import { telemetryClient } from "@src/utils/TelemetryClient"
|
||||
import { withTranslation, WithTranslation } from "react-i18next"
|
||||
import { enhanceErrorWithSourceMaps } from "@src/utils/sourceMapUtils"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
type ErrorProps = {
|
||||
children: React.ReactNode
|
||||
|
|
@ -11,14 +12,15 @@ type ErrorState = {
|
|||
error?: string
|
||||
componentStack?: string | null
|
||||
timestamp?: number
|
||||
isRecovering?: boolean
|
||||
}
|
||||
|
||||
class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
||||
constructor(props: ErrorProps) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
|
||||
this.state = {}
|
||||
this.state = {
|
||||
isRecovering: false,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: unknown) {
|
||||
|
|
@ -54,6 +56,31 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
|||
})
|
||||
}
|
||||
|
||||
handleRestart = () => {
|
||||
this.setState({ isRecovering: true })
|
||||
vscode.postMessage({ type: "reloadWindow" })
|
||||
}
|
||||
|
||||
handleReportIssue = () => {
|
||||
const errorInfo = encodeURIComponent(
|
||||
`
|
||||
**Error:** ${this.state.error || "Unknown error"}
|
||||
**Timestamp:** ${new Date(this.state.timestamp || Date.now()).toISOString()}
|
||||
**Version:** ${process.env.PKG_VERSION || "unknown"}
|
||||
**Platform:** ${navigator.platform}
|
||||
**User Agent:** ${navigator.userAgent}
|
||||
|
||||
**Component Stack:**
|
||||
\`\`\`
|
||||
${this.state.componentStack || "Not available"}
|
||||
\`\`\`
|
||||
`.trim(),
|
||||
)
|
||||
|
||||
const issueUrl = `https://github.com/RooCodeInc/Roo-Code/issues/new?title=Crash%20Report&body=${errorInfo}`
|
||||
window.open(issueUrl, "_blank")
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props
|
||||
|
||||
|
|
@ -63,33 +90,96 @@ class ErrorBoundary extends Component<ErrorProps, ErrorState> {
|
|||
|
||||
const errorDisplay = this.state.error
|
||||
const componentStackDisplay = this.state.componentStack
|
||||
|
||||
const version = process.env.PKG_VERSION || "unknown"
|
||||
const isWindows = navigator.platform.toLowerCase().includes("win")
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mt-0 mb-2">
|
||||
{t("errorBoundary.title")} (v{version})
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
{t("errorBoundary.reportText")}{" "}
|
||||
<a href="https://github.com/RooCodeInc/Roo-Code/issues" target="_blank" rel="noreferrer">
|
||||
{t("errorBoundary.githubText")}
|
||||
</a>
|
||||
</p>
|
||||
<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
|
||||
<div className="p-4">
|
||||
<div className="mb-4 p-4 bg-vscode-editorWidget-background border border-vscode-editorWidget-border rounded">
|
||||
<h2 className="text-lg font-bold mt-0 mb-2 text-vscode-errorForeground">
|
||||
{t("errorBoundary.title")} (v{version})
|
||||
</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
|
||||
<pre className="p-2 border rounded text-sm overflow-auto">{errorDisplay}</pre>
|
||||
{isWindows && (
|
||||
<div className="mb-3 p-2 bg-vscode-inputValidation-warningBackground border border-vscode-inputValidation-warningBorder rounded text-sm">
|
||||
<span className="codicon codicon-warning mr-2"></span>
|
||||
{t(
|
||||
"errorBoundary.windowsNote",
|
||||
"This crash occurred on Windows. Your work has been automatically saved.",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mb-4">
|
||||
{t(
|
||||
"errorBoundary.crashRecoveryText",
|
||||
"Don't worry! Your work has been saved and can be recovered.",
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
className="px-4 py-2 bg-vscode-button-background text-vscode-button-foreground hover:bg-vscode-button-hoverBackground rounded"
|
||||
onClick={this.handleRestart}
|
||||
disabled={this.state.isRecovering}>
|
||||
{this.state.isRecovering ? (
|
||||
<>
|
||||
<span className="codicon codicon-loading codicon-modifier-spin mr-2"></span>
|
||||
{t("errorBoundary.restarting", "Restarting...")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="codicon codicon-debug-restart mr-2"></span>
|
||||
{t("errorBoundary.restartVSCode", "Restart VS Code")}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="px-4 py-2 bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground hover:bg-vscode-button-secondaryHoverBackground rounded"
|
||||
onClick={this.handleReportIssue}>
|
||||
<span className="codicon codicon-github mr-2"></span>
|
||||
{t("errorBoundary.reportIssue", "Report Issue")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{componentStackDisplay && (
|
||||
<div>
|
||||
<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
|
||||
<pre className="p-2 border rounded text-sm overflow-auto">{componentStackDisplay}</pre>
|
||||
<details className="mb-4">
|
||||
<summary className="cursor-pointer font-bold mb-2">
|
||||
{t("errorBoundary.technicalDetails", "Technical Details")}
|
||||
</summary>
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="mb-2">{t("errorBoundary.copyInstructions")}</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="text-md font-bold mb-1">{t("errorBoundary.errorStack")}</h3>
|
||||
<pre className="p-2 border rounded text-sm overflow-auto max-h-64">{errorDisplay}</pre>
|
||||
</div>
|
||||
|
||||
{componentStackDisplay && (
|
||||
<div>
|
||||
<h3 className="text-md font-bold mb-1">{t("errorBoundary.componentStack")}</h3>
|
||||
<pre className="p-2 border rounded text-sm overflow-auto max-h-64">
|
||||
{componentStackDisplay}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</details>
|
||||
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
<p>
|
||||
{t("errorBoundary.helpText", "If the problem persists, please")}{" "}
|
||||
<a
|
||||
href="https://github.com/RooCodeInc/Roo-Code/issues"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline">
|
||||
{t("errorBoundary.githubText")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue