UNPKG

@itwin/core-backend

Version:
510 lines • 24.3 kB
/*--------------------------------------------------------------------------------------------- * 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