Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/serverHelpers/fileVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as vscode from 'vscode';

export async function toggleFileVisibility(fileName: string, hide: boolean): Promise<void> {
const config = vscode.workspace.getConfiguration('files');
const exclude = { ...config.get<Record<string, unknown>>('exclude') };

const isCurrentlyHidden = !!exclude[fileName];
if (hide === isCurrentlyHidden) return;

if (hide) {
exclude[fileName] = true;
} else {
delete exclude[fileName];
}

await config.update('exclude', exclude, vscode.ConfigurationTarget.Workspace);
}
29 changes: 29 additions & 0 deletions src/serverHelpers/routerBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as path from 'path';

export function buildRouterContent(injectionScript: string): string {
return `<?php
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$file = __DIR__ . $path;

if (is_dir($file)) {
$file = rtrim($file, "/") . "/index.php";
}

if (file_exists($file) && pathinfo($file, PATHINFO_EXTENSION) === "php") {
ob_start();
include $file;
$content = ob_get_clean();
if (strpos($content, '</body>') !== false) {
echo str_replace("</body>", "${injectionScript}</body>", $content);
} else {
echo $content . "${injectionScript}";
}
} else {
return false;
}
`;
}

export function getRouterFilePath(rootPath: string): string {
return path.join(rootPath, '.phive_router.php');
}
58 changes: 58 additions & 0 deletions src/serverHelpers/serverLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as vscode from 'vscode';

export class ServerLogger {
private readonly outputChannel: vscode.OutputChannel;
private requestCount = 0;

constructor(outputChannel: vscode.OutputChannel) {
this.outputChannel = outputChannel;
}

public reset(): void {
this.requestCount = 0;
}

public logInfo(message: string): void {
this.outputChannel.appendLine(message);
}

public logRequest(rawLog: string, time: string): void {
if (!rawLog) return;

if (rawLog.includes('Accepted')) {
this.requestCount++;
this.logInfo(`[INFO] [Req #${this.requestCount}] ${time} - ${rawLog}`);
return;
}

if (rawLog.includes('Closing')) {
this.logInfo(`[DEBUG] ${time} - ${rawLog}`);
return;
}

this.logInfo(this.formatHttpLog(rawLog, time));
}

private formatHttpLog(rawLog: string, time: string): string {
const statusCodeMatch = rawLog.match(/\b([1-5]\d\d)\b/);

if (statusCodeMatch) {
const statusCode = parseInt(statusCodeMatch[1], 10);

if (statusCode >= 200 && statusCode < 300) {
return `[INFO] [${statusCode} OK] ${time} - ${rawLog}`;
}
if (statusCode >= 300 && statusCode < 400) {
return `[WARN] [${statusCode} REDIRECT] ${time} - ${rawLog}`;
}
if (statusCode >= 400 && statusCode < 500) {
return `[WARN] [${statusCode} NOT FOUND] ${time} - ${rawLog}`;
}
if (statusCode >= 500) {
return `[ERROR] [${statusCode} SERVER ERROR] ${time} - ${rawLog}`;
}
}

return `[LOG] ${time} - ${rawLog}`;
}
}
119 changes: 23 additions & 96 deletions src/serverManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import * as cp from 'child_process';
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import { buildRouterContent, getRouterFilePath } from './serverHelpers/routerBuilder';
import { ServerLogger } from './serverHelpers/serverLogger';
import { toggleFileVisibility } from './serverHelpers/fileVisibility';

