284 lines
7.9 KiB
TypeScript
284 lines
7.9 KiB
TypeScript
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
|
import { err, getMessageFromError, ok } from "@shared/utils/result.ts";
|
|
import { errAsync } from "@shared/utils/index.ts";
|
|
import log from "@shared/utils/logger.ts";
|
|
import {
|
|
fromNullableVal,
|
|
none,
|
|
type Option,
|
|
some,
|
|
} from "@shared/utils/option.ts";
|
|
|
|
class CommandExecutionError extends Error {
|
|
code = "CommandExecutionError";
|
|
constructor(msg: string) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
class DeviceDoesNotExistError extends Error {
|
|
code = "DeviceDoesNotExist";
|
|
constructor(msg: string) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
class DeviceAlreadyBoundError extends Error {
|
|
code = "DeviceAlreadyBound";
|
|
constructor(msg: string) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
class DeviceNotBound extends Error {
|
|
code = "DeviceNotBound";
|
|
constructor(msg: string) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
class UsbipUknownError extends Error {
|
|
code = "UsbipUknownError";
|
|
constructor(msg: string) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
type UsbipCommonError = DeviceDoesNotExistError | UsbipUknownError;
|
|
|
|
class UsbipManager {
|
|
private readonly listDeatiledCmd = new Deno.Command("usbip", {
|
|
args: ["list", "-l"],
|
|
});
|
|
private readonly listParsableCmd = new Deno.Command("usbip", {
|
|
args: ["list", "-pl"],
|
|
});
|
|
private readonly decoder = new TextDecoder();
|
|
|
|
private readonly usbidRegex = /[0-9abcdef]{4}:[0-9abcdef]{4}/;
|
|
private readonly busidRegex =
|
|
/(?:[0-9]+(?:\.[0-9]+)*-)*[0-9]+(?:\.[0-9]+)*/;
|
|
|
|
private executeCommand(
|
|
cmd: Deno.Command,
|
|
): ResultAsync<CommandOutput, CommandExecutionError> {
|
|
const promise = cmd.output();
|
|
|
|
return ResultAsync.fromPromise(
|
|
promise,
|
|
(e) => new CommandExecutionError(getMessageFromError(e)),
|
|
)
|
|
.map(({ stdout, stderr, code }) =>
|
|
new CommandOutput(
|
|
this.decoder.decode(stdout).trim(),
|
|
this.decoder.decode(stderr).trim(),
|
|
code,
|
|
)
|
|
);
|
|
}
|
|
|
|
private handleCommonErrors(stderr: string): UsbipCommonError {
|
|
if (
|
|
stderr.includes("device with the specified bus ID does not exist")
|
|
) {
|
|
return new DeviceDoesNotExistError(stderr);
|
|
}
|
|
|
|
return new UsbipUknownError(stderr);
|
|
}
|
|
|
|
private parseDetailedList(stdout: string): Option<DeviceDetailed[]> {
|
|
const devices: DeviceDetailed[] = [];
|
|
|
|
const deviceEntries = stdout.trim().split("\n\n");
|
|
|
|
for (const deviceEntry of deviceEntries) {
|
|
const busid = deviceEntry.match(this.busidRegex)?.shift();
|
|
|
|
if (!busid) {
|
|
log.error(
|
|
`Failed to parse busid of a device:\n ${deviceEntry}`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
const usbid = fromNullableVal(
|
|
deviceEntry.match(this.usbidRegex)?.shift(),
|
|
);
|
|
|
|
const [_, line2] = deviceEntry.split("\n");
|
|
|
|
const [vendorVal, nameVal] = line2
|
|
? line2.split(" : ").map((s) => s.trim())
|
|
: [undefined, undefined];
|
|
|
|
const vendor = fromNullableVal(vendorVal);
|
|
const name = nameVal
|
|
? some(
|
|
nameVal.replace(
|
|
usbid.isSome() ? usbid.value : this.usbidRegex,
|
|
"",
|
|
).replace("()", "")
|
|
.trim(),
|
|
)
|
|
: none;
|
|
|
|
[["usbid", usbid], ["vendor", vendor], ["name", name]].filter((v) =>
|
|
(v[1] as Option<string>).isNone()
|
|
).map((v) => log.warn(`Failed to parse ${v[0]}:\n ${deviceEntry}`));
|
|
|
|
devices.push({
|
|
busid,
|
|
usbid,
|
|
vendor,
|
|
name,
|
|
});
|
|
}
|
|
|
|
return devices.length > 0 ? some(devices) : none;
|
|
}
|
|
|
|
public getDevicesDetailed(): ResultAsync<
|
|
Option<DeviceDetailed[]>,
|
|
CommandExecutionError | UsbipUknownError
|
|
> {
|
|
return this.executeCommand(this.listDeatiledCmd).andThen(
|
|
({ stdout, stderr, success }) => {
|
|
if (success) {
|
|
if (stderr) {
|
|
log.warn(
|
|
`usbip list -l succeeded but encountered an error: ${stderr}`,
|
|
);
|
|
}
|
|
return ok(this.parseDetailedList(stdout));
|
|
}
|
|
|
|
return err(new UsbipUknownError(stderr));
|
|
},
|
|
);
|
|
}
|
|
|
|
private parseParsableList(stdout: string): Option<Device[]> {
|
|
const devices: Device[] = [];
|
|
|
|
const devicesEntries = stdout.trim().split("\n");
|
|
|
|
for (const deviceEntry of devicesEntries) {
|
|
const [busid, usbid] = deviceEntry
|
|
.slice(0, -1)
|
|
.split("#")
|
|
.map((v) => v.split("=")[1].trim() || undefined);
|
|
|
|
if (!busid) {
|
|
log.error(
|
|
`Failed to parse busid of a device:\n ${deviceEntry}`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (!usbid) {
|
|
log.warn(
|
|
`Failed to parse usbid of a device:\n ${deviceEntry}`,
|
|
);
|
|
}
|
|
|
|
devices.push({
|
|
busid,
|
|
usbid: fromNullableVal(usbid),
|
|
});
|
|
}
|
|
|
|
return devices.length > 0 ? some(devices) : none;
|
|
}
|
|
|
|
public getDevices(): ResultAsync<
|
|
Option<Device[]>,
|
|
CommandExecutionError | UsbipUknownError
|
|
> {
|
|
return this.executeCommand(this.listParsableCmd).andThenAsync(
|
|
({ stdout, stderr, success }) => {
|
|
if (success) {
|
|
if (stderr) {
|
|
log.warn(
|
|
`usbip list -lp succeeded but encountered an error: ${stderr}`,
|
|
);
|
|
}
|
|
return okAsync(this.parseParsableList(stdout));
|
|
}
|
|
return errAsync(new UsbipUknownError(stderr));
|
|
},
|
|
);
|
|
}
|
|
|
|
public bindDevice(
|
|
busid: string,
|
|
): ResultAsync<
|
|
string,
|
|
UsbipCommonError | DeviceAlreadyBoundError | CommandExecutionError
|
|
> {
|
|
const cmd = new Deno.Command("usbip", { args: ["bind", "-b", busid] });
|
|
|
|
return this.executeCommand(cmd).andThen(
|
|
({ stderr, success }) => {
|
|
if (success) {
|
|
return ok(stderr.trim() || "Device bound successfully");
|
|
}
|
|
|
|
if (stderr.includes("is already bound to usbip-host")) {
|
|
return err(new DeviceAlreadyBoundError(stderr));
|
|
}
|
|
|
|
return err(this.handleCommonErrors(stderr));
|
|
},
|
|
);
|
|
}
|
|
|
|
public unbindDevice(
|
|
busid: string,
|
|
): ResultAsync<
|
|
string,
|
|
CommandExecutionError | DeviceNotBound | UsbipCommonError
|
|
> {
|
|
const cmd = new Deno.Command("usbip", {
|
|
args: ["unbind", "-b", busid],
|
|
});
|
|
|
|
return this.executeCommand(cmd).andThen(({ stderr, success }) => {
|
|
if (success) {
|
|
return ok(stderr.trim() || "Device unbound successfully");
|
|
}
|
|
|
|
if (stderr.includes("device is not bound to usbip-host driver")) {
|
|
return err(new DeviceNotBound(stderr));
|
|
}
|
|
|
|
return err(this.handleCommonErrors(stderr));
|
|
});
|
|
}
|
|
}
|
|
|
|
class CommandOutput {
|
|
constructor(
|
|
public readonly stdout: string,
|
|
public readonly stderr: string,
|
|
public readonly code: number,
|
|
) {}
|
|
get success(): boolean {
|
|
return this.code === 0;
|
|
}
|
|
}
|
|
|
|
interface DeviceDetailed {
|
|
busid: string;
|
|
usbid: Option<string>;
|
|
vendor: Option<string>;
|
|
name: Option<string>;
|
|
}
|
|
|
|
interface Device {
|
|
busid: string;
|
|
usbid: Option<string>;
|
|
}
|
|
|
|
export default UsbipManager;
|