Skip to content

Commit 1f0b6f5

Browse files
committed
feat: improve podman cli execution
Signed-off-by: axel7083 <[email protected]>
1 parent 9a1421f commit 1f0b6f5

File tree

9 files changed

+91
-168
lines changed

9 files changed

+91
-168
lines changed

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

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import os from 'node:os';
2121
import fs, { type Stats, type PathLike } from 'node:fs';
2222
import path from 'node:path';
2323
import { ModelsManager } from './modelsManager';
24-
import { env, process as coreProcess } from '@podman-desktop/api';
24+
import { env } from '@podman-desktop/api';
2525
import type { RunResult, TelemetryLogger, Webview, ContainerProviderConnection } from '@podman-desktop/api';
2626
import type { CatalogManager } from './catalogManager';
2727
import type { ModelInfo } from '@shared/src/models/IModelInfo';
@@ -33,7 +33,6 @@ import type { GGUFParseOutput } from '@huggingface/gguf';
3333
import { gguf } from '@huggingface/gguf';
3434
import type { PodmanConnection } from './podmanConnection';
3535
import { VMType } from '@shared/src/models/IPodman';
36-
import { getPodmanMachineName } from '../utils/podman';
3736
import type { ConfigurationRegistry } from '../registries/ConfigurationRegistry';
3837
import { Uploader } from '../utils/uploader';
3938

@@ -47,7 +46,6 @@ const mocks = vi.hoisted(() => {
4746
getTargetMock: vi.fn(),
4847
getDownloaderCompleter: vi.fn(),
4948
isCompletionEventMock: vi.fn(),
50-
getPodmanCliMock: vi.fn(),
5149
};
5250
});
5351

@@ -59,11 +57,6 @@ vi.mock('@huggingface/gguf', () => ({
5957
gguf: vi.fn(),
6058
}));
6159

