-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(core): add logging and progress message types to daemon #35342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
ecca529
2f94962
b12a456
a598127
0c31a66
a7bab9e
201e12a
7d19437
7cc774d
f64e9e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,13 @@ import { | |
| DAEMON_OUTPUT_LOG_FILE, | ||
| } from './tmp-dir'; | ||
| import { nxVersion } from '../utils/versions'; | ||
| import { ProgressTopic } from '../utils/progress-topics'; | ||
| import { EMIT_LOG, EmitLogLevel } from './message-types/streaming-messages'; | ||
| import { | ||
| assertOnDaemon, | ||
| getTopicSubscribers, | ||
| writeStreamingMessage, | ||
| } from './server/client-socket-context'; | ||
|
|
||
| type LogSource = 'Server' | 'Client'; | ||
|
|
||
|
|
@@ -51,6 +58,30 @@ class DaemonLogger { | |
| this.log(`[WATCHER]: ${s.join(' ')}`); | ||
| } | ||
|
|
||
| /** | ||
| * Broadcasts a log line to every client currently subscribed to the | ||
| * given topic. Useful for warnings raised inside daemon-executed code | ||
| * that we want the user to see in their terminal rather than lose to | ||
| * the daemon log file. | ||
| * | ||
| * Falls back to writing into the daemon log when no clients are | ||
| * subscribed to the topic. | ||
| * | ||
| * Must only be invoked from inside the Nx daemon process. | ||
| */ | ||
| logToClient(topic: ProgressTopic, level: EmitLogLevel, message: string) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should import a function from the client socket context... shouldn't have the implementation here |
||
| assertOnDaemon('DaemonLogger#logToClient'); | ||
| const subscribers = getTopicSubscribers(topic); | ||
| if (!subscribers?.size) { | ||
| this.log(`[emit-log:${level}] ${message}`); | ||
| return; | ||
| } | ||
| const payload = { type: EMIT_LOG, level, message }; | ||
| for (const socket of subscribers) { | ||
| writeStreamingMessage(socket, payload); | ||
| } | ||
| } | ||
|
|
||
| private writeToFile(message: string) { | ||
| try { | ||
| if (!existsSync(DAEMON_DIR_FOR_CURRENT_WORKSPACE)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| export const UPDATE_PROGRESS_MESSAGE = 'UPDATE_PROGRESS_MESSAGE' as const; | ||
|
|
||
| export type UpdateProgressMessage = { | ||
| type: typeof UPDATE_PROGRESS_MESSAGE; | ||
| message: string; | ||
| }; | ||
|
|
||
| export function isUpdateProgressMessage( | ||
| message: unknown | ||
| ): message is UpdateProgressMessage { | ||
| return ( | ||
| typeof message === 'object' && | ||
| message !== null && | ||
| 'type' in message && | ||
| message['type'] === UPDATE_PROGRESS_MESSAGE | ||
| ); | ||
| } | ||
|
|
||
| export const EMIT_LOG = 'EMIT_LOG' as const; | ||
|
|
||
| export type EmitLogLevel = 'log' | 'warn' | 'error'; | ||
|
|
||
| export type EmitLogMessage = { | ||
| type: typeof EMIT_LOG; | ||
| level: EmitLogLevel; | ||
| message: string; | ||
| }; | ||
|
|
||
| export function isEmitLogMessage(message: unknown): message is EmitLogMessage { | ||
| return ( | ||
| typeof message === 'object' && | ||
| message !== null && | ||
| 'type' in message && | ||
| message['type'] === EMIT_LOG | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import type { Socket } from 'net'; | ||
| import { MESSAGE_END_SEQ } from '../../utils/consume-messages-from-socket'; | ||
| import { ProgressTopic } from '../../utils/progress-topics'; | ||
| import { UPDATE_PROGRESS_MESSAGE } from '../message-types/streaming-messages'; | ||
| import { isOnDaemon } from '../is-on-daemon'; | ||
| import { serialize } from '../socket-utils'; | ||
|
|
||
| const topicSubscribers = new Map<ProgressTopic, Set<Socket>>(); | ||
|
|
||
| export function subscribeClientToTopic( | ||
| socket: Socket, | ||
| topic: ProgressTopic | ||
| ): void { | ||
| let subscribers = topicSubscribers.get(topic); | ||
| if (!subscribers) { | ||
| subscribers = new Set(); | ||
| topicSubscribers.set(topic, subscribers); | ||
| } | ||
| subscribers.add(socket); | ||
| } | ||
|
|
||
| export function unsubscribeClientFromTopic( | ||
| socket: Socket, | ||
| topic: ProgressTopic | ||
| ): void { | ||
| const subscribers = topicSubscribers.get(topic); | ||
| if (!subscribers) return; | ||
| subscribers.delete(socket); | ||
| if (subscribers.size === 0) topicSubscribers.delete(topic); | ||
| } | ||
|
|
||
| export function getTopicSubscribers( | ||
| topic: ProgressTopic | ||
| ): ReadonlySet<Socket> | undefined { | ||
| return topicSubscribers.get(topic); | ||
| } | ||
|
|
||
| export function assertOnDaemon(helperName: string) { | ||
| if (!isOnDaemon()) { | ||
| throw new Error( | ||
| `${helperName} can only be called from the Nx daemon process.` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Writes a streaming message over the given socket using the daemon's | ||
| * configured serialization format and terminated with MESSAGE_END_SEQ. | ||
| * Errors are logged to the daemon's stdout (redirected to the daemon | ||
| * log) rather than propagated — a disconnected client shouldn't tear | ||
| * down the current request handler or other subscribers. | ||
| */ | ||
| export function writeStreamingMessage(socket: Socket, payload: unknown) { | ||
| try { | ||
| socket.write(serialize(payload) + MESSAGE_END_SEQ, (err) => { | ||
| if (err) { | ||
| console.log( | ||
| `Streaming message write error (client likely disconnected): ${err.message}` | ||
| ); | ||
| } | ||
| }); | ||
| } catch (e) { | ||
| console.log( | ||
| `Failed to send streaming message to client: ${ | ||
| e instanceof Error ? e.message : String(e) | ||
| }` | ||
| ); | ||
| } | ||
|
Comment on lines
+67
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Look at |
||
| } | ||
|
|
||
| /** | ||
| * Broadcasts a progress message to every client currently subscribed to | ||
| * the given topic. No-op when there are no subscribers. | ||
| * | ||
| * Must only be invoked from inside the Nx daemon process. | ||
| */ | ||
| export function sendProgressMessageToTopic( | ||
| topic: ProgressTopic, | ||
| message: string | ||
| ): void { | ||
| assertOnDaemon('sendProgressMessageToTopic'); | ||
| const subscribers = topicSubscribers.get(topic); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| if (!subscribers?.size) return; | ||
| const payload = { type: UPDATE_PROGRESS_MESSAGE, message }; | ||
| for (const socket of subscribers) { | ||
| writeStreamingMessage(socket, payload); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.