Search code examples
node.jselectron

Register handler for content and window


For an environment i am developing (elecron + bs5.3 + node.js + go + cobra ...), the objective of which is to create a web interface so that users can execute system commands using buttons and forms on: Windows, Linux or Mac to Manage Dockers Container Environments.

I am currently getting this error:

Error: No handler registered for 'check-admin-rights'

According to this answer:

https://stackoverflow.com/a/65830600/29561751

I had this same issue. My problem was that I was instructing electron to navigate to my app before my IPC handlers were being registered. The reason it was only happening on the first load and not subsequent loads/reloads (and hot-reloads) was because the BrowserWindow was getting recreated and my IPC handlers were already registered (and getting re-registered in my code).

So be absolutely sure that your handlers are being registered before the BrowserWindow loads the renderer code that calls the IPC channels.

The problem I have is because of the order in which this.mainWindow and the ipcController are built in my case I have this:

initial code:

await app.whenReady();

        try {

            this.windowManager = new WindowManager();
            this.mainWindow = await this.windowManager.createMainWindow();

            if (this.mainWindow) {
                this.ipcController = new IpcController(
                    this.windowManager,
                    this.backendController,
                    this.appConfig
                );

                DevToolsManager.setupDevToolsHandlers(this.mainWindow);
            }
        } catch (error) {
            console.error('Setup error:', error);
            throw error;
        }
    }

I don't know how I should implement that response in my code since there is no example.

The other point is that inside my ipcController I am passing this.windowManager which currently already contains the mainWindow deployed, this is so I can re-create the events for the minimize, maximize and close buttons, as I understand it and what I have learned. This is done so that my custom buttons work perfectly.


