@ironsoftware/ironpdf
Version:
IronPDF for Node
482 lines (427 loc) • 13.2 kB
text/typescript
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<string>();
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<void> {
return new Promise<void>((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<void> {
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`
];
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<IronPdfServiceClient> {
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;
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}`);
}
}