Skip to content

Commit bd86658

Browse files
committed
refactor: deployer to device
1 parent ef375c1 commit bd86658

File tree

7 files changed

+116
-31
lines changed

7 files changed

+116
-31
lines changed

.changeset/cozy-cooks-bake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@arkts/image-manager": patch
3+
---
4+
5+
refactor: deployer to device

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,33 +49,33 @@ async function main() {
4949
throw new Error('MateBook Fold not found')
5050

5151
// Create the deployer
52-
const deployer = image.createDeployer('MateBook Fold', createDeployedImageConfig(image))
52+
const device = image.createDevice('MateBook Fold', createDeployedImageConfig(image))
5353
.setCpuNumber(4)
5454
.setMemoryRamSize(4096)
5555
.setDataDiskSize(6144)
5656

5757
// We can get the final deployed image options,
5858
// it will be written to the `imageBasePath/lists.json` file when deployed.
59-
const list = await deployer.buildList()
59+
const list = await device.buildList()
6060
console.warn(list)
6161

6262
// We can get the `config.ini` object,
6363
// it will be written to the `deployedPath/MateBook Fold/config.ini` file when deployed.
64-
const config = await deployer.buildIni()
64+
const config = await device.buildIni()
6565
console.warn(config)
6666
// You also can get the `config.ini` string version:
67-
const iniString = await deployer.toIniString()
67+
const iniString = await device.toIniString()
6868
console.warn(iniString)
6969

70-
// Deploy the image
71-
await deployer.deploy()
70+
// Deploy the device
71+
await device.deploy()
7272
console.warn('Image deployed successfully')
7373

7474
// Start the emulator
75-
await image.start(deployer)
75+
await image.start(device)
7676

7777
await new Promise<void>(resolve => setTimeout(resolve, 1000 * 60))
7878
// Stop the emulator
79-
image.stop(deployer)
79+
image.stop(device)
8080
}
8181
```

src/deployer/image-deployer.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { LocalImage } from '../images'
22
import type { LocalImageImpl } from '../images/local-image'
3-
import type { DeployedDevModel, DeployedImageConfigWithProductName, DeployedImageOptions, FullDeployedImageOptions } from './list'
3+
import type { DeployedDevModel, DeployedImageOptions, FullDeployedImageOptions, ProductNameable } from './list'
44

5-
export interface ImageDeployer {
5+
export interface Device {
66
setUuid(uuid: `${string}-${string}-${string}-${string}-${string}`): this
77
setModel(model: string): this
88
setDevModel(devModel: FullDeployedImageOptions['devModel']): this
@@ -36,14 +36,26 @@ export interface ImageDeployer {
3636
*/
3737
toIniString(): Promise<string>
3838
/**
39-
* Deploy the image.
39+
* Deploy the device.
4040
*
4141
* @param symlinkImage - If true, symlink the system image to current device directory. Default is `true`.
4242
*/
4343
deploy(symlinkImage?: boolean): Promise<void | Error>
44+
/**
45+
* Check if the device is deployed.
46+
*
47+
* @returns True if the device is deployed, false otherwise.
48+
*/
49+
isDeployed(): Promise<boolean>
50+
/**
51+
* Delete the device.
52+
*
53+
* @returns True if the device is deleted, false otherwise.
54+
*/
55+
delete(): Promise<void | Error>
4456
}
4557

46-
class ImageDeployerImpl implements ImageDeployer {
58+
class ImageDeployerImpl implements Device {
4759
private readonly options: Partial<FullDeployedImageOptions> = {}
4860
private isDefault = true
4961
private isCustomize = false
@@ -55,11 +67,13 @@ class ImageDeployerImpl implements ImageDeployer {
5567
private readonly image: LocalImageImpl,
5668
uuid: string,
5769
name: string,
58-
private readonly config: DeployedImageConfigWithProductName,
70+
private readonly config: ProductNameable<FullDeployedImageOptions>,
5971
) {
6072
this.options.uuid = uuid
6173
this.options.name = name
6274
Object.assign(this.options, config)
75+
if ('productName' in this.options)
76+
delete this.options.productName
6377
}
6478

6579
setUuid(uuid: string): this {
@@ -290,13 +304,37 @@ class ImageDeployerImpl implements ImageDeployer {
290304
}
291305
return undefined
292306
}
307+
308+
async isDeployed(): Promise<boolean> {
309+
const { fs, path } = this.image.getImageManager().getOptions()
310+
const listsPath = path.resolve(this.image.getImageManager().getOptions().deployedPath, 'lists.json')
311+
if (!fs.existsSync(listsPath) || !fs.statSync(listsPath).isFile())
312+
return false
313+
const lists: FullDeployedImageOptions[] = JSON.parse(fs.readFileSync(listsPath, 'utf-8')) ?? []
314+
return lists.find(item => item.name === this.options.name) !== undefined
315+
}
316+
317+
async delete(): Promise<void | Error> {
318+
const { fs, path } = this.image.getImageManager().getOptions()
319+
const listsPath = path.resolve(this.image.getImageManager().getOptions().deployedPath, 'lists.json')
320+
if (!fs.existsSync(listsPath) || !fs.statSync(listsPath).isFile())
321+
return new Error('Lists file not found')
322+
const lists: FullDeployedImageOptions[] = JSON.parse(fs.readFileSync(listsPath, 'utf-8')) ?? []
323+
const index = lists.findIndex(item => item.name === this.options.name)
324+
if (index === -1)
325+
return new Error(`Device ${this.options.name} not found`)
326+
lists.splice(index, 1)
327+
fs.writeFileSync(listsPath, JSON.stringify(lists, null, 2))
328+
fs.rmSync(path.resolve(this.options.path ?? ''), { recursive: true })
329+
return undefined
330+
}
293331
}
294332

295333
export function createImageDeployer(
296334
image: LocalImage,
297335
uuid: string,
298336
name: string,
299-
config: DeployedImageConfigWithProductName,
300-
): ImageDeployer {
337+
config: ProductNameable<FullDeployedImageOptions>,
338+
): Device {
301339
return new ImageDeployerImpl(image as LocalImageImpl, uuid, name, config)
302340
}

src/deployer/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export type { ImageDeployer } from './image-deployer'
1+
export type { Device } from './image-deployer'
22
export * from './list'

src/deployer/list.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ export type DeployedDevModel
1919
| DevModel
2020
| (string & {})
2121

22+
export type ProductNameable<T> = T & {
23+
/**
24+
* The name of the product.
25+
*
26+
* @example 'Mate 80 Pro Max、Mate 80 RS'
27+
*/
28+
productName: string
29+
}
30+
2231
export interface DeployedImageConfig {
2332
/**
2433
* Diagonal size.

src/images/local-image.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ImageDeployer } from '../deployer/image-deployer'
2-
import type { DeployedImageConfigWithProductName } from '../deployer/list'
1+
import type { Device } from '../deployer/image-deployer'
2+
import type { DeployedImageConfigWithProductName, FullDeployedImageOptions, ProductNameable } from '../deployer/list'
33
import type { ProductConfigItem } from '../product-config'
44
import type { DeviceType, Stringifiable } from '../types'
55
import type { BaseImage } from './image'
@@ -8,13 +8,14 @@ import { ImageBase } from './image'
88

99
export interface LocalImage extends BaseImage, Stringifiable<LocalImage.Stringifiable> {
1010
imageType: 'local'
11-
createDeployer(name: string, config: DeployedImageConfigWithProductName): ImageDeployer
11+
createDevice(name: string, config: DeployedImageConfigWithProductName): Device
1212
getProductConfig(): Promise<ProductConfigItem[]>
1313
delete(): Promise<void | Error>
14-
buildStartCommand(deployer: ImageDeployer): Promise<string>
15-
start(deployer: ImageDeployer): Promise<import('node:child_process').ChildProcess>
16-
buildStopCommand(deployer: ImageDeployer): Promise<string>
17-
stop(deployer: ImageDeployer): Promise<import('node:child_process').ChildProcess>
14+
buildStartCommand(deployer: Device): Promise<string>
15+
start(deployer: Device): Promise<import('node:child_process').ChildProcess>
16+
buildStopCommand(deployer: Device): Promise<string>
17+
stop(deployer: Device): Promise<import('node:child_process').ChildProcess>
18+
getDevices(): Promise<Device[]>
1819
}
1920

2021
export namespace LocalImage {
@@ -27,7 +28,7 @@ export namespace LocalImage {
2728
export class LocalImageImpl extends ImageBase<LocalImage.Stringifiable> implements LocalImage {
2829
imageType = 'local' as const
2930

30-
createDeployer(name: string, config: DeployedImageConfigWithProductName): ImageDeployer {
31+
createDevice(name: string, config: ProductNameable<FullDeployedImageOptions>): Device {
3132
return createImageDeployer(
3233
this,
3334
this.getImageManager().getOptions().crypto.randomUUID(),
@@ -52,11 +53,16 @@ export class LocalImageImpl extends ImageBase<LocalImage.Stringifiable> implemen
5253
}
5354

5455
async delete(): Promise<void | Error> {
55-
const fs = this.getImageManager().getOptions().fs
56+
const { fs } = this.getImageManager().getOptions()
5657
const path = this.getFsPath()
5758
if (!fs.existsSync(path) || !fs.statSync(path).isDirectory())
5859
return new Error('Image path does not exist')
5960
fs.rmSync(path, { recursive: true })
61+
const devices = await this.getDevices()
62+
const error = await Promise.allSettled(devices.map(device => device.delete()))
63+
.then(results => results.find(result => result.status === 'rejected'))
64+
if (error)
65+
return error.reason
6066
return undefined
6167
}
6268

@@ -65,7 +71,7 @@ export class LocalImageImpl extends ImageBase<LocalImage.Stringifiable> implemen
6571
return process.platform === 'win32' ? path.join(emulatorPath, 'Emulator.exe') : path.join(emulatorPath, 'Emulator')
6672
}
6773

68-
async buildStartCommand(deployer: ImageDeployer): Promise<string> {
74+
async buildStartCommand(deployer: Device): Promise<string> {
6975
const config = await deployer.buildList()
7076
const executablePath = this.getExecutablePath()
7177
const args = [
@@ -79,12 +85,12 @@ export class LocalImageImpl extends ImageBase<LocalImage.Stringifiable> implemen
7985
return `${executablePath} ${args}`
8086
}
8187

82-
async start(deployer: ImageDeployer): Promise<import('node:child_process').ChildProcess> {
88+
async start(deployer: Device): Promise<import('node:child_process').ChildProcess> {
8389
const { child_process, emulatorPath } = this.getImageManager().getOptions()
8490
return child_process.exec(await this.buildStartCommand(deployer), { cwd: emulatorPath })
8591
}
8692

87-
async buildStopCommand(deployer: ImageDeployer): Promise<string> {
93+
async buildStopCommand(deployer: Device): Promise<string> {
8894
const config = await deployer.buildList()
8995
const executablePath = this.getExecutablePath()
9096
const args = [
@@ -94,11 +100,38 @@ export class LocalImageImpl extends ImageBase<LocalImage.Stringifiable> implemen
94100
return `${executablePath} ${args}`
95101
}
96102

97-
async stop(deployer: ImageDeployer): Promise<import('node:child_process').ChildProcess> {
103+
async stop(deployer: Device): Promise<import('node:child_process').ChildProcess> {
98104
const { child_process, emulatorPath } = this.getImageManager().getOptions()
99105
return child_process.exec(await this.buildStopCommand(deployer), { cwd: emulatorPath })
100106
}
101107

108+
private typeAssert<T>(value: unknown): asserts value is T {}
109+
110+
async getDevices(): Promise<Device[]> {
111+
const { path, fs, imageBasePath } = this.getImageManager().getOptions()
112+
const listsJsonPath = path.resolve(this.getImageManager().getOptions().deployedPath, 'lists.json')
113+
if (!fs.existsSync(listsJsonPath) || !fs.statSync(listsJsonPath).isFile())
114+
return []
115+
const listsJson: unknown = JSON.parse(fs.readFileSync(listsJsonPath, 'utf-8'))
116+
if (!Array.isArray(listsJson) || this.imageType !== 'local')
117+
return []
118+
119+
this.typeAssert<LocalImage>(this)
120+
const devices: Device[] = []
121+
for (const listsJsonItem of listsJson as unknown[]) {
122+
if (typeof listsJsonItem !== 'object' || listsJsonItem === null)
123+
continue
124+
if (!('imageDir' in listsJsonItem) || typeof listsJsonItem.imageDir !== 'string')
125+
continue
126+
if (!('name' in listsJsonItem) || typeof listsJsonItem.name !== 'string')
127+
continue
128+
if (path.resolve(this.getFsPath()) !== path.resolve(imageBasePath, listsJsonItem.imageDir))
129+
continue
130+
devices.push(this.createDevice(listsJsonItem.name, listsJsonItem as ProductNameable<FullDeployedImageOptions>))
131+
}
132+
return devices
133+
}
134+
102135
toJSON(): LocalImage.Stringifiable {
103136
return {
104137
...super.toJSON(),

test/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe.sequential('image manager', (it) => {
6262
if (!mateBookFold)
6363
throw new Error('MateBook Fold not found')
6464
const uuid = crypto.randomUUID()
65-
const deployer = image.createDeployer('MateBook Fold', createDeployedImageConfig(mateBookFold))
65+
const deployer = image.createDevice('MateBook Fold', createDeployedImageConfig(mateBookFold))
6666
.setCpuNumber(4)
6767
.setMemoryRamSize(4096)
6868
.setDataDiskSize(6144)
@@ -201,6 +201,6 @@ describe.skip('start', (it) => {
201201
const mateBookFold = productConfig.find(item => item.name === 'MateBook Fold')
202202
if (!mateBookFold)
203203
throw new Error('MateBook Fold not found')
204-
await image.start(image.createDeployer('MateBook Fold', createDeployedImageConfig(mateBookFold)))
204+
await image.start(image.createDevice('MateBook Fold', createDeployedImageConfig(mateBookFold)))
205205
}, 1000 * 1000)
206206
})

0 commit comments

Comments
 (0)