Try changing the order to this:

        await app.whenReady();

        try {
            this.windowManager = new WindowManager();
            
            this.ipcController = new IpcController(
                this.windowManager,
                this.backendController,
                this.appConfig
            );
            
            this.mainWindow = await this.windowManager.createMainWindow();
            DevToolsManager.setupDevToolsHandlers(this.mainWindow);

The error: No handler registered ... disappears!!! ... The problem now is that the other handlers stopped working and in the IDE console I get this:

[IPC] No main window reference

This error appears when clicking on my custom minimize, maximize and close buttons.

I managed to solve one problem only to fall into another... and now I don't know how to solve this one, should I have 2 ipcControllers, one for the events associated with the window and another for the rendering events?


resources

this is my preload file:

const { contextBridge, ipcRenderer } = require('electron');

const validChannels = new Set([
    'window-control',
    'open-settings',
    'open-help',
    'open-about',
    'maintenance',
    'execute-command',
    'open-user-data-path',
    'refresh-security-nonce',
    'check-admin-rights'
]);


contextBridge.exposeInMainWorld('electronAPI', {
    invoke: async (channel, data) => {
        if (validChannels.has(channel)) {
            return await ipcRenderer.invoke(channel, data);
        }
        throw new Error(`Invalid channel: ${channel}`);
    },
    send: (channel, data) => {
        if (validChannels.has(channel)) {
            ipcRenderer.send(channel, data);
        }
    },
    receive: (channel, func) => {
        if (validChannels.has(channel)) {
            ipcRenderer.on(channel, (event, ...args) => func(...args));
        }
    }
});

this is my renderer.js file:

document.addEventListener('DOMContentLoaded', async () => {
    const windowControls = {
        minimize: document.getElementById('minimizeBtn'),
        maximize: document.getElementById('maximizeBtn'),
        close: document.getElementById('closeBtn')
    };

    Object.entries(windowControls).forEach(([action, element]) => {
        element.addEventListener('click', () => {
            window.electronAPI.send('window-control', action);
        });
    });

    //...

    // Get the admin button element
    const adminButton = document.getElementById('isAdmin');

    try {
        const isAdmin = await window.electronAPI.invoke('check-admin-rights');
        console.log('Admin privileges check result:', isAdmin);
        if (isAdmin) {
            adminButton.style.display = 'block';
            adminButton.setAttribute('title', 'Running with Administrator privileges');
        } else {
            adminButton.style.display = 'none';
        }
    } catch (error) {
        console.error('Failed to check admin privileges:', error);
        adminButton.style.display = 'none';
    }

});

this is my IpcController:

const { ipcMain } = require('electron');
const { shell } = require('electron');
const AdminPrivilegesManager = require('../core/AdminPrivilegesManager');

class IpcController {
    constructor(windowManager, backendController, appConfig) {
        this.mainWindow = windowManager.getMainWindow();
        this.backendController = backendController;
        this.appConfig = appConfig;
        this.setupHandlers();
    }

    setupHandlers() {

        ipcMain.on('window-control', (event, command) => {
            if (!this.mainWindow) {
                console.error('[IPC] No main window reference');
                return;
            }

            switch (command) {
                case 'minimize':
                    this.mainWindow.minimize();
                    break;
                case 'maximize':
                    if (this.mainWindow.isMaximized()) {
                        this.mainWindow.unmaximize();
                    } else {
                        this.mainWindow.maximize();
                    }
                    break;
                case 'close':
                    this.mainWindow.close();
                    break;
                default:
                    console.error(`[IPC] Unknown window command: ${command}`);
            }
        });

        ipcMain.on('open-settings', () => {
            // Implement settings window logic
            console.log('Settings requested');
        });

        ipcMain.on('open-help', () => {
            // Implement help window logic
            console.log('Help requested');
        });

        ipcMain.on('open-about', () => {
            // Implement about window logic
            console.log('About requested');
        });

        // Maintenance handlers
        ipcMain.on('maintenance', (event, action) => {
            switch(action) {
                case 'check-updates':
                    console.log('Checking for updates...');
                    break;
                case 'backup-settings':
                    console.log('Backing up settings...');
                    break;
                case 'clear-cache':
                    console.log('Clearing cache...');
                    break;
                case 'reset-preferences':
                    console.log('Resetting preferences...');
                    break;
                case 'exit':
                    this.mainWindow.close();
                    break;
                default:
                    console.error(`Unknown maintenance action: ${action}`);
            }
        });

        ipcMain.handle('open-user-data-path', () => {
            shell.openPath(this.appConfig.getUserDataPath())
                .then(result => console.log('Opened user data path:', result))
                .catch(err => console.error('Error opening user data path:', err));
        });

        // Command execution handler
        ipcMain.handle('execute-command', async (event, cmd) => {
            return await this.backendController.executeCommand(cmd);
        });

        // Add this handler with the existing ones
        ipcMain.handle('check-admin-rights', async () => {
            try {
                return await AdminPrivilegesManager.isRunningAsAdmin();
            } catch (error) {
                console.error('Error checking admin rights:', error);
                return false;
            }
        });
    }
}

module.exports = IpcController;

Solution

  • I want to clarify that I am not an expert in node.js and much less in electron, I am making this application for personal use and as I learn about the use of javascript focused on the backend with node.js, it is possible that I am not applying community conventions or best practices.

    I have managed to get the solution in the following way:

    • After whenReady.
    • First create the IpcController passing as argument windowManager.
    • then we create the mainWindow, for rendering.
    • This is where the magic happens, we inject into the IpcController the mainWindow resulting from the window creation..
    await app.whenReady();
    this.ipcController = new IpcController(
        this.windowManager,
        this.backendController,
        this.appConfig
    );
    
    this.mainWindow = await this.windowManager.createMainWindow();
    
    this.ipcController.setMainWindow(this.mainWindow);
    
    

    The new method in ipcController is:

    setMainWindow(window) {
        this.windowManager.mainWindow = window;
    }
    

    With this order and logic, both the content handlers and those associated with interactions of the created window work correctly.

    I think the answer I found only works for event control of rendered content so it is not a general solution.