@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
191 lines (167 loc) • 5.54 kB
JavaScript
import { spawn } from "child_process";
import https from 'https';
import http from 'http';
const port = 8424;
const licenseServerUrl = `http://localhost:${port}/api/license`;
const projectIdentifierUrl = `http://localhost:${port}/api/public_key`;
/**
* 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) {
// Wait for the server to start
await waitForLicenseServer();
let accessToken = args?.accessToken;
if (process.env.NEEDLE_CLOUD_TOKEN) {
console.log("INFO: Using Needle Cloud access token from environment variable");
accessToken = process.env.NEEDLE_CLOUD_TOKEN;
}
const url = new URL(licenseServerUrl);
if (args?.team)
url.searchParams.append("org", args.team);
if (accessToken) {
url.searchParams.append("token", accessToken);
}
console.log(`INFO: Fetching license...`);
const licenseResponse = await fetch(url.toString(), {
method: "GET",
}).catch(console.error);
if (!licenseResponse) {
console.warn("WARN: Failed to fetch license");
return null;
}
try {
/** @type {{needle_engine_license:string}} */
const licenseJson = JSON.parse(licenseResponse);
console.log("\n");
if (licenseJson.needle_engine_license) {
console.log(`INFO: Successfully received \"${licenseJson.needle_engine_license?.toUpperCase()}\" license`)
return licenseJson.needle_engine_license;
}
if ("error" in licenseJson) {
console.error(`ERROR in license check: \"${licenseJson.error}\"`);
}
else if (licenseJson.needle_engine_license == null) {
return null;
}
else {
console.warn("WARN: Received invalid license.", licenseJson);
}
return null;
}
catch (err) {
console.error("ERROR: Failed to parse license response");
return null;
}
}
/**
* @param {string | undefined} project_id
*/
export async function getPublicIdentifier(project_id) {
// Wait for the server to start
await waitForLicenseServer();
const res = await fetch(projectIdentifierUrl, {
method: "GET",
});
if (!res) {
console.warn("WARN: Failed to fetch project identifier");
return null;
}
try {
/** @type {{public_key:string}} */
const json = JSON.parse(res);
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@main", "start-server"]);
let attempts = 0;
while (attempts < 10) {
const response = await fetch(licenseServerUrl, {
method: "GET",
}).catch(() => { /** ignore errors */ });
if (response) {
return true;
}
if (attempts === 0) {
console.log("INFO: Waiting for license server to start...");
}
attempts++;
await new Promise(res => setTimeout(res, 1000));
}
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(`WARN: ${processName} exited with code ${code}`);
resolve(false);
}
});
process.on('error', (err) => {
resolve(err);
});
});
}
/**
* @param {string} str
*/
function obscure(str) {
const start = str.substring(0, 3);
return start + "******";
}
// 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);
});
});
}