Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Special thanks to: @rejas, @sdetweil, @MagMar94
- Reworked how weatherproviders handle units (#2849)
- Use unix() method for parsing times, fix suntimes on the way (#2950)
- Refactor conversion functions into utils class (#2958)
- Use async/await for startup of the application instead of callbacks
- The `cors`-method in `server.js` now supports sending and recieving HTTP headers.

### Fixed
Expand Down
191 changes: 99 additions & 92 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// Alias modules mentioned in package.js under _moduleAliases.
require("module-alias/register");

const fs = require("fs");
const fs = require("fs/promises");
const path = require("path");
const Log = require("logger");
const Server = require(`${__dirname}/server`);
Expand Down Expand Up @@ -54,9 +54,9 @@ function App() {
* Loads the config file. Combines it with the defaults, and runs the
* callback with the found config as argument.
*
* @param {Function} callback Function to be called after loading the config
* @returns {Promise} A promise with the config that should be used
*/
function loadConfig(callback) {
async function loadConfig() {
Log.log("Loading config ...");
const defaults = require(`${__dirname}/defaults`);

Expand All @@ -65,11 +65,11 @@ function App() {
const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`);

try {
fs.accessSync(configFilename, fs.F_OK);
await fs.access(configFilename, fs.F_OK);
const c = require(configFilename);
checkDeprecatedOptions(c);
const config = Object.assign(defaults, c);
callback(config);
return config;
} catch (e) {
if (e.code === "ENOENT") {
Log.error(Utils.colors.error("WARNING! Could not find config file. Please create one. Starting with default configuration."));
Expand All @@ -78,7 +78,7 @@ function App() {
} else {
Log.error(Utils.colors.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`));
}
callback(defaults);
return defaults;
}
}

Expand All @@ -102,9 +102,9 @@ function App() {
* Loads a specific module.
*
* @param {string} module The name of the module (including subpath).
* @param {Function} callback Function to be called after loading
* @returns {Promise} A promise that resolves as soon as the module is loaded.
*/
function loadModule(module, callback) {
async function loadModule(module) {
const elements = module.split("/");
const moduleName = elements[elements.length - 1];
let moduleFolder = `${__dirname}/../modules/${module}`;
Expand All @@ -113,17 +113,9 @@ function App() {
moduleFolder = `${__dirname}/../modules/default/${module}`;
}

const helperPath = `${moduleFolder}/node_helper.js`;

let loadHelper = true;
try {
fs.accessSync(helperPath, fs.R_OK);
} catch (e) {
loadHelper = false;
Log.log(`No helper found for module: ${moduleName}.`);
}
const helperPath = await resolveHelperPath(moduleFolder);

if (loadHelper) {
if (helperPath) {
const Module = require(helperPath);
let m = new Module();

Expand All @@ -141,39 +133,29 @@ function App() {
m.setPath(path.resolve(moduleFolder));
nodeHelpers.push(m);

m.loaded(callback);
return new Promise((resolve, reject) => {
m.loaded(resolve);
});
} else {
callback();
Log.log(`No helper found for module: ${moduleName}.`);
return Promise.resolve();
}
}

/**
* Loads all modules.
*
* @param {Module[]} modules All modules to be loaded
* @param {Function} callback Function to be called after loading
* @returns {Promise} A promise that is resolved when all modules been loaded
*/
function loadModules(modules, callback) {
async function loadModules(modules) {
Log.log("Loading module helpers ...");

/**
*
*/
function loadNextModule() {
if (modules.length > 0) {
const nextModule = modules[0];
loadModule(nextModule, function () {
modules = modules.slice(1);
loadNextModule();
});
} else {
// All modules are loaded
Log.log("All module helpers loaded.");
callback();
}
for (let module of modules) {
await loadModule(module);
}

loadNextModule();
Log.log("All module helpers loaded.");
}

/**
Expand All @@ -200,61 +182,74 @@ function App() {
return segmentsA.length - segmentsB.length;
}

/**
* Resolves the path to the node_helper
*
* @param {string} moduleFolder the folder that should contain the node_helper
* @returns {Promise} A promise with the path to the node_helper that should be used, or undefined if none exists
*/
async function resolveHelperPath(moduleFolder) {
const helperPath = `${moduleFolder}/node_helper.js`;

try {
await fs.access(helperPath, fs.R_OK);
return helperPath;
} catch (e) {
// The current extension may not have been found, try the next instead
return undefined;
}
}

/**
* Start the core app.
*
* It loads the config, then it loads all modules. When it's done it
* executes the callback with the config as argument.
*
* @param {Function} callback Function to be called after start
* @returns {Promise} A promise containing the config, it is resolved when the server has loaded all modules and are listening for requests
*/
this.start = function (callback) {
loadConfig(function (c) {
config = c;
this.start = async function () {
config = await loadConfig();

Log.setLogLevel(config.logLevel);
Log.setLogLevel(config.logLevel);

let modules = [];
let modules = [];

for (const module of config.modules) {
if (!modules.includes(module.module) && !module.disabled) {
modules.push(module.module);
}
for (const module of config.modules) {
if (!modules.includes(module.module) && !module.disabled) {
modules.push(module.module);
}
}

loadModules(modules, async function () {
httpServer = new Server(config);
const { app, io } = await httpServer.open();
Log.log("Server started ...");

const nodePromises = [];
for (let nodeHelper of nodeHelpers) {
nodeHelper.setExpressApp(app);
nodeHelper.setSocketIO(io);

try {
nodePromises.push(nodeHelper.start());
} catch (error) {
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
Log.error(error);
}
}
await loadModules(modules);

Promise.allSettled(nodePromises).then((results) => {
// Log errors that happened during async node_helper startup
results.forEach((result) => {
if (result.status === "rejected") {
Log.error(result.reason);
}
});

Log.log("Sockets connected & modules started ...");
if (typeof callback === "function") {
callback(config);
}
});
});
httpServer = new Server(config);
const { app, io } = await httpServer.open();
Log.log("Server started ...");

const nodePromises = [];
for (let nodeHelper of nodeHelpers) {
nodeHelper.setExpressApp(app);
nodeHelper.setSocketIO(io);

try {
nodePromises.push(nodeHelper.start());
} catch (error) {
Log.error(`Error when starting node_helper for module ${nodeHelper.name}:`);
Log.error(error);
}
}

let results = await Promise.allSettled(nodePromises);
// Log errors that happened during async node_helper startup
results.forEach((result) => {
if (result.status === "rejected") {
Log.error(result.reason);
}
});

Log.log("Sockets connected & modules started ...");
return config;
};

/**
Expand All @@ -263,15 +258,33 @@ function App() {
*
* Added to fix #1056
*
* @param {Function} callback Function to be called after the app has stopped
* @param {number} timeout the amount of milliseconds before the returned promise should be automatically resolved
* @returns {Promise} A promise that is resolved when all node_helpers and the http server has been closed
*/
this.stop = function (callback) {
this.stop = async function (timeout) {
for (const nodeHelper of nodeHelpers) {
if (typeof nodeHelper.stop === "function") {
nodeHelper.stop();
}
}
httpServer.close().then(callback);

// To be able to stop the app even if it hasn't been started (when running with Electron against another server)
if (!httpServer) {
return Promise.resolve();
}

let serverClosePromise = httpServer.close();

// If a timeout is set, resolve when the server is closed or the timeout has been reached
if (timeout) {
let timeoutPromise = new Promise((resolve) => {
setTimeout(resolve, timeout);
});

return Promise.race([serverClosePromise, timeoutPromise]);
} else {
return serverClosePromise;
}
};

/**
Expand All @@ -281,25 +294,19 @@ function App() {
* Note: this is only used if running `server-only`. Otherwise
* this.stop() is called by app.on("before-quit"... in `electron.js`
*/
process.on("SIGINT", () => {
process.on("SIGINT", async () => {
Log.log("[SIGINT] Received. Shutting down server...");
setTimeout(() => {
process.exit(0);
}, 3000); // Force quit after 3 seconds
this.stop();
await this.stop(3000); // Force quit after 3 seconds
process.exit(0);
});

/**
* Listen to SIGTERM signals so we can stop everything when we
* are asked to stop by the OS.
*/
process.on("SIGTERM", () => {
process.on("SIGTERM", async () => {
Log.log("[SIGTERM] Received. Shutting down server...");
setTimeout(() => {
process.exit(0);
}, 3000); // Force quit after 3 seconds
this.stop();
await this.stop(3000); // Force quit after 3 seconds
process.exit(0);
});
}
Expand Down
33 changes: 18 additions & 15 deletions js/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const core = require("./app.js");
const Log = require("logger");

// Config
let config = process.env.config ? JSON.parse(process.env.config) : {};
let config;
// Module to control application life.
const app = electron.app;
// If ELECTRON_DISABLE_GPU is set electron is started with --disable-gpu flag.
Expand All @@ -21,6 +21,19 @@ const BrowserWindow = electron.BrowserWindow;
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

/**
* Start the core application if server is run on localhost
* This starts all node helpers and starts the webserver.
*
* @returns {Promise} A promise that is resolved when the server has started
*/
async function startAppIfNeeded() {
let localConfig = process.env.config ? JSON.parse(process.env.config) : {};
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(localConfig.address)) {
config = await core.start();
}
}

/**
*
*/
Expand Down Expand Up @@ -107,7 +120,8 @@ function createWindow() {

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on("ready", function () {
app.on("ready", async function () {
await startAppIfNeeded();
Log.log("Launching application.");
createWindow();
});
Expand Down Expand Up @@ -136,13 +150,10 @@ app.on("activate", function () {
* Note: this is only used if running Electron. Otherwise
* core.stop() is called by process.on("SIGINT"... in `app.js`
*/
app.on("before-quit", (event) => {
app.on("before-quit", async (event) => {
Log.log("Shutting down server...");
event.preventDefault();
setTimeout(() => {
process.exit(0);
}, 3000); // Force-quit after 3 seconds.
core.stop();
await core.stop(3000); // Force-quit after 3 seconds.
process.exit(0);
});

Expand All @@ -152,11 +163,3 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba
event.preventDefault();
callback(true);
});

// Start the core application if server is run on localhost
// This starts all node helpers and starts the webserver.
if (["localhost", "127.0.0.1", "::1", "::ffff:127.0.0.1", undefined].includes(config.address)) {
core.start(function (c) {
config = c;
});
}
2 changes: 1 addition & 1 deletion serveronly/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const app = require("../js/app.js");
const Log = require("logger");

app.start((config) => {
app.start().then((config) => {
const bindAddress = config.address ? config.address : "localhost";
const httpType = config.useHttps ? "https" : "http";
Log.log("\nReady to go! Please point your browser to: " + httpType + "://" + bindAddress + ":" + config.port);
Expand Down
Loading