mirror of
https://github.com/MCSManager/MCSManager.git
synced 2025-04-06 17:10:29 +08:00
Refactor: update action with docker
This commit is contained in:
parent
ef63ae4528
commit
1349d31042
@ -1,6 +1,6 @@
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { DockerProcessAdapter } from "./docker_start";
|
||||
import { DockerProcessAdapter } from "../../../service/docker_process_service";
|
||||
|
||||
export default class DockerResizeCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
|
@ -6,87 +6,13 @@ import logger from "../../../service/log";
|
||||
import { EventEmitter } from "events";
|
||||
import { IInstanceProcess } from "../../instance/interface";
|
||||
import fs from "fs-extra";
|
||||
import { commandStringToArray } from "../base/command_parser";
|
||||
import path from "path";
|
||||
import { t } from "i18next";
|
||||
import DockerPullCommand from "./docker_pull";
|
||||
import os from "os";
|
||||
|
||||
// user identity function
|
||||
const processUserUid = process.getuid ? process.getuid : () => 0;
|
||||
const processGroupGid = process.getgid ? process.getgid : () => 0;
|
||||
|
||||
// Error exception at startup
|
||||
class StartupDockerProcessError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
interface IDockerProcessAdapterStartParam {
|
||||
isTty: boolean;
|
||||
h: number;
|
||||
w: number;
|
||||
}
|
||||
|
||||
// process adapter
|
||||
export class DockerProcessAdapter extends EventEmitter implements IInstanceProcess {
|
||||
pid?: number | string;
|
||||
|
||||
private stream?: NodeJS.ReadWriteStream;
|
||||
|
||||
constructor(public container: Docker.Container) {
|
||||
super();
|
||||
}
|
||||
|
||||
// Once the program is actually started, no errors can block the next startup process
|
||||
public async start(param: IDockerProcessAdapterStartParam) {
|
||||
try {
|
||||
await this.container.start();
|
||||
|
||||
const { isTty, h, w } = param;
|
||||
if (isTty) {
|
||||
this.container.resize({ h, w });
|
||||
}
|
||||
|
||||
this.pid = this.container.id;
|
||||
const stream = (this.stream = await this.container.attach({
|
||||
stream: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
stdin: true
|
||||
}));
|
||||
stream.on("data", (data) => this.emit("data", data));
|
||||
stream.on("error", (data) => this.emit("data", data));
|
||||
this.wait();
|
||||
} catch (error: any) {
|
||||
this.kill();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public write(data?: string) {
|
||||
if (this.stream && data) this.stream.write(data);
|
||||
}
|
||||
|
||||
public async kill(s?: string) {
|
||||
await this.container.kill();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
try {
|
||||
await this.container.remove();
|
||||
} catch (error: any) {}
|
||||
}
|
||||
|
||||
private wait() {
|
||||
this.container.wait(async (v) => {
|
||||
await this.destroy();
|
||||
this.emit("exit", v);
|
||||
});
|
||||
}
|
||||
}
|
||||
import {
|
||||
DockerProcessAdapter,
|
||||
SetupDockerContainer,
|
||||
StartupDockerProcessError
|
||||
} from "../../../service/docker_process_service";
|
||||
|
||||
export default class DockerStartCommand extends InstanceCommand {
|
||||
constructor() {
|
||||
@ -106,146 +32,13 @@ export default class DockerStartCommand extends InstanceCommand {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Command text parsing
|
||||
let commandList: string[] = [];
|
||||
if (instance.config.startCommand.trim()) {
|
||||
commandList = commandStringToArray(instance.config.startCommand);
|
||||
} else {
|
||||
commandList = [];
|
||||
}
|
||||
|
||||
const cwd = instance.absoluteCwdPath();
|
||||
|
||||
// Parsing port open
|
||||
// 25565:25565/tcp 8080:8080/tcp
|
||||
const portMap = instance.config.docker.ports || [];
|
||||
const publicPortArray: any = {};
|
||||
const exposedPorts: any = {};
|
||||
for (const iterator of portMap) {
|
||||
const elem = iterator.split("/");
|
||||
if (elem.length != 2) throw new Error(t("TXT_CODE_1cf6fc4b"));
|
||||
const ports = elem[0];
|
||||
const protocol = elem[1];
|
||||
//Host (host) port: container port
|
||||
const publicAndPrivatePort = ports.split(":");
|
||||
if (publicAndPrivatePort.length != 2) throw new Error(t("TXT_CODE_2029027e"));
|
||||
publicPortArray[`${publicAndPrivatePort[1]}/${protocol}`] = [
|
||||
{ HostPort: publicAndPrivatePort[0] }
|
||||
];
|
||||
exposedPorts[`${publicAndPrivatePort[1]}/${protocol}`] = {};
|
||||
}
|
||||
|
||||
// resolve extra path mounts
|
||||
const extraVolumes = instance.config.docker.extraVolumes || [];
|
||||
const extraBinds: { hostPath: string; containerPath: string }[] = [];
|
||||
for (const item of extraVolumes) {
|
||||
if (!item) throw new Error($t("TXT_CODE_ae441ea3"));
|
||||
const paths = item.split("|");
|
||||
if (paths.length < 2) throw new Error($t("TXT_CODE_dca030b8"));
|
||||
const hostPath = path.normalize(paths[0]);
|
||||
const containerPath = path.normalize(paths[1]);
|
||||
extraBinds.push({ hostPath, containerPath });
|
||||
}
|
||||
|
||||
// memory limit
|
||||
let maxMemory = undefined;
|
||||
if (instance.config.docker.memory) maxMemory = instance.config.docker.memory * 1024 * 1024;
|
||||
|
||||
// CPU usage calculation
|
||||
let cpuQuota = undefined;
|
||||
let cpuPeriod = undefined;
|
||||
if (instance.config.docker.cpuUsage) {
|
||||
cpuQuota = instance.config.docker.cpuUsage * 10 * 1000;
|
||||
cpuPeriod = 1000 * 1000;
|
||||
}
|
||||
|
||||
// Check the number of CPU cores
|
||||
let cpusetCpus = undefined;
|
||||
if (instance.config.docker.cpusetCpus) {
|
||||
const arr = instance.config.docker.cpusetCpus.split(",");
|
||||
arr.forEach((v) => {
|
||||
if (isNaN(Number(v))) throw new Error($t("TXT_CODE_instance.invalidCpu", { v }));
|
||||
});
|
||||
cpusetCpus = instance.config.docker.cpusetCpus;
|
||||
// Note: check
|
||||
}
|
||||
|
||||
// container name check
|
||||
let containerName =
|
||||
instance.config.docker.containerName || `MCSM-${instance.instanceUuid.slice(0, 6)}`;
|
||||
if (containerName && (containerName.length > 64 || containerName.length < 2)) {
|
||||
throw new Error($t("TXT_CODE_instance.invalidContainerName", { v: containerName }));
|
||||
}
|
||||
|
||||
// Whether to use TTY mode
|
||||
const isTty = instance.config.terminalOption.pty;
|
||||
const workingDir = instance.config.docker.workingDir ?? "/workspace/";
|
||||
|
||||
// output startup log
|
||||
logger.info("----------------");
|
||||
logger.info(`Session ${source}: Request to start an instance`);
|
||||
logger.info(`UUID: [${instance.instanceUuid}] [${instance.config.nickname}]`);
|
||||
logger.info(`NAME: [${containerName}]`);
|
||||
logger.info(`COMMAND: ${commandList.join(" ")}`);
|
||||
logger.info(`CWD: ${cwd}, WORKING_DIR: ${workingDir}`);
|
||||
logger.info(`NET_MODE: ${instance.config.docker.networkMode}`);
|
||||
logger.info(`OPEN_PORT: ${JSON.stringify(publicPortArray)}`);
|
||||
logger.info(`BINDS: ${JSON.stringify([`${cwd}->${workingDir}`, ...extraBinds])}`);
|
||||
logger.info(`NET_ALIASES: ${JSON.stringify(instance.config.docker.networkAliases)}`);
|
||||
logger.info(`MEM_LIMIT: ${maxMemory || "--"} MB`);
|
||||
logger.info(`TYPE: Docker Container`);
|
||||
logger.info("----------------");
|
||||
|
||||
// Start Docker container creation and running
|
||||
const docker = new Docker();
|
||||
const container = await docker.createContainer({
|
||||
name: containerName,
|
||||
Hostname: containerName,
|
||||
Image: instance.config.docker.image,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: isTty,
|
||||
WorkingDir: workingDir,
|
||||
Cmd: commandList ? commandList : undefined,
|
||||
OpenStdin: true,
|
||||
StdinOnce: false,
|
||||
ExposedPorts: exposedPorts,
|
||||
Env: instance.config.docker?.env || [],
|
||||
HostConfig: {
|
||||
Memory: maxMemory,
|
||||
AutoRemove: true,
|
||||
CpusetCpus: cpusetCpus,
|
||||
CpuPeriod: cpuPeriod,
|
||||
CpuQuota: cpuQuota,
|
||||
PortBindings: publicPortArray,
|
||||
NetworkMode: instance.config.docker.networkMode,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: cwd,
|
||||
Target: workingDir
|
||||
},
|
||||
...extraBinds.map((v) => {
|
||||
return {
|
||||
Type: "bind" as Docker.MountType,
|
||||
Source: v.hostPath,
|
||||
Target: v.containerPath
|
||||
};
|
||||
})
|
||||
]
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
[instance.config.docker.networkMode || "bridge"]: {
|
||||
Aliases: instance.config.docker.networkAliases
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const containerWrapper = new SetupDockerContainer(instance);
|
||||
await containerWrapper.start();
|
||||
|
||||
// Docker docks to the process adapter
|
||||
const processAdapter = new DockerProcessAdapter(container);
|
||||
const isTty = instance.config.terminalOption.pty;
|
||||
const workingDir = instance.config.docker.workingDir ?? "/workspace/";
|
||||
const processAdapter = new DockerProcessAdapter(containerWrapper.getContainer());
|
||||
await processAdapter.start({
|
||||
isTty,
|
||||
w: instance.config.terminalOption.ptyWindowCol,
|
||||
|
@ -1,93 +1,17 @@
|
||||
import { $t } from "../../../i18n";
|
||||
import { killProcess } from "common";
|
||||
import { ChildProcess, ChildProcessWithoutNullStreams, exec, spawn } from "child_process";
|
||||
import { ChildProcess } from "child_process";
|
||||
import logger from "../../../service/log";
|
||||
import Instance from "../../instance/instance";
|
||||
import InstanceCommand from "../base/command";
|
||||
import { commandStringToArray } from "../base/command_parser";
|
||||
import iconv from "iconv-lite";
|
||||
import { AsyncTask, IAsyncTaskJSON } from "../../../service/async_task_service";
|
||||
|
||||
export class InstanceUpdateAction extends AsyncTask {
|
||||
public pid?: number;
|
||||
public process?: ChildProcessWithoutNullStreams;
|
||||
|
||||
constructor(public readonly instance: Instance) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async onStart() {
|
||||
let updateCommand = this.instance.config.updateCommand;
|
||||
updateCommand = updateCommand.replace(/\{mcsm_workspace\}/gm, this.instance.config.cwd);
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_update.updateCmd", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
logger.info(updateCommand);
|
||||
|
||||
this.instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
|
||||
// command parsing
|
||||
const commandList = commandStringToArray(updateCommand);
|
||||
const commandExeFile = commandList[0];
|
||||
const commandParameters = commandList.slice(1);
|
||||
if (commandList.length === 0) {
|
||||
return this.instance.failure(new Error($t("TXT_CODE_general_update.cmdFormatErr")));
|
||||
}
|
||||
|
||||
// start the update command
|
||||
const process = spawn(commandExeFile, commandParameters, {
|
||||
cwd: this.instance.config.cwd,
|
||||
stdio: "pipe",
|
||||
windowsHide: true
|
||||
});
|
||||
if (!process || !process.pid) {
|
||||
return this.instance.println(
|
||||
$t("TXT_CODE_general_update.err"),
|
||||
$t("TXT_CODE_general_update.updateFailed")
|
||||
);
|
||||
}
|
||||
|
||||
// process & pid
|
||||
this.pid = process.pid;
|
||||
this.process = process;
|
||||
|
||||
process.stdout.on("data", (text) => {
|
||||
this.instance.print(iconv.decode(text, this.instance.config.oe));
|
||||
});
|
||||
process.stderr.on("data", (text) => {
|
||||
this.instance.print(iconv.decode(text, this.instance.config.oe));
|
||||
});
|
||||
|
||||
process.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.error(new Error($t("TXT_CODE_general_update.updateErr")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async onStop() {
|
||||
if (this.pid && this.process) {
|
||||
killProcess(this.pid, this.process);
|
||||
}
|
||||
}
|
||||
|
||||
public onError(err: Error): void {}
|
||||
public toObject(): IAsyncTaskJSON {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
import Docker from "dockerode";
|
||||
import { SetupDockerContainer } from "../../../service/docker_process_service";
|
||||
import { InstanceUpdateAction } from "../../../service/instance_update_action";
|
||||
|
||||
export default class GeneralUpdateCommand extends InstanceCommand {
|
||||
private pid?: number;
|
||||
private process?: ChildProcess;
|
||||
private container?: Docker.Container;
|
||||
|
||||
constructor() {
|
||||
super("GeneralUpdateCommand");
|
||||
@ -110,9 +34,18 @@ export default class GeneralUpdateCommand extends InstanceCommand {
|
||||
instance.asynchronousTask = this;
|
||||
instance.status(Instance.STATUS_BUSY);
|
||||
|
||||
const instanceUpdateAction = new InstanceUpdateAction(instance);
|
||||
await instanceUpdateAction.start();
|
||||
await instanceUpdateAction.wait();
|
||||
if (instance.config.docker.image) {
|
||||
// Docker Update Command Mode
|
||||
const containerWrapper = new SetupDockerContainer(instance);
|
||||
await containerWrapper.start();
|
||||
await containerWrapper.attach(instance);
|
||||
await containerWrapper.wait();
|
||||
} else {
|
||||
// Host Update Command Mode
|
||||
const instanceUpdateAction = new InstanceUpdateAction(instance);
|
||||
await instanceUpdateAction.start();
|
||||
await instanceUpdateAction.wait();
|
||||
}
|
||||
} catch (err: any) {
|
||||
instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
@ -136,8 +69,15 @@ export default class GeneralUpdateCommand extends InstanceCommand {
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.killProcess")
|
||||
);
|
||||
|
||||
if (instance.config.docker.image) {
|
||||
try {
|
||||
await this.container?.kill();
|
||||
} catch (error) {}
|
||||
return await this.container?.remove();
|
||||
}
|
||||
if (this.pid && this.process) {
|
||||
killProcess(this.pid, this.process);
|
||||
return killProcess(this.pid, this.process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
import EventEmitter from "events";
|
||||
import logger from "../log";
|
||||
export interface IAsyncTaskJSON {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type IAsyncTaskJSON = any;
|
||||
|
||||
export interface IAsyncTask extends EventEmitter {
|
||||
// The taskId must be complex enough to prevent other users from accessing the information
|
||||
taskId: string;
|
||||
type: string;
|
||||
start(): Promise<boolean | void>;
|
||||
stop(): Promise<boolean | void>;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
status(): number;
|
||||
toObject(): IAsyncTaskJSON;
|
||||
}
|
||||
@ -28,35 +27,31 @@ export abstract class AsyncTask extends EventEmitter implements IAsyncTask {
|
||||
super();
|
||||
}
|
||||
|
||||
public start() {
|
||||
public async start() {
|
||||
this._status = AsyncTask.STATUS_RUNNING;
|
||||
try {
|
||||
const r = this.onStart();
|
||||
await this.onStart();
|
||||
this.emit("started");
|
||||
return r;
|
||||
} catch (error: any) {
|
||||
this.error(error);
|
||||
return Promise.reject(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
public async stop() {
|
||||
if (this._status === AsyncTask.STATUS_STOP) return Promise.resolve();
|
||||
try {
|
||||
const r = this.onStop();
|
||||
return r;
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
await this.onStop();
|
||||
} finally {
|
||||
if (this._status !== AsyncTask.STATUS_ERROR) this._status = AsyncTask.STATUS_STOP;
|
||||
this.emit("stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public error(err: Error) {
|
||||
public async error(err: Error) {
|
||||
this._status = AsyncTask.STATUS_ERROR;
|
||||
logger.error(`AsyncTask - ID: ${this.taskId} TYPE: ${this.type} Error:`, err);
|
||||
this.onError(err);
|
||||
await this.onError(err);
|
||||
this.emit("error", err);
|
||||
this.stop();
|
||||
}
|
||||
@ -78,7 +73,7 @@ export abstract class AsyncTask extends EventEmitter implements IAsyncTask {
|
||||
|
||||
public abstract onStart(): Promise<void>;
|
||||
public abstract onStop(): Promise<void>;
|
||||
public abstract onError(err: Error): void;
|
||||
public abstract onError(err: Error): Promise<void>;
|
||||
public abstract toObject(): IAsyncTaskJSON;
|
||||
}
|
||||
|
||||
|
@ -5,16 +5,14 @@ import fs from "fs-extra";
|
||||
import Instance from "../../entity/instance/instance";
|
||||
import InstanceSubsystem from "../system_instance";
|
||||
import InstanceConfig from "../../entity/instance/Instance_config";
|
||||
import { $t, i18next } from "../../i18n";
|
||||
import { $t } from "../../i18n";
|
||||
import path from "path";
|
||||
import { getFileManager } from "../file_router_service";
|
||||
import { IAsyncTaskJSON, TaskCenter, AsyncTask } from "./index";
|
||||
import logger from "../log";
|
||||
import { t } from "i18next";
|
||||
import type { IJsonData } from "common/global";
|
||||
import GeneralUpdateCommand, {
|
||||
InstanceUpdateAction
|
||||
} from "../../entity/commands/general/general_update";
|
||||
import { InstanceUpdateAction } from "../instance_update_action";
|
||||
|
||||
export class QuickInstallTask extends AsyncTask {
|
||||
public static TYPE = "QuickInstallTask";
|
||||
@ -150,8 +148,6 @@ export class QuickInstallTask extends AsyncTask {
|
||||
} catch (error: any) {}
|
||||
}
|
||||
|
||||
onError(): void {}
|
||||
|
||||
toObject(): IAsyncTaskJSON {
|
||||
return JSON.parse(
|
||||
JSON.stringify({
|
||||
@ -163,6 +159,8 @@ export class QuickInstallTask extends AsyncTask {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async onError() {}
|
||||
}
|
||||
|
||||
export function createQuickInstallTask(
|
||||
|
285
daemon/src/service/docker_process_service.ts
Normal file
285
daemon/src/service/docker_process_service.ts
Normal file
@ -0,0 +1,285 @@
|
||||
import { t } from "i18next";
|
||||
import { commandStringToArray } from "../entity/commands/base/command_parser";
|
||||
import DockerPullCommand from "../entity/commands/docker/docker_pull";
|
||||
import Instance from "../entity/instance/instance";
|
||||
|
||||
import path from "path";
|
||||
import { $t } from "../i18n";
|
||||
import logger from "./log";
|
||||
import Docker from "dockerode";
|
||||
import { EventEmitter } from "stream";
|
||||
import { IInstanceProcess } from "../entity/instance/interface";
|
||||
import { AsyncTask } from "./async_task_service";
|
||||
import iconv from "iconv-lite";
|
||||
|
||||
// Error exception at startup
|
||||
export class StartupDockerProcessError extends Error {
|
||||
constructor(msg: string) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDockerProcessAdapterStartParam {
|
||||
isTty: boolean;
|
||||
h: number;
|
||||
w: number;
|
||||
}
|
||||
|
||||
export class SetupDockerContainer extends AsyncTask {
|
||||
private container?: Docker.Container;
|
||||
|
||||
constructor(public readonly instance: Instance, public readonly startCommand?: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async onStart() {
|
||||
const instance = this.instance;
|
||||
const customCommand = this.startCommand;
|
||||
// Docker Image check
|
||||
try {
|
||||
await instance.forceExec(new DockerPullCommand());
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Command text parsing
|
||||
let commandList: string[] = [];
|
||||
if (instance.config?.startCommand?.trim() || customCommand?.trim()) {
|
||||
commandList = commandStringToArray(customCommand ?? instance.config.startCommand);
|
||||
} else {
|
||||
commandList = [];
|
||||
}
|
||||
|
||||
const cwd = instance.absoluteCwdPath();
|
||||
|
||||
// Parsing port open
|
||||
// 25565:25565/tcp 8080:8080/tcp
|
||||
const portMap = instance.config.docker.ports || [];
|
||||
const publicPortArray: any = {};
|
||||
const exposedPorts: any = {};
|
||||
for (const iterator of portMap) {
|
||||
const elem = iterator.split("/");
|
||||
if (elem.length != 2) throw new Error($t("TXT_CODE_1cf6fc4b"));
|
||||
const ports = elem[0];
|
||||
const protocol = elem[1];
|
||||
//Host (host) port: container port
|
||||
const publicAndPrivatePort = ports.split(":");
|
||||
if (publicAndPrivatePort.length != 2) throw new Error(t("TXT_CODE_2029027e"));
|
||||
publicPortArray[`${publicAndPrivatePort[1]}/${protocol}`] = [
|
||||
{ HostPort: publicAndPrivatePort[0] }
|
||||
];
|
||||
exposedPorts[`${publicAndPrivatePort[1]}/${protocol}`] = {};
|
||||
}
|
||||
|
||||
// resolve extra path mounts
|
||||
const extraVolumes = instance.config.docker.extraVolumes || [];
|
||||
const extraBinds: { hostPath: string; containerPath: string }[] = [];
|
||||
for (const item of extraVolumes) {
|
||||
if (!item) throw new Error($t("TXT_CODE_ae441ea3"));
|
||||
const paths = item.split("|");
|
||||
if (paths.length < 2) throw new Error($t("TXT_CODE_dca030b8"));
|
||||
const hostPath = path.normalize(paths[0]);
|
||||
const containerPath = path.normalize(paths[1]);
|
||||
extraBinds.push({ hostPath, containerPath });
|
||||
}
|
||||
|
||||
// memory limit
|
||||
let maxMemory = undefined;
|
||||
if (instance.config.docker.memory) maxMemory = instance.config.docker.memory * 1024 * 1024;
|
||||
|
||||
// CPU usage calculation
|
||||
let cpuQuota = undefined;
|
||||
let cpuPeriod = undefined;
|
||||
if (instance.config.docker.cpuUsage) {
|
||||
cpuQuota = instance.config.docker.cpuUsage * 10 * 1000;
|
||||
cpuPeriod = 1000 * 1000;
|
||||
}
|
||||
|
||||
// Check the number of CPU cores
|
||||
let cpusetCpus = undefined;
|
||||
if (instance.config.docker.cpusetCpus) {
|
||||
const arr = instance.config.docker.cpusetCpus.split(",");
|
||||
arr.forEach((v) => {
|
||||
if (isNaN(Number(v))) throw new Error($t("TXT_CODE_instance.invalidCpu", { v }));
|
||||
});
|
||||
cpusetCpus = instance.config.docker.cpusetCpus;
|
||||
// Note: check
|
||||
}
|
||||
|
||||
// container name check
|
||||
let containerName =
|
||||
instance.config.docker.containerName || `MCSM-${instance.instanceUuid.slice(0, 6)}`;
|
||||
if (containerName && (containerName.length > 64 || containerName.length < 2)) {
|
||||
throw new Error($t("TXT_CODE_instance.invalidContainerName", { v: containerName }));
|
||||
}
|
||||
|
||||
// Whether to use TTY mode
|
||||
const isTty = instance.config.terminalOption.pty;
|
||||
const workingDir = instance.config.docker.workingDir ?? "/workspace/";
|
||||
|
||||
// output startup log
|
||||
logger.info("----------------");
|
||||
logger.info(`[SetupDockerContainer]`);
|
||||
logger.info(`UUID: [${instance.instanceUuid}] [${instance.config.nickname}]`);
|
||||
logger.info(`NAME: [${containerName}]`);
|
||||
logger.info(`COMMAND: ${commandList.join(" ")}`);
|
||||
logger.info(`CWD: ${cwd}, WORKING_DIR: ${workingDir}`);
|
||||
logger.info(`NET_MODE: ${instance.config.docker.networkMode}`);
|
||||
logger.info(`OPEN_PORT: ${JSON.stringify(publicPortArray)}`);
|
||||
logger.info(`BINDS: ${JSON.stringify([`${cwd}->${workingDir}`, ...extraBinds])}`);
|
||||
logger.info(`NET_ALIASES: ${JSON.stringify(instance.config.docker.networkAliases)}`);
|
||||
logger.info(`MEM_LIMIT: ${maxMemory || "--"} MB`);
|
||||
logger.info(`TYPE: Docker Container`);
|
||||
logger.info("----------------");
|
||||
|
||||
// Start Docker container creation and running
|
||||
const docker = new Docker();
|
||||
this.container = await docker.createContainer({
|
||||
name: containerName,
|
||||
Hostname: containerName,
|
||||
Image: instance.config.docker.image,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: isTty,
|
||||
WorkingDir: workingDir,
|
||||
Cmd: commandList ? commandList : undefined,
|
||||
OpenStdin: true,
|
||||
StdinOnce: false,
|
||||
ExposedPorts: exposedPorts,
|
||||
Env: instance.config.docker?.env || [],
|
||||
HostConfig: {
|
||||
Memory: maxMemory,
|
||||
AutoRemove: true,
|
||||
CpusetCpus: cpusetCpus,
|
||||
CpuPeriod: cpuPeriod,
|
||||
CpuQuota: cpuQuota,
|
||||
PortBindings: publicPortArray,
|
||||
NetworkMode: instance.config.docker.networkMode,
|
||||
Mounts: [
|
||||
{
|
||||
Type: "bind",
|
||||
Source: cwd,
|
||||
Target: workingDir
|
||||
},
|
||||
...extraBinds.map((v) => {
|
||||
return {
|
||||
Type: "bind" as Docker.MountType,
|
||||
Source: v.hostPath,
|
||||
Target: v.containerPath
|
||||
};
|
||||
})
|
||||
]
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
[instance.config.docker.networkMode || "bridge"]: {
|
||||
Aliases: instance.config.docker.networkAliases
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to events
|
||||
this.container.wait(async (v) => {
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
public async onStop() {
|
||||
try {
|
||||
await this.container?.kill();
|
||||
} catch (error) {}
|
||||
try {
|
||||
await this.container?.remove();
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
public getContainer() {
|
||||
if (!this.container) throw new Error("Function getContainer(): Failed, Container is Null!");
|
||||
return this.container;
|
||||
}
|
||||
|
||||
public async attach(instance: Instance) {
|
||||
const outputCode = instance.config.terminalOption.pty ? "utf-8" : instance.config.oe;
|
||||
const container = this.container;
|
||||
if (!container) throw new Error("Attach Failed, Container is Null!");
|
||||
try {
|
||||
await container.start();
|
||||
const stream = await container.attach({
|
||||
stream: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
stdin: true
|
||||
});
|
||||
stream.on("data", (text: any) => instance.print(iconv.decode(text, outputCode)));
|
||||
stream.on("error", (text: any) => instance.print(iconv.decode(text, outputCode)));
|
||||
await container.wait();
|
||||
} catch (error) {
|
||||
container.remove().catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async onError(err: Error) {}
|
||||
|
||||
public toObject() {}
|
||||
}
|
||||
|
||||
// SubProcess adapter for Instance
|
||||
export class DockerProcessAdapter extends EventEmitter implements IInstanceProcess {
|
||||
pid?: number | string;
|
||||
|
||||
private stream?: NodeJS.ReadWriteStream;
|
||||
|
||||
constructor(public container: Docker.Container) {
|
||||
super();
|
||||
}
|
||||
|
||||
// Once the program is actually started, no errors can block the next startup process
|
||||
public async start(param: IDockerProcessAdapterStartParam) {
|
||||
try {
|
||||
await this.container.start();
|
||||
|
||||
const { isTty, h, w } = param;
|
||||
if (isTty) {
|
||||
this.container.resize({ h, w });
|
||||
}
|
||||
|
||||
this.pid = this.container.id;
|
||||
const stream = (this.stream = await this.container.attach({
|
||||
stream: true,
|
||||
stdout: true,
|
||||
stderr: true,
|
||||
stdin: true
|
||||
}));
|
||||
stream.on("data", (data) => this.emit("data", data));
|
||||
stream.on("error", (data) => this.emit("data", data));
|
||||
this.wait();
|
||||
} catch (error: any) {
|
||||
this.kill();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public write(data?: string) {
|
||||
if (this.stream && data) this.stream.write(data);
|
||||
}
|
||||
|
||||
public async kill(s?: string) {
|
||||
await this.container.kill();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
try {
|
||||
await this.container.remove();
|
||||
} catch (error: any) {}
|
||||
}
|
||||
|
||||
private wait() {
|
||||
this.container.wait(async (v) => {
|
||||
await this.destroy();
|
||||
this.emit("exit", v);
|
||||
});
|
||||
}
|
||||
}
|
85
daemon/src/service/instance_update_action.ts
Normal file
85
daemon/src/service/instance_update_action.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { $t } from "../i18n";
|
||||
import { killProcess } from "common";
|
||||
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||
import logger from "../service/log";
|
||||
import Instance from "../entity/instance/instance";
|
||||
import { commandStringToArray } from "../entity/commands/base/command_parser";
|
||||
import iconv from "iconv-lite";
|
||||
import { AsyncTask, IAsyncTaskJSON } from "../service/async_task_service";
|
||||
|
||||
export class InstanceUpdateAction extends AsyncTask {
|
||||
public pid?: number;
|
||||
public process?: ChildProcessWithoutNullStreams;
|
||||
|
||||
constructor(public readonly instance: Instance) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async onStart() {
|
||||
let updateCommand = this.instance.config.updateCommand;
|
||||
updateCommand = updateCommand.replace(/\{mcsm_workspace\}/gm, this.instance.config.cwd);
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
logger.info(
|
||||
$t("TXT_CODE_general_update.updateCmd", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
logger.info(updateCommand);
|
||||
|
||||
this.instance.println(
|
||||
$t("TXT_CODE_general_update.update"),
|
||||
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
|
||||
);
|
||||
|
||||
// command parsing
|
||||
const commandList = commandStringToArray(updateCommand);
|
||||
const commandExeFile = commandList[0];
|
||||
const commandParameters = commandList.slice(1);
|
||||
if (commandList.length === 0) {
|
||||
return this.instance.failure(new Error($t("TXT_CODE_general_update.cmdFormatErr")));
|
||||
}
|
||||
|
||||
// start the update command
|
||||
const process = spawn(commandExeFile, commandParameters, {
|
||||
cwd: this.instance.config.cwd,
|
||||
stdio: "pipe",
|
||||
windowsHide: true
|
||||
});
|
||||
if (!process || !process.pid) {
|
||||
return this.instance.println(
|
||||
$t("TXT_CODE_general_update.err"),
|
||||
$t("TXT_CODE_general_update.updateFailed")
|
||||
);
|
||||
}
|
||||
|
||||
// process & pid
|
||||
this.pid = process.pid;
|
||||
this.process = process;
|
||||
|
||||
process.stdout.on("data", (text) => {
|
||||
this.instance.print(iconv.decode(text, this.instance.config.oe));
|
||||
});
|
||||
process.stderr.on("data", (text) => {
|
||||
this.instance.print(iconv.decode(text, this.instance.config.oe));
|
||||
});
|
||||
|
||||
process.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.error(new Error($t("TXT_CODE_general_update.updateErr")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async onStop() {
|
||||
if (this.pid && this.process) {
|
||||
killProcess(this.pid, this.process);
|
||||
}
|
||||
}
|
||||
|
||||
public async onError(err: Error) {}
|
||||
public toObject(): IAsyncTaskJSON {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user