Skip to content

Commit cee0398

Browse files
author
Roo
committed
Merge branch 'main' of https://github.com/jsboige/Roo-Code
2 parents 68d0ab8 + 7438f40 commit cee0398

File tree

6 files changed

+393
-0
lines changed

6 files changed

+393
-0
lines changed

myia/01-ui-crash-investigation.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Rapport d'Investigation : Crash de l'UI de Roo Code
2+
3+
**Date :** 2025-08-02
4+
**Auteur :** Roo, Assistant Technique
5+
**Protocole :** SDDD (Semantic Documentation Driven Design)
6+
7+
---
8+
9+
## 1. Contexte
10+
11+
Ce document analyse les causes potentielles du crash récurrent de l'interface utilisateur (WebView) de l'extension Roo Code. Le symptôme principal est un écran gris qui ne répond plus, survenant fréquemment lors de l'utilisation de plusieurs fenêtres VSCode ou après de longues sessions d'utilisation.
12+
13+
L'investigation s'est concentrée sur le fichier `roo-code/src/core/webview/ClineProvider.ts`, identifié comme le composant central de la gestion de la WebView via une recherche sémantique initiale.
14+
15+
---
16+
17+
## 2. Analyse et Hypothèses
18+
19+
L'examen du code de `ClineProvider.ts` a permis de formuler plusieurs hypothèses sur l'origine du problème.
20+
21+
### Fichier Clé Analysé
22+
23+
* **Chemin :** `roo-code/src/core/webview/ClineProvider.ts`
24+
* **Rôle :** Gère l'intégralité du cycle de vie de la WebView, de sa création à sa destruction, y compris la gestion des états et la communication avec l'extension.
25+
26+
### Hypothèse 1 : Libération incorrecte des ressources (`Disposables`)
27+
28+
**Observation :**
29+
La classe `ClineProvider` utilise deux collections pour les ressources à libérer : `disposables` (global) et `webviewDisposables` (spécifique à la vue). La logique dans `onDidDispose` (lignes 487-501) ne nettoie que `webviewDisposables` pour la vue de la barre latérale.
30+
31+
**Cause potentielle :**
32+
Une ressource liée à la vue (ex: listener) ajoutée par erreur au tableau global `disposables` ne sera pas libérée à la fermeture de la barre latérale, créant une **fuite de mémoire** et des listeners "fantômes" pouvant provoquer un crash.
33+
34+
### Hypothèse 2 : Conditions de concurrence ("Race Conditions")
35+
36+
**Observation :**
37+
La méthode `postStateToWebview` est appelée depuis de nombreux endroits, souvent de manière asynchrone, sans vérifier si la vue `this.view` est toujours valide avant d'appeler `postMessage`.
38+
39+
**Cause potentielle :**
40+
Un événement peut déclencher un `postStateToWebview` juste au moment où l'UI est fermée. L'appel à `postMessage` sur une référence invalide est une cause classique de crashs.
41+
42+
### Hypothèse 3 : Complexité de la gestion multi-fenêtres
43+
44+
**Observation :**
45+
La classe utilise un ensemble statique `activeInstances` pour suivre les vues dans différentes fenêtres.
46+
47+
**Cause potentielle :**
48+
Une mauvaise synchronisation lors de la destruction d'une vue pourrait laisser des instances "zombies" dans `activeInstances`, conduisant à des interactions avec des objets détruits.
49+
50+
---
51+
52+
## 3. Conclusion et Recommandations Initiales
53+
54+
Les causes les plus probables du crash sont des **fuites de ressources** et des **conditions de concurrence**, exacerbées par la complexité du multi-fenêtres.
55+
56+
---
57+
58+
## 4. Correctifs Implémentés
59+
60+
En réponse aux hypothèses formulées, les actions suivantes ont été menées pour stabiliser le `ClineProvider`.
61+
62+
### Renforcement de `postMessageToWebview`
63+
64+
* **Problème :** Des appels à `postMessage` sur une `webview` potentiellement invalide ou non visible pouvaient causer des crashs (Race Condition).
65+
* **Solution :** Une garde de protection a été ajoutée à la méthode `postMessageToWebview`. Avant tout appel, elle vérifie désormais si `this.view` est défini et si `this.view.visible` est `true`. De plus, un bloc `try...catch` intercepte les erreurs spécifiques à une `webview` "disposed", évitant ainsi un crash complet.
66+
67+
Cette modification prévient les erreurs fatales lorsqu'un message est envoyé à une `webview` en cours de fermeture ou déjà détruite, renforçant significativement la robustesse du composant face aux conditions de concurrence.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Analyse des Capacités de Test pour `roo-code`
2+
3+
**Date :** 2025-08-02
4+
**Auteur :** Roo, Assistant Technique
5+
**Protocole :** SDDD
6+
7+
---
8+
9+
## 1. Objectif
10+
11+
Cette analyse vise à déterminer une méthode viable pour tester localement les modifications apportées au fork de `roo-code` sur une instance installée de l'extension VS Code, sans nécessiter un processus de packaging et de signature complet.
12+
13+
---
14+
15+
## 2. Processus de Build
16+
17+
L'investigation du projet `roo-code` a révélé les points suivants concernant le processus de build :
18+
19+
* **Orchestration :** La commande principale `pnpm bundle` est lancée depuis le répertoire `roo-code/src/`.
20+
* **Technologie :** Le script [`esbuild.mjs`](roo-code/src/esbuild.mjs:1) est utilisé pour la transpilation du TypeScript en JavaScript.
21+
* **Sortie (Output) :** Le processus de build génère un répertoire `dist/` à la racine de `roo-code/src/`. Ce répertoire contient tous les fichiers JavaScript compilés et les ressources statiques nécessaires au fonctionnement de l'extension.
22+
23+
---
24+
25+
## 3. Localisation de l'Extension Installée
26+
27+
Le répertoire d'installation de l'extension a été localisé à l'adresse suivante sur le système de fichiers :
28+
29+
* `C:/Users/jsboi/.vscode/extensions/rooveterinaryinc.roo-cline-3.25.6/`
30+
31+
L'analyse de ce répertoire a montré qu'il contenait également un sous-répertoire `dist/` dont la structure est identique à celle produite par le script de build local.
32+
33+
---
34+
35+
## 4. Conclusion : Procédure de Test par "Hot-Swapping"
36+
37+
Le test local par remplacement manuel des fichiers ("hot-swapping") est **confirmé comme étant une méthode viable**.
38+
39+
La procédure est la suivante :
40+
41+
1. **Compiler localement :** Exécuter la commande `pnpm bundle` dans le répertoire `roo-code/src/`.
42+
2. **Copier les artefacts :** Remplacer le contenu du répertoire `dist/` de l'extension installée (`C:/Users/jsboi/.vscode/extensions/rooveterinaryinc.roo-cline-3.25.6/dist/`) par le contenu du répertoire `dist/` généré localement (`roo-code/src/dist/`).
43+
3. **Redémarrer VS Code :** Un redémarrage complet de l'éditeur est nécessaire pour qu'il charge les nouveaux fichiers de l'extension.

