Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions scripts/check-telemetry-policy-parity.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,6 @@ const protocolOnlyResults = new Set(
const terminalResults = new Set(
[...protocolResults].filter((value) => !protocolOnlyResults.has(value))
);
compareSets(
"TypeScript private protocol result",
protocolOnlyResults,
typescriptValues("CONSENT_PROTOCOL_ONLY_RESULTS")
);
compareSets(
"TypeScript consent result",
terminalResults,
Expand Down
10 changes: 5 additions & 5 deletions sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ getUserProfilePolicy() β†’ FilesystemPolicyResult
getTemporaryFilesPolicy(env?) β†’ FilesystemPolicyResult

// Telemetry consent (Windows-only; see Telemetry Consent section below)
queryTelemetryConsentAsync() β†’ Promise<{ storedState, effectiveState, needsPrompt, policy, error? }>
queryTelemetryConsentAsync() β†’ Promise<{ state, storedState, effectiveState, needsPrompt, policy, error? }>
requestTelemetryConsent(presenter, locale?) β†’ Promise<TelemetryConsentOutcome>
withdrawTelemetryConsentAsync() β†’ Promise<TelemetryConsentOutcome>

Expand Down Expand Up @@ -670,10 +670,10 @@ telemetry remains off. On non-Windows hosts requests and withdrawals return
`notApplicable` without invoking the presenter.

`queryTelemetryConsentAsync()` fails closed to `'undetermined'` rather than
`'granted'`. Its `error` field is present when the command fails or returns an
invalid response. A valid native fail-closed response can return
`'undetermined'` or a blocked policy without `error`; any accompanying native
diagnostic is reported once through `console.warn`:
`'granted'`. Its `error` field is present when the native query fails or
returns an invalid response, and the SDK writes a one-time diagnostic to the
process's standard error stream. A valid native fail-closed response can return
`'undetermined'` or a blocked policy without `error` or diagnostic output:

```typescript
const { effectiveState, storedState, needsPrompt, policy, error } =
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"watch": "tsc --watch",
"clean": "rimraf dist",
"test": "npm run test:unit",
"test:unit": "npm run build:test-unit && node --test dist-tests/tests/unit/sandbox.test.js dist-tests/tests/unit/policy.test.js dist-tests/tests/unit/logger.test.js dist-tests/tests/unit/errors.test.js dist-tests/tests/unit/state-aware-types.test.js dist-tests/tests/unit/state-aware.test.js dist-tests/tests/unit/state-aware-binding.test.js dist-tests/tests/unit/platform.test.js dist-tests/tests/unit/native-library.test.js dist-tests/tests/unit/binding-request.test.js dist-tests/tests/unit/binding-run.test.js dist-tests/tests/unit/streaming-binding.test.js dist-tests/tests/unit/inprocess-run.test.js dist-tests/tests/unit/sandbox-process.test.js dist-tests/tests/unit/telemetry.test.js dist-tests/tests/unit/default-consent-protocol-runner.test.js dist-tests/tests/unit/wire-conformance.test.js dist-tests/tests/unit/wire-conformance-state-aware.test.js",
"test:unit": "npm run build:test-unit && node --test dist-tests/tests/unit/sandbox.test.js dist-tests/tests/unit/policy.test.js dist-tests/tests/unit/logger.test.js dist-tests/tests/unit/errors.test.js dist-tests/tests/unit/state-aware-types.test.js dist-tests/tests/unit/state-aware.test.js dist-tests/tests/unit/state-aware-binding.test.js dist-tests/tests/unit/platform.test.js dist-tests/tests/unit/native-library.test.js dist-tests/tests/unit/binding-request.test.js dist-tests/tests/unit/binding-run.test.js dist-tests/tests/unit/streaming-binding.test.js dist-tests/tests/unit/inprocess-run.test.js dist-tests/tests/unit/sandbox-process.test.js dist-tests/tests/unit/telemetry.test.js dist-tests/tests/unit/telemetry-request-worker.test.js dist-tests/tests/unit/wire-conformance.test.js dist-tests/tests/unit/wire-conformance-state-aware.test.js",
"test:integration": "cd tests/integration && npm install && npm run build && npm test",
"prepublishOnly": "npm run build",
"typecheck:integration": "cd tests/integration && npx tsc --noEmit -p tsconfig.json",
Expand Down
2 changes: 2 additions & 0 deletions sdk/node/src/bindings/native-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function _errorCodeForNativeStatus(status: number): ErrorCode {
12: 'backend_error',
100: 'malformed_request',
101: 'malformed_request',
102: 'backend_error',
103: 'backend_error',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curiuos

};
return codes[status] ?? 'backend_error';
}
Expand Down
50 changes: 50 additions & 0 deletions sdk/node/src/bindings/telemetry-request-worker-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Worker-thread entry point for the blocking native consent request.

import { parentPort, workerData } from 'node:worker_threads';
import { MxcError } from '../errors.js';
import {
requestTelemetryConsentJson,
} from './telemetry.js';
import type {
TelemetryRequestWorkerData,
TelemetryRequestWorkerMessage,
} from './telemetry-request-worker.js';

function serializeError(error: unknown) {
if (error instanceof MxcError) {
return {
code: error.code,
message: error.message,
operation: error.operation,
nativeCode: error.nativeCode,
remediation: error.remediation,
details: error.details,
};
}
return {
code: 'backend_error' as const,
message: error instanceof Error ? error.message : String(error),
};
}

const data = workerData as TelemetryRequestWorkerData;
let message: TelemetryRequestWorkerMessage;
try {
const decision = new Int32Array(data.decisionShared);
message = {
kind: 'payload',
payload: requestTelemetryConsentJson(data.locale, (promptJson) => {
Atomics.store(decision, 0, 0);
Atomics.store(decision, 1, 0);
parentPort!.postMessage({ kind: 'present', promptJson } satisfies TelemetryRequestWorkerMessage);
Atomics.wait(decision, 0, 0);
return Atomics.load(decision, 1);
}),
};
} catch (error) {
message = { kind: 'error', error: serializeError(error) };
}
parentPort!.postMessage(message);
176 changes: 176 additions & 0 deletions sdk/node/src/bindings/telemetry-request-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Main-thread bridge for the blocking native consent presenter callback.

import { Worker } from 'node:worker_threads';
import { MxcError, type MxcErrorFields } from '../errors.js';
import { TELEMETRY_CONSENT_PRESENTER_ERROR } from './telemetry.js';

export interface TelemetryRequestWorkerData {
locale?: string;
decisionShared: SharedArrayBuffer;
}

export type TelemetryRequestWorkerMessage =
| { kind: 'payload'; payload: string }
| { kind: 'present'; promptJson: string }
| { kind: 'error'; error: MxcErrorFields };

export interface BindingTelemetryWorkerLike {
on(event: 'message', listener: (message: TelemetryRequestWorkerMessage) => void): this;
on(event: 'error', listener: (error: Error) => void): this;
on(event: 'exit', listener: (code: number) => void): this;
unref(): void;
terminate(): void;
}

type WorkerFactory = (data: TelemetryRequestWorkerData) => BindingTelemetryWorkerLike;

const DEFAULT_TELEMETRY_REQUEST_TIMEOUT_MS = 30_000;

const defaultWorkerFactory: WorkerFactory = (data) => new Worker(
new URL('./telemetry-request-worker-entry.js', import.meta.url),
{ workerData: data, execArgv: [] },
);

let workerFactory = defaultWorkerFactory;

export function _setBindingTelemetryWorkerFactory(factory?: WorkerFactory): void {
workerFactory = factory ?? defaultWorkerFactory;
}

function serializeUnknownError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}

export function runTelemetryConsentRequestAsync(
locale: string | undefined,
presenter: (promptJson: string, signal: AbortSignal) => number | Promise<number>,
timeoutMs = DEFAULT_TELEMETRY_REQUEST_TIMEOUT_MS,
): Promise<string> {
const decision = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 2));
return new Promise((resolve, reject) => {
const worker = workerFactory({
locale,
decisionShared: decision.buffer as SharedArrayBuffer,
});
let settled = false;
let presenterAbort: AbortController | null = null;
let presenterError: Error | undefined;
let decisionWritten = false;
let deadline: ReturnType<typeof setTimeout> | undefined;
let deadlineStartedAt = 0;
let deadlineRemainingMs = timeoutMs;

const writeDecision = (code: number): void => {
if (decisionWritten) {
return;
}
decisionWritten = true;
Atomics.store(decision, 1, code);
Atomics.store(decision, 0, 1);
Atomics.notify(decision, 0);
};

const clearDeadline = (): void => {
if (deadline !== undefined) {
clearTimeout(deadline);
deadline = undefined;
}
};

const pauseDeadline = (): void => {
if (deadline === undefined) {
return;
}
deadlineRemainingMs = Math.max(
0,
deadlineRemainingMs - (Date.now() - deadlineStartedAt),
);
clearDeadline();
};

const finish = (action: () => void) => {
if (settled) {
return;
}
settled = true;
clearDeadline();
presenterAbort?.abort();
if (!decisionWritten) {
writeDecision(TELEMETRY_CONSENT_PRESENTER_ERROR);
}
action();
};

const armDeadline = (): void => {
if (settled || deadline !== undefined) {
return;
}
if (deadlineRemainingMs <= 0) {
finish(() => {
worker.unref();
worker.terminate();
reject(new Error('telemetry consent request timed out'));
});
return;
}
deadlineStartedAt = Date.now();
deadline = setTimeout(() => {
finish(() => {
worker.unref();
worker.terminate();
reject(new Error('telemetry consent request timed out'));
});
}, deadlineRemainingMs);
};

worker.on('message', (message) => {
if (message.kind === 'present') {
if (presenterAbort !== null) {
finish(() => reject(new Error('telemetry request worker requested presentation twice')));
return;
}
pauseDeadline();
presenterAbort = new AbortController();
void (async () => {
let code = TELEMETRY_CONSENT_PRESENTER_ERROR;
try {
code = await presenter(message.promptJson, presenterAbort.signal);
if (!Number.isSafeInteger(code)) {
throw new Error(`consent presenter returned invalid decision '${String(code)}'`);
}
} catch (error) {
presenterError = serializeUnknownError(error);
code = TELEMETRY_CONSENT_PRESENTER_ERROR;
}
armDeadline();
writeDecision(code);
})();
return;
}
if (message.kind === 'payload') {
finish(() => {
if (presenterError) {
reject(presenterError);
} else {
resolve(message.payload);
}
});
return;
}
if (message.kind === 'error') {
finish(() => reject(presenterError ?? new MxcError(message.error)));
return;
}
finish(() => reject(new Error('telemetry request worker returned an unexpected message')));
});
worker.on('error', (error) => finish(() => reject(error)));
worker.on('exit', (code) => finish(() => reject(presenterError ?? new MxcError({
code: 'backend_error',
message: `telemetry worker exited before returning a result (code ${code})`,
}))));
armDeadline();
});
}
Loading
Loading