/**
|
* furnplan-instance-manager.js
|
*/
|
|
const Promise = require("bluebird");
|
|
const path = require("path");
|
const spawn = require("child_process").spawn;
|
const executable = Config.furnview.furnplan_dev.fpSocketServerExecutable;
|
const uuid = require("uuid");
|
|
|
const fs = Promise.promisifyAll(require("fs"));
|
const { sessionStore } = require("./session-store");
|
const PortManager = require("./port-manager");
|
const Messages = require("./messages");
|
const Dispatcher = require("./dispatcher");
|
const DhpFileManager = require("./services/DhpFileManager");
|
const converter = require("./converter");
|
|
const { decode } = require("furncloud-library").Services.FurncloudSecurity;
|
const { RB64 } = require("furncloud-library").Services;
|
|
const STATUS = {
|
RUNNING: 0,
|
CLOSE: 1,
|
CRASHED: 2
|
};
|
|
const { PathService } = require("./services/path.service");
|
const { FurnplanLocalService } = require("./services/furnplan-local.service");
|
const { ProcessArgumentsService } = require("./services/process-arguments.service");
|
const { SessionFolderService } = require("./services/session-folder.service");
|
const { url } = require("inspector");
|
const { mongodb } = require("./config");
|
const { any } = require("bluebird");
|
|
const preloadedInstances = [];
|
|
Config.furnview.preloadInstanceCount = Config.furnview.preloadInstanceCount || 0;
|
Config.furnview.manufacturersToLoad = Config.furnview.manufacturersToLoad || [];
|
Config.furnview.symmetricKey = Config.furnview.symmetricKey || "";
|
|
const { exec } = require("child_process");
|
|
// JM [2024|10|11]
|
async function checkMsBuildExeRunning() {
|
return new Promise((resolve, reject) => {
|
exec("tasklist /fi \"imagename eq MSBuild.exe\" | find \":\" > nul", (err) => resolve(!!err));
|
});
|
}
|
|
// JM [2024|10|11] wenn die FPSocketService.exe gekillt wird durch den debugger, warten bis keine cl.exe mehr in der tasklist ist.
|
async function waitForMsBuildExeToFinish() {
|
try {
|
let isRunning = true;
|
while (isRunning) {
|
isRunning = await checkMsBuildExeRunning();
|
if (isRunning) {
|
Winston.info("Compiler gefunden, warte...");
|
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 Sekunden warten
|
}
|
}
|
Winston.info("Kein Compiler gefunden..., neustart");
|
}
|
catch (error) {
|
Winston.warn("Error in Compiler Check!");
|
}
|
}
|
|
var FurnplanInstanceManager = {
|
|
freezeInstance: async function (sessionId) {
|
const sessionInstance = sessionStore.getSession(sessionId);
|
if (sessionInstance) {
|
console.log(`INSTANCE [${sessionInstance.instance.port}] - freeze`);
|
try {
|
await sessionInstance.saveSessionState();
|
}
|
catch (e) {
|
Winston.warn("Unable to save session state", e);
|
}
|
sessionInstance.frozen = true;
|
sessionInstance.freezeToken = uuid.v4();
|
sessionInstance.closeTimer = setTimeout(() => {
|
sessionStore.cleanUp(sessionId);
|
clearTimeout(sessionInstance.closeTimer);
|
sessionInstance.closeTimer = null;
|
}, 60000);
|
}
|
return sessionInstance.freezeToken;
|
},
|
|
startFrozenInstance: async function (parameters) {
|
const sessionInstance = sessionStore.getSession(parameters.sessionId);
|
if (sessionInstance && sessionInstance.frozen && sessionInstance.closeTimer) {
|
console.log(`INSTANCE [${sessionInstance.instance.port}] - unfreeze`);
|
if (sessionInstance.freezeToken === parameters.token) {
|
clearTimeout(sessionInstance.closeTimer);
|
sessionInstance.closeTimer = null;
|
sessionInstance.frozen = false;
|
return sessionInstance.instance;
|
}
|
else {
|
sessionStore.cleanUp(parameters.sessionId);
|
}
|
return null;
|
}
|
},
|
|
/**
|
* Get a running instance (if preloadInstanceCount > 0) or a newly started instance (preloadInstanceCount === 0)
|
*
|
* @param {string} sessionId
|
* @param {string} pathToOpusDealerInfo
|
* @param {{}} parameters
|
* @returns {{}}
|
*/
|
getInstance: async function (sessionId, pathToOpusDealerInfo, parameters) {
|
const customerNo = parameters.customerNo;
|
const userId = parameters.userId;
|
|
let instance;
|
let sessionInstance;
|
|
if (preloadedInstances.length > 0) {
|
|
// use preloaded instance
|
const preloadedInstance = preloadedInstances.pop();
|
|
await SessionFolderService.deleteFolder(preloadedInstance.sessionId);
|
|
sessionInstance = sessionStore.getSession(preloadedInstance.sessionId);
|
|
console.log(`INSTANCE [${preloadedInstance.port}] - assignd preloaded instance to SESSION [${sessionId}] `);
|
|
await sessionStore.updateSessionId(preloadedInstance.sessionId, sessionId, pathToOpusDealerInfo);
|
sessionInstance.startTimeout();
|
|
sessionInstance.com.serverMessage("ReinitializeFurnplan", { ADATA: [{ opusDealerInfoPath: pathToOpusDealerInfo, customerNo, sessionId, userId }] });
|
|
// asynchronously refill stack with a new instance
|
FurnplanInstanceManager.preloadInstance()
|
.then((preloadedInstance) => preloadedInstances.push(preloadedInstance));
|
|
instance = preloadedInstance;
|
}
|
else {
|
if (preloadedInstances.length < Config.furnview.preloadInstanceCount) {
|
// the stack with preloaded instances is exhausted, so refill
|
FurnplanInstanceManager.preloadInstance()
|
.then((preloadedInstance) => preloadedInstances.push(preloadedInstance));
|
}
|
|
instance = await this.startInstance(sessionId, pathToOpusDealerInfo, parameters);
|
|
sessionInstance = sessionStore.getSession(sessionId);
|
sessionInstance.startTimeout();
|
}
|
|
sessionInstance.com.serverMessage("GetScene", { ADATA: [] });
|
|
try {
|
const planningsFolder = path.resolve(PathService.getTempPath(), "data", sessionId, "plannings");
|
await fs.statAsync(path.resolve(planningsFolder, "initial-state.dhp"));
|
sessionInstance.com.serverMessage("LoadDHP", { ADATA: ["initial-state"] });
|
}
|
catch (e) {
|
|
}
|
|
return instance;
|
},
|
|
/**
|
* Preload an instance
|
*
|
* @returns {{}}
|
*/
|
preloadInstance: async function () {
|
const temporarySessionId = uuid.v4();
|
|
const temporarySessionFolder = await SessionFolderService.createFolder(temporarySessionId);
|
|
const instance = await FurnplanInstanceManager.startInstance(temporarySessionId, temporarySessionFolder + "\\", { customerNo: "0", userId: "0", userData: { tenant: "" }, queryParameters: { "buying-group": "" } });
|
|
console.log(`INSTANCE [${instance.port}] - is preloaded!`);
|
|
const finishedManufacturers = Config.furnview.manufacturersToLoad.map(({ manufacturer, program }) => {
|
const sessionInstance = sessionStore.getSession(temporarySessionId);
|
return sessionInstance.com.serverMessage("LoadManufacturerDll", { ADATA: [{ manufacturer, program }] });
|
});
|
|
await Promise.all(finishedManufacturers);
|
|
return instance;
|
},
|
|
/**
|
* Starts a new furnplan instance for the given session id
|
*
|
* @param {string} sessionId
|
* @param {string} pathToOpusDealerInfo
|
* @param {{}} parameters
|
*
|
* @returns {{}}
|
*/
|
startInstance: async function (sessionId, pathToOpusDealerInfo, parameters) {
|
const customerNo = parameters.customerNo;
|
const userId = parameters.userId;
|
const tenant = parameters.userData.tenant;
|
const buyingGroup = parameters.queryParameters["buying-group"];
|
const urlCloudId = parameters.queryParameters["cloudId"] || "";
|
|
const furnplanStartTimer = process.hrtime();
|
let timeMark = 0;
|
|
const port = PortManager.getFreePort();
|
|
console.log(`INSTANCE [${port}] - create for SESSION [${sessionId}] `);
|
|
if (!port) {
|
throw new Error("No free ports available!");
|
}
|
|
let queryString = "";
|
for (const [key, value] of Object.entries(parameters.queryParameters)) {
|
queryString += `${key}=${value}`;
|
queryString += "&";
|
}
|
queryString += "userId=" + userId;
|
|
// var pathNcr = converter.calstr(pathToOpusDealerInfo);
|
// var cliArguments = [port, pathNcr.ncrDec];
|
const cliArguments = ["/port", port, "/opusDealerInfoPath", pathToOpusDealerInfo, "/customerNo", customerNo, "/userId", userId, "/queryString", queryString];
|
|
if (buyingGroup) {
|
try {
|
const decodedBuyingGroup = decode(RB64.decode(buyingGroup), Config.furnview.symmetricKey);
|
|
cliArguments.push("/buyingGroup", decodedBuyingGroup);
|
}
|
catch (e) {
|
Winston.warn("Unable to decrypt buying group: ", e);
|
}
|
}
|
|
if (urlCloudId) {
|
cliArguments.push("/urlCloudId", urlCloudId);
|
}
|
|
const options = {
|
detached: false, // start furnplan instance attached to the node process, so it exits when node crashes
|
cwd: path.resolve(process.cwd(), Config.furnview.furnplan_dev.bin), // furnplan has to be run from the bin folder
|
env: process.env, // default environment variables
|
stdio: ["ignore"]
|
};
|
|
if (ProcessArgumentsService.isLocal()) {
|
// let projectPath = await FurnplanLocalService.getIndividualProjectPath(customerNo);
|
// var projectPathNcr = converter.calstr(projectPath);
|
// cliArguments.push(projectPathNcr.ncrDec);
|
if (typeof tenant !== "undefined" && tenant.length > 0) {
|
cliArguments.push("/projectPath", await FurnplanLocalService.getIndividualProjectPath(tenant));
|
}
|
else {
|
cliArguments.push("/projectPath", await FurnplanLocalService.getIndividualProjectPath(customerNo));
|
}
|
}
|
|
if (Config.connection.furnviewRegistryBaseUrl) {
|
cliArguments.push("/furnviewRegistryBaseUrl", Config.connection.furnviewRegistryBaseUrl);
|
}
|
|
// spawn process with executable on the given port with given options
|
const fpInstance = spawn(
|
path.resolve(process.cwd(), executable), // path to executable
|
cliArguments, // array with text values that will be passed as arguments / parameters to the executable (in the given order)
|
options // special options for process invocation
|
);
|
|
fpInstance.sessionId = sessionId;
|
fpInstance.port = port;
|
fpInstance.intent = STATUS.RUNNING;
|
fpInstance.cliArguments = cliArguments;
|
fpInstance.birth = Date.now();
|
fpInstance.exit = new Promise((resolve, reject) => {
|
fpInstance.exitResolverFunction = resolve;
|
});
|
|
Winston.info(`Furnplan instance successfully started on port ${fpInstance.port} with PID ${fpInstance.pid} for session id ${sessionId}`);
|
|
// free port if furnplan instance could not be created
|
fpInstance.on("error", function (error) {
|
Winston.warn(`Furnplan instance for port ${fpInstance.port} and session ${sessionId} had an error`, error);
|
});
|
|
// free port if furnplan instance exists
|
fpInstance.on("exit", async (code, signal) => {
|
Winston.info(`Furnplan instance on port ${fpInstance.port} with PID ${fpInstance.pid} for session id ${sessionId} exited with code ${code} via signal ${signal}`);
|
Winston.info("CLI arguments: ", fpInstance.cliArguments.join(" "));
|
|
const sessionInstance = sessionStore.getSession(sessionId);
|
|
// wait a certain security delay before actually freeing the port
|
await Promise.delay(1 * 1000);
|
|
// free port in port manager
|
PortManager.freePort(port);
|
|
Winston.info(`Freed port ${port} of session ${sessionId}.`);
|
|
// delete a possibly existing event handler for saving session state
|
Dispatcher.off("saved_" + sessionId);
|
|
fpInstance.exitResolverFunction();
|
|
if (!sessionInstance) {
|
return;
|
}
|
|
if (fpInstance.intent === STATUS.RUNNING) {
|
if (sessionInstance.restartCounter < 3) {
|
sessionInstance.restartCounter++;
|
|
try {
|
await DhpFileManager.restoreLastUndo(sessionId);
|
}
|
catch (e) {
|
Winston.warn(`Unable to restore last undo state for session id ${sessionId}`, e);
|
}
|
|
if (Config.furnview.furnplan_dev.waitForCompilerOnRestart) {
|
await waitForMsBuildExeToFinish();
|
sessionInstance.restartCounter--;
|
}
|
|
await FurnplanInstanceManager.startInstance(sessionId, pathToOpusDealerInfo, parameters);
|
|
Dispatcher.process(Messages.restartedInstance(sessionId));
|
}
|
else {
|
sessionInstance.instance.intent = STATUS.CLOSE;
|
|
sessionStore.cleanUp(sessionId);
|
}
|
}
|
});
|
|
// wait until furnplan writes a ready.txt
|
Winston.info("Waiting for furnplan to start socket server for session id " + sessionId);
|
|
let existsFile = false;
|
|
do {
|
try {
|
await Promise.delay(20);
|
await fs.accessAsync(path.resolve(pathToOpusDealerInfo, "ready.txt"), fs.constants.R_OK);
|
|
existsFile = true;
|
}
|
catch (e) {
|
existsFile = false;
|
}
|
} while (!existsFile);
|
|
timeMark = process.hrtime(furnplanStartTimer);
|
|
Winston.info(`Furnplan started at ${(timeMark[0] * 1000000000 + timeMark[1]) / 1000000} ms time mark (session id ${sessionId})`);
|
|
let sessionInstance = sessionStore.getSession(sessionId);
|
|
if (sessionInstance) {
|
sessionInstance.stopServerCom();
|
|
sessionInstance.setInstance(fpInstance, fpInstance.pid, port);
|
|
// sessionInstance.instance = fpInstance;
|
// sessionInstance.pid = fpInstance.pid;
|
// sessionInstance.port = port;
|
|
sessionInstance.startServerCom(port);
|
}
|
else {
|
sessionInstance = await sessionStore.createSession(sessionId, fpInstance, fpInstance.pid, port, pathToOpusDealerInfo);
|
sessionInstance.startServerCom(port);
|
}
|
|
timeMark = process.hrtime(furnplanStartTimer);
|
|
Winston.info(`Connection to furnplan established at ${(timeMark[0] * 1000000000 + timeMark[1]) / 1000000} ms time mark (session id ${sessionId})`);
|
|
const fplanInstanceHistory = await FurnplanInstanceHistory.findOne({ _sessionId: sessionId });
|
if (fplanInstanceHistory) {
|
fplanInstanceHistory.pid = fpInstance.pid;
|
fplanInstanceHistory.port = port;
|
fplanInstanceHistory.domain = sessionInstance.domain;
|
const SessionDuration = {
|
birth: new Date(),
|
death: new Date(),
|
duration: 0
|
};
|
fplanInstanceHistory.life.push(SessionDuration);
|
await fplanInstanceHistory.save();
|
}
|
|
return fpInstance;
|
},
|
|
/**
|
* Enumeration of intent
|
*/
|
intent: STATUS
|
|
};
|
|
Dispatcher.on("start-preload", async () => {
|
for (let i = 0; i < Config.furnview.preloadInstanceCount; i++) {
|
const instance = await FurnplanInstanceManager.preloadInstance();
|
preloadedInstances.push(instance);
|
}
|
});
|
|
module.exports = FurnplanInstanceManager;
|