querysub 0.199.0 → 0.201.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- require("typenode");
4
- require("../src/deployManager/applyAlwaysUpMain");
3
+ require("../src/deployManager/applyAlwaysUpMain.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.199.0",
3
+ "version": "0.201.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -12,13 +12,13 @@ function createHookFunction<Fnc extends (...args: any[]) => void>(debugName: str
12
12
  } {
13
13
  let queuedCalls = [] as Parameters<Fnc>[];
14
14
  let declaration: Fnc | undefined;
15
- setImmediate(() => {
15
+ setTimeout(() => {
16
16
  if (!declaration) {
17
17
  setTimeout(flushQueued, 1000);
18
18
  return;
19
19
  }
20
20
  flushQueued();
21
- });
21
+ }, 1000);
22
22
  function flushQueued() {
23
23
  if (!declaration) {
24
24
  throw new Error(`Hook function ${debugName} not declared`);
@@ -912,8 +912,9 @@ class AuthorityPathValueStorage {
912
912
  public async onShutdown() {
913
913
  if (yargObj.noarchive) return;
914
914
  this.shuttingDown.resolve("shutdown");
915
+ if (this.pendingArchiveValues.size === 0) return;
915
916
  await pathValueAuthority2.waitUntilRoutingIsReady();
916
- // If there are other nodes, we CAN'T just archive our values, as some may be rejected in the future.
917
+ // If there are other nodes, we CAN'T just archive our values, as some maybe rejected in the future.
917
918
  let nodes = pathValueAuthority2.getReadNodes(rootPathStr);
918
919
  let otherReadNodes = nodes.filter(x => !isOwnNodeId(x));
919
920
  if (otherReadNodes.length > 0 && this.pendingArchiveValues.size > 0) {
@@ -23,6 +23,7 @@ export const setGitRef = measureWrap(async function setGitRef(config: {
23
23
  repoUrl: string;
24
24
  gitRef: string;
25
25
  }) {
26
+ await fs.promises.mkdir(config.gitFolder, { recursive: true });
26
27
  let hostKey = await runPromise(`ssh-keyscan -t rsa bitbucket.org`);
27
28
  hostKey = hostKey.split("\n").filter(x => !x.startsWith("#")).join("\n");
28
29
  let knownHostsPath = os.homedir() + "/.ssh/known_hosts";
@@ -15,8 +15,8 @@ export class MachinesPage extends qreact.Component {
15
15
  //.bord2(0, 0, 20)
16
16
  }>
17
17
  {[
18
+ { key: "machines", label: "Machines", otherKeys: ["machine-detail"] },
18
19
  { key: "services", label: "Services", otherKeys: ["service-detail"] },
19
- { key: "machines", label: "Machines", otherKeys: ["machine-detail"] }
20
20
  ].map(tab => {
21
21
  let isActive = currentViewParam.value === tab.key || tab.otherKeys.includes(currentViewParam.value);
22
22
  return <div key={tab.key}
@@ -1,7 +1,8 @@
1
1
  const child_process = require("child_process");
2
2
  const fs = require("fs");
3
+ const os = require("os");
3
4
 
4
- export function shellEscape(array) {
5
+ function shellEscape(array) {
5
6
  return array
6
7
  .map((value) => {
7
8
  if (/[^A-Za-z0-9_\/:=-]/.test(value)) {
@@ -50,23 +51,22 @@ async function runCommandWithLogging(command) {
50
51
  });
51
52
 
52
53
  childProc.on("error", (err) => {
53
- reject(combinedOutput.slice(-1000));
54
+ reject(`Always up machine process error: ${err}\nRecent output:${combinedOutput.slice(-1000)}`);
54
55
  });
55
56
 
56
57
  childProc.on("close", (code) => {
57
- reject(`${combinedOutput.slice(-1000)}\nProcess exited with code ${code}, recent output:`);
58
+ reject(`Always up machine process exited with code ${code}, recent output:${combinedOutput.slice(-1000)}`);
58
59
  });
59
60
  });
60
61
  }
61
62
 
62
63
  async function main() {
63
- let args = process.argv.slice(2);
64
64
  while (true) {
65
65
  try {
66
- await runCommandWithLogging(shellEscape(args));
66
+ await runCommandWithLogging("yarn machine");
67
67
  } catch (e) {
68
68
  console.error(`Process finished, restarting in 5 seconds`);
69
- fs.writeFileSync("lastAlwaysUpError.txt", e.stack);
69
+ await fs.promises.writeFile(os.homedir() + "/lastAlwaysUpError.txt", e?.stack || e);
70
70
  }
71
71
  await delay(5000);
72
72
  }
@@ -52,7 +52,6 @@ export class MachinesListPage extends qreact.Component {
52
52
  <button
53
53
  className={css.pad2(12, 8).button.bord2(0, 0, 20)
54
54
  + (hasSelectedMachines ? css.hsl(0, 80, 90) : css.hsl(0, 0, 80))}
55
- disabled={!hasSelectedMachines || this.state.isDeleting}
56
55
  onClick={() => {
57
56
  if (!hasSelectedMachines) return;
58
57
 
@@ -111,14 +110,13 @@ export class MachinesListPage extends qreact.Component {
111
110
 
112
111
  return <div key={machineId}
113
112
  className={
114
- css.pad2(12).bord2(0, 0, 20)
115
- + (this.state.isDeleteMode ? "" : css.button)
113
+ css.pad2(12).bord2(0, 0, 20).button
116
114
  + (
117
115
  failingServices.length > 0 && css.hsl(0, 50, 60)
118
116
  || isMachineDead && css.hsl(0, 0, 50)
119
117
  || css.hsl(0, 0, 100)
120
118
  )
121
- + (this.state.isDeleteMode && isSelected ? css.bord2(200, 80, 60, 2) : "")
119
+ + (this.state.isDeleteMode && isSelected && css.bord2(200, 80, 60, 2))
122
120
  }
123
121
  onClick={() => {
124
122
  if (this.state.isDeleteMode) {
@@ -69,11 +69,15 @@ export class ServicesListPage extends qreact.Component {
69
69
  let serviceInfo = machineInfo?.services[serviceId];
70
70
  return serviceInfo && !serviceInfo.errorFromLastRun;
71
71
  });
72
+ let missingMachines = config.machineIds.filter(machineId => {
73
+ let machineInfo = getMachineInfo(machineId);
74
+ return !machineInfo?.services[serviceId];
75
+ });
72
76
  let totalLaunches = config.machineIds.reduce((acc, machineId) => {
73
77
  let machineInfo = getMachineInfo(machineId);
74
78
  return acc + (machineInfo?.services[serviceId].totalTimesLaunched || 0);
75
79
  }, 0);
76
- let notFailingNotRunning = config.machineIds.length - runningMachines.length - failingMachines.length;
80
+ let unknown = config.machineIds.length - runningMachines.length - failingMachines.length - missingMachines.length;
77
81
  return <div key={serviceId}
78
82
  className={
79
83
  css.pad2(12).button.bord2(0, 0, 20)
@@ -92,7 +96,7 @@ export class ServicesListPage extends qreact.Component {
92
96
  <div className={css.fontSize(14).boldStyle}>{config.info.title}</div>
93
97
  <div>{config.parameters.key}</div>
94
98
  <div>
95
- {config.machineIds.length} configured {failingMachines.length > 0 && `(${failingMachines.length} failing)` || notFailingNotRunning > 0 && `(${notFailingNotRunning} not fail and not running)`} • {totalLaunches} launches •
99
+ {config.machineIds.length} configured {failingMachines.length > 0 && `(${failingMachines.length} failing)`} {missingMachines.length > 0 && `(${missingMachines.length} missing machines)`} {unknown > 0 && `(${unknown} unknown)`} • {totalLaunches} launches •
96
100
  Deploy: {config.parameters.deploy ? "enabled" : "disabled"}
97
101
  </div>
98
102
  </div>
@@ -0,0 +1,3 @@
1
+ #! /bin/bash
2
+ tmux new -s machine-alwaysup -d
3
+ tmux send-keys -t machine-alwaysup "cd ~/machine-alwaysup && yarn machine-alwaysup" Enter
@@ -321,17 +321,8 @@ export async function machineApplyMain() {
321
321
  // Wait for the console to get shimmed
322
322
  await delay(1000);
323
323
 
324
- if (true) {
325
- console.log(`Dying in a bit`);
326
- setTimeout(() => {
327
- void shutdown();
328
- }, 10000);
329
- return;
330
- }
331
-
332
- console.error("Apply special error test");
333
-
334
- let lastErrorPath = "lastAlwaysUpError.txt";
324
+ // NOTE: Error's don't get logged unless we host, so... we can't do this earlier than this...
325
+ let lastErrorPath = os.homedir() + "/lastAlwaysUpError.txt";
335
326
  if (fs.existsSync(lastErrorPath)) {
336
327
  let lastError = await fs.promises.readFile(lastErrorPath, "utf8");
337
328
  await fs.promises.unlink(lastErrorPath);
@@ -351,6 +342,7 @@ export async function machineApplyMain() {
351
342
  }
352
343
  }
353
344
  await fs.promises.writeFile(isRunningPath, `${process.ppid}-${process.pid}`);
345
+
354
346
  await Querysub.hostService("machine-apply");
355
347
  onServiceConfigChange(resyncServices);
356
348
  }
@@ -1,3 +1,309 @@
1
- //todonext
2
- // The only arg is the remoteIP (and optionally username, I guess...)
3
- // Run a lot of the commands remotely, one at a time. I guess make a runSSH helper function
1
+ import { getBackblazePath } from "../-a-archives/archivesBackBlaze";
2
+ import { getGitURLLive, getGitRefLive } from "../4-deploy/git";
3
+ import { Querysub } from "../4-querysub/QuerysubController";
4
+ import { runPromise } from "../functional/runCommand";
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import readline from "readline";
8
+ import open from "open";
9
+ // Import querysub, to fix missing dependencies
10
+ Querysub;
11
+
12
+ const pinnedNodeVersion = 22;
13
+
14
+ async function getGitHubApiKey(repoUrl: string, sshRemote: string): Promise<string> {
15
+ // Parse repository info from URL
16
+ let repoOwner = "";
17
+ let repoName = "";
18
+ // Handle both SSH and HTTPS formats
19
+ // SSH: git@github.com:owner/repo.git
20
+ // HTTPS: https://github.com/owner/repo.git
21
+ const sshMatch = repoUrl.match(/git@github\.com:([^/]+)\/(.+)\.git$/);
22
+ const httpsMatch = repoUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+)\.git$/);
23
+
24
+ if (sshMatch) {
25
+ repoOwner = sshMatch[1];
26
+ repoName = sshMatch[2];
27
+ } else if (httpsMatch) {
28
+ repoOwner = httpsMatch[1];
29
+ repoName = httpsMatch[2];
30
+ }
31
+
32
+ const cacheFile = os.homedir() + `/githubkey_${repoOwner}_${repoName}.json`;
33
+
34
+ // Check if we have a cached key
35
+ if (fs.existsSync(cacheFile)) {
36
+ try {
37
+ const cached = JSON.parse(fs.readFileSync(cacheFile, "utf8"));
38
+ if (cached.apiKey) {
39
+ console.log(`✅ Using cached GitHub API key from ${cacheFile}`);
40
+ return cached.apiKey;
41
+ }
42
+ } catch {
43
+ // Invalid cache file, we'll ask for a new key
44
+ }
45
+ }
46
+
47
+ // Need to get a new API key from user
48
+ console.log("\n🔑 GitHub API key required for private repository access");
49
+ console.log("Opening GitHub token creation page...");
50
+
51
+ // Construct URL for classic token (fine-grained tokens don't support deploy keys)
52
+ const repoInfo = repoOwner && repoName ? ` for repository ${repoOwner}/${repoName}` : "";
53
+ const instructions = `yarn setup-machine ${sshRemote}${repoInfo}
54
+ 1) Set expiration to 'No expiration'
55
+ 2) Check the 'repo' scope (full repository access)
56
+ 3) Click 'Generate token'`;
57
+
58
+ let tokenUrl = `https://github.com/settings/tokens/new?description=${encodeURIComponent(instructions)}&scopes=repo`;
59
+ if (repoOwner && repoName) {
60
+ console.log(`Setting up access for repository: ${repoOwner}/${repoName}`);
61
+ }
62
+
63
+ await open(tokenUrl);
64
+
65
+ const rl = readline.createInterface({
66
+ input: process.stdin,
67
+ output: process.stdout
68
+ });
69
+
70
+ const apiKey = await new Promise<string>((resolve) => {
71
+ const repoInfo = repoOwner && repoName ? ` for repository ${repoOwner}/${repoName}` : "";
72
+ rl.question(`Please paste your GitHub API token (${repoInfo}): `, (answer) => {
73
+ rl.close();
74
+ resolve(answer.trim());
75
+ });
76
+ });
77
+
78
+ // Cache the key
79
+ fs.writeFileSync(cacheFile, JSON.stringify({ apiKey }));
80
+ console.log(`✅ Caching GitHub API key to ${cacheFile}`);
81
+
82
+ return apiKey;
83
+ }
84
+
85
+ async function addDeployKeyToGitHub(sshPublicKey: string, keyTitle: string, repoUrl: string, sshRemote: string): Promise<void> {
86
+ const apiKey = await getGitHubApiKey(repoUrl, sshRemote);
87
+
88
+ // Parse repository info from URL to get owner/repo for the API endpoint
89
+ let repoOwner = "";
90
+ let repoName = "";
91
+ const sshMatch = repoUrl.match(/git@github\.com:([^/]+)\/(.+)\.git$/);
92
+ const httpsMatch = repoUrl.match(/https:\/\/github\.com\/([^/]+)\/(.+)\.git$/);
93
+
94
+ if (sshMatch) {
95
+ repoOwner = sshMatch[1];
96
+ repoName = sshMatch[2];
97
+ } else if (httpsMatch) {
98
+ repoOwner = httpsMatch[1];
99
+ repoName = httpsMatch[2];
100
+ } else {
101
+ throw new Error(`Could not parse GitHub repository from URL: ${repoUrl}`);
102
+ }
103
+
104
+ let url = `https://api.github.com/repos/${repoOwner}/${repoName}/keys`;
105
+ console.log(url);
106
+ const response = await fetch(url, {
107
+ method: "POST",
108
+ headers: {
109
+ "Authorization": `Bearer ${apiKey}`,
110
+ "Accept": "application/vnd.github+json",
111
+ "Content-Type": "application/json"
112
+ },
113
+ body: JSON.stringify({
114
+ title: keyTitle,
115
+ key: sshPublicKey,
116
+ read_only: true
117
+ })
118
+ });
119
+
120
+ if (!response.ok) {
121
+ const errorText = await response.text();
122
+ throw new Error(`Failed to add deploy key to GitHub repository: ${response.status} ${errorText}`);
123
+ }
124
+
125
+ console.log("✅ Deploy key added to GitHub repository");
126
+ }
127
+
128
+ async function setupRepositoryOnRemote(sshRemote: string, gitURLLive: string, gitRefLive: string): Promise<void> {
129
+ // Create git folder on remote
130
+ await runPromise(`ssh ${sshRemote} "mkdir -p ~/machine-alwaysup"`);
131
+
132
+ // Add bitbucket.org host key to remote machine's known_hosts
133
+ await runPromise(`ssh ${sshRemote} "mkdir -p ~/.ssh"`);
134
+ await runPromise(`ssh ${sshRemote} "ssh-keyscan -t rsa bitbucket.org >> ~/.ssh/known_hosts 2>/dev/null || true"`);
135
+ await runPromise(`ssh ${sshRemote} "ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts 2>/dev/null || true"`);
136
+
137
+ // Check if repo already exists, if not clone it
138
+ try {
139
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git status"`);
140
+ console.log("Repository already exists, updating...");
141
+ // Repository exists, update it
142
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git remote update"`);
143
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git add --all"`);
144
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git stash"`);
145
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git fetch --all"`);
146
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git reset --hard ${gitRefLive}"`);
147
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git prune"`);
148
+ } catch {
149
+ console.log("Cloning repository...");
150
+ // Repository doesn't exist, clone it
151
+ await runPromise(`ssh ${sshRemote} "git clone ${gitURLLive} ~/machine-alwaysup"`);
152
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && git reset --hard ${gitRefLive}"`);
153
+ }
154
+ }
155
+
156
+ async function main() {
157
+ let sshRemote = process.argv.slice(2).join(" ");
158
+ if (!sshRemote) {
159
+ console.error("Incorrect usage. Examples:\nyarn setup-machine 153.34.64.2\nyarn setup-machine devops@153.34.64.2");
160
+ process.exit(1);
161
+ }
162
+
163
+ // Test command to verify ssh credentials work
164
+ await runPromise(`ssh ${sshRemote} whoami`);
165
+
166
+ let backblazePath = getBackblazePath();
167
+
168
+ console.log("Setting up machine:", sshRemote);
169
+
170
+ // 1. Copy backblaze file to remote server (~/backblaze.json)
171
+ console.log("Copying backblaze credentials...");
172
+ if (fs.existsSync(backblazePath)) {
173
+ await runPromise(`scp "${backblazePath}" ${sshRemote}:~/backblaze.json`);
174
+ console.log("✅ Backblaze credentials copied");
175
+ } else {
176
+ console.warn("⚠️ Backblaze file not found at:", backblazePath);
177
+ }
178
+
179
+ // 2. Ensure git is installed on remote server
180
+ console.log("Ensuring git is installed...");
181
+ try {
182
+ await runPromise(`ssh ${sshRemote} "which git"`);
183
+ console.log("✅ Git already installed");
184
+ } catch {
185
+ console.log("Installing git...");
186
+ await runPromise(`ssh ${sshRemote} "sudo apt update && sudo apt install -y git"`);
187
+ console.log("✅ Git installed");
188
+ }
189
+
190
+ // 3. Ensure build tools are installed (needed for native modules)
191
+ console.log("Ensuring build tools are installed...");
192
+ try {
193
+ await runPromise(`ssh ${sshRemote} "which make"`);
194
+ console.log("✅ Build tools already installed");
195
+ } catch {
196
+ console.log("Installing build tools...");
197
+ await runPromise(`ssh ${sshRemote} "sudo apt install -y build-essential"`);
198
+ console.log("✅ Build tools installed");
199
+ }
200
+
201
+ // 3. Ensure nodejs is installed on remote server
202
+ console.log("Ensuring Node.js is installed...");
203
+ try {
204
+ let nodeVersion = await runPromise(`ssh ${sshRemote} "node --version"`);
205
+ console.log("✅ Node.js already installed:", nodeVersion.trim());
206
+ } catch {
207
+ console.log(`Installing Node.js (v${pinnedNodeVersion})...`);
208
+ await runPromise(`ssh ${sshRemote} "curl -fsSL https://deb.nodesource.com/setup_${pinnedNodeVersion}.x | sudo -E bash -"`);
209
+ await runPromise(`ssh ${sshRemote} "sudo apt-get install -y nodejs"`);
210
+ let nodeVersion = await runPromise(`ssh ${sshRemote} "node --version"`);
211
+ console.log("✅ Node.js installed:", nodeVersion.trim());
212
+ }
213
+
214
+ // 4. Ensure yarn is installed on remote server
215
+ console.log("Ensuring yarn is installed...");
216
+ try {
217
+ await runPromise(`ssh ${sshRemote} "which yarn"`);
218
+ console.log("✅ Yarn already installed");
219
+ } catch {
220
+ console.log("Installing yarn...");
221
+ await runPromise(`ssh ${sshRemote} "npm install -g yarn"`);
222
+ console.log("✅ Yarn installed");
223
+ }
224
+
225
+ // 5. Clone current repo into ~/machine-alwaysup with SSH key handling for private repos
226
+ console.log("Setting up repository...");
227
+ let gitURLLive = await getGitURLLive();
228
+ let gitRefLive = await getGitRefLive();
229
+
230
+ try {
231
+ await setupRepositoryOnRemote(sshRemote, gitURLLive, gitRefLive);
232
+ console.log("✅ Repository cloned and set to correct reference");
233
+ } catch (error) {
234
+ // Check if this is a private repository access issue
235
+ const errorMessage = error instanceof Error ? error.message : String(error);
236
+ if (errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found") || errorMessage.includes("fatal: Could not read from remote repository")) {
237
+ console.log("⚠️ Repository appears to be private, setting up SSH key access...");
238
+
239
+ // Ensure SSH key exists on remote machine
240
+ await runPromise(`ssh ${sshRemote} "mkdir -p ~/.ssh"`);
241
+ try {
242
+ await runPromise(`ssh ${sshRemote} "test -f ~/.ssh/id_rsa"`);
243
+ console.log("✅ SSH key already exists on remote machine");
244
+ } catch {
245
+ console.log("Generating SSH key on remote machine...");
246
+ await runPromise(`ssh ${sshRemote} "ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ''"`);
247
+ console.log("✅ SSH key generated on remote machine");
248
+ }
249
+
250
+ // Get the public key from remote machine
251
+ const sshPublicKey = await runPromise(`ssh ${sshRemote} "cat ~/.ssh/id_rsa.pub"`);
252
+ const keyTitle = `Machine Setup - ${sshRemote} - ${new Date().toISOString()}`;
253
+
254
+ // Add the deploy key to GitHub repository
255
+ await addDeployKeyToGitHub(sshPublicKey.trim(), keyTitle, gitURLLive, sshRemote);
256
+
257
+ // Retry repository setup
258
+ console.log("Retrying repository setup with SSH key...");
259
+ await setupRepositoryOnRemote(sshRemote, gitURLLive, gitRefLive);
260
+ console.log("✅ Repository cloned and set to correct reference");
261
+ } else {
262
+ // Re-throw other errors
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ // Install dependencies
268
+ console.log("Installing dependencies...");
269
+ await runPromise(`ssh ${sshRemote} "cd ~/machine-alwaysup && yarn install"`);
270
+ console.log("✅ Dependencies installed");
271
+
272
+ // 6. Create ~/machine-startup.sh which runs ~/machine-alwaysup/src/deployManager/machine.sh, and setup crontab
273
+ console.log("Setting up cron job...");
274
+
275
+ // Create startup script directly on remote machine
276
+ await runPromise(`ssh ${sshRemote} "echo '#!/bin/bash' > ~/machine-startup.sh"`);
277
+ await runPromise(`ssh ${sshRemote} "echo 'cd ~/machine-alwaysup/node_modules/querysub/src/deployManager' >> ~/machine-startup.sh"`);
278
+ await runPromise(`ssh ${sshRemote} "echo 'bash machine.sh' >> ~/machine-startup.sh"`);
279
+ await runPromise(`ssh ${sshRemote} "chmod +x ~/machine-startup.sh"`);
280
+
281
+ // 7. Setup crontab to run ~/machine-startup.sh on startup
282
+ const cronEntry = "@reboot ~/machine-startup.sh";
283
+
284
+ // Get existing crontab, add our entry if not already present
285
+ let existingCron = "";
286
+ try {
287
+ existingCron = await runPromise(`ssh ${sshRemote} "crontab -l"`, { nothrow: true });
288
+ } catch {
289
+ // No existing crontab is fine
290
+ }
291
+
292
+ if (!existingCron.includes("machine-startup.sh")) {
293
+ // Add cron entry directly
294
+ await runPromise(`ssh ${sshRemote} "(crontab -l 2>/dev/null || true; echo '${cronEntry}') | crontab -"`);
295
+ console.log("✅ Cron job added");
296
+ } else {
297
+ console.log("✅ Cron job already exists");
298
+ }
299
+
300
+ // Start the machine service immediately
301
+ console.log("Starting machine service...");
302
+ await runPromise(`ssh ${sshRemote} "~/machine-startup.sh"`);
303
+ console.log("✅ Machine service started!");
304
+
305
+ console.log("\n🎉 Machine setup complete!");
306
+ }
307
+
308
+
309
+ main().catch(console.error).finally(() => process.exit(0));
@@ -1,10 +1,3 @@
1
- 3) Enable console shimming, so machineApplyMain gets error notifications
2
- 4) Verify applyMain terminating gets logged correctly
3
-
4
-
5
- 4) Always up wrapper
6
- - Reruns apply utility when it crashes
7
- - And error log forwarding support, so crashes get logged by the next apply script to run, by writing apply utility crashes to a special file the apply utility reads on startup
8
1
  4.1) Requisition script
9
2
  - yarn setup-machine 153.34.64.2
10
3
 
@@ -23,10 +16,11 @@
23
16
  setup crontab to startup.sh
24
17
  run startup.sh
25
18
  - We might need to have it use to github API to give the remote machine access to the repo?
26
- - Test on a new server, that we run temporarily with some test scripts
27
19
 
28
- 4.1) Requisition script in "bin", as a .js bootstrapper
29
- - Verify it works, so we can do
20
+ 4) Test on a new server, that we run temporarily with some test scripts
21
+
22
+ 5) OH! Also setup swap usage on the machine, as it isn't by default often?
23
+
30
24
 
31
25
 
32
26
 
@@ -35,7 +29,8 @@
35
29
  - Hmm... requisition is not usable, as it is too hard to type. But something...
36
30
 
37
31
  5) Setup on our regular digital ocean server
38
- - Remove previous startup script and kill existing tmux services
32
+ - Remove previous startup.sh, and crontab and kill existing tmux services
33
+
39
34
  6) Verify crash logging works with error notifications (it work if we just apply our console logs shims)
40
35
  7) Quick node removal on process crash or removal
41
36
  Detect the nodeId of services (if they have one), and when the service dies, immediately remove "edgenodes/" file, and trigger an update of "edge-nodes-index.json"
@@ -3,6 +3,6 @@ import { URLParam } from "../library-components/URLParam";
3
3
  export type ViewType = "services" | "machines" | "service-detail" | "machine-detail";
4
4
 
5
5
  // URL Parameters for navigation state
6
- export const currentViewParam = new URLParam<ViewType>("view", "services");
6
+ export const currentViewParam = new URLParam<ViewType>("view", "machines");
7
7
  export const selectedServiceIdParam = new URLParam("serviceId", "");
8
8
  export const selectedMachineIdParam = new URLParam("machineId", "");
@@ -9,7 +9,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
9
9
  import { requiresNetworkTrustHook } from "../../-d-trust/NetworkTrust2";
10
10
  import { ignoreErrors, logErrors, timeoutToError, timeoutToUndefinedSilent } from "../../errors";
11
11
  import debugbreak from "debugbreak";
12
- import { magenta } from "socket-function/src/formatting/logColors";
12
+ import { blue, magenta } from "socket-function/src/formatting/logColors";
13
13
  import { formatNumber } from "socket-function/src/formatting/format";
14
14
  import { registerShutdownHandler } from "../periodic";
15
15
  import { isNoNetwork } from "../../config";
@@ -20,6 +20,7 @@ import { isClient } from "../../config2";
20
20
  import { ControllerPick, SocketRegistered } from "socket-function/SocketFunctionTypes";
21
21
 
22
22
  const NOTIFY_HISTORY = timeInDay * 7;
23
+ const LOG_STORE_HISTORY = timeInDay * 14;
23
24
  // Only error / fatal are tracked, for now... we might also add warns?
24
25
  const logIssueNotifyTypes: LogType[] = ["error", "fatal"];
25
26
 
@@ -55,7 +56,7 @@ export function logSilentWarning(message: string) {
55
56
  let nextBlockSeqNum = 1;
56
57
  let pendingBlock: LogBlock | undefined;
57
58
  export function addLocalLog(log: LogRaw, typeHint: LogType) {
58
- // NOTE: Eventually... we should probably ignore logs for non-public machines?
59
+ // NOTE: Eventually... we should ignore logs for non-public machines?
59
60
  //if (isNoNetwork()) return;
60
61
  try {
61
62
  if (!pendingBlock) {
@@ -84,14 +85,20 @@ export function addLocalLog(log: LogRaw, typeHint: LogType) {
84
85
  return;
85
86
  }
86
87
  let classifier = logClassifier(log, typeHint);
87
- pendingBlock.threadId = pendingBlock.threadId || getOwnThreadId();
88
- pendingBlock.machineId = pendingBlock.machineId || getOwnMachineId();
88
+ // Add threadId and machineId, if we have them
89
+ try {
90
+ pendingBlock.threadId = pendingBlock.threadId || getOwnThreadId();
91
+ pendingBlock.machineId = pendingBlock.machineId || getOwnMachineId();
92
+ } catch { }
89
93
  addToLogBlock(pendingBlock, classifier, log);
90
94
  ErrorLogControllerBase.onLocalLog(log, classifier);
91
95
  } catch (e) {
92
96
  internalError(e);
93
97
  }
94
98
  }
99
+ setTimeout(() => {
100
+ void initLogClassifier();
101
+ }, 0);
95
102
  registerShutdownHandler(flushNow);
96
103
  async function flushNow() {
97
104
  let newBlock = pendingBlock;
@@ -102,6 +109,19 @@ async function flushNow() {
102
109
  if (newBlock.count > 0) {
103
110
  let fileInfo = await logBlockToBuffer(newBlock);
104
111
  await LOG_ARCHIVES().set(fileInfo.fileName, fileInfo.buffer);
112
+ if (Math.random() < 0.05) {
113
+ console.log(blue(`Looking for old log blocks`));
114
+ let deleteThreshold = Date.now() - LOG_STORE_HISTORY;
115
+ let files = await LOG_ARCHIVES().find("", { shallow: true, type: "files" });
116
+ for (let file of files) {
117
+ if (!file.endsWith(LOG_BLOCK_EXTENSION)) continue;
118
+ let info = getLogBlockInfo(file);
119
+ if (info.startTime < deleteThreshold) {
120
+ console.log(blue(`Deleting old log block ${file}`));
121
+ await LOG_ARCHIVES().del(file);
122
+ }
123
+ }
124
+ }
105
125
  }
106
126
  }
107
127
 
@@ -120,7 +140,6 @@ let ensureBlocksFlushing = lazy(() => {
120
140
  let pendingLogs: { log: LogRaw; type: LogType }[] = [];
121
141
  let logClassifier: ((log: LogRaw, typeHint: LogType) => LogClass) | undefined;
122
142
  let initLogClassifier = lazy(async () => {
123
- await timeoutToError(60 * 1000, SocketFunction.mountPromise, () => new Error("Logs lost because service never mounted"));
124
143
  let logClasses = await new ErrorLogControllerBase().getClasses();
125
144
  logClassifier = getLogClassCategorizer(logClasses);
126
145
  if (pendingLogs.length > 0) {
@@ -14,7 +14,7 @@ export function getMaxExamples(classifier: LogClass): number {
14
14
  return classifier.maxExamples ?? MAX_EXAMPLES;
15
15
  }
16
16
 
17
- export const LOG_ARCHIVES = lazy(() => getArchives("logs"));
17
+ export const LOG_ARCHIVES = lazy(() => getArchives("notify-error-logs"));
18
18
  export const LOG_CLASSES_PATH = "classes.yaml";
19
19
  export const LOG_BLOCK_EXTENSION = ".log";
20
20
 
@@ -1,9 +1,19 @@
1
1
  import { canHaveChildren } from "socket-function/src/types";
2
- import { logDisk } from "./diskLogger";
3
2
  import { isNode } from "typesafecss";
4
3
  import { red, yellow } from "socket-function/src/formatting/logColors";
5
- import { addLocalLog } from "../errorLogs/ErrorLogController";
6
- import { LogType } from "../errorLogs/ErrorLogCore";
4
+
5
+ let addLocalLogModule: (typeof addLocalLogPromise extends Promise<infer T> ? T : never) | undefined;
6
+ const addLocalLogPromise = import("../errorLogs/ErrorLogController").then(x => {
7
+ (addLocalLogModule as any) = x;
8
+ return x;
9
+ });
10
+
11
+ let diskLoggerModule: typeof diskLoggerPromise extends Promise<infer T> ? T : never;
12
+ const diskLoggerPromise = import("./diskLogger").then(x => {
13
+ (diskLoggerModule as any) = x;
14
+ return x;
15
+ });
16
+
7
17
 
8
18
  let shimmed = false;
9
19
  export function shimConsoleLogs() {
@@ -26,10 +36,10 @@ export function shimConsoleLogs() {
26
36
  args.length > 0
27
37
  && String(args[0]).trim().length > 0
28
38
  ) {
29
- if (typeof logDisk === "function") {
39
+ if (typeof diskLoggerModule?.logDisk === "function") {
30
40
  // Don't call it directly, so we don't get extra line debug context added to this call
31
41
  // (as it wouldn't be useful, as we really want the caller)
32
- let stopDoubleShim = logDisk;
42
+ let stopDoubleShim = diskLoggerModule.logDisk;
33
43
  stopDoubleShim(...args, { type: fncName });
34
44
  }
35
45
  }
@@ -39,10 +49,10 @@ export function shimConsoleLogs() {
39
49
  // Filter out objects added by injectFileLocationToConsole
40
50
  args = args.filter(x => !(canHaveChildren(x) && x["__FILE__"]));
41
51
 
42
- if (typeof addLocalLog === "function") {
52
+ if (typeof addLocalLogModule?.addLocalLog === "function") {
43
53
  if (fncName === "error" || fncName === "warn") {
44
54
  // ALSO, track the logs in a file, for error notifications, etc
45
- addLocalLog({ message: args.join(" | ") + " | " + fileObj?.["__FILE__"], time: Date.now() }, fncName);
55
+ addLocalLogModule.addLocalLog({ message: args.join(" | ") + " | " + fileObj?.["__FILE__"], time: Date.now() }, fncName);
46
56
  }
47
57
  }
48
58
 
@@ -34,8 +34,6 @@ export async function shutdown() {
34
34
  if (shuttingDown) {
35
35
  return;
36
36
  }
37
- require("debugbreak")(2);
38
- debugger;
39
37
  console.log(red("Starting shutdown"));
40
38
  shuttingDown = true;
41
39
  const { authorityStorage } = await import("../0-path-value-core/pathValueCore");