UNPKG

jlink-mcp

Version:

MCP server for SEGGER J-Link debug probes — LLM-driven embedded debugging with RTT, GDB server, and Trice/Pigweed support

1,233 lines (1,223 loc) 39.9 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/utils/config.ts function findJLinkInstallDir() { const candidates = [ "/opt/SEGGER/JLink", "/usr/local/SEGGER/JLink", "/Applications/SEGGER/JLink", "C:\\Program Files\\SEGGER\\JLink", "C:\\Program Files (x86)\\SEGGER\\JLink" ]; for (const dir of candidates) { if (fs.existsSync(dir)) return dir; } for (const base of ["/opt/SEGGER", "/Applications/SEGGER", "/usr/local/SEGGER"]) { if (fs.existsSync(base)) { try { const entries = fs.readdirSync(base).filter((e) => e.startsWith("JLink")); if (entries.length > 0) { return path.join(base, entries.sort().reverse()[0]); } } catch { } } } return ""; } function getConfig() { const cfg = vscode.workspace.getConfiguration("jlinkMcp"); return { jlink: { installDir: cfg.get("jlink.installDir") || findJLinkInstallDir(), device: cfg.get("jlink.device") || "Unspecified", interface: cfg.get("jlink.interface") || "SWD", speed: cfg.get("jlink.speed") || 4e3, serialNumber: cfg.get("jlink.serialNumber") || void 0, gdbPort: cfg.get("jlink.gdbPort") || 2331, rttTelnetPort: cfg.get("jlink.rttTelnetPort") || 19021, swoTelnetPort: cfg.get("jlink.swoTelnetPort") || 2332 }, telnetProxy: { listenPort: cfg.get("telnetProxy.listenPort") || 19400, sourcePort: cfg.get("telnetProxy.sourcePort") || 19021, sourceHost: cfg.get("telnetProxy.sourceHost") || "localhost" }, trice: { binaryPath: cfg.get("trice.binaryPath") || "trice", idListPath: cfg.get("trice.idListPath") || "", encoding: cfg.get("trice.encoding") || "TREX" }, pigweed: { tokenDatabase: cfg.get("pigweed.tokenDatabase") || "", pythonPath: cfg.get("pigweed.pythonPath") || "python3" } }; } function getJLinkExePath(config) { const exe = process.platform === "win32" ? "JLink.exe" : "JLinkExe"; return config.installDir ? path.join(config.installDir, exe) : exe; } function getJLinkGDBServerPath(config) { const exe = process.platform === "win32" ? "JLinkGDBServerCL.exe" : "JLinkGDBServerCLExe"; return config.installDir ? path.join(config.installDir, exe) : exe; } var vscode, path, fs; var init_config = __esm({ "src/utils/config.ts"() { "use strict"; vscode = __toESM(require("vscode")); path = __toESM(require("path")); fs = __toESM(require("fs")); } }); // src/utils/logger.ts function initLogger(channel) { outputChannel = channel; } function log(message) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const line = `[${timestamp}] ${message}`; outputChannel?.appendLine(line); } function logError(message, error) { const errMsg = error instanceof Error ? error.message : String(error ?? ""); log(`ERROR: ${message}${errMsg ? ` - ${errMsg}` : ""}`); } var outputChannel; var init_logger = __esm({ "src/utils/logger.ts"() { "use strict"; } }); // src/jlink/commander.ts var commander_exports = {}; __export(commander_exports, { clearBreakpoints: () => clearBreakpoints, decodeFaultRegisters: () => decodeFaultRegisters, eraseChip: () => eraseChip, executeJLinkCommands: () => executeJLinkCommands, executeRawCommands: () => executeRawCommands, flashFirmware: () => flashFirmware, formatRegistersCompact: () => formatRegistersCompact, getDeviceInfo: () => getDeviceInfo, haltDevice: () => haltDevice, parseMemoryDump: () => parseMemoryDump, parseRegisters: () => parseRegisters, readAllRegisters: () => readAllRegisters, readFaultRegisters: () => readFaultRegisters, readMemory: () => readMemory, readRegister: () => readRegister, resetDevice: () => resetDevice, resumeDevice: () => resumeDevice, setBreakpoint: () => setBreakpoint, stepInstruction: () => stepInstruction, stripBoilerplate: () => stripBoilerplate, writeMemory: () => writeMemory }); function stripBoilerplate(raw) { const lines = raw.split("\n"); const meaningful = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; const isBoilerplate = BOILERPLATE_PATTERNS.some((p) => p.test(trimmed)); if (!isBoilerplate) { meaningful.push(line); } } return meaningful.join("\n").trim(); } function parseRegisters(raw) { const regs = {}; const regPatterns = [ // "PC = 0000BF54, CycleCnt = 0000855D" /(\w+)\s*=\s*([0-9A-Fa-f]{2,8})(?:,|\s|$)/g, // "SP(R13)= 20062880" /(\w+)\((\w+)\)\s*=\s*([0-9A-Fa-f]{2,8})/g ]; for (const line of raw.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("SEGGER") || trimmed.startsWith("Connecting")) continue; let match; const simple = /(\w[\w()]*)\s*=\s*([0-9A-Fa-f]{2,8})/g; while ((match = simple.exec(trimmed)) !== null) { let name = match[1]; const value = match[2]; const parenMatch = name.match(/^(\w+)\(\w+\)$/); if (parenMatch) name = parenMatch[1]; regs[name] = `0x${value}`; } const xpsrMatch = trimmed.match(/APSR\s*=\s*(\w+)/); if (xpsrMatch) regs["APSR"] = xpsrMatch[1]; } return Object.keys(regs).length > 0 ? regs : null; } function formatRegistersCompact(regs) { const core = ["PC", "SP", "LR", "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7", "R8", "R9", "R10", "R11", "R12"]; const status = ["XPSR", "CONTROL", "PRIMASK", "BASEPRI", "FAULTMASK"]; const stack = ["MSP", "PSP", "MSPLIM", "PSPLIM"]; const lines = []; const coreVals = core.filter((r) => regs[r]).map((r) => `${r}=${regs[r]}`); lines.push("Core: " + coreVals.join(" ")); const statusVals = status.filter((r) => regs[r]).map((r) => `${r}=${regs[r]}`); if (statusVals.length > 0) lines.push("Status: " + statusVals.join(" ")); const stackVals = stack.filter((r) => regs[r]).map((r) => `${r}=${regs[r]}`); if (stackVals.length > 0) lines.push("Stack: " + stackVals.join(" ")); const fpNonZero = Object.entries(regs).filter(([k, v]) => k.startsWith("FPS") && v !== "0x00000000").map(([k, v]) => `${k}=${v}`); if (fpNonZero.length > 0) lines.push("FP (non-zero): " + fpNonZero.join(" ")); return lines.join("\n"); } function parseMemoryDump(raw) { const results = []; for (const line of raw.split("\n")) { const match = line.match(/^([0-9A-Fa-f]{8})\s*=\s*(.+?)\s{2,}(.*)$/); if (match) { results.push({ address: `0x${match[1]}`, hex: match[2].trim(), ascii: match[3].trim() }); } } return results; } function decodeFaultRegisters(cfsr, hfsr, mmfar, bfar) { const lines = []; const mmfsr = cfsr & 255; const bfsr = cfsr >> 8 & 255; const ufsr = cfsr >> 16 & 65535; if (cfsr === 0 && hfsr === 0) { lines.push("No faults detected (CFSR=0, HFSR=0)"); return lines.join("\n"); } if (mmfsr) { lines.push("## MemManage Fault (MMFSR):"); if (mmfsr & 1) lines.push(" - IACCVIOL: Instruction access violation"); if (mmfsr & 2) lines.push(" - DACCVIOL: Data access violation"); if (mmfsr & 8) lines.push(" - MUNSTKERR: MemManage on unstacking (exception return)"); if (mmfsr & 16) lines.push(" - MSTKERR: MemManage on stacking (exception entry)"); if (mmfsr & 32) lines.push(" - MLSPERR: MemManage during FP lazy state preservation"); if (mmfsr & 128) { lines.push(` - MMARVALID: Faulting address = 0x${mmfar.toString(16).padStart(8, "0")}`); } } if (bfsr) { lines.push("## BusFault (BFSR):"); if (bfsr & 1) lines.push(" - IBUSERR: Instruction bus error"); if (bfsr & 2) lines.push(" - PRECISERR: Precise data bus error"); if (bfsr & 4) lines.push(" - IMPRECISERR: Imprecise data bus error"); if (bfsr & 8) lines.push(" - UNSTKERR: BusFault on unstacking"); if (bfsr & 16) lines.push(" - STKERR: BusFault on stacking"); if (bfsr & 32) lines.push(" - LSPERR: BusFault during FP lazy state preservation"); if (bfsr & 128) { lines.push(` - BFARVALID: Faulting address = 0x${bfar.toString(16).padStart(8, "0")}`); } } if (ufsr) { lines.push("## UsageFault (UFSR):"); if (ufsr & 1) lines.push(" - UNDEFINSTR: Undefined instruction"); if (ufsr & 2) lines.push(" - INVSTATE: Invalid state (e.g., Thumb bit)"); if (ufsr & 4) lines.push(" - INVPC: Invalid PC load (bad EXC_RETURN)"); if (ufsr & 8) lines.push(" - NOCP: No coprocessor"); if (ufsr & 16) lines.push(" - STKOF: Stack overflow detected"); if (ufsr & 256) lines.push(" - UNALIGNED: Unaligned memory access"); if (ufsr & 512) lines.push(" - DIVBYZERO: Division by zero"); } if (hfsr) { lines.push("## HardFault (HFSR):"); if (hfsr & 2) lines.push(" - VECTTBL: Vector table read fault on exception processing"); if (hfsr & 1073741824) lines.push(" - FORCED: Forced HardFault (escalated from configurable fault)"); if (hfsr & 2147483648) lines.push(" - DEBUGEVT: Debug event triggered HardFault"); } return lines.join("\n"); } async function executeJLinkCommands(commands2, deviceOverride) { const config = getConfig(); const jlinkExe = getJLinkExePath(config.jlink); const device = deviceOverride || config.jlink.device; const scriptLines = [...commands2, "exit"]; const args = [ "-device", device, "-if", config.jlink.interface, "-speed", String(config.jlink.speed), "-autoconnect", "1", "-ExitOnError", "1", "-NoGui", "1" ]; if (config.jlink.serialNumber) { args.push("-SelectEmuBySN", config.jlink.serialNumber); } log(`JLink Commander: ${commands2.join("; ")}`); return new Promise((resolve) => { const proc = (0, import_child_process2.spawn)(jlinkExe, args, { stdio: ["pipe", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; proc.stdout?.on("data", (data) => { stdout += data.toString(); }); proc.stderr?.on("data", (data) => { stderr += data.toString(); }); const script = scriptLines.join("\n") + "\n"; proc.stdin?.write(script); proc.stdin?.end(); proc.on("error", (err) => { logError("JLink Commander spawn error", err); resolve({ success: false, rawOutput: stdout, output: stdout, error: `Failed to spawn JLinkExe: ${err.message}. Is J-Link installed at ${config.jlink.installDir}?` }); }); proc.on("exit", (code) => { const success = code === 0; if (!success) { logError(`JLink Commander exited with code ${code}`); } resolve({ success, rawOutput: stdout, output: stripBoilerplate(stdout), error: stderr || void 0 }); }); setTimeout(() => { proc.kill("SIGTERM"); resolve({ success: false, rawOutput: stdout, output: stripBoilerplate(stdout), error: "JLink Commander timed out after 30 seconds" }); }, 3e4); }); } async function getDeviceInfo() { return executeJLinkCommands(["halt", "regs"]); } async function haltDevice() { return executeJLinkCommands(["halt"]); } async function resumeDevice() { return executeJLinkCommands(["go"]); } async function resetDevice(halt = false) { return executeJLinkCommands(halt ? ["r", "halt"] : ["r", "go"]); } async function readMemory(address, numBytes) { const addrHex = `0x${address.toString(16)}`; return executeJLinkCommands([`mem ${addrHex}, ${numBytes}`]); } async function writeMemory(address, value) { const addrHex = `0x${address.toString(16)}`; const valHex = `0x${value.toString(16)}`; return executeJLinkCommands([`w4 ${addrHex}, ${valHex}`]); } async function readRegister(register) { return executeJLinkCommands(["halt", `rreg ${register}`]); } async function readAllRegisters() { return executeJLinkCommands(["halt", "regs"]); } async function flashFirmware(filePath, baseAddress) { const addr = baseAddress !== void 0 ? ` 0x${baseAddress.toString(16)}` : ""; return executeJLinkCommands([ "r", "halt", `loadfile ${filePath}${addr}`, "r", "go" ]); } async function eraseChip() { return executeJLinkCommands(["erase"]); } async function setBreakpoint(address) { return executeJLinkCommands([`SetBP 0x${address.toString(16)}`]); } async function clearBreakpoints() { return executeJLinkCommands(["ClrBP"]); } async function stepInstruction() { return executeJLinkCommands(["halt", "s"]); } async function executeRawCommands(rawCommands) { return executeJLinkCommands(rawCommands); } async function readFaultRegisters() { const result = await executeJLinkCommands(["halt", "mem 0xE000ED28, 20"]); const dump = parseMemoryDump(result.rawOutput); let cfsr = 0, hfsr = 0, mmfar = 0, bfar = 0; if (dump.length > 0) { const allHex = dump.map((d) => d.hex).join(" "); const bytes = allHex.split(/\s+/).filter(Boolean); if (bytes.length >= 16) { cfsr = parseLittleEndian32(bytes, 0); hfsr = parseLittleEndian32(bytes, 4); mmfar = parseLittleEndian32(bytes, 12); bfar = parseLittleEndian32(bytes, 16); } } return { result, decoded: decodeFaultRegisters(cfsr, hfsr, mmfar, bfar), raw: { cfsr, hfsr, mmfar, bfar } }; } function parseLittleEndian32(bytes, offset) { if (offset + 3 >= bytes.length) return 0; return (parseInt(bytes[offset], 16) | parseInt(bytes[offset + 1], 16) << 8 | parseInt(bytes[offset + 2], 16) << 16 | parseInt(bytes[offset + 3], 16) << 24) >>> 0; } var import_child_process2, BOILERPLATE_PATTERNS; var init_commander = __esm({ "src/jlink/commander.ts"() { "use strict"; import_child_process2 = require("child_process"); init_config(); init_logger(); BOILERPLATE_PATTERNS = [ /^SEGGER J-Link Commander/, /^DLL version/, /^J-Link Commander will now exit/, /^Connecting to J-Link via USB/, /^Firmware: J-Link/, /^Hardware version:/, /^J-Link uptime/, /^S\/N:/, /^License\(s\):/, /^USB speed mode:/, /^VTref=/, /^Device ".*" selected/, /^Connecting to target via SWD/, /^Connecting to target via JTAG/, /^ConfigTargetSettings\(\)/, /^InitTarget\(\)/, /^Found SW-DP with ID/, /^DPIDR:/, /^CoreSight/, /^AP map detection/, /^AP\[\d+\]:/, /^CPUID register:/, /^Feature set:/, /^Cache:/, /^Found Cortex-/, /^FPUnit:/, /^Security extension: /, /^Secure debug:/, /^ROMTbl\[\d+\]/, /^\[\d+\]\[\d+\]:/, /^Memory zones:/, /^\s+Zone:/, /^Cortex-M\d+ identified/, /^Type "connect"/, /^Please specify/, /^Specify target/, /^$/, // blank lines /^J-Link>/, // prompt /^J-Link\[\d+\]:/, /^Syntax:/, /^Sleep\(\d+\)/, /^Script processing completed/ ]; } }); // src/extension.ts var extension_exports = {}; __export(extension_exports, { activate: () => activate, deactivate: () => deactivate }); module.exports = __toCommonJS(extension_exports); var vscode2 = __toESM(require("vscode")); // src/jlink/gdb-server.ts init_config(); init_logger(); var GDB_SERVER_PROCESS_NAME = "jlink-gdb-server"; var GDBServerManager = class { processManager; outputBuffer = []; maxOutputLines = 1e3; constructor(processManager2) { this.processManager = processManager2; } /** Start JLinkGDBServer */ start() { const existing = this.processManager.get(GDB_SERVER_PROCESS_NAME); if (existing) { return { success: true, message: "GDB Server is already running" }; } const config = getConfig(); const gdbServerPath = getJLinkGDBServerPath(config.jlink); const args = [ "-device", config.jlink.device, "-if", config.jlink.interface, "-speed", String(config.jlink.speed), "-port", String(config.jlink.gdbPort), "-RTTTelnetPort", String(config.jlink.rttTelnetPort), "-SWOPort", String(config.jlink.swoTelnetPort), "-vd", "-noir", "-LocalhostOnly", "1", "-singlerun" ]; if (config.jlink.serialNumber) { args.push("-select", `USB=${config.jlink.serialNumber}`); } try { const managed = this.processManager.spawn( GDB_SERVER_PROCESS_NAME, gdbServerPath, args ); managed.process.stdout?.on("data", (data) => { const lines = data.toString().split("\n").filter(Boolean); for (const line of lines) { log(`[GDB Server] ${line}`); this.outputBuffer.push(line); if (this.outputBuffer.length > this.maxOutputLines) { this.outputBuffer.shift(); } } }); managed.process.stderr?.on("data", (data) => { const lines = data.toString().split("\n").filter(Boolean); for (const line of lines) { logError(`[GDB Server] ${line}`); this.outputBuffer.push(`[ERR] ${line}`); } }); return { success: true, message: `GDB Server started on port ${config.jlink.gdbPort}, RTT telnet on port ${config.jlink.rttTelnetPort}` }; } catch (err) { logError("Failed to start GDB Server", err); return { success: false, message: `Failed to start GDB Server: ${err instanceof Error ? err.message : String(err)}` }; } } /** Stop the GDB Server */ stop() { const killed = this.processManager.kill(GDB_SERVER_PROCESS_NAME); this.outputBuffer = []; return { success: true, message: killed ? "GDB Server stopped" : "GDB Server was not running" }; } /** Check if running */ isRunning() { return !!this.processManager.get(GDB_SERVER_PROCESS_NAME); } /** Get recent output */ getRecentOutput(lines = 50) { return this.outputBuffer.slice(-lines); } /** Get status info */ getStatus() { const config = getConfig(); return { running: this.isRunning(), gdbPort: config.jlink.gdbPort, rttTelnetPort: config.jlink.rttTelnetPort, swoTelnetPort: config.jlink.swoTelnetPort }; } }; // src/rtt/rtt-client.ts var net = __toESM(require("net")); var import_events = require("events"); init_logger(); function stripAnsi(text) { return text.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\[0m/g, ""); } function isRttHeader(line) { return line.startsWith("SEGGER J-Link") || line.startsWith("Process: JLink") || line.trim() === ""; } function parseZephyrLog(line) { const match = line.match( /^\[(\d{2}:\d{2}:\d{2}\.\d{3},?\d{0,3})\]\s*<(\w+)>\s*(\w[\w._-]*):\s*(.*)$/ ); if (match) { return { deviceTime: match[1], level: match[2], module: match[3], message: match[4], raw: line }; } return { deviceTime: null, level: null, module: null, message: line, raw: line }; } var RTTClient = class extends import_events.EventEmitter { socket = null; host; port; messages = []; /** Flat buffer of all parsed log lines for searching */ allLines = []; maxMessages = 5e3; maxLines = 2e4; reconnectTimer = null; connected = false; lineBuffer = ""; constructor(host = "localhost", port = 19021) { super(); this.host = host; this.port = port; } /** Connect to RTT telnet port */ connect() { return new Promise((resolve, reject) => { if (this.connected) { resolve(); return; } this.socket = new net.Socket(); this.socket.on("connect", () => { log(`RTT Client connected to ${this.host}:${this.port}`); this.connected = true; this.emit("connected"); resolve(); }); this.socket.on("data", (data) => { const raw = data.toString(); const cleaned = stripAnsi(raw); this.lineBuffer += cleaned; const parts = this.lineBuffer.split("\n"); this.lineBuffer = parts.pop() || ""; const parsedLines = []; for (const part of parts) { const trimmed = part.trim(); if (!trimmed || isRttHeader(trimmed)) continue; parsedLines.push(parseZephyrLog(trimmed)); } if (parsedLines.length > 0) { const msg = { channel: 0, timestamp: /* @__PURE__ */ new Date(), rawData: raw, lines: parsedLines }; this.messages.push(msg); if (this.messages.length > this.maxMessages) { this.messages.shift(); } for (const line of parsedLines) { this.allLines.push(line); } while (this.allLines.length > this.maxLines) { this.allLines.shift(); } this.emit("data", msg); } }); this.socket.on("close", () => { log("RTT Client disconnected"); this.connected = false; this.emit("disconnected"); }); this.socket.on("error", (err) => { logError("RTT Client error", err); this.connected = false; reject(err); }); this.socket.connect(this.port, this.host); }); } /** Disconnect */ disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.socket) { this.socket.destroy(); this.socket = null; } this.connected = false; this.lineBuffer = ""; } /** Send data to RTT down-channel (host → device) */ send(data) { if (!this.socket || !this.connected) return false; this.socket.write(data); return true; } /** Get recent log lines as formatted text */ getLines(count = 50) { const lines = this.allLines.slice(-count); return lines.map((l) => { if (l.deviceTime && l.level && l.module) { return `[${l.deviceTime}] <${l.level}> ${l.module}: ${l.message}`; } return l.message; }); } /** Search/filter log lines */ search(opts) { let results = [...this.allLines]; if (opts.level) { const lvl = opts.level.toLowerCase(); results = results.filter((l) => l.level?.toLowerCase() === lvl); } if (opts.module) { const mod = opts.module.toLowerCase(); results = results.filter((l) => l.module?.toLowerCase().includes(mod)); } if (opts.pattern) { try { const re = new RegExp(opts.pattern, "i"); results = results.filter((l) => re.test(l.message) || re.test(l.raw)); } catch { const pat = opts.pattern.toLowerCase(); results = results.filter( (l) => l.message.toLowerCase().includes(pat) || l.raw.toLowerCase().includes(pat) ); } } if (opts.count) { results = results.slice(-opts.count); } return results; } /** Get messages (for backward compat / resource access) */ getMessages(count) { if (count) return this.messages.slice(-count); return [...this.messages]; } /** Clear all buffers */ clearBuffer() { this.messages = []; this.allLines = []; this.lineBuffer = ""; } /** Check connection status */ isConnected() { return this.connected; } /** Get buffer stats */ getStats() { return { connected: this.connected, lineCount: this.allLines.length, messageCount: this.messages.length, host: this.host, port: this.port }; } }; // src/telnet/telnet-proxy.ts var net2 = __toESM(require("net")); var import_events2 = require("events"); init_logger(); var TelnetProxy = class extends import_events2.EventEmitter { server = null; sourceSocket = null; clients = /* @__PURE__ */ new Set(); listenPort; sourceHost; sourcePort; running = false; dataBuffer = []; maxBufferLines = 2e3; constructor(listenPort = 19400, sourceHost = "localhost", sourcePort = 19021) { super(); this.listenPort = listenPort; this.sourceHost = sourceHost; this.sourcePort = sourcePort; } /** Start the proxy: connect to source and listen for clients */ async start() { if (this.running) { return { success: true, message: `Telnet proxy already running on port ${this.listenPort}` }; } try { await this.connectToSource(); await this.startServer(); this.running = true; return { success: true, message: `Telnet proxy listening on port ${this.listenPort}, connected to ${this.sourceHost}:${this.sourcePort}` }; } catch (err) { logError("Failed to start telnet proxy", err); return { success: false, message: `Failed to start telnet proxy: ${err instanceof Error ? err.message : String(err)}` }; } } connectToSource() { return new Promise((resolve, reject) => { this.sourceSocket = new net2.Socket(); this.sourceSocket.on("connect", () => { log(`Telnet proxy connected to source ${this.sourceHost}:${this.sourcePort}`); resolve(); }); this.sourceSocket.on("data", (data) => { const text = data.toString(); const lines = text.split("\n"); for (const line of lines) { if (line.trim()) { this.dataBuffer.push(line); if (this.dataBuffer.length > this.maxBufferLines) { this.dataBuffer.shift(); } } } for (const client of this.clients) { try { client.write(data); } catch { this.clients.delete(client); } } this.emit("data", text); }); this.sourceSocket.on("close", () => { log("Telnet proxy: source connection closed"); this.emit("sourceDisconnected"); }); this.sourceSocket.on("error", (err) => { logError("Telnet proxy source error", err); reject(err); }); this.sourceSocket.connect(this.sourcePort, this.sourceHost); }); } startServer() { return new Promise((resolve, reject) => { this.server = net2.createServer((client) => { log(`Telnet proxy: client connected from ${client.remoteAddress}:${client.remotePort}`); this.clients.add(client); client.on("close", () => { this.clients.delete(client); log("Telnet proxy: client disconnected"); }); client.on("error", () => { this.clients.delete(client); }); client.on("data", (data) => { if (this.sourceSocket && !this.sourceSocket.destroyed) { this.sourceSocket.write(data); } }); }); this.server.on("error", (err) => { logError("Telnet proxy server error", err); reject(err); }); this.server.listen(this.listenPort, "127.0.0.1", () => { log(`Telnet proxy server listening on port ${this.listenPort}`); resolve(); }); }); } /** Stop the proxy */ stop() { for (const client of this.clients) { client.destroy(); } this.clients.clear(); if (this.sourceSocket) { this.sourceSocket.destroy(); this.sourceSocket = null; } if (this.server) { this.server.close(); this.server = null; } this.running = false; this.dataBuffer = []; log("Telnet proxy stopped"); } /** Get buffered data */ getBuffer(lines) { if (lines) return this.dataBuffer.slice(-lines); return [...this.dataBuffer]; } /** Clear buffer */ clearBuffer() { this.dataBuffer = []; } /** Write data to source (device) */ writeToSource(data) { if (this.sourceSocket && !this.sourceSocket.destroyed) { this.sourceSocket.write(data); return true; } return false; } /** Get proxy status */ getStatus() { return { running: this.running, listenPort: this.listenPort, sourceConnected: !!this.sourceSocket && !this.sourceSocket.destroyed, clientCount: this.clients.size, bufferedLines: this.dataBuffer.length }; } }; // src/utils/process-manager.ts var import_child_process = require("child_process"); init_logger(); var import_events3 = require("events"); var ProcessManager = class extends import_events3.EventEmitter { processes = /* @__PURE__ */ new Map(); spawn(name, command, args, options) { this.kill(name); log(`Spawning process "${name}": ${command} ${args.join(" ")}`); const proc = (0, import_child_process.spawn)(command, args, { stdio: ["pipe", "pipe", "pipe"], ...options }); const managed = { process: proc, name, kill: () => this.kill(name) }; proc.on("error", (err) => { logError(`Process "${name}" error`, err); this.processes.delete(name); this.emit("processExit", name, null, err); }); proc.on("exit", (code, signal) => { log(`Process "${name}" exited (code=${code}, signal=${signal})`); this.processes.delete(name); this.emit("processExit", name, code, signal); }); this.processes.set(name, managed); return managed; } kill(name) { const existing = this.processes.get(name); if (existing) { log(`Killing process "${name}" (pid=${existing.process.pid})`); existing.process.kill("SIGTERM"); setTimeout(() => { try { existing.process.kill("SIGKILL"); } catch { } }, 3e3); this.processes.delete(name); return true; } return false; } get(name) { return this.processes.get(name); } killAll() { for (const [name] of this.processes) { this.kill(name); } } listRunning() { return Array.from(this.processes.keys()); } }; // src/extension.ts init_logger(); init_config(); var mcpServer; var processManager; var gdbServer; var rttClient; var telnetProxy; var outputChannel2; var rttOutputChannel; var statusBarItem; function activate(context) { outputChannel2 = vscode2.window.createOutputChannel("J-Link MCP"); rttOutputChannel = vscode2.window.createOutputChannel("J-Link RTT"); initLogger(outputChannel2); log("J-Link MCP Extension activating..."); const mcpDidChange = new vscode2.EventEmitter(); const mcpProvider = vscode2.lm.registerMcpServerDefinitionProvider( "jlinkMcp.mcpServer", { onDidChangeMcpServerDefinitions: mcpDidChange.event, provideMcpServerDefinitions(_token) { const cfg = vscode2.workspace.getConfiguration("jlinkMcp"); const serverScript = vscode2.Uri.joinPath( context.extensionUri, "out", "mcp", "standalone.js" ).fsPath; const env = {}; const device = cfg.get("jlink.device"); if (device && device !== "Unspecified") env["JLINK_DEVICE"] = device; const installDir = cfg.get("jlink.installDir"); if (installDir) env["JLINK_INSTALL_DIR"] = installDir; const iface = cfg.get("jlink.interface"); if (iface) env["JLINK_INTERFACE"] = iface; const speed = cfg.get("jlink.speed"); if (speed) env["JLINK_SPEED"] = speed; const serial = cfg.get("jlink.serialNumber"); if (serial) env["JLINK_SERIAL"] = serial; const gdbPort = cfg.get("jlink.gdbPort"); if (gdbPort) env["JLINK_GDB_PORT"] = gdbPort; const rttPort = cfg.get("jlink.rttTelnetPort"); if (rttPort) env["JLINK_RTT_PORT"] = rttPort; return [ new vscode2.McpStdioServerDefinition( "J-Link Debug Probe", process.execPath, // Use VSCode's bundled Node.js [serverScript], env, context.extension.packageJSON.version ) ]; }, resolveMcpServerDefinition(server, _token) { return server; } } ); context.subscriptions.push(mcpProvider, mcpDidChange); context.subscriptions.push( vscode2.workspace.onDidChangeConfiguration((e) => { if (e.affectsConfiguration("jlinkMcp")) { log("J-Link MCP settings changed, notifying VSCode MCP client"); mcpDidChange.fire(); } }) ); log("MCP server definition provider registered"); processManager = new ProcessManager(); const config = getConfig(); gdbServer = new GDBServerManager(processManager); rttClient = new RTTClient("localhost", config.jlink.rttTelnetPort); telnetProxy = new TelnetProxy( config.telnetProxy.listenPort, config.telnetProxy.sourceHost, config.telnetProxy.sourcePort ); rttClient.on("data", (msg) => { for (const line of msg.lines) { if (line.deviceTime && line.level && line.module) { rttOutputChannel?.appendLine(`[${line.deviceTime}] <${line.level}> ${line.module}: ${line.message}`); } else { rttOutputChannel?.appendLine(line.message); } } }); statusBarItem = vscode2.window.createStatusBarItem(vscode2.StatusBarAlignment.Left, 100); statusBarItem.text = "$(debug-disconnect) J-Link"; statusBarItem.tooltip = "J-Link MCP - Click for status"; statusBarItem.command = "jlinkMcp.showStatus"; statusBarItem.show(); context.subscriptions.push(statusBarItem); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.showStatus", async () => { const gdbStatus = gdbServer.getStatus(); const rttStats = rttClient.getStats(); const proxyStatus = telnetProxy.getStatus(); const configInfo = getConfig(); const statusText = [ "# J-Link MCP Status", "", `**Device:** ${configInfo.jlink.device}`, `**Interface:** ${configInfo.jlink.interface} @ ${configInfo.jlink.speed} kHz`, `**J-Link Install Dir:** ${configInfo.jlink.installDir || "(auto-detect)"}`, "", "## GDB Server", `- Running: ${gdbStatus.running ? "Yes" : "No"}`, `- GDB Port: ${gdbStatus.gdbPort}`, `- RTT Telnet Port: ${gdbStatus.rttTelnetPort}`, "", "## RTT", `- Connected: ${rttStats.connected ? "Yes" : "No"}`, `- Messages buffered: ${rttStats.messageCount}`, "", "## Telnet Proxy", `- Running: ${proxyStatus.running ? "Yes" : "No"}`, `- Listen Port: ${proxyStatus.listenPort}`, `- Clients Connected: ${proxyStatus.clientCount}`, `- Buffered Lines: ${proxyStatus.bufferedLines}` ].join("\n"); const doc = await vscode2.workspace.openTextDocument({ content: statusText, language: "markdown" }); await vscode2.window.showTextDocument(doc, { preview: true }); }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.startGdbServer", () => { const result = gdbServer.start(); if (result.success) { vscode2.window.showInformationMessage(result.message); updateStatusBar(true); } else { vscode2.window.showErrorMessage(result.message); } }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.stopGdbServer", () => { const result = gdbServer.stop(); vscode2.window.showInformationMessage(result.message); updateStatusBar(false); }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.connectRtt", async () => { try { await rttClient.connect(); vscode2.window.showInformationMessage("Connected to RTT"); rttOutputChannel.show(); } catch (err) { vscode2.window.showErrorMessage( `Failed to connect to RTT: ${err instanceof Error ? err.message : String(err)}` ); } }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.disconnectRtt", () => { rttClient.disconnect(); vscode2.window.showInformationMessage("Disconnected from RTT"); }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.startTelnetProxy", async () => { const result = await telnetProxy.start(); if (result.success) { vscode2.window.showInformationMessage(result.message); } else { vscode2.window.showErrorMessage(result.message); } }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.stopTelnetProxy", () => { telnetProxy.stop(); vscode2.window.showInformationMessage("Telnet proxy stopped"); }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.flashFirmware", async () => { const uri = await vscode2.window.showOpenDialog({ canSelectMany: false, filters: { "Firmware Files": ["hex", "bin", "elf"], "All Files": ["*"] }, title: "Select firmware file to flash" }); if (!uri || uri.length === 0) return; const filePath = uri[0].fsPath; const { flashFirmware: flashFirmware2 } = await Promise.resolve().then(() => (init_commander(), commander_exports)); vscode2.window.withProgress( { location: vscode2.ProgressLocation.Notification, title: "Flashing firmware..." }, async () => { const result = await flashFirmware2(filePath); if (result.success) { vscode2.window.showInformationMessage(`Firmware flashed successfully: ${filePath}`); } else { vscode2.window.showErrorMessage(`Flash failed: ${result.error || result.output}`); } } ); }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.showOutput", () => { outputChannel2.show(); }) ); context.subscriptions.push( vscode2.commands.registerCommand("jlinkMcp.showRttOutput", () => { rttOutputChannel.show(); }) ); context.subscriptions.push({ dispose() { rttClient?.disconnect(); telnetProxy?.stop(); processManager?.killAll(); mcpServer?.dispose(); } }); log("J-Link MCP Extension activated"); outputChannel2.show(true); } function updateStatusBar(gdbRunning) { if (!statusBarItem) return; if (gdbRunning) { statusBarItem.text = "$(debug) J-Link Connected"; statusBarItem.backgroundColor = void 0; } else { statusBarItem.text = "$(debug-disconnect) J-Link"; statusBarItem.backgroundColor = void 0; } } function deactivate() { log("J-Link MCP Extension deactivating..."); rttClient?.disconnect(); telnetProxy?.stop(); processManager?.killAll(); mcpServer?.dispose(); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { activate, deactivate }); //# sourceMappingURL=extension.js.map