diff options
Diffstat (limited to 'deno/tools')
-rw-r--r-- | deno/tools/deno.json | 8 | ||||
-rw-r--r-- | deno/tools/generate-geosite-rules.ts | 160 | ||||
-rw-r--r-- | deno/tools/manage-service.ts | 42 | ||||
-rw-r--r-- | deno/tools/manage-vm.ts | 144 | ||||
-rw-r--r-- | deno/tools/template.ts | 124 |
5 files changed, 478 insertions, 0 deletions
diff --git a/deno/tools/deno.json b/deno/tools/deno.json new file mode 100644 index 0000000..355046a --- /dev/null +++ b/deno/tools/deno.json @@ -0,0 +1,8 @@ +{ + "version": "0.1.0", + "tasks": { + }, + "imports": { + "mustache": "npm:mustache@^4.2.0" + } +} diff --git a/deno/tools/generate-geosite-rules.ts b/deno/tools/generate-geosite-rules.ts new file mode 100644 index 0000000..bfa53ba --- /dev/null +++ b/deno/tools/generate-geosite-rules.ts @@ -0,0 +1,160 @@ +const PROXY_NAME = "node-select"; +const ATTR = "cn"; +const REPO_NAME = "domain-list-community"; +const URL = + "https://github.com/v2fly/domain-list-community/archive/refs/heads/master.zip"; +const SITES = [ + "github", + "google", + "youtube", + "twitter", + "facebook", + "discord", + "reddit", + "twitch", + "quora", + "telegram", + "imgur", + "stackexchange", + "onedrive", + "duckduckgo", + "wikimedia", + "gitbook", + "gitlab", + "creativecommons", + "archive", + "matrix", + "tor", + "python", + "ruby", + "rust", + "nodejs", + "npmjs", + "qt", + "docker", + "v2ray", + "homebrew", + "bootstrap", + "heroku", + "vercel", + "ieee", + "sci-hub", + "libgen", +]; + +const prefixes = ["include", "domain", "keyword", "full", "regexp"] as const; + +interface Rule { + kind: (typeof prefixes)[number]; + value: string; + attrs: string[]; +} + +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"; + } + 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 + .replaceAll("\c\n", "\n") + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length !== 0 && !l.startsWith("#")) + .map((l) => parseLine(l)); + } + + const visited = [] as string[]; + const rules = [] as Rule[]; + + function add(name: string) { + const text = provider(name); + for (const rule of parse(text)) { + if (rule.kind === "include") { + if (visited.includes(rule.value)) { + console.warn(`circular refs found: ${name} includes ${rule.value}.`); + continue; + } else { + visited.push(rule.value); + add(rule.value); + } + } else { + rules.push(rule); + } + } + } + + for (const start of starts) { + add(start); + } + + 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", + } as const; + if (rule.kind === "include") { + throw new Error("Include rule not parsed."); + } + return `${prefixMap[rule.kind]},${rule.value}`; + } + + function toLines(rules: Rule[]) { + return rules.map((r) => toLine(r)).join("\n"); + } + + const has: Rule[] = []; + const notHas: Rule[] = []; + rules.forEach((r) => (r.attrs.includes(attr) ? has.push(r) : notHas.push(r))); + + return [toLines(has), toLines(notHas)]; +} + +if (import.meta.main) { + const tmpDir = Deno.makeTempDirSync({ prefix: "geosite-rules-" }); + console.log("Work dir is ", tmpDir); + const zipFilePath = tmpDir + "/repo.zip"; + const res = await fetch(URL); + if (!res.ok) { + throw new Error("Failed to download repo."); + } + Deno.writeFileSync(zipFilePath, await res.bytes()); + const unzip = new Deno.Command("unzip", { + args: ["-q", zipFilePath], + cwd: tmpDir, + }); + if (!(await unzip.spawn().status).success) { + throw new Error("Failed to unzip"); + } + + const dataDir = tmpDir + "/" + REPO_NAME + "-master/data"; + 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); +} diff --git a/deno/tools/manage-service.ts b/deno/tools/manage-service.ts new file mode 100644 index 0000000..148f55a --- /dev/null +++ b/deno/tools/manage-service.ts @@ -0,0 +1,42 @@ +import { join } from "@std/path"; +// @ts-types="npm:@types/yargs" +import yargs from "yargs"; + +import { TemplateDir } from "./template.ts"; + +if (import.meta.main) { + await yargs(Deno.args) + .scriptName("manage-service") + .option("project-dir", { + type: "string", + }) + .demandOption("project-dir") + .command({ + command: "gen-tmpl", + describe: "generate files for templates", + builder: (builder) => { + return builder + .option("dry-run", { + type: "boolean", + default: true, + }) + .strict(); + }, + handler: (argv) => { + const { projectDir, dryRun } = argv; + new TemplateDir( + join(projectDir, "services/templates"), + ).generateWithVariableFiles( + [ + join(projectDir, "data/config"), + join(projectDir, "services/config.template"), + ], + dryRun ? undefined : join(projectDir, "services/generated"), + ); + }, + }) + .demandCommand(1, "One command must be specified.") + .help() + .strict() + .parse(); +} diff --git a/deno/tools/manage-vm.ts b/deno/tools/manage-vm.ts new file mode 100644 index 0000000..bb985ce --- /dev/null +++ b/deno/tools/manage-vm.ts @@ -0,0 +1,144 @@ +import os from "node:os"; +import { join } from "@std/path"; +// @ts-types="npm:@types/yargs" +import yargs from "yargs"; + +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; + disk: string; + sshForwardPort: number; + kvm?: boolean; +} + +interface VmSetup { + arch: Arch; + disk: string; + sshForwardPort: number; + kvm: boolean; +} + +const MY_VMS: GeneralVmSetup[] = [ + { + name: ["hurd", ...arches.i386.map((a) => `hurd-${a}`)], + arch: "i386", + disk: join(os.homedir(), "vms/hurd-i386.qcow2"), + sshForwardPort: 3222, + }, + { + name: [...arches.x86_64.map((a) => `hurd-${a}`)], + arch: "x86_64", + disk: join(os.homedir(), "vms/hurd-x86_64.qcow2"), + sshForwardPort: 3223, + }, +]; + +function normalizeVmSetup(generalSetup: GeneralVmSetup): VmSetup { + const { arch, disk, sshForwardPort, kvm } = generalSetup; + return { + arch: normalizeArch(arch), + disk, + sshForwardPort, + 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(arch: Arch): string[] { + const is64 = arch === "x86_64"; + const machineArgs = is64 ? ["-machine", "q35"] : []; + const memory = is64 ? 8 : 4; + return [...machineArgs, "-m", `${memory}G`]; +} + +function getNetworkArgs(sshForwardPort: number): string[] { + return ["-net", "nic", "-net", `user,hostfwd=tcp::${sshForwardPort}-:22`]; +} + +function getDisplayArgs(): string[] { + return ["-vga", "vmware"]; +} + +function getDiskArgs(disk: string): string[] { + return ["-drive", `cache=writeback,file=${disk}`]; +} + +function createQemuArgs(setup: VmSetup): string[] { + const { arch, disk, sshForwardPort } = setup; + return [ + getQemuBin(arch), + ...getLinuxHostArgs(setup.kvm), + ...getMachineArgs(arch), + ...getDisplayArgs(), + ...getNetworkArgs(sshForwardPort), + ...getDiskArgs(disk), + ]; +} + +if (import.meta.main) { + await yargs(Deno.args) + .scriptName("manage-vm") + .command({ + 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 cli = createQemuArgs(vm); + console.log(`${cli.join(" ")}`); + }, + }) + .demandCommand(1, "One command must be specified.") + .help() + .strict() + .parse(); +} diff --git a/deno/tools/template.ts b/deno/tools/template.ts new file mode 100644 index 0000000..1b67eb8 --- /dev/null +++ b/deno/tools/template.ts @@ -0,0 +1,124 @@ +import { dirname, join, relative } from "@std/path"; +import { copySync, existsSync, walkSync } from "@std/fs"; +import { parse } from "@std/dotenv"; +import { distinct } from "@std/collections"; +// @ts-types="npm:@types/mustache" +import Mustache from "mustache"; + +Mustache.tags = ["@@", "@@"]; +Mustache.escape = (value) => String(value); + +function getVariableKeys(original: string): string[] { + return distinct( + Mustache.parse(original) + .filter(function (v) { + return v[0] === "name"; + }) + .map(function (v) { + return v[1]; + }), + ); +} + +export function loadVariables(files: string[]): Record<string, string> { + const vars: Record<string, string> = {}; + for (const file of files) { + const text = Deno.readTextFileSync(file); + for (const [key, valueText] of Object.entries(parse(text))) { + getVariableKeys(valueText).forEach((name) => { + if (!(name in vars)) { + throw new Error( + `Variable ${name} is not defined yet, perhaps due to typos or wrong order.`, + ); + } + }); + vars[key] = Mustache.render(valueText, vars); + } + } + return vars; +} + +const TEMPLATE_FILE_EXT = ".template"; + +export class TemplateDir { + templates: { path: string; ext: string; text: string; vars: string[] }[] = []; + plains: { path: string }[] = []; + + constructor(public dir: string) { + console.log("Scanning template 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: getVariableKeys(text), + }); + } else { + console.log(` (plain) ${path}`); + this.plains.push({ path }); + } + }); + console.log("Done scanning template dir."); + } + + allNeededVars() { + return distinct(this.templates.flatMap((t) => t.vars)); + } + + generate(vars: Record<string, string>, generatedDir?: string) { + console.log( + `Generating, template dir: ${this.dir}, generated 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 ${generatedDir}`); + 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 ${source} to ${destination} ...`); + Deno.mkdirSync(dirname(destination), { recursive: true }); + copySync(source, destination); + } + for (const file of this.templates) { + const [source, destination] = [ + join(this.dir, file.path), + join(generatedDir, file.path.slice(0, -file.ext.length)), + ]; + console.log(` generate ${source} to ${destination} ...`); + const rendered = Mustache.render(file.text, vars); + Deno.mkdirSync(dirname(destination), { recursive: true }); + Deno.writeTextFileSync(destination, rendered); + } + } + console.log(`Done generating.`); + } + + generateWithVariableFiles(varFiles: string[], generatedDir?: string) { + console.log("Scanning defined vars:"); + const vars = loadVariables(varFiles); + Object.keys(vars).forEach((name) => console.log(` ${name}`)); + console.log("Done scanning defined vars."); + this.generate(vars, generatedDir); + } +} |