Skip to content

Commit c7e74a4

Browse files
authored
feat: make api port configurable (#1570)
Signed-off-by: Philippe Martin <[email protected]>
1 parent 11ea28e commit c7e74a4

File tree

7 files changed

+59
-11
lines changed

7 files changed

+59
-11
lines changed

packages/backend/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
"type": "boolean",
2525
"default": false,
2626
"description": "Experimental GPU support for inference servers"
27+
},
28+
"ai-lab.apiPort": {
29+
"type": "number",
30+
"default": 10434,
31+
"minimum": 1024,
32+
"maximum": 65535,
33+
"description": "Port on which the API is listening (requires restart of extension)"
2734
}
2835
}
2936
},

packages/backend/src/managers/apiServer.spec.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
***********************************************************************/
1818

1919
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
20-
import { ApiServer } from './apiServer';
20+
import { ApiServer, PREFERENCE_RANDOM_PORT } from './apiServer';
2121
import request from 'supertest';
2222
import type * as podmanDesktopApi from '@podman-desktop/api';
2323
import path from 'path';
2424
import type { Server } from 'http';
2525
import type { ModelsManager } from './modelsManager';
2626
import type { EventEmitter } from 'node:events';
2727
import { once } from 'node:events';
28+
import type { ConfigurationRegistry } from '../registries/ConfigurationRegistry';
2829

