diff --git a/.gitignore b/.gitignore index 23866bfd..eb5696ca 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ package-lock.json # Test generated files coverage + +# Node's bash completion file +.node_bash_completion diff --git a/PACKAGE-COMPARISON.md b/PACKAGE-COMPARISON.md index 2b32adaf..1fe2848d 100644 --- a/PACKAGE-COMPARISON.md +++ b/PACKAGE-COMPARISON.md @@ -22,7 +22,7 @@ Load Balancing | :heavy_check_mark: | :x: Other Properties | `grpc` | `@grpc/grpc-js` -----------------|--------|---------------- Pure JavaScript Code | :x: | :heavy_check_mark: -Supported Node Versions | >= 4 | ^8.11.2 or >=9.4 +Supported Node Versions | >= 4 | ^8.13.0 or >=10.10.0 Supported Electron Versions | All | >= 3 Supported Platforms | Linux, Windows, MacOS | All Supported Architectures | x86, x86-64, ARM7+ | All @@ -36,4 +36,4 @@ In addition, all channel arguments defined in [this header file](https://github. - `grpc.keepalive_time_ms` - `grpc.keepalive_timeout_ms` - `channelOverride` - - `channelFactoryOverride` \ No newline at end of file + - `channelFactoryOverride` diff --git a/gulpfile.ts b/gulpfile.ts index 50b10c03..2a4fc914 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -15,96 +15,55 @@ * */ -import * as _gulp from 'gulp'; -import * as help from 'gulp-help'; - -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); - -const runSequence = require('run-sequence'); - -/** - * Require a module at the given path with a patched gulp object that prepends - * the given prefix to each task name. - * @param path The path to require. - * @param prefix The string to use as a prefix. This will be prepended to a task - * name with a '.' separator. - */ -function loadGulpTasksWithPrefix(path: string, prefix: string) { - const gulpTask = gulp.task; - gulp.task = ((taskName: string, ...args: any[]) => { - // Don't create a task for ${prefix}.help - if (taskName === 'help') { - return; - } - // The only array passed to gulp.task must be a list of dependent tasks. - const newArgs = args.map(arg => Array.isArray(arg) ? - arg.map(dep => `${prefix}.${dep}`) : arg); - gulpTask(`${prefix}.${taskName}`, ...newArgs); - }); - const result = require(path); - gulp.task = gulpTask; - return result; -} - -[ - ['./packages/grpc-health-check/gulpfile', 'health-check'], - ['./packages/grpc-js/gulpfile', 'js.core'], - ['./packages/grpc-native-core/gulpfile', 'native.core'], - ['./packages/proto-loader/gulpfile', 'protobuf'], - ['./test/gulpfile', 'internal.test'], -].forEach((args) => loadGulpTasksWithPrefix(args[0], args[1])); +import * as gulp from 'gulp'; +import * as healthCheck from './packages/grpc-health-check/gulpfile'; +import * as jsCore from './packages/grpc-js/gulpfile'; +import * as nativeCore from './packages/grpc-native-core/gulpfile'; +import * as protobuf from './packages/proto-loader/gulpfile'; +import * as internalTest from './test/gulpfile'; const root = __dirname; -gulp.task('install.all', 'Install dependencies for all subdirectory packages', - ['js.core.install', 'native.core.install', 'health-check.install', 'protobuf.install', 'internal.test.install']); +const installAll = gulp.parallel(jsCore.install, nativeCore.install, healthCheck.install, protobuf.install, internalTest.install); -gulp.task('install.all.windows', 'Install dependencies for all subdirectory packages for MS Windows', - ['js.core.install', 'native.core.install.windows', 'health-check.install', 'protobuf.install', 'internal.test.install']); +const installAllWindows = gulp.parallel(jsCore.install, nativeCore.installWindows, healthCheck.install, protobuf.install, internalTest.install); -gulp.task('lint', 'Emit linting errors in source and test files', - ['js.core.lint', 'native.core.lint']); +const lint = gulp.parallel(jsCore.lint, nativeCore.lint); -gulp.task('build', 'Build packages', ['js.core.compile', 'native.core.build', 'protobuf.compile']); +const build = gulp.parallel(jsCore.compile, nativeCore.build, protobuf.compile); -gulp.task('link.surface', 'Link to surface packages', - ['health-check.link.add']); +const link = gulp.series(healthCheck.linkAdd); -gulp.task('link', 'Link together packages', (callback) => { - /** - * We use workarounds for linking in some modules. See npm/npm#18835 - */ - runSequence('link.surface', callback); -}); +const setup = gulp.series(installAll, link); -gulp.task('setup', 'One-time setup for a clean repository', (callback) => { - runSequence('install.all', 'link', callback); -}); -gulp.task('setup.windows', 'One-time setup for a clean repository for MS Windows', (callback) => { - runSequence('install.all.windows', 'link', callback); -}); +const setupWindows = gulp.series(installAllWindows, link); -gulp.task('clean', 'Delete generated files', ['js.core.clean', 'native.core.clean', 'protobuf.clean']); +const clean = gulp.parallel(jsCore.clean, nativeCore.clean, protobuf.clean); -gulp.task('clean.all', 'Delete all files created by tasks', - ['js.core.clean.all', 'native.core.clean.all', 'health-check.clean.all', - 'internal.test.clean.all', 'protobuf.clean.all']); +const cleanAll = gulp.parallel(jsCore.cleanAll, nativeCore.cleanAll, healthCheck.cleanAll, internalTest.cleanAll, protobuf.cleanAll); -gulp.task('native.test.only', 'Run tests of native code without rebuilding anything', - ['native.core.test', 'health-check.test']); +const nativeTestOnly = gulp.parallel(nativeCore.test, healthCheck.test); -gulp.task('native.test', 'Run tests of native code', (callback) => { - runSequence('build', 'native.test.only', callback); -}); +const nativeTest = gulp.series(build, nativeTestOnly); -gulp.task('test.only', 'Run tests without rebuilding anything', - ['js.core.test', 'native.test.only', 'protobuf.test']); +const testOnly = gulp.parallel(jsCore.test, nativeTestOnly, protobuf.test); -gulp.task('test', 'Run all tests', (callback) => { - runSequence('build', 'test.only', 'internal.test.test', callback); -}); +const test = gulp.series(build, testOnly, internalTest.test); -gulp.task('doc.gen', 'Generate documentation', ['native.core.doc.gen']); +const docGen = gulp.series(nativeCore.docGen); -gulp.task('default', ['help']); +export { + installAll, + installAllWindows, + lint, + build, + link, + setup, + setupWindows, + clean, + cleanAll, + nativeTestOnly, + nativeTest, + test, + docGen +}; diff --git a/package.json b/package.json index 1ec8cc2c..00c647ef 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "devDependencies": { "@types/execa": "^0.8.0", "@types/gulp": "^4.0.5", - "@types/gulp-help": "0.0.34", "@types/gulp-mocha": "0.0.31", "@types/ncp": "^2.0.1", "@types/node": "^8.0.32", @@ -20,8 +19,7 @@ "coveralls": "^3.0.1", "del": "^3.0.0", "execa": "^0.8.0", - "gulp": "^3.9.1", - "gulp-help": "^1.6.1", + "gulp": "^4.0.1", "gulp-jsdoc3": "^1.0.1", "gulp-jshint": "^2.0.4", "gulp-mocha": "^4.3.1", @@ -42,7 +40,7 @@ "semver": "^5.5.0", "symlink": "^2.1.0", "through2": "^2.0.3", - "ts-node": "^3.3.0", + "ts-node": "^8.1.0", "tslint": "^5.5.0", "typescript": "~3.3.3333", "xml2js": "^0.4.19" diff --git a/packages/grpc-health-check/gulpfile.js b/packages/grpc-health-check/gulpfile.js deleted file mode 100644 index 09ac9115..00000000 --- a/packages/grpc-health-check/gulpfile.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2017 gRPC authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -const _gulp = require('gulp'); -const help = require('gulp-help'); -const mocha = require('gulp-mocha'); -const execa = require('execa'); -const path = require('path'); -const del = require('del'); -const linkSync = require('../../util').linkSync; - -const gulp = help(_gulp); - -const healthCheckDir = __dirname; -const baseDir = path.resolve(healthCheckDir, '..', '..'); -const testDir = path.resolve(healthCheckDir, 'test'); - -gulp.task('clean.links', 'Delete npm links', () => { - return del(path.resolve(healthCheckDir, 'node_modules/grpc')); -}); - -gulp.task('clean.all', 'Delete all code created by tasks', - ['clean.links']); - -gulp.task('install', 'Install health check dependencies', ['clean.links'], () => { - return execa('npm', ['install', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'}); -}); - -gulp.task('link.add', 'Link local copy of grpc', () => { - linkSync(healthCheckDir, './node_modules/grpc', '../grpc-native-core'); -}); - -gulp.task('test', 'Run health check tests', - () => { - return gulp.src(`${testDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'})); - }); diff --git a/packages/grpc-health-check/gulpfile.ts b/packages/grpc-health-check/gulpfile.ts new file mode 100644 index 00000000..c01f133d --- /dev/null +++ b/packages/grpc-health-check/gulpfile.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import * as gulp from 'gulp'; +import * as mocha from 'gulp-mocha'; +import * as execa from 'execa'; +import * as path from 'path'; +import * as del from 'del'; +import {linkSync} from '../../util'; + +const healthCheckDir = __dirname; +const baseDir = path.resolve(healthCheckDir, '..', '..'); +const testDir = path.resolve(healthCheckDir, 'test'); + +const cleanLinks = () => del(path.resolve(healthCheckDir, 'node_modules/grpc')); + +const cleanAll = gulp.parallel(cleanLinks); + +const runInstall = () => execa('npm', ['install', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'}); + +const install = gulp.series(cleanLinks, runInstall); + +const linkAdd = (callback) => { + linkSync(healthCheckDir, './node_modules/grpc', '../grpc-native-core'); + callback(); +} + +const test = () => gulp.src(`${testDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'})); + +export { + cleanLinks, + cleanAll, + install, + linkAdd, + test +} \ No newline at end of file diff --git a/packages/grpc-js/gulpfile.ts b/packages/grpc-js/gulpfile.ts index 72434d5b..14ddbfef 100644 --- a/packages/grpc-js/gulpfile.ts +++ b/packages/grpc-js/gulpfile.ts @@ -15,8 +15,7 @@ * */ -import * as _gulp from 'gulp'; -import * as help from 'gulp-help'; +import * as gulp from 'gulp'; import * as fs from 'fs'; import * as mocha from 'gulp-mocha'; @@ -26,9 +25,6 @@ import * as pify from 'pify'; import * as semver from 'semver'; import { ncp } from 'ncp'; -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); - const ncpP = pify(ncp); Error.stackTraceLimit = Infinity; @@ -44,35 +40,29 @@ const execNpmVerb = (verb: string, ...args: string[]) => execa('npm', [verb, ...args], {cwd: jsCoreDir, stdio: 'inherit'}); const execNpmCommand = execNpmVerb.bind(null, 'run'); -gulp.task('install', 'Install native core dependencies', () => - execNpmVerb('install', '--unsafe-perm')); +const install = () => execNpmVerb('install', '--unsafe-perm'); /** * Runs tslint on files in src/, with linting rules defined in tslint.json. */ -gulp.task('lint', 'Emits linting errors found in src/ and test/.', () => - execNpmCommand('check')); +const lint = () => execNpmCommand('check'); -gulp.task('clean', 'Deletes transpiled code.', ['install'], - () => execNpmCommand('clean')); +const cleanFiles = () => execNpmCommand('clean'); -gulp.task('clean.all', 'Deletes all files added by targets', ['clean']); +const clean = gulp.series(install, cleanFiles); + +const cleanAll = gulp.parallel(clean); /** * Transpiles TypeScript files in src/ to JavaScript according to the settings * found in tsconfig.json. */ -gulp.task('compile', 'Transpiles src/.', () => execNpmCommand('compile')); +const compile = () => execNpmCommand('compile'); -gulp.task('copy-test-fixtures', 'Copy test fixtures.', () => { - return ncpP(`${jsCoreDir}/test/fixtures`, `${outDir}/test/fixtures`); -}); +const copyTestFixtures = () => ncpP(`${jsCoreDir}/test/fixtures`, `${outDir}/test/fixtures`); -/** - * Transpiles src/ and test/, and then runs all tests. - */ -gulp.task('test', 'Runs all tests.', ['lint', 'copy-test-fixtures'], () => { - if (semver.satisfies(process.version, '^8.11.2 || >=9.4')) { +const runTests = () => { + if (semver.satisfies(process.version, '^8.13.0 || >=10.10.0')) { return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', require: ['ts-node/register']})); @@ -80,4 +70,15 @@ gulp.task('test', 'Runs all tests.', ['lint', 'copy-test-fixtures'], () => { console.log(`Skipping grpc-js tests for Node ${process.version}`); return Promise.resolve(null); } -}); +}; + +const test = gulp.series(install, copyTestFixtures, runTests); + +export { + install, + lint, + clean, + cleanAll, + compile, + test +} \ No newline at end of file diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json index 78e33f27..e0827fbe 100644 --- a/packages/grpc-js/package.json +++ b/packages/grpc-js/package.json @@ -1,12 +1,12 @@ { "name": "@grpc/grpc-js", - "version": "0.3.6", + "version": "0.4.0", "description": "gRPC Library for Node - pure JS implementation", "homepage": "https://grpc.io/", "repository": "https://github.com/grpc/grpc-node/tree/master/packages/grpc-js", "main": "build/src/index.js", "engines": { - "node": "^8.11.2 || >=9.4" + "node": "^8.13.0 || >=10.10.0" }, "keywords": [], "author": { diff --git a/packages/grpc-js/src/call-stream.ts b/packages/grpc-js/src/call-stream.ts index c987d932..7c06c335 100644 --- a/packages/grpc-js/src/call-stream.ts +++ b/packages/grpc-js/src/call-stream.ts @@ -26,6 +26,7 @@ import {Filter} from './filter'; import {FilterStackFactory} from './filter-stack'; import {Metadata} from './metadata'; import {ObjectDuplex, WriteCallback} from './object-stream'; +import {StreamDecoder} from './stream-decoder'; const {HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE, NGHTTP2_CANCEL} = http2.constants; @@ -77,12 +78,6 @@ export type Call = { EmitterAugmentation1<'status', StatusObject>& ObjectDuplex; -enum ReadState { - NO_DATA, - READING_SIZE, - READING_MESSAGE -} - export class Http2CallStream extends Duplex implements Call { credentials: CallCredentials = CallCredentials.createEmpty(); filterStack: Filter; @@ -92,13 +87,7 @@ export class Http2CallStream extends Duplex implements Call { private pendingWriteCallback: WriteCallback|null = null; private pendingFinalCallback: Function|null = null; - private readState: ReadState = ReadState.NO_DATA; - private readCompressFlag: Buffer = Buffer.alloc(1); - private readPartialSize: Buffer = Buffer.alloc(4); - private readSizeRemaining = 4; - private readMessageSize = 0; - private readPartialMessage: Buffer[] = []; - private readMessageRemaining = 0; + private decoder = new StreamDecoder(); private isReadFilterPending = false; private canPush = false; @@ -292,62 +281,10 @@ export class Http2CallStream extends Duplex implements Call { }); stream.on('trailers', this.handleTrailers.bind(this)); stream.on('data', (data: Buffer) => { - let readHead = 0; - let toRead: number; - while (readHead < data.length) { - switch (this.readState) { - case ReadState.NO_DATA: - this.readCompressFlag = data.slice(readHead, readHead + 1); - readHead += 1; - this.readState = ReadState.READING_SIZE; - this.readPartialSize.fill(0); - this.readSizeRemaining = 4; - this.readMessageSize = 0; - this.readMessageRemaining = 0; - this.readPartialMessage = []; - break; - case ReadState.READING_SIZE: - toRead = Math.min(data.length - readHead, this.readSizeRemaining); - data.copy( - this.readPartialSize, 4 - this.readSizeRemaining, readHead, - readHead + toRead); - this.readSizeRemaining -= toRead; - readHead += toRead; - // readSizeRemaining >=0 here - if (this.readSizeRemaining === 0) { - this.readMessageSize = this.readPartialSize.readUInt32BE(0); - this.readMessageRemaining = this.readMessageSize; - if (this.readMessageRemaining > 0) { - this.readState = ReadState.READING_MESSAGE; - } else { - this.tryPush(Buffer.concat( - [this.readCompressFlag, this.readPartialSize])); - this.readState = ReadState.NO_DATA; - } - } - break; - case ReadState.READING_MESSAGE: - toRead = - Math.min(data.length - readHead, this.readMessageRemaining); - this.readPartialMessage.push( - data.slice(readHead, readHead + toRead)); - this.readMessageRemaining -= toRead; - readHead += toRead; - // readMessageRemaining >=0 here - if (this.readMessageRemaining === 0) { - // At this point, we have read a full message - const framedMessageBuffers = [ - this.readCompressFlag, this.readPartialSize - ].concat(this.readPartialMessage); - const framedMessage = Buffer.concat( - framedMessageBuffers, this.readMessageSize + 5); - this.tryPush(framedMessage); - this.readState = ReadState.NO_DATA; - } - break; - default: - throw new Error('This should never happen'); - } + const message = this.decoder.write(data); + + if (message !== null) { + this.tryPush(message); } }); stream.on('end', () => { diff --git a/packages/grpc-js/src/client.ts b/packages/grpc-js/src/client.ts index 83f07c4e..a793cf6f 100644 --- a/packages/grpc-js/src/client.ts +++ b/packages/grpc-js/src/client.ts @@ -24,9 +24,7 @@ import {ChannelOptions} from './channel-options'; import {Status} from './constants'; import {Metadata} from './metadata'; -// This symbol must be exported (for now). -// See: https://github.com/Microsoft/TypeScript/issues/20080 -export const kChannel = Symbol(); +const CHANNEL_SYMBOL = Symbol(); export interface UnaryCallback { (err: ServiceError|null, value?: ResponseType): void; @@ -52,26 +50,26 @@ export type ClientOptions = Partial&{ * clients. */ export class Client { - private readonly[kChannel]: Channel; + private readonly[CHANNEL_SYMBOL]: Channel; constructor( address: string, credentials: ChannelCredentials, options: ClientOptions = {}) { if (options.channelOverride) { - this[kChannel] = options.channelOverride; + this[CHANNEL_SYMBOL] = options.channelOverride; } else if (options.channelFactoryOverride) { - this[kChannel] = + this[CHANNEL_SYMBOL] = options.channelFactoryOverride(address, credentials, options); } else { - this[kChannel] = new Http2Channel(address, credentials, options); + this[CHANNEL_SYMBOL] = new Http2Channel(address, credentials, options); } } close(): void { - this[kChannel].close(); + this[CHANNEL_SYMBOL].close(); } getChannel(): Channel { - return this[kChannel]; + return this[CHANNEL_SYMBOL]; } waitForReady(deadline: Deadline, callback: (error?: Error) => void): void { @@ -82,7 +80,7 @@ export class Client { } let newState; try { - newState = this[kChannel].getConnectivityState(true); + newState = this[CHANNEL_SYMBOL].getConnectivityState(true); } catch (e) { callback(new Error('The channel has been closed')); return; @@ -91,7 +89,8 @@ export class Client { callback(); } else { try { - this[kChannel].watchConnectivityState(newState, deadline, checkState); + this[CHANNEL_SYMBOL].watchConnectivityState( + newState, deadline, checkState); } catch (e) { callback(new Error('The channel has been closed')); } @@ -188,7 +187,7 @@ export class Client { ({metadata, options, callback} = this.checkOptionalUnaryResponseArguments( metadata, options, callback)); - const call: Call = this[kChannel].createCall( + const call: Call = this[CHANNEL_SYMBOL].createCall( method, options.deadline, options.host, null, options.propagate_flags); if (options.credentials) { call.setCredentials(options.credentials); @@ -229,7 +228,7 @@ export class Client { ({metadata, options, callback} = this.checkOptionalUnaryResponseArguments( metadata, options, callback)); - const call: Call = this[kChannel].createCall( + const call: Call = this[CHANNEL_SYMBOL].createCall( method, options.deadline, options.host, null, options.propagate_flags); if (options.credentials) { call.setCredentials(options.credentials); @@ -277,7 +276,7 @@ export class Client { metadata?: Metadata|CallOptions, options?: CallOptions): ClientReadableStream { ({metadata, options} = this.checkMetadataAndOptions(metadata, options)); - const call: Call = this[kChannel].createCall( + const call: Call = this[CHANNEL_SYMBOL].createCall( method, options.deadline, options.host, null, options.propagate_flags); if (options.credentials) { call.setCredentials(options.credentials); @@ -304,7 +303,7 @@ export class Client { metadata?: Metadata|CallOptions, options?: CallOptions): ClientDuplexStream { ({metadata, options} = this.checkMetadataAndOptions(metadata, options)); - const call: Call = this[kChannel].createCall( + const call: Call = this[CHANNEL_SYMBOL].createCall( method, options.deadline, options.host, null, options.propagate_flags); if (options.credentials) { call.setCredentials(options.credentials); diff --git a/packages/grpc-js/src/index.ts b/packages/grpc-js/src/index.ts index 49437e4d..740cd636 100644 --- a/packages/grpc-js/src/index.ts +++ b/packages/grpc-js/src/index.ts @@ -17,7 +17,7 @@ import * as semver from 'semver'; -import {ClientDuplexStream, ClientReadableStream, ClientUnaryCall, ClientWritableStream} from './call'; +import {ClientDuplexStream, ClientReadableStream, ClientUnaryCall, ClientWritableStream, ServiceError} from './call'; import {CallCredentials} from './call-credentials'; import {Deadline, StatusObject} from './call-stream'; import {Channel, ConnectivityState, Http2Channel} from './channel'; @@ -30,7 +30,7 @@ import {Metadata} from './metadata'; import {KeyCertPair, ServerCredentials} from './server-credentials'; import {StatusBuilder} from './status-builder'; -const supportedNodeVersions = '^8.11.2 || >=9.4'; +const supportedNodeVersions = '^8.13.0 || >=10.10.0'; if (!semver.satisfies(process.version, supportedNodeVersions)) { throw new Error(`@grpc/grpc-js only works on Node ${supportedNodeVersions}`); } @@ -180,7 +180,8 @@ export { ClientWritableStream, ClientDuplexStream, CallOptions, - StatusObject + StatusObject, + ServiceError }; /* tslint:disable:no-any */ @@ -248,3 +249,5 @@ export const InterceptorBuilder = () => { export const InterceptingCall = () => { throw new Error('Not yet implemented'); }; + +export {GrpcObject} from './make-client'; diff --git a/packages/grpc-js/src/server-call.ts b/packages/grpc-js/src/server-call.ts index 57c3d20d..9528c30c 100644 --- a/packages/grpc-js/src/server-call.ts +++ b/packages/grpc-js/src/server-call.ts @@ -16,62 +16,75 @@ */ import {EventEmitter} from 'events'; +import * as http2 from 'http2'; import {Duplex, Readable, Writable} from 'stream'; + import {ServiceError} from './call'; +import {StatusObject} from './call-stream'; +import {Status} from './constants'; import {Deserialize, Serialize} from './make-client'; import {Metadata} from './metadata'; +function noop(): void {} -export class ServerUnaryCall extends EventEmitter { - cancelled: boolean; - request: RequestType|null; +export type PartialServiceError = Partial; - constructor(private call: ServerCall, public metadata: Metadata) { - super(); - this.cancelled = false; - this.request = null; // TODO(cjihrig): Read the unary request here. - } +type DeadlineUnitIndexSignature = { + [name: string]: number +}; - getPeer(): string { - throw new Error('not implemented yet'); - } - - sendMetadata(responseMetadata: Metadata): void { - throw new Error('not implemented yet'); - } -} +const GRPC_ACCEPT_ENCODING_HEADER = 'grpc-accept-encoding'; +const GRPC_ENCODING_HEADER = 'grpc-encoding'; +const GRPC_MESSAGE_HEADER = 'grpc-message'; +const GRPC_STATUS_HEADER = 'grpc-status'; +const GRPC_TIMEOUT_HEADER = 'grpc-timeout'; +const DEADLINE_REGEX = /(\d{1,8})\s*([HMSmun])/; +const deadlineUnitsToMs: DeadlineUnitIndexSignature = { + H: 3600000, + M: 60000, + S: 1000, + m: 1, + u: 0.001, + n: 0.000001 +}; +const defaultResponseHeaders = { + // TODO(cjihrig): Remove these encoding headers from the default response + // once compression is integrated. + [GRPC_ACCEPT_ENCODING_HEADER]: 'identity', + [GRPC_ENCODING_HEADER]: 'identity', + [http2.constants.HTTP2_HEADER_STATUS]: http2.constants.HTTP_STATUS_OK, + [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/grpc+proto' +}; +const defaultResponseOptions = { + waitForTrailers: true +} as http2.ServerStreamResponseOptions; -export class ServerReadableStream extends Readable { - cancelled: boolean; +export type ServerSurfaceCall = { + cancelled: boolean; getPeer(): string; + sendMetadata(responseMetadata: Metadata): void +}; - constructor( - private call: ServerCall, public metadata: Metadata, - private deserialize: Deserialize) { - super(); - this.cancelled = false; - } +export type ServerUnaryCall = + ServerSurfaceCall&{request: RequestType | null}; +export type ServerReadableStream = + ServerSurfaceCall&Readable; +export type ServerWritableStream = + ServerSurfaceCall&Writable&{request: RequestType | null}; +export type ServerDuplexStream = + ServerSurfaceCall&Duplex; - getPeer(): string { - throw new Error('not implemented yet'); - } - - sendMetadata(responseMetadata: Metadata): void { - throw new Error('not implemented yet'); - } -} - - -export class ServerWritableStream extends Writable { +export class ServerUnaryCallImpl extends EventEmitter + implements ServerUnaryCall { cancelled: boolean; request: RequestType|null; constructor( - private call: ServerCall, public metadata: Metadata, - private serialize: Serialize) { + private call: Http2ServerCallStream, + public metadata: Metadata) { super(); this.cancelled = false; - this.request = null; // TODO(cjihrig): Read the unary request here. + this.request = null; } getPeer(): string { @@ -79,19 +92,20 @@ export class ServerWritableStream extends Writable { } sendMetadata(responseMetadata: Metadata): void { - throw new Error('not implemented yet'); + this.call.sendMetadata(responseMetadata); } } -export class ServerDuplexStream extends Duplex { +export class ServerReadableStreamImpl extends + Readable implements ServerReadableStream { cancelled: boolean; constructor( - private call: ServerCall, public metadata: Metadata, - private serialize: Serialize, - private deserialize: Deserialize) { - super(); + private call: Http2ServerCallStream, + public metadata: Metadata, + private _deserialize: Deserialize) { + super({objectMode: true}); this.cancelled = false; } @@ -100,13 +114,103 @@ export class ServerDuplexStream extends Duplex { } sendMetadata(responseMetadata: Metadata): void { - throw new Error('not implemented yet'); + this.call.sendMetadata(responseMetadata); } } -// Internal class that wraps the HTTP2 request. -export class ServerCall {} +export class ServerWritableStreamImpl extends + Writable implements ServerWritableStream { + cancelled: boolean; + request: RequestType|null; + private trailingMetadata: Metadata; + + constructor( + private call: Http2ServerCallStream, + public metadata: Metadata, private _serialize: Serialize) { + super({objectMode: true}); + this.cancelled = false; + this.request = null; + this.trailingMetadata = new Metadata(); + + this.on('error', (err) => { + this.call.sendError(err as ServiceError); + this.end(); + }); + } + + getPeer(): string { + throw new Error('not implemented yet'); + } + + sendMetadata(responseMetadata: Metadata): void { + this.call.sendMetadata(responseMetadata); + } + + async _write( + chunk: ResponseType, encoding: string, + // tslint:disable-next-line:no-any + callback: (...args: any[]) => void) { + try { + const response = await this.call.serializeMessage(chunk); + + if (!this.call.write(response)) { + this.call.once('drain', callback); + return; + } + } catch (err) { + err.code = Status.INTERNAL; + this.emit('error', err); + } + + callback(); + } + + _final(callback: Function): void { + this.call.sendStatus( + {code: Status.OK, details: 'OK', metadata: this.trailingMetadata}); + callback(null); + } + + // tslint:disable-next-line:no-any + end(metadata?: any) { + if (metadata) { + this.trailingMetadata = metadata; + } + + super.end(); + } + + serialize(input: ResponseType): Buffer|null { + if (input === null || input === undefined) { + return null; + } + + return this._serialize(input); + } +} + + +export class ServerDuplexStreamImpl extends Duplex + implements ServerDuplexStream { + cancelled: boolean; + + constructor( + private call: Http2ServerCallStream, + public metadata: Metadata, private _serialize: Serialize, + private _deserialize: Deserialize) { + super({objectMode: true}); + this.cancelled = false; + } + + getPeer(): string { + throw new Error('not implemented yet'); + } + + sendMetadata(responseMetadata: Metadata): void { + this.call.sendMetadata(responseMetadata); + } +} // Unary response callback signature. @@ -116,12 +220,12 @@ export type sendUnaryData = // User provided handler for unary calls. export type handleUnaryCall = - (call: ServerUnaryCall, + (call: ServerUnaryCall, callback: sendUnaryData) => void; // User provided handler for client streaming calls. export type handleClientStreamingCall = - (call: ServerReadableStream, + (call: ServerReadableStream, callback: sendUnaryData) => void; // User provided handler for server streaming calls. @@ -138,11 +242,242 @@ export type HandleCall = handleServerStreamingCall| handleBidiStreamingCall; -export type Handler = { - func: HandleCall; +export type UnaryHandler = { + func: handleUnaryCall; serialize: Serialize; deserialize: Deserialize; type: HandlerType; }; +export type ClientStreamingHandler = { + func: handleClientStreamingCall; + serialize: Serialize; + deserialize: Deserialize; + type: HandlerType; +}; + +export type ServerStreamingHandler = { + func: handleServerStreamingCall; + serialize: Serialize; + deserialize: Deserialize; + type: HandlerType; +}; + +export type BidiStreamingHandler = { + func: handleBidiStreamingCall; + serialize: Serialize; + deserialize: Deserialize; + type: HandlerType; +}; + +export type Handler = + UnaryHandler| + ClientStreamingHandler| + ServerStreamingHandler| + BidiStreamingHandler; + export type HandlerType = 'bidi'|'clientStream'|'serverStream'|'unary'; + +const noopTimer: NodeJS.Timer = setTimeout(() => {}, 0); + +// Internal class that wraps the HTTP2 request. +export class Http2ServerCallStream extends + EventEmitter { + cancelled = false; + deadline: NodeJS.Timer = noopTimer; + private wantTrailers = false; + private metadataSent = false; + + constructor( + private stream: http2.ServerHttp2Stream, + private handler: Handler) { + super(); + + this.stream.once('error', (err: ServiceError) => { + err.code = Status.INTERNAL; + this.sendError(err); + }); + + this.stream.once('close', () => { + if (this.stream.rstCode === http2.constants.NGHTTP2_CANCEL) { + this.cancelled = true; + this.emit('cancelled', 'cancelled'); + } + }); + + this.stream.on('drain', () => { + this.emit('drain'); + }); + } + + sendMetadata(customMetadata?: Metadata) { + if (this.metadataSent) { + return; + } + + this.metadataSent = true; + const custom = customMetadata ? customMetadata.toHttp2Headers() : null; + // TODO(cjihrig): Include compression headers. + const headers = Object.assign(defaultResponseHeaders, custom); + this.stream.respond(headers, defaultResponseOptions); + } + + receiveMetadata(headers: http2.IncomingHttpHeaders) { + const metadata = Metadata.fromHttp2Headers(headers); + + // TODO(cjihrig): Receive compression metadata. + + const timeoutHeader = metadata.get(GRPC_TIMEOUT_HEADER); + + if (timeoutHeader.length > 0) { + const match = timeoutHeader[0].toString().match(DEADLINE_REGEX); + + if (match === null) { + const err = new Error('Invalid deadline') as ServiceError; + err.code = Status.OUT_OF_RANGE; + this.sendError(err); + return; + } + + const timeout = (+match[1] * deadlineUnitsToMs[match[2]]) | 0; + + this.deadline = setTimeout(handleExpiredDeadline, timeout, this); + metadata.remove(GRPC_TIMEOUT_HEADER); + } + + return metadata; + } + + receiveUnaryMessage(): Promise { + return new Promise((resolve, reject) => { + const stream = this.stream; + const chunks: Buffer[] = []; + let totalLength = 0; + + stream.on('data', (data: Buffer) => { + chunks.push(data); + totalLength += data.byteLength; + }); + + stream.once('end', async () => { + try { + const requestBytes = Buffer.concat(chunks, totalLength); + + resolve(await this.deserializeMessage(requestBytes)); + } catch (err) { + err.code = Status.INTERNAL; + this.sendError(err); + resolve(); + } + }); + }); + } + + serializeMessage(value: ResponseType) { + const messageBuffer = this.handler.serialize(value); + + // TODO(cjihrig): Call compression aware serializeMessage(). + const byteLength = messageBuffer.byteLength; + const output = Buffer.allocUnsafe(byteLength + 5); + output.writeUInt8(0, 0); + output.writeUInt32BE(byteLength, 1); + messageBuffer.copy(output, 5); + return output; + } + + async deserializeMessage(bytes: Buffer) { + // TODO(cjihrig): Call compression aware deserializeMessage(). + const receivedMessage = bytes.slice(5); + + return this.handler.deserialize(receivedMessage); + } + + async sendUnaryMessage( + err: ServiceError|null, value: ResponseType|null, metadata?: Metadata, + flags?: number) { + if (!metadata) { + metadata = new Metadata(); + } + + if (err) { + err.metadata = metadata; + this.sendError(err); + return; + } + + try { + const response = await this.serializeMessage(value!); + + this.write(response); + this.sendStatus({code: Status.OK, details: 'OK', metadata}); + } catch (err) { + err.code = Status.INTERNAL; + this.sendError(err); + } + } + + sendStatus(statusObj: StatusObject) { + if (this.cancelled) { + return; + } + + clearTimeout(this.deadline); + + if (!this.wantTrailers) { + this.wantTrailers = true; + this.stream.once('wantTrailers', () => { + const trailersToSend = Object.assign( + { + [GRPC_STATUS_HEADER]: statusObj.code, + [GRPC_MESSAGE_HEADER]: encodeURI(statusObj.details as string) + }, + statusObj.metadata.toHttp2Headers()); + + this.stream.sendTrailers(trailersToSend); + }); + this.sendMetadata(); + this.stream.end(); + } + } + + sendError(error: ServiceError) { + const status: StatusObject = { + code: Status.UNKNOWN, + details: error.hasOwnProperty('message') ? error.message : + 'Unknown Error', + metadata: error.hasOwnProperty('metadata') ? error.metadata : + new Metadata() + }; + + if (error.hasOwnProperty('code') && Number.isInteger(error.code)) { + status.code = error.code; + + if (error.hasOwnProperty('details')) { + status.details = error.details; + } + } + + this.sendStatus(status); + } + + write(chunk: Buffer) { + if (this.cancelled) { + return; + } + + this.sendMetadata(); + return this.stream.write(chunk); + } +} + +// tslint:disable:no-any +type UntypedServerCall = Http2ServerCallStream; + +function handleExpiredDeadline(call: UntypedServerCall) { + const err = new Error('Deadline exceeded') as ServiceError; + err.code = Status.DEADLINE_EXCEEDED; + + call.sendError(err); + call.cancelled = true; + call.emit('cancelled', 'deadline'); +} diff --git a/packages/grpc-js/src/server.ts b/packages/grpc-js/src/server.ts index decdbe5a..421c089d 100644 --- a/packages/grpc-js/src/server.ts +++ b/packages/grpc-js/src/server.ts @@ -20,20 +20,25 @@ import {AddressInfo, ListenOptions} from 'net'; import {URL} from 'url'; import {ServiceError} from './call'; +import {StatusObject} from './call-stream'; import {Status} from './constants'; import {Deserialize, Serialize, ServiceDefinition} from './make-client'; -import {HandleCall, Handler, HandlerType, sendUnaryData, ServerDuplexStream, ServerReadableStream, ServerUnaryCall, ServerWritableStream} from './server-call'; +import {Metadata} from './metadata'; +import {BidiStreamingHandler, ClientStreamingHandler, HandleCall, Handler, HandlerType, Http2ServerCallStream, PartialServiceError, sendUnaryData, ServerDuplexStream, ServerDuplexStreamImpl, ServerReadableStream, ServerReadableStreamImpl, ServerStreamingHandler, ServerUnaryCall, ServerUnaryCallImpl, ServerWritableStream, ServerWritableStreamImpl, UnaryHandler} from './server-call'; import {ServerCredentials} from './server-credentials'; function noop(): void {} -type PartialServiceError = Partial; const unimplementedStatusResponse: PartialServiceError = { code: Status.UNIMPLEMENTED, details: 'The server does not implement this method', }; // tslint:disable:no-any +type UntypedUnaryHandler = UnaryHandler; +type UntypedClientStreamingHandler = ClientStreamingHandler; +type UntypedServerStreamingHandler = ServerStreamingHandler; +type UntypedBidiStreamingHandler = BidiStreamingHandler; type UntypedHandleCall = HandleCall; type UntypedHandler = Handler; type UntypedServiceImplementation = { @@ -41,10 +46,11 @@ type UntypedServiceImplementation = { }; const defaultHandler = { - unary(call: ServerUnaryCall, callback: sendUnaryData): void { + unary(call: ServerUnaryCall, callback: sendUnaryData): void { callback(unimplementedStatusResponse as ServiceError, null); }, - clientStream(call: ServerReadableStream, callback: sendUnaryData): + clientStream( + call: ServerReadableStream, callback: sendUnaryData): void { callback(unimplementedStatusResponse as ServiceError, null); }, @@ -120,8 +126,8 @@ export class Server { } const success = this.register( - attrs.path, impl, attrs.responseSerialize, attrs.requestDeserialize, - methodType); + attrs.path, impl as UntypedHandleCall, attrs.responseSerialize, + attrs.requestDeserialize, methodType); if (success === false) { throw new Error(`Method handler for ${attrs.path} already provided.`); @@ -162,7 +168,7 @@ export class Server { this.http2Server = http2.createServer(); } - // TODO(cjihrig): Set up the handlers, to allow requests to be processed. + this._setupHandlers(); function onError(err: Error): void { callback(err, -1); @@ -193,8 +199,7 @@ export class Server { } this.handlers.set( - name, - {func: handler, serialize, deserialize, type: type as HandlerType}); + name, {func: handler, serialize, deserialize, type} as UntypedHandler); return true; } @@ -227,4 +232,113 @@ export class Server { addHttp2Port(): void { throw new Error('Not yet implemented'); } + + private _setupHandlers(): void { + if (this.http2Server === null) { + return; + } + + this.http2Server.on( + 'stream', + (stream: http2.ServerHttp2Stream, + headers: http2.IncomingHttpHeaders) => { + if (!this.started) { + stream.end(); + return; + } + + try { + const path = headers[http2.constants.HTTP2_HEADER_PATH] as string; + const handler = this.handlers.get(path); + + if (handler === undefined) { + throw unimplementedStatusResponse; + } + + const call = new Http2ServerCallStream(stream, handler); + const metadata: Metadata = + call.receiveMetadata(headers) as Metadata; + + switch (handler.type) { + case 'unary': + handleUnary(call, handler as UntypedUnaryHandler, metadata); + break; + case 'clientStream': + handleClientStreaming( + call, handler as UntypedClientStreamingHandler, metadata); + break; + case 'serverStream': + handleServerStreaming( + call, handler as UntypedServerStreamingHandler, metadata); + break; + case 'bidi': + handleBidiStreaming( + call, handler as UntypedBidiStreamingHandler, metadata); + break; + default: + throw new Error(`Unknown handler type: ${handler.type}`); + } + } catch (err) { + const call = new Http2ServerCallStream(stream, null!); + err.code = Status.INTERNAL; + call.sendError(err); + } + }); + } +} + + +async function handleUnary( + call: Http2ServerCallStream, + handler: UnaryHandler, + metadata: Metadata): Promise { + const emitter = + new ServerUnaryCallImpl(call, metadata); + const request = await call.receiveUnaryMessage(); + + if (request === undefined || call.cancelled) { + return; + } + + emitter.request = request; + handler.func( + emitter, + (err: ServiceError|null, value: ResponseType|null, trailer?: Metadata, + flags?: number) => { + call.sendUnaryMessage(err, value, trailer, flags); + }); +} + + +function handleClientStreaming( + call: Http2ServerCallStream, + handler: ClientStreamingHandler, + metadata: Metadata): void { + throw new Error('not implemented yet'); +} + + +async function handleServerStreaming( + call: Http2ServerCallStream, + handler: ServerStreamingHandler, + metadata: Metadata): Promise { + const request = await call.receiveUnaryMessage(); + + if (request === undefined || call.cancelled) { + return; + } + + const stream = new ServerWritableStreamImpl( + call, metadata, handler.serialize); + + stream.request = request; + handler.func(stream); +} + + +function handleBidiStreaming( + call: Http2ServerCallStream, + handler: BidiStreamingHandler, + metadata: Metadata): void { + throw new Error('not implemented yet'); } diff --git a/packages/grpc-js/src/stream-decoder.ts b/packages/grpc-js/src/stream-decoder.ts new file mode 100644 index 00000000..3a8e91ae --- /dev/null +++ b/packages/grpc-js/src/stream-decoder.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +enum ReadState { + NO_DATA, + READING_SIZE, + READING_MESSAGE +} + + +export class StreamDecoder { + private readState: ReadState = ReadState.NO_DATA; + private readCompressFlag: Buffer = Buffer.alloc(1); + private readPartialSize: Buffer = Buffer.alloc(4); + private readSizeRemaining = 4; + private readMessageSize = 0; + private readPartialMessage: Buffer[] = []; + private readMessageRemaining = 0; + + + write(data: Buffer): Buffer|null { + let readHead = 0; + let toRead: number; + + while (readHead < data.length) { + switch (this.readState) { + case ReadState.NO_DATA: + this.readCompressFlag = data.slice(readHead, readHead + 1); + readHead += 1; + this.readState = ReadState.READING_SIZE; + this.readPartialSize.fill(0); + this.readSizeRemaining = 4; + this.readMessageSize = 0; + this.readMessageRemaining = 0; + this.readPartialMessage = []; + break; + case ReadState.READING_SIZE: + toRead = Math.min(data.length - readHead, this.readSizeRemaining); + data.copy( + this.readPartialSize, 4 - this.readSizeRemaining, readHead, + readHead + toRead); + this.readSizeRemaining -= toRead; + readHead += toRead; + // readSizeRemaining >=0 here + if (this.readSizeRemaining === 0) { + this.readMessageSize = this.readPartialSize.readUInt32BE(0); + this.readMessageRemaining = this.readMessageSize; + if (this.readMessageRemaining > 0) { + this.readState = ReadState.READING_MESSAGE; + } else { + const message = Buffer.concat( + [this.readCompressFlag, this.readPartialSize], 5); + + this.readState = ReadState.NO_DATA; + return message; + } + } + break; + case ReadState.READING_MESSAGE: + toRead = Math.min(data.length - readHead, this.readMessageRemaining); + this.readPartialMessage.push(data.slice(readHead, readHead + toRead)); + this.readMessageRemaining -= toRead; + readHead += toRead; + // readMessageRemaining >=0 here + if (this.readMessageRemaining === 0) { + // At this point, we have read a full message + const framedMessageBuffers = [ + this.readCompressFlag, this.readPartialSize + ].concat(this.readPartialMessage); + const framedMessage = + Buffer.concat(framedMessageBuffers, this.readMessageSize + 5); + + this.readState = ReadState.NO_DATA; + return framedMessage; + } + break; + default: + throw new Error('Unexpected read state'); + } + } + + return null; + } +} diff --git a/packages/grpc-js/test/fixtures/echo_service.proto b/packages/grpc-js/test/fixtures/echo_service.proto new file mode 100644 index 00000000..20b3bfe0 --- /dev/null +++ b/packages/grpc-js/test/fixtures/echo_service.proto @@ -0,0 +1,33 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +syntax = "proto3"; + +message EchoMessage { + string value = 1; + int32 value2 = 2; +} + +service EchoService { + rpc Echo (EchoMessage) returns (EchoMessage); + + rpc EchoClientStream (stream EchoMessage) returns (EchoMessage); + + rpc EchoServerStream (EchoMessage) returns (stream EchoMessage); + + rpc EchoBidiStream (stream EchoMessage) returns (stream EchoMessage); +} diff --git a/packages/grpc-js/test/fixtures/test_service.proto b/packages/grpc-js/test/fixtures/test_service.proto new file mode 100644 index 00000000..db876be9 --- /dev/null +++ b/packages/grpc-js/test/fixtures/test_service.proto @@ -0,0 +1,41 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +syntax = "proto3"; + +message Request { + bool error = 1; + string message = 2; +} + +message Response { + int32 count = 1; +} + +service TestService { + rpc Unary (Request) returns (Response) { + } + + rpc ClientStream (stream Request) returns (Response) { + } + + rpc ServerStream (Request) returns (stream Response) { + } + + rpc BidiStream (stream Request) returns (stream Response) { + } +} diff --git a/packages/grpc-js/test/test-server-errors.ts b/packages/grpc-js/test/test-server-errors.ts new file mode 100644 index 00000000..eabae3aa --- /dev/null +++ b/packages/grpc-js/test/test-server-errors.ts @@ -0,0 +1,522 @@ +/* + * Copyright 2019 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Allow `any` data type for testing runtime type checking. +// tslint:disable no-any +import * as assert from 'assert'; +import {join} from 'path'; + +import * as grpc from '../src'; +import {ServiceError} from '../src/call'; +import {ServiceClient, ServiceClientConstructor} from '../src/make-client'; +import {Server} from '../src/server'; +import {sendUnaryData, ServerDuplexStream, ServerReadableStream, ServerUnaryCall, ServerWritableStream} from '../src/server-call'; + +import {loadProtoFile} from './common'; + +const protoFile = join(__dirname, 'fixtures', 'test_service.proto'); +const testServiceDef = loadProtoFile(protoFile); +const testServiceClient = + testServiceDef.TestService as ServiceClientConstructor; +const clientInsecureCreds = grpc.credentials.createInsecure(); +const serverInsecureCreds = grpc.ServerCredentials.createInsecure(); + + +describe('Client malformed response handling', () => { + let server: Server; + let client: ServiceClient; + const badArg = Buffer.from([0xFF]); + + before((done) => { + const malformedTestService = { + unary: { + path: '/TestService/Unary', + requestStream: false, + responseStream: false, + requestDeserialize: identity, + responseSerialize: identity + }, + clientStream: { + path: '/TestService/ClientStream', + requestStream: true, + responseStream: false, + requestDeserialize: identity, + responseSerialize: identity + }, + serverStream: { + path: '/TestService/ServerStream', + requestStream: false, + responseStream: true, + requestDeserialize: identity, + responseSerialize: identity + }, + bidiStream: { + path: '/TestService/BidiStream', + requestStream: true, + responseStream: true, + requestDeserialize: identity, + responseSerialize: identity + } + } as any; + + server = new Server(); + + server.addService(malformedTestService, { + unary(call: ServerUnaryCall, cb: sendUnaryData) { + cb(null, badArg); + }, + + clientStream( + stream: ServerReadableStream, cb: sendUnaryData) { + stream.on('data', noop); + stream.on('end', () => { + cb(null, badArg); + }); + }, + + serverStream(stream: ServerWritableStream) { + stream.write(badArg); + stream.end(); + }, + + bidiStream(stream: ServerDuplexStream) { + stream.on('data', () => { + // Ignore requests + stream.write(badArg); + }); + + stream.on('end', () => { + stream.end(); + }); + } + }); + + server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => { + assert.ifError(err); + client = new testServiceClient(`localhost:${port}`, clientInsecureCreds); + server.start(); + done(); + }); + }); + + after((done) => { + client.close(); + server.tryShutdown(done); + }); + + it('should get an INTERNAL status with a unary call', (done) => { + client.unary({}, (err: ServiceError, data: any) => { + assert(err); + assert.strictEqual(err.code, grpc.status.INTERNAL); + done(); + }); + }); + + it('should get an INTERNAL status with a server stream call', (done) => { + const call = client.serverStream({}); + + call.on('data', noop); + call.on('error', (err: ServiceError) => { + assert(err); + assert.strictEqual(err.code, grpc.status.INTERNAL); + done(); + }); + }); +}); + +describe('Server serialization failure handling', () => { + let client: ServiceClient; + let server: Server; + + before((done) => { + function serializeFail(obj: any) { + throw new Error('Serialization failed'); + } + + const malformedTestService = { + unary: { + path: '/TestService/Unary', + requestStream: false, + responseStream: false, + requestDeserialize: identity, + responseSerialize: serializeFail + }, + clientStream: { + path: '/TestService/ClientStream', + requestStream: true, + responseStream: false, + requestDeserialize: identity, + responseSerialize: serializeFail + }, + serverStream: { + path: '/TestService/ServerStream', + requestStream: false, + responseStream: true, + requestDeserialize: identity, + responseSerialize: serializeFail + }, + bidiStream: { + path: '/TestService/BidiStream', + requestStream: true, + responseStream: true, + requestDeserialize: identity, + responseSerialize: serializeFail + } + }; + + server = new Server(); + server.addService(malformedTestService as any, { + unary(call: ServerUnaryCall, cb: sendUnaryData) { + cb(null, {}); + }, + + clientStream( + stream: ServerReadableStream, cb: sendUnaryData) { + stream.on('data', noop); + stream.on('end', () => { + cb(null, {}); + }); + }, + + serverStream(stream: ServerWritableStream) { + stream.write({}); + stream.end(); + }, + + bidiStream(stream: ServerDuplexStream) { + stream.on('data', () => { + // Ignore requests + stream.write({}); + }); + stream.on('end', () => { + stream.end(); + }); + } + }); + + server.bindAsync('localhost:0', serverInsecureCreds, (err, port) => { + assert.ifError(err); + client = new testServiceClient(`localhost:${port}`, clientInsecureCreds); + server.start(); + done(); + }); + }); + + after((done) => { + client.close(); + server.tryShutdown(done); + }); + + it('should get an INTERNAL status with a unary call', (done) => { + client.unary({}, (err: ServiceError, data: any) => { + assert(err); + assert.strictEqual(err.code, grpc.status.INTERNAL); + done(); + }); + }); + + it('should get an INTERNAL status with a server stream call', (done) => { + const call = client.serverStream({}); + + call.on('data', noop); + call.on('error', (err: ServiceError) => { + assert(err); + assert.strictEqual(err.code, grpc.status.INTERNAL); + done(); + }); + }); +}); + + +describe('Other conditions', () => { + let client: ServiceClient; + let server: Server; + let port: number; + + before((done) => { + const trailerMetadata = new grpc.Metadata(); + + server = new Server(); + trailerMetadata.add('trailer-present', 'yes'); + + server.addService(testServiceClient.service, { + unary(call: ServerUnaryCall, cb: sendUnaryData) { + const req = call.request; + + if (req.error) { + const details = req.message || 'Requested error'; + + cb({code: grpc.status.UNKNOWN, details} as ServiceError, null, + trailerMetadata); + } else { + cb(null, {count: 1}, trailerMetadata); + } + }, + + clientStream( + stream: ServerReadableStream, cb: sendUnaryData) { + let count = 0; + let errored = false; + + stream.on('data', (data: any) => { + if (data.error) { + const message = data.message || 'Requested error'; + errored = true; + cb(new Error(message) as ServiceError, null, trailerMetadata); + } else { + count++; + } + }); + + stream.on('end', () => { + if (!errored) { + cb(null, {count}, trailerMetadata); + } + }); + }, + + serverStream(stream: ServerWritableStream) { + const req = stream.request; + + if (req.error) { + stream.emit('error', { + code: grpc.status.UNKNOWN, + details: req.message || 'Requested error', + metadata: trailerMetadata + }); + } else { + for (let i = 0; i < 5; i++) { + stream.write({count: i}); + } + + stream.end(trailerMetadata); + } + }, + + bidiStream(stream: ServerDuplexStream) { + let count = 0; + stream.on('data', (data: any) => { + if (data.error) { + const message = data.message || 'Requested error'; + const err = new Error(message) as ServiceError; + + err.metadata = trailerMetadata.clone(); + err.metadata.add('count', '' + count); + stream.emit('error', err); + } else { + stream.write({count}); + count++; + } + }); + + stream.on('end', () => { + stream.end(trailerMetadata); + }); + } + }); + + server.bindAsync('localhost:0', serverInsecureCreds, (err, _port) => { + assert.ifError(err); + port = _port; + client = new testServiceClient(`localhost:${port}`, clientInsecureCreds); + server.start(); + done(); + }); + }); + + after((done) => { + client.close(); + server.tryShutdown(done); + }); + + describe('Server receiving bad input', () => { + let misbehavingClient: ServiceClient; + const badArg = Buffer.from([0xFF]); + + before(() => { + const testServiceAttrs = { + unary: { + path: '/TestService/Unary', + requestStream: false, + responseStream: false, + requestSerialize: identity, + responseDeserialize: identity + }, + clientStream: { + path: '/TestService/ClientStream', + requestStream: true, + responseStream: false, + requestSerialize: identity, + responseDeserialize: identity + }, + serverStream: { + path: '/TestService/ServerStream', + requestStream: false, + responseStream: true, + requestSerialize: identity, + responseDeserialize: identity + }, + bidiStream: { + path: '/TestService/BidiStream', + requestStream: true, + responseStream: true, + requestSerialize: identity, + responseDeserialize: identity + } + } as any; + + const client = + grpc.makeGenericClientConstructor(testServiceAttrs, 'TestService'); + + misbehavingClient = new client(`localhost:${port}`, clientInsecureCreds); + }); + + after(() => { + misbehavingClient.close(); + }); + + it('should respond correctly to a unary call', (done) => { + misbehavingClient.unary(badArg, (err: ServiceError, data: any) => { + assert(err); + assert.strictEqual(err.code, grpc.status.INTERNAL); + done(); + }); + }); + + it('should respond correctly to a server stream', (done) => { + const call = misbehavingClient.serverStream(badArg); + + call.on('data', (data: any) => { + assert.fail(data); + }); + + call.on('error', (err: ServiceError) => { + assert(err); + assert.strictEqual(err.code, grpc.status.INTERNAL); + done(); + }); + }); + }); + + describe('Trailing metadata', () => { + it('should be present when a unary call succeeds', (done) => { + let count = 0; + const call = + client.unary({error: false}, (err: ServiceError, data: any) => { + assert.ifError(err); + + count++; + if (count === 2) { + done(); + } + }); + + call.on('status', (status: grpc.StatusObject) => { + assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']); + + count++; + if (count === 2) { + done(); + } + }); + }); + + it('should be present when a unary call fails', (done) => { + let count = 0; + const call = + client.unary({error: true}, (err: ServiceError, data: any) => { + assert(err); + + count++; + if (count === 2) { + done(); + } + }); + + call.on('status', (status: grpc.StatusObject) => { + assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']); + + count++; + if (count === 2) { + done(); + } + }); + }); + + it('should be present when a server stream call succeeds', (done) => { + const call = client.serverStream({error: false}); + + call.on('data', noop); + call.on('status', (status: grpc.StatusObject) => { + assert.strictEqual(status.code, grpc.status.OK); + assert.deepStrictEqual(status.metadata.get('trailer-present'), ['yes']); + done(); + }); + }); + + it('should be present when a server stream call fails', (done) => { + const call = client.serverStream({error: true}); + + call.on('data', noop); + call.on('error', (error: ServiceError) => { + assert.deepStrictEqual(error.metadata.get('trailer-present'), ['yes']); + done(); + }); + }); + }); + + describe('Error object should contain the status', () => { + it('for a unary call', (done) => { + client.unary({error: true}, (err: ServiceError, data: any) => { + assert(err); + assert.strictEqual(err.code, grpc.status.UNKNOWN); + assert.strictEqual(err.details, 'Requested error'); + done(); + }); + }); + + it('for a server stream call', (done) => { + const call = client.serverStream({error: true}); + + call.on('data', noop); + call.on('error', (error: ServiceError) => { + assert.strictEqual(error.code, grpc.status.UNKNOWN); + assert.strictEqual(error.details, 'Requested error'); + done(); + }); + }); + + it('for a UTF-8 error message', (done) => { + client.unary( + {error: true, message: '測試字符串'}, + (err: ServiceError, data: any) => { + assert(err); + assert.strictEqual(err.code, grpc.status.UNKNOWN); + assert.strictEqual(err.details, '測試字符串'); + done(); + }); + }); + }); +}); + + +function identity(arg: any): any { + return arg; +} + + +function noop(): void {} diff --git a/packages/grpc-js/test/test-server.ts b/packages/grpc-js/test/test-server.ts index 0b2f0524..f44f5d5d 100644 --- a/packages/grpc-js/test/test-server.ts +++ b/packages/grpc-js/test/test-server.ts @@ -21,8 +21,12 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; +import * as grpc from '../src'; import {ServerCredentials} from '../src'; +import {ServiceError} from '../src/call'; +import {ServiceClient, ServiceClientConstructor} from '../src/make-client'; import {Server} from '../src/server'; +import {sendUnaryData, ServerDuplexStream, ServerReadableStream, ServerUnaryCall, ServerWritableStream} from '../src/server-call'; import {loadProtoFile} from './common'; @@ -228,4 +232,158 @@ describe('Server', () => { server.bind('localhost:0', ServerCredentials.createInsecure()); }, /Not implemented. Use bindAsync\(\) instead/); }); + + describe('Default handlers', () => { + let server: Server; + let client: ServiceClient; + + const mathProtoFile = path.join(__dirname, 'fixtures', 'math.proto'); + const mathClient = (loadProtoFile(mathProtoFile).math as any).Math; + const mathServiceAttrs = mathClient.service; + + beforeEach((done) => { + server = new Server(); + server.addService(mathServiceAttrs, {}); + server.bindAsync( + 'localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + client = new mathClient( + `localhost:${port}`, grpc.credentials.createInsecure()); + server.start(); + done(); + }); + }); + + it('should respond to a unary call with UNIMPLEMENTED', (done) => { + client.div( + {divisor: 4, dividend: 3}, (error: ServiceError, response: any) => { + assert(error); + assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED); + done(); + }); + }); + + it('should respond to a server stream with UNIMPLEMENTED', (done) => { + const call = client.fib({limit: 5}); + + call.on('data', (value: any) => { + assert.fail('No messages expected'); + }); + + call.on('error', (err: ServiceError) => { + assert(err); + assert.strictEqual(err.code, grpc.status.UNIMPLEMENTED); + done(); + }); + }); + }); +}); + +describe('Echo service', () => { + let server: Server; + let client: ServiceClient; + + before((done) => { + const protoFile = path.join(__dirname, 'fixtures', 'echo_service.proto'); + const echoService = + loadProtoFile(protoFile).EchoService as ServiceClientConstructor; + + server = new Server(); + server.addService(echoService.service, { + echo(call: ServerUnaryCall, callback: sendUnaryData) { + callback(null, call.request); + } + }); + + server.bindAsync( + 'localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + client = new echoService( + `localhost:${port}`, grpc.credentials.createInsecure()); + server.start(); + done(); + }); + }); + + after((done) => { + client.close(); + server.tryShutdown(done); + }); + + it('should echo the recieved message directly', (done) => { + client.echo( + {value: 'test value', value2: 3}, + (error: ServiceError, response: any) => { + assert.ifError(error); + assert.deepStrictEqual(response, {value: 'test value', value2: 3}); + done(); + }); + }); +}); + +describe('Generic client and server', () => { + function toString(val: any) { + return val.toString(); + } + + function toBuffer(str: string) { + return Buffer.from(str); + } + + function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + const stringServiceAttrs = { + capitalize: { + path: '/string/capitalize', + requestStream: false, + responseStream: false, + requestSerialize: toBuffer, + requestDeserialize: toString, + responseSerialize: toBuffer, + responseDeserialize: toString + } + }; + + describe('String client and server', () => { + let client: ServiceClient; + let server: Server; + + before((done) => { + server = new Server(); + + server.addService(stringServiceAttrs as any, { + capitalize( + call: ServerUnaryCall, callback: sendUnaryData) { + callback(null, capitalize(call.request)); + } + }); + + server.bindAsync( + 'localhost:0', ServerCredentials.createInsecure(), (err, port) => { + assert.ifError(err); + server.start(); + const clientConstr = grpc.makeGenericClientConstructor( + stringServiceAttrs as any, + 'unused_but_lets_appease_typescript_anyway'); + client = new clientConstr( + `localhost:${port}`, grpc.credentials.createInsecure()); + done(); + }); + }); + + after((done) => { + client.close(); + server.tryShutdown(done); + }); + + it('Should respond with a capitalized string', (done) => { + client.capitalize('abc', (err: ServiceError, response: string) => { + assert.ifError(err); + assert.strictEqual(response, 'Abc'); + done(); + }); + }); + }); }); diff --git a/packages/grpc-native-core/binding.gyp b/packages/grpc-native-core/binding.gyp index aabdf479..542b1424 100644 --- a/packages/grpc-native-core/binding.gyp +++ b/packages/grpc-native-core/binding.gyp @@ -94,7 +94,10 @@ 'GPR_BACKWARDS_COMPATIBILITY_MODE', 'GRPC_ARES=1', 'GRPC_UV', - 'GRPC_NODE_VERSION="1.21.0-dev"' + 'GRPC_NODE_VERSION="1.20.3"' + ], + 'defines!': [ + 'OPENSSL_THREADS' ], 'conditions': [ ['grpc_gcov=="true"', { diff --git a/packages/grpc-native-core/build.yaml b/packages/grpc-native-core/build.yaml index 49e36fdf..74cb99d9 100644 --- a/packages/grpc-native-core/build.yaml +++ b/packages/grpc-native-core/build.yaml @@ -1,2 +1,3 @@ settings: '#': It's possible to have node_version here as a key to override the core's version. + node_version: 1.20.3 diff --git a/packages/grpc-native-core/ext/channel.cc b/packages/grpc-native-core/ext/channel.cc index f23dbd58..14293abd 100644 --- a/packages/grpc-native-core/ext/channel.cc +++ b/packages/grpc-native-core/ext/channel.cc @@ -289,7 +289,7 @@ NAN_METHOD(Channel::GetConnectivityState) { return Nan::ThrowError( "Cannot call getConnectivityState on a closed Channel"); } - int try_to_connect = (int)info[0]->Equals(Nan::True()); + int try_to_connect = (int)info[0]->StrictEquals(Nan::True()); info.GetReturnValue().Set(grpc_channel_check_connectivity_state( channel->wrapped_channel, try_to_connect)); } diff --git a/packages/grpc-native-core/ext/channel_credentials.cc b/packages/grpc-native-core/ext/channel_credentials.cc index a586b3e6..60184b2b 100644 --- a/packages/grpc-native-core/ext/channel_credentials.cc +++ b/packages/grpc-native-core/ext/channel_credentials.cc @@ -194,7 +194,7 @@ NAN_METHOD(ChannelCredentials::CreateSsl) { if (!info[3]->IsObject()) { return Nan::ThrowTypeError("createSsl's fourth argument must be an object"); } - Local object = info[3]->ToObject(); + Local object = Nan::To(info[3]).ToLocalChecked(); Local checkServerIdentityValue = Nan::Get(object, Nan::New("checkServerIdentity").ToLocalChecked()).ToLocalChecked(); diff --git a/packages/grpc-native-core/ext/server_credentials.cc b/packages/grpc-native-core/ext/server_credentials.cc index 1ce86632..fe26e3a9 100644 --- a/packages/grpc-native-core/ext/server_credentials.cc +++ b/packages/grpc-native-core/ext/server_credentials.cc @@ -66,7 +66,7 @@ void ServerCredentials::Init(Local exports) { Local tpl = Nan::New(New); tpl->SetClassName(Nan::New("ServerCredentials").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); - Local ctr = tpl->GetFunction(); + Local ctr = Nan::GetFunction(tpl).ToLocalChecked(); Nan::Set( ctr, Nan::New("createSsl").ToLocalChecked(), Nan::GetFunction(Nan::New(CreateSsl)).ToLocalChecked()); diff --git a/packages/grpc-native-core/gulpfile.js b/packages/grpc-native-core/gulpfile.ts similarity index 58% rename from packages/grpc-native-core/gulpfile.js rename to packages/grpc-native-core/gulpfile.ts index 8e32d743..cfb601d0 100644 --- a/packages/grpc-native-core/gulpfile.js +++ b/packages/grpc-native-core/gulpfile.ts @@ -1,5 +1,5 @@ /* - * Copyright 2017 gRPC authors. + * Copyright 2019 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,13 @@ * */ -const _gulp = require('gulp'); -const help = require('gulp-help'); - -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); - -const jsdoc = require('gulp-jsdoc3'); -const jshint = require('gulp-jshint'); -const mocha = require('gulp-mocha'); -const execa = require('execa'); -const path = require('path'); -const del = require('del'); + import * as gulp from 'gulp'; + import * as jsdoc from 'gulp-jsdoc3'; + import * as jshint from 'gulp-jshint'; + import * as mocha from 'gulp-mocha'; + import * as execa from 'execa'; + import * as path from 'path'; + import * as del from 'del'; const nativeCoreDir = __dirname; const srcDir = path.resolve(nativeCoreDir, 'src'); @@ -35,44 +30,54 @@ const testDir = path.resolve(nativeCoreDir, 'test'); const pkg = require('./package'); const jshintConfig = pkg.jshintConfig; -gulp.task('clean', 'Delete generated files', () => { - return del([path.resolve(nativeCoreDir, 'build'), - path.resolve(nativeCoreDir, 'ext/node')]); -}); +const clean = () => del([path.resolve(nativeCoreDir, 'build'), + path.resolve(nativeCoreDir, 'ext/node')]); -gulp.task('clean.all', 'Delete all files created by tasks', - ['clean']); +const cleanAll = gulp.parallel(clean); -gulp.task('install', 'Install native core dependencies', () => { +const install = () => { return execa('npm', ['install', '--build-from-source', '--unsafe-perm'], {cwd: nativeCoreDir, stdio: 'inherit'}); -}); +}; -gulp.task('install.windows', 'Install native core dependencies for MS Windows', () => { +const installWindows = () => { return execa('npm', ['install', '--build-from-source'], {cwd: nativeCoreDir, stdio: 'inherit'}).catch(() => del(path.resolve(process.env.USERPROFILE, '.node-gyp', process.versions.node, 'include/node/openssl'), { force: true }).then(() => execa('npm', ['install', '--build-from-source'], {cwd: nativeCoreDir, stdio: 'inherit'}) - )) -}); + )); +}; -gulp.task('lint', 'Emits linting errors', () => { +const lint = () => { return gulp.src([`${nativeCoreDir}/index.js`, `${srcDir}/*.js`, `${testDir}/*.js`]) .pipe(jshint(pkg.jshintConfig)) .pipe(jshint.reporter('default')); -}); +}; -gulp.task('build', 'Build native package', () => { +const build = () => { return execa('npm', ['run', 'build'], {cwd: nativeCoreDir, stdio: 'inherit'}); -}); +}; -gulp.task('test', 'Run all tests', ['build'], () => { +const runTests = () => { return gulp.src(`${testDir}/*.js`).pipe(mocha({timeout: 5000, reporter: 'mocha-jenkins-reporter'})); -}); +} -gulp.task('doc.gen', 'Generate docs', (cb) => { +const test = gulp.series(build, runTests); + +const docGen = (cb) => { var config = require('./jsdoc_conf.json'); - gulp.src([`${nativeCoreDir}/README.md`, `${nativeCoreDir}/index.js`, `${srcDir}/*.js`], {read: false}) - .pipe(jsdoc(config, cb)); -}); + return gulp.src([`${nativeCoreDir}/README.md`, `${nativeCoreDir}/index.js`, `${srcDir}/*.js`], {read: false}) + .pipe(jsdoc(config, cb)); +}; + +export { + clean, + cleanAll, + install, + installWindows, + lint, + build, + test, + docGen +}; \ No newline at end of file diff --git a/packages/grpc-native-core/index.d.ts b/packages/grpc-native-core/index.d.ts index 1a634164..1f8437f2 100644 --- a/packages/grpc-native-core/index.d.ts +++ b/packages/grpc-native-core/index.d.ts @@ -963,18 +963,6 @@ declare module "grpc" { * instance. */ compose(callCredentials: CallCredentials): ChannelCredentials; - - /** - * Gets the set of per-call credentials associated with this instance. - */ - getCallCredentials(): CallCredentials; - - /** - * Gets a SecureContext object generated from input parameters if this - * instance was created with createSsl, or null if this instance was created - * with createInsecure. - */ - getSecureContext(): SecureContext | null; } /** @@ -1545,7 +1533,7 @@ declare module "grpc" { TRANSIENT_FAILURE = 3, SHUTDOWN = 4 } - + export class Channel { /** * This constructor API is almost identical to the Client constructor, @@ -1583,8 +1571,8 @@ declare module "grpc" { watchConnectivityState(currentState: connectivityState, deadline: Date|number, callback: (error?: Error) => void): void; /** * Create a call object. Call is an opaque type that is used by the Client - * and Server classes. This function is called by the gRPC library when - * starting a request. Implementers should return an instance of Call that + * and Server classes. This function is called by the gRPC library when + * starting a request. Implementers should return an instance of Call that * is returned from calling createCall on an instance of the provided * Channel class. * @param method The full method string to request. @@ -1595,5 +1583,5 @@ declare module "grpc" { * that indicates what information to propagate from parentCall. */ createCall(method: string, deadline: Date|number, host: string|null, parentCall: Call|null, propagateFlags: number|null): Call; - } -} \ No newline at end of file + } +} diff --git a/packages/grpc-native-core/package.json b/packages/grpc-native-core/package.json index f849dde2..9a1a22c7 100644 --- a/packages/grpc-native-core/package.json +++ b/packages/grpc-native-core/package.json @@ -1,6 +1,6 @@ { "name": "grpc", - "version": "1.21.0-dev", + "version": "1.20.3", "author": "Google Inc.", "description": "gRPC Library for Node", "homepage": "https://grpc.io/", @@ -29,10 +29,11 @@ "node-pre-gyp" ], "dependencies": { + "@types/protobufjs": "^5.0.31", "lodash.camelcase": "^4.3.0", "lodash.clone": "^4.5.0", - "nan": "^2.0.0", - "node-pre-gyp": "^0.12.0", + "nan": "^2.13.2", + "node-pre-gyp": "^0.13.0", "protobufjs": "^5.0.3" }, "devDependencies": { diff --git a/packages/grpc-native-core/src/grpc_extension.js b/packages/grpc-native-core/src/grpc_extension.js index 9a023b7c..41ce389b 100644 --- a/packages/grpc-native-core/src/grpc_extension.js +++ b/packages/grpc-native-core/src/grpc_extension.js @@ -54,6 +54,7 @@ Original error: ${e.message}`; error.code = e.code; throw error; } else { + e.message = `Failed to load ${binding_path}. ${e.message}`; throw e; } } diff --git a/packages/grpc-native-core/templates/binding.gyp.template b/packages/grpc-native-core/templates/binding.gyp.template index 31442b11..3ed7d769 100644 --- a/packages/grpc-native-core/templates/binding.gyp.template +++ b/packages/grpc-native-core/templates/binding.gyp.template @@ -88,6 +88,9 @@ 'GRPC_UV', 'GRPC_NODE_VERSION="${settings.get('node_version', settings.version)}"' ], + 'defines!': [ + 'OPENSSL_THREADS' + ], 'conditions': [ ['grpc_gcov=="true"', { % for arg, prop in [('CPPFLAGS', 'cflags'), ('DEFINES', 'defines'), ('LDFLAGS', 'ldflags')]: diff --git a/packages/grpc-native-core/templates/package.json.template b/packages/grpc-native-core/templates/package.json.template index 00471167..9949797c 100644 --- a/packages/grpc-native-core/templates/package.json.template +++ b/packages/grpc-native-core/templates/package.json.template @@ -31,10 +31,11 @@ "node-pre-gyp" ], "dependencies": { + "@types/protobufjs": "^5.0.31", "lodash.camelcase": "^4.3.0", "lodash.clone": "^4.5.0", - "nan": "^2.0.0", - "node-pre-gyp": "^0.12.0", + "nan": "^2.13.2", + "node-pre-gyp": "^0.13.0", "protobufjs": "^5.0.3" }, "devDependencies": { diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.bat b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.bat index 1575b7ce..95ca024d 100644 --- a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.bat +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.bat @@ -14,7 +14,7 @@ set arch_list=ia32 x64 -set electron_versions=1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0.0 3.0.0 3.1.0 4.1.0 +set electron_versions=1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0.0 3.0.0 3.1.0 4.1.0 5.0.0 set PATH=%PATH%;C:\Program Files\nodejs\;%APPDATA%\npm diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.sh b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.sh index 7d128127..6eb65014 100755 --- a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.sh +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_electron.sh @@ -16,7 +16,7 @@ set -ex arch_list=( ia32 x64 ) -electron_versions=( 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0.0 3.0.0 3.1.0 4.1.0 ) +electron_versions=( 1.0.0 1.1.0 1.2.0 1.3.0 1.4.0 1.5.0 1.6.0 1.7.0 1.8.0 2.0.0 3.0.0 3.1.0 4.1.0 5.0.0 ) umask 022 diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.bat b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.bat index 1c9064a9..7f862edc 100644 --- a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.bat +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.bat @@ -14,7 +14,7 @@ set arch_list=ia32 x64 -set node_versions=4.0.0 5.0.0 6.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0 +set node_versions=4.0.0 5.0.0 6.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0 12.0.0 set PATH=%PATH%;C:\Program Files\nodejs\;%APPDATA%\npm diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.sh b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.sh index 7ee81de8..42149d44 100755 --- a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.sh +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.sh @@ -16,7 +16,7 @@ set -ex arch_list=( ia32 x64 ) -node_versions=( 4.0.0 5.0.0 6.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0 ) +node_versions=( 4.0.0 5.0.0 6.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0 12.0.0 ) while true ; do case $1 in diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node_arm.sh b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node_arm.sh index 139d3432..ace0aff9 100755 --- a/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node_arm.sh +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node_arm.sh @@ -26,7 +26,7 @@ mkdir -p "${ARTIFACTS_OUT}" npm update -node_versions=( 4.0.0 5.0.0 6.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0 ) +node_versions=( 4.0.0 5.0.0 6.0.0 7.0.0 8.0.0 9.0.0 10.0.0 11.0.0 12.0.0 ) for version in ${node_versions[@]} do diff --git a/packages/proto-loader/gulpfile.ts b/packages/proto-loader/gulpfile.ts index 42ea1313..ff8f0855 100644 --- a/packages/proto-loader/gulpfile.ts +++ b/packages/proto-loader/gulpfile.ts @@ -15,8 +15,7 @@ * */ -import * as _gulp from 'gulp'; -import * as help from 'gulp-help'; +import * as gulp from 'gulp'; import * as fs from 'fs'; import * as mocha from 'gulp-mocha'; @@ -24,9 +23,6 @@ import * as path from 'path'; import * as execa from 'execa'; import * as semver from 'semver'; -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); - Error.stackTraceLimit = Infinity; const protojsDir = __dirname; @@ -40,30 +36,29 @@ const execNpmVerb = (verb: string, ...args: string[]) => execa('npm', [verb, ...args], {cwd: protojsDir, stdio: 'inherit'}); const execNpmCommand = execNpmVerb.bind(null, 'run'); -gulp.task('install', 'Install native core dependencies', () => - execNpmVerb('install', '--unsafe-perm')); +const install = () => execNpmVerb('install', '--unsafe-perm'); /** * Runs tslint on files in src/, with linting rules defined in tslint.json. */ -gulp.task('lint', 'Emits linting errors found in src/ and test/.', () => - execNpmCommand('check')); +const lint = () => execNpmCommand('check'); -gulp.task('clean', 'Deletes transpiled code.', ['install'], - () => execNpmCommand('clean')); +const cleanFiles = () => execNpmCommand('clean'); -gulp.task('clean.all', 'Deletes all files added by targets', ['clean']); +const clean = gulp.series(install, cleanFiles); + +const cleanAll = gulp.parallel(clean); /** * Transpiles TypeScript files in src/ and test/ to JavaScript according to the settings * found in tsconfig.json. */ -gulp.task('compile', 'Transpiles src/ and test/.', () => execNpmCommand('compile')); +const compile = () => execNpmCommand('compile'); /** * Transpiles src/ and test/, and then runs all tests. */ -gulp.task('test', 'Runs all tests.', () => { +const runTests = () => { if (semver.satisfies(process.version, ">=6")) { return gulp.src(`${outDir}/test/**/*.js`) .pipe(mocha({reporter: 'mocha-jenkins-reporter', @@ -72,4 +67,15 @@ gulp.task('test', 'Runs all tests.', () => { console.log(`Skipping proto-loader tests for Node ${process.version}`); return Promise.resolve(null); } -}); +} + +const test = gulp.series(install, runTests); + +export { + install, + lint, + clean, + cleanAll, + compile, + test +} diff --git a/run-tests.bat b/run-tests.bat index f48ab4cb..fa2b7183 100644 --- a/run-tests.bat +++ b/run-tests.bat @@ -38,7 +38,7 @@ call npm install || goto :error SET JUNIT_REPORT_STACK=1 SET FAILED=0 -for %%v in (6 7 8 9 10 11) do ( +for %%v in (6 7 8 9 10 11 12) do ( call nvm install %%v call nvm use %%v if "%%v"=="4" ( @@ -53,8 +53,8 @@ for %%v in (6 7 8 9 10 11) do ( node -e "process.exit(process.version.startsWith('v%%v') ? 0 : -1)" || goto :error - call .\node_modules\.bin\gulp clean.all || SET FAILED=1 - call .\node_modules\.bin\gulp setup.windows || SET FAILED=1 + call .\node_modules\.bin\gulp cleanAll || SET FAILED=1 + call .\node_modules\.bin\gulp setupWindows || SET FAILED=1 call .\node_modules\.bin\gulp test || SET FAILED=1 ) diff --git a/run-tests.sh b/run-tests.sh index ff1caa2e..01e6741a 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -26,7 +26,7 @@ set -ex cd $ROOT if [ ! -n "$node_versions" ] ; then - node_versions="6 7 8 9 10 11" + node_versions="6 7 8 9 10 11 12" fi set +ex @@ -68,7 +68,7 @@ do node -e 'process.exit(process.version.startsWith("v'$version'") ? 0 : -1)' # Install dependencies and link packages together. - ./node_modules/.bin/gulp clean.all + ./node_modules/.bin/gulp cleanAll ./node_modules/.bin/gulp setup # npm test calls nyc gulp test diff --git a/test/gulpfile.js b/test/gulpfile.ts similarity index 69% rename from test/gulpfile.js rename to test/gulpfile.ts index 42ff04f0..f5762d3c 100644 --- a/test/gulpfile.js +++ b/test/gulpfile.ts @@ -1,5 +1,5 @@ /* - * Copyright 2017 gRPC authors. + * Copyright 2019 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,33 +15,28 @@ * */ -const _gulp = require('gulp'); -const help = require('gulp-help'); -const mocha = require('gulp-mocha'); -const execa = require('execa'); -const path = require('path'); -const del = require('del'); -const semver = require('semver'); -const linkSync = require('../util').linkSync; - -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); +import * as gulp from 'gulp'; +import * as mocha from 'gulp-mocha'; +import * as execa from 'execa'; +import * as path from 'path'; +import * as del from 'del'; +import * as semver from 'semver'; const testDir = __dirname; const apiTestDir = path.resolve(testDir, 'api'); -gulp.task('install', 'Install test dependencies', () => { +const install = () => { return execa('npm', ['install'], {cwd: testDir, stdio: 'inherit'}); -}); +}; -gulp.task('clean.all', 'Delete all files created by tasks', () => {}); +const cleanAll = () => Promise.resolve(); -gulp.task('test', 'Run API-level tests', () => { +const test = () => { // run mocha tests matching a glob with a pre-required fixture, // returning the associated gulp stream - if (!semver.satisfies(process.version, '>=9.4')) { + if (!semver.satisfies(process.version, '>=10.10.0')) { console.log(`Skipping cross-implementation tests for Node ${process.version}`); - return; + return Promise.resolve(); } const apiTestGlob = `${apiTestDir}/*.js`; const runTestsWithFixture = (server, client) => new Promise((resolve, reject) => { @@ -50,14 +45,14 @@ gulp.task('test', 'Run API-level tests', () => { gulp.src(apiTestGlob) .pipe(mocha({ reporter: 'mocha-jenkins-reporter', - require: `${testDir}/fixtures/${fixture}.js` + require: [`${testDir}/fixtures/${fixture}.js`] })) .resume() // put the stream in flowing mode .on('end', resolve) .on('error', reject); }); var runTestsArgPairs; - if (semver.satisfies(process.version, '^ 8.11.2 || >=9.4')) { + if (semver.satisfies(process.version, '^8.13.0 || >=10.10.0')) { runTestsArgPairs = [ ['native', 'native'], ['native', 'js'], @@ -72,4 +67,10 @@ gulp.task('test', 'Run API-level tests', () => { return runTestsArgPairs.reduce((previousPromise, argPair) => { return previousPromise.then(runTestsWithFixture.bind(null, argPair[0], argPair[1])); }, Promise.resolve()); -}); +}; + +export { + install, + cleanAll, + test +}; \ No newline at end of file