Skip to content

Commit

Permalink
Merge branch 'main' into feature/#2011
Browse files Browse the repository at this point in the history
  • Loading branch information
jdkfx authored Jan 26, 2025
2 parents ea58503 + af114ee commit 2eb2c57
Show file tree
Hide file tree
Showing 32 changed files with 950 additions and 604 deletions.
2 changes: 1 addition & 1 deletion src/backend/electron/engineAndVvppController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { getEngineProcessManager } from "./manager/engineProcessManager";
import { getRuntimeInfoManager } from "./manager/RuntimeInfoManager";
import { getVvppManager } from "./manager/vvppManager";
import { getWindowManager } from "./manager/windowManager";
import { ProgressCallback } from "./type";
import {
EngineId,
EngineInfo,
Expand All @@ -23,6 +22,7 @@ import {
} from "@/domain/defaultEngine/latetDefaultEngine";
import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo";
import { UnreachableError } from "@/type/utility";
import { ProgressCallback } from "@/helpers/progressHelper";
import { createLogger } from "@/helpers/log";

const log = createLogger("EngineAndVvppController");
Expand Down
9 changes: 3 additions & 6 deletions src/backend/electron/fileHelper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import fs from "fs";
import { moveFileSync } from "move-file";
import { uuid4 } from "@/helpers/random";
import { createLogger } from "@/helpers/log";

Expand All @@ -14,15 +13,13 @@ export function writeFileSafely(
data: string | NodeJS.ArrayBufferView,
) {
const tmpPath = `${path}-${uuid4()}.tmp`;
fs.writeFileSync(tmpPath, data, { flag: "wx" });

try {
moveFileSync(tmpPath, path, {
overwrite: true,
});
fs.writeFileSync(tmpPath, data, { flag: "wx" });
fs.renameSync(tmpPath, path);
} catch (error) {
if (fs.existsSync(tmpPath)) {
fs.promises.unlink(tmpPath).catch((reason) => {
void fs.promises.unlink(tmpPath).catch((reason) => {
log.warn("Failed to remove %s\n %o", tmpPath, reason);
});
}
Expand Down
235 changes: 12 additions & 223 deletions src/backend/electron/manager/vvppManager.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
import fs from "fs";
import path from "path";
import { spawn } from "child_process";
import fs from "node:fs";
import path from "node:path";
import { moveFile } from "move-file";
import { app, dialog } from "electron";
import MultiStream from "multistream";
import { glob } from "glob";
import AsyncLock from "async-lock";
import { ProgressCallback } from "../type";
import {
EngineId,
EngineInfo,
minimumEngineManifestSchema,
MinimumEngineManifestType,
} from "@/type/preload";
import { errorToMessage } from "@/helpers/errorHelper";
import { extractVvpp } from "@/backend/electron/vvppFile";
import { ProgressCallback } from "@/helpers/progressHelper";
import { createLogger } from "@/helpers/log";

const log = createLogger("VvppManager");

const isNotWin = process.platform !== "win32";

// https://www.garykessler.net/library/file_sigs.html#:~:text=7-zip%20compressed%20file
const SEVEN_ZIP_MAGIC_NUMBER = Buffer.from([
0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c,
]);

const ZIP_MAGIC_NUMBER = Buffer.from([0x50, 0x4b, 0x03, 0x04]);

export const isVvppFile = (filePath: string) => {
return (
path.extname(filePath) === ".vvpp" || path.extname(filePath) === ".vvppp"
Expand All @@ -35,136 +25,6 @@ export const isVvppFile = (filePath: string) => {

const lockKey = "lock-key-for-vvpp-manager";

/** VVPPファイルが分割されている場合、それらのファイルを取得する */
async function getArchiveFileParts(
vvppLikeFilePath: string,
): Promise<string[]> {
let archiveFileParts: string[];
// 名前.数値.vvpppの場合は分割されているとみなして連結する
if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) {
log.info("vvpp is split, finding other parts...");
const vvpppPathGlob = vvppLikeFilePath
.replace(/\.[0-9]+\.vvppp$/, ".*.vvppp")
.replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する
const filePaths: string[] = [];
for (const p of await glob(vvpppPathGlob)) {
if (!p.match(/\.[0-9]+\.vvppp$/)) {
continue;
}
log.info(`found ${p}`);
filePaths.push(p);
}
filePaths.sort((a, b) => {
const aMatch = a.match(/\.([0-9]+)\.vvppp$/);
const bMatch = b.match(/\.([0-9]+)\.vvppp$/);
if (aMatch == null || bMatch == null) {
throw new Error(`match is null: a=${a}, b=${b}`);
}
return parseInt(aMatch[1]) - parseInt(bMatch[1]);
});
archiveFileParts = filePaths;
} else {
log.info("Not a split file");
archiveFileParts = [vvppLikeFilePath];
}
return archiveFileParts;
}

/** 分割されているVVPPファイルを連結して返す */
async function concatenateVvppFiles(
format: "zip" | "7z",
archiveFileParts: string[],
) {
// -siオプションでの7z解凍はサポートされていないため、
// ファイルを連結した一次ファイルを作成し、それを7zで解凍する。
log.info(`Concatenating ${archiveFileParts.length} files...`);
const tmpConcatenatedFile = path.join(
app.getPath("temp"),
`vvpp-${new Date().getTime()}.${format}`,
);
log.info("Temporary file:", tmpConcatenatedFile);
await new Promise<void>((resolve, reject) => {
if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined");
const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f));
const outputStream = fs.createWriteStream(tmpConcatenatedFile);
new MultiStream(inputStreams)
.pipe(outputStream)
.on("close", () => {
outputStream.close();
resolve();
})
.on("error", reject);
});
log.info("Concatenated");
return tmpConcatenatedFile;
}

/** 7zでファイルを解凍する */
async function unarchive(
payload: {
archiveFile: string;
outputDir: string;
format: "zip" | "7z";
},
callbacks?: { onProgress?: ProgressCallback },
) {
const { archiveFile, outputDir, format } = payload;

const args = [
"x",
"-o" + outputDir,
archiveFile,
"-t" + format,
"-bsp1", // 進捗出力
];

let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME;
if (!sevenZipPath) {
throw new Error("7z path is not defined");
}
if (import.meta.env.PROD) {
sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath);
}
log.info("Spawning 7z:", sevenZipPath, args.join(" "));
await new Promise<void>((resolve, reject) => {
const child = spawn(sevenZipPath, args, {
stdio: ["pipe", "pipe", "pipe"],
});

child.stdout?.on("data", (data: Buffer) => {
const output = data.toString("utf-8");
log.info(`7z STDOUT: ${output}`);

// 進捗を取得
// NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る
// TODO: 出力が変わるかもしれないのでテストが必要
const progressMatch = output.match(
/ *(?<percent>\d+)% ?(?<fileCount>\d+)? ?(?<file>.*)/,
);
if (progressMatch?.groups?.percent) {
callbacks?.onProgress?.({
progress: parseInt(progressMatch.groups.percent),
});
}
});

child.stderr?.on("data", (data: Buffer) => {
log.error(`7z STDERR: ${data.toString("utf-8")}`);
});

child.on("exit", (code) => {
if (code === 0) {
callbacks?.onProgress?.({ progress: 100 });
resolve();
} else {
reject(new Error(`7z exited with code ${code}`));
}
});
// FIXME: rejectが2回呼ばれることがある
child.on("error", reject);
});
}

// # 軽い概要
//
// フォルダ名:"エンジン名+UUID"
Expand Down Expand Up @@ -240,67 +100,6 @@ export class VvppManager {
return true;
}

private async extractVvpp(
vvppLikeFilePath: string,
callbacks?: { onProgress?: ProgressCallback },
): Promise<{ outputDir: string; manifest: MinimumEngineManifestType }> {
callbacks?.onProgress?.({ progress: 0 });

const nonce = new Date().getTime().toString();
const outputDir = path.join(this.vvppEngineDir, ".tmp", nonce);

const archiveFileParts = await getArchiveFileParts(vvppLikeFilePath);

const format = await this.detectFileFormat(archiveFileParts[0]);
if (!format) {
throw new Error(`Unknown file format: ${archiveFileParts[0]}`);
}
log.info("Format:", format);
log.info("Extracting vvpp to", outputDir);
try {
let tmpConcatenatedFile: string | undefined;
let archiveFile: string;
try {
if (archiveFileParts.length > 1) {
tmpConcatenatedFile = await concatenateVvppFiles(
format,
archiveFileParts,
);
archiveFile = tmpConcatenatedFile;
} else {
archiveFile = archiveFileParts[0];
log.info("Single file, not concatenating");
}

await unarchive({ archiveFile, outputDir, format }, callbacks);
} finally {
if (tmpConcatenatedFile) {
log.info("Removing temporary file", tmpConcatenatedFile);
await fs.promises.rm(tmpConcatenatedFile);
}
}
const manifest: MinimumEngineManifestType =
minimumEngineManifestSchema.parse(
JSON.parse(
await fs.promises.readFile(
path.join(outputDir, "engine_manifest.json"),
"utf-8",
),
),
);
return {
outputDir,
manifest,
};
} catch (e) {
if (fs.existsSync(outputDir)) {
log.info("Failed to extract vvpp, removing", outputDir);
await fs.promises.rm(outputDir, { recursive: true });
}
throw e;
}
}

/**
* 追加
*/
Expand All @@ -314,7 +113,14 @@ export class VvppManager {
vvppPath: string,
callbacks?: { onProgress?: ProgressCallback },
) {
const { outputDir, manifest } = await this.extractVvpp(vvppPath, callbacks);
const { outputDir, manifest } = await extractVvpp(
{
vvppLikeFilePath: vvppPath,
vvppEngineDir: this.vvppEngineDir,
tmpDir: app.getPath("temp"),
},
callbacks,
);

const dirName = this.toValidDirName(manifest);
const engineDirectory = path.join(this.vvppEngineDir, dirName);
Expand Down Expand Up @@ -408,23 +214,6 @@ export class VvppManager {
this.willReplaceEngineDirs.length > 0 || this.willDeleteEngineIds.size > 0
);
}

private async detectFileFormat(
filePath: string,
): Promise<"zip" | "7z" | undefined> {
const file = await fs.promises.open(filePath, "r");

const buffer = Buffer.alloc(8);
await file.read(buffer, 0, 8, 0);
await file.close();

if (buffer.compare(SEVEN_ZIP_MAGIC_NUMBER, 0, 6, 0, 6) === 0) {
return "7z";
} else if (buffer.compare(ZIP_MAGIC_NUMBER, 0, 4, 0, 4) === 0) {
return "zip";
}
return undefined;
}
}

export default VvppManager;
Expand Down
Loading

0 comments on commit 2eb2c57

Please sign in to comment.