cnumr / EcoindexApp

EcoindexApp. Cette application de bureau vous permet d'effectuer des mesures comme sur le site econindex.fr , mais aussi d'avoir les mesures de lighthouse de Google, dans un seul et même rapport, sans limitation de mesures.
https://cnumr.github.io/lighthouse-plugin-ecoindex/
GNU Affero General Public License v3.0
4 stars 0 forks source link

nouvelle initialisation de l'application, utilisant un node externe #2

Closed hrenaud closed 1 month ago

hrenaud commented 2 months ago

Pour gérer le problème #46, une nouvelle manière d'initialiser l'application doit être mise en place.

  1. Une méthode qui déclenche depuis le front tout le process gérer par le back;
  2. Le back utilise une méthode pour informer le front de la lecture des valeurs nécessaires où des erreurs via sendConfigDatasToFront: (callback) => ConfigData
  3. Mise en place d'un object de transport ConfigData.

Process à mettre en place: EcoindexApp.drawio.pdf


Normalement, c'est OK, à tester sur:

hrenaud commented 2 months ago

Try

hrenaud commented 1 month ago

To keep

import { ChildProcess, fork, spawn } from 'child_process'
import { IpcMainEvent, shell } from 'electron'
import { getNodeDir, getNpmDir, getWorkDir, isDev } from '../memory'

import { _debugLogs } from '../utils/MultiDebugLogs'
import { _echoReadable } from '../utils/EchoReadable'
import { _sendMessageToFrontLog } from '../utils/SendMessageToFrontLog'
import { convertJSONDatasFromISimpleUrlInput } from '../utils/ConvertJSONDatas'
import fs from 'node:fs'
import { getMainLog } from '../main'
import i18n from '../../configs/i18next.config'
import os from 'node:os'
import path from 'node:path'
// import { runCourses } from 'lighthouse-plugin-ecoindex/run.cjs'
import { showNotification } from '../utils/ShowNotification'
import { utils } from '../../shared/constants'

const runCourses = import('lighthouse-plugin-ecoindex/run.cjs')
/**
 * Utils, prepare Json Collect.
 * @returns Promise<{
  command: string[]
  nodeDir: string
  workDir: string
}>
 */
async function _prepareCollect(): Promise<{
    command: string[]
    nodeDir: string
    workDir: string
}> {
    const mainLog = getMainLog().scope('main/prepareCollect')
    // create stream to log the output. TODO: use specified path
    try {
        const _workDir = getWorkDir() as string
        if (!_workDir || _workDir === '') {
            throw new Error('Work dir not found')
        }

        let nodeDir = getNodeDir()
        _debugLogs(`Node dir: ${nodeDir}`)

        const npmDir = getNpmDir()
        _debugLogs(`Npm dir: ${npmDir}`)

        const command = [
            path.join(
                __dirname,
                `../..`,
                `node_modules`,
                `lighthouse-plugin-ecoindex`,
                `cli`,
                `run.js`
            ),
            'collect',
        ]
        if (os.platform() === `win32`) {
            nodeDir = nodeDir.replace(/\\/gm, path.sep)
        }
        return { command, nodeDir, workDir: _workDir.replace(/ /g, '\\ ') }
    } catch (error) {
        mainLog.error('Error in _prepareCollect', error)
    }
}

class CollectDatas {
    collectType: `simple` | `complexe`
    outputPath: string
    output?: string[]
    listAllAudits: false
    generationDate: string
}

class SimpleCollectDatas extends CollectDatas {
    declare collectType: 'simple'
    url: string[]
}
class ComplexeCollectDatas extends CollectDatas {
    declare collectType: 'complexe'
    jsonFile: string
}

function _prepareDatas(
    collectType: `simple` | `complexe`,
    output: string[],
    input: string | ISimpleUrlInput[]
): ComplexeCollectDatas | SimpleCollectDatas {
    const _workDir = getWorkDir() as string
    if (!_workDir || _workDir === '') {
        throw new Error('Work dir not found')
    }
    if (collectType === 'simple') {
        const collectDatas: SimpleCollectDatas = {
            collectType,
            outputPath: _workDir,
            output,
            url: (input as ISimpleUrlInput[]).map((url) => {
                return url.value
            }),
            listAllAudits: false,
            generationDate: new Date().toISOString(),
        }
        return collectDatas
    } else {
        const collectDatas: ComplexeCollectDatas = {
            collectType,
            outputPath: _workDir,
            output,
            jsonFile: input as string,
            listAllAudits: false,
            generationDate: new Date().toISOString(),
        }
        return collectDatas
    }
}

/**
 * Utils, Collect
 * @param command string[]
 * @param nodeDir string
 * @param event IpcMainEvent
 * @param logStream
 * @returns string
 */
