Skip to content

Instantly share code, notes, and snippets.

@engalar
Last active October 27, 2025 07:41
Show Gist options
  • Select an option

  • Save engalar/56bb1de50e74b17c924e3208783e0bb4 to your computer and use it in GitHub Desktop.

Select an option

Save engalar/56bb1de50e74b17c924e3208783e0bb4 to your computer and use it in GitHub Desktop.
Mendix python plugin to collect support data.

Mendix Log Extractor Plugin

Business Requirements

Overview

A comprehensive Mendix Studio Pro plugin that extracts diagnostic information, logs, and project metadata to assist developers in troubleshooting, debugging, and forum support requests.

Core Requirements

1. Log Extraction

  • Studio Pro Logs: Extract logs from Windows AppData directory (%LOCALAPPDATA%\Mendix\log\{version}\log.txt)
  • Git Logs: Extract Git operation logs from (%LOCALAPPDATA%\Mendix\log\{version}\git\git.log.txt)
  • Dynamic Version Detection: Automatically detect current Mendix version from the active project
  • Log File Reading: Read last 1000 lines of log files with proper error handling

2. Project Analysis

  • Module Information: Extract all modules with their entities, microflows, and pages
  • JAR Dependencies: Scan userlib directory for Java dependencies
  • Frontend Components: Identify widgets and custom components in widgets and javascriptsource directories
  • File Metadata: Include file sizes, modification dates, and paths

3. Forum Integration

  • Automated Formatting: Convert extracted data into forum-friendly markdown format
  • Copy-to-Clipboard: One-click copy functionality for forum posts
  • Structured Output: Organized sections for version, modules, dependencies, and logs

4. User Experience

  • Tabbed Interface: Organized sections for different data types
  • Progress Indicators: Real-time feedback for long-running operations
  • Error Handling: Graceful handling of missing files and permissions
  • Responsive Design: Clean, modern UI using React and Tailwind CSS

Target Users

  • Mendix developers seeking technical support
  • Forum moderators assisting with troubleshooting
  • Developers debugging application issues
  • Team leads analyzing project structure

Technical Specification

Architecture

The plugin follows a clean architecture pattern with separation of concerns:

┌─────────────────────────────────────────────────────────────┐
│                    Frontend (React)                        │
├─────────────────────────────────────────────────────────────┤
│  UI Components  │  State Management  │  Communication     │
│  - Tab Panels   │  - React Hooks     │  - Message Passing │
│  - Progress UI  │  - RxJS Streams    │  - Error Handling  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                  Backend (Python)                          │
├─────────────────────────────────────────────────────────────┤
│  RPC Handlers   │  Job Handlers     │  Business Logic     │
│  - GetVersion   │  - ExtractAll     │  - LogExtractor     │
│  - GetLogs      │  - FormatForum    │  - File Operations  │
└─────────────────────────────────────────────────────────────┘
                              │
┌─────────────────────────────────────────────────────────────┐
│                  Data Sources                              │
├─────────────────────────────────────────────────────────────┤
│  File System    │  Mendix API    │  Project Structure     │
│  - AppData      │  - Modules     │  - Directory Scanning  │
│  - Userlib      │  - Version     │  - Metadata Extraction │
└─────────────────────────────────────────────────────────────┘

Technology Stack

Frontend

  • React 18: Modern functional components with hooks
  • Tailwind CSS: Utility-first CSS framework
  • RxJS: Reactive programming for real-time updates
  • React Spring: Smooth animations and transitions
  • REDI: Dependency injection for React

Backend

  • Python 3.x: Core business logic
  • pythonnet: .NET integration with Mendix APIs
  • dependency-injector: IoC container for dependency management
  • System.Text.Json: .NET JSON serialization

Communication

  • Message Passing: JSON-RPC style communication
  • Progress Updates: Real-time job progress streaming
  • Error Handling: Comprehensive error capture and display

Key Components

Backend Components

LogExtractor Class

  • get_mendix_log_path(version): Constructs log file paths
  • read_log_file(file_path, max_lines): Safely reads log files
  • extract_studio_pro_logs(version): Extracts Studio Pro logs
  • extract_git_logs(version): Extracts Git logs
  • extract_modules_info(): Analyzes project modules
  • extract_jar_dependencies(): Scans for JAR files
  • extract_frontend_components(): Identifies frontend components
  • format_for_forum(data): Creates forum-ready markdown

RPC Handlers

  • GetVersionRpc: Retrieves Mendix version and project info
  • GetStudioProLogsRpc: Fetches Studio Pro logs
  • GetGitLogsRpc: Fetches Git logs
  • GetModulesInfoRpc: Gets module information
  • GetJarDependenciesRpc: Gets JAR dependencies
  • GetFrontendComponentsRpc: Gets frontend components
  • FormatForForumRpc: Formats data for forum posting

Job Handlers

  • ExtractAllLogsJob: Background extraction of all log types

Frontend Components

Main App Component

  • Tab navigation system
  • Progress tracking
  • Error display
  • Data state management

Panel Components

  • Overview dashboard
  • Log viewers with syntax highlighting
  • Module browser
  • Dependency explorer
  • Forum export preview

File Structure

log-extractor/
├── main.py              # Backend logic and handlers
├── index.html           # Frontend React application
├── manifest.json        # Plugin manifest with metadata and configuration
├── README.md           # Documentation

API Specification

RPC Methods

logs:getVersion

  • Returns: { version: string, projectPath: string }
  • Description: Gets current Mendix version and project path

logs:getStudioProLogs

  • Params: { version: string }
  • Returns: { version: string, logPath: string, exists: boolean, lines: string[], lastModified: string }
  • Description: Extracts Studio Pro logs for specified version

logs:getGitLogs

  • Params: { version: string }
  • Returns: { version: string, logPath: string, exists: boolean, lines: string[], lastModified: string }
  • Description: Extracts Git logs for specified version

logs:getModulesInfo

  • Returns: [{ id: string, name: string, type: string, entities: [], microflows: [], pages: [] }]
  • Description: Gets comprehensive module information

logs:getJarDependencies

  • Returns: [{ name: string, path: string, size: number, lastModified: string }]
  • Description: Lists all JAR dependencies

logs:getFrontendComponents

  • Returns: [{ name: string, path: string, size: number, lastModified: string, type: string }]
  • Description: Lists frontend components and widgets

logs:formatForForum

  • Params: { data: object }
  • Returns: { formattedText: string, timestamp: string }
  • Description: Formats extracted data for forum posting

Job Methods

logs:extractAll

  • Params: { version: string }
  • Returns: { data: object, forumFormatted: string }
  • Description: Extracts all log types and formats for forum

Error Handling

Backend Error Handling

  • File system errors (missing files, permissions)
  • Mendix API errors (version detection, module access)
  • JSON serialization errors
  • Comprehensive traceback capture

Frontend Error Handling

  • Network timeouts (10-second RPC timeout)
  • Backend error display with expandable tracebacks
  • Graceful degradation for missing data
  • User-friendly error messages

Security Considerations

File System Access

  • Read-only operations (no file modifications)
  • Path validation to prevent directory traversal
  • Encoding handling for text files (UTF-8 with fallback)

Data Privacy

  • No sensitive data transmission
  • Local file system access only
  • No external network requests

Performance Optimization

Backend

  • Efficient file reading (last 1000 lines only)
  • Cached Mendix API calls
  • Background job processing
  • Memory-efficient streaming

Frontend

  • Lazy loading of tab content
  • Debounced UI updates
  • Efficient React re-renders
  • Optimized bundle size

Testing Strategy

Unit Tests

  • Individual component testing
  • API method validation
  • Error scenario coverage

Integration Tests

  • End-to-end workflow testing
  • Cross-component communication
  • Real-time update verification

Deployment

Installation

  1. Copy plugin files to Mendix plugins directory
  2. Ensure Python dependencies are available
  3. Restart Mendix Studio Pro

Configuration

  • No manual configuration required
  • Automatic version detection
  • Adaptive to different project structures

Future Enhancements

Planned Features

  • Log filtering and search functionality
  • Export to multiple formats (JSON, XML, CSV)
  • Custom log path configuration
  • Integration with external logging services
  • Log analysis and pattern detection
  • Performance metrics extraction
  • Team collaboration features

Technical Improvements

  • Caching mechanism for frequently accessed data
  • Incremental updates for large projects
  • Plugin configuration persistence
  • Multi-language support

Usage Instructions

Basic Usage

  1. Open Mendix Studio Pro with your project
  2. Navigate to the Log Extractor plugin
  3. Click "Extract All Logs" to gather comprehensive information
  4. Use individual tabs to view specific data types
  5. Click "Format for Forum" to generate forum-ready content
  6. Use "Copy to Clipboard" to copy formatted content

Advanced Usage

  • Extract individual log types for targeted analysis
  • Monitor real-time progress during extraction
  • Review error details with full tracebacks
  • Export data for external analysis tools

Troubleshooting

  • Check file permissions for log directory access
  • Verify Mendix version compatibility
  • Review error messages for specific issues
  • Ensure Python dependencies are properly installed
  • Check manifest.json for correct plugin metadata and configuration

