Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CP-2914] Flashing process for Linux OS #2061

Merged
merged 24 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
76c9a5a
Start Flashing process for Linux OS
MateuszMudita Sep 16, 2024
ff1be9f
Merge branch 'CP-3027' of https://github.com/mudita/mudita-center int…
MateuszMudita Sep 16, 2024
d059a5a
Start flashing process on linux
MateuszMudita Sep 16, 2024
bc3eb29
Merge branch 'CP-3027' of https://github.com/mudita/mudita-center int…
MateuszMudita Sep 17, 2024
106f9e2
[CP-2914] Workaround for ensuring device flashing on Linux has been a…
dkarski Sep 17, 2024
70f3805
[CP-2914] Fix lint in exec-command-with-sudo.ts
dkarski Sep 18, 2024
60e026e
[CP-2914] linting errors and warnings resolved from the main feature …
dkarski Sep 18, 2024
a289168
[CP-2914] testing errors resolved from the main feature branch
dkarski Sep 18, 2024
fed4646
[CP-2914] Refactor execCommandWithSudo function for cleaner error han…
dkarski Sep 18, 2024
927445f
Display modal with progression
MateuszMudita Sep 18, 2024
f8f6008
Merge branch 'CP-2914' of https://github.com/mudita/mudita-center int…
MateuszMudita Sep 18, 2024
1ccf78b
Fixed lint errors
MateuszMudita Sep 18, 2024
1c4e27d
[CP-2914] Device flashing workaround added for Linux (#2066)
MateuszMudita Sep 18, 2024
a18e07d
[CP-2914] Device flashing workaround added for Linux - vol.2 (#2071)
dkarski Sep 19, 2024
94c40c9
Added Restarting modal
MateuszMudita Sep 19, 2024
c61dadf
Fix typo
MateuszMudita Sep 19, 2024
289f54d
[CP-2914] MUDITA_CENTER_SERVER_URL to MUDITA_CENTER_SERVER_V2_URL
dkarski Sep 19, 2024
ca70033
[CP-2914] await deviceFlash.findDeviceByDeviceName("HARMONY MSC") to…
dkarski Sep 19, 2024
cdd8031
Remove downloaded files
MateuszMudita Sep 25, 2024
deb4daf
Added warning about Ubuntu version
MateuszMudita Sep 27, 2024
6d4994e
Merge branch 'CP-3027' of https://github.com/mudita/mudita-center int…
MateuszMudita Oct 10, 2024
48baf95
Fix lint errors
MateuszMudita Oct 10, 2024
5b9697d
Merge branch 'CP-3027' of https://github.com/mudita/mudita-center int…
MateuszMudita Oct 10, 2024
f5e1c7e
Fixed errors
MateuszMudita Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion libs/core/__deprecated__/renderer/locales/default/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -998,13 +998,24 @@
"module.genericViews.dataMigration.success.buttonLabel": "Ok",
"module.recoveryMode.harmony.title": "Recovery Mode",
"module.recoveryMode.harmony.header": "Harmony is Ready for Recovery",
"module.recoveryMode.harmony.description": "Please read the instructions carefully. Not following the instructions may void the\nwarrenty!",
"module.recoveryMode.harmony.description": "Please read the instructions carefully. Not following the instructions may void the\nwarranty!",
"module.recoveryMode.harmony.warning1": "Make sure your device is running <b>OS 1.9.0 or later.</b>",
"module.recoveryMode.harmony.warning2": "Once you start the recovery, it must not be cancelled or interrupted.",
"module.recoveryMode.harmony.warning3": "Do not disconnect your Harmony during the recovery process.",
"module.recoveryMode.harmony.warning4": "Before starting the recovery, charge your device for 1 hour (or more) from a suitable power outlet.",
"module.recoveryMode.harmony.warningLinux": "Make sure your computer is running <b>Ubuntu 22.04 or later.</b>",
"module.recoveryMode.harmony.confirmation": "I understand that not following these instructions may void the warranty",
"module.recoveryMode.harmony.action": "Start Recovery",
"module.recoveryMode.modal.message": "Recovery mode in progress...",
"module.recoveryMode.modal.warning": "Warning!",
"module.recoveryMode.modal.warningMessage": "Do not disconnect Harmony or interrupt the process!",
"module.recoveryMode.modal.step0": "Initializing...",
"module.recoveryMode.modal.step1": "Configuration downloading... (1 of 4)",
"module.recoveryMode.modal.step2": "Image downloading... (2 of 4)",
"module.recoveryMode.modal.step3": "Image unpacking... (3 of 4)",
"module.recoveryMode.modal.step4": "Flashing process... (4 of 4)",
"module.recoveryMode.modal.restarting.subtitle": "Restarting Harmony...",
"module.recoveryMode.modal.restarting.message": "Please do not disconnect your Harmony.",
"module.genericViews.dataMigration.cancelConfirm.title": "Cancel data transfer?",
"module.genericViews.dataMigration.cancelConfirm.description": "We’ll stop the transfer but some data may already be on your Kompakt.",
"module.genericViews.dataMigration.cancelConfirm.cancelButtonLabel": "Cancel transfer",
Expand Down
17 changes: 6 additions & 11 deletions libs/core/core/components/apps/base-app/base-app-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,9 @@ import AvailableDeviceListContainer from "Core/discovery-device/components/avail
import DeviceConnecting from "Core/discovery-device/components/device-connecting.component"
import ManageSounds from "Core/files-manager/components/manage-sounds.component"
import { GenericView } from "generic-view/feature"
import {
APIConnectionDemo,
DataMigrationPage,
RecoveryModePage,
} from "generic-view/ui"
import { APIConnectionDemo, DataMigrationPage } from "generic-view/ui"
import { ArticlePage, HelpPage } from "help/ui"
import { RecoveryModePage } from "msc-flash-harmony"

// AUTO DISABLED - fix me if you like :)
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
Expand Down Expand Up @@ -134,17 +131,15 @@ export default () => (
key="help-category-article"
path={`${URL_MAIN.help}/:categoryId/:articleId`}
component={ArticlePage}
/>,
/>
,
<Route
key="help-category"
path={`${URL_MAIN.help}/:categoryId`}
component={HelpPage}
/>,
<Route
key="help-root"
path={URL_MAIN.help}
component={HelpPage}
/>
,
<Route key="help-root" path={URL_MAIN.help} component={HelpPage} />
<Route
path={`${URL_MAIN.settings}${URL_TABS.about}`}
component={AboutContainer}
Expand Down
2 changes: 2 additions & 0 deletions libs/core/core/styles/theming/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const transparentBlack3 = "rgba(0, 0, 0, 0.3)"
const red = "#e96a6a"
const green = "#dfefde"
const orange = "#FD9900"
const darkOrange = "#DD802A"

const white = "#ffffff"

Expand Down Expand Up @@ -78,6 +79,7 @@ const theme = {
tetheringSeparator: blue3,
deviceListSeparator: grey5,
deviceListSeparatorHover: grey4,
warning: darkOrange,
},
boxShadow: {
full: transparentBlack2,
Expand Down
18 changes: 8 additions & 10 deletions libs/core/update/reducers/update-os.reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,15 @@ import {
} from "Core/__deprecated__/renderer/store"
import { CheckForUpdateState } from "../constants/check-for-update-state.constant"

const mockHistory = {
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
block: jest.fn(),
listen: jest.fn(),
location: { pathname: "", search: "", hash: "", state: null },
}

jest.mock("history", () => ({
createHashHistory: jest.fn(() => mockHistory),
createHashHistory: jest.fn(() => ({
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
block: jest.fn(),
listen: jest.fn(),
location: { pathname: "", search: "", hash: "", state: null },
})),
}))

const exampleError = new AppError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum FlashingProcessState {
DownloadingFiles = "downloading-files",
UnpackingFiles = "unpacking-files",
FlashingProcess = "flashing-process",
Restarting = "restarting-device",
Completed = "completed",
Failed = "failed",
}
6 changes: 6 additions & 0 deletions libs/msc-flash/msc-flash-harmony/src/lib/selectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

export * from "./msc-flash.selector"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import { createSelector } from "@reduxjs/toolkit"
import { ReduxRootState } from "Core/__deprecated__/renderer/store"
import { FlashingProcessState } from "../constants"

export const selectFlashingProcessState = createSelector(
(state: ReduxRootState) => state.flashing.processState,
(processState: FlashingProcessState) => processState
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import IDeviceFlash from "./device-flash.interface"
import LinuxDeviceFlashService from "./linux-device-flash.service"

class DeviceFlashFactory {
static createDeviceFlashService(): IDeviceFlash {
const platform = process.platform

if (platform === "linux") {
return new LinuxDeviceFlashService()
} else {
throw new Error(`Unsupported platform: ${platform}`)
}
}
}

export default DeviceFlashFactory
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

interface IDeviceFlash {
findDeviceByDeviceName(deviceName?: string): Promise<string>

execute(device: string, imagePath: string, scriptPath: string): Promise<void>
}

export default IDeviceFlash
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import path from "path"
import { execPromise, execCommandWithSudo } from "shared/utils"
import IDeviceFlash from "./device-flash.interface"
import LinuxPartitionParser from "./linux-partition-parser"

class LinuxDeviceFlashService implements IDeviceFlash {
async findDeviceByDeviceName(deviceName: string): Promise<string> {
console.log(`Searching for device with model name: ${deviceName}`)
const devices = await this.getDevices()

for (const device of devices) {
if (device.includes(deviceName)) {
return device.split(" ")[0]
}
}
console.error(`${deviceName} device model not found`)
process.exit(1)
}

async execute(
device: string,
imagePath: string,
scriptPath: string
): Promise<void> {
const unmountDeviceCommand = await this.getUnmountDeviceCommand(device)
const flashImageToDeviceCommand = await this.getFlashImageToDeviceCommand(
device,
imagePath,
scriptPath
)
const ejectDeviceCommand = await this.getEjectDeviceCommand(device)

const command = `${unmountDeviceCommand} && ${flashImageToDeviceCommand} && ${ejectDeviceCommand}`
await execCommandWithSudo(command, { name: "Mudita Auto Flash" })

console.log("Flash process completed successfully")
}

private async getUnmountDeviceCommand(device: string): Promise<string> {
const partitions = await this.getPartitions(device)
const partitionsString = partitions
.map((partition) => `/dev/${partition}`)
.join(" ")

return `umount ${partitionsString} 2>/dev/null || true`
}

private async getFlashImageToDeviceCommand(
device: string,
imagePath: string,
scriptPath: string
): Promise<string> {
const [path, scriptBasename] =
this.splitPathToDirnameAndBasename(scriptPath)
const [, imageBasename] = this.splitPathToDirnameAndBasename(imagePath)
return `chmod +x ${scriptPath} && cd ${path} && ./${scriptBasename} ${imageBasename} /dev/${device}`
}

private async getEjectDeviceCommand(device: string): Promise<string> {
return `eject /dev/${device} 2>/dev/null || true`
}

private async getDevices(): Promise<string[]> {
const devices = await execPromise("lsblk -o NAME,MODEL")

return devices?.split("\n") ?? []
}

private async getPartitions(device: string): Promise<string[]> {
const partitions = await execPromise(
`lsblk /dev/${device} -o NAME,MOUNTPOINT`
)
return LinuxPartitionParser.parsePartitions(partitions ?? "")
}

private splitPathToDirnameAndBasename(currentPath: string) {
const dirname = path.dirname(currentPath)
const basename = path.basename(currentPath)
return [dirname, basename]
}
}

export default LinuxDeviceFlashService
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

import LinuxPartitionParser from "./linux-partition-parser"

describe("LinuxPartitionParser", () => {
test("correctly parses partitions from 'sdb' device output", () => {
const execOutput = `
NAME MOUNTPOINT
sdb
├─sdb1 /media/system_a
├─sdb2 /media/system_b
└─sdb3 /media/user
`
const result = LinuxPartitionParser.parsePartitions(execOutput)
expect(result).toEqual(["sdb1", "sdb2", "sdb3"])
})

test("correctly parses partitions from a device with a custom name", () => {
const execOutput = `
NAME MOUNTPOINT
nvme0n1
├─nvme0n1p1 /boot
├─nvme0n1p2 /
└─nvme0n1p3 /home
`
const result = LinuxPartitionParser.parsePartitions(execOutput)
expect(result).toEqual(["nvme0n1p1", "nvme0n1p2", "nvme0n1p3"])
})

test("returns an empty array when no partitions are found", () => {
const execOutput = `
NAME MOUNTPOINT
sdb
`
const result = LinuxPartitionParser.parsePartitions(execOutput)
expect(result).toEqual([])
})

test("correctly handles irregular formatting", () => {
const execOutput = `
NAME MOUNTPOINT
sdb
├─ sdb1 /media/system_a
├─sdb2 /media/system_b
└─sdb3 /media/user
`
const result = LinuxPartitionParser.parsePartitions(execOutput)
expect(result).toEqual(["sdb1", "sdb2", "sdb3"])
})

test("correctly parses partitions when mountpoints are missing", () => {
const execOutput = `
NAME MOUNTPOINT
sdb
├─sdb1
├─sdb2
└─sdb3
`
const result = LinuxPartitionParser.parsePartitions(execOutput)
expect(result).toEqual(["sdb1", "sdb2", "sdb3"])
})

test('ignores non-partition lines like "NAME" and "MOUNTPOINT"', () => {
const execOutput = `
NAME MOUNTPOINT
sdb
├─sdb1 /media/system_a
├─sdb2 /media/system_b
NAME
MOUNTPOINT
└─sdb3 /media/user
`
const result = LinuxPartitionParser.parsePartitions(execOutput)
expect(result).toEqual(["sdb1", "sdb2", "sdb3"])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/

export class LinuxPartitionParser {
public static parsePartitions(execOutput: string): string[] {
const partitions: string[] = [];
const lines = execOutput.split("\n");

let deviceName: string | null = null;

for (const rawLine of lines) {
const line = rawLine.trim();

if (!line || line.startsWith("NAME") || line.startsWith("MOUNTPOINT")) {
continue;
}

const name = LinuxPartitionParser.extractName(line);

if (!name) {
continue;
}

if (!deviceName) {
deviceName = name;
continue;
}

if (name !== deviceName) {
partitions.push(name);
}
}

return partitions;
}

private static extractName(line: string): string | null {
const match = /^\W*(\w+)/.exec(line);
return match ? match[1] : null;
}
}

export default LinuxPartitionParser;
Loading
Loading