src/core/logging/FileLogger.ts

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
import { createWriteStream, WriteStream } from "fs"
4+
5+
export type LogLevel = "INFO" | "WARN" | "ERROR" | "DEBUG"
6+
7+
export interface LogMetadata {
8+
[key: string]: any
9+
}
10+
11+
export interface LogEntry {
12+
timestamp: string
13+
level: LogLevel
14+
component: string
15+
message: string
16+
metadata?: LogMetadata
17+
}
18+
19+
/**
20+
* FileLogger - Système de journalisation persistante pour diagnostic des crashes webview
21+
*
22+
* Fonctionnalités :
23+
* - Logging persistant survit aux crashes de webview
24+
* - Support des niveaux de log (INFO, WARN, ERROR, DEBUG)
25+
* - Métadonnées structurées pour contexte enrichi
26+
* - Rotation automatique des logs si taille dépassée
27+
* - Thread-safe avec gestion d'erreurs gracieuse
28+
*/
29+
export class FileLogger {
30+
private logFilePath: string
31+
private logStream?: WriteStream
32+
private isInitialized: boolean = false
33+
private writeQueue: string[] = []
34+
private isWriting: boolean = false
35+
private maxLogFileSize: number = 10 * 1024 * 1024 // 10MB par défaut
36+
private maxLogFiles: number = 5
37+
38+
constructor(baseDir: string, filename: string = "roo-code-debug.log") {
39+
// Créer le répertoire .logs dans le baseDir
40+
const logsDir = path.join(baseDir, ".logs")
41+
this.logFilePath = path.join(logsDir, filename)
42+
43+
// Initialisation asynchrone pour éviter de bloquer le constructeur
44+
this.initialize().catch((error) => {
45+
console.error(`[FileLogger] Failed to initialize: ${error}`)
46+
})
47+
}
48+
49+
/**
50+
* Initialise le logger et crée le répertoire si nécessaire
51+
*/
52+
private async initialize(): Promise<void> {
53+
try {
54+
// Créer le répertoire .logs s'il n'existe pas
55+
const logsDir = path.dirname(this.logFilePath)
56+
await fs.promises.mkdir(logsDir, { recursive: true })
57+
58+
// Vérifier si rotation nécessaire
59+
await this.checkAndRotateLog()
60+
61+
// Créer le stream de log
62+
this.logStream = createWriteStream(this.logFilePath, { flags: "a", encoding: "utf8" })
63+
64+
// Gérer les erreurs du stream
65+
this.logStream.on("error", (error) => {
66+
console.error(`[FileLogger] Stream error: ${error}`)
67+
})
68+
69+
this.isInitialized = true
70+
71+
// Écrire les messages en attente
72+
await this.processWriteQueue()
73+
74+
// Log d'initialisation
75+
await this.log("INFO", "FILE_LOGGER", "FileLogger initialized successfully", {
76+
logFilePath: this.logFilePath,
77+
timestamp: new Date().toISOString(),
78+
})
79+
} catch (error) {
80+
console.error(`[FileLogger] Initialization failed: ${error}`)
81+
this.isInitialized = false
82+
}
83+
}
84+
85+
/**
86+
* Vérifie la taille du fichier log et effectue une rotation si nécessaire
87+
*/
88+
private async checkAndRotateLog(): Promise<void> {
89+
try {
90+
const stats = await fs.promises.stat(this.logFilePath)
91+
92+
if (stats.size > this.maxLogFileSize) {
93+
await this.rotateLogFiles()
94+
}
95+
} catch (error) {
96+
// Fichier n'existe pas encore, pas d'action nécessaire
97+
if (error.code !== "ENOENT") {
98+
console.error(`[FileLogger] Error checking log file size: ${error}`)
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Effectue la rotation des fichiers de log
105+
*/
106+
private async rotateLogFiles(): Promise<void> {
107+
try {
108+
const baseFilename = this.logFilePath
109+
const dir = path.dirname(baseFilename)
110+
const ext = path.extname(baseFilename)
111+
const name = path.basename(baseFilename, ext)
112+
113+
// Décaler les fichiers existants (.1 -> .2, .2 -> .3, etc.)
114+
for (let i = this.maxLogFiles - 1; i >= 1; i--) {
115+
const currentFile = path.join(dir, `${name}.${i}${ext}`)
116+
const nextFile = path.join(dir, `${name}.${i + 1}${ext}`)
117+
118+
try {
119+
await fs.promises.access(currentFile)
120+
if (i === this.maxLogFiles - 1) {
121+
// Supprimer le plus ancien
122+
await fs.promises.unlink(currentFile)
123+
} else {
124+
// Renommer vers le suivant
125+
await fs.promises.rename(currentFile, nextFile)
126+
}
127+
} catch {
128+
// Fichier n'existe pas, continuer
129+
}
130+
}
131+
132+
// Renommer le fichier actuel vers .1
133+
const rotatedFile = path.join(dir, `${name}.1${ext}`)
134+
try {
135+
await fs.promises.rename(baseFilename, rotatedFile)
136+
} catch (error) {
137+
console.error(`[FileLogger] Error rotating main log file: ${error}`)
138+
}
139+
} catch (error) {
140+
console.error(`[FileLogger] Error during log rotation: ${error}`)
141+
}
142+
}
143+
144+
/**
145+
* Traite la queue d'écriture
146+
*/
147+
private async processWriteQueue(): Promise<void> {
148+
if (this.isWriting || !this.isInitialized || this.writeQueue.length === 0) {
149+
return
150+
}
151+
152+
this.isWriting = true
153+
154+
try {
155+
while (this.writeQueue.length > 0) {
156+
const logLine = this.writeQueue.shift()
157+
if (logLine && this.logStream) {
158+
await new Promise<void>((resolve, reject) => {
159+
this.logStream!.write(logLine, (error) => {
160+
if (error) reject(error)
161+
else resolve()
162+
})
163+
})
164+
}
165+
}
166+
} catch (error) {
167+
console.error(`[FileLogger] Error processing write queue: ${error}`)
168+
} finally {
169+
this.isWriting = false
170+
}
171+
}
172+
173+
/**
174+
* Log un message avec le niveau spécifié
175+
*/
176+
async log(level: LogLevel, component: string, message: string, metadata?: LogMetadata): Promise<void> {
177+
const logEntry: LogEntry = {
178+
timestamp: new Date().toISOString(),
179+
level,
180+
component,
181+
message,
182+
metadata,
183+
}
184+
185+
// Formatter la ligne de log
186+
const logLine = this.formatLogEntry(logEntry)
187+
188+
// Ajouter à la queue
189+
this.writeQueue.push(logLine)
190+
191+
// Traiter la queue si possible
192+
if (this.isInitialized) {
193+
await this.processWriteQueue()
194+
}
195+
196+
// Aussi logger dans la console pour les erreurs
197+
if (level === "ERROR" || level === "WARN") {
198+
console.log(`[${level}] ${component}: ${message}`, metadata || "")
199+
}
200+
}
201+
202+
/**
203+
* Formate une entrée de log en ligne de texte
204+
*/
205+
private formatLogEntry(entry: LogEntry): string {
206+
const metadataStr = entry.metadata ? ` | ${JSON.stringify(entry.metadata)}` : ""
207+
return `[${entry.timestamp}] ${entry.level} ${entry.component}: ${entry.message}${metadataStr}\n`
208+
}
209+
210+
/**
211+
* Méthodes de convenance pour chaque niveau
212+
*/
213+
async info(component: string, message: string, metadata?: LogMetadata): Promise<void> {
214+
return this.log("INFO", component, message, metadata)
215+
}
216+
217+
async warn(component: string, message: string, metadata?: LogMetadata): Promise<void> {
218+
return this.log("WARN", component, message, metadata)
219+
}
220+
221+
async error(component: string, message: string, metadata?: LogMetadata): Promise<void> {
222+
return this.log("ERROR", component, message, metadata)
223+
}
224+
225+
async debug(component: string, message: string, metadata?: LogMetadata): Promise<void> {
226+
return this.log("DEBUG", component, message, metadata)
227+
}
228+
229+
/**
230+
* Force l'écriture de tous les logs en attente et ferme le stream
231+
*/
232+
async dispose(): Promise<void> {
233+
try {
234+
// Traiter tous les messages en attente
235+
await this.processWriteQueue()
236+
237+
// Log de fermeture
238+
if (this.isInitialized) {
239+
await this.log("INFO", "FILE_LOGGER", "FileLogger disposing", {
240+
pendingMessages: this.writeQueue.length,
241+
})
242+
}
243+
244+
// Fermer le stream
245+
if (this.logStream) {
246+
await new Promise<void>((resolve, reject) => {
247+
this.logStream!.end((error: any) => {
248+
if (error) reject(error)
249+
else resolve()
250+
})
251+
})
252+
this.logStream = undefined
253+
}
254+
255+
this.isInitialized = false
256+
} catch (error) {
257+
console.error(`[FileLogger] Error during disposal: ${error}`)
258+
}
259+
}
260+
261+
/**
262+
* Retourne le chemin du fichier de log actuel
263+
*/
264+
getLogFilePath(): string {
265+
return this.logFilePath
266+
}
267+
268+
/**
269+
* Vérifie si le logger est initialisé
270+
*/
271+
isReady(): boolean {
272+
return this.isInitialized
273+
}
274+
}

0 commit comments

Comments
 (0)