UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in

342 lines (308 loc) • 11.7 kB
import { spawn } from "child_process"; import { NEEDLE_CLOUD_CLI_NAME } from "./cloud.js"; const port = 8424; const licenseServerUrl = `http://localhost:${port}/api/license`; const projectIdentifierUrl = `http://localhost:${port}/api/public_key`; const needleCloudApiEndpoint = "https://cloud.needle.tools/api"; /** * Replace license string - used for webpack * @param {string} code * @param {{team:string|undefined}} opts */ export async function replaceLicense(code, opts) { const index = code.indexOf("NEEDLE_ENGINE_LICENSE_TYPE"); if (index >= 0) { const licenseType = await resolveLicense(opts); if (!licenseType) { return code; } const end = code.indexOf(";", index); if (end >= 0) { const line = code.substring(index, end); const replaced = "NEEDLE_ENGINE_LICENSE_TYPE = \"" + licenseType + "\""; code = code.replace(line, replaced); return code; } } return code; } /** * Resolve the license using the needle engine licensing server * @param {{accessToken?:string, team?:string} | null} args * @returns {Promise<string | null>} */ export async function resolveLicense(args = null) { let accessToken = args?.accessToken; if (!accessToken && process.env.CI) { if (process.env.NEEDLE_CLOUD_TOKEN) { console.log("[needle-license] INFO: Using Needle Cloud access token from environment variable"); accessToken = process.env.NEEDLE_CLOUD_TOKEN; } else { console.warn("[needle-license] WARN: Missing NEEDLE_CLOUD_TOKEN for CI environment run"); } } if (accessToken) { const timeout = AbortSignal.timeout(10_000); const url = new URL(`${needleCloudApiEndpoint}/v1/account/get/licenses`); const res = await fetch(url, { method: "GET", signal: timeout, headers: { Authorization: `Bearer ${accessToken}`, "x-needle": "cli" } }).catch(err => { return { ok: false, error: err.message }; }); if ("error" in res) { console.error(`[needle-license] Could not fetch license from Needle Cloud API (${res.error})`); return null; } if (res.ok) { const text = await res.text(); return tryParseLicense(text); } else { console.error(`[needle-license] Could not fetch license from Needle Cloud API (${res.status})`); if (process.env.CI) { return null; } } } else if (process.env.CI) { const isGithubCI = process.env.GITHUB_ACTIONS; let message = "[needle-license] WARN: Missing NEEDLE_CLOUD_TOKEN for CI environment run."; if (isGithubCI) { const repositoryUrl = process.env.GITHUB_REPOSITORY; const url = `${repositoryUrl}/settings/secrets/actions`; message += `\nPlease add the token to your GitHub repository secrets: ${url}`; } console.warn(message); return null; } // Fallback to use CLI // Wait for the server to start await waitForLicenseServer(); const url = new URL(licenseServerUrl); if (args?.team) url.searchParams.append("org", args.team); if (accessToken) { url.searchParams.append("token", accessToken); } console.log(`[needle-license] INFO: Fetching license...`); const timeout = AbortSignal.timeout(10_000); const licenseResponse = await fetch(url.toString(), { method: "GET", signal: timeout }).catch(err => { if (err.cause?.code === "ECONNREFUSED") { return { error: "[needle-license] ERR: Failed to connect to license server (ECONNREFUSED)" }; } else { return { error: "[needle-license] ERR: Failed to fetch license." }; } }); if (!licenseResponse) { console.warn("[needle-license] WARN: Failed to fetch license"); return null; } else if ("error" in licenseResponse) { console.error(licenseResponse.error); return null; } else if (!licenseResponse.ok) { console.error("[needle-license] ERROR: Failed to fetch license"); return null; } const text = await licenseResponse.text(); return tryParseLicense(text); } /** * @param {string} str License string */ function tryParseLicense(str) { try { /** @type {{needle_engine_license:string}} */ const licenseJson = JSON.parse(str); console.log("\n"); if (licenseJson.needle_engine_license) { console.log(`[needle-license] INFO: Successfully received \"${licenseJson.needle_engine_license?.toUpperCase()}\" license`) return licenseJson.needle_engine_license; } if ("error" in licenseJson) { console.error(`[needle-license] ERROR in license check: \"${licenseJson.error}\"`); } else if (licenseJson.needle_engine_license == null) { return null; } else { console.warn("[needle-license] WARN: Received invalid license.", licenseJson); } return null; } catch (err) { console.error("[needle-license] ERROR: Failed to parse license response"); return null; } } /** * @param {string | undefined} project_id */ export async function getPublicIdentifier(project_id) { let accessToken = undefined; if (process.env.CI) { if (process.env.NEEDLE_CLOUD_TOKEN) { console.log("[needle-identifier] INFO: Using Needle Cloud access token from environment variable"); accessToken = process.env.NEEDLE_CLOUD_TOKEN; } const url = new URL(`${needleCloudApiEndpoint}/v1/account/public_key`); const body = { project_id: project_id || process.env.GITHUB_REPOSITORY || undefined, machine_id: process.env.GITHUB_REPOSITORY_ID || "unknown", } const timeout = AbortSignal.timeout(10_000); const res = await fetch(url, { method: "POST", signal: timeout, headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", "x-needle": "cli" }, body: JSON.stringify(body), }).catch(err => { return { ok: false, error: err.message }; }); if ("error" in res) { console.error(`[needle-identifier] Could not fetch project identifier from Needle Cloud API (${res.error})`); return null; } if (res.ok) { const text = await res.text(); try { /** @type {{public_key:string}} */ const json = JSON.parse(text); console.log(`[needle-identifier] INFO: Successfully received public project identifier`); return json.public_key; } catch (err) { console.error("[needle-identifier] ERROR: Failed to parse project identifier response"); return null; } } else { const message = await res.text(); console.error(`[needle-identifier] Could not fetch project identifier from Needle Cloud API (${res.status}, ${res.statusText}, ${message})`); return null; } return null; } // Wait for the server to start await waitForLicenseServer(); console.log(`[needle-identifier] INFO: Fetching project identifier...`); const url = new URL(projectIdentifierUrl); if (project_id) url.searchParams.append("project_id", project_id); const timeout = AbortSignal.timeout(10_000); const res = await fetch(projectIdentifierUrl, { method: "GET", signal: timeout }).catch(err => { if (err.cause?.code === "ECONNREFUSED") { return { error: "[needle-identifier] Could not connect to the license server: The connection was actively refused" }; } else { return { error: "[needle-identifier] ERR: Failed to fetch project identifier." }; } }) if (!res) { console.warn("[needle-identifier] WARN: Failed to fetch project identifier"); return null; } else if ("error" in res) { console.error(res.error); return null; } else if (!res.ok) { console.error("[needle-identifier] ERROR: Failed to fetch project identifier"); return null; } const text = await res.text(); try { /** @type {{public_key:string}} */ const json = JSON.parse(text); return json.public_key; } catch (err) { // TODO: report error to backend return null; } }; // If we run the build command without an editor and the license server is just being started // we need to to wait for the root URL to return a response async function waitForLicenseServer() { // Make sure the licensing server is running runCommand("npx", ["--yes", NEEDLE_CLOUD_CLI_NAME, "start-server"]); let attempts = 0; const maxAttempts = 10; do { const timeout = AbortSignal.timeout(1_000); const response = await fetch(licenseServerUrl, { method: "GET", signal: timeout }).catch(err => { if (err.cause.code === "ECONNREFUSED") { console.warn("[needle-license] WARN: Stop waiting for license server because the connection was actively refused. (ECONNREFUSED)"); attempts += maxAttempts; } }); if (response) { return true; } if (attempts === 0) { console.log("[needle-license] INFO: Waiting for license server to start..."); } attempts++; if (attempts <= maxAttempts) { await new Promise(res => setTimeout(res, 1000)); } } while (attempts < maxAttempts); return false; } /** * @param {string} processName * @param {string[]} args */ async function runCommand(processName, args) { const process = spawn(processName, [...args], { shell: true, timeout: 30_000 }); return new Promise((resolve, reject) => { process.on('close', (code) => { if (code === 0) { resolve(true); } else { console.warn(`[needle-license] WARN: \"${processName}\" process exited with code ${code}\nProcess Arguments: ${args.join(" ")}`); resolve(false); } }); process.on('error', (err) => { resolve(err); }); }); } // NODE 16 doesn't support fetch yet // function fetch(url, options) { // const module = url.startsWith("https") ? https : http; // return new Promise((resolve, reject) => { // module.get(url, options, (res) => { // let data = ''; // res.on('data', (chunk) => { // data += chunk; // }); // res.on('end', () => { // resolve(data); // }); // }).on("error", (err) => { // reject(err); // }); // }); // }