CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/lib/landing/software-specs.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { keys, map, sortBy, zipObject } from "lodash";
7
import { promises } from "node:fs";
8
import { basename } from "node:path";
9
10
import {
11
SOFTWARE_ENV_NAMES,
12
SoftwareEnvNames,
13
} from "@cocalc/util/consts/software-envs";
14
import { hours_ago } from "@cocalc/util/relative-time";
15
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
16
import withCustomize from "lib/with-customize";
17
import { SOFTWARE_FALLBACK, SOFTWARE_URLS } from "./software-data";
18
import {
19
ComputeComponents,
20
ComputeInventory,
21
EnvData,
22
LanguageName,
23
SoftwareSpec,
24
} from "./types";
25
26
const { readFile } = promises;
27
28
async function makeObject(keys, fn) {
29
return zipObject(keys, await Promise.all(map(keys, fn)));
30
}
31
32
type SoftwareEnvironments = { [key in SoftwareEnvNames]: EnvData };
33
34
let SoftwareEnvSpecs: SoftwareEnvironments | null = null;
35
let SoftwareEnvDownloadedTimestamp: number = 0;
36
37
async function file2json(path: string): Promise<any> {
38
const data = await readFile(path, "utf8");
39
return JSON.parse(data);
40
}
41
42
async function downloadInventoryJson(name: SoftwareEnvNames): Promise<EnvData> {
43
try {
44
const raw = await fetch(SOFTWARE_URLS[name]);
45
if (!raw.ok) {
46
console.log(`Problem downloading: ${raw.status}: ${raw.statusText}`);
47
} else {
48
const data = await raw.json();
49
console.log(`Downloaded software inventory ${name} successfully`);
50
return data;
51
}
52
} catch (err) {
53
console.log(`Problem downloading: ${err}`);
54
}
55
return SOFTWARE_FALLBACK[name] as EnvData;
56
}
57
58
// load the current version of the software specs – if there is a problem, use the locally stored files as fallback.
59
// both files go hand-in-hand, hence either both work or both are the fallback!
60
async function fetchInventory(): Promise<SoftwareEnvironments> {
61
// for development, set the env variable to directory, where this files are
62
const localSpec = process.env.COCALC_SOFTWARE_ENVIRONMENTS;
63
if (localSpec != null) {
64
// read compute-inventory.json and compute-components.json from the local filesystem
65
console.log(`Reading inventory information from directory ${localSpec}`);
66
return await makeObject(
67
SOFTWARE_ENV_NAMES,
68
async (name) =>
69
await file2json(`${localSpec}/software-inventory-${name}.json`),
70
);
71
}
72
try {
73
// download the files for the newest information from the server
74
const ret = await makeObject(
75
SOFTWARE_ENV_NAMES,
76
async (name) => await downloadInventoryJson(name),
77
);
78
return ret;
79
} catch (err) {
80
console.error(`Problem fetching software inventory: ${err}`);
81
return SOFTWARE_FALLBACK;
82
}
83
}
84
85
const fetchSoftwareSpec = reuseInFlight(async function () {
86
SoftwareEnvSpecs = await fetchInventory();
87
SoftwareEnvDownloadedTimestamp = Date.now();
88
return SoftwareEnvSpecs;
89
});
90
91
/**
92
* get a cached copy of the software specs
93
*/
94
async function getSoftwareInfo(name: SoftwareEnvNames): Promise<EnvData> {
95
// if SoftwareEnvSpecs is not set or not older than one hour, fetch it
96
if (SoftwareEnvSpecs != null) {
97
if (SoftwareEnvDownloadedTimestamp > hours_ago(1).getTime()) {
98
// fresh enough, just return it
99
return SoftwareEnvSpecs[name];
100
} else {
101
// we asynchroneously fetch to refresh, but return the data immediately to the client
102
fetchSoftwareSpec();
103
return SoftwareEnvSpecs[name];
104
}
105
} else {
106
const specs = await fetchSoftwareSpec();
107
return specs[name];
108
}
109
}
110
111
async function getSoftwareInfoLang(
112
name: SoftwareEnvNames,
113
lang: LanguageName,
114
): Promise<{
115
inventory: ComputeInventory[LanguageName];
116
components: ComputeComponents[LanguageName];
117
timestamp: string;
118
}> {
119
const { inventory, data, timestamp } = await getSoftwareInfo(name);
120
return { inventory: inventory[lang], components: data[lang], timestamp };
121
}
122
123
// during startup, we fetch getSoftwareSpec() once to warm up the cache…
124
(async function () {
125
fetchSoftwareSpec(); // not blocking
126
})();
127
128
// cached processed software specs
129
let SPEC: Record<SoftwareEnvNames, Readonly<SoftwareSpec> | null> = {} as any;
130
131
async function getSoftwareSpec(name: SoftwareEnvNames): Promise<SoftwareSpec> {
132
const cached = SPEC[name];
133
if (cached != null) return cached;
134
const nextSpec: Partial<SoftwareSpec> = {};
135
const { inventory } = await getSoftwareInfo(name);
136
for (const cmd in inventory.language_exes) {
137
const info = inventory.language_exes[cmd];
138
if (nextSpec[info.lang] == null) {
139
nextSpec[info.lang] = {};
140
}
141
// the basename of the cmd path
142
const base = cmd.indexOf(" ") > 0 ? cmd : basename(cmd);
143
nextSpec[info.lang][base] = {
144
cmd,
145
name: info.name,
146
doc: info.doc,
147
url: info.url,
148
path: info.path,
149
};
150
}
151
SPEC[name] = nextSpec as SoftwareSpec;
152
return nextSpec as SoftwareSpec;
153
}
154
155
/**
156
* This determines the order of columns when there is more than on executable for a language.
157
*/
158
function getLanguageExecutables({ lang, inventory }): string[] {
159
if (inventory == null) return [];
160
return sortBy(keys(inventory[lang]), (x: string) => {
161
if (lang === "python") {
162
if (x.endsWith("python3")) return ["0", x];
163
if (x.indexOf("sage") >= 0) return ["2", x];
164
if (x.endsWith("python2")) return ["3", x];
165
return ["1", x]; // anaconda envs and others
166
} else {
167
return x.toLowerCase();
168
}
169
});
170
}
171
172
// this is for the server side getServerSideProps function
173
export async function withCustomizedAndSoftwareSpec(
174
context,
175
lang: LanguageName | "executables",
176
) {
177
const { name } = context.params;
178
179
// if name is not in SOFTWARE_ENV_NAMES, return {notFound : true}
180
if (!SOFTWARE_ENV_NAMES.includes(name)) {
181
return { notFound: true };
182
}
183
184
const [customize, spec] = await Promise.all([
185
withCustomize({ context }),
186
getSoftwareSpec(name),
187
]);
188
189
customize.props.name = name;
190
191
if (lang === "executables") {
192
// this is instant because specs are already in the cache
193
const softwareInfo = await getSoftwareInfo(name);
194
const { inventory, timestamp } = softwareInfo;
195
customize.props.executablesSpec = inventory.executables;
196
customize.props.timestamp = timestamp;
197
return customize;
198
} else {
199
customize.props.spec = spec[lang];
200
// this is instant because specs are already in the cache
201
const { inventory, components, timestamp } = await getSoftwareInfoLang(
202
name,
203
lang,
204
);
205
customize.props.inventory = inventory;
206
customize.props.components = components;
207
customize.props.timestamp = timestamp;
208
}
209
210
// at this point, lang != "executables"
211
// we gather the list of interpreters (executables) for the given language
212
const { inventory } = await getSoftwareInfo(name);
213
customize.props.execInfo = {};
214
for (const cmd of getLanguageExecutables({ inventory, lang })) {
215
const path = inventory.language_exes[cmd]?.path ?? cmd;
216
customize.props.execInfo[path] = inventory.executables?.[path] ?? null;
217
}
218
219
return customize;
220
}
221
222