async function _runCollect(
    command: string[],
    nodeDir: string,
    event: IpcMainEvent,
    isSimple = false
): Promise<string> {
    const mainLog = getMainLog().scope('main/runCollect')
    try {
        const out: string[] = []

        const [script, ...args] = command
        _debugLogs(`runCollect: ${script} ${JSON.stringify(args, null, 2)}`)
        _debugLogs(
            `runCollect: ${process.execPath} ${JSON.stringify(command, null, 2)}`
        )
        // const controller = new AbortController()
        // const { signal } = controller
        // const childProcess: ChildProcess = fork(script, args, {
        //     stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
        //     // stdio: [0, 1, 2, 'ipc'],
        //     // silent: true,
        //     // signal,
        // })
        // childProcess.send('hello')
        const childProcess: ChildProcess = spawn(nodeDir, command, {
            stdio: ['pipe', 'pipe', process.stderr],
            shell: true,
            windowsHide: true,
            // signal,
            // signal,
        })
        childProcess.on('message', (mess) => {
            _debugLogs(`Message detected`, mess)
        })
        childProcess.on('error', (err) => {
            _debugLogs(`Error detected`, err)
            // This will be called with err being an AbortError if the controller aborts
        })

        childProcess.on('exit', (code, signal) => {
            if (isSimple && out.length > 0) {
                const fl = (item: string) => {
                    return item.includes('Report generated')
                }
                const filtered = out.filter(fl)
                const url =
                    'file:///' +
                    filtered
                        .at(-1)
                        .replace(`Report generated: `, ``)
                        .split('generic.report.html')[0] +
                    `generic.report.html`
                mainLog.debug(`url`, url)
                shell.openExternal(url, { activate: true })
            }
            _debugLogs(
                `Child process exited with code ${code} and signal ${signal}`
            )
        })

        childProcess.on('close', (code) => {
            _debugLogs(`Child process close with code ${code}`)
            _debugLogs('Measure done 🚀')
        })

        childProcess.stdout.on('data', (data) => {
            out.push(data.toString())
            _debugLogs(`stdout: ${data}`)
        })

        if (childProcess.stderr) {
            childProcess.stderr.on('data', (data) => {
                _debugLogs(`stderr: ${data.toString()}`)
            })
        }

        childProcess.on('disconnect', () => {
            _debugLogs('Child process disconnected')
        })

        childProcess.on('message', (message, sendHandle) => {
            _debugLogs(`Child process message: ${message}`)
        })

        await _echoReadable(event, childProcess.stdout)
        // controller.abort()
        return 'mesure done'
    } catch (error) {
        mainLog.error('Error in _runCollect', error)
    }
}

async function _runDirectCollect(
    command: SimpleCollectDatas | ComplexeCollectDatas,
    event: IpcMainEvent,
    isSimple = false
) {
    const mainLog = getMainLog().scope('main/runDirectCollect')
    try {
        // eslint-disable-next-line @typescript-eslint/no-extra-semi
        ;(await runCourses).default(command)
        // gérer les logs
        if (isSimple) {
            // gérer l'ouverture dans le navigateur is simple
            shell.openExternal(
                path.join(
                    command.outputPath,
                    command.generationDate,
                    `generic.report.html`
                ),
                {
                    activate: true,
                }
            )
        }
        return 'mesure done'
    } catch (error) {
        mainLog.error('Error in _runCollect', error)
    }
}

/**
 * Handlers, SimpleCollect
 * @param event IpcMainEvent
 * @param urlsList ISimpleUrlInput[]
 * @returns string
 */
export const handleSimpleCollect = async (
    event: IpcMainEvent,
    urlsList: ISimpleUrlInput[]
) => {
    const mainLog = getMainLog().scope('main/handleSimpleCollect')
    if (!urlsList || urlsList.length === 0) {
        throw new Error('Urls list is empty')
    }
    showNotification({
        subtitle: i18n.t('🧩 Simple collect'),
        body: i18n.t('Process intialization.'),
    })

    // prepare common collect
    // const collectDatas = _prepareDatas(`simple`, [`html`], urlsList)
    const { command, nodeDir, workDir: _workDir } = await _prepareCollect()

    _debugLogs('Simple measure start, process intialization...')
    _debugLogs(`Urls list: ${JSON.stringify(urlsList)}`)
    try {
        urlsList.forEach((url) => {
            if (url.value) {
                command.push('-u')
                command.push(url.value)
            }
        })
        command.push('-o')
        command.push('html')
        command.push('--output-path')
        command.push(`${_workDir}`)
        // Fake mesure and path. TODO: use specified path and urls
        showNotification({
            subtitle: i18n.t(' 🚀Simple collect'),
            body: i18n.t('Collect started...'),
        })
        try {
            if (isDev()) {
                // mainLog.debug(`before (simple) runCollect`, nodeDir, command)
                const [script, ...args] = command
                mainLog.debug(`before (simple) runCollect`, script, args)
                // mainLog.debug(`before (simple) runCollect`, collectDatas)
            }
            await _runCollect(command, nodeDir, event, true)
            // await _runDirectCollect(collectDatas, event, true)
        } catch (error) {
            showNotification({
                subtitle: i18n.t('🚫 Simple collect'),
                body: i18n.t(`Collect KO, {{error}}\n`, { error }),
            })
            throw new Error('Simple collect error')
        }
        // process.stdout.write(data)
        // const _workDir = collectDatas.outputPath
        showNotification({
            subtitle: i18n.t('🎉 Simple collect'),
            body: i18n.t(
                `Collect done, you can consult reports in\n{{_workDir}}`,
                { _workDir }
            ),
        })
        if (isDev()) mainLog.debug('Simple collect done 🚀')
        return 'collect done'
    } catch (error) {
        _debugLogs(`stderr: ${error}`)
    }
    // alert process done
}