2930
class TestApiServer extends ApiServer {
3031
public override getListener(): Server | undefined {
@@ -40,12 +41,20 @@ const modelsManager = {
4041
getModelsInfo: vi.fn(),
4142
} as unknown as ModelsManager;
4243

44+
const configurationRegistry = {
45+
getExtensionConfiguration: () => {
46+
return {
47+
apiPort: PREFERENCE_RANDOM_PORT,
48+
};
49+
},
50+
} as unknown as ConfigurationRegistry;
4351
beforeEach(async () => {
44-
server = new TestApiServer(extensionContext, modelsManager);
52+
server = new TestApiServer(extensionContext, modelsManager, configurationRegistry);
4553
vi.spyOn(server, 'displayApiInfo').mockReturnValue();
4654
vi.spyOn(server, 'getSpecFile').mockReturnValue(path.join(__dirname, '../../../../api/openapi.yaml'));
4755
vi.spyOn(server, 'getPackageFile').mockReturnValue(path.join(__dirname, '../../../../package.json'));
4856
await server.init();
57+
await new Promise(resolve => setTimeout(resolve, 0)); // wait for random port to be set
4958
});
5059

5160
afterEach(async () => {

packages/backend/src/managers/apiServer.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,18 @@ import type { Server } from 'http';
2323
import path from 'node:path';
2424
import http from 'node:http';
2525
import { existsSync } from 'fs';
26-
import { getFreeRandomPort } from '../utils/ports';
2726
import * as podmanDesktopApi from '@podman-desktop/api';
2827
import { readFile } from 'fs/promises';
2928
import type { ModelsManager } from './modelsManager';
3029
import type { components } from '../../src-generated/openapi';
3130
import type { ModelInfo } from '@shared/src/models/IModelInfo';
31+
import type { ConfigurationRegistry } from '../registries/ConfigurationRegistry';
32+
import { getFreeRandomPort } from '../utils/ports';
3233

33-
const DEFAULT_PORT = 10434;
3434
const SHOW_API_INFO_COMMAND = 'ai-lab.show-api-info';
35+
const SHOW_API_ERROR_COMMAND = 'ai-lab.show-api-error';
36+
37+
export const PREFERENCE_RANDOM_PORT = 0;
3538

3639
type ListModelResponse = components['schemas']['ListModelResponse'];
3740

@@ -52,6 +55,7 @@ export class ApiServer implements Disposable {
5255
constructor(
5356
private extensionContext: podmanDesktopApi.ExtensionContext,
5457
private modelsManager: ModelsManager,
58+
private configurationRegistry: ConfigurationRegistry,
5559
) {}
5660

5761
protected getListener(): Server | undefined {
@@ -72,22 +76,25 @@ export class ApiServer implements Disposable {
7276
app.use('/spec', this.getSpec.bind(this));
7377

7478
const server = http.createServer(app);
75-
let listeningOn = DEFAULT_PORT;
79+
let listeningOn = this.configurationRegistry.getExtensionConfiguration().apiPort;
7680
server.on('listening', () => {
7781
this.displayApiInfo(listeningOn);
7882
});
7983
server.on('error', () => {
84+
this.displayApiError(listeningOn);
85+
});
86+
if (listeningOn === PREFERENCE_RANDOM_PORT) {
8087
getFreeRandomPort('0.0.0.0')
8188
.then((randomPort: number) => {
82-
console.warn(`port ${DEFAULT_PORT} in use, using ${randomPort} for API server`);
8389
listeningOn = randomPort;
84-
this.#listener = server.listen(randomPort);
90+
this.#listener = server.listen(listeningOn);
8591
})
8692
.catch((e: unknown) => {
8793
console.error('unable to get a free port for the api server', e);
8894
});
89-
});
90-
this.#listener = server.listen(DEFAULT_PORT);
95+
} else {
96+
this.#listener = server.listen(listeningOn);
97+
}
9198
}
9299

93100
displayApiInfo(port: number): void {
@@ -111,6 +118,23 @@ export class ApiServer implements Disposable {
111118
apiStatusBarItem.show();
112119
}
113120

121+
displayApiError(port: number): void {
122+
const apiStatusBarItem = podmanDesktopApi.window.createStatusBarItem();
123+
apiStatusBarItem.text = `AI Lab API listening error`;
124+
apiStatusBarItem.command = SHOW_API_ERROR_COMMAND;
125+
this.extensionContext.subscriptions.push(
126+
podmanDesktopApi.commands.registerCommand(SHOW_API_ERROR_COMMAND, async () => {
127+
const address = `http://localhost:${port}`;
128+
await podmanDesktopApi.window.showErrorMessage(
129+
`AI Lab API failed to listen on\n${address}\nYou can change the port in the Preferences then restart the extension.`,
130+
'OK',
131+
);
132+
}),
133+
apiStatusBarItem,
134+
);
135+
apiStatusBarItem.show();
136+
}
137+
114138
private getFile(filepath: string): string {
115139
// when plugin is installed, the file is placed in the plugin directory (~/.local/share/containers/podman-desktop/plugins/<pluginname>/)
116140
const prodFile = path.join(__dirname, filepath);

packages/backend/src/registries/ConfigurationRegistry.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import type { ExtensionConfiguration } from '@shared/src/models/IExtensionConfig
2121
import { Messages } from '@shared/Messages';
2222
import path from 'node:path';
2323

24-
const CONFIGURATION_SECTIONS: string[] = ['ai-lab.models.path', 'ai-lab.experimentalGPU'];
24+
const CONFIGURATION_SECTIONS: string[] = ['ai-lab.models.path', 'ai-lab.experimentalGPU', 'ai-lab.apiPort'];
25+
26+
const API_PORT_DEFAULT = 10434;
2527

2628
export class ConfigurationRegistry extends Publisher<ExtensionConfiguration> implements Disposable {
2729
#configuration: Configuration;
@@ -40,6 +42,7 @@ export class ConfigurationRegistry extends Publisher<ExtensionConfiguration> imp
4042
return {
4143
modelsPath: this.getModelsPath(),
4244
experimentalGPU: this.#configuration.get<boolean>('experimentalGPU') ?? false,
45+
apiPort: this.#configuration.get<number>('apiPort') ?? API_PORT_DEFAULT,
4346
};
4447
}
4548

packages/backend/src/studio.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ export class Studio {
328328
// Register the instance
329329
this.#rpcExtension.registerInstance<StudioApiImpl>(StudioApiImpl, this.#studioApi);
330330

331-
const apiServer = new ApiServer(this.#extensionContext, this.#modelsManager);
331+
const apiServer = new ApiServer(this.#extensionContext, this.#modelsManager, this.#configurationRegistry);
332332
await apiServer.init();
333333
this.#extensionContext.subscriptions.push(apiServer);
334334
}

packages/backend/src/workers/provider/LlamaCppPython.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ beforeEach(() => {
9595
vi.mocked(configurationRegistry.getExtensionConfiguration).mockReturnValue({
9696
experimentalGPU: false,
9797
modelsPath: 'model-path',
98+
apiPort: 10434,
9899
});
99100
vi.mocked(podmanConnection.findRunningContainerProviderConnection).mockReturnValue(dummyConnection);
100101
vi.mocked(podmanConnection.getContainerProviderConnection).mockReturnValue(dummyConnection);
@@ -271,6 +272,7 @@ describe('perform', () => {
271272
vi.mocked(configurationRegistry.getExtensionConfiguration).mockReturnValue({
272273
experimentalGPU: true,
273274
modelsPath: '',
275+
apiPort: 10434,
274276
});
275277

276278
vi.mocked(gpuManager.collectGPUs).mockResolvedValue([
@@ -300,6 +302,7 @@ describe('perform', () => {
300302
vi.mocked(configurationRegistry.getExtensionConfiguration).mockReturnValue({
301303
experimentalGPU: true,
302304
modelsPath: '',
305+
apiPort: 10434,
303306
});
304307

305308
vi.mocked(gpuManager.collectGPUs).mockResolvedValue([
@@ -331,6 +334,7 @@ describe('perform', () => {
331334
vi.mocked(configurationRegistry.getExtensionConfiguration).mockReturnValue({
332335
experimentalGPU: true,
333336
modelsPath: '',
337+
apiPort: 10434,
334338
});
335339

336340
vi.mocked(gpuManager.collectGPUs).mockResolvedValue([

packages/shared/src/models/IExtensionConfiguration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@
1919
export interface ExtensionConfiguration {
2020
experimentalGPU: boolean;
2121
modelsPath: string;
22+
apiPort: number;
2223
}

0 commit comments

Comments
 (0)