aboutsummaryrefslogtreecommitdiff
path: root/deno/tools
diff options
context:
space:
mode:
Diffstat (limited to 'deno/tools')
-rw-r--r--deno/tools/deno.json1
-rw-r--r--deno/tools/geosite.ts (renamed from deno/tools/gen-geosite-rules.ts)76
-rw-r--r--deno/tools/main.ts14
-rw-r--r--deno/tools/service.ts180
-rw-r--r--deno/tools/vm.ts225
-rw-r--r--deno/tools/yargs.ts12
6 files changed, 470 insertions, 38 deletions
diff --git a/deno/tools/deno.json b/deno/tools/deno.json
index 3597182..355046a 100644
--- a/deno/tools/deno.json
+++ b/deno/tools/deno.json
@@ -3,5 +3,6 @@
"tasks": {
},
"imports": {
+ "mustache": "npm:mustache@^4.2.0"
}
}
diff --git a/deno/tools/gen-geosite-rules.ts b/deno/tools/geosite.ts
index c59d34f..3aabec2 100644
--- a/deno/tools/gen-geosite-rules.ts
+++ b/deno/tools/geosite.ts
@@ -1,7 +1,7 @@
-const PROXY_NAME = "node-select"
-const ATTR = "cn"
+const ATTR = "cn";
const REPO_NAME = "domain-list-community";
-const URL = "https://github.com/v2fly/domain-list-community/archive/refs/heads/master.zip"
+const URL =
+ "https://github.com/v2fly/domain-list-community/archive/refs/heads/master.zip";
const SITES = [
"github",
"google",
@@ -39,9 +39,9 @@ const SITES = [
"ieee",
"sci-hub",
"libgen",
-]
+];
-const prefixes = ["include", "domain", "keyword", "full", "regexp"] as const
+const prefixes = ["include", "domain", "keyword", "full", "regexp"] as const;
interface Rule {
kind: (typeof prefixes)[number];
@@ -52,20 +52,20 @@ interface Rule {
type FileProvider = (name: string) => string;
function extract(starts: string[], provider: FileProvider): Rule[] {
-function parseLine(line: string): Rule {
- let kind = prefixes.find((p) => line.startsWith(p + ":"));
- if (kind != null) {
- line = line.slice(line.indexOf(":") + 1);
- } else {
- kind = "domain";
+ function parseLine(line: string): Rule {
+ let kind = prefixes.find((p) => line.startsWith(p + ":"));
+ if (kind != null) {
+ line = line.slice(line.indexOf(":") + 1);
+ } else {
+ kind = "domain";
+ }
+ const segs = line.split("@");
+ return {
+ kind,
+ value: segs[0].trim(),
+ attrs: [...segs.slice(1)].map((s) => s.trim()),
+ };
}
- const segs = line.split("@");
- return {
- kind,
- value: segs[0].trim(),
- attrs: [...segs.slice(1)].map((s) => s.trim()),
- };
-}
function parse(text: string): Rule[] {
return text
@@ -76,10 +76,10 @@ function parseLine(line: string): Rule {
.map((l) => parseLine(l));
}
- const visited = [] as string[]
- const rules = [] as Rule[]
+ const visited = [] as string[];
+ const rules = [] as Rule[];
- function add(name :string) {
+ function add(name: string) {
const text = provider(name);
for (const rule of parse(text)) {
if (rule.kind === "include") {
@@ -100,25 +100,25 @@ function parseLine(line: string): Rule {
add(start);
}
- return rules
+ return rules;
}
function toNewFormat(rules: Rule[], attr: string): [string, string] {
function toLine(rule: Rule) {
const prefixMap = {
- "domain": "DOMAIN-SUFFIX",
- "full": "DOMAIN",
- "keyword": "DOMAIN-KEYWORD",
- "regexp": "DOMAIN-REGEX",
+ domain: "DOMAIN-SUFFIX",
+ full: "DOMAIN",
+ keyword: "DOMAIN-KEYWORD",
+ regexp: "DOMAIN-REGEX",
} as const;
if (rule.kind === "include") {
- throw new Error("Include rule not parsed.")
+ throw new Error("Include rule not parsed.");
}
- return `${prefixMap[rule.kind]},${rule.value}`
+ return `${prefixMap[rule.kind]},${rule.value}`;
}
function toLines(rules: Rule[]) {
- return rules.map(r => toLine(r)).join("\n")
+ return rules.map((r) => toLine(r)).join("\n");
}
const has: Rule[] = [];
@@ -128,7 +128,6 @@ function toNewFormat(rules: Rule[], attr: string): [string, string] {
return [toLines(has), toLines(notHas)];
}
-
if (import.meta.main) {
const tmpDir = Deno.makeTempDirSync({ prefix: "geosite-rules-" });
console.log("Work dir is ", tmpDir);
@@ -150,12 +149,13 @@ if (import.meta.main) {
const provider = (name: string) =>
Deno.readTextFileSync(dataDir + "/" + name);
- const rules = extract(SITES, provider)
- const [has, notHas] = toNewFormat(rules, ATTR)
- const hasFile = tmpDir + "/has-rule"
- const notHasFile = tmpDir + "/not-has-rule"
- console.log("Write result to: " + hasFile + " , " + notHasFile)
- Deno.writeTextFileSync(hasFile, has)
- Deno.writeTextFileSync(notHasFile, notHas)
+ const rules = extract(SITES, provider);
+ const [has, notHas] = toNewFormat(rules, ATTR);
+ const resultDir = tmpDir + "/result";
+ Deno.mkdirSync(resultDir);
+ const hasFile = resultDir + "/has-rule";
+ const notHasFile = resultDir + "/not-has-rule";
+ console.log("Write result to: " + hasFile + " , " + notHasFile);
+ Deno.writeTextFileSync(hasFile, has);
+ Deno.writeTextFileSync(notHasFile, notHas);
}
-
diff --git a/deno/tools/main.ts b/deno/tools/main.ts
new file mode 100644
index 0000000..897350c
--- /dev/null
+++ b/deno/tools/main.ts
@@ -0,0 +1,14 @@
+import yargs, { DEMAND_COMMAND_MESSAGE } from "./yargs.ts";
+import vm from "./vm.ts";
+import service from "./service.ts";
+
+if (import.meta.main) {
+ await yargs(Deno.args)
+ .scriptName("crupest")
+ .command(vm)
+ .command(service)
+ .demandCommand(1, DEMAND_COMMAND_MESSAGE)
+ .help()
+ .strict()
+ .parse();
+}
diff --git a/deno/tools/service.ts b/deno/tools/service.ts
new file mode 100644
index 0000000..bd4d22c
--- /dev/null
+++ b/deno/tools/service.ts
@@ -0,0 +1,180 @@
+import { dirname, join, relative } from "@std/path";
+import { copySync, existsSync, walkSync } from "@std/fs";
+import { distinct } from "@std/collections";
+// @ts-types="npm:@types/mustache"
+import Mustache from "mustache";
+
+import { defineYargsModule, DEMAND_COMMAND_MESSAGE } from "./yargs.ts";
+
+const MUSTACHE_RENDER_OPTIONS: Mustache.RenderOptions = {
+ tags: ["@@", "@@"],
+ escape: (value: unknown) => String(value),
+};
+
+function mustacheParse(template: string) {
+ return Mustache.parse(template, MUSTACHE_RENDER_OPTIONS.tags);
+}
+
+function mustacheRender(template: string, view: Record<string, string>) {
+ return Mustache.render(template, view, {}, MUSTACHE_RENDER_OPTIONS);
+}
+
+function getVariableKeysOfTemplate(template: string): string[] {
+ return distinct(
+ mustacheParse(template)
+ .filter((v) => v[0] === "name")
+ .map((v) => v[1]),
+ );
+}
+
+function loadTemplatedConfigFiles(
+ files: string[],
+): Record<string, string> {
+ console.log("Scan config files ...");
+ const config: Record<string, string> = {};
+ for (const file of files) {
+ console.log(` from file ${file}`);
+ const text = Deno.readTextFileSync(file);
+ let lineNumber = 0;
+ for (const rawLine of text.split("\n")) {
+ lineNumber++;
+ const line = rawLine.trim();
+ if (line.length === 0) continue;
+ if (line.startsWith("#")) continue;
+ const equalSymbolIndex = line.indexOf("=");
+ if (equalSymbolIndex === -1) {
+ throw new Error(`Line ${lineNumber} of ${file} is invalid.`);
+ }
+ const [key, valueText] = [
+ line.slice(0, equalSymbolIndex).trim(),
+ line.slice(equalSymbolIndex + 1).trim(),
+ ];
+ console.log(` (${key in config ? "override" : "new"}) ${key}`);
+ getVariableKeysOfTemplate(valueText).forEach((name) => {
+ if (!(name in config)) {
+ throw new Error(
+ `Variable ${name} is not defined yet, perhaps due to typos or wrong order.`,
+ );
+ }
+ });
+ config[key] = mustacheRender(valueText, config);
+ }
+ }
+ return config;
+}
+
+const TEMPLATE_FILE_EXT = ".template";
+
+class TemplateDir {
+ templates: { path: string; ext: string; text: string; vars: string[] }[] = [];
+ plains: { path: string }[] = [];
+
+ constructor(public dir: string) {
+ console.log(`Scan template dir ${dir} ...`);
+ Array.from(
+ walkSync(dir, { includeDirs: false, followSymlinks: true }),
+ ).forEach(({ path }) => {
+ path = relative(this.dir, path);
+ if (path.endsWith(TEMPLATE_FILE_EXT)) {
+ console.log(` (template) ${path}`);
+ const text = Deno.readTextFileSync(join(dir, path));
+ this.templates.push({
+ path,
+ ext: TEMPLATE_FILE_EXT,
+ text,
+ vars: getVariableKeysOfTemplate(text),
+ });
+ } else {
+ console.log(` (plain) ${path}`);
+ this.plains.push({ path });
+ }
+ });
+ }
+
+ allNeededVars() {
+ return distinct(this.templates.flatMap((t) => t.vars));
+ }
+
+ generate(vars: Record<string, string>, generatedDir?: string) {
+ console.log(
+ `Generate to dir ${generatedDir ?? "[dry-run]"} ...`,
+ );
+
+ const undefinedVars = this.allNeededVars().filter((v) => !(v in vars));
+ if (undefinedVars.length !== 0) {
+ throw new Error(
+ `Needed variables are not defined: ${undefinedVars.join(", ")}`,
+ );
+ }
+
+ if (generatedDir != null) {
+ if (existsSync(generatedDir)) {
+ console.log(` delete old generated dir`);
+ Deno.removeSync(generatedDir, { recursive: true });
+ }
+
+ for (const file of this.plains) {
+ const [source, destination] = [
+ join(this.dir, file.path),
+ join(generatedDir, file.path),
+ ];
+ console.log(` copy ${file.path}`);
+ Deno.mkdirSync(dirname(destination), { recursive: true });
+ copySync(source, destination);
+ }
+ for (const file of this.templates) {
+ const path = file.path.slice(0, -file.ext.length);
+ const destination = join(generatedDir, path);
+ console.log(` generate ${path}`);
+ const rendered = mustacheRender(file.text, vars);
+ Deno.mkdirSync(dirname(destination), { recursive: true });
+ Deno.writeTextFileSync(destination, rendered);
+ }
+ }
+ }
+}
+
+export default defineYargsModule({
+ command: "service",
+ aliases: ["sv"],
+ describe: "Manage services.",
+ builder: (builder) => {
+ return builder
+ .option("project-dir", {
+ type: "string",
+ })
+ .demandOption("project-dir")
+ .command({
+ command: "gen-tmpl",
+ describe: "Generate files from templates",
+ builder: (builder) => {
+ return builder
+ .option("dry-run", {
+ type: "boolean",
+ default: true,
+ })
+ .strict();
+ },
+ handler: (argv) => {
+ const { projectDir, dryRun } = argv;
+
+ const config = loadTemplatedConfigFiles(
+ [
+ join(projectDir, "data/config"),
+ join(projectDir, "services/config.template"),
+ ],
+ );
+
+ new TemplateDir(
+ join(projectDir, "services/templates"),
+ ).generate(
+ config,
+ dryRun ? undefined : join(projectDir, "services/generated"),
+ );
+ console.log("Done!");
+ },
+ })
+ .demandCommand(1, DEMAND_COMMAND_MESSAGE);
+ },
+ handler: () => {},
+});
diff --git a/deno/tools/vm.ts b/deno/tools/vm.ts
new file mode 100644
index 0000000..b54c0d4
--- /dev/null
+++ b/deno/tools/vm.ts
@@ -0,0 +1,225 @@
+import os from "node:os";
+import { join } from "@std/path";
+import { defineYargsModule, DEMAND_COMMAND_MESSAGE } from "./yargs.ts";
+
+type ArchAliasMap = { [name: string]: string[] };
+const arches = {
+ x86_64: ["x86_64", "amd64"],
+ i386: ["i386", "x86", "i686"],
+} as const satisfies ArchAliasMap;
+type Arch = keyof typeof arches;
+type GeneralArch = (typeof arches)[Arch][number];
+
+function normalizeArch(generalName: GeneralArch): Arch {
+ for (const [name, aliases] of Object.entries(arches as ArchAliasMap)) {
+ if (aliases.includes(generalName)) return name as Arch;
+ }
+ throw Error("Unknown architecture name.");
+}
+
+interface GeneralVmSetup {
+ name?: string[];
+ arch: GeneralArch;
+ cpuNumber?: number;
+ memory?: number;
+ disk: string;
+ usbTablet?: boolean;
+ sshForwardPort?: number;
+ tpm?: boolean;
+ kvm?: boolean;
+}
+
+interface VmSetup {
+ arch: Arch;
+ cpuNumber: number;
+ memory: number;
+ disk: string;
+ usbTablet: boolean;
+ sshForwardPort?: number;
+ tpm: boolean;
+ kvm: boolean;
+}
+
+const VM_DIR = join(os.homedir(), "vms");
+
+function getDiskFilePath(name: string): string {
+ return join(VM_DIR, `${name}.qcow2`);
+}
+
+const MY_VMS: GeneralVmSetup[] = [
+ {
+ name: ["hurd", ...arches.i386.map((a) => `hurd-${a}`)],
+ arch: "i386",
+ disk: getDiskFilePath("hurd-i386"),
+ sshForwardPort: 3222,
+ },
+ {
+ name: [...arches.x86_64.map((a) => `hurd-${a}`)],
+ arch: "x86_64",
+ disk: getDiskFilePath("hurd-x86_64"),
+ sshForwardPort: 3223,
+ },
+ {
+ name: ["win"],
+ arch: "x86_64",
+ cpuNumber: 4,
+ memory: 16,
+ disk: getDiskFilePath("win"),
+ usbTablet: true,
+ tpm: true,
+ },
+];
+
+function normalizeVmSetup(generalSetup: GeneralVmSetup): VmSetup {
+ const { arch, cpuNumber, memory, disk, usbTablet, sshForwardPort, tpm, kvm } =
+ generalSetup;
+
+ const normalizedArch = normalizeArch(arch);
+ const is64 = normalizedArch === "x86_64";
+
+ return {
+ arch: normalizedArch,
+ disk,
+ cpuNumber: cpuNumber ?? 1,
+ memory: memory ?? (is64 ? 8 : 4),
+ usbTablet: usbTablet ?? false,
+ sshForwardPort,
+ tpm: tpm ?? false,
+ kvm: kvm ?? Deno.build.os === "linux",
+ };
+}
+
+function resolveVmSetup(
+ name: string,
+ vms: GeneralVmSetup[],
+): VmSetup | undefined {
+ const setup = vms.find((vm) => vm.name?.includes(name));
+ return setup == null ? undefined : normalizeVmSetup(setup);
+}
+
+const qemuBinPrefix = "qemu-system" as const;
+
+const qemuBinSuffix = {
+ x86_64: "x86_64",
+ i386: "x86_64",
+} as const;
+
+function getQemuBin(arch: Arch): string {
+ return `${qemuBinPrefix}-${qemuBinSuffix[arch]}`;
+}
+
+function getLinuxHostArgs(kvm: boolean): string[] {
+ return kvm ? ["-enable-kvm"] : [];
+}
+
+function getMachineArgs(vm: VmSetup): string[] {
+ const is64 = vm.arch === "x86_64";
+ const machineArgs = is64 ? ["-machine", "q35"] : [];
+ return [...machineArgs, "-smp", String(vm.cpuNumber), "-m", `${vm.memory}G`];
+}
+
+function getDeviceArgs(vm: VmSetup): string[] {
+ const { usbTablet } = vm;
+ return usbTablet ? ["-usb", "-device", "usb-tablet"] : [];
+}
+
+function getNetworkArgs(sshForwardPort?: number): string[] {
+ const args = ["-net", "nic"];
+ if (sshForwardPort != null) {
+ args.push("-net", `user,hostfwd=tcp::${sshForwardPort}-:22`);
+ }
+ return args;
+}
+
+function getDisplayArgs(): string[] {
+ return ["-vga", "vmware"];
+}
+
+function getDiskArgs(disk: string): string[] {
+ return ["-drive", `cache=writeback,file=${disk}`];
+}
+
+function getTpmControlSocketPath(): string {
+ return join(VM_DIR, "tpm2/swtpm-sock");
+}
+
+function getTpmArgs(tpm: boolean): string[] {
+ if (!tpm) return [];
+ return [
+ "-chardev",
+ `socket,id=chrtpm,path=${getTpmControlSocketPath()}`,
+ "-tpmdev",
+ "emulator,id=tpm0,chardev=chrtpm",
+ "-device",
+ "tpm-tis,tpmdev=tpm0",
+ ];
+}
+
+function getTpmPreCommand(): string[] {
+ return [
+ "swtpm",
+ "socket",
+ "--tpm2",
+ "--tpmstate",
+ `dir=${join(VM_DIR, "tpm2")}`,
+ "--ctrl",
+ `type=unixio,path=${getTpmControlSocketPath()}`,
+ ];
+}
+
+function createPreCommands(setup: VmSetup): string[][] {
+ const { tpm } = setup;
+ const result = [];
+ if (tpm) result.push(getTpmPreCommand());
+ return result;
+}
+
+function createQemuArgs(setup: VmSetup): string[] {
+ const { arch, disk, sshForwardPort, tpm } = setup;
+ return [
+ getQemuBin(arch),
+ ...getLinuxHostArgs(setup.kvm),
+ ...getMachineArgs(setup),
+ ...getDeviceArgs(setup),
+ ...getDisplayArgs(),
+ ...getNetworkArgs(sshForwardPort),
+ ...getDiskArgs(disk),
+ ...getTpmArgs(tpm),
+ ];
+}
+
+const gen = defineYargsModule({
+ command: "gen <name>",
+ describe: "generate cli command to run the vm",
+ builder: (builder) => {
+ return builder
+ .positional("name", {
+ describe: "name of the vm to run",
+ type: "string",
+ })
+ .demandOption("name")
+ .strict();
+ },
+ handler: (argv) => {
+ const vm = resolveVmSetup(argv.name, MY_VMS);
+ if (vm == null) {
+ console.error(`No vm called ${argv.name} is found.`);
+ Deno.exit(-1);
+ }
+ const preCommands = createPreCommands(vm);
+ const cli = createQemuArgs(vm);
+ for (const command of preCommands) {
+ console.log(`${command.join(" ")} &`);
+ }
+ console.log(`${cli.join(" ")}`);
+ },
+});
+
+export default defineYargsModule({
+ command: "vm",
+ describe: "Manage (qemu) virtual machines.",
+ builder: (builder) => {
+ return builder.command(gen).demandCommand(1, DEMAND_COMMAND_MESSAGE);
+ },
+ handler: () => {},
+});
diff --git a/deno/tools/yargs.ts b/deno/tools/yargs.ts
new file mode 100644
index 0000000..eaa7803
--- /dev/null
+++ b/deno/tools/yargs.ts
@@ -0,0 +1,12 @@
+// @ts-types="npm:@types/yargs"
+export { default } from "yargs";
+export * from "yargs";
+
+import { CommandModule } from "yargs";
+export function defineYargsModule<T, U>(
+ module: CommandModule<T, U>,
+): CommandModule<T, U> {
+ return module;
+}
+
+export const DEMAND_COMMAND_MESSAGE = "No command is specified";