typescript-virtual-container 1.1.7 → 1.2.1

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,5 +1,7 @@
1
1
  import { runCommand } from "../commands";
2
2
  import type { CommandResult } from "../types/commands";
3
+ import type { PerfLogger } from "../utils/perfLogger";
4
+ import { createPerfLogger } from "../utils/perfLogger";
3
5
  import type { VirtualShell } from "../VirtualShell";
4
6
 
5
7
  /**
@@ -16,6 +18,8 @@ import type { VirtualShell } from "../VirtualShell";
16
18
  * const list = await client.ls();
17
19
  * ```
18
20
  */
21
+ const perf: PerfLogger = createPerfLogger("SshClient");
22
+
19
23
  export class SshClient {
20
24
  private currentCwd = "/";
21
25
 
@@ -28,7 +32,9 @@ export class SshClient {
28
32
  constructor(
29
33
  private shell: VirtualShell,
30
34
  private username: string,
31
- ) {}
35
+ ) {
36
+ perf.mark("constructor");
37
+ }
32
38
 
33
39
  /**
34
40
  * Executes raw shell command.
@@ -37,6 +43,7 @@ export class SshClient {
37
43
  * @returns Command result with stdout/stderr/exitCode.
38
44
  */
39
45
  async exec(command: string): Promise<CommandResult> {
46
+ perf.mark("exec");
40
47
  const vfs = this.shell.getVfs();
41
48
  const users = this.shell.getUsers();
42
49
  const hostname = this.shell.getHostname();
@@ -69,6 +76,7 @@ export class SshClient {
69
76
  * @returns Result with directory listing in stdout.
70
77
  */
71
78
  async ls(path?: string): Promise<CommandResult> {
79
+ perf.mark("ls");
72
80
  const target = path ?? ".";
73
81
  return this.exec(`ls ${target}`);
74
82
  }
@@ -79,6 +87,7 @@ export class SshClient {
79
87
  * @returns Result with cwd path in stdout.
80
88
  */
81
89
  async pwd(): Promise<CommandResult> {
90
+ perf.mark("pwd");
82
91
  return this.exec("pwd");
83
92
  }
84
93
 
@@ -89,6 +98,7 @@ export class SshClient {
89
98
  * @returns Result; updates internal cwd on success.
90
99
  */
91
100
  async cd(path: string): Promise<CommandResult> {
101
+ perf.mark("cd");
92
102
  const result = await this.exec(`cd ${path}`);
93
103
  if (result.nextCwd && result.exitCode !== 1) {
94
104
  this.currentCwd = result.nextCwd;
@@ -103,6 +113,7 @@ export class SshClient {
103
113
  * @returns Result with file content in stdout.
104
114
  */
105
115
  async cat(path: string): Promise<CommandResult> {
116
+ perf.mark("cat");
106
117
  return this.exec(`cat ${path}`);
107
118
  }
108
119
 
@@ -114,6 +125,7 @@ export class SshClient {
114
125
  * @returns Result from mkdir command.
115
126
  */
116
127
  async mkdir(path: string, recursive = false): Promise<CommandResult> {
128
+ perf.mark("mkdir");
117
129
  const flag = recursive ? "-p " : "";
118
130
  return this.exec(`mkdir ${flag}${path}`);
119
131
  }
@@ -125,6 +137,7 @@ export class SshClient {
125
137
  * @returns Result from touch command.
126
138
  */
127
139
  async touch(path: string): Promise<CommandResult> {
140
+ perf.mark("touch");
128
141
  return this.exec(`touch ${path}`);
129
142
  }
130
143
 
@@ -136,6 +149,7 @@ export class SshClient {
136
149
  * @returns Result from rm command.
137
150
  */
138
151
  async rm(path: string, recursive = false): Promise<CommandResult> {
152
+ perf.mark("rm");
139
153
  const flag = recursive ? "-r " : "";
140
154
  return this.exec(`rm ${flag}${path}`);
141
155
  }
@@ -148,6 +162,7 @@ export class SshClient {
148
162
  * @returns Result from touch/write simulation.
149
163
  */
150
164
  async writeFile(path: string, content: string): Promise<CommandResult> {
165
+ perf.mark("writeFile");
151
166
  const vfs = this.shell.getVfs();
152
167
  if (!vfs) {
153
168
  throw new Error("SSH client not started");
@@ -171,6 +186,7 @@ export class SshClient {
171
186
  * @returns File content as string or error in result.
172
187
  */
173
188
  async readFile(path: string): Promise<CommandResult> {
189
+ perf.mark("readFile");
174
190
  const vfs = this.shell.getVfs();
175
191
  if (!vfs) {
176
192
  throw new Error("SSH client not started");
@@ -193,6 +209,7 @@ export class SshClient {
193
209
  * @returns Normalized cwd path.
194
210
  */
195
211
  getCwd(): string {
212
+ perf.mark("getCwd");
196
213
  return this.currentCwd;
197
214
  }
198
215
 
@@ -202,6 +219,7 @@ export class SshClient {
202
219
  * @returns Associated username.
203
220
  */
204
221
  getUsername(): string {
222
+ perf.mark("getUsername");
205
223
  return this.username;
206
224
  }
207
225
 
@@ -212,6 +230,7 @@ export class SshClient {
212
230
  * @returns Result with ASCII tree in stdout.
213
231
  */
214
232
  async tree(path?: string): Promise<CommandResult> {
233
+ perf.mark("tree");
215
234
  const target = path ?? ".";
216
235
  return this.exec(`tree ${target}`);
217
236
  }
@@ -222,6 +241,7 @@ export class SshClient {
222
241
  * @returns Result from whoami command.
223
242
  */
224
243
  async whoami(): Promise<CommandResult> {
244
+ perf.mark("whoami");
225
245
  return this.exec("whoami");
226
246
  }
227
247
 
@@ -231,6 +251,7 @@ export class SshClient {
231
251
  * @returns Result from hostname command.
232
252
  */
233
253
  async hostname(): Promise<CommandResult> {
254
+ perf.mark("hostname");
234
255
  return this.exec("hostname");
235
256
  }
236
257
 
@@ -240,6 +261,7 @@ export class SshClient {
240
261
  * @returns Result from who command.
241
262
  */
242
263
  async who(): Promise<CommandResult> {
264
+ perf.mark("who");
243
265
  return this.exec("who");
244
266
  }
245
267
  }
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { Server as SshServer } from "ssh2";
3
3
  import { VirtualShell } from "../VirtualShell";
4
+ import { createPerfLogger, type PerfLogger } from "../utils/perfLogger";
4
5
  import { runExec } from "./exec";
5
6
  import { loadOrCreateHostKey } from "./hostKey";
6
7
 
@@ -11,6 +12,8 @@ import { loadOrCreateHostKey } from "./hostKey";
11
12
  * Create an instance, call {@link SshMimic.start}, and stop it with
12
13
  * {@link SshMimic.stop} when your process exits.
13
14
  */
15
+ const perf: PerfLogger = createPerfLogger("SshMimic");
16
+
14
17
  class SshMimic extends EventEmitter {
15
18
  port: number;
16
19
  server: SshServer | null;
@@ -34,6 +37,7 @@ class SshMimic extends EventEmitter {
34
37
  shell?: VirtualShell;
35
38
  }) {
36
39
  super();
40
+ perf.mark("constructor");
37
41
  this.port = port;
38
42
  this.shellHostname = hostname;
39
43
  this.server = null;
@@ -46,6 +50,7 @@ class SshMimic extends EventEmitter {
46
50
  * @returns Promise resolved with bound listening port.
47
51
  */
48
52
  public async start(): Promise<number> {
53
+ perf.mark("start");
49
54
  const shell = this.shell;
50
55
  const privateKey = loadOrCreateHostKey();
51
56
 
@@ -169,6 +174,7 @@ class SshMimic extends EventEmitter {
169
174
  * Stops server if running.
170
175
  */
171
176
  public stop(): void {
177
+ perf.mark("stop");
172
178
  if (this.server) {
173
179
  this.server.close(() => {
174
180
  console.log("SSH Mimic stopped");
@@ -3,10 +3,12 @@ import { EventEmitter } from "node:events";
3
3
  import * as path from "node:path";
4
4
  import type { AuthenticationType, KeyboardAuthContext } from "ssh2";
5
5
  import { Server as SshServer } from "ssh2";
6
+ import type { VfsNodeStats } from "../types/vfs";
7
+ import type { PerfLogger } from "../utils/perfLogger";
8
+ import { createPerfLogger } from "../utils/perfLogger";
6
9
  import type VirtualFileSystem from "../VirtualFileSystem";
7
10
  import { VirtualShell } from "../VirtualShell";
8
11
  import type { VirtualUserManager } from "../VirtualUserManager";
9
- import type { VfsNodeStats } from "../types/vfs";
10
12
  import { loadOrCreateHostKey } from "./hostKey";
11
13
 
12
14
  const SFTP_STATUS_CODE = {
@@ -30,6 +32,8 @@ const OPEN_MODE = {
30
32
  EXCL: 0x00000020,
31
33
  };
32
34
 
35
+ const perf: PerfLogger = createPerfLogger("SftpMimic");
36
+
33
37
  interface SftpFileHandle {
34
38
  type: "file";
35
39
  path: string;
@@ -154,6 +158,7 @@ export class SftpMimic extends EventEmitter {
154
158
  users,
155
159
  }: SftpMimicOptions) {
156
160
  super();
161
+ perf.mark("constructor");
157
162
  this.port = port;
158
163
  this.server = null;
159
164
  this.hostname = hostname;
@@ -184,6 +189,7 @@ export class SftpMimic extends EventEmitter {
184
189
  }
185
190
 
186
191
  public async start(): Promise<number> {
192
+ perf.mark("start");
187
193
  const privateKey = loadOrCreateHostKey();
188
194
 
189
195
  // Ensure VirtualShell is fully initialized before accepting connections
@@ -336,6 +342,7 @@ export class SftpMimic extends EventEmitter {
336
342
  }
337
343
 
338
344
  public stop(): void {
345
+ perf.mark("stop");
339
346
  if (this.server) {
340
347
  this.server.close(() => {
341
348
  console.log("SFTP Mimic stopped");
@@ -7,6 +7,8 @@ import type {
7
7
  VfsNodeStats,
8
8
  WriteFileOptions,
9
9
  } from "../types/vfs";
10
+ import type { PerfLogger } from "../utils/perfLogger";
11
+ import { createPerfLogger } from "../utils/perfLogger";
10
12
  import { normalizePath } from "./path";
11
13
 
12
14
  /**
@@ -16,6 +18,8 @@ import { normalizePath } from "./path";
16
18
  * {@link VirtualFileSystem.restoreMirror} on startup and
17
19
  * {@link VirtualFileSystem.flushMirror} to persist pending changes.
18
20
  */
21
+ const perf: PerfLogger = createPerfLogger("VirtualFileSystem");
22
+
19
23
  class VirtualFileSystem extends EventEmitter {
20
24
  private readonly mirrorRoot: string;
21
25
 
@@ -95,6 +99,7 @@ class VirtualFileSystem extends EventEmitter {
95
99
  */
96
100
  constructor(baseDir: string = process.cwd()) {
97
101
  super();
102
+ perf.mark("constructor");
98
103
  this.mirrorRoot = path.resolve(baseDir, ".vfs", "mirror");
99
104
  }
100
105
 
@@ -104,6 +109,7 @@ class VirtualFileSystem extends EventEmitter {
104
109
  * If archive does not exist or cannot be read, creates fresh mirror file.
105
110
  */
106
111
  public async restoreMirror(): Promise<void> {
112
+ perf.mark("restoreMirror");
107
113
  this.ensureMirrorRoot();
108
114
  }
109
115
 
@@ -113,6 +119,7 @@ class VirtualFileSystem extends EventEmitter {
113
119
  * No-op when nothing changed and archive already exists.
114
120
  */
115
121
  public async flushMirror(): Promise<void> {
122
+ perf.mark("flushMirror");
116
123
  this.ensureMirrorRoot();
117
124
  this.emit("mirror:flush");
118
125
  }
@@ -124,6 +131,7 @@ class VirtualFileSystem extends EventEmitter {
124
131
  * @param mode POSIX-like mode bits for new directories.
125
132
  */
126
133
  public mkdir(targetPath: string, mode: number = 0o755): void {
134
+ perf.mark("mkdir");
127
135
  this.ensureMirrorRoot();
128
136
  const fsPath = this.resolveFsPath(targetPath);
129
137
  if (fs.existsSync(fsPath) && !fs.statSync(fsPath).isDirectory()) {
@@ -149,6 +157,7 @@ class VirtualFileSystem extends EventEmitter {
149
157
  content: string | Buffer,
150
158
  options: WriteFileOptions = {},
151
159
  ): void {
160
+ perf.mark("writeFile");
152
161
  this.ensureMirrorRoot();
153
162
  const normalized = normalizePath(targetPath);
154
163
  const fsPath = this.resolveFsPath(normalized);
@@ -181,6 +190,7 @@ class VirtualFileSystem extends EventEmitter {
181
190
  * @returns UTF-8 string content.
182
191
  */
183
192
  public readFile(targetPath: string): string {
193
+ perf.mark("readFile");
184
194
  this.ensureMirrorRoot();
185
195
  const fsPath = this.resolveFsPath(targetPath);
186
196
  if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
@@ -201,6 +211,7 @@ class VirtualFileSystem extends EventEmitter {
201
211
  * @returns True when file or directory exists.
202
212
  */
203
213
  public exists(targetPath: string): boolean {
214
+ perf.mark("exists");
204
215
  try {
205
216
  const fsPath = this.resolveFsPath(targetPath);
206
217
  return fs.existsSync(fsPath);
@@ -216,6 +227,7 @@ class VirtualFileSystem extends EventEmitter {
216
227
  * @param mode New POSIX-like mode.
217
228
  */
218
229
  public chmod(targetPath: string, mode: number): void {
230
+ perf.mark("chmod");
219
231
  const fsPath = this.resolveFsPath(targetPath);
220
232
  if (!fs.existsSync(fsPath)) {
221
233
  throw new Error(`Path '${normalizePath(targetPath)}' does not exist.`);
@@ -230,6 +242,7 @@ class VirtualFileSystem extends EventEmitter {
230
242
  * @returns Typed stat object based on node type.
231
243
  */
232
244
  public stat(targetPath: string): VfsNodeStats {
245
+ perf.mark("stat");
233
246
  this.ensureMirrorRoot();
234
247
  const normalized = normalizePath(targetPath);
235
248
  const fsPath = this.resolveFsPath(normalized);
@@ -273,6 +286,7 @@ class VirtualFileSystem extends EventEmitter {
273
286
  * @returns Sorted child names.
274
287
  */
275
288
  public list(dirPath: string = "/"): string[] {
289
+ perf.mark("list");
276
290
  const fsPath = this.resolveFsPath(dirPath);
277
291
  if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isDirectory()) {
278
292
  throw new Error(`Cannot list '${dirPath}': not a directory.`);
@@ -288,6 +302,7 @@ class VirtualFileSystem extends EventEmitter {
288
302
  * @returns Multi-line tree string.
289
303
  */
290
304
  public tree(dirPath: string = "/"): string {
305
+ perf.mark("tree");
291
306
  const fsPath = this.resolveFsPath(dirPath);
292
307
  if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isDirectory()) {
293
308
  throw new Error(`Cannot render tree for '${dirPath}': not a directory.`);
@@ -308,6 +323,7 @@ class VirtualFileSystem extends EventEmitter {
308
323
  * @returns Total byte usage for file content under target path.
309
324
  */
310
325
  public getUsageBytes(targetPath: string = "/"): number {
326
+ perf.mark("getUsageBytes");
311
327
  const fsPath = this.resolveFsPath(targetPath);
312
328
  if (!fs.existsSync(fsPath)) {
313
329
  throw new Error(`Path '${normalizePath(targetPath)}' does not exist.`);
@@ -321,6 +337,7 @@ class VirtualFileSystem extends EventEmitter {
321
337
  * @param targetPath Path to file.
322
338
  */
323
339
  public compressFile(targetPath: string): void {
340
+ perf.mark("compressFile");
324
341
  const fsPath = this.resolveFsPath(targetPath);
325
342
  if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
326
343
  throw new Error(`Cannot compress '${targetPath}': not a file.`);
@@ -338,6 +355,7 @@ class VirtualFileSystem extends EventEmitter {
338
355
  * @param targetPath Path to file.
339
356
  */
340
357
  public decompressFile(targetPath: string): void {
358
+ perf.mark("decompressFile");
341
359
  const fsPath = this.resolveFsPath(targetPath);
342
360
  if (!fs.existsSync(fsPath) || !fs.statSync(fsPath).isFile()) {
343
361
  throw new Error(`Cannot decompress '${targetPath}': not a file.`);
@@ -356,6 +374,7 @@ class VirtualFileSystem extends EventEmitter {
356
374
  * @param options Removal options, including recursive delete.
357
375
  */
358
376
  public remove(targetPath: string, options: RemoveOptions = {}): void {
377
+ perf.mark("remove");
359
378
  const normalized = normalizePath(targetPath);
360
379
  if (normalized === "/") {
361
380
  throw new Error("Cannot remove root directory.");
@@ -390,6 +409,7 @@ class VirtualFileSystem extends EventEmitter {
390
409
  * @param toPath Destination path.
391
410
  */
392
411
  public move(fromPath: string, toPath: string): void {
412
+ perf.mark("move");
393
413
  const fromNormalized = normalizePath(fromPath);
394
414
  const toNormalized = normalizePath(toPath);
395
415
 
@@ -3,6 +3,8 @@ import { EventEmitter } from "node:events";
3
3
  import { createCustomCommand, registerCommand, runCommand } from "../commands";
4
4
  import type { CommandContext, CommandResult } from "../types/commands";
5
5
  import type { ShellStream } from "../types/streams";
6
+ import type { PerfLogger } from "../utils/perfLogger";
7
+ import { createPerfLogger } from "../utils/perfLogger";
6
8
  import VirtualFileSystem from "../VirtualFileSystem";
7
9
  import { VirtualUserManager } from "../VirtualUserManager";
8
10
  import { startShell } from "./shell";
@@ -19,13 +21,23 @@ const defaultShellProperties: ShellProperties = {
19
21
  arch: "x86_64",
20
22
  };
21
23
 
24
+ const perf: PerfLogger = createPerfLogger("VirtualShell");
25
+
26
+ let cachedRootPassword: string | null = null;
27
+
22
28
  function resolveRootPassword(): string {
29
+ if (cachedRootPassword) {
30
+ return cachedRootPassword;
31
+ }
32
+
23
33
  const configured = process.env.SSH_MIMIC_ROOT_PASSWORD;
24
34
  if (configured && configured.trim().length > 0) {
25
- return configured;
35
+ cachedRootPassword = configured.trim();
36
+ return cachedRootPassword;
26
37
  }
27
38
 
28
39
  const generated = randomBytes(18).toString("base64url");
40
+ cachedRootPassword = generated;
29
41
  console.warn(
30
42
  `[ssh-mimic] SSH_MIMIC_ROOT_PASSWORD missing; generated ephemeral root password: ${generated}`,
31
43
  );
@@ -68,6 +80,7 @@ class VirtualShell extends EventEmitter {
68
80
  basePath?: string,
69
81
  ) {
70
82
  super();
83
+ perf.mark("constructor");
71
84
  this.hostname = hostname;
72
85
  this.properties = properties || defaultShellProperties;
73
86
  this.basePath = basePath || ".";
@@ -95,6 +108,7 @@ class VirtualShell extends EventEmitter {
95
108
  * Call this before any authentication or command execution.
96
109
  */
97
110
  public async ensureInitialized(): Promise<void> {
111
+ perf.mark("ensureInitialized");
98
112
  await this.initialized;
99
113
  }
100
114
 
@@ -126,6 +140,7 @@ class VirtualShell extends EventEmitter {
126
140
  * @param cwd
127
141
  */
128
142
  executeCommand(rawInput: string, authUser: string, cwd: string): void {
143
+ perf.mark("executeCommand");
129
144
  runCommand(rawInput, authUser, this.hostname, "shell", cwd, this);
130
145
  this.emit("command", { command: rawInput, user: authUser, cwd });
131
146
  }
@@ -146,6 +161,7 @@ class VirtualShell extends EventEmitter {
146
161
  remoteAddress: string,
147
162
  terminalSize: { cols: number; rows: number },
148
163
  ): void {
164
+ perf.mark("startInteractiveSession");
149
165
  // Interactive shell logic
150
166
  this.emit("session:start", { user: authUser, sessionId, remoteAddress });
151
167
  startShell(
@@ -199,6 +215,7 @@ class VirtualShell extends EventEmitter {
199
215
  targetPath: string,
200
216
  content: string | Buffer,
201
217
  ): void {
218
+ perf.mark("writeFileAsUser");
202
219
  this.users.assertWriteWithinQuota(authUser, targetPath, content);
203
220
  this.vfs.writeFile(targetPath, content);
204
221
  }