Switch to Typescript
This commit is contained in:
parent
34cb00139f
commit
535535efd3
37
.eslintrc.js
Normal file
37
.eslintrc.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { node: true },
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["@typescript-eslint", "prettier"],
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
],
|
||||||
|
ignorePatterns: [".eslintrc.js", "src/**/*.test.ts", "@types/*"],
|
||||||
|
parserOptions: {
|
||||||
|
sourceType: "module",
|
||||||
|
ecmaFeatures: {
|
||||||
|
modules: true,
|
||||||
|
},
|
||||||
|
project: ["./tsconfig.json"],
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/require-await": 0,
|
||||||
|
"no-await-in-loop": 2,
|
||||||
|
},
|
||||||
|
settings: { jsdoc: { mode: "typescript" } },
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.subtest.ts", "*.test.ts"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": 0,
|
||||||
|
"prefer-const": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-call": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-return": 0,
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": 0,
|
||||||
|
"no-await-in-loop": 1, // sometimes it's easier to debug when requests run sequentially
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,6 @@
|
|||||||
|
!@types
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/static/
|
/static/
|
||||||
/.log/
|
/.log/
|
||||||
/yarn-error.log
|
/yarn-error.log
|
||||||
|
/lib/
|
||||||
|
23
@types/src/container-pool.d.ts
vendored
Normal file
23
@types/src/container-pool.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
import { ChildProcessWithoutNullStreams } from "child_process";
|
||||||
|
export declare class Container {
|
||||||
|
callbacks: Array<() => void>;
|
||||||
|
ready: boolean;
|
||||||
|
id: string;
|
||||||
|
output: "";
|
||||||
|
bg_process: ChildProcessWithoutNullStreams;
|
||||||
|
constructor();
|
||||||
|
signalReady(): void;
|
||||||
|
onReady(callback: () => void): void;
|
||||||
|
waitReady(): Promise<void>;
|
||||||
|
close(): void;
|
||||||
|
closeSync(): void;
|
||||||
|
}
|
||||||
|
declare const _default: {
|
||||||
|
pool: Container[];
|
||||||
|
concurrency: number;
|
||||||
|
generateContainer(): void;
|
||||||
|
getContainer(): Container;
|
||||||
|
clear(): void;
|
||||||
|
};
|
||||||
|
export default _default;
|
2
@types/src/docker-args.d.ts
vendored
Normal file
2
@types/src/docker-args.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const DOCKER_ARGS: string[];
|
||||||
|
export const IMAGE_NAME: "headless-fox";
|
1
@types/src/index.d.ts
vendored
Normal file
1
@types/src/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
export {};
|
5
@types/src/memory.d.ts
vendored
Normal file
5
@types/src/memory.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import queue from "queue";
|
||||||
|
import ScreenshotRequest from "./request";
|
||||||
|
declare const q: queue;
|
||||||
|
declare const requests: Record<string, ScreenshotRequest | undefined>;
|
||||||
|
export { requests, q };
|
35
@types/src/request.d.ts
vendored
Normal file
35
@types/src/request.d.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
import { ChildProcessWithoutNullStreams } from "child_process";
|
||||||
|
export default class ScreenshotRequest {
|
||||||
|
url: string;
|
||||||
|
domains: string[];
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
output: string;
|
||||||
|
images: Record<string, unknown>[];
|
||||||
|
request_time: number;
|
||||||
|
started_time: number | null;
|
||||||
|
finished_time: number | null;
|
||||||
|
processing_took: number | null;
|
||||||
|
waiting_took: number | null;
|
||||||
|
process: ChildProcessWithoutNullStreams;
|
||||||
|
constructor(url: string, domains: string[]);
|
||||||
|
getJobsAhead(): number;
|
||||||
|
getJSON(): Promise<{
|
||||||
|
url: string;
|
||||||
|
domains: string[];
|
||||||
|
jobs_ahead: number;
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
output: string;
|
||||||
|
images: Record<string, unknown>[];
|
||||||
|
request_time: number;
|
||||||
|
started_time: number | null;
|
||||||
|
finished_time: number | null;
|
||||||
|
processing_took: number | null;
|
||||||
|
waiting_took: number | null;
|
||||||
|
elapsed_time_s: number;
|
||||||
|
}>;
|
||||||
|
setFinished(): void;
|
||||||
|
exec(): Promise<void>;
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
const queue = require("queue");
|
|
||||||
const { concurrency } = require("./config.json");
|
|
||||||
const q = queue({ concurrency, autostart: true, results: [] });
|
|
||||||
const requests = {};
|
|
||||||
|
|
||||||
module.exports = { q, requests };
|
|
3474
package-lock.json
generated
3474
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@ -2,10 +2,12 @@
|
|||||||
"name": "screenshot-service",
|
"name": "screenshot-service",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "## Dependencies",
|
"description": "## Dependencies",
|
||||||
"main": "index.js",
|
"main": "lib/src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install-daemon": "cp screenshot-service.service /etc/systemd/system/ && systemctl start screenshot-service && systemctl enable screenshot-service",
|
"install-daemon": "cp screenshot-service.service /etc/systemd/system/ && systemctl start screenshot-service && systemctl enable screenshot-service",
|
||||||
"start": "docker image build -t headless-fox Docker && BASE_URL=http://localhost:3000 node ."
|
"start": "npm run build && docker image build -t headless-fox Docker && BASE_URL=http://localhost:3000 node .",
|
||||||
|
"build": "tsc",
|
||||||
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -15,6 +17,8 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/router": "^10.1.1",
|
"@koa/router": "^10.1.1",
|
||||||
|
"@sealcode/ts-predicates": "^0.2.2",
|
||||||
|
"@types/node": "^18.0.0",
|
||||||
"amqplib": "^0.8.0",
|
"amqplib": "^0.8.0",
|
||||||
"koa": "^2.13.4",
|
"koa": "^2.13.4",
|
||||||
"koa-mount": "^4.0.0",
|
"koa-mount": "^4.0.0",
|
||||||
@ -22,5 +26,21 @@
|
|||||||
"qs": "^6.10.3",
|
"qs": "^6.10.3",
|
||||||
"queue": "^6.0.2",
|
"queue": "^6.0.2",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/koa": "^2.13.4",
|
||||||
|
"@types/koa__router": "^8.0.11",
|
||||||
|
"@types/koa-mount": "^4.0.1",
|
||||||
|
"@types/koa-static": "^4.0.2",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.29.3",
|
||||||
|
"@typescript-eslint/parser": "^4.29.3",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^3.4.1",
|
||||||
|
"eslint-plugin-with-tsc-error": "^0.0.7",
|
||||||
|
"prettier": "^2.3.2",
|
||||||
|
"source-map-support": "^0.5.19",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
119
request.js
119
request.js
@ -1,119 +0,0 @@
|
|||||||
const { q, requests } = require("./memory");
|
|
||||||
|
|
||||||
const DOCKER_ARGS = require("./docker-args");
|
|
||||||
const { v4: uuid } = require("uuid");
|
|
||||||
const { spawn } = require("child_process");
|
|
||||||
const containerPool = require("./container-pool");
|
|
||||||
|
|
||||||
let queue_order = [];
|
|
||||||
|
|
||||||
module.exports = class ScreenshotRequest {
|
|
||||||
constructor(url, domains) {
|
|
||||||
this.url = url;
|
|
||||||
this.domains = domains;
|
|
||||||
this.id = uuid();
|
|
||||||
this.status = "waiting";
|
|
||||||
this.output = "";
|
|
||||||
this.images = [];
|
|
||||||
this.request_time = Date.now();
|
|
||||||
this.started_time = null;
|
|
||||||
this.finished_time = null;
|
|
||||||
this.processing_took = null;
|
|
||||||
this.waiting_took = null;
|
|
||||||
q.push(async () => {
|
|
||||||
return this.exec();
|
|
||||||
});
|
|
||||||
requests[this.id] = this;
|
|
||||||
queue_order.push(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
getJobsAhead() {
|
|
||||||
if (this.status != "waiting") {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let count = 0;
|
|
||||||
for (const request of queue_order) {
|
|
||||||
if (request == this) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getJSON() {
|
|
||||||
return {
|
|
||||||
url: this.url,
|
|
||||||
domains: this.domains,
|
|
||||||
jobs_ahead: this.getJobsAhead(),
|
|
||||||
id: this.id,
|
|
||||||
status: this.status,
|
|
||||||
output: this.output,
|
|
||||||
images: this.images,
|
|
||||||
request_time: this.request_time,
|
|
||||||
started_time: this.started_time,
|
|
||||||
finished_time: this.finished_time,
|
|
||||||
processing_took: this.processing_took,
|
|
||||||
waiting_took: this.waiting_took,
|
|
||||||
elapsed_time_s: Math.round(
|
|
||||||
((this.status === "finished" ? this.finished_time : Date.now()) -
|
|
||||||
this.request_time) /
|
|
||||||
1000
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setFinished() {
|
|
||||||
this.status = "finished";
|
|
||||||
this.finished_time = Date.now();
|
|
||||||
this.processing_took = this.finished_time - this.started_time;
|
|
||||||
this.waiting_took = this.started_time - this.request_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
async exec() {
|
|
||||||
this.started_time = Date.now();
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
this.status = "running";
|
|
||||||
const container = containerPool.getContainer();
|
|
||||||
await container.waitReady();
|
|
||||||
this.process = spawn(
|
|
||||||
"docker",
|
|
||||||
[
|
|
||||||
"exec",
|
|
||||||
container.id,
|
|
||||||
"/opt/run-analysis.sh",
|
|
||||||
JSON.stringify({
|
|
||||||
url: this.url,
|
|
||||||
third_party_domains: this.domains,
|
|
||||||
}),
|
|
||||||
this.id,
|
|
||||||
],
|
|
||||||
{ cwd: process.cwd() }
|
|
||||||
);
|
|
||||||
this.process.on("close", (exitCode) => {
|
|
||||||
this.setFinished();
|
|
||||||
container.close();
|
|
||||||
queue_order = queue_order.filter((request) => request != this);
|
|
||||||
if (exitCode === 0) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.process.stdout.on("data", (d) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(d.toString());
|
|
||||||
if (parsed.new_file) {
|
|
||||||
this.images.push(parsed.new_file);
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
this.output += d.toString();
|
|
||||||
/* console.log("DATA!", d.toString()); */
|
|
||||||
});
|
|
||||||
this.process.stderr.on("data", (d) => {
|
|
||||||
this.output += d.toString();
|
|
||||||
/* console.log("STDERR!", d.toString()); */
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,11 +1,19 @@
|
|||||||
const { spawn, spawnSync } = require("child_process");
|
import { hasShape, predicates, is } from "@sealcode/ts-predicates";
|
||||||
const { IMAGE_NAME } = require("./docker-args");
|
import {
|
||||||
const { concurrency } = require("./config.json");
|
ChildProcessWithoutNullStreams,
|
||||||
|
spawn,
|
||||||
|
spawnSync,
|
||||||
|
} from "child_process";
|
||||||
|
import { concurrency } from "../config.json";
|
||||||
|
import { IMAGE_NAME } from "./docker-args";
|
||||||
|
|
||||||
class Container {
|
export class Container {
|
||||||
|
callbacks: Array<() => void> = [];
|
||||||
|
ready = false;
|
||||||
|
id: string;
|
||||||
|
output: "";
|
||||||
|
bg_process: ChildProcessWithoutNullStreams;
|
||||||
constructor() {
|
constructor() {
|
||||||
this.callbacks = [];
|
|
||||||
this.ready = false;
|
|
||||||
this.id = spawnSync(
|
this.id = spawnSync(
|
||||||
"docker",
|
"docker",
|
||||||
["run", "-d", "-v", `${process.cwd()}/static:/opt/static`, IMAGE_NAME],
|
["run", "-d", "-v", `${process.cwd()}/static:/opt/static`, IMAGE_NAME],
|
||||||
@ -15,51 +23,56 @@ class Container {
|
|||||||
)
|
)
|
||||||
.stdout.toString()
|
.stdout.toString()
|
||||||
.replace("\n", "");
|
.replace("\n", "");
|
||||||
this.output = "";
|
|
||||||
this.bg_process = spawn("docker", ["logs", "-f", this.id]);
|
this.bg_process = spawn("docker", ["logs", "-f", this.id]);
|
||||||
this.bg_process.stdout.on("data", (d) => {
|
this.bg_process.stdout.on("data", (d: Buffer) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(d.toString());
|
const parsed = JSON.parse(d.toString()) as unknown;
|
||||||
if (parsed.code == "ready") {
|
if (
|
||||||
|
is(parsed, predicates.object) &&
|
||||||
|
hasShape({ code: predicates.string }, parsed) &&
|
||||||
|
parsed.code == "ready"
|
||||||
|
) {
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
this.signalReady();
|
this.signalReady();
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
this.output += d.toString();
|
this.output += d.toString();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
signalReady() {
|
signalReady(): void {
|
||||||
this.callbacks.forEach((callback) => callback(this));
|
this.callbacks.forEach((callback) => callback());
|
||||||
}
|
}
|
||||||
|
|
||||||
onReady(callback) {
|
onReady(callback: () => void): void {
|
||||||
this.ready ? callback() : this.callbacks.push(callback);
|
this.ready ? callback() : this.callbacks.push(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitReady() {
|
async waitReady(): Promise<void> {
|
||||||
if (this.ready) {
|
if (this.ready) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.onReady(resolve);
|
this.onReady(resolve);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close(): void {
|
||||||
spawn("docker", ["rm", "-f", this.id]);
|
spawn("docker", ["rm", "-f", this.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeSync() {
|
closeSync(): void {
|
||||||
spawnSync("docker", ["rm", "-f", this.id]);
|
spawnSync("docker", ["rm", "-f", this.id]);
|
||||||
console.log("doker rm done", this.id);
|
console.log("doker rm done", this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new (class ContainerPool {
|
export default new (class ContainerPool {
|
||||||
constructor(concurrency) {
|
pool: Container[] = [];
|
||||||
|
constructor(public concurrency: number) {
|
||||||
this.concurrency = concurrency;
|
this.concurrency = concurrency;
|
||||||
this.pool = [];
|
|
||||||
for (let i = 1; i <= this.concurrency; i++) {
|
for (let i = 1; i <= this.concurrency; i++) {
|
||||||
this.generateContainer();
|
this.generateContainer();
|
||||||
}
|
}
|
||||||
@ -79,6 +92,9 @@ module.exports = new (class ContainerPool {
|
|||||||
throw new Error("pool is empty, try again!");
|
throw new Error("pool is empty, try again!");
|
||||||
}
|
}
|
||||||
const container = this.pool.shift(); // get and remove from pool the oldest container
|
const container = this.pool.shift(); // get and remove from pool the oldest container
|
||||||
|
if (!container) {
|
||||||
|
throw new Error("Pool was somehow empty!");
|
||||||
|
}
|
||||||
this.generateContainer();
|
this.generateContainer();
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
@ -1,33 +1,34 @@
|
|||||||
var serve = require("koa-static");
|
import Router from "@koa/router";
|
||||||
const { v4: uuid } = require("uuid");
|
import { hasShape, predicates } from "@sealcode/ts-predicates";
|
||||||
const Koa = require("koa");
|
import { spawn } from "child_process";
|
||||||
const Router = require("@koa/router");
|
import Koa from "koa";
|
||||||
const mount = require("koa-mount");
|
import mount from "koa-mount";
|
||||||
const qs = require("qs");
|
import serve from "koa-static";
|
||||||
const { Readable } = require("stream");
|
import qs from "qs";
|
||||||
const { spawn } = require("child_process");
|
import { Readable } from "stream";
|
||||||
const { requests } = require("./memory");
|
import { v4 as uuid } from "uuid";
|
||||||
const ScreenshotRequest = require("./request");
|
import { DOCKER_ARGS } from "./docker-args";
|
||||||
const { DOCKER_ARGS } = require("./docker-args");
|
import { requests } from "./memory";
|
||||||
|
import ScreenshotRequest from "./request";
|
||||||
|
|
||||||
const router = new Router();
|
const router = new Router();
|
||||||
|
|
||||||
// response
|
// response
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
const static = new Koa();
|
const Static = new Koa();
|
||||||
static.use(serve("./static"));
|
Static.use(serve("./static"));
|
||||||
|
|
||||||
app.use(mount("/static", static));
|
app.use(mount("/static", Static));
|
||||||
|
|
||||||
function attach(docker_id, output_stream) {
|
function attach(docker_id: string, output_stream: Readable) {
|
||||||
// to prevent browser timeout
|
// to prevent browser timeout
|
||||||
const interval = setInterval(() => output_stream.push("<span></span>"), 500);
|
const interval = setInterval(() => output_stream.push("<span></span>"), 500);
|
||||||
const task = spawn("docker", ["logs", "-f", docker_id]);
|
const task = spawn("docker", ["logs", "-f", docker_id]);
|
||||||
task.stdout.on("data", (d) => {
|
task.stdout.on("data", (d: Buffer) => {
|
||||||
output_stream.push(d);
|
output_stream.push(d);
|
||||||
console.log("DATA!", d.toString());
|
console.log("DATA!", d.toString());
|
||||||
});
|
});
|
||||||
task.stderr.on("data", (d) => {
|
task.stderr.on("data", (d: Buffer) => {
|
||||||
/* output_stream.push(d); */
|
/* output_stream.push(d); */
|
||||||
console.log("STDERR!", d.toString());
|
console.log("STDERR!", d.toString());
|
||||||
});
|
});
|
||||||
@ -91,21 +92,28 @@ router.get("/", async (ctx) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.get("/preview", async (ctx) => {
|
router.get("/preview", async (ctx) => {
|
||||||
const s = new Readable({ read() {} });
|
const response = new Readable({
|
||||||
|
read() {
|
||||||
|
/*noop */
|
||||||
|
},
|
||||||
|
});
|
||||||
// stream data
|
// stream data
|
||||||
ctx.response.set("content-type", "txt/html");
|
ctx.response.set("content-type", "txt/html");
|
||||||
ctx.type = "html"; // <-- THIS is the important step!
|
ctx.type = "html"; // <-- THIS is the important step!
|
||||||
ctx.body = s;
|
ctx.body = response;
|
||||||
ctx.body.push("<!doctype html>");
|
response.push("<!doctype html>");
|
||||||
const id = uuid();
|
const id = uuid();
|
||||||
ctx.body.push(
|
response.push(
|
||||||
`<img id="preview" width="1080" height="608" src="/static/${id}/preview.png?id=0"/><br/>`
|
`<img id="preview" width="1080" height="608" src="/static/${id}/preview.png?id=0"/><br/>`
|
||||||
);
|
);
|
||||||
ctx.body.push(/* HTML */ `<script>
|
response.push(/* HTML */ `<script>
|
||||||
window.interval = setInterval(() => (preview.src = preview.src + "0"), 500);
|
window.interval = setInterval(() => (preview.src = preview.src + "0"), 500);
|
||||||
</script>`);
|
</script>`);
|
||||||
const params = qs.parse(ctx.querystring);
|
const params = qs.parse(ctx.querystring);
|
||||||
s.push(`Got request to screenshot ${params.url}<pre>`);
|
if (!hasShape({ url: predicates.string }, params)) {
|
||||||
|
throw new Error("MISSING URL PARAM");
|
||||||
|
}
|
||||||
|
response.push(`Got request to screenshot ${params.url}<pre>`);
|
||||||
let docker_id = "";
|
let docker_id = "";
|
||||||
if (!params.url) {
|
if (!params.url) {
|
||||||
ctx.body = "specify url!";
|
ctx.body = "specify url!";
|
||||||
@ -120,25 +128,25 @@ router.get("/preview", async (ctx) => {
|
|||||||
],
|
],
|
||||||
{ cwd: process.cwd() }
|
{ cwd: process.cwd() }
|
||||||
);
|
);
|
||||||
starter.stdout.on("data", (data) => {
|
starter.stdout.on("data", (data: Buffer) => {
|
||||||
docker_id += data.toString().replace(/\n/g, "");
|
docker_id += data.toString().replace(/\n/g, "");
|
||||||
});
|
});
|
||||||
|
|
||||||
starter.on("close", () => {
|
starter.on("close", () => {
|
||||||
ctx.body.push("spawned " + docker_id);
|
response.push("spawned " + docker_id);
|
||||||
attach(docker_id, ctx.body);
|
attach(docker_id, ctx.body);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/api/requests", async (ctx) => {
|
router.post("/api/requests", async (ctx) => {
|
||||||
const params = qs.parse(ctx.querystring);
|
const params = qs.parse(ctx.querystring);
|
||||||
if (!params.url) {
|
if (!hasShape({ url: predicates.string }, params)) {
|
||||||
ctx.body = "Specify url";
|
ctx.body = "Specify url";
|
||||||
ctx.status = 422;
|
ctx.status = 422;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!params.domains) {
|
if (!hasShape({ domains: predicates.array(predicates.string) }, params)) {
|
||||||
ctx.body = "Specify domains";
|
ctx.body = "Specify domains as an array of strings";
|
||||||
ctx.status = 422;
|
ctx.status = 422;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
7
src/memory.ts
Normal file
7
src/memory.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import queue from "queue";
|
||||||
|
import { concurrency } from "../config.json";
|
||||||
|
import ScreenshotRequest from "./request";
|
||||||
|
const q = queue({ concurrency, autostart: true, results: [] });
|
||||||
|
const requests: Record<string, ScreenshotRequest | undefined> = {};
|
||||||
|
|
||||||
|
export { requests, q };
|
139
src/request.ts
Normal file
139
src/request.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import { is, predicates } from "@sealcode/ts-predicates";
|
||||||
|
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import containerPool from "./container-pool";
|
||||||
|
import { q, requests } from "./memory";
|
||||||
|
|
||||||
|
let queue_order: ScreenshotRequest[] = [];
|
||||||
|
|
||||||
|
export default class ScreenshotRequest {
|
||||||
|
public id = uuid();
|
||||||
|
public status = "waiting";
|
||||||
|
public output = "";
|
||||||
|
public images: Record<string, unknown>[] = [];
|
||||||
|
public request_time: number = Date.now();
|
||||||
|
public started_time: number | null = null;
|
||||||
|
public finished_time: number | null = null;
|
||||||
|
public processing_took: number | null = null;
|
||||||
|
public waiting_took: number | null = null;
|
||||||
|
public process: ChildProcessWithoutNullStreams;
|
||||||
|
|
||||||
|
constructor(public url: string, public domains: string[]) {
|
||||||
|
q.push(async () => {
|
||||||
|
return this.exec();
|
||||||
|
});
|
||||||
|
requests[this.id] = this;
|
||||||
|
queue_order.push(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getJobsAhead(): number {
|
||||||
|
if (this.status != "waiting") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let count = 0;
|
||||||
|
for (const request of queue_order) {
|
||||||
|
if (request == this) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getJSON(): Promise<{
|
||||||
|
url: string;
|
||||||
|
domains: string[];
|
||||||
|
jobs_ahead: number;
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
output: string;
|
||||||
|
images: Record<string, unknown>[];
|
||||||
|
request_time: number;
|
||||||
|
started_time: number | null;
|
||||||
|
finished_time: number | null;
|
||||||
|
processing_took: number | null;
|
||||||
|
waiting_took: number | null;
|
||||||
|
elapsed_time_s: number;
|
||||||
|
}> {
|
||||||
|
return {
|
||||||
|
url: this.url,
|
||||||
|
domains: this.domains,
|
||||||
|
jobs_ahead: this.getJobsAhead(),
|
||||||
|
id: this.id,
|
||||||
|
status: this.status,
|
||||||
|
output: this.output,
|
||||||
|
images: this.images,
|
||||||
|
request_time: this.request_time,
|
||||||
|
started_time: this.started_time,
|
||||||
|
finished_time: this.finished_time,
|
||||||
|
processing_took: this.processing_took,
|
||||||
|
waiting_took: this.waiting_took,
|
||||||
|
elapsed_time_s: Math.round(
|
||||||
|
((this.status === "finished" ? this.finished_time || -1 : Date.now()) -
|
||||||
|
this.request_time) /
|
||||||
|
1000
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setFinished(): void {
|
||||||
|
this.status = "finished";
|
||||||
|
this.finished_time = Date.now();
|
||||||
|
if (this.started_time) {
|
||||||
|
this.processing_took = this.finished_time - this.started_time;
|
||||||
|
this.waiting_took = this.started_time - this.request_time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(): Promise<void> {
|
||||||
|
this.started_time = Date.now();
|
||||||
|
this.status = "running";
|
||||||
|
const container = containerPool.getContainer();
|
||||||
|
await container.waitReady();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.process = spawn(
|
||||||
|
"docker",
|
||||||
|
[
|
||||||
|
"exec",
|
||||||
|
container.id,
|
||||||
|
"/opt/run-analysis.sh",
|
||||||
|
JSON.stringify({
|
||||||
|
url: this.url,
|
||||||
|
third_party_domains: this.domains,
|
||||||
|
}),
|
||||||
|
this.id,
|
||||||
|
],
|
||||||
|
{ cwd: process.cwd() }
|
||||||
|
);
|
||||||
|
this.process.on("close", (exitCode) => {
|
||||||
|
this.setFinished();
|
||||||
|
container.close();
|
||||||
|
queue_order = queue_order.filter((request) => request != this);
|
||||||
|
if (exitCode === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.process.stdout.on("data", (d: Buffer) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(d.toString()) as unknown;
|
||||||
|
if (
|
||||||
|
is(parsed, predicates.object) &&
|
||||||
|
is(parsed.new_file, predicates.object)
|
||||||
|
) {
|
||||||
|
this.images.push(parsed.new_file);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//noop
|
||||||
|
}
|
||||||
|
this.output += d.toString();
|
||||||
|
/* console.log("DATA!", d.toString()); */
|
||||||
|
});
|
||||||
|
this.process.stderr.on("data", (d: Buffer) => {
|
||||||
|
this.output += d.toString();
|
||||||
|
/* console.log("STDERR!", d.toString()); */
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
28
tsconfig.json
Normal file
28
tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"target": "ES6",
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": [
|
||||||
|
"ES6",
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"outDir": "lib",
|
||||||
|
"checkJs": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"declarationDir": "@types",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/*.test.ts"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user