export class PHPStackManager {
private _process: cp.ChildProcess | undefined;
private _outputChannel: vscode.OutputChannel;
private _logger: ServerLogger;
private _routerPath: string | undefined;
private _requestCount = 0;

// Métadonnées de session conservées pour permettre le redémarrage à chaud (v1.1.5)
private _lastServerParams: {
Expand All @@ -21,6 +24,7 @@ export class PHPStackManager {
constructor() {
// v1.1.6 : Utilisation de la grammaire "log" pour activer la coloration syntaxique native (Light/Dark mode)
this._outputChannel = vscode.window.createOutputChannel("Phive Server Logs", "log");
this._logger = new ServerLogger(this._outputChannel);
}

/**
Expand All @@ -31,15 +35,15 @@ export class PHPStackManager {
this._lastServerParams = { rootPath, host, port, wsPort, ip };

this.stopProcessOnly();
this._requestCount = 0;
this._logger.reset();

// 1. Récupérer le chemin PHP depuis la configuration
const config = vscode.workspace.getConfiguration('phive');
const phpBinary = config.get<string>('phpPath') || 'php';

this._outputChannel.clear();
this._outputChannel.show();
this._outputChannel.appendLine(`[INFO] [Phive] Attempting to start using: ${phpBinary}`);
this._logger.logInfo(`[INFO] [Phive] Attempting to start using: ${phpBinary}`);

// 2. Script JS à injecter (Live Reload)
const injectionScript = `
Expand All @@ -59,33 +63,13 @@ export class PHPStackManager {

// 3. Création du fichier Router PHP temporaire
const routerFileName = '.phive_router.php';
this._routerPath = path.join(rootPath, routerFileName);
const routerContent = `<?php
$path = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$file = __DIR__ . $path;

if (is_dir($file)) {
$file = rtrim($file, "/") . "/index.php";
}

if (file_exists($file) && pathinfo($file, PATHINFO_EXTENSION) === "php") {
ob_start();
include $file;
$content = ob_get_clean();
if (strpos($content, '</body>') !== false) {
echo str_replace("</body>", "${injectionScript}</body>", $content);
} else {
echo $content . "${injectionScript}";
}
} else {
return false;
}
`;
this._routerPath = getRouterFilePath(rootPath);
const routerContent = buildRouterContent(injectionScript);

try {
fs.writeFileSync(this._routerPath, routerContent);
// Masquer le fichier dans l'explorateur VS Code
this._toggleFileVisibility(routerFileName, true);
await toggleFileVisibility(routerFileName, true);
} catch (err) {
vscode.window.showErrorMessage(`Failed to create router file: ${err}`);
return;
Expand All @@ -96,72 +80,35 @@ export class PHPStackManager {
cwd: rootPath
});

this._outputChannel.appendLine(`[INFO] [Phive] Server started: http://${ip}:${port}`);
this._logger.logInfo(`[INFO] [Phive] Server started: http://${ip}:${port}`);

// 5. Gestion des logs et erreurs (v1.1.6 : Formatage coloré selon le statut HTTP)
this._process.stderr?.on('data', (data) => {
const rawLog = data.toString().trim();
if (!rawLog) return;

const time = new Date().toLocaleTimeString();

if (rawLog.includes('Accepted')) {
this._requestCount++;
this._outputChannel.appendLine(`[INFO] [Req #${this._requestCount}] ${time} - ${rawLog}`);
} else if (rawLog.includes('Closing') || rawLog.includes('Closing')) {
this._outputChannel.appendLine(`[DEBUG] ${time} - ${rawLog}`);
} else {
// Analyse du code statut HTTP retourné par le serveur de dev PHP
const formattedLog = this.formatHttpLog(rawLog, time);
this._outputChannel.appendLine(formattedLog);
}
this._logger.logRequest(rawLog, time);
});

this._process.stdout?.on('data', (data) => {
this._outputChannel.appendLine(`[INFO] ${data.toString().trim()}`);
this._logger.logInfo(`[INFO] ${data.toString().trim()}`);
});

this._process.on('close', (code) => {
this._outputChannel.appendLine(`[WARN] [Phive] Server stopped (Code: ${code})`);
this._cleanup();
this._process.on('close', async (code) => {
this._logger.logInfo(`[WARN] [Phive] Server stopped (Code: ${code})`);
await this._cleanup();
});

this._process.on('error', (err: any) => {
this._process.on('error', async (err: any) => {
const errorMsg = err.code === 'ENOENT'
? `PHP executable not found at "${phpBinary}". Check your Phive settings.`
: `PHP Error: ${err.message}`;

this._outputChannel.appendLine(`[ERROR] ${errorMsg}`);
this._logger.logInfo(`[ERROR] ${errorMsg}`);
vscode.window.showErrorMessage(errorMsg);
this._cleanup();
await this._cleanup();
});
}

/**
* Formatage visuel des requêtes HTTP (v1.1.6)
* Ajoute des préfixes standardisés pris en charge par le moteur de coloration de VS Code.
*/
private formatHttpLog(rawLog: string, time: string): string {
// Extraction du code HTTP (ex: 200, 404, 500)
const statusCodeMatch = rawLog.match(/\b([1-5]\d\d)\b/);

if (statusCodeMatch) {
const statusCode = parseInt(statusCodeMatch[1], 10);

if (statusCode >= 200 && statusCode < 300) {
return `[INFO] [${statusCode} OK] ${time} - ${rawLog}`; // Vert en thème VS Code Log
} else if (statusCode >= 300 && statusCode < 400) {
return `[WARN] [${statusCode} REDIRECT] ${time} - ${rawLog}`; // Jaune
} else if (statusCode >= 400 && statusCode < 500) {
return `[WARN] [${statusCode} NOT FOUND] ${time} - ${rawLog}`; // Jaune / Orange
} else if (statusCode >= 500) {
return `[ERROR] [${statusCode} SERVER ERROR] ${time} - ${rawLog}`; // Rouge
}
}

return `[LOG] ${time} - ${rawLog}`;
}

/**
* Redémarre à chaud le serveur PHP (Utile pour recharger les fichiers d'environnement .env)
*/
Expand Down Expand Up @@ -192,25 +139,25 @@ export class PHPStackManager {
/**
* Arrête le processus PHP, notifie l'utilisateur et nettoie les fichiers temporaires
*/
public stop() {
public async stop() {
if (this._process) {
this._process.kill();
this._process = undefined;
vscode.window.showInformationMessage("Phive server stopped.");
}
this._lastServerParams = undefined;
this._cleanup();
await this._cleanup();
}

/**
* Supprime le fichier router et le réaffiche dans VS Code
*/
private _cleanup() {
private async _cleanup() {
if (this._routerPath) {
const routerFileName = path.basename(this._routerPath);

// 1. Réafficher le fichier avant de le supprimer pour éviter les résidus de config
this._toggleFileVisibility(routerFileName, false);
await toggleFileVisibility(routerFileName, false);

// 2. Suppression physique
if (fs.existsSync(this._routerPath)) {
Expand All @@ -222,24 +169,4 @@ export class PHPStackManager {
}
}
}

/**
* Ajoute ou retire le fichier de la liste d'exclusion de VS Code
*/
private async _toggleFileVisibility(fileName: string, hide: boolean) {
const config = vscode.workspace.getConfiguration('files');
// On récupère une copie profonde pour ne pas muter l'original directement
const exclude = { ...config.get<any>('exclude') };

const isCurrentlyHidden = !!exclude[fileName];
if (hide === isCurrentlyHidden) return; // Pas de changement nécessaire

if (hide) {
exclude[fileName] = true;
} else {
delete exclude[fileName];
}

await config.update('exclude', exclude, vscode.ConfigurationTarget.Workspace);
}
}
Loading