Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/lib/landing/software-specs.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { keys, map, sortBy, zipObject } from "lodash";6import { promises } from "node:fs";7import { basename } from "node:path";89import {10SOFTWARE_ENV_NAMES,11SoftwareEnvNames,12} from "@cocalc/util/consts/software-envs";13import { hours_ago } from "@cocalc/util/relative-time";14import { reuseInFlight } from "@cocalc/util/reuse-in-flight";15import withCustomize from "lib/with-customize";16import { SOFTWARE_FALLBACK, SOFTWARE_URLS } from "./software-data";17import {18ComputeComponents,19ComputeInventory,20EnvData,21LanguageName,22SoftwareSpec,23} from "./types";2425const { readFile } = promises;2627async function makeObject(keys, fn) {28return zipObject(keys, await Promise.all(map(keys, fn)));29}3031type SoftwareEnvironments = { [key in SoftwareEnvNames]: EnvData };3233let SoftwareEnvSpecs: SoftwareEnvironments | null = null;34let SoftwareEnvDownloadedTimestamp: number = 0;3536async function file2json(path: string): Promise<any> {37const data = await readFile(path, "utf8");38return JSON.parse(data);39}4041async function downloadInventoryJson(name: SoftwareEnvNames): Promise<EnvData> {42try {43const raw = await fetch(SOFTWARE_URLS[name]);44if (!raw.ok) {45console.log(`Problem downloading: ${raw.status}: ${raw.statusText}`);46} else {47const data = await raw.json();48console.log(`Downloaded software inventory ${name} successfully`);49return data;50}51} catch (err) {52console.log(`Problem downloading: ${err}`);53}54return SOFTWARE_FALLBACK[name] as EnvData;55}5657// load the current version of the software specs – if there is a problem, use the locally stored files as fallback.58// both files go hand-in-hand, hence either both work or both are the fallback!59async function fetchInventory(): Promise<SoftwareEnvironments> {60// for development, set the env variable to directory, where this files are61const localSpec = process.env.COCALC_SOFTWARE_ENVIRONMENTS;62if (localSpec != null) {63// read compute-inventory.json and compute-components.json from the local filesystem64console.log(`Reading inventory information from directory ${localSpec}`);65return await makeObject(66SOFTWARE_ENV_NAMES,67async (name) =>68await file2json(`${localSpec}/software-inventory-${name}.json`),69);70}71try {72// download the files for the newest information from the server73const ret = await makeObject(74SOFTWARE_ENV_NAMES,75async (name) => await downloadInventoryJson(name),76);77return ret;78} catch (err) {79console.error(`Problem fetching software inventory: ${err}`);80return SOFTWARE_FALLBACK;81}82}8384const fetchSoftwareSpec = reuseInFlight(async function () {85SoftwareEnvSpecs = await fetchInventory();86SoftwareEnvDownloadedTimestamp = Date.now();87return SoftwareEnvSpecs;88});8990/**91* get a cached copy of the software specs92*/93async function getSoftwareInfo(name: SoftwareEnvNames): Promise<EnvData> {94// if SoftwareEnvSpecs is not set or not older than one hour, fetch it95if (SoftwareEnvSpecs != null) {96if (SoftwareEnvDownloadedTimestamp > hours_ago(1).getTime()) {97// fresh enough, just return it98return SoftwareEnvSpecs[name];99} else {100// we asynchroneously fetch to refresh, but return the data immediately to the client101fetchSoftwareSpec();102return SoftwareEnvSpecs[name];103}104} else {105const specs = await fetchSoftwareSpec();106return specs[name];107}108}109110async function getSoftwareInfoLang(111name: SoftwareEnvNames,112lang: LanguageName,113): Promise<{114inventory: ComputeInventory[LanguageName];115components: ComputeComponents[LanguageName];116timestamp: string;117}> {118const { inventory, data, timestamp } = await getSoftwareInfo(name);119return { inventory: inventory[lang], components: data[lang], timestamp };120}121122// during startup, we fetch getSoftwareSpec() once to warm up the cache…123(async function () {124fetchSoftwareSpec(); // not blocking125})();126127// cached processed software specs128let SPEC: Record<SoftwareEnvNames, Readonly<SoftwareSpec> | null> = {} as any;129130async function getSoftwareSpec(name: SoftwareEnvNames): Promise<SoftwareSpec> {131const cached = SPEC[name];132if (cached != null) return cached;133const nextSpec: Partial<SoftwareSpec> = {};134const { inventory } = await getSoftwareInfo(name);135for (const cmd in inventory.language_exes) {136const info = inventory.language_exes[cmd];137if (nextSpec[info.lang] == null) {138nextSpec[info.lang] = {};139}140// the basename of the cmd path141const base = cmd.indexOf(" ") > 0 ? cmd : basename(cmd);142nextSpec[info.lang][base] = {143cmd,144name: info.name,145doc: info.doc,146url: info.url,147path: info.path,148};149}150SPEC[name] = nextSpec as SoftwareSpec;151return nextSpec as SoftwareSpec;152}153154/**155* This determines the order of columns when there is more than on executable for a language.156*/157function getLanguageExecutables({ lang, inventory }): string[] {158if (inventory == null) return [];159return sortBy(keys(inventory[lang]), (x: string) => {160if (lang === "python") {161if (x.endsWith("python3")) return ["0", x];162if (x.indexOf("sage") >= 0) return ["2", x];163if (x.endsWith("python2")) return ["3", x];164return ["1", x]; // anaconda envs and others165} else {166return x.toLowerCase();167}168});169}170171// this is for the server side getServerSideProps function172export async function withCustomizedAndSoftwareSpec(173context,174lang: LanguageName | "executables",175) {176const { name } = context.params;177178// if name is not in SOFTWARE_ENV_NAMES, return {notFound : true}179if (!SOFTWARE_ENV_NAMES.includes(name)) {180return { notFound: true };181}182183const [customize, spec] = await Promise.all([184withCustomize({ context }),185getSoftwareSpec(name),186]);187188customize.props.name = name;189190if (lang === "executables") {191// this is instant because specs are already in the cache192const softwareInfo = await getSoftwareInfo(name);193const { inventory, timestamp } = softwareInfo;194customize.props.executablesSpec = inventory.executables;195customize.props.timestamp = timestamp;196return customize;197} else {198customize.props.spec = spec[lang];199// this is instant because specs are already in the cache200const { inventory, components, timestamp } = await getSoftwareInfoLang(201name,202lang,203);204customize.props.inventory = inventory;205customize.props.components = components;206customize.props.timestamp = timestamp;207}208209// at this point, lang != "executables"210// we gather the list of interpreters (executables) for the given language211const { inventory } = await getSoftwareInfo(name);212customize.props.execInfo = {};213for (const cmd of getLanguageExecutables({ inventory, lang })) {214const path = inventory.language_exes[cmd]?.path ?? cmd;215customize.props.execInfo[path] = inventory.executables?.[path] ?? null;216}217218return customize;219}220221222