62-
vi.mock('../utils/podman', () => ({
63-
getPodmanCli: mocks.getPodmanCliMock,
64-
getPodmanMachineName: vi.fn(),
65-
}));
66-
6760
vi.mock('@podman-desktop/api', () => {
6861
return {
6962
Disposable: {
@@ -72,9 +65,6 @@ vi.mock('@podman-desktop/api', () => {
7265
env: {
7366
isWindows: false,
7467
},
75-
process: {
76-
exec: vi.fn(),
77-
},
7868
fs: {
7969
createFileSystemWatcher: (): unknown => ({
8070
onDidCreate: vi.fn(),
@@ -102,6 +92,7 @@ vi.mock('../utils/downloader', () => ({
10292

10393
const podmanConnectionMock = {
10494
getContainerProviderConnections: vi.fn(),
95+
executeSSH: vi.fn(),
10596
} as unknown as PodmanConnection;
10697

10798
const cancellationTokenRegistryMock = {
@@ -598,8 +589,7 @@ describe('deleting models', () => {
598589
});
599590

600591
test('deleting on windows should check for all connections', async () => {
601-
vi.mocked(coreProcess.exec).mockResolvedValue({} as RunResult);
602-
mocks.getPodmanCliMock.mockReturnValue('dummyCli');
592+
vi.mocked(podmanConnectionMock.executeSSH).mockResolvedValue({} as RunResult);
603593
vi.mocked(env).isWindows = true;
604594
const connections: ContainerProviderConnection[] = [
605595
{
@@ -622,7 +612,6 @@ describe('deleting models', () => {
622612
},
623613
];
624614
vi.mocked(podmanConnectionMock.getContainerProviderConnections).mockReturnValue(connections);
625-
vi.mocked(getPodmanMachineName).mockReturnValue('machine-2');
626615

627616
const rmSpy = vi.spyOn(fs.promises, 'rm');
628617
rmSpy.mockResolvedValue(undefined);
@@ -659,10 +648,7 @@ describe('deleting models', () => {
659648

660649
expect(podmanConnectionMock.getContainerProviderConnections).toHaveBeenCalledOnce();
661650

662-
expect(coreProcess.exec).toHaveBeenCalledWith('dummyCli', [
663-
'machine',
664-
'ssh',
665-
'machine-2',
651+
expect(podmanConnectionMock.executeSSH).toHaveBeenCalledWith(connections[1], [
666652
'rm',
667653
'-f',
668654
'/home/user/ai-lab/models/dummyFile',

packages/backend/src/managers/modelsManager.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ import type { Task } from '@shared/src/models/ITask';
3030
import type { BaseEvent } from '../models/baseEvent';
3131
import { isCompletionEvent, isProgressEvent } from '../models/baseEvent';
3232
import { Uploader } from '../utils/uploader';
33-
import { deleteRemoteModel, getLocalModelFile, isModelUploaded } from '../utils/modelsUtils';
34-
import { getPodmanMachineName } from '../utils/podman';
33+
import { getLocalModelFile, getRemoteModelFile } from '../utils/modelsUtils';
3534
import type { CancellationTokenRegistry } from '../registries/CancellationTokenRegistry';
3635
import { getHash, hasValidSha } from '../utils/sha';
3736
import type { GGUFParseOutput } from '@huggingface/gguf';
@@ -231,17 +230,32 @@ export class ModelsManager implements Disposable {
231230
for (const connection of connections) {
232231
// ignore non-wsl machines
233232
if (connection.vmType !== VMType.WSL) continue;
234-
// Get the corresponding machine name
235-
const machineName = getPodmanMachineName(connection);
236233

237-
// check if model already loaded on the podman machine
238-
const existsRemote = await isModelUploaded(machineName, modelInfo);
239-
if (!existsRemote) return;
234+
// check if remote model is
235+
try {
236+
await this.podmanConnection.executeSSH(connection, ['stat', getRemoteModelFile(modelInfo)]);
237+
} catch (err: unknown) {
238+
console.warn(err);
239+
continue;
240+
}
240241

241-
await deleteRemoteModel(machineName, modelInfo);
242+
await this.deleteRemoteModelByConnection(connection, modelInfo);
242243
}
243244
}
244245

246+
/**
247+
* Delete a model given a {@link ContainerProviderConnection}
248+
* @param connection
249+
* @param modelInfo
250+
* @protected
251+
*/
252+
protected async deleteRemoteModelByConnection(
253+
connection: ContainerProviderConnection,
254+
modelInfo: ModelInfo,
255+
): Promise<void> {
256+
await this.podmanConnection.executeSSH(connection, ['rm', '-f', getRemoteModelFile(modelInfo)]);
257+
}
258+
245259
/**
246260
* This method will resolve when the provided model will be downloaded.
247261
*
@@ -439,7 +453,7 @@ export class ModelsManager implements Disposable {
439453
connection: connection.name,
440454
});
441455

442-
const uploader = new Uploader(connection, model);
456+
const uploader = new Uploader(this.podmanConnection, connection, model);
443457
uploader.onEvent(event => this.onDownloadUploadEvent(event, 'upload'), this);
444458

445459
// perform download

packages/backend/src/utils/modelsUtils.spec.ts

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,8 @@
1616
* SPDX-License-Identifier: Apache-2.0
1717
***********************************************************************/
1818
import { beforeEach, describe, expect, test, vi } from 'vitest';
19-
import { process as apiProcess } from '@podman-desktop/api';
20-
import {
21-
deleteRemoteModel,
22-
getLocalModelFile,
23-
getRemoteModelFile,
24-
isModelUploaded,
25-
MACHINE_BASE_FOLDER,
26-
} from './modelsUtils';
19+
import { getLocalModelFile, getRemoteModelFile, MACHINE_BASE_FOLDER } from './modelsUtils';
2720
import type { ModelInfo } from '@shared/src/models/IModelInfo';
28-
import { getPodmanCli } from './podman';
2921

3022
vi.mock('@podman-desktop/api', () => {
3123
return {
@@ -35,14 +27,8 @@ vi.mock('@podman-desktop/api', () => {
3527
};
3628
});
3729

38-
vi.mock('./podman', () => ({
39-
getPodmanCli: vi.fn(),
40-
}));
41-
4230
beforeEach(() => {
4331
vi.resetAllMocks();
44-
45-
vi.mocked(getPodmanCli).mockReturnValue('dummyPodmanCli');
4632
});
4733

4834
describe('getLocalModelFile', () => {
@@ -94,48 +80,3 @@ describe('getRemoteModelFile', () => {
9480
expect(path).toBe(`${MACHINE_BASE_FOLDER}dummy.guff`);
9581
});
9682
});
97-
98-
describe('isModelUploaded', () => {
99-
test('execute stat on targeted machine', async () => {
100-
expect(
101-
await isModelUploaded('dummyMachine', {
102-
id: 'dummyModelId',
103-
file: {
104-
path: 'dummyPath',
105-
file: 'dummy.guff',
106-
},
107-
} as unknown as ModelInfo),
108-
).toBeTruthy();
109-
110-
expect(getPodmanCli).toHaveBeenCalled();
111-
expect(apiProcess.exec).toHaveBeenCalledWith('dummyPodmanCli', [
112-
'machine',
113-
'ssh',
114-
'dummyMachine',
115-
'stat',
116-
expect.anything(),
117-
]);
118-
});
119-
});
120-
121-
describe('deleteRemoteModel', () => {
122-
test('execute stat on targeted machine', async () => {
123-
await deleteRemoteModel('dummyMachine', {
124-
id: 'dummyModelId',
125-
file: {
126-
path: 'dummyPath',
127-
file: 'dummy.guff',
128-
},
129-
} as unknown as ModelInfo);
130-
131-
expect(getPodmanCli).toHaveBeenCalled();
132-
expect(apiProcess.exec).toHaveBeenCalledWith('dummyPodmanCli', [
133-
'machine',
134-
'ssh',
135-
'dummyMachine',
136-
'rm',
137-
'-f',
138-
expect.anything(),
139-
]);
140-
});
141-
});

packages/backend/src/utils/modelsUtils.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
***********************************************************************/
1818
import type { ModelInfo } from '@shared/src/models/IModelInfo';
1919
import { join, posix } from 'node:path';
20-
import { getPodmanCli } from './podman';
21-
import { process } from '@podman-desktop/api';
2220

2321
export const MACHINE_BASE_FOLDER = '/home/user/ai-lab/models/';
2422

@@ -42,36 +40,6 @@ export function getRemoteModelFile(modelInfo: ModelInfo): string {
4240
return posix.join(MACHINE_BASE_FOLDER, modelInfo.file.file);
4341
}
4442

45-
/**
46-
* utility method to determine if a model is already uploaded to the podman machine
47-
* @param machine
48-
* @param modelInfo
49-
*/
50-
export async function isModelUploaded(machine: string, modelInfo: ModelInfo): Promise<boolean> {
51-
try {
52-
const remotePath = getRemoteModelFile(modelInfo);
53-
await process.exec(getPodmanCli(), ['machine', 'ssh', machine, 'stat', remotePath]);
54-
return true;
55-
} catch (err: unknown) {
56-
console.error('Something went wrong while trying to stat remote model path', err);
57-
return false;
58-
}
59-
}
60-
61-
/**
62-
* Given a machine and a modelInfo, delete the corresponding file on the podman machine
63-
* @param machine the machine to target
64-
* @param modelInfo the model info
65-
*/
66-
export async function deleteRemoteModel(machine: string, modelInfo: ModelInfo): Promise<void> {
67-
try {
68-
const remotePath = getRemoteModelFile(modelInfo);
69-
await process.exec(getPodmanCli(), ['machine', 'ssh', machine, 'rm', '-f', remotePath]);
70-
} catch (err: unknown) {
71-
console.error('Something went wrong while trying to stat remote model path', err);
72-
}
73-
}
74-
7543
export function getModelPropertiesForEnvironment(modelInfo: ModelInfo): string[] {
7644
const envs: string[] = [];
7745
if (modelInfo.properties) {

packages/backend/src/utils/podman.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export type MachineJSON = {
3232
VMType?: string;
3333
};
3434

35+
/**
36+
* We should be using the {@link @podman-desktop/api#extensions.getExtensions} function to get podman
37+
* exec method
38+
* @deprecated
39+
*/
3540
export function getPodmanCli(): string {
3641
// If we have a custom binary path regardless if we are running Windows or not
3742
const customBinaryPath = getCustomBinaryPath();
@@ -52,8 +57,9 @@ export function getCustomBinaryPath(): string | undefined {
5257
}
5358

5459
/**
55-
* In the ${link ContainerProviderConnection.name} property the name is not usage, and we need to transform it
60+
* In the {@link ContainerProviderConnection.name} property the name is not usage, and we need to transform it
5661
* @param connection
62+
* @deprecated
5763
*/
5864
export function getPodmanMachineName(connection: ContainerProviderConnection): string {
5965
const runningConnectionName = connection.name;

packages/backend/src/utils/uploader.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,13 @@ import { Uploader } from './uploader';
2424
import type { ModelInfo } from '@shared/src/models/IModelInfo';
2525
import type { ContainerProviderConnection } from '@podman-desktop/api';
2626
import { VMType } from '@shared/src/models/IPodman';
27+
import type { PodmanConnection } from '../managers/podmanConnection';
2728

2829
vi.mock('@podman-desktop/api', async () => {
2930
return {
3031
env: {
3132
isWindows: false,
3233
},
33-
process: {
34-
exec: vi.fn(),
35-
},
3634
EventEmitter: vi.fn().mockImplementation(() => {
3735
return {
3836
fire: vi.fn(),
@@ -41,6 +39,10 @@ vi.mock('@podman-desktop/api', async () => {
4139
};
4240
});
4341

42+
const podmanConnectionMock: PodmanConnection = {
43+
executeSSH: vi.fn(),
44+
} as unknown as PodmanConnection;
45+
4446
const connectionMock: ContainerProviderConnection = {
4547
name: 'machine2',
4648
type: 'podman',
@@ -51,7 +53,7 @@ const connectionMock: ContainerProviderConnection = {
5153
},
5254
};
5355

54-
const uploader = new Uploader(connectionMock, {
56+
const uploader = new Uploader(podmanConnectionMock, connectionMock, {
5557
id: 'dummyModelId',
5658
file: {
5759
file: 'dummyFile.guff',

packages/backend/src/utils/uploader.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ import type { ModelInfo } from '@shared/src/models/IModelInfo';
2424
import { getLocalModelFile } from './modelsUtils';
2525
import type { IWorker } from '../workers/IWorker';
2626
import type { UploaderOptions } from '../workers/uploader/UploaderOptions';
27+
import type { PodmanConnection } from '../managers/podmanConnection';
2728

2829
export class Uploader {
2930
readonly #_onEvent = new EventEmitter<BaseEvent>();
3031
readonly onEvent: Event<BaseEvent> = this.#_onEvent.event;
3132
readonly #workers: IWorker<UploaderOptions, string>[] = [];
3233

3334
constructor(
35+
podman: PodmanConnection,
3436
private connection: ContainerProviderConnection,
3537
private modelInfo: ModelInfo,
3638
private abortSignal?: AbortSignal,
3739
) {
38-
this.#workers = [new WSLUploader()];
40+
this.#workers = [new WSLUploader(podman)];
3941
}
4042

4143
/**

0 commit comments

Comments
 (0)