diff --git a/.changeset/light-terms-ring.md b/.changeset/light-terms-ring.md
new file mode 100644
index 000000000000..4437c5c4d596
--- /dev/null
+++ b/.changeset/light-terms-ring.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixes the issue where newly created teams are incorrectly displayed as channels on the sidebar when the DISABLE_DB_WATCHERS environment variable is enabled
diff --git a/.changeset/old-coins-bow.md b/.changeset/old-coins-bow.md
new file mode 100644
index 000000000000..1790cc205160
--- /dev/null
+++ b/.changeset/old-coins-bow.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/apps-engine': patch
+---
+
+Fixes an issue that would cause apps to appear disabled after a subprocess restart
diff --git a/.changeset/serious-mice-film.md b/.changeset/serious-mice-film.md
new file mode 100644
index 000000000000..35a2d6704071
--- /dev/null
+++ b/.changeset/serious-mice-film.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fixes client-side updates for recent emoji list when custom emojis are modified.
diff --git a/.changeset/stale-actors-enjoy.md b/.changeset/stale-actors-enjoy.md
new file mode 100644
index 000000000000..baff2b19b667
--- /dev/null
+++ b/.changeset/stale-actors-enjoy.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Fixes `waiting queue` feature. When `Livechat_waiting_queue` setting is enabled, incoming conversations should be sent to the queue instead of being assigned directly.
diff --git a/.changeset/unlucky-kangaroos-yawn.md b/.changeset/unlucky-kangaroos-yawn.md
new file mode 100644
index 000000000000..1aaa97cbd8d8
--- /dev/null
+++ b/.changeset/unlucky-kangaroos-yawn.md
@@ -0,0 +1,6 @@
+---
+"@rocket.chat/meteor": patch
+"@rocket.chat/i18n": patch
+---
+
+Updates VoIP field labels from 'Free Extension Numbers' to 'Available Extensions' to better describe the field's purpose and improve clarity.
diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts
index ee186ebd4e15..3f4b876f12a7 100644
--- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts
+++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts
@@ -3,7 +3,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
-import { emoji, updateRecent } from '../../../emoji/client';
+import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../../emoji/client';
import { CachedCollectionManager } from '../../../ui-cached-collection/client';
import { getURL } from '../../../utils/client';
import { sdk } from '../../../utils/client/lib/SDKClient';
@@ -49,7 +49,8 @@ export const deleteEmojiCustom = (emojiData: IEmoji) => {
}
}
}
- updateRecent(['rocket']);
+
+ removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent);
};
export const updateEmojiCustom = (emojiData: IEmoji) => {
@@ -94,7 +95,9 @@ export const updateEmojiCustom = (emojiData: IEmoji) => {
}
}
- updateRecent(['rocket']);
+ if (previousExists) {
+ replaceEmojiInRecent({ oldEmoji: emojiData.previousName, newEmoji: emojiData.name });
+ }
};
const customRender = (html: string) => {
diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts
index 35badda26a73..a203216640f5 100644
--- a/apps/meteor/app/emoji/client/helpers.ts
+++ b/apps/meteor/app/emoji/client/helpers.ts
@@ -138,7 +138,7 @@ export const getEmojisBySearchTerm = (
return emojis;
};
-export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void) => {
+export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecentEmojis?: (emojis: string[]) => void) => {
const _emoji = emoji.replace(/(^:|:$)/g, '');
const pos = recentEmojis.indexOf(_emoji as never);
@@ -146,7 +146,7 @@ export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecen
return;
}
recentEmojis.splice(pos, 1);
- setRecentEmojis(recentEmojis);
+ setRecentEmojis?.(recentEmojis);
};
export const updateRecent = (recentList: string[]) => {
@@ -156,6 +156,15 @@ export const updateRecent = (recentList: string[]) => {
});
};
+export const replaceEmojiInRecent = ({ oldEmoji, newEmoji }: { oldEmoji: string; newEmoji: string }) => {
+ const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent;
+ const pos = recentPkgList.indexOf(oldEmoji);
+
+ if (pos !== -1) {
+ recentPkgList[pos] = newEmoji;
+ }
+};
+
const getEmojiRender = (emojiName: string) => {
const emojiPackageName = emoji.list[emojiName]?.emojiPackage;
const emojiPackage = emoji.packages[emojiPackageName];
diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts
index 24be8d42b7a4..a467b6e0b360 100644
--- a/apps/meteor/app/livechat/server/lib/QueueManager.ts
+++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts
@@ -94,8 +94,7 @@ export class QueueManager {
const inquiryAgent = await RoutingManager.delegateAgent(defaultAgent, inquiry);
logger.debug(`Delegating inquiry with id ${inquiry._id} to agent ${defaultAgent?.username}`);
- await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
- const dbInquiry = await LivechatInquiry.findOneById(inquiry._id);
+ const dbInquiry = await callbacks.run('livechat.beforeRouteChat', inquiry, inquiryAgent);
if (!dbInquiry) {
throw new Error('inquiry-not-found');
@@ -122,6 +121,10 @@ export class QueueManager {
return LivechatInquiryStatus.QUEUED;
}
+ if (settings.get('Livechat_waiting_queue')) {
+ return LivechatInquiryStatus.QUEUED;
+ }
+
if (RoutingManager.getConfig()?.autoAssignAgent) {
return LivechatInquiryStatus.READY;
}
@@ -135,6 +138,7 @@ export class QueueManager {
static async queueInquiry(inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, defaultAgent?: SelectedAgent | null) {
if (inquiry.status === 'ready') {
+ logger.debug({ msg: 'Inquiry is ready. Delegating', inquiry, defaultAgent });
return RoutingManager.delegateInquiry(inquiry, defaultAgent, undefined, room);
}
@@ -252,7 +256,11 @@ export class QueueManager {
throw new Error('room-not-found');
}
- if (!newRoom.servedBy && settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) {
+ if (
+ !newRoom.servedBy &&
+ settings.get('Livechat_waiting_queue') &&
+ settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')
+ ) {
const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({
inquiryId: inquiry._id,
department,
@@ -320,6 +328,10 @@ export class QueueManager {
}
private static dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, room: IOmnichannelRoom, agent?: SelectedAgent | null) => {
+ if (RoutingManager.getConfig()?.autoAssignAgent) {
+ return;
+ }
+
logger.debug(`Notifying agents of new inquiry ${inquiry._id} queued`);
const { department, rid, v } = inquiry;
diff --git a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx
index 10a317832c03..518ac64b35d0 100644
--- a/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx
+++ b/apps/meteor/client/views/admin/settings/groups/VoipGroupPage/AssignAgentModal.tsx
@@ -52,7 +52,7 @@ const AssignAgentModal = ({ existingExtension, closeModal, reload }: AssignAgent
- {t('Free_Extension_Numbers')}
+ {t('Available_extensions')}
- {t('Free_Extension_Numbers')}
+ {t('Available_extensions')}
{
}
if (semver.satisfies(semver.coerce(mongoVersion), '<5.0.0')) {
- msg += ['', '', 'YOUR CURRENT MONGODB VERSION IS NOT SUPPORTED,', 'PLEASE UPGRADE TO VERSION 5.0 OR LATER'].join('\n');
+ msg += ['', '', 'YOUR CURRENT MONGODB VERSION IS NOT SUPPORTED BY ROCKET.CHAT,', 'PLEASE UPGRADE TO VERSION 5.0 OR LATER'].join('\n');
showErrorBox('SERVER ERROR', msg);
exitIfNotBypassed(process.env.BYPASS_MONGO_VALIDATION);
diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts
new file mode 100644
index 000000000000..62bf3111cab7
--- /dev/null
+++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-queue-management-autoselection.spec.ts
@@ -0,0 +1,118 @@
+import { createFakeVisitor } from '../../mocks/data';
+import { IS_EE } from '../config/constants';
+import { createAuxContext } from '../fixtures/createAuxContext';
+import { Users } from '../fixtures/userStates';
+import { HomeOmnichannel, OmnichannelLiveChat } from '../page-objects';
+import { test, expect } from '../utils/test';
+
+const firstVisitor = createFakeVisitor();
+
+const secondVisitor = createFakeVisitor();
+
+test.use({ storageState: Users.user1.state });
+
+test.describe('OC - Livechat - Queue Management', () => {
+ test.skip(!IS_EE, 'Enterprise Only');
+
+ let poHomeOmnichannel: HomeOmnichannel;
+ let poLiveChat: OmnichannelLiveChat;
+
+ const waitingQueueMessage = 'This is a message from Waiting Queue';
+
+ test.beforeAll(async ({ api, browser }) => {
+ await Promise.all([
+ api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }),
+ api.post('/settings/Livechat_accept_chats_with_no_agents', { value: true }),
+ api.post('/settings/Livechat_waiting_queue', { value: true }),
+ api.post('/settings/Livechat_waiting_queue_message', { value: waitingQueueMessage }),
+ api.post('/livechat/users/agent', { username: 'user1' }),
+ ]);
+
+ const { page: omniPage } = await createAuxContext(browser, Users.user1, '/', true);
+ poHomeOmnichannel = new HomeOmnichannel(omniPage);
+
+ // Agent will be offline for these tests
+ await poHomeOmnichannel.sidenav.switchOmnichannelStatus('offline');
+ });
+
+ test.beforeEach(async ({ browser, api }) => {
+ const context = await browser.newContext();
+ const page2 = await context.newPage();
+
+ poLiveChat = new OmnichannelLiveChat(page2, api);
+ await poLiveChat.page.goto('/livechat');
+ });
+
+ test.afterAll(async ({ api }) => {
+ await Promise.all([
+ api.post('/settings/Livechat_waiting_queue', { value: false }),
+ api.post('/settings/Livechat_waiting_queue_message', { value: '' }),
+ api.delete('/livechat/users/agent/user1'),
+ ]);
+ await poHomeOmnichannel.page.close();
+ });
+
+ test.describe('OC - Queue Management - Auto Selection', () => {
+ let poLiveChat2: OmnichannelLiveChat;
+
+ test.beforeEach(async ({ browser, api }) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ poLiveChat2 = new OmnichannelLiveChat(page, api);
+ await poLiveChat2.page.goto('/livechat');
+ });
+
+ test.afterEach(async () => {
+ await poLiveChat2.closeChat();
+ await poLiveChat2.page.close();
+ await poLiveChat.closeChat();
+ await poLiveChat.page.close();
+ });
+
+ test('Update user position on Queue', async () => {
+ await test.step('should start livechat session', async () => {
+ await poLiveChat.openAnyLiveChatAndSendMessage({
+ liveChatUser: firstVisitor,
+ message: 'Test message',
+ isOffline: false,
+ });
+ });
+
+ await test.step('expect to receive Waiting Queue message on chat', async () => {
+ await expect(poLiveChat.page.locator(`div >> text=${waitingQueueMessage}`)).toBeVisible();
+ });
+
+ await test.step('expect to be on spot #1', async () => {
+ await expect(poLiveChat.queuePosition(1)).toBeVisible();
+ });
+
+ await test.step('should start secondary livechat session', async () => {
+ await poLiveChat2.openAnyLiveChatAndSendMessage({
+ liveChatUser: secondVisitor,
+ message: 'Test message',
+ isOffline: false,
+ });
+ });
+
+ await test.step('should start secondary livechat on spot #2', async () => {
+ await expect(poLiveChat2.queuePosition(2)).toBeVisible();
+ });
+
+ await test.step('should start the queue by making the agent available again', async () => {
+ await poHomeOmnichannel.sidenav.switchOmnichannelStatus('online');
+ });
+
+ await test.step('user1 should get assigned to the first chat', async () => {
+ await expect(poLiveChat.queuePosition(1)).not.toBeVisible();
+ });
+
+ await test.step('secondary session should be on position #1', async () => {
+ await expect(poLiveChat2.queuePosition(1)).toBeVisible();
+ });
+
+ await test.step('secondary session should be taken by user1', async () => {
+ await expect(poLiveChat2.queuePosition(1)).not.toBeVisible();
+ });
+ });
+ });
+});
diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts
index cd6b5668d394..0973b39ef89a 100644
--- a/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts
+++ b/apps/meteor/tests/e2e/page-objects/omnichannel-livechat.ts
@@ -223,4 +223,8 @@ export class OmnichannelLiveChat {
await this.fileUploadTarget.dispatchEvent('drop', { dataTransfer });
}
+
+ queuePosition(position: number): Locator {
+ return this.page.locator(`div[role='alert'] >> text=Your spot is #${position}`);
+ }
}
diff --git a/apps/meteor/tests/e2e/retention-policy.spec.ts b/apps/meteor/tests/e2e/retention-policy.spec.ts
index 9d6dab66448d..066f1166abe4 100644
--- a/apps/meteor/tests/e2e/retention-policy.spec.ts
+++ b/apps/meteor/tests/e2e/retention-policy.spec.ts
@@ -104,17 +104,10 @@ test.describe.serial('retention-policy', () => {
await auxContext.page.close();
});
test('should not show prune section in edit channel for users without permission', async () => {
- await auxContext.poHomeChannel.sidenav.openChat(targetChannel);
- await auxContext.poHomeChannel.tabs.btnRoomInfo.click();
- await auxContext.poHomeChannel.tabs.room.btnEdit.click();
-
await expect(poHomeChannel.tabs.room.pruneAccordion).not.toBeVisible();
});
test('users without permission should be able to edit the channel', async () => {
- await auxContext.poHomeChannel.sidenav.openChat(targetChannel);
- await auxContext.poHomeChannel.tabs.btnRoomInfo.click();
- await auxContext.poHomeChannel.tabs.room.btnEdit.click();
await auxContext.poHomeChannel.tabs.room.advancedSettingsAccordion.click();
await auxContext.poHomeChannel.tabs.room.checkboxReadOnly.check();
await auxContext.poHomeChannel.tabs.room.btnSave.click();
@@ -174,21 +167,20 @@ test.describe.serial('retention-policy', () => {
expect((await setSettingValueById(api, 'RetentionPolicy_TTL_Channels', timeUnitToMs(TIMEUNIT.days, 15))).status()).toBe(200);
});
- test('should display the default max age in edit channel', async () => {
+ test.beforeEach(async () => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.tabs.btnRoomInfo.click();
await poHomeChannel.tabs.room.btnEdit.click();
await poHomeChannel.tabs.room.pruneAccordion.click();
+ });
+
+ test('should display the default max age in edit channel', async () => {
await poHomeChannel.tabs.room.checkboxOverrideGlobalRetention.click();
await expect(poHomeChannel.tabs.room.getMaxAgeLabel('15')).toBeVisible();
});
test('should display overridden retention max age value', async () => {
- await poHomeChannel.sidenav.openChat(targetChannel);
- await poHomeChannel.tabs.btnRoomInfo.click();
- await poHomeChannel.tabs.room.btnEdit.click();
- await poHomeChannel.tabs.room.pruneAccordion.click();
await poHomeChannel.tabs.room.checkboxOverrideGlobalRetention.click();
await poHomeChannel.tabs.room.inputRetentionMaxAge.fill('365');
await poHomeChannel.tabs.room.btnSave.click();
@@ -203,19 +195,10 @@ test.describe.serial('retention-policy', () => {
});
test('should ignore threads be checked accordingly with the global default value', async () => {
- await poHomeChannel.sidenav.openChat(targetChannel);
- await poHomeChannel.tabs.btnRoomInfo.click();
- await poHomeChannel.tabs.room.btnEdit.click();
- await poHomeChannel.tabs.room.pruneAccordion.click();
-
await expect(poHomeChannel.tabs.room.checkboxIgnoreThreads).toBeChecked({ checked: ignoreThreadsSetting });
});
test('should override ignore threads default value', async () => {
- await poHomeChannel.sidenav.openChat(targetChannel);
- await poHomeChannel.tabs.btnRoomInfo.click();
- await poHomeChannel.tabs.room.btnEdit.click();
- await poHomeChannel.tabs.room.pruneAccordion.click();
await poHomeChannel.tabs.room.checkboxIgnoreThreads.click();
await poHomeChannel.tabs.room.btnSave.click();
await poHomeChannel.dismissToast();
diff --git a/apps/meteor/tests/unit/app/emoji/helpers.spec.ts b/apps/meteor/tests/unit/app/emoji/helpers.spec.ts
new file mode 100644
index 000000000000..c7d83b5a36c2
--- /dev/null
+++ b/apps/meteor/tests/unit/app/emoji/helpers.spec.ts
@@ -0,0 +1,51 @@
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+
+import { updateRecent, removeFromRecent, replaceEmojiInRecent } from '../../../../app/emoji/client/helpers';
+import { emoji } from '../../../../app/emoji/client/lib';
+
+describe('Emoji Client Helpers', () => {
+ beforeEach(() => {
+ emoji.packages.base.emojisByCategory.recent = [];
+ });
+
+ describe('updateRecent', () => {
+ it('should update recent emojis with the provided emojis', () => {
+ const recentEmojis = ['emoji1', 'emoji2'];
+ updateRecent(recentEmojis);
+ expect(emoji.packages.base.emojisByCategory.recent).to.contain('emoji1');
+ expect(emoji.packages.base.emojisByCategory.recent).to.contain('emoji2');
+ });
+ });
+
+ describe('removeFromRecent', () => {
+ it('should remove a specific emoji from recent emojis', () => {
+ emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2', 'emoji3'];
+ removeFromRecent('emoji2', emoji.packages.base.emojisByCategory.recent);
+ expect(emoji.packages.base.emojisByCategory.recent).to.not.include('emoji2');
+ expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji3']);
+ });
+
+ it('should do nothing if the emoji is not in the recent list', () => {
+ emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2'];
+ removeFromRecent('emoji3', emoji.packages.base.emojisByCategory.recent);
+ expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji2']);
+ });
+ });
+
+ describe('replaceEmojiInRecent', () => {
+ it('should replace an existing emoji with a new one in recent emojis', () => {
+ emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2', 'emoji3'];
+ replaceEmojiInRecent({ oldEmoji: 'emoji2', newEmoji: 'emoji4' });
+ expect(emoji.packages.base.emojisByCategory.recent).to.not.include('emoji2');
+ expect(emoji.packages.base.emojisByCategory.recent).to.include('emoji4');
+ expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji4', 'emoji3']);
+ });
+
+ it('should do nothing if the emoji to replace is not in the recent list', () => {
+ emoji.packages.base.emojisByCategory.recent = ['emoji1', 'emoji2'];
+ replaceEmojiInRecent({ oldEmoji: 'emoji3', newEmoji: 'emoji4' });
+ expect(emoji.packages.base.emojisByCategory.recent).to.deep.equal(['emoji1', 'emoji2']);
+ });
+ });
+});
diff --git a/packages/apps-engine/src/definition/metadata/AppMethod.ts b/packages/apps-engine/src/definition/metadata/AppMethod.ts
index 71a6d0e914cc..8ec07f53e001 100644
--- a/packages/apps-engine/src/definition/metadata/AppMethod.ts
+++ b/packages/apps-engine/src/definition/metadata/AppMethod.ts
@@ -99,4 +99,6 @@ export enum AppMethod {
EXECUTE_POST_USER_LOGGED_IN = 'executePostUserLoggedIn',
EXECUTE_POST_USER_LOGGED_OUT = 'executePostUserLoggedOut',
EXECUTE_POST_USER_STATUS_CHANGED = 'executePostUserStatusChanged',
+ // Runtime specific methods
+ RUNTIME_RESTART = 'runtime:restart',
}
diff --git a/packages/apps-engine/src/server/compiler/AppCompiler.ts b/packages/apps-engine/src/server/compiler/AppCompiler.ts
index d1f9ddf8a0c0..33f937771ec0 100644
--- a/packages/apps-engine/src/server/compiler/AppCompiler.ts
+++ b/packages/apps-engine/src/server/compiler/AppCompiler.ts
@@ -21,7 +21,7 @@ export class AppCompiler {
throw new Error(`Invalid App package for "${storage.info.name}". Could not find the classFile (${storage.info.classFile}) file.`);
}
- const runtime = await manager.getRuntime().startRuntimeForApp(packageResult);
+ const runtime = await manager.getRuntime().startRuntimeForApp(packageResult, storage);
const app = new ProxiedApp(manager, storage, runtime);
diff --git a/packages/apps-engine/src/server/managers/AppRuntimeManager.ts b/packages/apps-engine/src/server/managers/AppRuntimeManager.ts
index 64adf9b0a98d..6c63307a9ba2 100644
--- a/packages/apps-engine/src/server/managers/AppRuntimeManager.ts
+++ b/packages/apps-engine/src/server/managers/AppRuntimeManager.ts
@@ -1,6 +1,7 @@
import type { AppManager } from '../AppManager';
import type { IParseAppPackageResult } from '../compiler';
import { DenoRuntimeSubprocessController } from '../runtime/deno/AppsEngineDenoRuntime';
+import type { IAppStorageItem } from '../storage';
export type AppRuntimeParams = {
appId: string;
@@ -21,14 +22,18 @@ export class AppRuntimeManager {
constructor(private readonly manager: AppManager) {}
- public async startRuntimeForApp(appPackage: IParseAppPackageResult, options = { force: false }): Promise {
+ public async startRuntimeForApp(
+ appPackage: IParseAppPackageResult,
+ storageItem: IAppStorageItem,
+ options = { force: false },
+ ): Promise {
const { id: appId } = appPackage.info;
if (appId in this.subprocesses && !options.force) {
throw new Error('App already has an associated runtime');
}
- this.subprocesses[appId] = new DenoRuntimeSubprocessController(this.manager, appPackage);
+ this.subprocesses[appId] = new DenoRuntimeSubprocessController(this.manager, appPackage, storageItem);
await this.subprocesses[appId].setupApp();
diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts
index 91bb9aee549c..8e5fc34daa04 100644
--- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts
+++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts
@@ -9,9 +9,9 @@ import { AppStatus } from '../../../definition/AppStatus';
import type { AppManager } from '../../AppManager';
import type { AppBridges } from '../../bridges';
import type { IParseAppPackageResult } from '../../compiler';
-import type { ILoggerStorageEntry } from '../../logging';
+import { AppConsole, type ILoggerStorageEntry } from '../../logging';
import type { AppAccessorManager, AppApiManager } from '../../managers';
-import type { AppLogStorage } from '../../storage';
+import type { AppLogStorage, IAppStorageItem } from '../../storage';
import { LivenessManager } from './LivenessManager';
import { ProcessMessenger } from './ProcessMessenger';
import { bundleLegacyApp } from './bundler';
@@ -109,6 +109,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
constructor(
manager: AppManager,
private readonly appPackage: IParseAppPackageResult,
+ private readonly storageItem: IAppStorageItem,
) {
super();
@@ -177,28 +178,38 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
}
}
- public async killProcess(): Promise {
+ /**
+ * Attempts to kill the process currently controlled by this.deno
+ *
+ * @returns boolean - if a process has been killed or not
+ */
+ public async killProcess(): Promise {
if (!this.deno) {
this.debug('No child process reference');
- return;
+ return false;
}
+ let { killed } = this.deno;
+
// This field is not populated if the process is killed by the OS
- if (this.deno.killed) {
+ if (killed) {
this.debug('App process was already killed');
- return;
+ return killed;
}
// What else should we do?
if (this.deno.kill('SIGKILL')) {
// Let's wait until we get confirmation the process exited
await new Promise((r) => this.deno.on('exit', r));
+ killed = true;
} else {
this.debug('Tried killing the process but failed. Was it already dead?');
+ killed = false;
}
delete this.deno;
this.messenger.clearReceiver();
+ return killed;
}
// Debug purposes, could be deleted later
@@ -249,19 +260,40 @@ export class DenoRuntimeSubprocessController extends EventEmitter {
public async restartApp() {
this.debug('Restarting app subprocess');
+ const logger = new AppConsole('runtime:restart');
+
+ logger.info('Starting restart procedure for app subprocess...', this.livenessManager.getRuntimeData());
this.state = 'restarting';
- await this.killProcess();
+ try {
+ const pid = this.deno?.pid;
- await this.setupApp();
+ const hasKilled = await this.killProcess();
- // setupApp() changes the state to 'ready' - we'll need to workaround that for now
- this.state = 'restarting';
+ if (hasKilled) {
+ logger.debug('Process successfully terminated', { pid });
+ } else {
+ logger.warn('Could not terminate process. Maybe it was already dead?', { pid });
+ }
- await this.sendRequest({ method: 'app:initialize' });
+ await this.setupApp();
+ logger.info('New subprocess successfully spawned', { pid: this.deno.pid });
- this.state = 'ready';
+ // setupApp() changes the state to 'ready' - we'll need to workaround that for now
+ this.state = 'restarting';
+
+ await this.sendRequest({ method: 'app:initialize' });
+ await this.sendRequest({ method: 'app:setStatus', params: [this.storageItem.status] });
+
+ this.state = 'ready';
+
+ logger.info('Successfully restarted app subprocess');
+ } catch (e) {
+ logger.error("Failed to restart app's subprocess", { error: e });
+ } finally {
+ await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger));
+ }
}
public getAppId(): string {
diff --git a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts
index 4fb2433e8e0a..dc89acc8718b 100644
--- a/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts
+++ b/packages/apps-engine/src/server/runtime/deno/LivenessManager.ts
@@ -10,7 +10,7 @@ const defaultOptions: LivenessManager['options'] = {
pingRequestTimeout: 10000,
pingFrequencyInMS: 10000,
consecutiveTimeoutLimit: 4,
- maxRestarts: 3,
+ maxRestarts: Infinity,
};
/**
@@ -65,6 +65,16 @@ export class LivenessManager {
this.options = Object.assign({}, defaultOptions, options);
}
+ public getRuntimeData() {
+ const { restartCount, pingTimeoutConsecutiveCount, restartLog } = this;
+
+ return {
+ restartCount,
+ pingTimeoutConsecutiveCount,
+ restartLog,
+ };
+ }
+
public attach(deno: ChildProcess) {
this.subprocess = deno;
diff --git a/packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts b/packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts
index 0e9c1f7f8786..5c675805e813 100644
--- a/packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts
+++ b/packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts
@@ -4,14 +4,16 @@ import * as path from 'path';
import { TestFixture, Setup, Expect, AsyncTest, SpyOn, Any, AsyncSetupFixture, Teardown } from 'alsatian';
import type { SuccessObject } from 'jsonrpc-lite';
+import { AppStatus } from '../../../src/definition/AppStatus';
import { UserStatusConnection, UserType } from '../../../src/definition/users';
import type { AppManager } from '../../../src/server/AppManager';
import type { IParseAppPackageResult } from '../../../src/server/compiler';
import { AppAccessorManager, AppApiManager } from '../../../src/server/managers';
import { DenoRuntimeSubprocessController } from '../../../src/server/runtime/deno/AppsEngineDenoRuntime';
+import type { IAppStorageItem } from '../../../src/server/storage';
import { TestInfastructureSetup } from '../../test-data/utilities';
-@TestFixture('DenoRuntimeSubprocessController')
+@TestFixture()
export class DenuRuntimeSubprocessControllerTestFixture {
private manager: AppManager;
@@ -19,6 +21,8 @@ export class DenuRuntimeSubprocessControllerTestFixture {
private appPackage: IParseAppPackageResult;
+ private appStorageItem: IAppStorageItem;
+
@AsyncSetupFixture
public async fixture() {
const infrastructure = new TestInfastructureSetup();
@@ -35,11 +39,16 @@ export class DenuRuntimeSubprocessControllerTestFixture {
const appPackage = await fs.readFile(path.join(__dirname, '../../test-data/apps/hello-world-test_0.0.1.zip'));
this.appPackage = await this.manager.getParser().unpackageApp(appPackage);
+
+ this.appStorageItem = {
+ id: 'hello-world-test',
+ status: AppStatus.MANUALLY_ENABLED,
+ } as IAppStorageItem;
}
@Setup
public setup() {
- this.controller = new DenoRuntimeSubprocessController(this.manager, this.appPackage);
+ this.controller = new DenoRuntimeSubprocessController(this.manager, this.appPackage, this.appStorageItem);
this.controller.setupApp();
}
diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json
index fdea9d753c64..ba545e9267e7 100644
--- a/packages/i18n/src/locales/ar.i18n.json
+++ b/packages/i18n/src/locales/ar.i18n.json
@@ -2005,7 +2005,6 @@
"Forward_to_user": "إعادة التوجيه إلى مستخدم",
"Forwarding": "تتم إعادة التوجيه",
"Free": "مجانًا",
- "Free_Extension_Numbers": "أرقام امتداد مجانية",
"Free_Apps": "تطبيقات مجانية",
"Frequently_Used": "الأكثر استعمالاً",
"Friday": "الجمعة",
diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json
index 5de7e4b83877..766ae3472e14 100644
--- a/packages/i18n/src/locales/ca.i18n.json
+++ b/packages/i18n/src/locales/ca.i18n.json
@@ -1975,7 +1975,6 @@
"Forward_to_user": "Remetre a l'usuari",
"Forwarding": "Reenviament",
"Free": "Lliure",
- "Free_Extension_Numbers": "Números d'extensió gratuïts",
"Free_Apps": "Aplicacions gratuïtes",
"Frequently_Used": "Usat freqüentment",
"Friday": "divendres",
diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json
index 6b8ac710c0a3..d51dc0cdc572 100644
--- a/packages/i18n/src/locales/de.i18n.json
+++ b/packages/i18n/src/locales/de.i18n.json
@@ -2232,7 +2232,6 @@
"Forward_to_user": "An BenutzerIn weiterleiten",
"Forwarding": "Weiterleitung",
"Free": "Frei",
- "Free_Extension_Numbers": "Freie Erweiterungsnummern",
"Free_Apps": "Freie Apps",
"Frequently_Used": "Häufig verwendet",
"Friday": "Freitag",
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index 7965479ade61..d21e1bdbaac8 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -778,6 +778,7 @@
"Available": "Available",
"Available_agents": "Available agents",
"Available_departments": "Available Departments",
+ "Available_extensions": "Available extensions",
"Avatar": "Avatar",
"Avatars": "Avatars",
"Avatar_changed_successfully": "Avatar changed successfully",
@@ -2559,7 +2560,6 @@
"Forward_to_user": "Forward to user",
"Forwarding": "Forwarding",
"Free": "Free",
- "Free_Extension_Numbers": "Free Extension Numbers",
"Free_Apps": "Free Apps",
"Frequently_Used": "Frequently Used",
"Friday": "Friday",
diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json
index 1886914d320c..0a30b658022f 100644
--- a/packages/i18n/src/locales/es.i18n.json
+++ b/packages/i18n/src/locales/es.i18n.json
@@ -1998,7 +1998,6 @@
"Forward_to_user": "Reenviar a usuario",
"Forwarding": "Reenvío",
"Free": "Gratuito",
- "Free_Extension_Numbers": "Números de extensión gratuitos",
"Free_Apps": "Aplicaciones gratuitas",
"Frequently_Used": "Usado frecuentemente",
"Friday": "Viernes",
diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json
index a89291f444d9..05a023283acd 100644
--- a/packages/i18n/src/locales/fi.i18n.json
+++ b/packages/i18n/src/locales/fi.i18n.json
@@ -2264,7 +2264,6 @@
"Forward_to_user": "Välitä käyttäjälle",
"Forwarding": "Välitys",
"Free": "Vapaa",
- "Free_Extension_Numbers": "Maksuttomat alanumerot",
"Free_Apps": "Maksuttomat sovellukset",
"Frequently_Used": "Usein käytetyt",
"Friday": "Perjantai",
diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json
index 49e4d04b3730..c69f58131d2d 100644
--- a/packages/i18n/src/locales/fr.i18n.json
+++ b/packages/i18n/src/locales/fr.i18n.json
@@ -1990,7 +1990,6 @@
"Forward_to_user": "Transmettre à l'utilisateur",
"Forwarding": "Transmission",
"Free": "Gratuit",
- "Free_Extension_Numbers": "Numéros d'extension gratuits",
"Free_Apps": "Applications gratuites",
"Frequently_Used": "Fréquemment utilisé",
"Friday": "Vendredi",
diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json
index d9ed823b2205..f9acc163ae96 100644
--- a/packages/i18n/src/locales/hi-IN.i18n.json
+++ b/packages/i18n/src/locales/hi-IN.i18n.json
@@ -2353,7 +2353,6 @@
"Forward_to_user": "उपयोगकर्ता को अग्रेषित करें",
"Forwarding": "अग्रेषित करना",
"Free": "मुक्त",
- "Free_Extension_Numbers": "निःशुल्क एक्सटेंशन नंबर",
"Free_Apps": "मुक्त एप्लिकेशन्स",
"Frequently_Used": "बहुधा प्रयुक्त",
"Friday": "शुक्रवार",
diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json
index 3000c2cb041d..6e441f350f95 100644
--- a/packages/i18n/src/locales/hu.i18n.json
+++ b/packages/i18n/src/locales/hu.i18n.json
@@ -2186,7 +2186,6 @@
"Forward_to_user": "Továbbítás felhasználónak",
"Forwarding": "Továbbítás",
"Free": "Ingyenes",
- "Free_Extension_Numbers": "Ingyenes kiterjesztések száma",
"Free_Apps": "Ingyenes alkalmazások",
"Frequently_Used": "Gyakran használt",
"Friday": "Péntek",
diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json
index 8c208bca1b2d..ecdc9e283364 100644
--- a/packages/i18n/src/locales/ja.i18n.json
+++ b/packages/i18n/src/locales/ja.i18n.json
@@ -1968,7 +1968,6 @@
"Forward_to_user": "ユーザーに転送",
"Forwarding": "転送",
"Free": "無料",
- "Free_Extension_Numbers": "無料の内線番号",
"Free_Apps": "無料のアプリ",
"Frequently_Used": "よく使うもの",
"Friday": "金曜日",
diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json
index b6d371ba6dbf..705f12c83935 100644
--- a/packages/i18n/src/locales/nl.i18n.json
+++ b/packages/i18n/src/locales/nl.i18n.json
@@ -1982,7 +1982,6 @@
"Forward_to_user": "Doorsturen naar gebruiker",
"Forwarding": "Doorsturen",
"Free": "Gratis",
- "Free_Extension_Numbers": "Gratis extensienummers",
"Free_Apps": "Gratis apps",
"Frequently_Used": "Vaak gebruikt",
"Friday": "Vrijdag",
diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json
index 89f5ccfd13cb..123d359852ae 100644
--- a/packages/i18n/src/locales/pl.i18n.json
+++ b/packages/i18n/src/locales/pl.i18n.json
@@ -2211,7 +2211,6 @@
"Forward_to_user": "Przekaż do użytkownika",
"Forwarding": "Przekierowanie",
"Free": "Darmowy",
- "Free_Extension_Numbers": "Bezpłatne numery wewnętrzne",
"Free_Apps": "Bezpłatne aplikacje",
"Frequently_Used": "Często używany",
"Friday": "Piątek",
diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json
index 320d72afbadd..f9ace637d8eb 100644
--- a/packages/i18n/src/locales/pt-BR.i18n.json
+++ b/packages/i18n/src/locales/pt-BR.i18n.json
@@ -655,6 +655,7 @@
"Available": "Disponível",
"Available_agents": "Agentes disponíveis",
"Available_departments": "Departamentos disponíveis",
+ "Available_extensions": "Extensões disponíveis",
"Avatar": "Avatar",
"Avatar_changed_successfully": "Avatar alterado com sucesso",
"Avatar_URL": "URL do avatar",
@@ -2064,7 +2065,6 @@
"Forward_to_user": "Encaminhar ao usuário",
"Forwarding": "Encaminhando",
"Free": "Grátis",
- "Free_Extension_Numbers": "Números de extensão livres",
"Free_Apps": "Aplicativos gratuitos",
"Frequently_Used": "Usados frequentemente",
"Friday": "Sexta-feira",
diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json
index 99e739836c95..c574496584b6 100644
--- a/packages/i18n/src/locales/ru.i18n.json
+++ b/packages/i18n/src/locales/ru.i18n.json
@@ -2134,7 +2134,6 @@
"Forward_to_user": "Отправить пользователю",
"Forwarding": "Перенаправление",
"Free": "Свободно",
- "Free_Extension_Numbers": "Номера бесплатных расширений",
"Free_Apps": "Бесплатные приложения",
"Frequently_Used": "Часто используемые",
"Friday": "Пятницу",
diff --git a/packages/i18n/src/locales/se.i18n.json b/packages/i18n/src/locales/se.i18n.json
index 3695f4698e84..9382c292bd84 100644
--- a/packages/i18n/src/locales/se.i18n.json
+++ b/packages/i18n/src/locales/se.i18n.json
@@ -2511,7 +2511,6 @@
"Forward_to_user": "Forward to user",
"Forwarding": "Forwarding",
"Free": "Free",
- "Free_Extension_Numbers": "Free Extension Numbers",
"Free_Apps": "Free Apps",
"Frequently_Used": "Frequently Used",
"Friday": "Friday",
diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json
index 838c7648f727..d33a79a22aef 100644
--- a/packages/i18n/src/locales/sv.i18n.json
+++ b/packages/i18n/src/locales/sv.i18n.json
@@ -2266,7 +2266,6 @@
"Forward_to_user": "Vidarebefodra till användare",
"Forwarding": "Vidarebefordran",
"Free": "Kostnadsfritt",
- "Free_Extension_Numbers": "Kostnadsfria anknytningsnummer",
"Free_Apps": "Kostnadsfria appar",
"Frequently_Used": "Ofta använd",
"Friday": "Fredag",
diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json
index e3c0796a99a6..93b1cb2d6adf 100644
--- a/packages/i18n/src/locales/zh-TW.i18n.json
+++ b/packages/i18n/src/locales/zh-TW.i18n.json
@@ -1947,7 +1947,6 @@
"Forward_to_user": "轉發給使用者",
"Forwarding": "轉寄",
"Free": "免費",
- "Free_Extension_Numbers": "免費分機號碼",
"Free_Apps": "免費應用程式",
"Frequently_Used": "常用",
"Friday": "星期五",