@itwin/core-backend
Version:
iTwin.js backend components
510 lines • 24.3 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module IModelHost
*/
// To avoid circular load errors, the "Element" classes must be loaded before IModelHost.
import "./IModelDb"; // DO NOT REMOVE OR MOVE THIS LINE!
import { IModelNative, loadNativePlatform } from "./internal/NativePlatform";
import * as os from "node:os";
import { NativeLibrary } from "@bentley/imodeljs-native";
import { assert, BeEvent, BentleyStatus, DbResult, Guid, IModelStatus, Logger, ProcessDetector } from "@itwin/core-bentley";
import { IModelError } from "@itwin/core-common";
import { AzureServerStorage, BlobServiceClientWrapper } from "@itwin/object-storage-azure";
import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob";
import { BackendLoggerCategory } from "./BackendLoggerCategory";
import { BisCoreSchema } from "./BisCoreSchema";
import { BriefcaseManager } from "./BriefcaseManager";
import { CloudSqlite } from "./CloudSqlite";
import { FunctionalSchema } from "./domains/FunctionalSchema";
import { GenericSchema } from "./domains/GenericSchema";
import { GeoCoordConfig } from "./GeoCoordConfig";
import { IModelJsFs } from "./IModelJsFs";
import { DevToolsRpcImpl } from "./rpc-impl/DevToolsRpcImpl";
import { IModelReadRpcImpl } from "./rpc-impl/IModelReadRpcImpl";
import { IModelTileRpcImpl } from "./rpc-impl/IModelTileRpcImpl";
import { SnapshotIModelRpcImpl } from "./rpc-impl/SnapshotIModelRpcImpl";
import { initializeRpcBackend } from "./RpcBackend";
import { TileStorage } from "./TileStorage";
import { SettingsPriority } from "./workspace/Settings";
import { join, normalize as normalizeDir } from "path";
import { constructWorkspace } from "./internal/workspace/WorkspaceImpl";
import { SettingsImpl } from "./internal/workspace/SettingsImpl";
import { constructSettingsSchemas } from "./internal/workspace/SettingsSchemasImpl";
import { _getHubAccess, _hubAccess, _setHubAccess } from "./internal/Symbols";
const loggerCategory = BackendLoggerCategory.IModelHost;
/** Configuration of core-backend.
* @public
*/
export class IModelHostConfiguration {
static defaultTileRequestTimeout = 20 * 1000;
static defaultLogTileLoadTimeThreshold = 40;
static defaultLogTileSizeThreshold = 20 * 1000000;
/** @internal */
static defaultMaxTileCacheDbSize = 1024 * 1024 * 1024;
appAssetsDir;
cacheDir;
/** @beta */
workspace;
hubAccess;
/** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */
authorizationClient;
/** @beta */
restrictTileUrlsByClientIp;
compressCachedTiles;
/** @beta */
tileCacheAzureCredentials;
/** @internal */
tileTreeRequestTimeout = IModelHostConfiguration.defaultTileRequestTimeout;
/** @internal */
tileContentRequestTimeout = IModelHostConfiguration.defaultTileRequestTimeout;
/** @internal */
logTileLoadTimeThreshold = IModelHostConfiguration.defaultLogTileLoadTimeThreshold;
/** @internal */
logTileSizeThreshold = IModelHostConfiguration.defaultLogTileSizeThreshold;
/** @internal */
crashReportingConfig;
/**
* Configuration controlling whether to use the thinned down native instance functions for element, model, and aspect CRUD operations
* or use the previous behavior of using the native side for all CRUD operations. Set to true to revert to the previous behavior.
* @beta
*/
disableThinnedNativeInstanceWorkflow;
/**
* Configuration controlling whether to disable the creation of restore points during pull/merge operations.
* @beta
*/
disableRestorePointOnPullMerge;
/**
* Configuration controlling whether incremental schema loading is disabled.
* Default is "disabled" at the moment to preserve existing behavior.
* @beta
*/
incrementalSchemaLoading = "disabled";
}
/**
* Settings for `IModelHost.appWorkspace`.
* @note this includes the default dictionary from the SettingsSpecRegistry
*/
class ApplicationSettings extends SettingsImpl {
_remove;
verifyPriority(priority) {
if (priority > SettingsPriority.application) // only application or lower may appear in ApplicationSettings
throw new Error("Use IModelSettings");
}
updateDefaults() {
const defaults = {};
for (const [schemaName, val] of IModelHost.settingsSchemas.settingDefs) {
if (val.default)
defaults[schemaName] = val.default;
}
this.addDictionary({ name: "_default_", priority: 0 }, defaults);
}
constructor() {
super();
this._remove = IModelHost.settingsSchemas.onSchemaChanged.addListener(() => this.updateDefaults());
this.updateDefaults();
}
close() {
if (this._remove) {
this._remove();
this._remove = undefined;
}
}
}
const definedInStartup = (obj) => {
if (obj === undefined)
throw new Error("IModelHost.startup must be called first");
return obj;
};
/** IModelHost initializes ($backend) and captures its configuration. A backend must call [[IModelHost.startup]] before using any backend classes.
* See [the learning article]($docs/learning/backend/IModelHost.md)
* @public
*/
export class IModelHost {
constructor() { }
/** The AuthorizationClient used to obtain [AccessToken]($bentley)s. */
static authorizationClient;
static backendVersion = "";
static _profileName;
static _cacheDir = "";
static _settingsSchemas;
static _appWorkspace;
// Omit the hubAccess field from configuration so it stays internal.
static configuration;
/**
* The name of the *Profile* directory (a subdirectory of "[[cacheDir]]/profiles/") for this process.
*
* The *Profile* directory is used to cache data that is specific to a type-of-usage of the iTwin.js library.
* It is important that information in the profile cache be consistent but isolated across sessions (i.e.
* data for a profile is maintained between runs, but each profile is completely independent and
* unaffected by the presence or use of others.)
* @note **Only one process at a time may be using a given profile**, and an exception will be thrown by [[startup]]
* if a second process attempts to use the same profile.
* @beta
*/
static get profileName() {
return this._profileName;
}
/** The full path of the Profile directory.
* @see [[profileName]]
* @beta
*/
static get profileDir() {
return join(this._cacheDir, "profiles", this._profileName);
}
/** Event raised during startup to allow loading settings data */
static onWorkspaceStartup = new BeEvent();
/** Event raised just after the backend IModelHost was started */
static onAfterStartup = new BeEvent();
/** Event raised just before the backend IModelHost is to be shut down */
static onBeforeShutdown = new BeEvent();
/** @internal */
static session = { applicationId: "2686", applicationVersion: "1.0.0", sessionId: "" };
/** A uniqueId for this session */
static get sessionId() { return this.session.sessionId; }
static set sessionId(id) { this.session.sessionId = id; }
/** The Id of this application - needs to be set only if it is an agent application. The applicationId will otherwise originate at the frontend. */
static get applicationId() { return this.session.applicationId; }
static set applicationId(id) { this.session.applicationId = id; }
/** The version of this application - needs to be set if is an agent application. The applicationVersion will otherwise originate at the frontend. */
static get applicationVersion() { return this.session.applicationVersion; }
static set applicationVersion(version) { this.session.applicationVersion = version; }
/** A string that can identify the current user to other users when collaborating. */
static userMoniker = "unknown";
/** Root directory holding files that iTwin.js caches */
static get cacheDir() { return this._cacheDir; }
/** The application [[Workspace]] for this `IModelHost`
* @note this `Workspace` only holds [[WorkspaceContainer]]s and [[Settings]] scoped to the currently loaded application(s).
* All organization, iTwin, and iModel based containers or settings must be accessed through [[IModelDb.workspace]] and
* attempting to add them to this Workspace will fail.
* @beta
*/
static get appWorkspace() { return definedInStartup(this._appWorkspace); }
/** The registry of schemas describing the [[Setting]]s for the application session.
* Applications should register their schemas via methods like [[SettingsSchemas.addGroup]].
* @beta
*/
static get settingsSchemas() { return definedInStartup(this._settingsSchemas); }
/** The optional [[FileNameResolver]] that resolves keys and partial file names for snapshot iModels.
* @deprecated in 4.10 - will not be removed until after 2026-06-13. When opening a snapshot by file name, ensure to pass already resolved path. Using a key to open a snapshot is now deprecated.
*/
static snapshotFileNameResolver; // eslint-disable-line @typescript-eslint/no-deprecated
/** Get the current access token for this IModelHost, or a blank string if none is available.
* @note for web backends, this will *always* return a blank string because the backend itself has no token (but never needs one either.)
* For all IpcHosts, where this backend is servicing a single frontend, this will be the user's token. For ElectronHost, the backend
* obtains the token and forwards it to the frontend.
* @note accessTokens expire periodically and are automatically refreshed, if possible. Therefore tokens should not be saved, and the value
* returned by this method may change over time throughout the course of a session.
*/
static async getAccessToken() {
try {
return (await IModelHost.authorizationClient?.getAccessToken()) ?? "";
}
catch {
return "";
}
}
static loadNative(options) {
loadNativePlatform();
if (options.crashReportingConfig && options.crashReportingConfig.crashDir && !ProcessDetector.isElectronAppBackend && !ProcessDetector.isMobileAppBackend) {
IModelNative.platform.setCrashReporting(options.crashReportingConfig);
Logger.logTrace(loggerCategory, "Configured crash reporting", {
enableCrashDumps: options.crashReportingConfig?.enableCrashDumps,
wantFullMemoryDumps: options.crashReportingConfig?.wantFullMemoryDumps,
enableNodeReport: options.crashReportingConfig?.enableNodeReport,
uploadToBentley: options.crashReportingConfig?.uploadToBentley,
});
if (options.crashReportingConfig.enableNodeReport) {
if (process.report !== undefined) {
process.report.reportOnFatalError = true;
process.report.reportOnUncaughtException = true;
process.report.directory = options.crashReportingConfig.crashDir;
Logger.logTrace(loggerCategory, "Configured Node.js crash reporting");
}
else {
Logger.logWarning(loggerCategory, "Unable to configure Node.js crash reporting");
}
}
}
}
/** @internal */
static tileStorage;
static _hubAccess;
/** @internal */
static [_setHubAccess](hubAccess) { this._hubAccess = hubAccess; }
/** get the current hubAccess, if present.
* @internal
*/
static [_getHubAccess]() { return this._hubAccess; }
/** Provides access to the IModelHub for this IModelHost
* @internal
* @note If [[IModelHostOptions.hubAccess]] was undefined when initializing this class, accessing this property will throw an error.
* To determine whether one is present, use [[_getHubAccess]].
*/
static get [_hubAccess]() {
if (IModelHost._hubAccess === undefined)
throw new IModelError(IModelStatus.BadRequest, "No BackendHubAccess supplied in IModelHostOptions");
return IModelHost._hubAccess;
}
static initializeWorkspace(configuration) {
const settingAssets = join(KnownLocations.packageAssetsDir, "Settings");
this._settingsSchemas = constructSettingsSchemas();
this._settingsSchemas.addDirectory(join(settingAssets, "Schemas"));
this._appWorkspace = constructWorkspace(new ApplicationSettings(), configuration.workspace);
// Create the CloudCache for Workspaces. This will fail if another process is already using the same profile.
try {
this.appWorkspace.getCloudCache();
}
catch (e) {
throw (e.errorNumber === DbResult.BE_SQLITE_BUSY) ? new IModelError(DbResult.BE_SQLITE_BUSY, `Profile [${this.profileDir}] is already in use by another process`) : e;
}
this.appWorkspace.settings.addDirectory(settingAssets, SettingsPriority.defaults);
GeoCoordConfig.onStartup();
// allow applications to load their default settings
this.onWorkspaceStartup.raiseEvent();
}
static _isValid = false;
/** true between a successful call to [[startup]] and before [[shutdown]] */
static get isValid() {
return IModelHost._isValid;
}
/** This method must be called before any iTwin.js services are used.
* @param options Host configuration data.
* Raises [[onAfterStartup]].
* @see [[shutdown]].
*/
static async startup(options) {
if (this._isValid)
return; // we're already initialized
this._isValid = true;
options = options ?? {};
if (this.sessionId === "")
this.sessionId = Guid.createValue();
this.authorizationClient = options.authorizationClient;
this.backendVersion = require("../../package.json").version; // eslint-disable-line @typescript-eslint/no-require-imports
initializeRpcBackend(options.enableOpenTelemetry);
this.loadNative(options);
this.setupCacheDir(options);
this.initializeWorkspace(options);
BriefcaseManager.initialize(join(this._cacheDir, "imodels"));
[
IModelReadRpcImpl,
IModelTileRpcImpl,
SnapshotIModelRpcImpl, // eslint-disable-line @typescript-eslint/no-deprecated
DevToolsRpcImpl,
].forEach((rpc) => rpc.register()); // register all of the RPC implementations
[
BisCoreSchema,
GenericSchema,
FunctionalSchema,
].forEach((schema) => schema.registerSchema()); // register all of the schemas
const { hubAccess, ...otherOptions } = options;
if (undefined !== hubAccess)
this._hubAccess = hubAccess;
this.configuration = otherOptions;
this.setupTileCache();
process.once("beforeExit", IModelHost.shutdown);
this.onAfterStartup.raiseEvent();
}
static setupCacheDir(configuration) {
this._cacheDir = normalizeDir(configuration.cacheDir ?? NativeLibrary.defaultCacheDir);
IModelJsFs.recursiveMkDirSync(this._cacheDir);
this._profileName = configuration.profileName ?? "default";
Logger.logInfo(loggerCategory, `cacheDir: [${this.cacheDir}], profileDir: [${this.profileDir}]`);
}
/** This method must be called when an iTwin.js host is shut down. Raises [[onBeforeShutdown]] */
static async shutdown() {
// Note: This method is set as a node listener where `this` is unbound. Call private method to
// ensure `this` is correct. Don't combine these methods.
return IModelHost.doShutdown();
}
/**
* Create a new iModel.
* @returns the Guid of the newly created iModel.
* @throws [IModelError]($common) in case of errors.
* @note If [[IModelHostOptions.hubAccess]] was undefined in the call to [[startup]], this function will throw an error.
*/
static async createNewIModel(arg) {
return this[_hubAccess].createNewIModel(arg);
}
static async doShutdown() {
if (!this._isValid)
return;
this._isValid = false;
this.onBeforeShutdown.raiseEvent();
this.configuration = undefined;
this.tileStorage = undefined;
this._appWorkspace?.close();
this._appWorkspace = undefined;
this._settingsSchemas = undefined;
CloudSqlite.CloudCaches.destroy();
process.removeListener("beforeExit", IModelHost.shutdown);
}
/**
* Add or update a property that should be included in a crash report.
* @internal
*/
static setCrashReportProperty(name, value) {
IModelNative.platform.setCrashReportProperty(name, value);
}
/**
* Remove a previously defined property so that will not be included in a crash report.
* @internal
*/
static removeCrashReportProperty(name) {
IModelNative.platform.setCrashReportProperty(name, undefined);
}
/**
* Get all properties that will be included in a crash report.
* @internal
*/
static getCrashReportProperties() {
return IModelNative.platform.getCrashReportProperties();
}
/** The directory where application assets may be found */
static get appAssetsDir() {
return undefined !== IModelHost.configuration ? IModelHost.configuration.appAssetsDir : undefined;
}
/** The time, in milliseconds, for which IModelTileRpcInterface.requestTileTreeProps should wait before returning a "pending" status.
* @internal
*/
static get tileTreeRequestTimeout() {
return IModelHost.configuration?.tileTreeRequestTimeout ?? IModelHostConfiguration.defaultTileRequestTimeout;
}
/** The time, in milliseconds, for which IModelTileRpcInterface.requestTileContent should wait before returning a "pending" status.
* @internal
*/
static get tileContentRequestTimeout() {
return IModelHost.configuration?.tileContentRequestTimeout ?? IModelHostConfiguration.defaultTileRequestTimeout;
}
/** The backend will log when a tile took longer to load than this threshold in seconds. */
static get logTileLoadTimeThreshold() {
return IModelHost.configuration?.logTileLoadTimeThreshold ?? IModelHostConfiguration.defaultLogTileLoadTimeThreshold;
}
/** The backend will log when a tile is loaded with a size in bytes above this threshold. */
static get logTileSizeThreshold() {
return IModelHost.configuration?.logTileSizeThreshold ?? IModelHostConfiguration.defaultLogTileSizeThreshold;
}
/** Whether external tile caching is active.
* @internal
*/
static get usingExternalTileCache() {
return undefined !== IModelHost.tileStorage;
}
/** Whether to restrict tile cache URLs by client IP address.
* @internal
*/
static get restrictTileUrlsByClientIp() {
return undefined !== IModelHost.configuration && (IModelHost.configuration.restrictTileUrlsByClientIp ? true : false);
}
/** Whether to compress cached tiles.
* @internal
*/
static get compressCachedTiles() {
return false !== IModelHost.configuration?.compressCachedTiles;
}
static setupTileCache() {
assert(undefined !== IModelHost.configuration);
const config = IModelHost.configuration;
const storage = config.tileCacheStorage;
const credentials = config.tileCacheAzureCredentials;
if (!storage && !credentials) {
IModelNative.platform.setMaxTileCacheSize(config.maxTileCacheDbSize ?? IModelHostConfiguration.defaultMaxTileCacheDbSize);
return;
}
IModelNative.platform.setMaxTileCacheSize(0);
if (credentials) {
if (storage)
throw new IModelError(BentleyStatus.ERROR, "Cannot use both Azure and custom cloud storage providers for tile cache.");
this.setupAzureTileCache(credentials);
}
if (storage)
IModelHost.tileStorage = new TileStorage(storage);
}
static setupAzureTileCache(credentials) {
const storageConfig = {
accountName: credentials.account,
accountKey: credentials.accessKey,
baseUrl: credentials.baseUrl ?? `https://${credentials.account}.blob.core.windows.net`,
};
const blobServiceClient = new BlobServiceClient(storageConfig.baseUrl, new StorageSharedKeyCredential(storageConfig.accountName, storageConfig.accountKey));
const azureStorage = new AzureServerStorage(storageConfig, new BlobServiceClientWrapper(blobServiceClient));
IModelHost.tileStorage = new TileStorage(azureStorage);
}
/** @internal */
static computeSchemaChecksum(arg) {
return IModelNative.platform.computeSchemaChecksum(arg);
}
}
/** Information about the platform on which the app is running.
* @public
*/
export class Platform {
/** Get the name of the platform. */
static get platformName() {
return process.platform;
}
}
/** Well known directories that may be used by the application.
* @public
*/
export class KnownLocations {
/** The directory where the imodeljs-native assets are stored. */
static get nativeAssetsDir() {
return IModelNative.platform.DgnDb.getAssetsDir();
}
/** The directory where the core-backend assets are stored. */
static get packageAssetsDir() {
return join(__dirname, "assets");
}
/** The temporary directory. */
static get tmpdir() {
return os.tmpdir();
}
}
/** Extend this class to provide custom file name resolution behavior.
* @note Only `tryResolveKey` and/or `tryResolveFileName` need to be overridden as the implementations of `resolveKey` and `resolveFileName` work for most purposes.
* @see [[IModelHost.snapshotFileNameResolver]]
* @public
* @deprecated in 4.10 - will not be removed until after 2026-06-13. When opening a snapshot by file name, ensure to pass already resolved path. Using a key to open a snapshot is now deprecated.
*/
export class FileNameResolver {
/** Resolve a file name from the specified key.
* @param _fileKey The key that identifies the file name in a `Map` or other similar data structure.
* @returns The resolved file name or `undefined` if not found.
*/
tryResolveKey(_fileKey) { return undefined; }
/** Resolve a file name from the specified key.
* @param fileKey The key that identifies the file name in a `Map` or other similar data structure.
* @returns The resolved file name.
* @throws [[IModelError]] if not found.
*/
resolveKey(fileKey) {
const resolvedFileName = this.tryResolveKey(fileKey);
if (undefined === resolvedFileName) {
throw new IModelError(IModelStatus.NotFound, `${fileKey} not resolved`);
}
return resolvedFileName;
}
/** Resolve the input file name, which may be a partial name, into a full path file name.
* @param inFileName The partial file name.
* @returns The resolved full path file name or `undefined` if not found.
*/
tryResolveFileName(inFileName) { return inFileName; }
/** Resolve the input file name, which may be a partial name, into a full path file name.
* @param inFileName The partial file name.
* @returns The resolved full path file name.
* @throws [[IModelError]] if not found.
*/
resolveFileName(inFileName) {
const resolvedFileName = this.tryResolveFileName(inFileName);
if (undefined === resolvedFileName) {
throw new IModelError(IModelStatus.NotFound, `${inFileName} not resolved`);
}
return resolvedFileName;
}
}
//# sourceMappingURL=IModelHost.js.map