This plugin provides a comprehensive solution for Mendix developers to extract, analyze, and share diagnostic information efficiently and effectively.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="assets/tailwindcss.js"></script>
<style>
.log-container {
max-height: 400px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.forum-preview {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.loading-spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body class="bg-gray-100 font-sans">
<div id="app"></div>
<script src="assets/vendor-bundle.umd.js"></script>
<script src="assets/babel.min.js"></script>
<script type="text/babel">
// ===================================================================
// =================== FRAMEWORK CODE ========================
// ===================================================================
// This section contains the reusable, application-agnostic core.
// You should not need to modify this section to add new features.
// -------------------------------------------------------------------
// 等价于
// import * as React from 'react';
// import ReactDOM from 'react-dom/client';
// import * as redi from '@wendellhu/redi';
// import * as rediReact from '@wendellhu/redi/react-bindings';
// import * as rxjs from 'rxjs';
// import * as reactSpring from '@react-spring/web';
const { React, ReactDOM, redi, rediReact, rxjs, reactSpring } = globalThis.__tmp;
delete globalThis.__tmp;
// keep unused import for redi and rediReact
const {
Inject,
Injector,
LookUp,
Many,
Optional,
Quantity,
RediError,
Self,
SkipSelf,
WithNew,
createIdentifier,
forwardRef,
isAsyncDependencyItem,
isAsyncHook,
isClassDependencyItem,
isCtor,
isDisposable,
isFactoryDependencyItem,
isValueDependencyItem,
setDependencies,
} = redi;
const {
RediConsumer,
RediContext,
RediProvider,
WithDependency,
connectDependencies,
connectInjector,
useDependency,
useInjector,
useObservable,
useUpdateBinder,
} = rediReact;
const {
useState,
useEffect,
useRef,
useReducer,
Fragment,
useCallback,
createContext,
useContext,
} = React;
const { BehaviorSubject, Subject, fromEvent, map, filter } = rxjs;
const { useSpring, useTransition, animated } = reactSpring;
// 1. FRAMEWORK: CORE COMMUNICATION SERVICES
const IApiService = createIdentifier('IApiService');
class ApiService {
constructor() {
this.reqIdCounter = 0;
this.pendingRpcs = new Map();
this.activeJobs = new Map();
this.activeSessions = new Map();
this.broadcastChannels = new Map();
fromEvent(window, 'message')
.pipe(filter(event => event.data && event.data.type === 'backendResponse'))
.pipe(map(event => JSON.parse(event.data.data)))
.subscribe(msg => this._handleBackendMessage(msg));
}
// --- Public Methods for Hooks ---
rpc(method, params) {
return new Promise((resolve, reject) => {
const reqId = `req-${this.reqIdCounter++}`;
this.pendingRpcs.set(reqId, { resolve, reject });
this._sendMessage({ type: 'RPC', reqId, method, params });
setTimeout(() => {
if (this.pendingRpcs.has(reqId)) {
this.pendingRpcs.delete(reqId);
reject(new Error(`RPC call '${method}' timed out.`));
}
}, 10000);
});
}
startJob(method, params) {
const subject = new Subject();
const reqId = `req-${this.reqIdCounter++}`;
const promise = new Promise((resolve, reject) => {
this.pendingRpcs.set(reqId, {
resolve: ({ jobId }) => {
this.activeJobs.set(jobId, subject);
resolve({ jobId }); // Resolve promise with jobId once started
},
reject
});
});
this._sendMessage({ type: 'JOB_START', reqId, method, params });
return { startPromise: promise, updates$: subject.asObservable() };
}
connectSession(channel, payload) {
const sessionId = `${channel}-${Math.random().toString(36).substr(2, 9)}`;
if (!this.activeSessions.has(sessionId)) {
const subject = new Subject();
this.activeSessions.set(sessionId, subject);
this._sendMessage({ type: 'SESSION_CONNECT', sessionId, channel, payload });
}
return { sessionId, messages$: this.activeSessions.get(sessionId).asObservable() };
}
disconnectSession(sessionId, channel) {
if (this.activeSessions.has(sessionId)) {
this.activeSessions.get(sessionId).complete();
this.activeSessions.delete(sessionId);
this._sendMessage({ type: 'SESSION_DISCONNECT', sessionId, channel });
}
}
subscribeBroadcast(channel) {
if (!this.broadcastChannels.has(channel)) {
this.broadcastChannels.set(channel, new Subject());
}
return this.broadcastChannels.get(channel).asObservable();
}
// --- Internal Message Handling ---
_sendMessage(data) {
window.parent.sendMessage('frontend:message', data);
}
_handleBackendMessage(msg) {
const { type, reqId, jobId, sessionId, channel } = msg;
switch (type) {
case 'RPC_SUCCESS':
this.pendingRpcs.get(reqId)?.resolve(msg.data);
this.pendingRpcs.delete(reqId);
break;
case 'RPC_ERROR':
this.pendingRpcs.get(reqId)?.reject({ message: msg.message, traceback: msg.traceback });
this.pendingRpcs.delete(reqId);
break;
case 'JOB_STARTED':
this.pendingRpcs.get(reqId)?.resolve({ jobId });
this.pendingRpcs.delete(reqId);
break;
case 'JOB_PROGRESS':
this.activeJobs.get(jobId)?.next({ type: 'progress', ...msg });
break;
case 'JOB_SUCCESS':
this.activeJobs.get(jobId)?.next({ type: 'success', ...msg });
this.activeJobs.get(jobId)?.complete();
this.activeJobs.delete(jobId);
break;
case 'JOB_ERROR':
this.activeJobs.get(jobId)?.next({ type: 'error', ...msg });
this.activeJobs.get(jobId)?.complete();
this.activeJobs.delete(jobId);
break;
case 'EVENT_BROADCAST':
this.broadcastChannels.get(channel)?.next(msg.data);
break;
case 'EVENT_SESSION':
this.activeSessions.get(sessionId)?.next(msg.data);
break;
}
}
}
setDependencies(ApiService, []);
// --- High-Level React Hooks (Simplifying Boilerplate) ---
function useRpc(method) {
const api = useDependency(IApiService);
const [state, setState] = useState({ isLoading: false, data: null, error: null });
const execute = useCallback(async (params) => {
setState({ isLoading: true, data: null, error: null });
try {
const result = await api.rpc(method, params);
setState({ isLoading: false, data: result, error: null });
return result;
} catch (err) {
setState({ isLoading: false, data: null, error: err });
throw err;
}
}, [api, method]);
return { ...state, execute };
}
const initialJobState = { status: 'idle', progress: { percent: 0, message: '' }, data: null, error: null };
function useJob(method) {
const api = useDependency(IApiService);
const [state, setState] = useState(initialJobState);
const subRef = useRef(null);
const start = useCallback((params) => {
setState({ ...initialJobState, status: 'starting' });
const { startPromise, updates$ } = api.startJob(method, params);
startPromise.then(({ jobId }) => {
subRef.current = updates$.subscribe({
next: (update) => {
if (update.type === "JOB_PROGRESS") setState(s => ({ ...s, status: 'running', progress: update.progress }));
else if (update.type === "JOB_SUCCESS") setState(s => {
s.progress.percent = 100;
return { ...s, status: 'success', data: update.data };
})
else if (update.type === 'JOB_ERROR') setState(s => ({ ...s, status: 'error', error: { message: update.message, traceback: update.traceback } }));
},
error: (err) => setState(s => ({ ...s, status: 'error', error: err })),
complete: () => {
// Handled by success/error messages
}
});
}).catch(err => {
setState({ ...initialJobState, status: 'error', error: err });
});
}, [api, method]);
useEffect(() => {
// Cleanup subscription on unmount
return () => subRef.current?.unsubscribe();
}, []);
return { ...state, start };
}
function usePush(channel, options = {}) {
const api = useDependency(IApiService);
const [latestMessage, setLatestMessage] = useState(null);
useEffect(() => {
let sub;
let sessionId;
if (options.isSession) {
const session = api.connectSession(channel, options.payload);
sessionId = session.sessionId;
sub = session.messages$.subscribe(setLatestMessage);
} else {
sub = api.subscribeBroadcast(channel).subscribe(setLatestMessage);
}
return () => {
sub?.unsubscribe();
if (sessionId) {
api.disconnectSession(sessionId, channel);
}
};
}, [api, channel, options.isSession, JSON.stringify(options.payload)]); // re-run if payload changes
return { latestMessage };
}
// 3. NEW: I18N Service for multi-language support
const II18nService = createIdentifier('II18nService');
const ITranslationSource = createIdentifier('ITranslationSource');
class I18nService {
constructor(apiService, translationSources) {
this.apiService = apiService;
this._mergedTranslations = this._mergeSources(translationSources);
this.language$ = new BehaviorSubject('en'); // Default to English
this.translations$ = this.language$.pipe(
map(lang => this._mergedTranslations[lang] || this._mergedTranslations['en'])
);
}
// Deep merge an array of translation source objects
_mergeSources(sources) {
const merged = {};
for (const source of sources) {
for (const lang in source) {
if (!merged[lang]) {
merged[lang] = {};
}
// Simple, non-recursive merge is sufficient here
Object.assign(merged[lang], source[lang]);
}
}
return merged;
}
async initialize() {
try {
const env = await this.apiService.rpc('app:getEnvironment', {});
// Use only the primary language code (e.g., 'en' from 'en-US')
const lang = env.language.split('-')[0].toLowerCase();
this.language$.next(lang);
} catch (e) {
console.error("Failed to fetch environment info for language.", e);
// Keep the default 'en'
}
}
}
setDependencies(I18nService, [IApiService, [new Many(), ITranslationSource]]);
const coreTranslations = {
en: {
'Refresh': 'Refresh',
'Mendix Log Extractor': 'Mendix Log Extractor',
'Overview': 'Overview',
'Studio Pro Logs': 'Studio Pro Logs',
'Git Logs': 'Git Logs',
'App Logs': 'App Logs',
'Modules': 'Modules',
'Dependencies': 'Dependencies',
'Forum Export Tab': 'Forum Export',
},
zh: {
'Refresh': '刷新',
'Mendix Log Extractor': 'Mendix 日志提取器',
'Overview': '概览',
'Studio Pro Logs': 'Studio Pro 日志',
'Git Logs': 'Git 日志',
'App Logs': '应用日志',
'Modules': '模块',
'Dependencies': '依赖项',
'Forum Export Tab': '论坛导出',
}
};
const sidebarTranslations = {
en: {
'Global Actions': 'Global Actions',
'Extract All Logs': 'Extract All Logs',
'Forum Export': 'Forum Export',
'Generate a complete diagnostic report...': 'Generate a complete diagnostic report formatted for forum posts. The result will be shown in the \'Forum Export\' tab.',
'Generate Export': 'Generate Export',
'Generating...': 'Generating...',
},
zh: {
'Global Actions': '全局操作',
'Extract All Logs': '提取所有日志',
'Forum Export': '论坛导出',
'Generate a complete diagnostic report...': '生成格式化为论坛帖子的完整诊断报告。结果将显示在“论坛导出”选项卡中。',
'Generate Export': '生成导出',
'Generating...': '生成中...',
}
};
const overviewPanelTranslations = {
en: {
'Mendix Log Extractor Overview': 'Mendix Log Extractor Overview',
'Mendix Version': 'Mendix Version',
'Project Path': 'Project Path',
'Click "Extract All Logs"...': 'Click "Extract All Logs" in the sidebar to begin diagnostics.',
},
zh: {
'Mendix Log Extractor Overview': 'Mendix 日志提取器概览',
'Mendix Version': 'Mendix 版本',
'Project Path': '项目路径',
'Click "Extract All Logs"...': '点击侧边栏的“提取所有日志”以开始诊断。',
}
};
const logViewerPanelTranslations = {
en: {
'Lines': 'Lines',
'Log Path': 'Log Path',
'Select a log file': 'Select a log file',
'No app logs found...': 'No app logs found in deployment/log directory.'
},
zh: {
'Lines': '行数',
'Log Path': '日志路径',
'Select a log file': '选择一个日志文件',
'No app logs found...': '在 deployment/log 目录中未找到应用日志。'
},
};
const modulesPanelTranslations = {
en: { 'Project Modules': 'Project Modules', 'Version': 'Version' },
zh: { 'Project Modules': '项目模块', 'Version': '版本' },
};
const forumPanelTranslations = {
en: {
'Forum Export Preview': 'Forum Export Preview',
'Copy to Clipboard': 'Copy to Clipboard',
'Copied!': '✓ Copied!',
'Go to Forum to Ask': 'Go to Forum to Ask',
'Click "Generate Export"...': 'Click "Generate Export" in the sidebar on the right to create forum-formatted content.',
'The generated report will appear here...': 'The generated report will appear here for you to review and copy.',
},
zh: {
'Forum Export Preview': '论坛导出预览',
'Copy to Clipboard': '复制到剪贴板',
'Copied!': '✓ 已复制!',
'Go to Forum to Ask': '前往论坛提问',
'Click "Generate Export"...': '点击右侧边栏的“生成导出”以创建论坛格式的内容。',
'The generated report will appear here...': '生成的报告将在此处显示,供您查看和复制。',
}
};
function useTranslation() {
const i18nService = useDependency(II18nService);
const currentTranslations = useObservable(i18nService.translations$);
const t = useCallback((key) => {
return currentTranslations[key] || key;
}, [currentTranslations]);
return { t };
}
const IPanel = createIdentifier("IPanel");
const IPanelService = createIdentifier("IPanelService");
class PanelService {
constructor(panels) {
this.allPanels = new Map(panels.map(p => [p.id, p]));
this._state$ = new BehaviorSubject(this.getInitialState());
this.state$ = this._state$.asObservable();
// For shared data from the extract-all job
this.extractedData$ = new BehaviorSubject(null);
}
setExtractedData = (data) => {
this.extractedData$.next(data);
}
getInitialState() {
const initialOpenIds = new Set(
Array.from(this.allPanels.values())
.filter(p => p.defaultActive)
.map(p => p.id)
);
const initialActiveId = Array.from(this.allPanels.values())
.find(p => p.defaultActive)?.id || null;
return this._computeState(initialOpenIds, initialActiveId);
}
// Private method to compute the full state object from IDs
_computeState(openPanelIds, activePanelId) {
const openPanels = Array.from(openPanelIds)
.map(id => this.allPanels.get(id))
.filter(Boolean); // Filter out potential undefineds
const activePanel = activePanelId ? this.allPanels.get(activePanelId) : null;
return { openPanels, activePanel };
}
// Private method to update the stream
_updateState(newOpenIds, newActiveId) {
const newState = this._computeState(newOpenIds, newActiveId);
this._state$.next(newState);
}
// --- Public API for Panel Control ---
openPanel(id) {
if (!this.allPanels.has(id)) return;
const currentState = this._state$.getValue();
const currentOpenIds = new Set(currentState.openPanels.map(p => p.id));
if (currentOpenIds.has(id)) {
this.setActivePanel(id);
return;
}
currentOpenIds.add(id);
this._updateState(currentOpenIds, id);
}
closePanel(id) {
const currentState = this._state$.getValue();
const currentOpenIds = new Set(currentState.openPanels.map(p => p.id));
if (!currentOpenIds.has(id)) return;
currentOpenIds.delete(id);
let newActiveId = currentState.activePanel?.id;
if (newActiveId === id) {
newActiveId = currentOpenIds.values().next().value || null;
}
this._updateState(currentOpenIds, newActiveId);
}
setActivePanel(id) {
const currentState = this._state$.getValue();
if (currentState.activePanel?.id === id) return;
const currentOpenIds = new Set(currentState.openPanels.map(p => p.id));
if (!currentOpenIds.has(id)) return; // Can't activate a panel that isn't open
this._updateState(currentOpenIds, id);
}
}
setDependencies(PanelService, [[new Many(), IPanel]]);
const ErrorDisplay = ({ error }) => {
if (!error) return null;
const [showTraceback, setShowTraceback] = useState(false);
return (
<div className="mt-4 p-3 bg-red-100 text-red-800 rounded-lg border border-red-200">
<div className="flex justify-between items-center">
<p className="font-semibold break-all">Error: {error.message}</p>
{error.traceback && (
<button
onClick={() => setShowTraceback(!showTraceback)}
className="text-xs text-red-600 hover:text-red-800 font-medium ml-4 flex-shrink-0"
>
{showTraceback ? 'Hide Details' : 'Show Details'}
</button>
)}
</div>
{showTraceback && error.traceback && (
<pre className="mt-3 p-2 bg-red-50 text-xs text-red-900 overflow-auto rounded font-mono">
<code>{error.traceback}</code>
</pre>
)}
</div>
);
};
// ===================================================================
// =============== BUSINESS LOGIC CODE =======================
// ===================================================================
// This section contains your feature-specific components (Views).
// To add a new feature, add your new Component here and register
// it as an `IView` in the IOC Configuration section below.
// -------------------------------------------------------------------
const IDataService = createIdentifier('IDataService');
class DataService {
constructor(apiService) {
this.apiService = apiService;
// Observables for different data pieces
this.overviewData$ = new BehaviorSubject({ isLoading: true, data: null, error: null });
this.appLogSources$ = new BehaviorSubject({ isLoading: true, data: [], error: null });
this.logContentCache$ = new BehaviorSubject(new Map());
this.modules$ = new BehaviorSubject({ isLoading: true, data: null, error: null });
this.dependencies$ = new BehaviorSubject({ isLoading: true, data: null, error: null });
this.frontendComponents$ = new BehaviorSubject({ isLoading: true, data: null, error: null });
this.forumExport$ = new BehaviorSubject({ isLoading: false, data: null, error: null });
}
async _fetchData(method, params, subject$) {
subject$.next({ ...subject$.getValue(), isLoading: true, error: null });
try {
const data = await this.apiService.rpc(method, params);
subject$.next({ isLoading: false, data, error: null });
} catch (error) {
subject$.next({ isLoading: false, data: null, error });
}
}
_updateCache(logId, state) {
this.logContentCache$.next(
new Map(this.logContentCache$.getValue()).set(logId, state)
);
}
fetchLogContent = async (logId, params) => {
this._updateCache(logId, { isLoading: true, data: null, error: null });
try {
const data = await this.apiService.rpc('logs:getContent', { ...params, id: logId });
this._updateCache(logId, { isLoading: false, data, error: null });
} catch (error) {
this._updateCache(logId, { isLoading: false, data: null, error });
}
}
fetchOverview = () => this._fetchData('app:getEnvironment', {}, this.overviewData$);
listAppLogSources = () => this._fetchData('logs:listAppLogSources', {}, this.appLogSources$);
fetchModules = () => this._fetchData('logs:getModulesInfo', {}, this.modules$);
fetchJarDependencies = () => this._fetchData('logs:getJarDependencies', {}, this.dependencies$);
fetchFrontendComponents = () => this._fetchData('logs:getFrontendComponents', {}, this.frontendComponents$);
generateForumExport = () => this._fetchData('logs:generateCompleteForumExport', {}, this.forumExport$);
}
setDependencies(DataService, [IApiService]);
const OverviewPanel = () => {
const { t } = useTranslation();
const dataService = useDependency(IDataService);
const { isLoading, data, error } = useObservable(dataService.overviewData$, dataService.overviewData$.getValue());
const extractedData = useObservable(useDependency(IPanelService).extractedData$, null);
useEffect(() => {
if (dataService.overviewData$.getValue().data === null) {
// This RPC was changed in the previous step, but the old name is used here.
// Correcting it to 'app:getEnvironment' to align with previous changes.
dataService.apiService.rpc('app:getEnvironment', {}).then(envData => {
dataService.overviewData$.next({ isLoading: false, data: envData, error: null });
}).catch(err => {
dataService.overviewData$.next({ isLoading: false, data: null, error: err });
});
}
}, []);
if (isLoading && !data) return <div>Loading project info...</div>;
if (error) return <ErrorDisplay error={error} />;
return (
<div>
<h2 className="text-xl font-semibold text-gray-700 mb-4">{t('Mendix Log Extractor Overview')}</h2>
{data && (
<div className="mb-6 p-4 bg-blue-50 rounded-lg">
<p><span className="font-semibold">{t('Mendix Version')}:</span> {data.version}</p>
<p><span className="font-semibold">{t('Project Path')}:</span> {data.projectPath}</p>
</div>
)}
{extractedData ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Cards showing summary, similar to original */}
</div>
) : (
<div className="text-center p-6 bg-gray-50 rounded-lg">
<p className="text-gray-600">
{t('Click "Extract All Logs"...')}
</p>
</div>
)}
</div>
);
};
const LogViewerPanel = ({ title, logId, icon }) => {
const { t } = useTranslation();
const dataService = useDependency(IDataService);
const cache = useObservable(dataService.logContentCache$, dataService.logContentCache$.getValue());
const { isLoading, data, error } = cache.get(logId) || { isLoading: true, data: null, error: null };
const [lineCount, setLineCount] = useState(100);
const handleRefresh = () => {
dataService.fetchLogContent(logId, { limit: parseInt(lineCount, 10) || 100 });
};
useEffect(() => {
if (!cache.has(logId)) {
handleRefresh();
}
}, [logId]);
const renderLogContent = () => {
if (isLoading && !data) return <div className="text-gray-500 italic">Loading...</div>;
if (error) return <ErrorDisplay error={error} />;
if (!data) return <div className="text-gray-500 italic">No data available.</div>;
if (data.error) return <div className="text-red-600">Error: {data.error}</div>;
return (
<>
<div className="mb-4 p-3 bg-blue-50 rounded-lg">
<p><span className="font-semibold">{t('Log Path')}:</span> {data.logPath || 'N/A'}</p>
<p className="text-sm text-gray-600 mt-1">
{data.exists ? `✅ File exists. Showing last ${data.lines?.length || 0} of ${data.totalLines} lines.` : '❌ File not found'}
</p>
</div>
{data.lines && data.lines.length > 0 ? (
<div className="log-container bg-gray-900 text-green-400 p-4 rounded-lg">
{data.lines.map((line, index) => <div key={index}>{line.trim()}</div>)}
</div>
) : (
<div className="text-gray-500 italic">No log entries found.</div>
)}
</>
);
};
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold flex items-center space-x-2">
<span>{icon}</span><span>{t(title)}</span>
</h2>
<div className="flex items-center space-x-2">
<label htmlFor="line-count" className="text-sm">{t('Lines')}:</label>
<input
id="line-count"
type="number"
value={lineCount}
onChange={(e) => setLineCount(e.target.value)}
className="w-20 p-1 border rounded"
min="1"
max="5000"
/>
<button onClick={handleRefresh} disabled={isLoading} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400">
{t('Refresh')}
</button>
</div>
</div>
{renderLogContent()}
</div>
);
};
const AppLogsPanel = () => {
const { t } = useTranslation();
const dataService = useDependency(IDataService);
const { isLoading: sourcesLoading, data: sources, error: sourcesError } = useObservable(dataService.appLogSources$, dataService.appLogSources$.getValue());
const [selectedLogId, setSelectedLogId] = useState('');
useEffect(() => {
dataService.listAppLogSources();
}, []);
useEffect(() => {
// Auto-select the first log file if available and none is selected
if (sources && sources.length > 0 && !selectedLogId) {
setSelectedLogId(sources[0].id);
}
}, [sources, selectedLogId]);
const handleSelectionChange = (e) => {
const newId = e.target.value;
setSelectedLogId(newId);
};
return (
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-4 flex-shrink-0">
<h2 className="text-xl font-bold flex items-center space-x-2">
<span>🚀</span><span>{t('App Logs')}</span>
</h2>
{sources && sources.length > 0 && (
<select
value={selectedLogId}
onChange={handleSelectionChange}
className="p-2 border rounded bg-white"
>
<option value="" disabled>{t('Select a log file')}</option>
{sources.map(source => (
<option key={source.id} value={source.id}>{source.name}</option>
))}
</select>
)}
</div>
<div className="flex-grow overflow-auto">
{sourcesLoading && <div>Loading log sources...</div>}
<ErrorDisplay error={sourcesError} />
{sources && sources.length === 0 && !sourcesLoading && (
<div className="text-gray-500 italic text-center p-4">{t('No app logs found...')}</div>
)}
{selectedLogId && (
<LogViewerPanel title="" logId={selectedLogId} icon=""/>
)}
</div>
</div>
)
}
const ActionsSidebar = () => {
const { t } = useTranslation();
const dataService = useDependency(IDataService);
const panelService = useDependency(IPanelService);
const { isLoading: isExporting, data: exportData, error: exportError } = useObservable(dataService.forumExport$, dataService.forumExport$.getValue());
useEffect(() => {
if (exportData && !isExporting && !exportError) {
panelService.setActivePanel('forum');
}
}, [exportData, isExporting, exportError, panelService]);
return (
<div className="bg-white p-4 rounded-lg shadow-md space-y-6 h-full">
<div className="border-t pt-4">
<h3 className="font-semibold text-lg mb-2">{t('Forum Export')}</h3>
<p className="text-sm text-gray-600 mb-3">
{t('Generate a complete diagnostic report...')}
</p>
<button
onClick={dataService.generateForumExport}
disabled={isExporting}
className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400 flex items-center justify-center space-x-2"
>
{isExporting && <div className="loading-spinner"></div>}
<span>{isExporting ? t('Generating...') : t('Generate Export')}</span>
</button>
<ErrorDisplay error={exportError} />
</div>
</div>
);
};
const ModulesPanel = () => {
const { t } = useTranslation();
const { isLoading, data, error, execute } = useRpc('logs:getModulesInfo');
useEffect(() => {
execute();
}, []);
const renderModules = () => {
if (isLoading) return <div className="text-gray-500 italic">Loading...</div>;
if (error) return <ErrorDisplay error={error} />;
if (!data || data.length === 0) return <div className="text-gray-500 italic">No modules found.</div>;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{data.map((module, index) => (
<div key={index} className="bg-white p-4 rounded-lg shadow border">
<h4 className="font-bold text-lg mb-2">{module.name}</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span>{t('Version')}:</span>
<span className="font-semibold">{module.version}</span>
</div>
<div className="flex justify-between">
<span>Id:</span>
<span className="font-semibold">{module.id}</span>
</div>
</div>
</div>
))}
</div>
);
};
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">{t('Project Modules')}</h2>
<button
onClick={() => execute()}
disabled={isLoading}
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 disabled:bg-gray-400"
>
{t('Refresh')}
</button>
</div>
{renderModules()}
</div>
);
};
const DependenciesPanel = () => {
const { t } = useTranslation();
const { isLoading: jarsLoading, data: jarsData, error: jarsError, execute: executeJars } = useRpc('logs:getJarDependencies');
const { isLoading: frontendLoading, data: frontendData, error: frontendError, execute: executeFrontend } = useRpc('logs:getFrontendComponents');
useEffect(() => {
executeJars();
executeFrontend();
}, []);
const widgets = frontendData?.filter(item => item.type === 'Widget') || [];
const jsActions = frontendData?.filter(item => item.type === 'JavaScript Action') || [];
const renderList = (items, titleIfEmpty) => {
if (!items || items.length === 0) {
return <div className="text-gray-500 italic p-4">No {titleIfEmpty.toLowerCase()} found.</div>;
}
return (
<div className="space-y-2 max-h-60 overflow-y-auto p-2 border rounded-md bg-gray-50">
{items.map((item, index) => (
<div key={index} className="bg-white p-2 rounded-lg shadow-sm border flex justify-between items-center">
<div>
<div className="font-semibold text-sm">{item.name}</div>
<div className="text-xs text-gray-500 truncate" title={item.path}>{item.path}</div>
</div>
<div className="text-xs text-gray-500 flex-shrink-0 ml-2">
{item.size != null ? `${(item.size / 1024).toFixed(1)} KB` : 'N/A'}
</div>
</div>
))}
</div>
);
};
return (
<div className="space-y-8">
{/* JARs section */}
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-bold">{t('JAR Dependencies')} ({jarsData?.length || 0})</h2>
<button
onClick={() => executeJars()}
disabled={jarsLoading}
className="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700 disabled:bg-gray-400 text-sm"
>
{t('Refresh')}
</button>
</div>
{jarsLoading ? (
<div className="text-gray-500 italic">Loading JAR dependencies...</div>
) : jarsError ? (
<ErrorDisplay error={jarsError} />
) : (
renderList(jarsData, t('JAR Dependencies'))
)}
</div>
{/* Frontend Components section */}
<div>
<div className="flex justify-between items-center mb-2">
<h2 className="text-xl font-bold">{t('Frontend Components')}</h2>
<button
onClick={() => executeFrontend()}
disabled={frontendLoading}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:bg-gray-400 text-sm"
>
{t('Refresh')}
</button>
</div>
{frontendLoading ? (
<div className="text-gray-500 italic">Loading frontend components...</div>
) : frontendError ? (
<ErrorDisplay error={frontendError} />
) : (
<div className="space-y-4">
<div>
<h3 className="font-semibold text-lg">{t('Widgets')} ({widgets.length})</h3>
{renderList(widgets, t('Widgets'))}
</div>
<div>
<h3 className="font-semibold text-lg">{t('JavaScript Actions')} ({jsActions.length})</h3>
{renderList(jsActions, t('JavaScript Actions'))}
</div>
</div>
)}
</div>
</div>
);
};
const ForumExportPanel = () => {
const { t } = useTranslation();
const dataService = useDependency(IDataService);
const { isLoading, data, error } = useObservable(dataService.forumExport$, dataService.forumExport$.getValue());
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), 3000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
};
const openForum = () => {
window.open('https://mendix.bbscloud.com/', '_blank');
}
return (
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">{t('Forum Export Preview')}</h2>
<div className="flex items-center space-x-2">
{isCopied && <span className="text-sm text-green-600 transition-opacity duration-300">{t('Copied!')}</span>}
{data?.formattedText && (
<button
onClick={() => copyToClipboard(data.formattedText)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{t('Copy to Clipboard')}
</button>
)}
{isCopied && (
<button
onClick={openForum}
className="px-4 py-2 bg-orange-500 text-white rounded hover:bg-orange-600 animate-pulse"
>
{t('Go to Forum to Ask')}
</button>
)}
</div>
</div>
<ErrorDisplay error={error} />
{isLoading ? (
<div className="flex justify-center items-center h-64">
<div className="loading-spinner" style={{ width: '40px', height: '40px' }}></div>
</div>
) : data?.formattedText ? (
<div className="forum-preview h-[calc(100vh-250px)]">
<pre>{data.formattedText}</pre>
</div>
) : (
<div className="text-center text-gray-500 p-8 bg-gray-50 rounded-lg">
<p>{t('Click "Generate Export"...')}</p>
<p className="text-sm mt-2">{t('The generated report will appear here...')}</p>
</div>
)}
</div>
);
};
// ===================================================================
// ============== IOC & APP INITIALIZATION ===================
// ===================================================================
const AppWithDependencies = connectDependencies(() => {
const i18nService = useDependency(II18nService);
const [isI18nReady, setI18nReady] = useState(false);
useEffect(() => {
const init = async () => {
await i18nService.initialize();
setI18nReady(true);
}
init();
}, [i18nService]);
const { t } = useTranslation();
const panelService = useDependency(IPanelService);
const { openPanels, activePanel } = useObservable(panelService.state$, panelService.getInitialState());
const panelTransitions = useTransition(activePanel, {
key: panel => panel?.id,
from: { opacity: 0, transform: 'translateY(10px)' },
enter: { opacity: 1, transform: 'translateY(0px)' },
leave: { opacity: 0, transform: 'translateY(-10px)', position: 'absolute', top: 0, left: 0, right: 0 },
config: { tension: 220, friction: 25 },
});
if (!isI18nReady) {
return <div className="flex items-center justify-center h-screen">Initializing...</div>;
}
return (
<div className="p-4 max-w-7xl mx-auto h-[100vh] flex flex-col">
<h1 className="text-3xl font-bold text-gray-800 mb-4 text-center">
{t('Mendix Log Extractor')}
</h1>
<div className="flex-grow grid grid-cols-1 lg:grid-cols-3 gap-4 overflow-hidden">
{/* Main Content Area */}
<div className="lg:col-span-2 bg-gray-50 shadow-lg rounded-lg flex flex-col">
<div className="flex border-b border-gray-300 flex-wrap">
{openPanels.map(panel => (
<button
key={panel.id}
onClick={() => panelService.setActivePanel(panel.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 ${panel.id === activePanel?.id
? 'border-blue-600 text-blue-700'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{t(panel.title)}
</button>
))}
</div>
<div className="flex-grow p-4 bg-white relative overflow-auto">
{panelTransitions((style, panel) =>
panel ? (
<animated.div style={style} className="w-full h-full">
<panel.component />
</animated.div>
) : (
<div>No panel selected.</div>
)
)}
</div>
</div>
{/* Sidebar */}
<div className="lg:col-span-1">
<ActionsSidebar />
</div>
</div>
</div>
);
}, [
// --- Framework Registrations ---
[IApiService, { useClass: ApiService }],
[IPanelService, { useClass: PanelService }],
[IDataService, { useClass: DataService }],
[II18nService, { useClass: I18nService }],
// --- Register all modular translation sources ---
[ITranslationSource, { useValue: coreTranslations }],
[ITranslationSource, { useValue: sidebarTranslations }],
[ITranslationSource, { useValue: overviewPanelTranslations }],
[ITranslationSource, { useValue: logViewerPanelTranslations }],
[ITranslationSource, { useValue: modulesPanelTranslations }],
[ITranslationSource, { useValue: forumPanelTranslations }],
// --- Business Logic Registrations (as Panels) ---
[IPanel, { useValue: { id: 'overview', title: '🏠 Overview', component: OverviewPanel, defaultActive: true } }],
[IPanel, { useValue: { id: 'studio-pro', title: '📝 Studio Pro Logs', component: () => <LogViewerPanel title="Studio Pro Logs" logId="studio_pro" icon="📝" />, defaultActive: true } }],
[IPanel, { useValue: { id: 'git', title: '🔀 Git Logs', component: () => <LogViewerPanel title="Git Logs" logId="git" icon="🔀" />, defaultActive: true } }],
[IPanel, { useValue: { id: 'app-logs', title: '🚀 App Logs', component: AppLogsPanel, defaultActive: true } }],
[IPanel, { useValue: { id: 'modules', title: '📦 Modules', component: ModulesPanel, defaultActive: true } }],
[IPanel, { useValue: { id: 'dependencies', title: '🏗️ Dependencies', component: DependenciesPanel, defaultActive: true } }],
[IPanel, { useValue: { id: 'forum', title: '🌐 Forum Export Tab', component: ForumExportPanel, defaultActive: true } }],
]);
const root = ReactDOM.createRoot(document.getElementById("app"));
root.render(<AppWithDependencies />);
</script>
</body>
</html>
# region FRAMEWORK CODE
import json
import os
import sys
import glob
import re
import traceback
from typing import Any, Dict, Callable, Iterable, Optional
from abc import ABC, abstractmethod
from datetime import datetime
# Mendix and .NET related imports
import clr
clr.AddReference("System.Text.Json")
clr.AddReference("Mendix.StudioPro.ExtensionsAPI")
import threading
import uuid
from dependency_injector import containers, providers
from System.Text.Json import JsonSerializer
# ShowDevTools()
# ===================================================================
# =================== FRAMEWORK CODE ========================
# ===================================================================
# This section contains the reusable, application-agnostic core.
# You should not need to modify this section to add new features.
# -------------------------------------------------------------------
# 1. FRAMEWORK: CORE ABSTRACTIONS AND INTERFACES
def info(e):
PostMessage("backend:info", f'{e}')
_dir = dir
def dir(e):
PostMessage("backend:info", f'{_dir(e)}')
def error(e):
PostMessage("backend:error", f'{e}')
def print(e):
PostMessage("backend:info", e)
class MendixEnvironmentService:
"""Abstracts the Mendix host environment global variables."""
def __init__(self, app_context, window_service, post_message_func: Callable):
self.app = app_context
self.window_service = window_service
self.post_message = post_message_func
# Assuming configurationService is a global available in the Mendix environment
self._config = configurationService.Configuration
def get_project_path(self) -> str:
return self.app.Root.DirectoryPath
def get_appdata_path(self) -> str:
"""Get Windows AppData Local path."""
return os.environ.get('LOCALAPPDATA', '')
def get_mendix_log_path(self, version: str) -> str:
"""Get Mendix log directory path for specific version."""
appdata = self.get_appdata_path()
if not appdata:
return ""
return os.path.join(appdata, "Mendix", "log", version)
def get_mendix_version(self) -> str:
"""Get current Mendix version from configuration."""
try:
return f"{self._config.MendixVersion}.{self._config.BuildTag}"
except:
return "Unknown"
def get_current_language(self) -> str:
"""Get the current language of the Studio Pro IDE."""
try:
# Example: 'en-US', 'nl-NL', 'zh-CN'
return self._config.CurrentLanguage.Name
except:
return "en-US" # Default to English if not found
from abc import ABC, abstractmethod
from typing import Any, Dict, Callable, Iterable, Optional, Protocol, List
import uuid
import threading
import json
import traceback
class ProgressUpdate:
"""Structured progress data."""
def __init__(self, percent: float, message: str, stage: Optional[str] = None, metadata: Optional[Dict] = None):
self.percent = percent
self.message = message
self.stage = stage
self.metadata = metadata
def to_dict(self):
return {k: v for k, v in self.__dict__.items() if v is not None}
class IMessageHub(Protocol):
"""Abstraction for sending messages to the frontend."""
def send(self, message: Dict): ...
def broadcast(self, channel: str, data: Any): ...
class IJobContext(Protocol):
"""Context object provided to a running job handler."""
job_id: str
def report_progress(self, progress: ProgressUpdate): ...
class IHandler(ABC):
@property
@abstractmethod
def command_type(self) -> str: ...
class IRpcHandler(IHandler):
@abstractmethod
def execute(self, payload: Dict) -> Any: ...
class IJobHandler(IHandler):
@abstractmethod
def run(self, payload: Dict, context: IJobContext): ...
class ISessionHandler(IHandler):
@abstractmethod
def on_connect(self, session_id: str, payload: Optional[Dict]): ...
@abstractmethod
def on_disconnect(self, session_id: str): ...
class MendixMessageHub:
"""Low-level implementation of IMessageHub for Mendix."""
def __init__(self, post_message_func):
self._post_message = post_message_func
def send(self, message: Dict):
self._post_message("backend:response", json.dumps(message))
def broadcast(self, channel: str, data: Any):
self.send({"type": "EVENT_BROADCAST", "channel": channel, "data": data})
class AppController:
"""Routes incoming messages to registered handlers."""
def __init__(self, rpc_handlers: List[IRpcHandler], job_handlers: List[IJobHandler],session_handlers: List[ISessionHandler], message_hub: IMessageHub):
self._rpc = {h.command_type: h for h in rpc_handlers}
self._jobs = {h.command_type: h for h in job_handlers}
self._hub = message_hub
print(f"Controller initialized. RPCs: {list(self._rpc.keys())}, Jobs: {list(self._jobs.keys())}")
def dispatch(self, request: Dict):
msg_type = request.get("type")
try:
if msg_type == "RPC": self._handle_rpc(request)
elif msg_type == "JOB_START": self._handle_job_start(request)
elif msg_type == "SESSION_CONNECT": self._handle_session_connect(request)
elif msg_type == "SESSION_DISCONNECT": self._handle_session_disconnect(request)
else: raise ValueError(f"Unknown message type: {msg_type}")
except Exception as e:
req_id = request.get("reqId")
if req_id:
# MODIFIED: Capture and send the full traceback string
tb_string = traceback.format_exc()
self._hub.send({"type": "RPC_ERROR", "reqId": req_id, "message": str(e), "traceback": tb_string})
traceback.print_exc()
def _handle_rpc(self, request):
handler = self._rpc.get(request["method"])
if not handler:
raise ValueError(f"No RPC handler for '{request['method']}'")
result = handler.execute(request.get("params"))
self._hub.send({"type": "RPC_SUCCESS", "reqId": request["reqId"], "data": result})
def _handle_job_start(self, request):
handler = self._jobs.get(request["method"])
if not handler:
raise ValueError(f"No Job handler for '{request['method']}'")
import uuid
import threading
job_id = f"job-{uuid.uuid4()}"
class JobContext(IJobContext):
def __init__(self, job_id: str, hub: IMessageHub):
self.job_id = job_id
self._hub = hub
def report_progress(self, progress: ProgressUpdate):
self._hub.send({"type": "JOB_PROGRESS", "jobId": self.job_id, "progress": progress.to_dict()})
context = JobContext(job_id, self._hub)
def job_runner():
try:
result = handler.run(request.get("params"), context)
self._hub.send({"type": "JOB_SUCCESS", "jobId": job_id, "data": result})
except Exception as e:
self._hub.send({"type": "JOB_ERROR", "jobId": job_id, "message": str(e)})
thread = threading.Thread(target=job_runner, daemon=True)
thread.start()
self._hub.send({"type": "JOB_STARTED", "reqId": request["reqId"], "jobId": job_id})
# endregion
# region BUSINESS LOGIC CODE
# ===================================================================
# =============== BUSINESS LOGIC CODE =======================
# ===================================================================
# This section contains your feature-specific command handlers.
# To add a new feature, create a new class implementing ICommandHandler
# or IAsyncCommandHandler, and register it in the Container below.
# -------------------------------------------------------------------
from pathlib import Path
def sanitize_path_prefix_pathlib(file_path: str, sensitive_prefix: str = None, replacement: str = "~") -> str:
"""
使用 pathlib 检查路径是否以敏感前缀开始,并进行脱敏。
默认脱敏用户的 HOME 目录。
"""
try:
# 1. 确保敏感前缀是 Path 对象,并进行绝对路径规范化
if sensitive_prefix is None:
# 默认使用用户主目录作为敏感前缀
prefix_path = Path.home().resolve()
else:
prefix_path = Path(sensitive_prefix).resolve()
# 2. 规范化输入路径
input_path = Path(file_path).resolve()
# 3. 检查输入路径是否以敏感前缀开始
if input_path.is_relative_to(prefix_path):
# is_relative_to() 是 Python 3.9+ 的方法,判断路径是否在另一个路径下。
# 计算剩余部分(即去掉前缀后的路径)
try:
# relative_to 会返回去掉前缀后的路径对象
relative_part = input_path.relative_to(prefix_path)
except ValueError:
# 如果路径完全等于前缀,relative_to 会抛出 ValueError,此时相对部分为空
relative_part = Path("")
# 4. 组合脱敏后的路径
# Windows/Linux 都会正确处理路径分隔符
return f"{replacement}{os.sep}{relative_part}"
# 如果不是敏感路径,返回原路径
return file_path
except Exception as e:
# 如果路径无效,返回原路径
# print(f"Error processing path: {e}")
return file_path
class ILogSource(ABC):
"""Abstract representation of a log file source."""
@property
@abstractmethod
def id(self) -> str: ...
@property
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def get_path(self, mendix_env: MendixEnvironmentService) -> str: ...
class StaticLogSource(ILogSource):
"""A concrete log source with a pre-defined path."""
def __init__(self, id: str, name: str, path_func: Callable[[MendixEnvironmentService], str]):
self._id = id
self._name = name
self._path_func = path_func
@property
def id(self) -> str: return self._id
@property
def name(self) -> str: return self._name
def get_path(self, mendix_env: MendixEnvironmentService) -> str:
return self._path_func(mendix_env)
class ILogSourceProvider(ABC):
"""Abstract provider for dynamically discovering log sources."""
@abstractmethod
def get_sources(self, mendix_env: MendixEnvironmentService) -> List[ILogSource]: ...
class AppLogSourceProvider(ILogSourceProvider):
"""Discovers all *.txt log files in the project's deployment/log directory."""
def get_sources(self, mendix_env: MendixEnvironmentService) -> List[ILogSource]:
project_path = mendix_env.get_project_path()
log_dir = os.path.join(project_path, "deployment", "log")
sources = []
if not os.path.exists(log_dir):
return []
for log_file_path in glob.glob(os.path.join(log_dir, "*.txt")):
base_name = os.path.basename(log_file_path)
# Sanitize basename to create a stable ID
file_id = f"app_{re.sub('[^a-zA-Z0-9_.-]', '_', base_name.lower())}"
# Use a lambda to capture the specific path for the StaticLogSource instance
sources.append(StaticLogSource(
id=file_id,
name=base_name,
path_func=lambda env, p=log_file_path: p
))
return sources
class LogExtractor:
"""Core log extraction functionality."""
def __init__(self, mendix_env: MendixEnvironmentService):
self.mendix_env = mendix_env
def get_appdata_path(self) -> str:
"""Get Windows AppData Local path."""
return os.environ.get('LOCALAPPDATA', '')
def get_mendix_log_path(self, version: str) -> str:
"""Get Mendix log directory path for specific version."""
appdata = self.get_appdata_path()
if not appdata:
return ""
return os.path.join(appdata, "Mendix", "log", version)
def get_studio_pro_install_path(self, version: str) -> str:
"""Get Studio Pro installation path."""
program_files = os.environ.get('ProgramFiles', '')
if not program_files:
return ""
return os.path.join(program_files, "Mendix", version, "modeler")
def read_log_file(self, file_path: str, limit: int = 100, offset: int = 0) -> dict:
"""Read a specified range of lines from a log file, from the end."""
try:
if not os.path.exists(file_path):
return {"lines": [], "totalLines": 0, "limit": limit, "offset": offset, "error": "File not found"}
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
total_lines = len(lines)
# Calculate slicing indices from the end of the file
start_index = max(0, total_lines - offset - limit)
end_index = max(0, total_lines - offset)
return {
"lines": lines[start_index:end_index],
"totalLines": total_lines,
"limit": limit,
"offset": offset,
}
except Exception as e:
return {"lines": [f"Error reading log file: {str(e)}"], "totalLines": 0, "limit": limit, "offset": offset, "error": str(e)}
def extract_log_by_path(self, file_path: str, limit: int = 100) -> dict:
"""Extracts log data for a given file path."""
log_data = self.read_log_file(file_path, limit=limit)
return {
"logPath": file_path,
"exists": os.path.exists(file_path),
"lastModified": datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat() if os.path.exists(file_path) else None,
**log_data
}
def extract_studio_pro_logs(self, version: str, limit: int = 100) -> dict:
"""Extract Studio Pro logs with line limit."""
log_path = self.get_mendix_log_path(version)
log_file = os.path.join(log_path, "log.txt")
log_data = self.read_log_file(log_file, limit=limit)
return {
"version": version,
"logPath": log_file,
"exists": os.path.exists(log_file),
"lastModified": datetime.fromtimestamp(os.path.getmtime(log_file)).isoformat() if os.path.exists(log_file) else None,
**log_data
}
def extract_git_logs(self, version: str, limit: int = 100) -> dict:
"""Extract Git logs with line limit."""
log_path = self.get_mendix_log_path(version)
git_log_file = os.path.join(log_path, "git", "git.log.txt")
log_data = self.read_log_file(git_log_file, limit=limit)
return {
"version": version,
"logPath": git_log_file,
"exists": os.path.exists(git_log_file),
"lastModified": datetime.fromtimestamp(os.path.getmtime(git_log_file)).isoformat() if os.path.exists(git_log_file) else None,
**log_data
}
def extract_modules_info(self) -> list:
"""Extract module information from current project."""
try:
modules = []
for module in currentApp.Root.GetModules():
module_info = {
"id": module.AppStorePackageId,
"version": module.AppStoreVersion,
"name": module.Name,
"type": 'FromAppStore' if module.FromAppStore else "NotFromAppStore"
}
modules.append(module_info)
return modules
except Exception as e:
return [{"error": f"Failed to extract modules: {str(e)}"}]
def extract_jar_dependencies(self) -> list:
"""Extract JAR dependencies from project."""
try:
project_path = self.mendix_env.get_project_path()
userlib_path = os.path.join(project_path, "userlib")
if not os.path.exists(userlib_path):
return []
jars = []
for jar_file in glob.glob(os.path.join(userlib_path, "*.jar")):
file_stat = os.stat(jar_file)
jars.append({
"name": os.path.basename(jar_file),
"path": jar_file,
"size": file_stat.st_size,
"lastModified": datetime.fromtimestamp(file_stat.st_mtime).isoformat()
})
return jars
except Exception as e:
return [{"error": f"Failed to extract JAR dependencies: {str(e)}"}]
def extract_frontend_components(self) -> list:
"""Extract frontend components and widgets, distinguishing their types."""
try:
project_path = self.mendix_env.get_project_path()
components = []
# Extract Widgets (.mpk files)
widgets_path = os.path.join(project_path, "widgets")
if os.path.exists(widgets_path):
for widget_file in glob.glob(os.path.join(widgets_path, "*.mpk")):
file_stat = os.stat(widget_file)
components.append({
"name": os.path.basename(widget_file),
"path": widget_file,
"size": file_stat.st_size,
"lastModified": datetime.fromtimestamp(file_stat.st_mtime).isoformat(),
"type": "Widget"
})
# Extract JavaScript Actions (.js files)
js_source_path = os.path.join(project_path, "javascriptsource")
if os.path.exists(js_source_path):
# The glob pattern correctly finds JS actions inside module-specific action folders
for item_full_path in glob.glob(os.path.join(js_source_path, "*", "actions", "*.js")):
if os.path.isfile(item_full_path):
file_stat = os.stat(item_full_path)
components.append({
"name": os.path.basename(item_full_path),
"path": item_full_path,
"type": "JavaScript Action",
"size": file_stat.st_size,
"lastModified": datetime.fromtimestamp(file_stat.st_mtime).isoformat()
})
return components
except Exception as e:
return [{"error": f"Failed to extract frontend components: {str(e)}"}]
def extract_app_logs(self, limit: int = 100) -> dict:
"""Extract application runtime logs (e.g., m2ee_log.txt) with line limit."""
project_path = self.mendix_env.get_project_path()
app_log_file = os.path.join(project_path, "deployment", "log", "m2ee_log.txt")
log_data = self.read_log_file(app_log_file, limit=limit)
return {
"logPath": app_log_file,
"exists": os.path.exists(app_log_file),
"lastModified": datetime.fromtimestamp(os.path.getmtime(app_log_file)).isoformat() if os.path.exists(app_log_file) else None,
**log_data
}
def format_for_forum(self, data: dict) -> str:
"""Format extracted data for comprehensive forum posting."""
output = []
output.append("# Mendix Project Diagnostic Information")
output.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
output.append("")
# Version info
if "version" in data:
output.append("## Mendix Version")
output.append(f"- **Version:** {data['version']}")
output.append("")
# Project Overview
output.append("## Project Overview")
output.append(f"- **Total Modules:** {len(data.get('modules', []))}")
output.append(f"- **JAR Dependencies:** {len(data.get('jarDependencies', []))}")
output.append(f"- **Frontend Components:** {len(data.get('frontendComponents', []))}")
output.append("")
# Modules
if "modules" in data and data["modules"]:
output.append("## Modules")
for module in data["modules"]:
output.append(f"### {module['name']}")
output.append(f"- **Type:** {module.get('type', 'N/A')}")
output.append(f"- **Version:** {module.get('version', 'N/A')}")
output.append(f"- **Guid:** {module.get('id','N/A')}")
output.append("")
# Dependencies
if "jarDependencies" in data and data["jarDependencies"]:
output.append("## JAR Dependencies")
output.append(f"- **Total JARs:** {len(data['jarDependencies'])}")
for jar in data["jarDependencies"]:
size_kb = jar.get('size', 0) / 1024
output.append(f"- {jar['name']} ({size_kb:.1f} KB)")
output.append("")
# Frontend Components
if "frontendComponents" in data and data["frontendComponents"]:
output.append("## Frontend Components")
output.append(f"- **Total Components:** {len(data['frontendComponents'])}")
for component in data["frontendComponents"]:
component_type = component.get('type', 'Widget')
if component.get('size'):
size_kb = component['size'] / 1024
output.append(f"- {component['name']} ({component_type}, {size_kb:.1f} KB)")
else:
output.append(f"- {component['name']} ({component_type})")
output.append("")
# Studio Pro Log summaries
if "studioProLogs" in data and data["studioProLogs"]:
logs = data["studioProLogs"]
output.append("## Studio Pro Logs")
output.append(f"- **Log file exists:** {logs.get('exists', False)}")
output.append(f"- **Log file path:** {sanitize_path_prefix_pathlib(logs.get('logPath', 'Unknown'))}")
output.append(f"- **Last modified:** {logs.get('lastModified', 'Unknown')}")
output.append(f"- **Total lines:** {len(logs.get('lines', []))}")
if logs.get("lines"):
output.append("### Recent Log Entries:")
output.append("```")
for line in logs["lines"]:
output.append(line.strip())
output.append("```")
output.append("")
# Git logs
if "gitLogs" in data and data["gitLogs"]:
git_logs = data["gitLogs"]
output.append("## Git Logs")
output.append(f"- **Git log file exists:** {git_logs.get('exists', False)}")
output.append(f"- **Log file path:** {sanitize_path_prefix_pathlib(git_logs.get('logPath', 'Unknown'))}")
output.append(f"- **Last modified:** {git_logs.get('lastModified', 'Unknown')}")
output.append(f"- **Total lines:** {len(git_logs.get('lines', []))}")
if git_logs.get("lines"):
output.append("### Recent Git Log Entries:")
output.append("```")
for line in git_logs["lines"]:
output.append(line.strip())
output.append("```")
output.append("")
# App logs
if "appLogs" in data and data["appLogs"]:
output.append("## Application Runtime Logs")
for log_id, log_content in data["appLogs"].items():
output.append(f"### App Log: {log_content.get('name', log_id)}")
output.append(f"- **Log file exists:** {log_content.get('exists', False)}")
output.append(f"- **Log file path:** {sanitize_path_prefix_pathlib(log_content.get('logPath', 'Unknown'))}")
output.append(f"- **Total lines:** {len(log_content.get('lines', []))}")
if log_content.get("lines"):
output.append("#### Recent Entries:")
output.append("```")
for line in log_content["lines"]:
output.append(line.strip())
output.append("```")
output.append("")
# System info
output.append("## System Information")
output.append(f"- **Operating System:** {os.name}")
output.append(f"- **Python Version:** {sys.version}")
output.append("")
return "\n".join(output)
# ===================================================================
# =================== RPC HANDLERS ==============================
# ===================================================================
import time
class GetEnvironmentRpc(IRpcHandler):
"""Get current Mendix version, project path, and IDE language."""
command_type = "app:getEnvironment"
def __init__(self, mendix_env: MendixEnvironmentService):
self.mendix_env = mendix_env
def execute(self, payload: dict) -> Any:
return {
"version": self.mendix_env.get_mendix_version(),
"projectPath": self.mendix_env.get_project_path(),
"language": self.mendix_env.get_current_language()
}
class GetVersionRpc(IRpcHandler):
"""Get current Mendix version."""
command_type = "logs:getVersion"
def __init__(self, mendix_env: MendixEnvironmentService):
self.mendix_env = mendix_env
def execute(self, payload: dict) -> Any:
return {
"version": self.mendix_env.get_mendix_version(),
"projectPath": self.mendix_env.get_project_path()
}
class ListAppLogSourcesRpc(IRpcHandler):
"""Lists all available application log sources."""
command_type = "logs:listAppLogSources"
def __init__(self, app_log_provider: AppLogSourceProvider, mendix_env: MendixEnvironmentService):
self._provider = app_log_provider
self._mendix_env = mendix_env
def execute(self, payload: dict) -> Any:
sources = self._provider.get_sources(self._mendix_env)
return [{"id": s.id, "name": s.name} for s in sources]
class GetLogContentRpc(IRpcHandler):
"""Gets the content for a specific log source by its ID."""
command_type = "logs:getContent"
def __init__(self, log_sources: List[ILogSource], log_extractor: LogExtractor, mendix_env: MendixEnvironmentService):
# We receive the provider to dynamically build the map at runtime
self._log_sources_provider = log_sources
self._log_extractor = log_extractor
self._mendix_env = mendix_env
self._source_map = None
def _ensure_map(self):
"""Builds the source map on first use."""
if self._source_map is None:
all_sources = self._log_sources_provider
self._source_map = {s.id: s for s in all_sources}
def execute(self, payload: dict) -> Any:
self._ensure_map()
log_id = payload.get('id')
if not log_id:
raise ValueError("Parameter 'id' is required.")
source = self._source_map.get(log_id)
if not source:
raise ValueError(f"Log source with id '{log_id}' not found.")
limit = payload.get('limit', 100)
path = source.get_path(self._mendix_env)
return self._log_extractor.extract_log_by_path(path, limit)
class GetAppLogsRpc(IRpcHandler):
"""Get Application runtime logs."""
command_type = "logs:getAppLogs"
def __init__(self, log_extractor: LogExtractor):
self.log_extractor = log_extractor
def execute(self, payload: dict) -> Any:
limit = payload.get('limit', 100)
return self.log_extractor.extract_app_logs(limit=limit)
class GetStudioProLogsRpc(IRpcHandler):
"""Get Studio Pro logs."""
command_type = "logs:getStudioProLogs"
def __init__(self, log_extractor: LogExtractor, mendix_env: MendixEnvironmentService):
self.log_extractor = log_extractor
self.mendix_env = mendix_env
def execute(self, payload: dict) -> Any:
version = self.mendix_env.get_mendix_version()
limit = payload.get('limit', 100)
return self.log_extractor.extract_studio_pro_logs(version, limit=limit)
class GetGitLogsRpc(IRpcHandler):
"""Get Git logs."""
command_type = "logs:getGitLogs"
def __init__(self, log_extractor: LogExtractor, mendix_env: MendixEnvironmentService):
self.log_extractor = log_extractor
self.mendix_env = mendix_env
def execute(self, payload: dict) -> Any:
version = self.mendix_env.get_mendix_version()
limit = payload.get('limit', 100)
return self.log_extractor.extract_git_logs(version, limit=limit)
class GetModulesInfoRpc(IRpcHandler):
"""Get modules information."""
command_type = "logs:getModulesInfo"
def __init__(self, log_extractor: LogExtractor):
self.log_extractor = log_extractor
def execute(self, payload: dict) -> Any:
return self.log_extractor.extract_modules_info()
class GetJarDependenciesRpc(IRpcHandler):
"""Get JAR dependencies."""
command_type = "logs:getJarDependencies"
def __init__(self, log_extractor: LogExtractor):
self.log_extractor = log_extractor
def execute(self, payload: dict) -> Any:
return self.log_extractor.extract_jar_dependencies()
class GetFrontendComponentsRpc(IRpcHandler):
"""Get frontend components."""
command_type = "logs:getFrontendComponents"
def __init__(self, log_extractor: LogExtractor):
self.log_extractor = log_extractor
def execute(self, payload: dict) -> Any:
return self.log_extractor.extract_frontend_components()
class GenerateCompleteForumExportRpc(IRpcHandler):
"""Generate complete forum export with all log data."""
command_type = "logs:generateCompleteForumExport"
def __init__(self,all_log_sources_provider, log_extractor: LogExtractor, mendix_env: MendixEnvironmentService):
self.log_extractor = log_extractor
self.mendix_env = mendix_env
self.all_log_sources_provider = all_log_sources_provider
def execute(self, payload: dict) -> Any:
version = self.mendix_env.get_mendix_version()
all_sources = self.all_log_sources_provider
# Extract all log data by iterating through sources
all_log_data = {}
for source in all_sources:
path = source.get_path(self.mendix_env)
all_log_data[source.id] = self.log_extractor.extract_log_by_path(path)
# Separate logs for forum formatting
studio_pro_log = all_log_data.get('studio_pro')
git_log = all_log_data.get('git')
app_logs = {k: v for k, v in all_log_data.items() if k.startswith('app_')}
for log in app_logs.values(): # Add name property for easier formatting
log['name'] = os.path.basename(log['logPath'])
modules_info = self.log_extractor.extract_modules_info()
jar_dependencies = self.log_extractor.extract_jar_dependencies()
frontend_components = self.log_extractor.extract_frontend_components()
# Combine all data
all_data = {
"version": version,
"studioProLogs": studio_pro_log,
"gitLogs": git_log,
"appLogs": app_logs,
"modules": modules_info,
"jarDependencies": jar_dependencies,
"frontendComponents": frontend_components
}
# Format for forum
formatted_text = self.log_extractor.format_for_forum(all_data)
return {
"formattedText": formatted_text,
"timestamp": datetime.now().isoformat(),
"data": all_data
}
# endregion
# region IOC & APP INITIALIZATION
# ===================================================================
# ============== IOC & APP INITIALIZATION ===================
# ===================================================================
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
"""The application's Inversion of Control (IoC) container."""
config = providers.Configuration()
# --- Framework Services (DIP) ---
message_hub: providers.Provider[IMessageHub] = providers.Singleton(
MendixMessageHub,
post_message_func=config.post_message_func
)
mendix_env = providers.Singleton(
MendixEnvironmentService,
app_context=config.app_context,
window_service=config.window_service,
post_message_func=config.post_message_func,
)
log_extractor = providers.Singleton(LogExtractor, mendix_env=mendix_env)
studio_pro_log_source = providers.Singleton(
StaticLogSource,
id="studio_pro",
name="Studio Pro Log",
path_func=lambda env: os.path.join(env.get_mendix_log_path(env.get_mendix_version()), "log.txt")
)
git_log_source = providers.Singleton(
StaticLogSource,
id="git",
name="Git Log",
path_func=lambda env: os.path.join(env.get_mendix_log_path(env.get_mendix_version()), "git", "git.log.txt")
)
app_log_source_provider = providers.Singleton(AppLogSourceProvider)
# Provider that aggregates all log sources
all_log_sources = providers.Callable(
lambda studio, git, app_list: [studio, git] + app_list,
studio_pro_log_source,
git_log_source,
providers.Callable(lambda provider, env: provider.get_sources(env), app_log_source_provider, mendix_env),
)
# --- Business Logic Handlers (OCP) ---
rpc_handlers = providers.List(
providers.Singleton(GetEnvironmentRpc, mendix_env=mendix_env),
providers.Singleton(GetVersionRpc, mendix_env=mendix_env),
providers.Singleton(ListAppLogSourcesRpc, app_log_provider=app_log_source_provider, mendix_env=mendix_env),
providers.Singleton(GetLogContentRpc, log_sources=all_log_sources, log_extractor=log_extractor, mendix_env=mendix_env),
providers.Singleton(GetModulesInfoRpc, log_extractor=log_extractor),
providers.Singleton(GetJarDependenciesRpc, log_extractor=log_extractor),
providers.Singleton(GetFrontendComponentsRpc, log_extractor=log_extractor),
providers.Singleton(GenerateCompleteForumExportRpc, all_log_sources_provider=all_log_sources, log_extractor=log_extractor, mendix_env=mendix_env),
)
job_handlers = providers.List(
)
session_handlers = providers.List(
)
# --- Core Controller ---
app_controller = providers.Singleton(
AppController,
rpc_handlers=rpc_handlers,
job_handlers=job_handlers,
session_handlers=session_handlers,
message_hub=message_hub,
)
def onMessage(e: Any):
"""Entry point called by Mendix Studio Pro for messages from the UI."""
if e.Message != "frontend:message": return
controller = container.app_controller()
try:
request_string = JsonSerializer.Serialize(e.Data)
request_object = json.loads(request_string)
controller.dispatch(request_object)
except Exception as ex:
traceback.print_exc()
def initialize_app():
container = Container()
container.config.from_dict({
"post_message_func": PostMessage,
"app_context": currentApp,
"window_service": dockingWindowService
})
return container
# --- Application Start ---
PostMessage("backend:clear", '')
container = initialize_app()
PostMessage("backend:info", "Mendix Log Extractor Plugin initialized successfully.")
# endregion
{
"name": "mendix-log-extractor",
"description": "A comprehensive plugin for extracting and analyzing Mendix Studio Pro logs, project information, and dependencies. Features include Studio Pro log extraction, Git log analysis, module information, JAR dependencies, frontend components, and forum-ready export formatting.",
"author": "wengao liu",
"email": "liuwengao@foxmail.com",
"ui": "index.html",
"plugin": "main.py",
"deps": [
"pythonnet",
"dependency-injector"
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment