@kapeta/local-cluster-service 0.18.0 → 0.19.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.19.0](https://github.com/kapetacom/local-cluster-service/compare/v0.18.0...v0.19.0) (2023-09-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * Listen for docker logs when user joins room ([#67](https://github.com/kapetacom/local-cluster-service/issues/67)) ([53bd6b6](https://github.com/kapetacom/local-cluster-service/commit/53bd6b6b6de27dc2f9011887176f270d60a0d9dc))
7
+
1
8
  # [0.18.0](https://github.com/kapetacom/local-cluster-service/compare/v0.17.0...v0.18.0) (2023-09-02)
2
9
 
3
10
 
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ import FSExtra from 'fs-extra';
1
3
  import { Docker } from 'node-docker-api';
2
4
  import { Container } from 'node-docker-api/lib/container';
3
5
  import { InstanceInfo, LogEntry } from './types';
@@ -50,6 +52,7 @@ declare class ContainerManager {
50
52
  private _mountDir;
51
53
  private _version;
52
54
  private _lastDockerAccessCheck;
55
+ private logStreams;
53
56
  constructor();
54
57
  initialize(): Promise<void>;
55
58
  checkAlive(): Promise<boolean>;
@@ -83,6 +86,17 @@ declare class ContainerManager {
83
86
  */
84
87
  get(name: string): Promise<ContainerInfo | null>;
85
88
  getLogs(instance: InstanceInfo): Promise<LogEntry[]>;
89
+ stopLogListening(systemId: string, instanceId: string): Promise<void>;
90
+ ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void): Promise<void>;
91
+ }
92
+ declare class ClosableLogStream {
93
+ private readonly stream;
94
+ private readonly eventEmitter;
95
+ constructor(stream: FSExtra.ReadStream);
96
+ onLog(listener: (log: LogEntry) => void): () => void;
97
+ onEnd(listener: () => void): () => void;
98
+ onError(listener: (error: Error) => void): () => void;
99
+ close(): Promise<void>;
86
100
  }
87
101
  export declare class ContainerInfo {
88
102
  private readonly _container;
@@ -107,6 +121,7 @@ export declare class ContainerInfo {
107
121
  inspect(): Promise<any>;
108
122
  status(): Promise<DockerState>;
109
123
  getPorts(): Promise<PortMap | false>;
124
+ getLogStream(): Promise<ClosableLogStream>;
110
125
  getLogs(): Promise<LogEntry[]>;
111
126
  }
112
127
  export declare function getExtraHosts(dockerVersion: string): string[] | undefined;
@@ -17,6 +17,7 @@ const md5_1 = __importDefault(require("md5"));
17
17
  const utils_1 = require("./utils/utils");
18
18
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
19
19
  const taskManager_1 = require("./taskManager");
20
+ const node_events_1 = require("node:events");
20
21
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
21
22
  const NANO_SECOND = 1000000;
22
23
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -33,6 +34,7 @@ class ContainerManager {
33
34
  _mountDir;
34
35
  _version;
35
36
  _lastDockerAccessCheck = 0;
37
+ logStreams = {};
36
38
  constructor() {
37
39
  this._docker = null;
38
40
  this._alive = false;
@@ -472,6 +474,175 @@ class ContainerManager {
472
474
  }
473
475
  return containerInfo.getLogs();
474
476
  }
477
+ async stopLogListening(systemId, instanceId) {
478
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(systemId, instanceId);
479
+ if (this.logStreams[containerName]) {
480
+ if (this.logStreams[containerName]?.timer) {
481
+ clearTimeout(this.logStreams[containerName].timer);
482
+ }
483
+ console.log('Stopped listening for logs on container: %s', containerName);
484
+ try {
485
+ const stream = this.logStreams[containerName].stream;
486
+ if (stream) {
487
+ await stream.close();
488
+ }
489
+ }
490
+ catch (err) {
491
+ // Ignore
492
+ }
493
+ delete this.logStreams[containerName];
494
+ }
495
+ }
496
+ async ensureLogListening(systemId, instanceId, handler) {
497
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(systemId, instanceId);
498
+ try {
499
+ if (this.logStreams[containerName]?.stream) {
500
+ // Already listening - will shut itself down
501
+ return;
502
+ }
503
+ if (this.logStreams[containerName]?.timer) {
504
+ clearTimeout(this.logStreams[containerName].timer);
505
+ }
506
+ const tryLater = () => {
507
+ this.logStreams[containerName] = {
508
+ timer: setTimeout(() => {
509
+ // Keep trying until user decides to not listen anymore
510
+ this.ensureLogListening(systemId, instanceId, handler);
511
+ }, 5000),
512
+ };
513
+ };
514
+ const containerInfo = await this.getContainerByName(containerName);
515
+ if (!containerInfo || !(await containerInfo.isRunning())) {
516
+ // Container not currently running - try again in 5 seconds
517
+ tryLater();
518
+ return;
519
+ }
520
+ const stream = await containerInfo.getLogStream();
521
+ stream.onLog((log) => {
522
+ try {
523
+ handler(log);
524
+ }
525
+ catch (err) {
526
+ console.warn('Error handling log', err);
527
+ }
528
+ });
529
+ stream.onEnd(() => {
530
+ // We get here if the container is stopped
531
+ delete this.logStreams[containerName];
532
+ tryLater();
533
+ });
534
+ stream.onError((err) => {
535
+ // We get here if the container crashes
536
+ delete this.logStreams[containerName];
537
+ tryLater();
538
+ });
539
+ this.logStreams[containerName] = {
540
+ stream,
541
+ };
542
+ }
543
+ catch (err) {
544
+ // Ignore
545
+ }
546
+ }
547
+ }
548
+ function readLogBuffer(logBuffer) {
549
+ const out = [];
550
+ let offset = 0;
551
+ while (offset < logBuffer.length) {
552
+ try {
553
+ // Read the docker log format - explained here:
554
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
555
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
556
+ // First byte is stream type
557
+ const streamTypeInt = logBuffer.readInt8(offset);
558
+ const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
559
+ if (streamTypeInt !== 1 && streamTypeInt !== 2) {
560
+ console.error('Unknown stream type: %s', streamTypeInt, out[out.length - 1]);
561
+ break;
562
+ }
563
+ // Bytes 4-8 is frame size
564
+ const messageLength = logBuffer.readInt32BE(offset + 4);
565
+ // After that is the message - with the message length
566
+ const dataWithoutStreamType = logBuffer.subarray(offset + 8, offset + 8 + messageLength);
567
+ const raw = dataWithoutStreamType.toString();
568
+ // Split the message into date and message
569
+ const firstSpaceIx = raw.indexOf(' ');
570
+ const dateString = raw.substring(0, firstSpaceIx);
571
+ const line = raw.substring(firstSpaceIx + 1);
572
+ offset = offset + messageLength + 8;
573
+ if (!dateString) {
574
+ break;
575
+ }
576
+ out.push({
577
+ time: new Date(dateString).getTime(),
578
+ message: line,
579
+ level: 'INFO',
580
+ source: streamType,
581
+ });
582
+ }
583
+ catch (err) {
584
+ console.error('Error parsing log entry', err);
585
+ offset = logBuffer.length;
586
+ break;
587
+ }
588
+ }
589
+ return out;
590
+ }
591
+ class ClosableLogStream {
592
+ stream;
593
+ eventEmitter;
594
+ constructor(stream) {
595
+ this.stream = stream;
596
+ this.eventEmitter = new node_events_1.EventEmitter();
597
+ stream.on('data', (data) => {
598
+ const logs = readLogBuffer(data);
599
+ logs.forEach((log) => {
600
+ this.eventEmitter.emit('log', log);
601
+ });
602
+ });
603
+ stream.on('end', () => {
604
+ this.eventEmitter.emit('end');
605
+ });
606
+ stream.on('error', (error) => {
607
+ this.eventEmitter.emit('error', error);
608
+ });
609
+ stream.on('close', () => {
610
+ this.eventEmitter.emit('end');
611
+ });
612
+ }
613
+ onLog(listener) {
614
+ this.eventEmitter.on('log', listener);
615
+ return () => {
616
+ this.eventEmitter.removeListener('log', listener);
617
+ };
618
+ }
619
+ onEnd(listener) {
620
+ this.eventEmitter.on('end', listener);
621
+ return () => {
622
+ this.eventEmitter.removeListener('end', listener);
623
+ };
624
+ }
625
+ onError(listener) {
626
+ this.eventEmitter.on('error', listener);
627
+ return () => {
628
+ this.eventEmitter.removeListener('error', listener);
629
+ };
630
+ }
631
+ close() {
632
+ return new Promise((resolve, reject) => {
633
+ try {
634
+ this.stream.close((err) => {
635
+ if (err) {
636
+ console.warn('Error closing log stream', err);
637
+ }
638
+ resolve();
639
+ });
640
+ }
641
+ catch (err) {
642
+ // Ignore
643
+ }
644
+ });
645
+ }
475
646
  }
476
647
  class ContainerInfo {
477
648
  _container;
@@ -555,52 +726,34 @@ class ContainerInfo {
555
726
  });
556
727
  return ports;
557
728
  }
729
+ async getLogStream() {
730
+ try {
731
+ const logStream = (await this.native.logs({
732
+ stdout: true,
733
+ stderr: true,
734
+ follow: true,
735
+ tail: 0,
736
+ timestamps: true,
737
+ }));
738
+ return new ClosableLogStream(logStream);
739
+ }
740
+ catch (err) {
741
+ console.log('Error getting log stream', err);
742
+ throw err;
743
+ }
744
+ }
558
745
  async getLogs() {
559
746
  const logStream = (await this.native.logs({
560
747
  stdout: true,
561
748
  stderr: true,
562
749
  follow: false,
563
- tail: 100,
564
750
  timestamps: true,
565
751
  }));
566
- const out = [];
752
+ const chunks = [];
567
753
  await promisifyStream(logStream, (data) => {
568
- const buf = data;
569
- let offset = 0;
570
- while (offset < buf.length) {
571
- try {
572
- // Read the docker log format - explained here:
573
- // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
574
- // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
575
- // First byte is stream type
576
- const streamTypeInt = buf.readInt8(offset);
577
- const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
578
- // Bytes 4-8 is frame size
579
- const messageLength = buf.readInt32BE(offset + 4);
580
- // After that is the message - with the message length
581
- const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
582
- const raw = dataWithoutStreamType.toString();
583
- // Split the message into date and message
584
- const firstSpaceIx = raw.indexOf(' ');
585
- const dateString = raw.substring(0, firstSpaceIx);
586
- const line = raw.substring(firstSpaceIx + 1);
587
- offset = offset + messageLength + 8;
588
- if (!dateString) {
589
- continue;
590
- }
591
- out.push({
592
- time: new Date(dateString).getTime(),
593
- message: line,
594
- level: 'INFO',
595
- source: streamType,
596
- });
597
- }
598
- catch (err) {
599
- console.error('Error parsing log entry', err);
600
- offset = buf.length;
601
- }
602
- }
754
+ chunks.push(data);
603
755
  });
756
+ const out = readLogBuffer(Buffer.concat(chunks));
604
757
  if (out.length === 0) {
605
758
  out.push({
606
759
  time: Date.now(),
@@ -422,11 +422,6 @@ class InstanceManager {
422
422
  errorMessage: e.message ?? 'Failed to start - Check logs for details.',
423
423
  });
424
424
  socketManager_1.socketManager.emitInstanceLog(systemId, instanceId, logs[0]);
425
- socketManager_1.socketManager.emitInstanceEvent(systemId, blockInstance.id, socketManager_1.EVENT_INSTANCE_EXITED, {
426
- error: `Failed to start instance: ${e.message}`,
427
- status: socketManager_1.EVENT_INSTANCE_EXITED,
428
- instanceId: blockInstance.id,
429
- });
430
425
  return out;
431
426
  }
432
427
  }, {
@@ -486,6 +481,7 @@ class InstanceManager {
486
481
  try {
487
482
  const plan = await assetManager_1.assetManager.getAsset(instance.systemId, true, false);
488
483
  if (!plan) {
484
+ console.log('Plan not found - reset to stop', instance.ref, instance.systemId);
489
485
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
490
486
  changed = true;
491
487
  return;
@@ -493,12 +489,14 @@ class InstanceManager {
493
489
  const planData = plan.data;
494
490
  const planInstance = planData?.spec?.blocks?.find((b) => b.id === instance.instanceId);
495
491
  if (!planInstance || !planInstance?.block?.ref) {
492
+ console.log('Plan instance not found - reset to stop', instance.ref, instance.systemId);
496
493
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
497
494
  changed = true;
498
495
  return;
499
496
  }
500
497
  const blockDef = await assetManager_1.assetManager.getAsset(instance.ref, true, false);
501
498
  if (!blockDef) {
499
+ console.log('Block definition not found - reset to stop', instance.ref, instance.systemId);
502
500
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
503
501
  changed = true;
504
502
  return;
@@ -676,6 +674,3 @@ class InstanceManager {
676
674
  }
677
675
  exports.InstanceManager = InstanceManager;
678
676
  exports.instanceManager = new InstanceManager();
679
- process.on('exit', async () => {
680
- await exports.instanceManager.stopAll();
681
- });
@@ -19,10 +19,10 @@ export declare class SocketManager {
19
19
  emitInstanceLog(systemId: string, instanceId: string, payload: LogEntry): void;
20
20
  emitSystemLog(systemId: string, payload: LogEntry): void;
21
21
  emitGlobalLog(payload: LogEntry): void;
22
- emitInstanceEvent(systemId: string, instanceId: string, type: string, payload: any): void;
23
22
  private _bindIO;
24
23
  private _handleSocketCreated;
25
24
  private _bindSocket;
26
- private _handleSocketDestroyed;
25
+ private handleJoinRoom;
26
+ private handleLeaveRoom;
27
27
  }
28
28
  export declare const socketManager: SocketManager;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.socketManager = exports.SocketManager = exports.EVENT_LOG = exports.EVENT_SYSTEM_LOG = exports.EVENT_INSTANCE_LOG = exports.EVENT_INSTANCE_EXITED = exports.EVENT_INSTANCE_CREATED = exports.EVENT_STATUS_CHANGED = void 0;
7
7
  const lodash_1 = __importDefault(require("lodash"));
8
8
  const utils_1 = require("./utils/utils");
9
+ const containerManager_1 = require("./containerManager");
9
10
  exports.EVENT_STATUS_CHANGED = 'status-changed';
10
11
  exports.EVENT_INSTANCE_CREATED = 'instance-created';
11
12
  exports.EVENT_INSTANCE_EXITED = 'instance-exited';
@@ -48,30 +49,28 @@ class SocketManager {
48
49
  emitSystemEvent(systemId, type, payload) {
49
50
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
50
51
  try {
51
- exports.socketManager.emit(`${systemId}/instances`, type, payload);
52
+ const contextId = `system-events/${encodeURIComponent(systemId)}`;
53
+ this.emit(contextId, type, payload);
52
54
  }
53
55
  catch (e) {
54
56
  console.warn('Failed to emit instance event: %s', e.message);
55
57
  }
56
58
  }
57
59
  emitInstanceLog(systemId, instanceId, payload) {
58
- this.emitInstanceEvent(systemId, instanceId, exports.EVENT_INSTANCE_LOG, payload);
59
- }
60
- emitSystemLog(systemId, payload) {
61
- this.emitSystemEvent(systemId, exports.EVENT_SYSTEM_LOG, payload);
62
- }
63
- emitGlobalLog(payload) {
64
- this.emitGlobal(exports.EVENT_LOG, payload);
65
- }
66
- emitInstanceEvent(systemId, instanceId, type, payload) {
67
60
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
68
61
  try {
69
- exports.socketManager.emit(`${systemId}/instances/${instanceId}`, type, payload);
62
+ this.emit(`instance-logs/${encodeURIComponent(systemId)}/${encodeURIComponent(instanceId)}`, exports.EVENT_INSTANCE_LOG, payload);
70
63
  }
71
64
  catch (e) {
72
65
  console.warn('Failed to emit instance event: %s', e.message);
73
66
  }
74
67
  }
68
+ emitSystemLog(systemId, payload) {
69
+ this.emitSystemEvent(systemId, exports.EVENT_SYSTEM_LOG, payload);
70
+ }
71
+ emitGlobalLog(payload) {
72
+ this.emitGlobal(exports.EVENT_LOG, payload);
73
+ }
75
74
  _bindIO() {
76
75
  this.io.on('connection', (socket) => this._handleSocketCreated(socket));
77
76
  }
@@ -80,16 +79,42 @@ class SocketManager {
80
79
  this._sockets.push(socket);
81
80
  }
82
81
  _bindSocket(socket) {
83
- socket.on('disconnect', () => this._handleSocketDestroyed(socket));
82
+ socket.on('disconnect', () => {
83
+ lodash_1.default.pull(this._sockets, socket);
84
+ socket.rooms.forEach((roomId) => {
85
+ this.handleLeaveRoom(roomId);
86
+ });
87
+ });
84
88
  socket.on('join', (id) => {
85
89
  socket.join(id);
90
+ this.handleJoinRoom(id);
86
91
  });
87
92
  socket.on('leave', (id) => {
88
93
  socket.leave(id);
94
+ this.handleLeaveRoom(id);
89
95
  });
90
96
  }
91
- _handleSocketDestroyed(socket) {
92
- lodash_1.default.pull(this._sockets, socket);
97
+ handleJoinRoom(id) {
98
+ if (id.startsWith('instance-logs/')) {
99
+ let [, systemId, instanceId] = id.split(/\//g);
100
+ systemId = decodeURIComponent(systemId);
101
+ instanceId = decodeURIComponent(instanceId);
102
+ console.log('Start listening for logs', systemId, instanceId);
103
+ containerManager_1.containerManager
104
+ .ensureLogListening(systemId, instanceId, (log) => {
105
+ this.emitInstanceLog(systemId, instanceId, log);
106
+ })
107
+ .catch((e) => { });
108
+ }
109
+ }
110
+ handleLeaveRoom(id) {
111
+ if (id.startsWith('instance-logs/')) {
112
+ let [, systemId, instanceId] = id.split(/\//g);
113
+ systemId = decodeURIComponent(systemId);
114
+ instanceId = decodeURIComponent(instanceId);
115
+ console.log('Stop listening for logs', systemId, instanceId);
116
+ containerManager_1.containerManager.stopLogListening(systemId, instanceId).catch((e) => { });
117
+ }
93
118
  }
94
119
  }
95
120
  exports.SocketManager = SocketManager;
@@ -1,3 +1,5 @@
1
+ /// <reference types="node" />
2
+ import FSExtra from 'fs-extra';
1
3
  import { Docker } from 'node-docker-api';
2
4
  import { Container } from 'node-docker-api/lib/container';
3
5
  import { InstanceInfo, LogEntry } from './types';
@@ -50,6 +52,7 @@ declare class ContainerManager {
50
52
  private _mountDir;
51
53
  private _version;
52
54
  private _lastDockerAccessCheck;
55
+ private logStreams;
53
56
  constructor();
54
57
  initialize(): Promise<void>;
55
58
  checkAlive(): Promise<boolean>;
@@ -83,6 +86,17 @@ declare class ContainerManager {
83
86
  */
84
87
  get(name: string): Promise<ContainerInfo | null>;
85
88
  getLogs(instance: InstanceInfo): Promise<LogEntry[]>;
89
+ stopLogListening(systemId: string, instanceId: string): Promise<void>;
90
+ ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void): Promise<void>;
91
+ }
92
+ declare class ClosableLogStream {
93
+ private readonly stream;
94
+ private readonly eventEmitter;
95
+ constructor(stream: FSExtra.ReadStream);
96
+ onLog(listener: (log: LogEntry) => void): () => void;
97
+ onEnd(listener: () => void): () => void;
98
+ onError(listener: (error: Error) => void): () => void;
99
+ close(): Promise<void>;
86
100
  }
87
101
  export declare class ContainerInfo {
88
102
  private readonly _container;
@@ -107,6 +121,7 @@ export declare class ContainerInfo {
107
121
  inspect(): Promise<any>;
108
122
  status(): Promise<DockerState>;
109
123
  getPorts(): Promise<PortMap | false>;
124
+ getLogStream(): Promise<ClosableLogStream>;
110
125
  getLogs(): Promise<LogEntry[]>;
111
126
  }
112
127
  export declare function getExtraHosts(dockerVersion: string): string[] | undefined;
@@ -17,6 +17,7 @@ const md5_1 = __importDefault(require("md5"));
17
17
  const utils_1 = require("./utils/utils");
18
18
  const nodejs_api_client_1 = require("@kapeta/nodejs-api-client");
19
19
  const taskManager_1 = require("./taskManager");
20
+ const node_events_1 = require("node:events");
20
21
  exports.CONTAINER_LABEL_PORT_PREFIX = 'kapeta_port-';
21
22
  const NANO_SECOND = 1000000;
22
23
  const HEALTH_CHECK_INTERVAL = 3000;
@@ -33,6 +34,7 @@ class ContainerManager {
33
34
  _mountDir;
34
35
  _version;
35
36
  _lastDockerAccessCheck = 0;
37
+ logStreams = {};
36
38
  constructor() {
37
39
  this._docker = null;
38
40
  this._alive = false;
@@ -472,6 +474,175 @@ class ContainerManager {
472
474
  }
473
475
  return containerInfo.getLogs();
474
476
  }
477
+ async stopLogListening(systemId, instanceId) {
478
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(systemId, instanceId);
479
+ if (this.logStreams[containerName]) {
480
+ if (this.logStreams[containerName]?.timer) {
481
+ clearTimeout(this.logStreams[containerName].timer);
482
+ }
483
+ console.log('Stopped listening for logs on container: %s', containerName);
484
+ try {
485
+ const stream = this.logStreams[containerName].stream;
486
+ if (stream) {
487
+ await stream.close();
488
+ }
489
+ }
490
+ catch (err) {
491
+ // Ignore
492
+ }
493
+ delete this.logStreams[containerName];
494
+ }
495
+ }
496
+ async ensureLogListening(systemId, instanceId, handler) {
497
+ const containerName = (0, utils_1.getBlockInstanceContainerName)(systemId, instanceId);
498
+ try {
499
+ if (this.logStreams[containerName]?.stream) {
500
+ // Already listening - will shut itself down
501
+ return;
502
+ }
503
+ if (this.logStreams[containerName]?.timer) {
504
+ clearTimeout(this.logStreams[containerName].timer);
505
+ }
506
+ const tryLater = () => {
507
+ this.logStreams[containerName] = {
508
+ timer: setTimeout(() => {
509
+ // Keep trying until user decides to not listen anymore
510
+ this.ensureLogListening(systemId, instanceId, handler);
511
+ }, 5000),
512
+ };
513
+ };
514
+ const containerInfo = await this.getContainerByName(containerName);
515
+ if (!containerInfo || !(await containerInfo.isRunning())) {
516
+ // Container not currently running - try again in 5 seconds
517
+ tryLater();
518
+ return;
519
+ }
520
+ const stream = await containerInfo.getLogStream();
521
+ stream.onLog((log) => {
522
+ try {
523
+ handler(log);
524
+ }
525
+ catch (err) {
526
+ console.warn('Error handling log', err);
527
+ }
528
+ });
529
+ stream.onEnd(() => {
530
+ // We get here if the container is stopped
531
+ delete this.logStreams[containerName];
532
+ tryLater();
533
+ });
534
+ stream.onError((err) => {
535
+ // We get here if the container crashes
536
+ delete this.logStreams[containerName];
537
+ tryLater();
538
+ });
539
+ this.logStreams[containerName] = {
540
+ stream,
541
+ };
542
+ }
543
+ catch (err) {
544
+ // Ignore
545
+ }
546
+ }
547
+ }
548
+ function readLogBuffer(logBuffer) {
549
+ const out = [];
550
+ let offset = 0;
551
+ while (offset < logBuffer.length) {
552
+ try {
553
+ // Read the docker log format - explained here:
554
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
555
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
556
+ // First byte is stream type
557
+ const streamTypeInt = logBuffer.readInt8(offset);
558
+ const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
559
+ if (streamTypeInt !== 1 && streamTypeInt !== 2) {
560
+ console.error('Unknown stream type: %s', streamTypeInt, out[out.length - 1]);
561
+ break;
562
+ }
563
+ // Bytes 4-8 is frame size
564
+ const messageLength = logBuffer.readInt32BE(offset + 4);
565
+ // After that is the message - with the message length
566
+ const dataWithoutStreamType = logBuffer.subarray(offset + 8, offset + 8 + messageLength);
567
+ const raw = dataWithoutStreamType.toString();
568
+ // Split the message into date and message
569
+ const firstSpaceIx = raw.indexOf(' ');
570
+ const dateString = raw.substring(0, firstSpaceIx);
571
+ const line = raw.substring(firstSpaceIx + 1);
572
+ offset = offset + messageLength + 8;
573
+ if (!dateString) {
574
+ break;
575
+ }
576
+ out.push({
577
+ time: new Date(dateString).getTime(),
578
+ message: line,
579
+ level: 'INFO',
580
+ source: streamType,
581
+ });
582
+ }
583
+ catch (err) {
584
+ console.error('Error parsing log entry', err);
585
+ offset = logBuffer.length;
586
+ break;
587
+ }
588
+ }
589
+ return out;
590
+ }
591
+ class ClosableLogStream {
592
+ stream;
593
+ eventEmitter;
594
+ constructor(stream) {
595
+ this.stream = stream;
596
+ this.eventEmitter = new node_events_1.EventEmitter();
597
+ stream.on('data', (data) => {
598
+ const logs = readLogBuffer(data);
599
+ logs.forEach((log) => {
600
+ this.eventEmitter.emit('log', log);
601
+ });
602
+ });
603
+ stream.on('end', () => {
604
+ this.eventEmitter.emit('end');
605
+ });
606
+ stream.on('error', (error) => {
607
+ this.eventEmitter.emit('error', error);
608
+ });
609
+ stream.on('close', () => {
610
+ this.eventEmitter.emit('end');
611
+ });
612
+ }
613
+ onLog(listener) {
614
+ this.eventEmitter.on('log', listener);
615
+ return () => {
616
+ this.eventEmitter.removeListener('log', listener);
617
+ };
618
+ }
619
+ onEnd(listener) {
620
+ this.eventEmitter.on('end', listener);
621
+ return () => {
622
+ this.eventEmitter.removeListener('end', listener);
623
+ };
624
+ }
625
+ onError(listener) {
626
+ this.eventEmitter.on('error', listener);
627
+ return () => {
628
+ this.eventEmitter.removeListener('error', listener);
629
+ };
630
+ }
631
+ close() {
632
+ return new Promise((resolve, reject) => {
633
+ try {
634
+ this.stream.close((err) => {
635
+ if (err) {
636
+ console.warn('Error closing log stream', err);
637
+ }
638
+ resolve();
639
+ });
640
+ }
641
+ catch (err) {
642
+ // Ignore
643
+ }
644
+ });
645
+ }
475
646
  }
476
647
  class ContainerInfo {
477
648
  _container;
@@ -555,52 +726,34 @@ class ContainerInfo {
555
726
  });
556
727
  return ports;
557
728
  }
729
+ async getLogStream() {
730
+ try {
731
+ const logStream = (await this.native.logs({
732
+ stdout: true,
733
+ stderr: true,
734
+ follow: true,
735
+ tail: 0,
736
+ timestamps: true,
737
+ }));
738
+ return new ClosableLogStream(logStream);
739
+ }
740
+ catch (err) {
741
+ console.log('Error getting log stream', err);
742
+ throw err;
743
+ }
744
+ }
558
745
  async getLogs() {
559
746
  const logStream = (await this.native.logs({
560
747
  stdout: true,
561
748
  stderr: true,
562
749
  follow: false,
563
- tail: 100,
564
750
  timestamps: true,
565
751
  }));
566
- const out = [];
752
+ const chunks = [];
567
753
  await promisifyStream(logStream, (data) => {
568
- const buf = data;
569
- let offset = 0;
570
- while (offset < buf.length) {
571
- try {
572
- // Read the docker log format - explained here:
573
- // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
574
- // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
575
- // First byte is stream type
576
- const streamTypeInt = buf.readInt8(offset);
577
- const streamType = streamTypeInt === 1 ? 'stdout' : 'stderr';
578
- // Bytes 4-8 is frame size
579
- const messageLength = buf.readInt32BE(offset + 4);
580
- // After that is the message - with the message length
581
- const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
582
- const raw = dataWithoutStreamType.toString();
583
- // Split the message into date and message
584
- const firstSpaceIx = raw.indexOf(' ');
585
- const dateString = raw.substring(0, firstSpaceIx);
586
- const line = raw.substring(firstSpaceIx + 1);
587
- offset = offset + messageLength + 8;
588
- if (!dateString) {
589
- continue;
590
- }
591
- out.push({
592
- time: new Date(dateString).getTime(),
593
- message: line,
594
- level: 'INFO',
595
- source: streamType,
596
- });
597
- }
598
- catch (err) {
599
- console.error('Error parsing log entry', err);
600
- offset = buf.length;
601
- }
602
- }
754
+ chunks.push(data);
603
755
  });
756
+ const out = readLogBuffer(Buffer.concat(chunks));
604
757
  if (out.length === 0) {
605
758
  out.push({
606
759
  time: Date.now(),
@@ -422,11 +422,6 @@ class InstanceManager {
422
422
  errorMessage: e.message ?? 'Failed to start - Check logs for details.',
423
423
  });
424
424
  socketManager_1.socketManager.emitInstanceLog(systemId, instanceId, logs[0]);
425
- socketManager_1.socketManager.emitInstanceEvent(systemId, blockInstance.id, socketManager_1.EVENT_INSTANCE_EXITED, {
426
- error: `Failed to start instance: ${e.message}`,
427
- status: socketManager_1.EVENT_INSTANCE_EXITED,
428
- instanceId: blockInstance.id,
429
- });
430
425
  return out;
431
426
  }
432
427
  }, {
@@ -486,6 +481,7 @@ class InstanceManager {
486
481
  try {
487
482
  const plan = await assetManager_1.assetManager.getAsset(instance.systemId, true, false);
488
483
  if (!plan) {
484
+ console.log('Plan not found - reset to stop', instance.ref, instance.systemId);
489
485
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
490
486
  changed = true;
491
487
  return;
@@ -493,12 +489,14 @@ class InstanceManager {
493
489
  const planData = plan.data;
494
490
  const planInstance = planData?.spec?.blocks?.find((b) => b.id === instance.instanceId);
495
491
  if (!planInstance || !planInstance?.block?.ref) {
492
+ console.log('Plan instance not found - reset to stop', instance.ref, instance.systemId);
496
493
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
497
494
  changed = true;
498
495
  return;
499
496
  }
500
497
  const blockDef = await assetManager_1.assetManager.getAsset(instance.ref, true, false);
501
498
  if (!blockDef) {
499
+ console.log('Block definition not found - reset to stop', instance.ref, instance.systemId);
502
500
  instance.desiredStatus = types_1.DesiredInstanceStatus.STOP;
503
501
  changed = true;
504
502
  return;
@@ -676,6 +674,3 @@ class InstanceManager {
676
674
  }
677
675
  exports.InstanceManager = InstanceManager;
678
676
  exports.instanceManager = new InstanceManager();
679
- process.on('exit', async () => {
680
- await exports.instanceManager.stopAll();
681
- });
@@ -19,10 +19,10 @@ export declare class SocketManager {
19
19
  emitInstanceLog(systemId: string, instanceId: string, payload: LogEntry): void;
20
20
  emitSystemLog(systemId: string, payload: LogEntry): void;
21
21
  emitGlobalLog(payload: LogEntry): void;
22
- emitInstanceEvent(systemId: string, instanceId: string, type: string, payload: any): void;
23
22
  private _bindIO;
24
23
  private _handleSocketCreated;
25
24
  private _bindSocket;
26
- private _handleSocketDestroyed;
25
+ private handleJoinRoom;
26
+ private handleLeaveRoom;
27
27
  }
28
28
  export declare const socketManager: SocketManager;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.socketManager = exports.SocketManager = exports.EVENT_LOG = exports.EVENT_SYSTEM_LOG = exports.EVENT_INSTANCE_LOG = exports.EVENT_INSTANCE_EXITED = exports.EVENT_INSTANCE_CREATED = exports.EVENT_STATUS_CHANGED = void 0;
7
7
  const lodash_1 = __importDefault(require("lodash"));
8
8
  const utils_1 = require("./utils/utils");
9
+ const containerManager_1 = require("./containerManager");
9
10
  exports.EVENT_STATUS_CHANGED = 'status-changed';
10
11
  exports.EVENT_INSTANCE_CREATED = 'instance-created';
11
12
  exports.EVENT_INSTANCE_EXITED = 'instance-exited';
@@ -48,30 +49,28 @@ class SocketManager {
48
49
  emitSystemEvent(systemId, type, payload) {
49
50
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
50
51
  try {
51
- exports.socketManager.emit(`${systemId}/instances`, type, payload);
52
+ const contextId = `system-events/${encodeURIComponent(systemId)}`;
53
+ this.emit(contextId, type, payload);
52
54
  }
53
55
  catch (e) {
54
56
  console.warn('Failed to emit instance event: %s', e.message);
55
57
  }
56
58
  }
57
59
  emitInstanceLog(systemId, instanceId, payload) {
58
- this.emitInstanceEvent(systemId, instanceId, exports.EVENT_INSTANCE_LOG, payload);
59
- }
60
- emitSystemLog(systemId, payload) {
61
- this.emitSystemEvent(systemId, exports.EVENT_SYSTEM_LOG, payload);
62
- }
63
- emitGlobalLog(payload) {
64
- this.emitGlobal(exports.EVENT_LOG, payload);
65
- }
66
- emitInstanceEvent(systemId, instanceId, type, payload) {
67
60
  systemId = (0, utils_1.normalizeKapetaUri)(systemId);
68
61
  try {
69
- exports.socketManager.emit(`${systemId}/instances/${instanceId}`, type, payload);
62
+ this.emit(`instance-logs/${encodeURIComponent(systemId)}/${encodeURIComponent(instanceId)}`, exports.EVENT_INSTANCE_LOG, payload);
70
63
  }
71
64
  catch (e) {
72
65
  console.warn('Failed to emit instance event: %s', e.message);
73
66
  }
74
67
  }
68
+ emitSystemLog(systemId, payload) {
69
+ this.emitSystemEvent(systemId, exports.EVENT_SYSTEM_LOG, payload);
70
+ }
71
+ emitGlobalLog(payload) {
72
+ this.emitGlobal(exports.EVENT_LOG, payload);
73
+ }
75
74
  _bindIO() {
76
75
  this.io.on('connection', (socket) => this._handleSocketCreated(socket));
77
76
  }
@@ -80,16 +79,42 @@ class SocketManager {
80
79
  this._sockets.push(socket);
81
80
  }
82
81
  _bindSocket(socket) {
83
- socket.on('disconnect', () => this._handleSocketDestroyed(socket));
82
+ socket.on('disconnect', () => {
83
+ lodash_1.default.pull(this._sockets, socket);
84
+ socket.rooms.forEach((roomId) => {
85
+ this.handleLeaveRoom(roomId);
86
+ });
87
+ });
84
88
  socket.on('join', (id) => {
85
89
  socket.join(id);
90
+ this.handleJoinRoom(id);
86
91
  });
87
92
  socket.on('leave', (id) => {
88
93
  socket.leave(id);
94
+ this.handleLeaveRoom(id);
89
95
  });
90
96
  }
91
- _handleSocketDestroyed(socket) {
92
- lodash_1.default.pull(this._sockets, socket);
97
+ handleJoinRoom(id) {
98
+ if (id.startsWith('instance-logs/')) {
99
+ let [, systemId, instanceId] = id.split(/\//g);
100
+ systemId = decodeURIComponent(systemId);
101
+ instanceId = decodeURIComponent(instanceId);
102
+ console.log('Start listening for logs', systemId, instanceId);
103
+ containerManager_1.containerManager
104
+ .ensureLogListening(systemId, instanceId, (log) => {
105
+ this.emitInstanceLog(systemId, instanceId, log);
106
+ })
107
+ .catch((e) => { });
108
+ }
109
+ }
110
+ handleLeaveRoom(id) {
111
+ if (id.startsWith('instance-logs/')) {
112
+ let [, systemId, instanceId] = id.split(/\//g);
113
+ systemId = decodeURIComponent(systemId);
114
+ instanceId = decodeURIComponent(instanceId);
115
+ console.log('Stop listening for logs', systemId, instanceId);
116
+ containerManager_1.containerManager.stopLogListening(systemId, instanceId).catch((e) => { });
117
+ }
93
118
  }
94
119
  }
95
120
  exports.SocketManager = SocketManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kapeta/local-cluster-service",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Manages configuration, ports and service discovery for locally running Kapeta systems",
5
5
  "type": "commonjs",
6
6
  "exports": {
@@ -13,6 +13,7 @@ import { getBlockInstanceContainerName } from './utils/utils';
13
13
  import { InstanceInfo, LogEntry, LogSource } from './types';
14
14
  import { KapetaAPI } from '@kapeta/nodejs-api-client';
15
15
  import { taskManager, Task } from './taskManager';
16
+ import { EventEmitter } from 'node:events';
16
17
 
17
18
  type StringMap = { [key: string]: string };
18
19
 
@@ -78,6 +79,7 @@ class ContainerManager {
78
79
  private _mountDir: string;
79
80
  private _version: string;
80
81
  private _lastDockerAccessCheck: number = 0;
82
+ private logStreams: { [p: string]: { stream?: ClosableLogStream; timer?: NodeJS.Timeout } } = {};
81
83
 
82
84
  constructor() {
83
85
  this._docker = null;
@@ -607,6 +609,193 @@ class ContainerManager {
607
609
 
608
610
  return containerInfo.getLogs();
609
611
  }
612
+
613
+ async stopLogListening(systemId: string, instanceId: string) {
614
+ const containerName = getBlockInstanceContainerName(systemId, instanceId);
615
+ if (this.logStreams[containerName]) {
616
+ if (this.logStreams[containerName]?.timer) {
617
+ clearTimeout(this.logStreams[containerName].timer);
618
+ }
619
+
620
+ console.log('Stopped listening for logs on container: %s', containerName);
621
+ try {
622
+ const stream = this.logStreams[containerName].stream;
623
+ if (stream) {
624
+ await stream.close();
625
+ }
626
+ } catch (err) {
627
+ // Ignore
628
+ }
629
+ delete this.logStreams[containerName];
630
+ }
631
+ }
632
+
633
+ async ensureLogListening(systemId: string, instanceId: string, handler: (log: LogEntry) => void) {
634
+ const containerName = getBlockInstanceContainerName(systemId, instanceId);
635
+ try {
636
+ if (this.logStreams[containerName]?.stream) {
637
+ // Already listening - will shut itself down
638
+ return;
639
+ }
640
+
641
+ if (this.logStreams[containerName]?.timer) {
642
+ clearTimeout(this.logStreams[containerName].timer);
643
+ }
644
+
645
+ const tryLater = () => {
646
+ this.logStreams[containerName] = {
647
+ timer: setTimeout(() => {
648
+ // Keep trying until user decides to not listen anymore
649
+ this.ensureLogListening(systemId, instanceId, handler);
650
+ }, 5000),
651
+ };
652
+ };
653
+
654
+ const containerInfo = await this.getContainerByName(containerName);
655
+ if (!containerInfo || !(await containerInfo.isRunning())) {
656
+ // Container not currently running - try again in 5 seconds
657
+ tryLater();
658
+ return;
659
+ }
660
+
661
+ const stream = await containerInfo.getLogStream();
662
+ stream.onLog((log) => {
663
+ try {
664
+ handler(log);
665
+ } catch (err) {
666
+ console.warn('Error handling log', err);
667
+ }
668
+ });
669
+ stream.onEnd(() => {
670
+ // We get here if the container is stopped
671
+ delete this.logStreams[containerName];
672
+ tryLater();
673
+ });
674
+ stream.onError((err) => {
675
+ // We get here if the container crashes
676
+ delete this.logStreams[containerName];
677
+ tryLater();
678
+ });
679
+
680
+ this.logStreams[containerName] = {
681
+ stream,
682
+ };
683
+ } catch (err) {
684
+ // Ignore
685
+ }
686
+ }
687
+ }
688
+
689
+ function readLogBuffer(logBuffer: Buffer) {
690
+ const out: LogEntry[] = [];
691
+ let offset = 0;
692
+ while (offset < logBuffer.length) {
693
+ try {
694
+ // Read the docker log format - explained here:
695
+ // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
696
+ // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
697
+
698
+ // First byte is stream type
699
+ const streamTypeInt = logBuffer.readInt8(offset);
700
+ const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
701
+ if (streamTypeInt !== 1 && streamTypeInt !== 2) {
702
+ console.error('Unknown stream type: %s', streamTypeInt, out[out.length - 1]);
703
+ break;
704
+ }
705
+
706
+ // Bytes 4-8 is frame size
707
+ const messageLength = logBuffer.readInt32BE(offset + 4);
708
+
709
+ // After that is the message - with the message length
710
+ const dataWithoutStreamType = logBuffer.subarray(offset + 8, offset + 8 + messageLength);
711
+ const raw = dataWithoutStreamType.toString();
712
+
713
+ // Split the message into date and message
714
+ const firstSpaceIx = raw.indexOf(' ');
715
+ const dateString = raw.substring(0, firstSpaceIx);
716
+ const line = raw.substring(firstSpaceIx + 1);
717
+ offset = offset + messageLength + 8;
718
+ if (!dateString) {
719
+ break;
720
+ }
721
+ out.push({
722
+ time: new Date(dateString).getTime(),
723
+ message: line,
724
+ level: 'INFO',
725
+ source: streamType,
726
+ });
727
+ } catch (err) {
728
+ console.error('Error parsing log entry', err);
729
+ offset = logBuffer.length;
730
+ break;
731
+ }
732
+ }
733
+ return out;
734
+ }
735
+
736
+ class ClosableLogStream {
737
+ private readonly stream: FSExtra.ReadStream;
738
+
739
+ private readonly eventEmitter: EventEmitter;
740
+
741
+ constructor(stream: FSExtra.ReadStream) {
742
+ this.stream = stream;
743
+ this.eventEmitter = new EventEmitter();
744
+ stream.on('data', (data) => {
745
+ const logs = readLogBuffer(data as Buffer);
746
+ logs.forEach((log) => {
747
+ this.eventEmitter.emit('log', log);
748
+ });
749
+ });
750
+
751
+ stream.on('end', () => {
752
+ this.eventEmitter.emit('end');
753
+ });
754
+
755
+ stream.on('error', (error) => {
756
+ this.eventEmitter.emit('error', error);
757
+ });
758
+
759
+ stream.on('close', () => {
760
+ this.eventEmitter.emit('end');
761
+ });
762
+ }
763
+
764
+ onLog(listener: (log: LogEntry) => void) {
765
+ this.eventEmitter.on('log', listener);
766
+ return () => {
767
+ this.eventEmitter.removeListener('log', listener);
768
+ };
769
+ }
770
+
771
+ onEnd(listener: () => void) {
772
+ this.eventEmitter.on('end', listener);
773
+ return () => {
774
+ this.eventEmitter.removeListener('end', listener);
775
+ };
776
+ }
777
+
778
+ onError(listener: (error: Error) => void) {
779
+ this.eventEmitter.on('error', listener);
780
+ return () => {
781
+ this.eventEmitter.removeListener('error', listener);
782
+ };
783
+ }
784
+
785
+ close() {
786
+ return new Promise<void>((resolve, reject) => {
787
+ try {
788
+ this.stream.close((err) => {
789
+ if (err) {
790
+ console.warn('Error closing log stream', err);
791
+ }
792
+ resolve();
793
+ });
794
+ } catch (err) {
795
+ // Ignore
796
+ }
797
+ });
798
+ }
610
799
  }
611
800
 
612
801
  export class ContainerInfo {
@@ -718,57 +907,38 @@ export class ContainerInfo {
718
907
  return ports;
719
908
  }
720
909
 
910
+ async getLogStream() {
911
+ try {
912
+ const logStream = (await this.native.logs({
913
+ stdout: true,
914
+ stderr: true,
915
+ follow: true,
916
+ tail: 0,
917
+ timestamps: true,
918
+ })) as ReadStream;
919
+
920
+ return new ClosableLogStream(logStream);
921
+ } catch (err) {
922
+ console.log('Error getting log stream', err);
923
+ throw err;
924
+ }
925
+ }
926
+
721
927
  async getLogs(): Promise<LogEntry[]> {
722
928
  const logStream = (await this.native.logs({
723
929
  stdout: true,
724
930
  stderr: true,
725
931
  follow: false,
726
- tail: 100,
727
932
  timestamps: true,
728
933
  })) as ReadStream;
729
934
 
730
- const out = [] as LogEntry[];
935
+ const chunks: Buffer[] = [];
731
936
  await promisifyStream(logStream, (data) => {
732
- const buf = data as Buffer;
733
- let offset = 0;
734
- while (offset < buf.length) {
735
- try {
736
- // Read the docker log format - explained here:
737
- // https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach
738
- // or here : https://ahmet.im/blog/docker-logs-api-binary-format-explained/
739
-
740
- // First byte is stream type
741
- const streamTypeInt = buf.readInt8(offset);
742
- const streamType: LogSource = streamTypeInt === 1 ? 'stdout' : 'stderr';
743
-
744
- // Bytes 4-8 is frame size
745
- const messageLength = buf.readInt32BE(offset + 4);
746
-
747
- // After that is the message - with the message length
748
- const dataWithoutStreamType = buf.subarray(offset + 8, offset + 8 + messageLength);
749
- const raw = dataWithoutStreamType.toString();
750
-
751
- // Split the message into date and message
752
- const firstSpaceIx = raw.indexOf(' ');
753
- const dateString = raw.substring(0, firstSpaceIx);
754
- const line = raw.substring(firstSpaceIx + 1);
755
- offset = offset + messageLength + 8;
756
- if (!dateString) {
757
- continue;
758
- }
759
- out.push({
760
- time: new Date(dateString).getTime(),
761
- message: line,
762
- level: 'INFO',
763
- source: streamType,
764
- });
765
- } catch (err) {
766
- console.error('Error parsing log entry', err);
767
- offset = buf.length;
768
- }
769
- }
937
+ chunks.push(data as Buffer);
770
938
  });
771
939
 
940
+ const out = readLogBuffer(Buffer.concat(chunks));
941
+
772
942
  if (out.length === 0) {
773
943
  out.push({
774
944
  time: Date.now(),
@@ -529,12 +529,6 @@ export class InstanceManager {
529
529
 
530
530
  socketManager.emitInstanceLog(systemId, instanceId, logs[0]);
531
531
 
532
- socketManager.emitInstanceEvent(systemId, blockInstance.id, EVENT_INSTANCE_EXITED, {
533
- error: `Failed to start instance: ${e.message}`,
534
- status: EVENT_INSTANCE_EXITED,
535
- instanceId: blockInstance.id,
536
- });
537
-
538
532
  return out;
539
533
  }
540
534
  },
@@ -606,6 +600,7 @@ export class InstanceManager {
606
600
  try {
607
601
  const plan = await assetManager.getAsset(instance.systemId, true, false);
608
602
  if (!plan) {
603
+ console.log('Plan not found - reset to stop', instance.ref, instance.systemId);
609
604
  instance.desiredStatus = DesiredInstanceStatus.STOP;
610
605
  changed = true;
611
606
  return;
@@ -614,6 +609,7 @@ export class InstanceManager {
614
609
  const planData = plan.data as Plan;
615
610
  const planInstance = planData?.spec?.blocks?.find((b) => b.id === instance.instanceId);
616
611
  if (!planInstance || !planInstance?.block?.ref) {
612
+ console.log('Plan instance not found - reset to stop', instance.ref, instance.systemId);
617
613
  instance.desiredStatus = DesiredInstanceStatus.STOP;
618
614
  changed = true;
619
615
  return;
@@ -621,6 +617,7 @@ export class InstanceManager {
621
617
 
622
618
  const blockDef = await assetManager.getAsset(instance.ref, true, false);
623
619
  if (!blockDef) {
620
+ console.log('Block definition not found - reset to stop', instance.ref, instance.systemId);
624
621
  instance.desiredStatus = DesiredInstanceStatus.STOP;
625
622
  changed = true;
626
623
  return;
@@ -836,7 +833,3 @@ export class InstanceManager {
836
833
  }
837
834
 
838
835
  export const instanceManager = new InstanceManager();
839
-
840
- process.on('exit', async () => {
841
- await instanceManager.stopAll();
842
- });
@@ -2,6 +2,7 @@ import _ from 'lodash';
2
2
  import { Socket, Server } from 'socket.io';
3
3
  import { normalizeKapetaUri } from './utils/utils';
4
4
  import { LogEntry } from './types';
5
+ import { containerManager } from './containerManager';
5
6
  export const EVENT_STATUS_CHANGED = 'status-changed';
6
7
  export const EVENT_INSTANCE_CREATED = 'instance-created';
7
8
  export const EVENT_INSTANCE_EXITED = 'instance-exited';
@@ -53,14 +54,24 @@ export class SocketManager {
53
54
  emitSystemEvent(systemId: string, type: string, payload: any) {
54
55
  systemId = normalizeKapetaUri(systemId);
55
56
  try {
56
- socketManager.emit(`${systemId}/instances`, type, payload);
57
+ const contextId = `system-events/${encodeURIComponent(systemId)}`;
58
+ this.emit(contextId, type, payload);
57
59
  } catch (e: any) {
58
60
  console.warn('Failed to emit instance event: %s', e.message);
59
61
  }
60
62
  }
61
63
 
62
64
  emitInstanceLog(systemId: string, instanceId: string, payload: LogEntry) {
63
- this.emitInstanceEvent(systemId, instanceId, EVENT_INSTANCE_LOG, payload);
65
+ systemId = normalizeKapetaUri(systemId);
66
+ try {
67
+ this.emit(
68
+ `instance-logs/${encodeURIComponent(systemId)}/${encodeURIComponent(instanceId)}`,
69
+ EVENT_INSTANCE_LOG,
70
+ payload
71
+ );
72
+ } catch (e: any) {
73
+ console.warn('Failed to emit instance event: %s', e.message);
74
+ }
64
75
  }
65
76
 
66
77
  emitSystemLog(systemId: string, payload: LogEntry) {
@@ -71,15 +82,6 @@ export class SocketManager {
71
82
  this.emitGlobal(EVENT_LOG, payload);
72
83
  }
73
84
 
74
- emitInstanceEvent(systemId: string, instanceId: string, type: string, payload: any) {
75
- systemId = normalizeKapetaUri(systemId);
76
- try {
77
- socketManager.emit(`${systemId}/instances/${instanceId}`, type, payload);
78
- } catch (e: any) {
79
- console.warn('Failed to emit instance event: %s', e.message);
80
- }
81
- }
82
-
83
85
  private _bindIO() {
84
86
  this.io.on('connection', (socket) => this._handleSocketCreated(socket));
85
87
  }
@@ -90,17 +92,42 @@ export class SocketManager {
90
92
  }
91
93
 
92
94
  private _bindSocket(socket: Socket) {
93
- socket.on('disconnect', () => this._handleSocketDestroyed(socket));
95
+ socket.on('disconnect', () => {
96
+ _.pull(this._sockets, socket);
97
+ socket.rooms.forEach((roomId) => {
98
+ this.handleLeaveRoom(roomId);
99
+ });
100
+ });
94
101
  socket.on('join', (id) => {
95
102
  socket.join(id);
103
+ this.handleJoinRoom(id);
96
104
  });
97
105
  socket.on('leave', (id) => {
98
106
  socket.leave(id);
107
+ this.handleLeaveRoom(id);
99
108
  });
100
109
  }
101
-
102
- private _handleSocketDestroyed(socket: Socket) {
103
- _.pull(this._sockets, socket);
110
+ private handleJoinRoom(id: string) {
111
+ if (id.startsWith('instance-logs/')) {
112
+ let [, systemId, instanceId] = id.split(/\//g);
113
+ systemId = decodeURIComponent(systemId);
114
+ instanceId = decodeURIComponent(instanceId);
115
+ console.log('Start listening for logs', systemId, instanceId);
116
+ containerManager
117
+ .ensureLogListening(systemId, instanceId, (log) => {
118
+ this.emitInstanceLog(systemId, instanceId, log);
119
+ })
120
+ .catch((e) => {});
121
+ }
122
+ }
123
+ private handleLeaveRoom(id: string) {
124
+ if (id.startsWith('instance-logs/')) {
125
+ let [, systemId, instanceId] = id.split(/\//g);
126
+ systemId = decodeURIComponent(systemId);
127
+ instanceId = decodeURIComponent(instanceId);
128
+ console.log('Stop listening for logs', systemId, instanceId);
129
+ containerManager.stopLogListening(systemId, instanceId).catch((e) => {});
130
+ }
104
131
  }
105
132
  }
106
133
  export const socketManager = new SocketManager();