/**
 * Handler, JsonSaveAndCollect
 * @param event IpcMainEvent
 * @param jsonDatas IJsonMesureData
 * @param andCollect boolean
 * @returns string
 */
export const handleJsonSaveAndCollect = async (
    event: IpcMainEvent,
    jsonDatas: IJsonMesureData,
    andCollect: boolean
) => {
    const mainLog = getMainLog().scope('main/handleJsonSaveAndCollect')
    if (!jsonDatas) {
        throw new Error('Json data is empty')
    }
    showNotification({
        subtitle: andCollect
            ? i18n.t('🧩 JSON save and collect')
            : i18n.t('🧩 JSON save'),
        body: i18n.t('Process intialization.'),
    })
    _debugLogs('Json save or/and collect start...')

    try {
        const _workDir = await getWorkDir()
        if (!_workDir || _workDir === '') {
            throw new Error('Work dir not found')
        }
        // _workDir = (_workDir as string).replace(/ /g, '\\\\ ')
        if (isDev()) mainLog.debug(`Work dir: ${_workDir}`)
        const jsonFilePath = path.join(_workDir as string, utils.JSON_FILE_NAME)
        const jsonStream = fs.createWriteStream(jsonFilePath)
        showNotification({
            subtitle: andCollect
                ? i18n.t('🚀 JSON save and collect')
                : i18n.t('🚀 JSON save'),
            body: andCollect
                ? i18n.t('Json save and collect started...')
                : i18n.t('Json save started...'),
        })
        try {
            if (jsonDatas && typeof jsonDatas === 'object') {
                jsonStream.write(
                    JSON.stringify(
                        convertJSONDatasFromISimpleUrlInput(jsonDatas),
                        null,
                        2
                    )
                )
            } else {
                mainLog.error('jsonDatas have a problem!')
                throw new Error('jsonDatas have a problem!')
            }
        } catch (error) {
            showNotification({
                subtitle: andCollect
                    ? i18n.t('🚫 JSON save and collect')
                    : i18n.t('🚫 JSON save'),
                body: i18n.t('Json file not saved.'),
            })
            _debugLogs(`Error writing JSON file. ${error}`)
            throw new Error(`Error writing JSON file. ${error}`)
        }
        if (!andCollect) {
            showNotification({
                subtitle: i18n.t('💾 JSON save'),
                body: i18n.t('Json file saved.'),
            })
        } else {
            if (isDev()) mainLog.debug('Json measure start...')

            // prepare common collect
            const collectDatas = _prepareDatas(`complexe`, null, jsonFilePath)
            // const {
            //     command,
            //     nodeDir,
            //     workDir: _workDir,
            // } = await _prepareCollect()
            _debugLogs('Json measure start...')
            _debugLogs(`JSON datas ${JSON.stringify(jsonDatas, null, 2)}`)
            // command.push('--json-file')
            // command.push(path.join(_workDir, utils.JSON_FILE_NAME))
            // command.push('--output-path')
            // command.push(_workDir)
            try {
                // await _runCollect(command, nodeDir, event)
                await _runDirectCollect(collectDatas, event, false)
            } catch (error) {
                mainLog.error('Simple collect error', error)
                throw new Error('Simple collect error')
            }
            showNotification({
                subtitle: i18n.t('🎉 JSON collect'),
                body: i18n.t(
                    `Measures done, you can consult reports in\n{{_workDir}}`,
                    { _workDir }
                ),
            })
            try {
                shell.openPath((await getWorkDir()) as string)
            } catch (error) {
                mainLog.error(error)
            }
            _debugLogs('Json collect done 🚀')
            return 'measure done'
        }
    } catch (error) {
        if (!andCollect) {
            _sendMessageToFrontLog('ERROR, Json file not saved', error)
            _debugLogs('ERROR, Json file not saved', error)
            showNotification({
                subtitle: i18n.t('🚫 JSON save'),
                body: i18n.t('Json file not saved.'),
            })
        } else {
            _sendMessageToFrontLog(
                'ERROR, Json file not saved or collect',
                error
            )
            _debugLogs('ERROR, Json file not saved or collect', error)
            showNotification({
                subtitle: i18n.t('🚫 JSON save and collect'),
                body: i18n.t('Json file not saved or collect.'),
            })
        }
    }
}