mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-15 15:24:52 +09:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6424c5411 |
No files matched your search
@@ -6,6 +6,7 @@ import setBlocking from "set-blocking";
|
|||||||
import phin from "phin";
|
import phin from "phin";
|
||||||
import stream from "stream";
|
import stream from "stream";
|
||||||
import util from "util";
|
import util from "util";
|
||||||
|
import child_process from "child_process";
|
||||||
|
|
||||||
// internal libraries
|
// internal libraries
|
||||||
import * as lib from "@clusterio/lib";
|
import * as lib from "@clusterio/lib";
|
||||||
@@ -384,6 +385,8 @@ export default class Host extends lib.Link {
|
|||||||
this.handle(lib.HostMetricsRequest, this.handleHostMetricsRequest.bind(this));
|
this.handle(lib.HostMetricsRequest, this.handleHostMetricsRequest.bind(this));
|
||||||
this.handle(lib.SyncUserListsEvent, this.handleSyncUserListsEvent.bind(this));
|
this.handle(lib.SyncUserListsEvent, this.handleSyncUserListsEvent.bind(this));
|
||||||
this.handle(lib.SystemInfoRequest, this.handleSystemInfoRequest.bind(this));
|
this.handle(lib.SystemInfoRequest, this.handleSystemInfoRequest.bind(this));
|
||||||
|
this.handle(lib.HostFactorioVersionsRequest, this.handleHostFactorioVersionsRequest.bind(this));
|
||||||
|
this.handle(lib.HostDownloadFactorioRequest, this.handleHostDownloadFactorioRequest.bind(this));
|
||||||
this.handle(lib.PluginListRequest, this.handlePluginListRequest.bind(this));
|
this.handle(lib.PluginListRequest, this.handlePluginListRequest.bind(this));
|
||||||
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
|
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
|
||||||
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
|
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
|
||||||
@@ -859,6 +862,160 @@ export default class Host extends lib.Link {
|
|||||||
return { results };
|
return { results };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleHostFactorioVersionsRequest() {
|
||||||
|
const factorioDir = this.config.get("host.factorio_directory");
|
||||||
|
const versions: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if factorioDir is a simple installation
|
||||||
|
const simpleVersion = await this.getFactorioVersion(path.join(factorioDir, "data", "changelog.txt"));
|
||||||
|
if (simpleVersion !== null) {
|
||||||
|
versions.push(simpleVersion);
|
||||||
|
} else {
|
||||||
|
// Check for multiple versions in subdirectories
|
||||||
|
const entries = await fs.readdir(factorioDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = await this.getFactorioVersion(path.join(factorioDir, entry.name, "data", "changelog.txt"));
|
||||||
|
if (version !== null) {
|
||||||
|
versions.push(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code !== "ENOENT") {
|
||||||
|
logger.warn(`Error reading Factorio directory: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort versions in descending order (newest first)
|
||||||
|
versions.sort((a, b) => {
|
||||||
|
const aParts = a.split(".").map(s => parseInt(s, 10));
|
||||||
|
const bParts = b.split(".").map(s => parseInt(s, 10));
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||||
|
const aPart = aParts[i] || 0;
|
||||||
|
const bPart = bParts[i] || 0;
|
||||||
|
if (aPart !== bPart) {
|
||||||
|
return bPart - aPart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new lib.HostFactorioVersionsRequest.Response(versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleHostDownloadFactorioRequest() {
|
||||||
|
try {
|
||||||
|
const factorioDir = this.config.get("host.factorio_directory");
|
||||||
|
|
||||||
|
// Fetch the latest version info
|
||||||
|
const res = await fetch("https://factorio.com/get-download/stable/headless/linux64");
|
||||||
|
const location = res.headers.get("location");
|
||||||
|
if (!location) {
|
||||||
|
throw new Error("No redirect location found in response headers");
|
||||||
|
}
|
||||||
|
const url = new URL(location);
|
||||||
|
const filename = path.posix.basename(url.pathname);
|
||||||
|
const match = filename.match(/(?<=factorio_headless_x64_|factorio-headless_linux_)\d+\.\d+\.\d+(?=\.tar\.xz)/);
|
||||||
|
|
||||||
|
if (!match || !match.length) {
|
||||||
|
throw new Error(`Unable to extract version from filename: ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = match[0];
|
||||||
|
const tmpDir = "temp/create-temp/";
|
||||||
|
const archivePath = path.join(tmpDir, filename);
|
||||||
|
const tmpArchivePath = `${archivePath}.tmp`;
|
||||||
|
const versionDir = path.join(factorioDir, version);
|
||||||
|
const tmpFactorioDir = path.join(tmpDir, version);
|
||||||
|
|
||||||
|
// Check if version already exists
|
||||||
|
if (await fs.pathExists(versionDir)) {
|
||||||
|
return new lib.HostDownloadFactorioRequest.Response(true, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.ensureDir(tmpDir);
|
||||||
|
|
||||||
|
// Download the factorio archive
|
||||||
|
const downloadRes = await fetch(url.href);
|
||||||
|
if (!downloadRes.ok) {
|
||||||
|
throw new Error(`Failed to download Factorio: ${downloadRes.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Downloading Factorio ${version}. This may take a while.`);
|
||||||
|
const writeStream = fs.createWriteStream(tmpArchivePath);
|
||||||
|
if (downloadRes.body) {
|
||||||
|
const readableStream = stream.Readable.fromWeb(downloadRes.body);
|
||||||
|
readableStream.pipe(writeStream);
|
||||||
|
await finished(writeStream);
|
||||||
|
} else {
|
||||||
|
throw new Error("No response body received from download");
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.rename(tmpArchivePath, archivePath);
|
||||||
|
|
||||||
|
// Extract the archive
|
||||||
|
await fs.ensureDir(tmpFactorioDir);
|
||||||
|
await this.execFile("tar", [
|
||||||
|
"xf", archivePath, "-C", tmpFactorioDir, "--strip-components", "1",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Clean up archive and move to final location
|
||||||
|
await fs.unlink(archivePath);
|
||||||
|
await fs.rename(tmpFactorioDir, versionDir);
|
||||||
|
|
||||||
|
logger.info(`Successfully downloaded and installed Factorio ${version}`);
|
||||||
|
return new lib.HostDownloadFactorioRequest.Response(true, version);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Failed to download Factorio: ${error.message}`);
|
||||||
|
return new lib.HostDownloadFactorioRequest.Response(false, undefined, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFactorioVersion(changelogPath: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const changelog = await fs.readFile(changelogPath, "utf-8");
|
||||||
|
for (const line of changelog.split(/[\r\n]+/)) {
|
||||||
|
const index = line.indexOf(":");
|
||||||
|
if (index !== -1) {
|
||||||
|
const name = line.slice(0, index).trim();
|
||||||
|
if (name.toLowerCase() === "version") {
|
||||||
|
return line.slice(index + 1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async execFile(cmd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
child_process.execFile(
|
||||||
|
cmd,
|
||||||
|
args,
|
||||||
|
{ shell: true },
|
||||||
|
(err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({ stdout, stderr });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fallbackInstanceSaveDetailsListRequest(
|
async fallbackInstanceSaveDetailsListRequest(
|
||||||
request: lib.InstanceSaveDetailsListRequest,
|
request: lib.InstanceSaveDetailsListRequest,
|
||||||
src: lib.Address,
|
src: lib.Address,
|
||||||
|
|||||||
@@ -288,3 +288,51 @@ export class HostRevokeTokensRequest {
|
|||||||
return new this(json.hostId);
|
return new this(json.hostId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class HostFactorioVersionsRequest {
|
||||||
|
declare ["constructor"]: typeof HostFactorioVersionsRequest;
|
||||||
|
static type = "request" as const;
|
||||||
|
static src = ["control", "controller"] as const;
|
||||||
|
static dst = "host" as const;
|
||||||
|
static permission = "core.host.list" as const;
|
||||||
|
|
||||||
|
static Response = class Response {
|
||||||
|
constructor(
|
||||||
|
public versions: string[],
|
||||||
|
) { }
|
||||||
|
|
||||||
|
static jsonSchema = Type.Object({
|
||||||
|
"versions": Type.Array(Type.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||||
|
return new this(json.versions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HostDownloadFactorioRequest {
|
||||||
|
declare ["constructor"]: typeof HostDownloadFactorioRequest;
|
||||||
|
static type = "request" as const;
|
||||||
|
static src = ["control", "controller"] as const;
|
||||||
|
static dst = "host" as const;
|
||||||
|
static permission = "core.host.update" as const;
|
||||||
|
|
||||||
|
static Response = class Response {
|
||||||
|
constructor(
|
||||||
|
public success: boolean,
|
||||||
|
public version?: string,
|
||||||
|
public error?: string,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
static jsonSchema = Type.Object({
|
||||||
|
"success": Type.Boolean(),
|
||||||
|
"version": Type.Optional(Type.String()),
|
||||||
|
"error": Type.Optional(Type.String()),
|
||||||
|
});
|
||||||
|
|
||||||
|
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||||
|
return new this(json.success, json.version, json.error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -52,6 +52,8 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
|||||||
host.PrepareControllerDisconnectRequest,
|
host.PrepareControllerDisconnectRequest,
|
||||||
host.SyncUserListsEvent,
|
host.SyncUserListsEvent,
|
||||||
host.HostRevokeTokensRequest,
|
host.HostRevokeTokensRequest,
|
||||||
|
host.HostFactorioVersionsRequest,
|
||||||
|
host.HostDownloadFactorioRequest,
|
||||||
|
|
||||||
instance.InstanceDetailsGetRequest,
|
instance.InstanceDetailsGetRequest,
|
||||||
instance.InstanceDetailsListRequest,
|
instance.InstanceDetailsListRequest,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useHost } from "../model/host";
|
|||||||
import PageHeader from "./PageHeader";
|
import PageHeader from "./PageHeader";
|
||||||
import PageLayout from "./PageLayout";
|
import PageLayout from "./PageLayout";
|
||||||
import PluginExtra from "./PluginExtra";
|
import PluginExtra from "./PluginExtra";
|
||||||
|
import FactorioVersionManager from "./FactorioVersionManager";
|
||||||
import {
|
import {
|
||||||
MetricCpuRatio, MetricCpuUsed, MetricMemoryRatio, MetricMemoryUsed,
|
MetricCpuRatio, MetricCpuUsed, MetricMemoryRatio, MetricMemoryUsed,
|
||||||
MetricDiskUsed, MetricDiskRatio,
|
MetricDiskUsed, MetricDiskRatio,
|
||||||
@@ -151,6 +152,7 @@ export default function HostViewPage() {
|
|||||||
<Title level={5} style={{ marginTop: 16 }}>Instances</Title>
|
<Title level={5} style={{ marginTop: 16 }}>Instances</Title>
|
||||||
<InstanceList instances={hostInstances} size="small" hideAssignedHost />
|
<InstanceList instances={hostInstances} size="small" hideAssignedHost />
|
||||||
</>}
|
</>}
|
||||||
|
<FactorioVersionManager hostId={hostId} system={system} connected={host.connected} />
|
||||||
{account.hasPermission("core.log.follow") && <>
|
{account.hasPermission("core.log.follow") && <>
|
||||||
<Flex justify="space-between" align="baseline">
|
<Flex justify="space-between" align="baseline">
|
||||||
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||||
|
|||||||
Reference in new issue
Block a user