import * as path from "path"; import * as grpc from "@grpc/grpc-js"; import * as protoLoader from "@grpc/proto-loader"; import { IronPdfServiceClient } from "./generated_proto/ironpdfengineproto/IronPdfService"; import * as cp from "child_process"; import * as Os from "os"; import * as fs from "fs"; import { glob } from "glob"; import * as https from "https"; import * as unzipper from "unzipper"; import { spawn } from "child_process"; import * as net from "net"; import * as util from "util"; import {ProtoGrpcType} from "./generated_proto/iron_pdf_service"; import {IronPdfGlobalConfig} from "../public/ironpdfglobalconfig"; import {handshakeWithRetry} from "./grpc_layer/handshake"; import {setIsDebug, setLicenseKey} from "./grpc_layer/system"; export class Access { private static _instance: Access; private constructor() { if (Access._instance) { throw new Error( "Error: Instantiation failed: Use Access.getInstance() instead of new." ); } Access._instance = this; } public static get Instance() { return this._instance || (this._instance = new this()); } public static forceShutdown() { if(this.ironPdfEngineProcess) this.ironPdfEngineProcess?.kill(); } public static usedDocumentIds = new Set(); private static PROTO_FILE = "IronPdfEngine.ProtoFiles/iron_pdf_service.proto"; private static packageDef = protoLoader.loadSync( path.resolve(__dirname, this.PROTO_FILE) ); private static grpcObj = grpc.loadPackageDefinition( this.packageDef ) as unknown as ProtoGrpcType; private static client: IronPdfServiceClient; private static targetDir: string = path.join( __dirname, `../../ironpdf-engine-bin-${IronPdfGlobalConfig.ironPdfEngineVersion}` ); public static ironPdfEngineAddress = `127.0.0.1:33350`; private static ironPdfEngineProcess: cp.ChildProcess; private static downloadFromCDN(): Promise { return new Promise((resolve, reject) => { let redirectCount = 0; const zipFilePath = "./ironPdfEngineDownload.zip"; const downloadZip = (url: string) => { https .get(url, (response) => { if ( response.statusCode === 302 || response.statusCode === 301 ) { if (redirectCount >= 5) { reject("Too many redirects"); return; } redirectCount++; const redirectUrl: string | undefined = response.headers.location; if (!redirectUrl) { reject( `Invalid redirect URL code: ${response.statusCode} : ${redirectUrl}` ); } downloadZip(redirectUrl!); return; } if (response.statusCode !== 200) { reject( `Invalid status code: ${response.statusCode}` ); return; } const totalLength: number = parseInt( response.headers["content-length"]!, 10 ); let downloadedLength = 0; const zipFile = fs.createWriteStream(zipFilePath); let lastLoggedPercent = 0; response.on("data", (data) => { downloadedLength += data.length; const percent = Math.floor( ((downloadedLength / totalLength) * 100) / 10 ) * 10; if (percent > lastLoggedPercent && percent < 100) { console.debug( `Download IronPdfEngine progress: ${percent}%` ); lastLoggedPercent = percent; } zipFile.write(data); }); response.on("end", () => { console?.log(`Download IronPdfEngine complete`); zipFile.end(); }); zipFile.on("finish", () => { console?.log( `Extract IronPdfEngine Zip to ${this.targetDir}` ); const readStream = fs.createReadStream(zipFilePath); readStream.on("open", () => { readStream .pipe( unzipper.Extract({ path: this.targetDir, }) ) .on("close", () => { try { fs.unlinkSync(zipFilePath); } catch (e) {} resolve(); }) .on("error", (error) => { reject( `Error extracting ZIP file: ${error}` ); }); }); readStream.on("error", (error) => { reject(`Error reading ZIP file: ${error}`); }); }); response.on("error", (error) => { reject(`Error downloading ZIP file: ${error}`); }); }) .on("error", (error) => { reject(`Error downloading ZIP file: ${error}`); }); }; const zipUrl = `https://ironpdfengine.azurewebsites.net/api/IronPdfEngineDownload?version=${ IronPdfGlobalConfig.ironPdfEngineVersion }&platform=${getPlatformName()}&architect=${getOsArch()}`; console.debug("Download IronPdfEngine"); downloadZip(zipUrl); }); } private static async tryDeleteUnusedEngineBin( baseDir: string, excludeFolder: string ): Promise { try { //const folders = await glob(path.join(baseDir, 'ironpdf-engine-bin-*')); const wc = path .join(baseDir, "ironpdfenginebin*") .replace(/\\/g, "/"); const folders = await glob.glob(wc, { absolute: true }); // exclude the folder you don't want to delete const foldersToDelete = folders.filter( (folder) => path.basename(folder) !== excludeFolder ); // delete all other folders await Promise.all( foldersToDelete.map((folder) => fs.promises.rmdir(folder, { recursive: true }) ) ); } catch (err) {} } private static async getAvailableIronPdfEngineFile() { let dir; try { const ironPdfEnginePackageName = `@ironsoftware/ironpdf-engine-${getOsName()}-${getOsArch()}`; // eslint-disable-next-line @typescript-eslint/no-var-requires const ironPdfEnginePackage = require(ironPdfEnginePackageName); dir = `${ironPdfEnginePackage.dir}${path.sep}ironpdf-engine-bin-${ironPdfEnginePackage.version}`; console.debug( `FOUND ${ironPdfEnginePackageName}:${ironPdfEnginePackage.version} at:${dir}` ); } catch (e) { //NOT FOUND ironpdf-engine-windows-x64, ignore //if files exists Locally const isLocalFilesExists = fs.existsSync( `${this.targetDir}${path.sep}${ironPdfEngineExecutable()}` ); if (!isLocalFilesExists) { await this.downloadFromCDN(); } dir = this.targetDir; } await tryChangePermissions(dir, "777"); return `${dir}${path.sep}${ironPdfEngineExecutable()}`; } private static async startServer() { const config = IronPdfGlobalConfig.getConfig(); if (config.debugMode) console.debug("Start IronPdfEngine"); const ironPdfEngineBinPath = await this.getAvailableIronPdfEngineFile(); if (config.debugMode) console.debug(`IronPdfEngine bin: ${ironPdfEngineBinPath}`); let host = "localhost"; let port = "33350"; if (config.ironPdfEngineAddress) { const splitter = config.ironPdfEngineAddress.lastIndexOf(":"); host = config.ironPdfEngineAddress.substring(0, splitter); port = config.ironPdfEngineAddress.substring(splitter + 1); this.ironPdfEngineAddress = config.ironPdfEngineAddress; } const args = [ `host=${host}`, `port=${port}`, `docker_build=false`, `keep_alive=true`, `linux_and_docker_auto_config=false`, `skip_initialization=false`, `single_process=${config.singleProcess ?? getOsName() == "macos"}`, `chrome_browser_limit=${config.chromeBrowserLimit??"30"}`, `chrome_gpu_mode=${config.chromeGpuMode??0}`, `linux_and_docker_auto_config=${config.autoInstallDependency??"true"}`, `programming_language=nodejs` ]; // Add license key if configured (from config or environment variable) const licenseKey = config.licenseKey || process.env['IRONPDF_LICENSE_KEY']; if (licenseKey) { args.push(`license_key=${licenseKey}`); } if (config.debugMode) { args.push(`enable_debug=true`) args.push(`log_path=./IronPdfEngine.log`) } else { args.push(`enable_debug=false`) } if(config.chromeBrowserCachePath){ args.push(`chrome_cache_path=${config.chromeBrowserCachePath}`) } if (config.debugMode){ console.debug("args:"+JSON.stringify(args)) } const ironPdfEngineProcess: cp.ChildProcess = spawn( `${ironPdfEngineBinPath}`, args, { detached: false, stdio: IronPdfGlobalConfig.getConfig().debugMode ? ["ignore"] : "ignore", } ) .on("error", (err) => console?.debug(`spawn IRON_PDF_ENGINE error: ${err}`) ) .on("message", (err) => console?.debug(`spawn IRON_PDF_ENGINE message: ${err}`) ); this.ironPdfEngineProcess = ironPdfEngineProcess; if (IronPdfGlobalConfig.getConfig().debugMode) { this.ironPdfEngineProcess.stdout?.on("data", (data) => { console?.debug(`[IRON_PDF_ENGINE] ${data}`); }); } process.on("exit", function () { ironPdfEngineProcess.kill(); }); process.on("beforeExit", function () { ironPdfEngineProcess.kill(); }); process.on("disconnect", function () { ironPdfEngineProcess.kill(); }); process.on("SIGINT", () => { ironPdfEngineProcess.kill(); }); process.on("SIGTERM", () => { ironPdfEngineProcess.kill(); }); this.ironPdfEngineProcess.unref(); Access.tryDeleteUnusedEngineBin( path.join(__dirname, `../..`), `ironpdf-engine-bin-${IronPdfGlobalConfig.ironPdfEngineVersion}` ).then(); if (IronPdfGlobalConfig.getConfig().debugMode) console.debug("wait for IronPdfEngine to start up "); await this.waitUntilPortIsOpen(+port); await new Promise((resolve) => setTimeout(resolve, 10000)); } private static async checkPort(port: number) { return new Promise((resolve, reject) => { const server = net .createServer() .once("error", (err) => { if (err.name !== "EADDRINUSE") reject(err); }) .once("listening", () => { server.close(); resolve(null); }) .listen(port); }); } private static async waitUntilPortIsOpen(port: number) { while (true) { try { await this.checkPort(port); break; } catch (err) { await new Promise((resolve) => setTimeout(resolve, 1000)); } } } public static async ensureConnection(): Promise { if (!this.client) { if (!IronPdfGlobalConfig.getConfig().ironPdfEngineDockerAddress) { //local mode (non-docker) await this.startServer(); }else{ this.ironPdfEngineAddress = IronPdfGlobalConfig.getConfig().ironPdfEngineDockerAddress! } for (let i = 0; i < 5; i++) { try { this.client = new this.grpcObj.ironpdfengineproto.IronPdfService( this.ironPdfEngineAddress, grpc.credentials.createInsecure() ); break; } catch (e) { if(IronPdfGlobalConfig.getConfig().debugMode) console.error(`Attempt ${i+1} to connect to IronPdfEngine Retrying...`); await new Promise(r => setTimeout(r, 2000)); // wait for 2 seconds before next try } } const response = await handshakeWithRetry(this.client, 20).catch( async (reason) => { throw new Error( `Cannot connect to IronPdfEngine: ${reason}` ); } ); if (response) { if (response.exception) { throw new Error( `${response.exception.exceptionType} ${response.exception.message} \n ${response.exception.remoteStackTrace} \n ${response.exception.rootException}` ); } if (response.requiredVersion) { console.warn( `[IronPdf] mismatch version, required: ${IronPdfGlobalConfig.ironPdfEngineVersion} found: ${response.requiredVersion}` ); } //apply configuration after handshake await setIsDebug( this.client, IronPdfGlobalConfig.getConfig().debugMode ?? false ); const licenseKey = IronPdfGlobalConfig.getConfig().licenseKey || process.env['IRONPDF_LICENSE_KEY']; if (licenseKey) { await setLicenseKey(this.client, licenseKey); } if (IronPdfGlobalConfig.getConfig().debugMode) console.debug("Connected to IronPdfEngine"); } } return this.client; } } export function getOsName() { switch (process.platform) { case "win32": return `windows`; case "darwin": return `macos`; case "linux": return `linux`; default: throw new Error(`OS: ${process.platform} are not supported`); } } export function getPlatformName() { switch (process.platform) { case "win32": return `Windows`; case "darwin": return `MacOS`; case "linux": return `Linux`; default: throw new Error(`Platform: ${process.platform} are not supported`); } } export function ironPdfEngineExecutable() { switch (process.platform) { case "win32": return `IronPdfEngineConsole.exe`; case "darwin": return `IronPdfEngineConsole`; case "linux": return `IronPdfEngineConsole`; default: throw new Error(`OS: ${process.platform} are not supported`); } } export function getOsArch() { switch (Os.arch()) { case "ppc64": case "x64": case "s390x": return `x64`; case "arm": case "arm64": return `arm64`; default: return "x86"; } } async function tryChangePermissions(directoryPath: string, fileMode: string) { try { const syncReaddir = util.promisify(fs.readdir); const syncChmod = util.promisify(fs.chmod); const files = await syncReaddir(directoryPath); // Listing all files using forEach for (const file of files) { const filePath = path.join(directoryPath, file); if (IronPdfGlobalConfig.getConfig().debugMode) console.debug(`chmod ${filePath}`); await syncChmod(filePath, fileMode); } } catch (e) { console.debug(`tryChangePermissions of: ${directoryPath} Error: ${e}`); } }