diff --git a/.gitignore b/.gitignore index 58368339..4dc6fc88 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ npm-debug.log yarn-error.log yarn.lock +# Emacs temp files *~ +\#*\# +.\#* packages/grpc-native-core/src/node/ \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index db75e032..00000000 --- a/gulpfile.js +++ /dev/null @@ -1,63 +0,0 @@ -const _gulp = require('gulp'); -const help = require('gulp-help'); - -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); - -require('./packages/grpc-health-check/gulpfile'); -require('./packages/grpc-js-core/gulpfile'); -require('./packages/grpc-native-core/gulpfile'); -require('./test/gulpfile'); - -const root = __dirname; - -gulp.task('install.all', 'Install dependencies for all subdirectory packages', - ['js.core.install', 'native.core.install', 'health-check.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']); - -gulp.task('lint', 'Emit linting errors in source and test files', - ['js.core.lint', 'native.core.lint']); - -gulp.task('build', 'Build packages', ['js.core.compile', 'native.core.build']); - -gulp.task('link.create', 'Initialize npm links to packages', - ['native.core.link.create']); - -gulp.task('link.only', 'Link packages together without rebuilding anything', - ['health-check.link.add', 'internal.test.link.add']); - -gulp.task('link', 'Link local packages together after building', - ['link.create'], () => { - gulp.start('link.only'); - }); - -gulp.task('setup', 'One-time setup for a clean repository', ['install.all'], () => { - gulp.start('link'); -}); -gulp.task('setup.windows', 'One-time setup for a clean repository for MS Windows', ['install.all.windows'], () => { - gulp.start('link'); -}); - -gulp.task('clean', 'Delete generated files', ['js.core.clean', 'native.core.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']); - -gulp.task('native.test.only', 'Run tests of native code without rebuilding anything', - ['native.core.test', 'internal.test.test', 'health-check.test']); - -gulp.task('native.test', 'Run tests of native code', ['build'], () => { - gulp.start('native.test.only'); -}); - -gulp.task('test.only', 'Run tests without rebuilding anything', - ['js.core.test', 'native.test.only']); - -gulp.task('test', 'Run all tests', ['build'], () => { - gulp.start('test.only'); -}); - -gulp.task('default', ['help']); diff --git a/gulpfile.ts b/gulpfile.ts new file mode 100644 index 00000000..a49fac2c --- /dev/null +++ b/gulpfile.ts @@ -0,0 +1,117 @@ +/* + * 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. + * + */ + +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'], + ['./packages/grpc-js-core/gulpfile', 'js.core'], + ['./packages/grpc-native/gulpfile', 'native'], + ['./packages/grpc-native-core/gulpfile', 'native.core'], + ['./packages/grpc-surface/gulpfile', 'surface'], + ['./packages/grpc-protobufjs/gulpfile', 'protobuf'], + ['./test/gulpfile', 'internal.test'], +].forEach((args) => loadGulpTasksWithPrefix(args[0], args[1])); + +const root = __dirname; + +gulp.task('install.all', 'Install dependencies for all subdirectory packages', + ['js.install', 'js.core.install', 'native.core.install', 'surface.install', 'health-check.install', 'protobuf.install', 'internal.test.install']); + +gulp.task('install.all.windows', 'Install dependencies for all subdirectory packages for MS Windows', + ['js.core.install', 'native.core.install.windows', 'surface.install', 'health-check.install', 'protobuf.install', 'internal.test.install']); + +gulp.task('lint', 'Emit linting errors in source and test files', + ['js.core.lint', 'native.core.lint']); + +gulp.task('build', 'Build packages', ['js.compile', 'js.core.compile', 'native.core.build', 'protobuf.compile']); + +gulp.task('link.core', 'Add links to core packages without rebuilding', + ['js.link.add', 'native.link.add']); + +gulp.task('link.surface', 'Link to surface packages', + ['health-check.link.add']); + +gulp.task('link', 'Link together packages', (callback) => { + /** + * We use workarounds for linking in some modules. See npm/npm#18835 + */ + runSequence('link.core', 'link.surface', + callback); +}); + +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); +}); + +gulp.task('clean', 'Delete generated files', ['js.core.clean', 'native.core.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', 'js.clean.all', 'native.clean.all', 'protobuf.clean.all']); + +gulp.task('native.test.only', 'Run tests of native code without rebuilding anything', + ['native.core.test', 'health-check.test']); + +gulp.task('native.test', 'Run tests of native code', (callback) => { + runSequence('build', 'native.test.only', callback); +}); + +gulp.task('test.only', 'Run tests without rebuilding anything', + ['js.core.test', 'native.test.only']); + +gulp.task('test', 'Run all tests', (callback) => { + runSequence('build', 'test.only', callback); +}); + +gulp.task('doc.gen', 'Generate documentation', ['native.core.doc.gen']); + +gulp.task('default', ['help']); diff --git a/install-nvm-windows.ps1 b/install-nvm-windows.ps1 index 709a1ec2..b3794055 100644 --- a/install-nvm-windows.ps1 +++ b/install-nvm-windows.ps1 @@ -15,6 +15,9 @@ # We're going to store nvm-windows in the .\nvm directory. $env:NVM_HOME = (Get-Item -Path ".\" -Verbose).FullName + "\nvm" +# Switching to TLS/1.2 - see https://githubengineering.com/crypto-removal-notice/ +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + # Downloading and unpacking nvm-windows Invoke-WebRequest -Uri https://github.com/coreybutler/nvm-windows/releases/download/1.1.5/nvm-noinstall.zip -OutFile nvm-noinstall.zip Add-Type -AssemblyName System.IO.Compression.FileSystem diff --git a/merge_kokoro_logs.js b/merge_kokoro_logs.js index 5f524f88..d8cc98db 100644 --- a/merge_kokoro_logs.js +++ b/merge_kokoro_logs.js @@ -1,3 +1,20 @@ +/* + * 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 xml2js = require('xml2js'); const fs = require('fs'); const path = require('path'); diff --git a/package.json b/package.json index 4caf864b..5310e7f5 100644 --- a/package.json +++ b/package.json @@ -8,37 +8,46 @@ "name": "Google Inc." }, "license": "Apache-2.0", - "dependencies": { - "async": "^2.5.0", - "body-parser": "^1.18.0", - "execa": "^0.8.0", - "express": "^4.15.4", - "google-auth-library": "^0.11.0", - "lodash": "^4.17.4", - "mocha-jenkins-reporter": "^0.3.9", - "poisson-process": "^0.2.2" - }, "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", + "@types/pify": "^3.0.0", "del": "^3.0.0", + "execa": "^0.8.0", "gulp": "^3.9.1", "gulp-help": "^1.6.1", + "gulp-jsdoc3": "^1.0.1", "gulp-jshint": "^2.0.4", "gulp-mocha": "^4.3.1", "gulp-sourcemaps": "^2.6.1", "gulp-tslint": "^8.1.1", "gulp-typescript": "^3.2.2", "gulp-util": "^3.0.8", + "jsdoc": "^3.3.2", "jshint": "^2.9.5", + "make-dir": "^1.1.0", "merge2": "^1.1.0", "mocha": "^3.5.3", + "mocha-jenkins-reporter": "^0.3.9", + "ncp": "^2.0.0", + "pify": "^3.0.0", "through2": "^2.0.3", + "ts-node": "^3.3.0", "tslint": "^5.5.0", - "typescript": "^2.5.1", + "typescript": "~2.7.0", "xml2js": "^0.4.19" }, "contributors": [ { "name": "Google Inc." } - ] + ], + "dependencies": { + "run-sequence": "^2.2.0", + "symlink": "^2.1.0" + } } diff --git a/packages/grpc-health-check/gulpfile.js b/packages/grpc-health-check/gulpfile.js index 7c66a9cd..6fcd1804 100644 --- a/packages/grpc-health-check/gulpfile.js +++ b/packages/grpc-health-check/gulpfile.js @@ -1,9 +1,27 @@ +/* + * 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); @@ -11,22 +29,22 @@ const healthCheckDir = __dirname; const baseDir = path.resolve(healthCheckDir, '..', '..'); const testDir = path.resolve(healthCheckDir, 'test'); -gulp.task('health-check.clean.links', 'Delete npm links', () => { +gulp.task('clean.links', 'Delete npm links', () => { return del(path.resolve(healthCheckDir, 'node_modules/grpc')); }); -gulp.task('health-check.clean.all', 'Delete all code created by tasks', - ['health-check.clean.links']); +gulp.task('clean.all', 'Delete all code created by tasks', + ['clean.links']); -gulp.task('health-check.install', 'Install health check dependencies', () => { +gulp.task('install', 'Install health check dependencies', () => { return execa('npm', ['install', '--unsafe-perm'], {cwd: healthCheckDir, stdio: 'inherit'}); }); -gulp.task('health-check.link.add', 'Link local copy of grpc', ['health-check.install'], () => { - return execa('npm', ['link', 'grpc'], {cwd: healthCheckDir, stdio: 'inherit'}); +gulp.task('link.add', 'Link local copy of grpc', () => { + linkSync(healthCheckDir, './node_modules/grpc', '../grpc-native-core'); }); -gulp.task('health-check.test', 'Run health check tests', +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/package.json b/packages/grpc-health-check/package.json index 20c4584e..ce179a94 100644 --- a/packages/grpc-health-check/package.json +++ b/packages/grpc-health-check/package.json @@ -1,6 +1,6 @@ { - "name": "grpc-health-check", - "version": "1.7.0-dev", + "name": "@grpc/health-check", + "version": "1.8.0-dev", "author": "Google Inc.", "description": "Health check service for use with gRPC", "repository": { @@ -15,9 +15,9 @@ } ], "dependencies": { - "google-protobuf": "^3.0.0", + "google-protobuf": "^3.4.0", "grpc": "^1.6.0", - "lodash": "^3.9.3" + "lodash": "^3.10.1" }, "files": [ "LICENSE", diff --git a/packages/grpc-js-core/gulpfile.js b/packages/grpc-js-core/gulpfile.js deleted file mode 100644 index 5a787084..00000000 --- a/packages/grpc-js-core/gulpfile.js +++ /dev/null @@ -1,180 +0,0 @@ -'use strict'; - -const _gulp = require('gulp'); -const help = require('gulp-help'); - -// gulp-help monkeypatches tasks to have an additional description parameter -const gulp = help(_gulp); - -const del = require('del'); -const mocha = require('gulp-mocha'); -const sourcemaps = require('gulp-sourcemaps'); -const tslint = require('gulp-tslint'); -const typescript = require('gulp-typescript'); -const util = require('gulp-util'); -const merge2 = require('merge2'); -const path = require('path'); -const through = require('through2'); -const execa = require('execa'); - -Error.stackTraceLimit = Infinity; - -const jsCoreDir = __dirname; -const tslintPath = path.resolve(jsCoreDir, 'node_modules/google-ts-style/tslint.json'); -const tsconfigPath = path.resolve(jsCoreDir, 'tsconfig.json'); -const outDir = path.resolve(jsCoreDir, 'build'); -const srcDir = path.resolve(jsCoreDir, 'src'); -const testDir = path.resolve(jsCoreDir, 'test'); - -function onError() {} - -// Coalesces all specified --file parameters into a single array -const files = !util.env.file ? [] : - Array.isArray(util.env.file) ? util.env.file : [util.env.file]; - -// If --dev is passed, override certain ts config options -let tsDevOptions = {}; -if (util.env.dev) { - tsDevOptions = { - allowUnreachableCode: true, - noUnusedParameters: false, - noImplicitAny: false, - noImplicitThis: false, - noEmitOnError: false - }; -} - -/** - * Helper function that creates a gulp task function that opens files in a - * directory that match a certain glob pattern, transpiles them, and writes them - * to an output directory. - * @param {Object} globs - * @param {string=} globs.transpile The glob pattern for files to transpile. - * Defaults to match all *.ts files in baseDir (incl. subdirectories). - * @param {string=} globs.copy The glob pattern for files to transpile. - * Defaults to match all but *.ts files in baseDir (incl. subdirectories). - * @return A gulp task function. - */ -function makeCompileFn(globs) { - const transpileGlob = globs.transpile || `${srcDir}/**/*.ts`; - const copyGlob = globs.copy || '!(**/*)'; - return () => { - const tsProject = typescript.createProject(tsconfigPath, tsDevOptions)(); - const data = gulp.src(transpileGlob, { base: jsCoreDir }) - .pipe(sourcemaps.init()) - .pipe(tsProject) - .on('error', onError); - const dts = data.dts; - const js = data.js; - const jsmap = js.pipe(sourcemaps.write('.', { - includeContent: false, - sourceRoot: '..' - })); - const copy = gulp.src(copyGlob, { base: jsCoreDir }); - return merge2([ - js.pipe(gulp.dest(`${outDir}`)), - dts.pipe(gulp.dest(`${outDir}/types`)), - jsmap.pipe(gulp.dest(`${outDir}`)), - copy.pipe(gulp.dest(`${outDir}`)) - ]); - }; -} - -gulp.task('js.core.install', 'Install native core dependencies', () => { - return execa('npm', ['install', '--unsafe-perm'], {cwd: jsCoreDir, stdio: 'inherit'}); -}); - -/** - * Runs tslint on files in src/, with linting rules defined in tslint.json. - */ -gulp.task('js.core.lint', 'Emits linting errors found in src/ and test/.', () => { - const program = require('tslint').Linter.createProgram(tsconfigPath); - gulp.src([`${srcDir}/**/*.ts`, `${testDir}/**/*.ts`]) - .pipe(tslint({ - configuration: tslintPath, - formatter: 'codeFrame', - program - })) - .pipe(tslint.report()) - .on('warning', onError); -}); - -gulp.task('js.core.clean', 'Deletes transpiled code.', () => { - return del(outDir); -}); - -gulp.task('js.core.clean.all', 'Deletes all files added by targets', - ['js.core.clean']); - -/** - * Transpiles TypeScript files in src/ to JavaScript according to the settings - * found in tsconfig.json. - * Currently, all errors are emitted twice. This is being tracked here: - * https://github.com/ivogabe/gulp-typescript/issues/438 - */ -gulp.task('js.core.compile', 'Transpiles src/.', - makeCompileFn({ transpile: [`${srcDir}/**/*.ts`] })); - -/** - * Transpiles TypeScript files in both src/ and test/. - */ -gulp.task('js.core.test.compile', 'After dep tasks, transpiles test/.', ['js.core.compile'], - makeCompileFn({ transpile: [`${testDir}/**/*.ts`], copy: `${testDir}/**/!(*.ts)` })); - -/** - * Transpiles src/ and test/, and then runs all tests. - */ -gulp.task('js.core.test', 'After dep tasks, runs all tests.', - ['js.core.test.compile'], () => { - return gulp.src(`${outDir}/test/**/*.js`) - .pipe(mocha({reporter: 'mocha-jenkins-reporter'})); - } - ); - -/** - * Transpiles individual files, specified by the --file flag. - */ -gulp.task('js.core.compile.single', 'Transpiles individual files specified by --file.', - makeCompileFn({ - transpile: files.map(f => path.relative('.', f)) - }) - ); - -/** - * Run individual tests, specified by their pre-transpiled source path (as - * supplied through the '--file' flag). This is intended to be used as part of a - * VS Code "Gulp task" launch configuration; setting the "args" field to - * ["test.single", "--file", "${file}"] makes it possible for one to debug the - * currently open TS mocha test file in one step. - */ -gulp.task('js.core.test.single', 'After dep tasks, runs individual files specified ' + - 'by --file.', ['js.core.compile', 'js.core.compile.single'], () => { - // util.env contains CLI arguments for the gulp task. - // Determine the path to the transpiled version of this TS file. - const getTranspiledPath = (file) => { - const dir = path.dirname(path.relative(jsCoreDir, file)); - const basename = path.basename(file, '.ts'); - const result = `${outDir}/${dir}/${basename}.js`; - console.log(result); - return result; - }; - // Construct an instance of Mocha's runner API and feed it the path to the - // transpiled source. - return gulp.src(files.map(getTranspiledPath)) - .pipe(through.obj((file, enc, cb) => { - // Construct a new Mocha runner instance. - const Mocha = require('mocha'); - const runner = new Mocha(); - // Add the path to the test file to debug. - runner.addFile(file.path); - // Run the test suite. - runner.run((failures) => { - if (failures > 0) { - cb(new Error(`Mocha: ${failures} failures in ${file.path}]`)); - } else { - cb(null); - } - }); - })); - } - ); diff --git a/packages/grpc-js-core/gulpfile.ts b/packages/grpc-js-core/gulpfile.ts new file mode 100644 index 00000000..d309f688 --- /dev/null +++ b/packages/grpc-js-core/gulpfile.ts @@ -0,0 +1,76 @@ +/* + * 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. + * + */ + +import * as _gulp from 'gulp'; +import * as help from 'gulp-help'; + +import * as fs from 'fs'; +import * as mocha from 'gulp-mocha'; +import * as path from 'path'; +import * as execa from 'execa'; +import * as pify from 'pify'; +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; + +const jsCoreDir = __dirname; +const tslintPath = path.resolve(jsCoreDir, 'node_modules/google-ts-style/tslint.json'); +const tsconfigPath = path.resolve(jsCoreDir, 'tsconfig.json'); +const outDir = path.resolve(jsCoreDir, 'build'); +const srcDir = path.resolve(jsCoreDir, 'src'); +const testDir = path.resolve(jsCoreDir, 'test'); + +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')); + +/** + * 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')); + +gulp.task('clean', 'Deletes transpiled code.', ['install'], + () => execNpmCommand('clean')); + +gulp.task('clean.all', 'Deletes all files added by targets', ['clean']); + +/** + * Transpiles TypeScript files in src/ to JavaScript according to the settings + * found in tsconfig.json. + */ +gulp.task('compile', 'Transpiles src/.', () => execNpmCommand('compile')); + +gulp.task('copy-test-fixtures', 'Copy test fixtures.', () => { + return ncpP(`${jsCoreDir}/test/fixtures`, `${outDir}/test/fixtures`); +}); + +/** + * Transpiles src/ and test/, and then runs all tests. + */ +gulp.task('test', 'Runs all tests.', ['copy-test-fixtures'], () => { + return gulp.src(`${outDir}/test/**/*.js`) + .pipe(mocha({reporter: 'mocha-jenkins-reporter'})); +}); diff --git a/packages/grpc-js-core/package.json b/packages/grpc-js-core/package.json index 83fcb0e9..ffdc2b4a 100644 --- a/packages/grpc-js-core/package.json +++ b/packages/grpc-js-core/package.json @@ -1,10 +1,9 @@ { - "name": "grpc-js-core", + "name": "@grpc/js-core", "version": "0.1.0", "description": "gRPC Library for Node - pure JS core", "homepage": "https://grpc.io/", "main": "build/src/index.js", - "private": true, "engines": { "node": ">=8.4" }, @@ -12,29 +11,34 @@ "author": { "name": "Google Inc." }, - "types": "src/index.ts", + "types": "build/src/index.d.ts", "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.73", - "@types/mocha": "^2.2.42", - "@types/node": "^8.0.25", - "clang-format": "^1.0.53", - "google-ts-style": "^0.2.0" + "@types/lodash": "^4.14.77", + "@types/mocha": "^2.2.43", + "@types/node": "^9.4.6", + "clang-format": "^1.0.55", + "gts": "^0.5.1", + "typescript": "~2.7.0" }, "contributors": [ { "name": "Google Inc." } ], - "_id": "grpc-js-core@0.1.0", + "_id": "@grpc/js-core@0.1.0", "scripts": { "build": "npm run compile", - "clean": "gulp clean", - "compile": "gulp js.core.compile", + "clean": "gts clean", + "compile": "tsc -p .", "format": "clang-format -i -style=\"{Language: JavaScript, BasedOnStyle: Google, ColumnLimit: 80}\" src/*.ts test/*.ts", "lint": "tslint -c node_modules/google-ts-style/tslint.json -p . -t codeFrame --type-check", - "prepare": "npm run build", - "test": "gulp test" + "prepare": "npm run compile", + "test": "gulp test", + "check": "gts check", + "fix": "gts fix", + "pretest": "npm run compile", + "posttest": "npm run check" }, "dependencies": { "lodash": "^4.17.4" diff --git a/packages/grpc-js-core/src/call-credentials-filter.ts b/packages/grpc-js-core/src/call-credentials-filter.ts index 67812459..e432935b 100644 --- a/packages/grpc-js-core/src/call-credentials-filter.ts +++ b/packages/grpc-js-core/src/call-credentials-filter.ts @@ -12,8 +12,8 @@ export class CallCredentialsFilter extends BaseFilter implements Filter { } async sendMetadata(metadata: Promise): Promise { - // TODO(murgatroid99): pass real options to generateMetadata - let credsMetadata = this.credentials.generateMetadata.bind({}); + // TODO(kjin): pass real service URL to generateMetadata + let credsMetadata = this.credentials.generateMetadata({ service_url: '' }); let resultMetadata = await metadata; resultMetadata.merge(await credsMetadata); return resultMetadata; diff --git a/packages/grpc-js-core/src/call-credentials.ts b/packages/grpc-js-core/src/call-credentials.ts index 5b322402..0c0f6d53 100644 --- a/packages/grpc-js-core/src/call-credentials.ts +++ b/packages/grpc-js-core/src/call-credentials.ts @@ -2,8 +2,10 @@ import {map, reduce} from 'lodash'; import {Metadata} from './metadata'; +export type CallMetadataOptions = { service_url: string; }; + export type CallMetadataGenerator = - (options: Object, cb: (err: Error|null, metadata?: Metadata) => void) => + (options: CallMetadataOptions, cb: (err: Error|null, metadata?: Metadata) => void) => void; /** @@ -15,7 +17,7 @@ export interface CallCredentials { * Asynchronously generates a new Metadata object. * @param options Options used in generating the Metadata object. */ - generateMetadata(options: Object): Promise; + generateMetadata(options: CallMetadataOptions): Promise; /** * Creates a new CallCredentials object from properties of both this and * another CallCredentials object. This object's metadata generator will be @@ -28,7 +30,7 @@ export interface CallCredentials { class ComposedCallCredentials implements CallCredentials { constructor(private creds: CallCredentials[]) {} - async generateMetadata(options: Object): Promise { + async generateMetadata(options: CallMetadataOptions): Promise { let base: Metadata = new Metadata(); let generated: Metadata[] = await Promise.all( map(this.creds, (cred) => cred.generateMetadata(options))); @@ -46,7 +48,7 @@ class ComposedCallCredentials implements CallCredentials { class SingleCallCredentials implements CallCredentials { constructor(private metadataGenerator: CallMetadataGenerator) {} - async generateMetadata(options: Object): Promise { + generateMetadata(options: CallMetadataOptions): Promise { return new Promise((resolve, reject) => { this.metadataGenerator(options, (err, metadata) => { if (metadata !== undefined) { @@ -64,8 +66,8 @@ class SingleCallCredentials implements CallCredentials { } class EmptyCallCredentials implements CallCredentials { - async generateMetadata(options: Object): Promise { - return new Metadata(); + generateMetadata(options: CallMetadataOptions): Promise { + return Promise.resolve(new Metadata()); } compose(other: CallCredentials): CallCredentials { diff --git a/packages/grpc-js-core/src/call-stream.ts b/packages/grpc-js-core/src/call-stream.ts index 51c58756..3a403d68 100644 --- a/packages/grpc-js-core/src/call-stream.ts +++ b/packages/grpc-js-core/src/call-stream.ts @@ -3,12 +3,13 @@ import {Duplex} from 'stream'; import {CallCredentials} from './call-credentials'; import {Status} from './constants'; +import {EmitterAugmentation1} from './events'; import {Filter} from './filter'; import {FilterStackFactory} from './filter-stack'; import {Metadata} from './metadata'; import {ObjectDuplex} from './object-stream'; -const {HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE} = http2.constants; +const {HTTP2_HEADER_STATUS, HTTP2_HEADER_CONTENT_TYPE, NGHTTP2_CANCEL} = http2.constants; export type Deadline = Date | number; @@ -34,7 +35,7 @@ export interface WriteObject { /** * This interface represents a duplex stream associated with a single gRPC call. */ -export interface CallStream extends ObjectDuplex { +export type CallStream = { cancelWithStatus(status: Status, details: string): void; getPeer(): string; @@ -43,37 +44,9 @@ export interface CallStream extends ObjectDuplex { /* If the return value is null, the call has not ended yet. Otherwise, it has * ended with the specified status */ getStatus(): StatusObject|null; - - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; - - addListener(event: 'metadata', listener: (metadata: Metadata) => void): this; - emit(event: 'metadata', metadata: Metadata): boolean; - on(event: 'metadata', listener: (metadata: Metadata) => void): this; - once(event: 'metadata', listener: (metadata: Metadata) => void): this; - prependListener(event: 'metadata', listener: (metadata: Metadata) => void): - this; - prependOnceListener( - event: 'metadata', listener: (metadata: Metadata) => void): this; - removeListener(event: 'metadata', listener: (metadata: Metadata) => void): - this; - - addListener(event: 'status', listener: (status: StatusObject) => void): this; - emit(event: 'status', status: StatusObject): boolean; - on(event: 'status', listener: (status: StatusObject) => void): this; - once(event: 'status', listener: (status: StatusObject) => void): this; - prependListener(event: 'status', listener: (status: StatusObject) => void): - this; - prependOnceListener( - event: 'status', listener: (status: StatusObject) => void): this; - removeListener(event: 'status', listener: (status: StatusObject) => void): - this; -} +} & EmitterAugmentation1<'metadata', Metadata> + & EmitterAugmentation1<'status', StatusObject> + & ObjectDuplex; enum ReadState { NO_DATA, @@ -105,6 +78,13 @@ export class Http2CallStream extends Duplex implements CallStream { // Status code mapped from :status. To be used if grpc-status is not received private mappedStatusCode: Status = Status.UNKNOWN; + // Promise objects that are re-assigned to resolving promises when headers + // or trailers received. Processing headers/trailers is asynchronous, so we + // can use these objects to await their completion. This helps us establish + // order of precedence when obtaining the status of the call. + private handlingHeaders = Promise.resolve(); + private handlingTrailers = Promise.resolve(); + // This is populated (non-null) if and only if the call has ended private finalStatus: StatusObject|null = null; @@ -116,6 +96,11 @@ export class Http2CallStream extends Duplex implements CallStream { this.filterStack = filterStackFactory.createFilter(this); } + /** + * On first call, emits a 'status' event with the given StatusObject. + * Subsequent calls are no-ops. + * @param status The status of the call. + */ private endCall(status: StatusObject): void { if (this.finalStatus === null) { this.finalStatus = status; @@ -135,12 +120,46 @@ export class Http2CallStream extends Duplex implements CallStream { return canPush; } + private handleTrailers(headers: http2.IncomingHttpHeaders) { + let code: Status = this.mappedStatusCode; + let details = ''; + let metadata: Metadata; + try { + metadata = Metadata.fromHttp2Headers(headers); + } catch (e) { + metadata = new Metadata(); + } + let status: StatusObject = {code, details, metadata}; + this.handlingTrailers = (async () => { + let finalStatus; + try { + // Attempt to assign final status. + finalStatus = await this.filterStack.receiveTrailers(Promise.resolve(status)); + } catch (error) { + await this.handlingHeaders; + // This is a no-op if the call was already ended when handling headers. + this.endCall({ + code: Status.INTERNAL, + details: 'Failed to process received status', + metadata: new Metadata() + }); + return; + } + // It's possible that headers were received but not fully handled yet. + // Give the headers handler an opportunity to end the call first, + // if an error occurred. + await this.handlingHeaders; + // This is a no-op if the call was already ended when handling headers. + this.endCall(finalStatus); + })(); + } + attachHttp2Stream(stream: http2.ClientHttp2Stream): void { if (this.finalStatus !== null) { - stream.rstWithCancel(); + stream.close(NGHTTP2_CANCEL); } else { this.http2Stream = stream; - stream.on('response', (headers) => { + stream.on('response', (headers, flags) => { switch (headers[HTTP2_HEADER_STATUS]) { // TODO(murgatroid99): handle 100 and 101 case '400': @@ -166,57 +185,27 @@ export class Http2CallStream extends Duplex implements CallStream { } delete headers[HTTP2_HEADER_STATUS]; delete headers[HTTP2_HEADER_CONTENT_TYPE]; - let metadata: Metadata; - try { - metadata = Metadata.fromHttp2Headers(headers); - } catch (e) { - this.cancelWithStatus(Status.UNKNOWN, e.message); - return; - } - this.filterStack.receiveMetadata(Promise.resolve(metadata)) - .then( - (finalMetadata) => { - this.emit('metadata', finalMetadata); - }, - (error) => { - this.cancelWithStatus(Status.UNKNOWN, error.message); - }); - }); - stream.on('trailers', (headers) => { - let code: Status = this.mappedStatusCode; - if (headers.hasOwnProperty('grpc-status')) { - let receivedCode = Number(headers['grpc-status']); - if (receivedCode in Status) { - code = receivedCode; - } else { - code = Status.UNKNOWN; + if (flags & http2.constants.NGHTTP2_FLAG_END_STREAM) { + this.handleTrailers(headers); + } else { + let metadata: Metadata; + try { + metadata = Metadata.fromHttp2Headers(headers); + } catch (error) { + this.endCall({code: Status.UNKNOWN, details: error.message, metadata: new Metadata()}); + return; } - delete headers['grpc-status']; + this.handlingHeaders = + this.filterStack.receiveMetadata(Promise.resolve(metadata)) + .then((finalMetadata) => { + this.emit('metadata', finalMetadata); + }).catch((error) => { + this.destroyHttp2Stream(); + this.endCall({code: Status.UNKNOWN, details: error.message, metadata: new Metadata()}); + }); } - let details = ''; - if (headers.hasOwnProperty('grpc-message')) { - details = decodeURI(headers['grpc-message']); - } - let metadata: Metadata; - try { - metadata = Metadata.fromHttp2Headers(headers); - } catch (e) { - metadata = new Metadata(); - } - let status: StatusObject = {code, details, metadata}; - this.filterStack.receiveTrailers(Promise.resolve(status)) - .then( - (finalStatus) => { - this.endCall(finalStatus); - }, - (error) => { - this.endCall({ - code: Status.INTERNAL, - details: 'Failed to process received status', - metadata: new Metadata() - }); - }); }); + stream.on('trailers', this.handleTrailers.bind(this)); stream.on('data', (data) => { let readHead = 0; let canPush = true; @@ -278,7 +267,7 @@ export class Http2CallStream extends Duplex implements CallStream { this.unpushedReadMessages.push(null); } }); - stream.on('streamClosed', (errorCode) => { + stream.on('close', async (errorCode) => { let code: Status; let details = ''; switch (errorCode) { @@ -299,9 +288,16 @@ export class Http2CallStream extends Duplex implements CallStream { default: code = Status.INTERNAL; } + // This guarantees that if trailers were received, the value of the + // 'grpc-status' header takes precedence for emitted status data. + await this.handlingTrailers; + // This is a no-op if trailers were received at all. + // This is OK, because status codes emitted here correspond to more + // catastrophic issues that prevent us from receiving trailers in the + // first place. this.endCall({code: code, details: details, metadata: new Metadata()}); }); - stream.on('error', () => { + stream.on('error', (err: Error) => { this.endCall({ code: Status.INTERNAL, details: 'Internal HTTP2 error', @@ -323,15 +319,26 @@ export class Http2CallStream extends Duplex implements CallStream { } } - cancelWithStatus(status: Status, details: string): void { - this.endCall({code: status, details: details, metadata: new Metadata()}); - if (this.http2Stream !== null) { + private destroyHttp2Stream() { + // The http2 stream could already have been destroyed if cancelWithStatus + // is called in response to an internal http2 error. + if (this.http2Stream !== null && !this.http2Stream.destroyed) { /* TODO(murgatroid99): Determine if we want to send different RST_STREAM * codes based on the status code */ - this.http2Stream.rstWithCancel(); + this.http2Stream.close(NGHTTP2_CANCEL); } } + cancelWithStatus(status: Status, details: string): void { + this.destroyHttp2Stream(); + (async () => { + // If trailers are currently being processed, the call should be ended + // by handleTrailers instead. + await this.handlingTrailers; + this.endCall({code: status, details: details, metadata: new Metadata()}); + })(); + } + getDeadline(): Deadline { return this.options.deadline; } diff --git a/packages/grpc-js-core/src/call.ts b/packages/grpc-js-core/src/call.ts index ba38a41c..af305b24 100644 --- a/packages/grpc-js-core/src/call.ts +++ b/packages/grpc-js-core/src/call.ts @@ -1,53 +1,62 @@ import {EventEmitter} from 'events'; +import {EmitterAugmentation1} from './events'; import {Duplex, Readable, Writable} from 'stream'; import {CallStream, StatusObject, WriteObject} from './call-stream'; import {Status} from './constants'; import {Metadata} from './metadata'; import {ObjectReadable, ObjectWritable} from './object-stream'; +import * as _ from 'lodash'; -export interface ServiceError extends Error { - code?: number; - metadata?: Metadata; -} +/** + * A type extending the built-in Error object with additional fields. + */ +export type ServiceError = StatusObject & Error; -export class ServiceErrorImpl extends Error implements ServiceError { - code?: number; - metadata?: Metadata; -} - -export interface Call extends EventEmitter { +/** + * A base type for all user-facing values returned by client-side method calls. + */ +export type Call = { cancel(): void; getPeer(): string; +} & EmitterAugmentation1<'metadata', Metadata> + & EmitterAugmentation1<'status', StatusObject> + & EventEmitter; - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; +/** + * A type representing the return value of a unary method call. + */ +export type ClientUnaryCall = Call; - addListener(event: 'metadata', listener: (metadata: Metadata) => void): this; - emit(event: 'metadata', metadata: Metadata): boolean; - on(event: 'metadata', listener: (metadata: Metadata) => void): this; - once(event: 'metadata', listener: (metadata: Metadata) => void): this; - prependListener(event: 'metadata', listener: (metadata: Metadata) => void): - this; - prependOnceListener( - event: 'metadata', listener: (metadata: Metadata) => void): this; - removeListener(event: 'metadata', listener: (metadata: Metadata) => void): - this; -} +/** + * A type representing the return value of a server stream method call. + */ +export type ClientReadableStream = { + deserialize: (chunk: Buffer) => ResponseType; +} & Call & ObjectReadable; -export interface ClientUnaryCall extends Call {} +/** + * A type representing the return value of a client stream method call. + */ +export type ClientWritableStream = { + serialize: (value: RequestType) => Buffer; +} & Call & ObjectWritable; -export class ClientUnaryCallImpl extends EventEmitter implements Call { +/** + * A type representing the return value of a bidirectional stream method call. + */ +export type ClientDuplexStream = + ClientWritableStream & ClientReadableStream; + +export class ClientUnaryCallImpl extends EventEmitter implements ClientUnaryCall { constructor(private readonly call: CallStream) { super(); call.on('metadata', (metadata: Metadata) => { this.emit('metadata', metadata); }); + call.on('status', (status: StatusObject) => { + this.emit('status', status); + }); } cancel(): void { @@ -59,54 +68,6 @@ export class ClientUnaryCallImpl extends EventEmitter implements Call { } } -export interface ClientReadableStream extends - Call, ObjectReadable { - deserialize: (chunk: Buffer) => ResponseType; - - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; - - addListener(event: 'status', listener: (status: StatusObject) => void): this; - emit(event: 'status', status: StatusObject): boolean; - on(event: 'status', listener: (status: StatusObject) => void): this; - once(event: 'status', listener: (status: StatusObject) => void): this; - prependListener(event: 'status', listener: (status: StatusObject) => void): - this; - prependOnceListener( - event: 'status', listener: (status: StatusObject) => void): this; - removeListener(event: 'status', listener: (status: StatusObject) => void): - this; -} - -export interface ClientWritableStream extends - Call, ObjectWritable { - serialize: (value: RequestType) => Buffer; - - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; -} - -export interface ClientDuplexStream extends - ClientWritableStream, ClientReadableStream { - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; -} - function setUpReadableStream( stream: ClientReadableStream, call: CallStream, deserialize: (chunk: Buffer) => ResponseType): void { @@ -128,9 +89,9 @@ function setUpReadableStream( call.on('status', (status: StatusObject) => { stream.emit('status', status); if (status.code !== Status.OK) { - const error = new ServiceErrorImpl(status.details); - error.code = status.code; - error.metadata = status.metadata; + const statusName = _.invert(Status)[status.code]; + const message: string = `${status.code} ${statusName}: ${status.details}`; + const error: ServiceError = Object.assign(new Error(status.details), status); stream.emit('error', error); } }); @@ -190,6 +151,9 @@ export class ClientWritableStreamImpl extends Writable implements call.on('metadata', (metadata: Metadata) => { this.emit('metadata', metadata); }); + call.on('status', (status: StatusObject) => { + this.emit('status', status); + }); } cancel(): void { diff --git a/packages/grpc-js-core/src/channel-credentials.ts b/packages/grpc-js-core/src/channel-credentials.ts index 0d87d5de..419e3d17 100644 --- a/packages/grpc-js-core/src/channel-credentials.ts +++ b/packages/grpc-js-core/src/channel-credentials.ts @@ -79,7 +79,14 @@ class SecureChannelCredentialsImpl extends ChannelCredentialsImpl { } } +function verifyIsBufferOrNull(obj: any, friendlyName: string): void { + if (obj && !(obj instanceof Buffer)) { + throw new TypeError(`${friendlyName}, if provided, must be a Buffer.`); + } +} + export namespace ChannelCredentials { + /** * Return a new ChannelCredentials instance with a given set of credentials. * The resulting instance can be used to construct a Channel that communicates @@ -91,6 +98,9 @@ export namespace ChannelCredentials { export function createSsl( rootCerts?: Buffer|null, privateKey?: Buffer|null, certChain?: Buffer|null): ChannelCredentials { + verifyIsBufferOrNull(rootCerts, 'Root certificate'); + verifyIsBufferOrNull(privateKey, 'Private key'); + verifyIsBufferOrNull(certChain, 'Certificate chain'); if (privateKey && !certChain) { throw new Error( 'Private key must be given with accompanying certificate chain'); diff --git a/packages/grpc-js-core/src/channel.ts b/packages/grpc-js-core/src/channel.ts index 1b7fda5b..a4d04023 100644 --- a/packages/grpc-js-core/src/channel.ts +++ b/packages/grpc-js-core/src/channel.ts @@ -1,6 +1,6 @@ import {EventEmitter} from 'events'; import * as http2 from 'http2'; -import {SecureContext} from 'tls'; +import {checkServerIdentity, SecureContext, PeerCertificate} from 'tls'; import * as url from 'url'; import {CallCredentials} from './call-credentials'; @@ -12,22 +12,38 @@ import {Status} from './constants'; import {DeadlineFilterFactory} from './deadline-filter'; import {FilterStackFactory} from './filter-stack'; import {Metadata, MetadataObject} from './metadata'; +import { MetadataStatusFilterFactory } from './metadata-status-filter'; + +const { version: clientVersion } = require('../../package'); const IDLE_TIMEOUT_MS = 300000; +const MIN_CONNECT_TIMEOUT_MS = 20000; +const INITIAL_BACKOFF_MS = 1000; +const BACKOFF_MULTIPLIER = 1.6; +const MAX_BACKOFF_MS = 120000; +const BACKOFF_JITTER = 0.2; + const { HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_SCHEME, - HTTP2_HEADER_TE + HTTP2_HEADER_TE, + HTTP2_HEADER_USER_AGENT } = http2.constants; /** * An interface that contains options used when initializing a Channel instance. */ -export interface ChannelOptions { [index: string]: string|number; } +export interface ChannelOptions { + 'grpc.ssl_target_name_override': string; + 'grpc.primary_user_agent': string; + 'grpc.secondary_user_agent': string; + 'grpc.default_authority': string; + [key: string]: string | number; +} export enum ConnectivityState { CONNECTING, @@ -37,6 +53,10 @@ export enum ConnectivityState { SHUTDOWN } +function uniformRandom(min:number, max: number) { + return Math.random() * (max - min) + min; +} + // todo: maybe we want an interface for load balancing, but no implementation // for anything complicated @@ -47,7 +67,7 @@ export enum ConnectivityState { export interface Channel extends EventEmitter { createStream(methodName: string, metadata: Metadata, options: CallOptions): CallStream; - connect(callback: () => void): void; + connect(): Promise; getConnectivityState(): ConnectivityState; close(): void; @@ -61,104 +81,186 @@ export interface Channel extends EventEmitter { } export class Http2Channel extends EventEmitter implements Channel { + private readonly userAgent: string; + private readonly authority: url.URL; private connectivityState: ConnectivityState = ConnectivityState.IDLE; - private idleTimerId: NodeJS.Timer|null = null; /* For now, we have up to one subchannel, which will exist as long as we are * connecting or trying to connect */ - private subChannel: http2.ClientHttp2Session|null; + private subChannel: http2.ClientHttp2Session|null = null; private filterStackFactory: FilterStackFactory; - private transitionToState(newState: ConnectivityState): void { - if (newState !== this.connectivityState) { + private subChannelConnectCallback: ()=>void = () => {}; + private subChannelCloseCallback: ()=>void = () => {}; + + private backoffTimerId: NodeJS.Timer; + private currentBackoff: number = INITIAL_BACKOFF_MS; + private currentBackoffDeadline: Date; + + private handleStateChange(oldState: ConnectivityState, newState: ConnectivityState): void { + let now: Date = new Date(); + switch(newState) { + case ConnectivityState.CONNECTING: + if (oldState === ConnectivityState.IDLE) { + this.currentBackoff = INITIAL_BACKOFF_MS; + this.currentBackoffDeadline = new Date(now.getTime() + INITIAL_BACKOFF_MS); + } else if (oldState === ConnectivityState.TRANSIENT_FAILURE) { + this.currentBackoff = Math.min(this.currentBackoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS); + let jitterMagnitude: number = BACKOFF_JITTER * this.currentBackoff; + this.currentBackoffDeadline = new Date(now.getTime() + this.currentBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude)); + } + this.startConnecting(); + break; + case ConnectivityState.READY: + this.emit('connect'); + break; + case ConnectivityState.TRANSIENT_FAILURE: + this.subChannel = null; + this.backoffTimerId = setTimeout(() => { + this.transitionToState([ConnectivityState.TRANSIENT_FAILURE], ConnectivityState.CONNECTING); + }, this.currentBackoffDeadline.getTime() - now.getTime()); + break; + case ConnectivityState.IDLE: + case ConnectivityState.SHUTDOWN: + if (this.subChannel) { + this.subChannel.close(); + this.subChannel.removeListener('connect', this.subChannelConnectCallback); + this.subChannel.removeListener('close', this.subChannelCloseCallback); + this.subChannel = null; + clearTimeout(this.backoffTimerId); + } + break; + } + } + + // Transition from any of a set of oldStates to a specific newState + private transitionToState(oldStates: ConnectivityState[], newState: ConnectivityState): void { + if (oldStates.indexOf(this.connectivityState) > -1) { + let oldState: ConnectivityState = this.connectivityState; this.connectivityState = newState; + this.handleStateChange(oldState, newState); this.emit('connectivityStateChanged', newState); } } private startConnecting(): void { - this.transitionToState(ConnectivityState.CONNECTING); + let subChannel: http2.ClientHttp2Session; let secureContext = this.credentials.getSecureContext(); if (secureContext === null) { - this.subChannel = http2.connect(this.address); + subChannel = http2.connect(this.authority); } else { - this.subChannel = http2.connect(this.address, {secureContext}); - } - this.subChannel.once('connect', () => { - this.transitionToState(ConnectivityState.READY); - }); - this.subChannel.setTimeout(IDLE_TIMEOUT_MS, () => { - this.goIdle(); - }); - /* TODO(murgatroid99): add connection-level error handling with exponential - * reconnection backoff */ - } - - private goIdle(): void { - if (this.subChannel !== null) { - this.subChannel.shutdown({graceful: true}, () => undefined); - this.subChannel = null; - } - this.transitionToState(ConnectivityState.IDLE); - } - - private kickConnectivityState(): void { - if (this.connectivityState === ConnectivityState.IDLE) { - this.startConnecting(); + const connectionOptions: http2.SecureClientSessionOptions = { + secureContext, + } + // If provided, the value of grpc.ssl_target_name_override should be used + // to override the target hostname when checking server identity. + // This option is used for testing only. + if (this.options['grpc.ssl_target_name_override']) { + const sslTargetNameOverride = this.options['grpc.ssl_target_name_override']!; + connectionOptions.checkServerIdentity = (host: string, cert: PeerCertificate): Error | undefined => { + return checkServerIdentity(sslTargetNameOverride, cert); + } + connectionOptions.servername = sslTargetNameOverride; + } + subChannel = http2.connect(this.authority, connectionOptions); } + this.subChannel = subChannel; + let now = new Date(); + let connectionTimeout: number = Math.max( + this.currentBackoffDeadline.getTime() - now.getTime(), + MIN_CONNECT_TIMEOUT_MS); + let connectionTimerId: NodeJS.Timer = setTimeout(() => { + // This should trigger the 'close' event, which will send us back to TRANSIENT_FAILURE + subChannel.close(); + }, connectionTimeout); + this.subChannelConnectCallback = () => { + // Connection succeeded + clearTimeout(connectionTimerId); + this.transitionToState([ConnectivityState.CONNECTING], ConnectivityState.READY); + }; + subChannel.once('connect', this.subChannelConnectCallback); + this.subChannelCloseCallback = () => { + // Connection failed + clearTimeout(connectionTimerId); + /* TODO(murgatroid99): verify that this works for CONNECTING->TRANSITIVE_FAILURE + * see nodejs/node#16645 */ + this.transitionToState([ConnectivityState.CONNECTING, ConnectivityState.READY], + ConnectivityState.TRANSIENT_FAILURE); + }; + subChannel.once('close', this.subChannelCloseCallback); } constructor( - private readonly address: url.URL, + address: string, public readonly credentials: ChannelCredentials, - private readonly options: ChannelOptions) { + private readonly options: Partial) { super(); if (credentials.getSecureContext() === null) { - address.protocol = 'http'; + this.authority = new url.URL(`http://${address}`); } else { - address.protocol = 'https'; + this.authority = new url.URL(`https://${address}`); } this.filterStackFactory = new FilterStackFactory([ new CompressionFilterFactory(this), - new CallCredentialsFilterFactory(this), new DeadlineFilterFactory(this) + new CallCredentialsFilterFactory(this), + new DeadlineFilterFactory(this), + new MetadataStatusFilterFactory(this) ]); + this.currentBackoffDeadline = new Date(); + /* The only purpose of these lines is to ensure that this.backoffTimerId has + * a value of type NodeJS.Timer. */ + this.backoffTimerId = setTimeout(() => {}, 0); + clearTimeout(this.backoffTimerId); + + // Build user-agent string. + this.userAgent = [ + options['grpc.primary_user_agent'], + `grpc-node-js/${clientVersion}`, + options['grpc.secondary_user_agent'] + ].filter(e => e).join(' '); // remove falsey values first } private startHttp2Stream( methodName: string, stream: Http2CallStream, metadata: Metadata) { let finalMetadata: Promise = - stream.filterStack.sendMetadata(Promise.resolve(metadata)); - this.connect(() => { - finalMetadata.then( - (metadataValue) => { - let headers = metadataValue.toHttp2Headers(); - headers[HTTP2_HEADER_AUTHORITY] = this.address.hostname; - headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; - headers[HTTP2_HEADER_METHOD] = 'POST'; - headers[HTTP2_HEADER_PATH] = methodName; - headers[HTTP2_HEADER_TE] = 'trailers'; - if (stream.getStatus() === null) { - if (this.connectivityState === ConnectivityState.READY) { - let session: http2.ClientHttp2Session = - (this.subChannel as http2.ClientHttp2Session); - stream.attachHttp2Stream(session.request(headers)); - } else { - /* In this case, we lost the connection while finalizing - * metadata. That should be very unusual */ - setImmediate(() => { - this.startHttp2Stream(methodName, stream, metadata); - }); - } - } - }, - (error) => { - stream.cancelWithStatus( - Status.UNKNOWN, 'Failed to generate metadata'); + stream.filterStack.sendMetadata(Promise.resolve(metadata.clone())); + Promise.all([finalMetadata, this.connect()]) + .then(([metadataValue]) => { + let headers = metadataValue.toHttp2Headers(); + let host: string; + // TODO(murgatroid99): Add more centralized handling of channel options + if (this.options['grpc.default_authority']) { + host = this.options['grpc.default_authority'] as string; + } else { + host = this.authority.hostname; + } + headers[HTTP2_HEADER_AUTHORITY] = host; + headers[HTTP2_HEADER_USER_AGENT] = this.userAgent; + headers[HTTP2_HEADER_CONTENT_TYPE] = 'application/grpc'; + headers[HTTP2_HEADER_METHOD] = 'POST'; + headers[HTTP2_HEADER_PATH] = methodName; + headers[HTTP2_HEADER_TE] = 'trailers'; + if (this.connectivityState === ConnectivityState.READY) { + const session: http2.ClientHttp2Session = this.subChannel!; + // Prevent the HTTP/2 session from keeping the process alive. + // Note: this function is only available in Node 9 + session.unref(); + stream.attachHttp2Stream(session.request(headers)); + } else { + /* In this case, we lost the connection while finalizing + * metadata. That should be very unusual */ + setImmediate(() => { + this.startHttp2Stream(methodName, stream, metadata); }); - }); + } + }).catch((error: Error & { code: number }) => { + // We assume the error code isn't 0 (Status.OK) + stream.cancelWithStatus(error.code || Status.UNKNOWN, + `Getting metadata from plugin failed with error: ${error.message}`); + }); } createStream(methodName: string, metadata: Metadata, options: CallOptions): - CallStream { + CallStream { if (this.connectivityState === ConnectivityState.SHUTDOWN) { throw new Error('Channel has been shut down'); } @@ -173,17 +275,15 @@ export class Http2Channel extends EventEmitter implements Channel { return stream; } - connect(callback: () => void): void { - this.kickConnectivityState(); - if (this.connectivityState === ConnectivityState.READY) { - setImmediate(callback); - } else { - this.once('connectivityStateChanged', (newState) => { - if (newState === ConnectivityState.READY) { - callback(); - } - }); - } + connect(): Promise { + return new Promise((resolve) => { + this.transitionToState([ConnectivityState.IDLE], ConnectivityState.CONNECTING); + if (this.connectivityState === ConnectivityState.READY) { + setImmediate(resolve); + } else { + this.once('connect', resolve); + } + }); } getConnectivityState(): ConnectivityState { @@ -194,9 +294,9 @@ export class Http2Channel extends EventEmitter implements Channel { if (this.connectivityState === ConnectivityState.SHUTDOWN) { throw new Error('Channel has been shut down'); } - this.transitionToState(ConnectivityState.SHUTDOWN); - if (this.subChannel !== null) { - this.subChannel.shutdown({graceful: true}); - } + this.transitionToState([ConnectivityState.CONNECTING, + ConnectivityState.READY, + ConnectivityState.TRANSIENT_FAILURE, + ConnectivityState.IDLE], ConnectivityState.SHUTDOWN); } } diff --git a/packages/grpc-js-core/src/client.ts b/packages/grpc-js-core/src/client.ts index 59a84d24..25fa7bc8 100644 --- a/packages/grpc-js-core/src/client.ts +++ b/packages/grpc-js-core/src/client.ts @@ -1,41 +1,46 @@ import {once} from 'lodash'; import {URL} from 'url'; -import {ClientDuplexStream, ClientDuplexStreamImpl, ClientReadableStream, ClientReadableStreamImpl, ClientUnaryCall, ClientUnaryCallImpl, ClientWritableStream, ClientWritableStreamImpl, ServiceError, ServiceErrorImpl} from './call'; +import {ClientDuplexStream, ClientDuplexStreamImpl, ClientReadableStream, ClientReadableStreamImpl, ClientUnaryCall, ClientUnaryCallImpl, ClientWritableStream, ClientWritableStreamImpl, ServiceError} from './call'; import {CallOptions, CallStream, StatusObject, WriteObject} from './call-stream'; import {Channel, ChannelOptions, Http2Channel} from './channel'; import {ChannelCredentials} from './channel-credentials'; 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(); + export interface UnaryCallback { (err: ServiceError|null, value?: ResponseType): void; } +/** + * A generic gRPC client. Primarily useful as a base class for all generated + * clients. + */ export class Client { - private readonly channel: Channel; + private readonly [kChannel]: Channel; constructor( address: string, credentials: ChannelCredentials, - options: ChannelOptions = {}) { - if (options['grpc.primary_user_agent']) { - options['grpc.primary_user_agent'] += ' '; - } else { - options['grpc.primary_user_agent'] = ''; - } - // TODO(murgatroid99): Figure out how to get version number - // options['grpc.primary_user_agent'] += 'grpc-node/' + version; - this.channel = new Http2Channel(new URL(address), credentials, options); + options: Partial = {}) { + this[kChannel] = new Http2Channel(address, credentials, options); } close(): void { - this.channel.close(); + this[kChannel].close(); } waitForReady(deadline: Date|number, callback: (error: Error|null) => void): void { let cb: (error: Error|null) => void = once(callback); let callbackCalled = false; - this.channel.connect(() => { + let timer: NodeJS.Timer | null = null; + this[kChannel].connect().then(() => { + if (timer) { + clearTimeout(timer); + } cb(null); }); if (deadline !== Infinity) { @@ -49,7 +54,7 @@ export class Client { if (timeout < 0) { timeout = 0; } - setTimeout(() => { + timer = setTimeout(() => { cb(new Error('Failed to connect before the deadline')); }, timeout); } @@ -83,9 +88,7 @@ export class Client { if (status.code === Status.OK) { callback(null, responseMessage as ResponseType); } else { - const error = new ServiceErrorImpl(status.details); - error.code = status.code; - error.metadata = status.metadata; + const error: ServiceError = Object.assign(new Error(status.details), status); callback(error); } }); @@ -135,7 +138,6 @@ export class Client { method: string, serialize: (value: RequestType) => Buffer, deserialize: (value: Buffer) => ResponseType, argument: RequestType, callback: UnaryCallback): ClientUnaryCall; - makeUnaryRequest( method: string, serialize: (value: RequestType) => Buffer, deserialize: (value: Buffer) => ResponseType, argument: RequestType, @@ -146,15 +148,14 @@ export class Client { this.checkOptionalUnaryResponseArguments( metadata, options, callback)); const call: CallStream = - this.channel.createStream(method, metadata, options); - const emitter: ClientUnaryCall = new ClientUnaryCallImpl(call); + this[kChannel].createStream(method, metadata, options); const message: Buffer = serialize(argument); const writeObj: WriteObject = {message: message}; writeObj.flags = options.flags; call.write(writeObj); call.end(); this.handleUnaryResponse(call, deserialize, callback); - return emitter; + return new ClientUnaryCallImpl(call); } makeClientStreamRequest( @@ -174,7 +175,6 @@ export class Client { method: string, serialize: (value: RequestType) => Buffer, deserialize: (value: Buffer) => ResponseType, callback: UnaryCallback): ClientWritableStream; - makeClientStreamRequest( method: string, serialize: (value: RequestType) => Buffer, deserialize: (value: Buffer) => ResponseType, @@ -186,11 +186,9 @@ export class Client { this.checkOptionalUnaryResponseArguments( metadata, options, callback)); const call: CallStream = - this.channel.createStream(method, metadata, options); - const stream: ClientWritableStream = - new ClientWritableStreamImpl(call, serialize); + this[kChannel].createStream(method, metadata, options); this.handleUnaryResponse(call, deserialize, callback); - return stream; + return new ClientWritableStreamImpl(call, serialize); } private checkMetadataAndOptions( @@ -232,15 +230,13 @@ export class Client { options?: CallOptions): ClientReadableStream { ({metadata, options} = this.checkMetadataAndOptions(metadata, options)); const call: CallStream = - this.channel.createStream(method, metadata, options); - const stream: ClientReadableStream = - new ClientReadableStreamImpl(call, deserialize); + this[kChannel].createStream(method, metadata, options); const message: Buffer = serialize(argument); const writeObj: WriteObject = {message: message}; writeObj.flags = options.flags; call.write(writeObj); call.end(); - return stream; + return new ClientReadableStreamImpl(call, deserialize); } makeBidiStreamRequest( @@ -258,10 +254,8 @@ export class Client { options?: CallOptions): ClientDuplexStream { ({metadata, options} = this.checkMetadataAndOptions(metadata, options)); const call: CallStream = - this.channel.createStream(method, metadata, options); - const stream: ClientDuplexStream = - new ClientDuplexStreamImpl( - call, serialize, deserialize); - return stream; + this[kChannel].createStream(method, metadata, options); + return new ClientDuplexStreamImpl( + call, serialize, deserialize); } } diff --git a/packages/grpc-js-core/src/deadline-filter.ts b/packages/grpc-js-core/src/deadline-filter.ts index 66b18107..428ed8a9 100644 --- a/packages/grpc-js-core/src/deadline-filter.ts +++ b/packages/grpc-js-core/src/deadline-filter.ts @@ -7,7 +7,20 @@ import {Metadata} from './metadata'; const units: [string, number][] = [['m', 1], ['S', 1000], ['M', 60 * 1000], ['H', 60 * 60 * 1000]]; +function getDeadline(deadline: number) { + let now = (new Date()).getTime(); + let timeoutMs = Math.max(deadline - now, 0); + for (let [unit, factor] of units) { + let amount = timeoutMs / factor; + if (amount < 1e8) { + return String(Math.ceil(amount)) + unit; + } + } + throw new Error('Deadline is too far in the future'); +} + export class DeadlineFilter extends BaseFilter implements Filter { + private timer: NodeJS.Timer | null = null; private deadline: number; constructor( private readonly channel: Http2Channel, @@ -25,10 +38,11 @@ export class DeadlineFilter extends BaseFilter implements Filter { timeout = 0; } if (this.deadline !== Infinity) { - setTimeout(() => { + this.timer = setTimeout(() => { callStream.cancelWithStatus( Status.DEADLINE_EXCEEDED, 'Deadline exceeded'); }, timeout); + callStream.on('status', () => clearTimeout(this.timer as NodeJS.Timer)); } } @@ -36,22 +50,10 @@ export class DeadlineFilter extends BaseFilter implements Filter { if (this.deadline === Infinity) { return await metadata; } - let timeoutString: Promise = - new Promise((resolve, reject) => { - this.channel.connect(() => { - let now = (new Date()).getTime(); - let timeoutMs = this.deadline - now; - for (let [unit, factor] of units) { - let amount = timeoutMs / factor; - if (amount < 1e8) { - resolve(String(Math.ceil(amount)) + unit); - return; - } - } - }); - }); - let finalMetadata = await metadata; - finalMetadata.set('grpc-timeout', await timeoutString); + await this.channel.connect(); + const timeoutString = getDeadline(this.deadline); + const finalMetadata = await metadata; + finalMetadata.set('grpc-timeout', timeoutString); return finalMetadata; } } diff --git a/packages/grpc-js-core/src/events.ts b/packages/grpc-js-core/src/events.ts new file mode 100644 index 00000000..591120d1 --- /dev/null +++ b/packages/grpc-js-core/src/events.ts @@ -0,0 +1,29 @@ +export interface EmitterAugmentation0 { + addListener(event: Name, listener: () => void): this; + emit(event: Name): boolean; + on(event: Name, listener: () => void): this; + once(event: Name, listener: () => void): this; + prependListener(event: Name, listener: () => void): this; + prependOnceListener(event: Name, listener: () => void): this; + removeListener(event: Name, listener: () => void): this; +} + +export interface EmitterAugmentation1 { + addListener(event: Name, listener: (arg1: Arg) => void): this; + emit(event: Name, arg1: Arg): boolean; + on(event: Name, listener: (arg1: Arg) => void): this; + once(event: Name, listener: (arg1: Arg) => void): this; + prependListener(event: Name, listener: (arg1: Arg) => void): this; + prependOnceListener(event: Name, listener: (arg1: Arg) => void): this; + removeListener(event: Name, listener: (arg1: Arg) => void): this; +} + +export interface EmitterAugmentation2 { + addListener(event: Name, listener: (arg1: Arg1, arg2: Arg2) => void): this; + emit(event: Name, arg1: Arg1, arg2: Arg2): boolean; + on(event: Name, listener: (arg1: Arg1, arg2: Arg2) => void): this; + once(event: Name, listener: (arg1: Arg1, arg2: Arg2) => void): this; + prependListener(event: Name, listener: (arg1: Arg1, arg2: Arg2) => void): this; + prependOnceListener(event: Name, listener: (arg1: Arg1, arg2: Arg2) => void): this; + removeListener(event: Name, listener: (arg1: Arg1, arg2: Arg2) => void): this; +} diff --git a/packages/grpc-js-core/src/index.ts b/packages/grpc-js-core/src/index.ts index be03f1bb..5a3f6025 100644 --- a/packages/grpc-js-core/src/index.ts +++ b/packages/grpc-js-core/src/index.ts @@ -1,5 +1,87 @@ -export * from './call-credentials'; -export * from './channel-credentials'; -export * from './client'; -export * from './constants'; -export * from './metadata'; + +import { CallCredentials } from './call-credentials'; +import { ChannelCredentials } from './channel-credentials'; +import { Client } from './client'; +import { Status} from './constants'; +import { makeClientConstructor, loadPackageDefinition } from './make-client'; +import { Metadata } from './metadata'; +import { IncomingHttpHeaders } from 'http'; + +export interface OAuth2Client { + getRequestMetadata: (url: string, callback: (err: Error|null, headers?: { Authorization: string }) => void) => void; +} + +/**** Client Credentials ****/ + +// Using assign only copies enumerable properties, which is what we want +export const credentials = Object.assign({ + /** + * Create a gRPC credential from a Google credential object. + * @param googleCredentials The authentication client to use. + * @return The resulting CallCredentials object. + */ + createFromGoogleCredential: (googleCredentials: OAuth2Client): CallCredentials => { + return CallCredentials.createFromMetadataGenerator((options, callback) => { + googleCredentials.getRequestMetadata(options.service_url, (err, headers) => { + if (err) { + callback(err); + return; + } + const metadata = new Metadata(); + metadata.add('authorization', headers!.Authorization); + callback(null, metadata); + }); + }); + }, + + /** + * Combine a ChannelCredentials with any number of CallCredentials into a + * single ChannelCredentials object. + * @param channelCredentials The ChannelCredentials object. + * @param callCredentials Any number of CallCredentials objects. + * @return The resulting ChannelCredentials object. + */ + combineChannelCredentials: ( + channelCredentials: ChannelCredentials, + ...callCredentials: CallCredentials[]): ChannelCredentials => { + return callCredentials.reduce((acc, other) => acc.compose(other), channelCredentials); + }, + + /** + * Combine any number of CallCredentials into a single CallCredentials object. + * @param first The first CallCredentials object. + * @param additional Any number of additional CallCredentials objects. + * @return The resulting CallCredentials object. + */ + combineCallCredentials: ( + first: CallCredentials, + ...additional: CallCredentials[]): CallCredentials => { + return additional.reduce((acc, other) => acc.compose(other), first); + } +}, ChannelCredentials, CallCredentials); + +/**** Metadata ****/ + +export { Metadata }; + +/**** Constants ****/ + +export { + Status as status + // TODO: Other constants as well +}; + +/**** Client ****/ + +export { + Client, + loadPackageDefinition, + makeClientConstructor, + makeClientConstructor as makeGenericClientConstructor +}; + +/** + * Close a Client object. + * @param client The client to close. + */ +export const closeClient = (client: Client) => client.close(); diff --git a/packages/grpc-js-core/src/make-client.ts b/packages/grpc-js-core/src/make-client.ts new file mode 100644 index 00000000..f5433f57 --- /dev/null +++ b/packages/grpc-js-core/src/make-client.ts @@ -0,0 +1,154 @@ +import { Metadata } from "./metadata"; +import { Client, UnaryCallback } from "./client"; +import { CallOptions } from "./call-stream"; +import * as _ from 'lodash'; +import { ChannelCredentials } from "./channel-credentials"; +import { ChannelOptions } from "./channel"; + +export interface Serialize { + (value: T): Buffer; +} + +export interface Deserialize { + (bytes: Buffer): T; +} + +export interface MethodDefinition { + path: string; + requestStream: boolean; + responseStream: boolean; + requestSerialize: Serialize; + responseSerialize: Serialize; + requestDeserialize: Deserialize; + responseDeserialize: Deserialize; + originalName?: string; +} + +export interface ServiceDefinition { + [index: string]: MethodDefinition; +} + +export interface PackageDefinition { + [index: string]: ServiceDefinition; +} + +function getDefaultValues(metadata?: Metadata, options?: T): { + metadata: Metadata; + options: Partial; +} { + return { + metadata: metadata || new Metadata(), + options: options || {} + }; +} + +/** + * Map with short names for each of the requester maker functions. Used in + * makeClientConstructor + * @private + */ +const requesterFuncs = { + unary: Client.prototype.makeUnaryRequest, + server_stream: Client.prototype.makeServerStreamRequest, + client_stream: Client.prototype.makeClientStreamRequest, + bidi: Client.prototype.makeBidiStreamRequest +}; + +export interface ServiceClient extends Client { + [methodName: string]: Function; +} + +export interface ServiceClientConstructor { + new(address: string, credentials: ChannelCredentials, + options?: Partial): ServiceClient; + service: ServiceDefinition; +}; + +/** + * Creates a constructor for a client with the given methods, as specified in + * the methods argument. The resulting class will have an instance method for + * each method in the service, which is a partial application of one of the + * [Client]{@link grpc.Client} request methods, depending on `requestSerialize` + * and `responseSerialize`, with the `method`, `serialize`, and `deserialize` + * arguments predefined. + * @param methods An object mapping method names to + * method attributes + * @param serviceName The fully qualified name of the service + * @param classOptions An options object. + * @return New client constructor, which is a subclass of + * {@link grpc.Client}, and has the same arguments as that constructor. + */ +export function makeClientConstructor( + methods: ServiceDefinition, serviceName: string, + classOptions?: {}): ServiceClientConstructor { + if (!classOptions) { + classOptions = {}; + } + + class ServiceClientImpl extends Client implements ServiceClient { + static service: ServiceDefinition; + [methodName: string]: Function; + } + + _.each(methods, (attrs, name) => { + let methodType: keyof typeof requesterFuncs; + // TODO(murgatroid99): Verify that we don't need this anymore + if (_.startsWith(name, '$')) { + throw new Error('Method names cannot start with $'); + } + if (attrs.requestStream) { + if (attrs.responseStream) { + methodType = 'bidi'; + } else { + methodType = 'client_stream'; + } + } else { + if (attrs.responseStream) { + methodType = 'server_stream'; + } else { + methodType = 'unary'; + } + } + const serialize = attrs.requestSerialize; + const deserialize = attrs.responseDeserialize; + const methodFunc = _.partial(requesterFuncs[methodType], attrs.path, + serialize, deserialize); + ServiceClientImpl.prototype[name] = methodFunc; + // Associate all provided attributes with the method + _.assign(ServiceClientImpl.prototype[name], attrs); + if (attrs.originalName) { + ServiceClientImpl.prototype[attrs.originalName] = ServiceClientImpl.prototype[name]; + } + }); + + ServiceClientImpl.service = methods; + + return ServiceClientImpl; +}; + +export type GrpcObject = { + [index: string]: GrpcObject | ServiceClientConstructor; +}; + +/** + * Load a gRPC package definition as a gRPC object hierarchy. + * @param packageDef The package definition object. + * @return The resulting gRPC object. + */ +export function loadPackageDefinition(packageDef: PackageDefinition): GrpcObject { + const result: GrpcObject = {}; + for (const serviceFqn in packageDef) { + const service = packageDef[serviceFqn]; + const nameComponents = serviceFqn.split('.'); + const serviceName = nameComponents[nameComponents.length-1]; + let current = result; + for (const packageName of nameComponents.slice(0, -1)) { + if (!current[packageName]) { + current[packageName] = {}; + } + current = current[packageName] as GrpcObject; + } + current[serviceName] = makeClientConstructor(service, serviceName, {}); + } + return result; +} diff --git a/packages/grpc-js-core/src/metadata-status-filter.ts b/packages/grpc-js-core/src/metadata-status-filter.ts new file mode 100644 index 00000000..43d42ea6 --- /dev/null +++ b/packages/grpc-js-core/src/metadata-status-filter.ts @@ -0,0 +1,36 @@ +import {CallStream} from './call-stream'; +import {Channel} from './channel'; +import {BaseFilter, Filter, FilterFactory} from './filter'; +import {StatusObject} from './call-stream'; +import {Status} from './constants'; + +export class MetadataStatusFilter extends BaseFilter implements Filter { + async receiveTrailers(status: Promise): Promise { + let { code, details, metadata } = await status; + if (code !== Status.UNKNOWN) { + // we already have a known status, so don't assign a new one. + return { code, details, metadata }; + } + const metadataMap = metadata.getMap(); + if (typeof metadataMap['grpc-status'] === 'string') { + let receivedCode = Number(metadataMap['grpc-status']); + if (receivedCode in Status) { + code = receivedCode; + } + metadata.remove('grpc-status'); + } + if (typeof metadataMap['grpc-message'] === 'string') { + details = decodeURI(metadataMap['grpc-message'] as string); + metadata.remove('grpc-message'); + } + return { code, details, metadata }; + } +} + +export class MetadataStatusFilterFactory implements + FilterFactory { + constructor(private readonly channel: Channel) {} + createFilter(callStream: CallStream): MetadataStatusFilter { + return new MetadataStatusFilter(); + } +} diff --git a/packages/grpc-js-core/src/metadata.ts b/packages/grpc-js-core/src/metadata.ts index c4bb2568..84e9160e 100644 --- a/packages/grpc-js-core/src/metadata.ts +++ b/packages/grpc-js-core/src/metadata.ts @@ -26,7 +26,7 @@ function isLegalKey(key: string): boolean { } function isLegalNonBinaryValue(value: string): boolean { - return !!value.match(/^[ -~]+$/); + return !!value.match(/^[ -~]*$/); } function isBinaryKey(key: string): boolean { @@ -166,6 +166,7 @@ export class Metadata { * Creates an OutgoingHttpHeaders object that can be used with the http2 API. */ toHttp2Headers(): http2.OutgoingHttpHeaders { + // NOTE: Node <8.9 formats http2 headers incorrectly. const result: http2.OutgoingHttpHeaders = {}; forOwn(this.internalRepr, (values, key) => { // We assume that the user's interaction with this object is limited to @@ -180,6 +181,11 @@ export class Metadata { }); return result; } + + // For compatibility with the other Metadata implementation + private _getCoreRepresentation() { + return this.internalRepr; + } /** * Returns a new Metadata object based fields in a given IncomingHttpHeaders @@ -194,16 +200,18 @@ export class Metadata { values.forEach((value) => { result.add(key, Buffer.from(value, 'base64')); }); - } else { - result.add(key, Buffer.from(values, 'base64')); + } else if (values !== undefined) { + values.split(',').map(v => v.trim()).forEach(v => + result.add(key, Buffer.from(v, 'base64'))); } } else { if (Array.isArray(values)) { values.forEach((value) => { result.add(key, value); }); - } else { - result.add(key, values); + } else if (values !== undefined) { + values.split(',').map(v => v.trim()).forEach(v => + result.add(key, v)); } } }); diff --git a/packages/grpc-js-core/src/object-stream.ts b/packages/grpc-js-core/src/object-stream.ts index bed77f04..48988daa 100644 --- a/packages/grpc-js-core/src/object-stream.ts +++ b/packages/grpc-js-core/src/object-stream.ts @@ -1,28 +1,14 @@ import {Duplex, Readable, Writable} from 'stream'; +import {EmitterAugmentation1} from './events'; export interface IntermediateObjectReadable extends Readable { read(size?: number): any&T; } -export interface ObjectReadable extends IntermediateObjectReadable { +export type ObjectReadable = { read(size?: number): T; - - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; - - addListener(event: 'data', listener: (chunk: T) => void): this; - emit(event: 'data', chunk: T): boolean; - on(event: 'data', listener: (chunk: T) => void): this; - once(event: 'data', listener: (chunk: T) => void): this; - prependListener(event: 'data', listener: (chunk: T) => void): this; - prependOnceListener(event: 'data', listener: (chunk: T) => void): this; - removeListener(event: 'data', listener: (chunk: T) => void): this; -} +} & EmitterAugmentation1<'data', T> + & IntermediateObjectReadable; export interface IntermediateObjectWritable extends Writable { _write(chunk: any&T, encoding: string, callback: Function): void; @@ -44,8 +30,7 @@ export interface ObjectWritable extends IntermediateObjectWritable { end(chunk: T, encoding?: any, cb?: Function): void; } -export interface ObjectDuplex extends Duplex, ObjectWritable, - ObjectReadable { +export type ObjectDuplex = { read(size?: number): U; _write(chunk: T, encoding: string, callback: Function): void; @@ -54,13 +39,4 @@ export interface ObjectDuplex extends Duplex, ObjectWritable, end(): void; end(chunk: T, cb?: Function): void; end(chunk: T, encoding?: any, cb?: Function): void; - - - addListener(event: string, listener: Function): this; - emit(event: string|symbol, ...args: any[]): boolean; - on(event: string, listener: Function): this; - once(event: string, listener: Function): this; - prependListener(event: string, listener: Function): this; - prependOnceListener(event: string, listener: Function): this; - removeListener(event: string, listener: Function): this; -} +} & Duplex & ObjectWritable & ObjectReadable; diff --git a/packages/grpc-js-core/test/test-call-credentials.ts b/packages/grpc-js-core/test/test-call-credentials.ts index ab516ed9..64cfe140 100644 --- a/packages/grpc-js-core/test/test-call-credentials.ts +++ b/packages/grpc-js-core/test/test-call-credentials.ts @@ -5,18 +5,6 @@ import {Metadata} from '../src/metadata'; // Metadata generators -function makeGenerator(props: Array): CallMetadataGenerator { - return (options: {[propName: string]: string}, cb) => { - const metadata: Metadata = new Metadata(); - props.forEach((prop) => { - if (options[prop]) { - metadata.add(prop, options[prop]); - } - }); - cb(null, metadata); - }; -} - function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator { return (options, cb) => { const metadata = new Metadata(); @@ -25,7 +13,11 @@ function makeAfterMsElapsedGenerator(ms: number): CallMetadataGenerator { }; } -const generateFromName: CallMetadataGenerator = makeGenerator(['name']); +const generateFromServiceURL: CallMetadataGenerator = (options, cb) => { + const metadata: Metadata = new Metadata(); + metadata.add('service_url', options.service_url); + cb(null, metadata); +}; const generateWithError: CallMetadataGenerator = (options, cb) => cb(new Error()); @@ -35,16 +27,16 @@ describe('CallCredentials', () => { describe('createFromMetadataGenerator', () => { it('should accept a metadata generator', () => { assert.doesNotThrow( - () => CallCredentials.createFromMetadataGenerator(generateFromName)); + () => CallCredentials.createFromMetadataGenerator(generateFromServiceURL)); }); }); describe('compose', () => { it('should accept a CallCredentials object and return a new object', () => { const callCredentials1 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); const callCredentials2 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); const combinedCredentials = callCredentials1.compose(callCredentials2); assert.notEqual(combinedCredentials, callCredentials1); assert.notEqual(combinedCredentials, callCredentials2); @@ -52,9 +44,9 @@ describe('CallCredentials', () => { it('should be chainable', () => { const callCredentials1 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); const callCredentials2 = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); assert.doesNotThrow(() => { callCredentials1.compose(callCredentials2) .compose(callCredentials2) @@ -67,14 +59,14 @@ describe('CallCredentials', () => { it('should call the function passed to createFromMetadataGenerator', async () => { const callCredentials = - CallCredentials.createFromMetadataGenerator(generateFromName); + CallCredentials.createFromMetadataGenerator(generateFromServiceURL); let metadata: Metadata; try { - metadata = await callCredentials.generateMetadata({name: 'foo'}); + metadata = await callCredentials.generateMetadata({service_url: 'foo'}); } catch (err) { throw err; } - assert.deepEqual(metadata.get('name'), ['foo']); + assert.deepEqual(metadata.get('service_url'), ['foo']); }); it('should emit an error if the associated metadataGenerator does', @@ -83,7 +75,7 @@ describe('CallCredentials', () => { CallCredentials.createFromMetadataGenerator(generateWithError); let metadata: Metadata|null = null; try { - metadata = await callCredentials.generateMetadata({}); + metadata = await callCredentials.generateMetadata({service_url: ''}); } catch (err) { assert.ok(err instanceof Error); } @@ -115,13 +107,12 @@ describe('CallCredentials', () => { expected: ['150', '200', '50', '100'] } ]; - const options = {}; // Try each test case and make sure the msElapsed field is as expected await Promise.all(testCases.map(async (testCase) => { const {credentials, expected} = testCase; let metadata: Metadata; try { - metadata = await credentials.generateMetadata(options); + metadata = await credentials.generateMetadata({service_url: ''}); } catch (err) { throw err; } diff --git a/packages/grpc-js-core/test/test-call-stream.ts b/packages/grpc-js-core/test/test-call-stream.ts index e7126d62..fb8f5b02 100644 --- a/packages/grpc-js-core/test/test-call-stream.ts +++ b/packages/grpc-js-core/test/test-call-stream.ts @@ -38,11 +38,14 @@ class ClientHttp2StreamMock extends stream.Duplex implements http2.ClientHttp2St } bytesRead = 0; dataFrame = 0; - aborted: boolean; - destroyed: boolean; - rstCode: number; - session: http2.Http2Session; - state: http2.StreamState; + aborted: boolean = false; + closed: boolean = false; + destroyed: boolean = false; + pending: boolean = false; + rstCode: number = 0; + session: http2.Http2Session = {} as any; + state: http2.StreamState = {} as any; + close = mockFunction; priority = mockFunction; rstStream = mockFunction; rstWithNoError = mockFunction; @@ -100,7 +103,7 @@ describe('CallStream', () => { assert2.afterMustCallsSatisfied(done); }); - it('should end a call with an error if a stream was closed', (done) => { + describe('should end a call with an error if a stream was closed', () => { const c = http2.constants; const s = Status; const errorCodeMapping = { @@ -118,21 +121,31 @@ describe('CallStream', () => { [c.NGHTTP2_ENHANCE_YOUR_CALM]: s.RESOURCE_EXHAUSTED, [c.NGHTTP2_INADEQUATE_SECURITY]: s.PERMISSION_DENIED }; - forOwn(errorCodeMapping, (value: Status | null, key) => { - const callStream = new Http2CallStream('foo', callStreamArgs, filterStackFactory); - const http2Stream = new ClientHttp2StreamMock({ - payload: Buffer.alloc(0), - frameLengths: [] + const keys = Object.keys(errorCodeMapping).map(key => Number(key)); + keys.forEach((key) => { + const value = errorCodeMapping[key]; + // A null value indicates: behavior isn't specified, so skip this test. + let maybeSkip = (fn: typeof it) => value ? fn : fn.skip; + maybeSkip(it)(`for error code ${key}`, () => { + return new Promise((resolve, reject) => { + const callStream = new Http2CallStream('foo', callStreamArgs, filterStackFactory); + const http2Stream = new ClientHttp2StreamMock({ + payload: Buffer.alloc(0), + frameLengths: [] + }); + callStream.attachHttp2Stream(http2Stream); + callStream.once('status', (status) => { + try { + assert.strictEqual(status.code, value); + resolve(); + } catch (e) { + reject(e); + } + }); + http2Stream.emit('close', Number(key)); + }); }); - callStream.attachHttp2Stream(http2Stream); - if (value !== null) { - callStream.once('status', assert2.mustCall((status) => { - assert.strictEqual(status.code, value); - })); - } - http2Stream.emit('streamClosed', Number(key)); }); - assert2.afterMustCallsSatisfied(done); }); it('should have functioning getters', (done) => { diff --git a/packages/grpc-js-core/test/test-channel-credentials.ts b/packages/grpc-js-core/test/test-channel-credentials.ts index fac88110..15dfbfac 100644 --- a/packages/grpc-js-core/test/test-channel-credentials.ts +++ b/packages/grpc-js-core/test/test-channel-credentials.ts @@ -8,7 +8,7 @@ import {ChannelCredentials} from '../src/channel-credentials'; import {assert2, mockFunction} from './common'; class CallCredentialsMock implements CallCredentials { - child: CallCredentialsMock; + child: CallCredentialsMock|null = null; constructor(child?: CallCredentialsMock) { if (child) { this.child = child; diff --git a/packages/grpc-js-core/tsconfig.json b/packages/grpc-js-core/tsconfig.json index 62024719..c47ad1de 100644 --- a/packages/grpc-js-core/tsconfig.json +++ b/packages/grpc-js-core/tsconfig.json @@ -1,13 +1,13 @@ { - "extends": "./node_modules/google-ts-style/tsconfig-google.json", + "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { - "lib": [ "es6" ], - "typeRoots": [ - "node_modules/h2-types", "node_modules/@types" - ] + "rootDir": ".", + "outDir": "build" }, "include": [ + "src/*.ts", "src/**/*.ts", + "test/*.ts", "test/**/*.ts" ], "exclude": [ diff --git a/packages/grpc-js/gulpfile.ts b/packages/grpc-js/gulpfile.ts new file mode 100644 index 00000000..7e09725c --- /dev/null +++ b/packages/grpc-js/gulpfile.ts @@ -0,0 +1,53 @@ +/* + * 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'); + +// gulp-help monkeypatches tasks to have an additional description parameter +const gulp = help(_gulp); + +const execa = require('execa'); +const path = require('path'); +const del = require('del'); +const linkSync = require('../../util').linkSync; + +const jsDir = __dirname; + +const execNpmVerb = (verb: string, ...args: string[]) => + execa('npm', [verb, ...args], {cwd: jsDir, stdio: 'inherit'}); +const execNpmCommand = execNpmVerb.bind(null, 'run'); + +gulp.task('clean.links', 'Delete npm links', () => { + return del([path.resolve(jsDir, 'node_modules/@grpc/js-core'), + path.resolve(jsDir, 'node_modules/@grpc/surface')]); +}); + +gulp.task('clean.all', 'Delete all files created by tasks', ['clean.links']); + +/** + * Transpiles TypeScript files in src/ to JavaScript according to the settings + * found in tsconfig.json. + */ +gulp.task('compile', 'Transpiles src/.', () => execNpmCommand('compile')); + +gulp.task('install', 'Install dependencies', () => execNpmVerb('install')); + +gulp.task('link.add', 'Link local copies of dependencies', () => { + linkSync(jsDir, './node_modules/@grpc/js-core', '../grpc-js-core'); + linkSync(jsDir, './node_modules/@grpc/surface', '../grpc-surface'); +}); diff --git a/packages/grpc-js/package.json b/packages/grpc-js/package.json new file mode 100644 index 00000000..8486c043 --- /dev/null +++ b/packages/grpc-js/package.json @@ -0,0 +1,34 @@ +{ + "name": "@grpc/js", + "version": "1.0.0", + "description": "", + "main": "build/src/index.js", + "scripts": { + "compile": "tsc -p .", + "test": "echo \"Error: no test specified\" && exit 1", + "check": "gts check", + "clean": "gts clean", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run check" + }, + "repository": { + "type": "git", + "url": "https://github.com/grpc/grpc-node.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/grpc/grpc-node/issues" + }, + "homepage": "https://grpc.io", + "dependencies": { + "@grpc/js-core": "^0.1.0", + "@grpc/surface": "^0.1.0" + }, + "devDependencies": { + "gts": "^0.5.1", + "typescript": "~2.7.0" + } +} diff --git a/packages/grpc-native-core/test/math/node_modules/grpc.js b/packages/grpc-js/src/index.ts similarity index 75% rename from packages/grpc-native-core/test/math/node_modules/grpc.js rename to packages/grpc-js/src/index.ts index b824d8dd..47b866a4 100644 --- a/packages/grpc-native-core/test/math/node_modules/grpc.js +++ b/packages/grpc-js/src/index.ts @@ -1,6 +1,6 @@ -/* - * - * Copyright 2016 gRPC authors. +/** + * @license + * 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. @@ -16,7 +16,6 @@ * */ -/* This exists solely to allow the generated code to import the grpc module - * without using a relative path */ +'use strict'; -module.exports = require('../../..'); +module.exports = require('@grpc/surface')(require('@grpc/js-core')); diff --git a/packages/grpc-js/tsconfig.json b/packages/grpc-js/tsconfig.json new file mode 100644 index 00000000..c47ad1de --- /dev/null +++ b/packages/grpc-js/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/*.ts", + "src/**/*.ts", + "test/*.ts", + "test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/grpc-native-core/.npmignore b/packages/grpc-native-core/.npmignore new file mode 100644 index 00000000..e66c5e92 --- /dev/null +++ b/packages/grpc-native-core/.npmignore @@ -0,0 +1 @@ +**/BUILD \ No newline at end of file diff --git a/packages/grpc-native-core/binding.gyp b/packages/grpc-native-core/binding.gyp index 3e8153f5..aa4d4be6 100644 --- a/packages/grpc-native-core/binding.gyp +++ b/packages/grpc-native-core/binding.gyp @@ -63,6 +63,7 @@ '-Wno-long-long', '-Wno-unused-parameter', '-DOSATOMIC_USE_INLINED=1', + '-Wno-deprecated-declarations', ], 'ldflags': [ '-g', @@ -77,9 +78,11 @@ ], 'include_dirs': [ 'deps/grpc', - 'deps/grpc/include' + 'deps/grpc/include', + 'deps/grpc/third_party/abseil-cpp' ], 'defines': [ + 'PB_FIELD_16BIT', 'GPR_BACKWARDS_COMPATIBILITY_MODE', 'GRPC_ARES=0', 'GRPC_UV' @@ -101,6 +104,7 @@ '-fprofile-arcs', '-ftest-coverage', '-rdynamic', + '-lstdc++', ], }], ['grpc_alpine=="true"', { @@ -183,6 +187,7 @@ '-Wno-long-long', '-Wno-unused-parameter', '-DOSATOMIC_USE_INLINED=1', + '-Wno-deprecated-declarations', ], 'OTHER_CPLUSPLUSFLAGS': [ '-g', @@ -192,6 +197,7 @@ '-Wno-long-long', '-Wno-unused-parameter', '-DOSATOMIC_USE_INLINED=1', + '-Wno-deprecated-declarations', '-stdlib=libc++', '-std=c++11', '-Wno-error=deprecated-declarations' @@ -207,13 +213,13 @@ 'target_name': 'boringssl', 'product_prefix': 'lib', 'type': 'static_library', + 'cflags': [ + '-Wno-implicit-fallthrough' + ], 'dependencies': [ ], 'sources': [ 'deps/grpc/src/boringssl/err_data.c', - 'deps/grpc/third_party/boringssl/crypto/aes/aes.c', - 'deps/grpc/third_party/boringssl/crypto/aes/key_wrap.c', - 'deps/grpc/third_party/boringssl/crypto/aes/mode_wrappers.c', 'deps/grpc/third_party/boringssl/crypto/asn1/a_bitstr.c', 'deps/grpc/third_party/boringssl/crypto/asn1/a_bool.c', 'deps/grpc/third_party/boringssl/crypto/asn1/a_d2i_fp.c', @@ -237,7 +243,6 @@ 'deps/grpc/third_party/boringssl/crypto/asn1/f_enum.c', 'deps/grpc/third_party/boringssl/crypto/asn1/f_int.c', 'deps/grpc/third_party/boringssl/crypto/asn1/f_string.c', - 'deps/grpc/third_party/boringssl/crypto/asn1/t_bitst.c', 'deps/grpc/third_party/boringssl/crypto/asn1/tasn_dec.c', 'deps/grpc/third_party/boringssl/crypto/asn1/tasn_enc.c', 'deps/grpc/third_party/boringssl/crypto/asn1/tasn_fre.c', @@ -245,8 +250,6 @@ 'deps/grpc/third_party/boringssl/crypto/asn1/tasn_typ.c', 'deps/grpc/third_party/boringssl/crypto/asn1/tasn_utl.c', 'deps/grpc/third_party/boringssl/crypto/asn1/time_support.c', - 'deps/grpc/third_party/boringssl/crypto/asn1/x_bignum.c', - 'deps/grpc/third_party/boringssl/crypto/asn1/x_long.c', 'deps/grpc/third_party/boringssl/crypto/base64/base64.c', 'deps/grpc/third_party/boringssl/crypto/bio/bio.c', 'deps/grpc/third_party/boringssl/crypto/bio/bio_mem.c', @@ -258,44 +261,25 @@ 'deps/grpc/third_party/boringssl/crypto/bio/printf.c', 'deps/grpc/third_party/boringssl/crypto/bio/socket.c', 'deps/grpc/third_party/boringssl/crypto/bio/socket_helper.c', - 'deps/grpc/third_party/boringssl/crypto/bn/add.c', - 'deps/grpc/third_party/boringssl/crypto/bn/asm/x86_64-gcc.c', - 'deps/grpc/third_party/boringssl/crypto/bn/bn.c', - 'deps/grpc/third_party/boringssl/crypto/bn/bn_asn1.c', - 'deps/grpc/third_party/boringssl/crypto/bn/cmp.c', - 'deps/grpc/third_party/boringssl/crypto/bn/convert.c', - 'deps/grpc/third_party/boringssl/crypto/bn/ctx.c', - 'deps/grpc/third_party/boringssl/crypto/bn/div.c', - 'deps/grpc/third_party/boringssl/crypto/bn/exponentiation.c', - 'deps/grpc/third_party/boringssl/crypto/bn/gcd.c', - 'deps/grpc/third_party/boringssl/crypto/bn/generic.c', - 'deps/grpc/third_party/boringssl/crypto/bn/kronecker.c', - 'deps/grpc/third_party/boringssl/crypto/bn/montgomery.c', - 'deps/grpc/third_party/boringssl/crypto/bn/montgomery_inv.c', - 'deps/grpc/third_party/boringssl/crypto/bn/mul.c', - 'deps/grpc/third_party/boringssl/crypto/bn/prime.c', - 'deps/grpc/third_party/boringssl/crypto/bn/random.c', - 'deps/grpc/third_party/boringssl/crypto/bn/rsaz_exp.c', - 'deps/grpc/third_party/boringssl/crypto/bn/shift.c', - 'deps/grpc/third_party/boringssl/crypto/bn/sqrt.c', + 'deps/grpc/third_party/boringssl/crypto/bn_extra/bn_asn1.c', + 'deps/grpc/third_party/boringssl/crypto/bn_extra/convert.c', 'deps/grpc/third_party/boringssl/crypto/buf/buf.c', 'deps/grpc/third_party/boringssl/crypto/bytestring/asn1_compat.c', 'deps/grpc/third_party/boringssl/crypto/bytestring/ber.c', 'deps/grpc/third_party/boringssl/crypto/bytestring/cbb.c', 'deps/grpc/third_party/boringssl/crypto/bytestring/cbs.c', 'deps/grpc/third_party/boringssl/crypto/chacha/chacha.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/aead.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/cipher.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/derive_key.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_aes.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_chacha20poly1305.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_des.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_null.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_rc2.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_rc4.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_ssl3.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/e_tls.c', - 'deps/grpc/third_party/boringssl/crypto/cipher/tls_cbc.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/cipher_extra.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/derive_key.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_aesctrhmac.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_aesgcmsiv.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_chacha20poly1305.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_null.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_rc2.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_rc4.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_ssl3.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/e_tls.c', + 'deps/grpc/third_party/boringssl/crypto/cipher_extra/tls_cbc.c', 'deps/grpc/third_party/boringssl/crypto/cmac/cmac.c', 'deps/grpc/third_party/boringssl/crypto/conf/conf.c', 'deps/grpc/third_party/boringssl/crypto/cpu-aarch64-linux.c', @@ -307,29 +291,16 @@ 'deps/grpc/third_party/boringssl/crypto/curve25519/curve25519.c', 'deps/grpc/third_party/boringssl/crypto/curve25519/spake25519.c', 'deps/grpc/third_party/boringssl/crypto/curve25519/x25519-x86_64.c', - 'deps/grpc/third_party/boringssl/crypto/des/des.c', 'deps/grpc/third_party/boringssl/crypto/dh/check.c', 'deps/grpc/third_party/boringssl/crypto/dh/dh.c', 'deps/grpc/third_party/boringssl/crypto/dh/dh_asn1.c', 'deps/grpc/third_party/boringssl/crypto/dh/params.c', - 'deps/grpc/third_party/boringssl/crypto/digest/digest.c', - 'deps/grpc/third_party/boringssl/crypto/digest/digests.c', + 'deps/grpc/third_party/boringssl/crypto/digest_extra/digest_extra.c', 'deps/grpc/third_party/boringssl/crypto/dsa/dsa.c', 'deps/grpc/third_party/boringssl/crypto/dsa/dsa_asn1.c', - 'deps/grpc/third_party/boringssl/crypto/ec/ec.c', - 'deps/grpc/third_party/boringssl/crypto/ec/ec_asn1.c', - 'deps/grpc/third_party/boringssl/crypto/ec/ec_key.c', - 'deps/grpc/third_party/boringssl/crypto/ec/ec_montgomery.c', - 'deps/grpc/third_party/boringssl/crypto/ec/oct.c', - 'deps/grpc/third_party/boringssl/crypto/ec/p224-64.c', - 'deps/grpc/third_party/boringssl/crypto/ec/p256-64.c', - 'deps/grpc/third_party/boringssl/crypto/ec/p256-x86_64.c', - 'deps/grpc/third_party/boringssl/crypto/ec/simple.c', - 'deps/grpc/third_party/boringssl/crypto/ec/util-64.c', - 'deps/grpc/third_party/boringssl/crypto/ec/wnaf.c', + 'deps/grpc/third_party/boringssl/crypto/ec_extra/ec_asn1.c', 'deps/grpc/third_party/boringssl/crypto/ecdh/ecdh.c', - 'deps/grpc/third_party/boringssl/crypto/ecdsa/ecdsa.c', - 'deps/grpc/third_party/boringssl/crypto/ecdsa/ecdsa_asn1.c', + 'deps/grpc/third_party/boringssl/crypto/ecdsa_extra/ecdsa_asn1.c', 'deps/grpc/third_party/boringssl/crypto/engine/engine.c', 'deps/grpc/third_party/boringssl/crypto/err/err.c', 'deps/grpc/third_party/boringssl/crypto/evp/digestsign.c', @@ -339,24 +310,20 @@ 'deps/grpc/third_party/boringssl/crypto/evp/p_dsa_asn1.c', 'deps/grpc/third_party/boringssl/crypto/evp/p_ec.c', 'deps/grpc/third_party/boringssl/crypto/evp/p_ec_asn1.c', + 'deps/grpc/third_party/boringssl/crypto/evp/p_ed25519.c', + 'deps/grpc/third_party/boringssl/crypto/evp/p_ed25519_asn1.c', 'deps/grpc/third_party/boringssl/crypto/evp/p_rsa.c', 'deps/grpc/third_party/boringssl/crypto/evp/p_rsa_asn1.c', 'deps/grpc/third_party/boringssl/crypto/evp/pbkdf.c', 'deps/grpc/third_party/boringssl/crypto/evp/print.c', + 'deps/grpc/third_party/boringssl/crypto/evp/scrypt.c', 'deps/grpc/third_party/boringssl/crypto/evp/sign.c', 'deps/grpc/third_party/boringssl/crypto/ex_data.c', + 'deps/grpc/third_party/boringssl/crypto/fipsmodule/bcm.c', + 'deps/grpc/third_party/boringssl/crypto/fipsmodule/is_fips.c', 'deps/grpc/third_party/boringssl/crypto/hkdf/hkdf.c', - 'deps/grpc/third_party/boringssl/crypto/hmac/hmac.c', 'deps/grpc/third_party/boringssl/crypto/lhash/lhash.c', - 'deps/grpc/third_party/boringssl/crypto/md4/md4.c', - 'deps/grpc/third_party/boringssl/crypto/md5/md5.c', 'deps/grpc/third_party/boringssl/crypto/mem.c', - 'deps/grpc/third_party/boringssl/crypto/modes/cbc.c', - 'deps/grpc/third_party/boringssl/crypto/modes/cfb.c', - 'deps/grpc/third_party/boringssl/crypto/modes/ctr.c', - 'deps/grpc/third_party/boringssl/crypto/modes/gcm.c', - 'deps/grpc/third_party/boringssl/crypto/modes/ofb.c', - 'deps/grpc/third_party/boringssl/crypto/modes/polyval.c', 'deps/grpc/third_party/boringssl/crypto/obj/obj.c', 'deps/grpc/third_party/boringssl/crypto/obj/obj_xref.c', 'deps/grpc/third_party/boringssl/crypto/pem/pem_all.c', @@ -367,30 +334,24 @@ 'deps/grpc/third_party/boringssl/crypto/pem/pem_pkey.c', 'deps/grpc/third_party/boringssl/crypto/pem/pem_x509.c', 'deps/grpc/third_party/boringssl/crypto/pem/pem_xaux.c', + 'deps/grpc/third_party/boringssl/crypto/pkcs7/pkcs7.c', + 'deps/grpc/third_party/boringssl/crypto/pkcs7/pkcs7_x509.c', 'deps/grpc/third_party/boringssl/crypto/pkcs8/p5_pbev2.c', - 'deps/grpc/third_party/boringssl/crypto/pkcs8/p8_pkey.c', 'deps/grpc/third_party/boringssl/crypto/pkcs8/pkcs8.c', + 'deps/grpc/third_party/boringssl/crypto/pkcs8/pkcs8_x509.c', 'deps/grpc/third_party/boringssl/crypto/poly1305/poly1305.c', 'deps/grpc/third_party/boringssl/crypto/poly1305/poly1305_arm.c', 'deps/grpc/third_party/boringssl/crypto/poly1305/poly1305_vec.c', 'deps/grpc/third_party/boringssl/crypto/pool/pool.c', - 'deps/grpc/third_party/boringssl/crypto/rand/deterministic.c', - 'deps/grpc/third_party/boringssl/crypto/rand/fuchsia.c', - 'deps/grpc/third_party/boringssl/crypto/rand/rand.c', - 'deps/grpc/third_party/boringssl/crypto/rand/urandom.c', - 'deps/grpc/third_party/boringssl/crypto/rand/windows.c', + 'deps/grpc/third_party/boringssl/crypto/rand_extra/deterministic.c', + 'deps/grpc/third_party/boringssl/crypto/rand_extra/forkunsafe.c', + 'deps/grpc/third_party/boringssl/crypto/rand_extra/fuchsia.c', + 'deps/grpc/third_party/boringssl/crypto/rand_extra/rand_extra.c', + 'deps/grpc/third_party/boringssl/crypto/rand_extra/windows.c', 'deps/grpc/third_party/boringssl/crypto/rc4/rc4.c', 'deps/grpc/third_party/boringssl/crypto/refcount_c11.c', 'deps/grpc/third_party/boringssl/crypto/refcount_lock.c', - 'deps/grpc/third_party/boringssl/crypto/rsa/blinding.c', - 'deps/grpc/third_party/boringssl/crypto/rsa/padding.c', - 'deps/grpc/third_party/boringssl/crypto/rsa/rsa.c', - 'deps/grpc/third_party/boringssl/crypto/rsa/rsa_asn1.c', - 'deps/grpc/third_party/boringssl/crypto/rsa/rsa_impl.c', - 'deps/grpc/third_party/boringssl/crypto/sha/sha1-altivec.c', - 'deps/grpc/third_party/boringssl/crypto/sha/sha1.c', - 'deps/grpc/third_party/boringssl/crypto/sha/sha256.c', - 'deps/grpc/third_party/boringssl/crypto/sha/sha512.c', + 'deps/grpc/third_party/boringssl/crypto/rsa_extra/rsa_asn1.c', 'deps/grpc/third_party/boringssl/crypto/stack/stack.c', 'deps/grpc/third_party/boringssl/crypto/thread.c', 'deps/grpc/third_party/boringssl/crypto/thread_none.c', @@ -405,7 +366,6 @@ 'deps/grpc/third_party/boringssl/crypto/x509/by_dir.c', 'deps/grpc/third_party/boringssl/crypto/x509/by_file.c', 'deps/grpc/third_party/boringssl/crypto/x509/i2d_pr.c', - 'deps/grpc/third_party/boringssl/crypto/x509/pkcs7.c', 'deps/grpc/third_party/boringssl/crypto/x509/rsa_pss.c', 'deps/grpc/third_party/boringssl/crypto/x509/t_crl.c', 'deps/grpc/third_party/boringssl/crypto/x509/t_req.c', @@ -431,7 +391,6 @@ 'deps/grpc/third_party/boringssl/crypto/x509/x509name.c', 'deps/grpc/third_party/boringssl/crypto/x509/x509rset.c', 'deps/grpc/third_party/boringssl/crypto/x509/x509spki.c', - 'deps/grpc/third_party/boringssl/crypto/x509/x509type.c', 'deps/grpc/third_party/boringssl/crypto/x509/x_algor.c', 'deps/grpc/third_party/boringssl/crypto/x509/x_all.c', 'deps/grpc/third_party/boringssl/crypto/x509/x_attrib.c', @@ -479,41 +438,42 @@ 'deps/grpc/third_party/boringssl/crypto/x509v3/v3_skey.c', 'deps/grpc/third_party/boringssl/crypto/x509v3/v3_sxnet.c', 'deps/grpc/third_party/boringssl/crypto/x509v3/v3_utl.c', - 'deps/grpc/third_party/boringssl/ssl/bio_ssl.c', - 'deps/grpc/third_party/boringssl/ssl/custom_extensions.c', - 'deps/grpc/third_party/boringssl/ssl/d1_both.c', - 'deps/grpc/third_party/boringssl/ssl/d1_lib.c', - 'deps/grpc/third_party/boringssl/ssl/d1_pkt.c', - 'deps/grpc/third_party/boringssl/ssl/d1_srtp.c', - 'deps/grpc/third_party/boringssl/ssl/dtls_method.c', - 'deps/grpc/third_party/boringssl/ssl/dtls_record.c', - 'deps/grpc/third_party/boringssl/ssl/handshake_client.c', - 'deps/grpc/third_party/boringssl/ssl/handshake_server.c', - 'deps/grpc/third_party/boringssl/ssl/s3_both.c', - 'deps/grpc/third_party/boringssl/ssl/s3_lib.c', - 'deps/grpc/third_party/boringssl/ssl/s3_pkt.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_aead_ctx.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_asn1.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_buffer.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_cert.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_cipher.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_ecdh.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_file.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_lib.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_privkey.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_privkey_cc.cc', - 'deps/grpc/third_party/boringssl/ssl/ssl_session.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_stat.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_transcript.c', - 'deps/grpc/third_party/boringssl/ssl/ssl_x509.c', - 'deps/grpc/third_party/boringssl/ssl/t1_enc.c', - 'deps/grpc/third_party/boringssl/ssl/t1_lib.c', - 'deps/grpc/third_party/boringssl/ssl/tls13_both.c', - 'deps/grpc/third_party/boringssl/ssl/tls13_client.c', - 'deps/grpc/third_party/boringssl/ssl/tls13_enc.c', - 'deps/grpc/third_party/boringssl/ssl/tls13_server.c', - 'deps/grpc/third_party/boringssl/ssl/tls_method.c', - 'deps/grpc/third_party/boringssl/ssl/tls_record.c', + 'deps/grpc/third_party/boringssl/ssl/bio_ssl.cc', + 'deps/grpc/third_party/boringssl/ssl/custom_extensions.cc', + 'deps/grpc/third_party/boringssl/ssl/d1_both.cc', + 'deps/grpc/third_party/boringssl/ssl/d1_lib.cc', + 'deps/grpc/third_party/boringssl/ssl/d1_pkt.cc', + 'deps/grpc/third_party/boringssl/ssl/d1_srtp.cc', + 'deps/grpc/third_party/boringssl/ssl/dtls_method.cc', + 'deps/grpc/third_party/boringssl/ssl/dtls_record.cc', + 'deps/grpc/third_party/boringssl/ssl/handshake.cc', + 'deps/grpc/third_party/boringssl/ssl/handshake_client.cc', + 'deps/grpc/third_party/boringssl/ssl/handshake_server.cc', + 'deps/grpc/third_party/boringssl/ssl/s3_both.cc', + 'deps/grpc/third_party/boringssl/ssl/s3_lib.cc', + 'deps/grpc/third_party/boringssl/ssl/s3_pkt.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_aead_ctx.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_asn1.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_buffer.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_cert.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_cipher.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_file.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_key_share.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_lib.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_privkey.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_session.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_stat.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_transcript.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_versions.cc', + 'deps/grpc/third_party/boringssl/ssl/ssl_x509.cc', + 'deps/grpc/third_party/boringssl/ssl/t1_enc.cc', + 'deps/grpc/third_party/boringssl/ssl/t1_lib.cc', + 'deps/grpc/third_party/boringssl/ssl/tls13_both.cc', + 'deps/grpc/third_party/boringssl/ssl/tls13_client.cc', + 'deps/grpc/third_party/boringssl/ssl/tls13_enc.cc', + 'deps/grpc/third_party/boringssl/ssl/tls13_server.cc', + 'deps/grpc/third_party/boringssl/ssl/tls_method.cc', + 'deps/grpc/third_party/boringssl/ssl/tls_record.cc', ], 'conditions': [ ['OS == "mac"', { @@ -593,52 +553,46 @@ 'dependencies': [ ], 'sources': [ - 'deps/grpc/src/core/lib/profiling/basic_timers.c', - 'deps/grpc/src/core/lib/profiling/stap_timers.c', - 'deps/grpc/src/core/lib/support/alloc.c', - 'deps/grpc/src/core/lib/support/arena.c', - 'deps/grpc/src/core/lib/support/atm.c', - 'deps/grpc/src/core/lib/support/avl.c', - 'deps/grpc/src/core/lib/support/backoff.c', - 'deps/grpc/src/core/lib/support/cmdline.c', - 'deps/grpc/src/core/lib/support/cpu_iphone.c', - 'deps/grpc/src/core/lib/support/cpu_linux.c', - 'deps/grpc/src/core/lib/support/cpu_posix.c', - 'deps/grpc/src/core/lib/support/cpu_windows.c', - 'deps/grpc/src/core/lib/support/env_linux.c', - 'deps/grpc/src/core/lib/support/env_posix.c', - 'deps/grpc/src/core/lib/support/env_windows.c', - 'deps/grpc/src/core/lib/support/histogram.c', - 'deps/grpc/src/core/lib/support/host_port.c', - 'deps/grpc/src/core/lib/support/log.c', - 'deps/grpc/src/core/lib/support/log_android.c', - 'deps/grpc/src/core/lib/support/log_linux.c', - 'deps/grpc/src/core/lib/support/log_posix.c', - 'deps/grpc/src/core/lib/support/log_windows.c', - 'deps/grpc/src/core/lib/support/mpscq.c', - 'deps/grpc/src/core/lib/support/murmur_hash.c', - 'deps/grpc/src/core/lib/support/stack_lockfree.c', - 'deps/grpc/src/core/lib/support/string.c', - 'deps/grpc/src/core/lib/support/string_posix.c', - 'deps/grpc/src/core/lib/support/string_util_windows.c', - 'deps/grpc/src/core/lib/support/string_windows.c', - 'deps/grpc/src/core/lib/support/subprocess_posix.c', - 'deps/grpc/src/core/lib/support/subprocess_windows.c', - 'deps/grpc/src/core/lib/support/sync.c', - 'deps/grpc/src/core/lib/support/sync_posix.c', - 'deps/grpc/src/core/lib/support/sync_windows.c', - 'deps/grpc/src/core/lib/support/thd.c', - 'deps/grpc/src/core/lib/support/thd_posix.c', - 'deps/grpc/src/core/lib/support/thd_windows.c', - 'deps/grpc/src/core/lib/support/time.c', - 'deps/grpc/src/core/lib/support/time_posix.c', - 'deps/grpc/src/core/lib/support/time_precise.c', - 'deps/grpc/src/core/lib/support/time_windows.c', - 'deps/grpc/src/core/lib/support/tls_pthread.c', - 'deps/grpc/src/core/lib/support/tmpfile_msys.c', - 'deps/grpc/src/core/lib/support/tmpfile_posix.c', - 'deps/grpc/src/core/lib/support/tmpfile_windows.c', - 'deps/grpc/src/core/lib/support/wrap_memcpy.c', + 'deps/grpc/src/core/lib/gpr/alloc.cc', + 'deps/grpc/src/core/lib/gpr/arena.cc', + 'deps/grpc/src/core/lib/gpr/atm.cc', + 'deps/grpc/src/core/lib/gpr/cpu_iphone.cc', + 'deps/grpc/src/core/lib/gpr/cpu_linux.cc', + 'deps/grpc/src/core/lib/gpr/cpu_posix.cc', + 'deps/grpc/src/core/lib/gpr/cpu_windows.cc', + 'deps/grpc/src/core/lib/gpr/env_linux.cc', + 'deps/grpc/src/core/lib/gpr/env_posix.cc', + 'deps/grpc/src/core/lib/gpr/env_windows.cc', + 'deps/grpc/src/core/lib/gpr/fork.cc', + 'deps/grpc/src/core/lib/gpr/host_port.cc', + 'deps/grpc/src/core/lib/gpr/log.cc', + 'deps/grpc/src/core/lib/gpr/log_android.cc', + 'deps/grpc/src/core/lib/gpr/log_linux.cc', + 'deps/grpc/src/core/lib/gpr/log_posix.cc', + 'deps/grpc/src/core/lib/gpr/log_windows.cc', + 'deps/grpc/src/core/lib/gpr/mpscq.cc', + 'deps/grpc/src/core/lib/gpr/murmur_hash.cc', + 'deps/grpc/src/core/lib/gpr/string.cc', + 'deps/grpc/src/core/lib/gpr/string_posix.cc', + 'deps/grpc/src/core/lib/gpr/string_util_windows.cc', + 'deps/grpc/src/core/lib/gpr/string_windows.cc', + 'deps/grpc/src/core/lib/gpr/sync.cc', + 'deps/grpc/src/core/lib/gpr/sync_posix.cc', + 'deps/grpc/src/core/lib/gpr/sync_windows.cc', + 'deps/grpc/src/core/lib/gpr/thd.cc', + 'deps/grpc/src/core/lib/gpr/thd_posix.cc', + 'deps/grpc/src/core/lib/gpr/thd_windows.cc', + 'deps/grpc/src/core/lib/gpr/time.cc', + 'deps/grpc/src/core/lib/gpr/time_posix.cc', + 'deps/grpc/src/core/lib/gpr/time_precise.cc', + 'deps/grpc/src/core/lib/gpr/time_windows.cc', + 'deps/grpc/src/core/lib/gpr/tls_pthread.cc', + 'deps/grpc/src/core/lib/gpr/tmpfile_msys.cc', + 'deps/grpc/src/core/lib/gpr/tmpfile_posix.cc', + 'deps/grpc/src/core/lib/gpr/tmpfile_windows.cc', + 'deps/grpc/src/core/lib/gpr/wrap_memcpy.cc', + 'deps/grpc/src/core/lib/profiling/basic_timers.cc', + 'deps/grpc/src/core/lib/profiling/stap_timers.cc', ], 'conditions': [ ['OS == "mac"', { @@ -656,265 +610,257 @@ 'gpr', ], 'sources': [ - 'deps/grpc/src/core/lib/surface/init.c', - 'deps/grpc/src/core/lib/channel/channel_args.c', - 'deps/grpc/src/core/lib/channel/channel_stack.c', - 'deps/grpc/src/core/lib/channel/channel_stack_builder.c', - 'deps/grpc/src/core/lib/channel/connected_channel.c', - 'deps/grpc/src/core/lib/channel/handshaker.c', - 'deps/grpc/src/core/lib/channel/handshaker_factory.c', - 'deps/grpc/src/core/lib/channel/handshaker_registry.c', - 'deps/grpc/src/core/lib/compression/compression.c', - 'deps/grpc/src/core/lib/compression/message_compress.c', - 'deps/grpc/src/core/lib/compression/stream_compression.c', - 'deps/grpc/src/core/lib/debug/stats.c', - 'deps/grpc/src/core/lib/debug/stats_data.c', - 'deps/grpc/src/core/lib/http/format_request.c', - 'deps/grpc/src/core/lib/http/httpcli.c', - 'deps/grpc/src/core/lib/http/parser.c', - 'deps/grpc/src/core/lib/iomgr/call_combiner.c', - 'deps/grpc/src/core/lib/iomgr/closure.c', - 'deps/grpc/src/core/lib/iomgr/combiner.c', - 'deps/grpc/src/core/lib/iomgr/endpoint.c', - 'deps/grpc/src/core/lib/iomgr/endpoint_pair_posix.c', - 'deps/grpc/src/core/lib/iomgr/endpoint_pair_uv.c', - 'deps/grpc/src/core/lib/iomgr/endpoint_pair_windows.c', - 'deps/grpc/src/core/lib/iomgr/error.c', - 'deps/grpc/src/core/lib/iomgr/ev_epoll1_linux.c', - 'deps/grpc/src/core/lib/iomgr/ev_epollex_linux.c', - 'deps/grpc/src/core/lib/iomgr/ev_epollsig_linux.c', - 'deps/grpc/src/core/lib/iomgr/ev_poll_posix.c', - 'deps/grpc/src/core/lib/iomgr/ev_posix.c', - 'deps/grpc/src/core/lib/iomgr/ev_windows.c', - 'deps/grpc/src/core/lib/iomgr/exec_ctx.c', - 'deps/grpc/src/core/lib/iomgr/executor.c', - 'deps/grpc/src/core/lib/iomgr/gethostname_fallback.c', - 'deps/grpc/src/core/lib/iomgr/gethostname_host_name_max.c', - 'deps/grpc/src/core/lib/iomgr/gethostname_sysconf.c', - 'deps/grpc/src/core/lib/iomgr/iocp_windows.c', - 'deps/grpc/src/core/lib/iomgr/iomgr.c', - 'deps/grpc/src/core/lib/iomgr/iomgr_posix.c', - 'deps/grpc/src/core/lib/iomgr/iomgr_uv.c', - 'deps/grpc/src/core/lib/iomgr/iomgr_windows.c', - 'deps/grpc/src/core/lib/iomgr/is_epollexclusive_available.c', - 'deps/grpc/src/core/lib/iomgr/load_file.c', - 'deps/grpc/src/core/lib/iomgr/lockfree_event.c', - 'deps/grpc/src/core/lib/iomgr/network_status_tracker.c', - 'deps/grpc/src/core/lib/iomgr/polling_entity.c', - 'deps/grpc/src/core/lib/iomgr/pollset_set_uv.c', - 'deps/grpc/src/core/lib/iomgr/pollset_set_windows.c', - 'deps/grpc/src/core/lib/iomgr/pollset_uv.c', - 'deps/grpc/src/core/lib/iomgr/pollset_windows.c', - 'deps/grpc/src/core/lib/iomgr/resolve_address_posix.c', - 'deps/grpc/src/core/lib/iomgr/resolve_address_uv.c', - 'deps/grpc/src/core/lib/iomgr/resolve_address_windows.c', - 'deps/grpc/src/core/lib/iomgr/resource_quota.c', - 'deps/grpc/src/core/lib/iomgr/sockaddr_utils.c', - 'deps/grpc/src/core/lib/iomgr/socket_factory_posix.c', - 'deps/grpc/src/core/lib/iomgr/socket_mutator.c', - 'deps/grpc/src/core/lib/iomgr/socket_utils_common_posix.c', - 'deps/grpc/src/core/lib/iomgr/socket_utils_linux.c', - 'deps/grpc/src/core/lib/iomgr/socket_utils_posix.c', - 'deps/grpc/src/core/lib/iomgr/socket_utils_uv.c', - 'deps/grpc/src/core/lib/iomgr/socket_utils_windows.c', - 'deps/grpc/src/core/lib/iomgr/socket_windows.c', - 'deps/grpc/src/core/lib/iomgr/tcp_client_posix.c', - 'deps/grpc/src/core/lib/iomgr/tcp_client_uv.c', - 'deps/grpc/src/core/lib/iomgr/tcp_client_windows.c', - 'deps/grpc/src/core/lib/iomgr/tcp_posix.c', - 'deps/grpc/src/core/lib/iomgr/tcp_server_posix.c', - 'deps/grpc/src/core/lib/iomgr/tcp_server_utils_posix_common.c', - 'deps/grpc/src/core/lib/iomgr/tcp_server_utils_posix_ifaddrs.c', - 'deps/grpc/src/core/lib/iomgr/tcp_server_utils_posix_noifaddrs.c', - 'deps/grpc/src/core/lib/iomgr/tcp_server_uv.c', - 'deps/grpc/src/core/lib/iomgr/tcp_server_windows.c', - 'deps/grpc/src/core/lib/iomgr/tcp_uv.c', - 'deps/grpc/src/core/lib/iomgr/tcp_windows.c', - 'deps/grpc/src/core/lib/iomgr/time_averaged_stats.c', - 'deps/grpc/src/core/lib/iomgr/timer_generic.c', - 'deps/grpc/src/core/lib/iomgr/timer_heap.c', - 'deps/grpc/src/core/lib/iomgr/timer_manager.c', - 'deps/grpc/src/core/lib/iomgr/timer_uv.c', - 'deps/grpc/src/core/lib/iomgr/udp_server.c', - 'deps/grpc/src/core/lib/iomgr/unix_sockets_posix.c', - 'deps/grpc/src/core/lib/iomgr/unix_sockets_posix_noop.c', - 'deps/grpc/src/core/lib/iomgr/wakeup_fd_cv.c', - 'deps/grpc/src/core/lib/iomgr/wakeup_fd_eventfd.c', - 'deps/grpc/src/core/lib/iomgr/wakeup_fd_nospecial.c', - 'deps/grpc/src/core/lib/iomgr/wakeup_fd_pipe.c', - 'deps/grpc/src/core/lib/iomgr/wakeup_fd_posix.c', - 'deps/grpc/src/core/lib/json/json.c', - 'deps/grpc/src/core/lib/json/json_reader.c', - 'deps/grpc/src/core/lib/json/json_string.c', - 'deps/grpc/src/core/lib/json/json_writer.c', - 'deps/grpc/src/core/lib/slice/b64.c', - 'deps/grpc/src/core/lib/slice/percent_encoding.c', - 'deps/grpc/src/core/lib/slice/slice.c', - 'deps/grpc/src/core/lib/slice/slice_buffer.c', - 'deps/grpc/src/core/lib/slice/slice_hash_table.c', - 'deps/grpc/src/core/lib/slice/slice_intern.c', - 'deps/grpc/src/core/lib/slice/slice_string_helpers.c', - 'deps/grpc/src/core/lib/surface/alarm.c', - 'deps/grpc/src/core/lib/surface/api_trace.c', - 'deps/grpc/src/core/lib/surface/byte_buffer.c', - 'deps/grpc/src/core/lib/surface/byte_buffer_reader.c', - 'deps/grpc/src/core/lib/surface/call.c', - 'deps/grpc/src/core/lib/surface/call_details.c', - 'deps/grpc/src/core/lib/surface/call_log_batch.c', - 'deps/grpc/src/core/lib/surface/channel.c', - 'deps/grpc/src/core/lib/surface/channel_init.c', - 'deps/grpc/src/core/lib/surface/channel_ping.c', - 'deps/grpc/src/core/lib/surface/channel_stack_type.c', - 'deps/grpc/src/core/lib/surface/completion_queue.c', - 'deps/grpc/src/core/lib/surface/completion_queue_factory.c', - 'deps/grpc/src/core/lib/surface/event_string.c', + 'deps/grpc/src/core/lib/surface/init.cc', + 'deps/grpc/src/core/lib/avl/avl.cc', + 'deps/grpc/src/core/lib/backoff/backoff.cc', + 'deps/grpc/src/core/lib/channel/channel_args.cc', + 'deps/grpc/src/core/lib/channel/channel_stack.cc', + 'deps/grpc/src/core/lib/channel/channel_stack_builder.cc', + 'deps/grpc/src/core/lib/channel/connected_channel.cc', + 'deps/grpc/src/core/lib/channel/handshaker.cc', + 'deps/grpc/src/core/lib/channel/handshaker_factory.cc', + 'deps/grpc/src/core/lib/channel/handshaker_registry.cc', + 'deps/grpc/src/core/lib/compression/compression.cc', + 'deps/grpc/src/core/lib/compression/compression_internal.cc', + 'deps/grpc/src/core/lib/compression/message_compress.cc', + 'deps/grpc/src/core/lib/compression/stream_compression.cc', + 'deps/grpc/src/core/lib/compression/stream_compression_gzip.cc', + 'deps/grpc/src/core/lib/compression/stream_compression_identity.cc', + 'deps/grpc/src/core/lib/debug/stats.cc', + 'deps/grpc/src/core/lib/debug/stats_data.cc', + 'deps/grpc/src/core/lib/http/format_request.cc', + 'deps/grpc/src/core/lib/http/httpcli.cc', + 'deps/grpc/src/core/lib/http/parser.cc', + 'deps/grpc/src/core/lib/iomgr/call_combiner.cc', + 'deps/grpc/src/core/lib/iomgr/combiner.cc', + 'deps/grpc/src/core/lib/iomgr/endpoint.cc', + 'deps/grpc/src/core/lib/iomgr/endpoint_pair_posix.cc', + 'deps/grpc/src/core/lib/iomgr/endpoint_pair_uv.cc', + 'deps/grpc/src/core/lib/iomgr/endpoint_pair_windows.cc', + 'deps/grpc/src/core/lib/iomgr/error.cc', + 'deps/grpc/src/core/lib/iomgr/ev_epoll1_linux.cc', + 'deps/grpc/src/core/lib/iomgr/ev_epollex_linux.cc', + 'deps/grpc/src/core/lib/iomgr/ev_epollsig_linux.cc', + 'deps/grpc/src/core/lib/iomgr/ev_poll_posix.cc', + 'deps/grpc/src/core/lib/iomgr/ev_posix.cc', + 'deps/grpc/src/core/lib/iomgr/ev_windows.cc', + 'deps/grpc/src/core/lib/iomgr/exec_ctx.cc', + 'deps/grpc/src/core/lib/iomgr/executor.cc', + 'deps/grpc/src/core/lib/iomgr/fork_posix.cc', + 'deps/grpc/src/core/lib/iomgr/fork_windows.cc', + 'deps/grpc/src/core/lib/iomgr/gethostname_fallback.cc', + 'deps/grpc/src/core/lib/iomgr/gethostname_host_name_max.cc', + 'deps/grpc/src/core/lib/iomgr/gethostname_sysconf.cc', + 'deps/grpc/src/core/lib/iomgr/iocp_windows.cc', + 'deps/grpc/src/core/lib/iomgr/iomgr.cc', + 'deps/grpc/src/core/lib/iomgr/iomgr_posix.cc', + 'deps/grpc/src/core/lib/iomgr/iomgr_uv.cc', + 'deps/grpc/src/core/lib/iomgr/iomgr_windows.cc', + 'deps/grpc/src/core/lib/iomgr/is_epollexclusive_available.cc', + 'deps/grpc/src/core/lib/iomgr/load_file.cc', + 'deps/grpc/src/core/lib/iomgr/lockfree_event.cc', + 'deps/grpc/src/core/lib/iomgr/network_status_tracker.cc', + 'deps/grpc/src/core/lib/iomgr/polling_entity.cc', + 'deps/grpc/src/core/lib/iomgr/pollset_set_uv.cc', + 'deps/grpc/src/core/lib/iomgr/pollset_set_windows.cc', + 'deps/grpc/src/core/lib/iomgr/pollset_uv.cc', + 'deps/grpc/src/core/lib/iomgr/pollset_windows.cc', + 'deps/grpc/src/core/lib/iomgr/resolve_address_posix.cc', + 'deps/grpc/src/core/lib/iomgr/resolve_address_uv.cc', + 'deps/grpc/src/core/lib/iomgr/resolve_address_windows.cc', + 'deps/grpc/src/core/lib/iomgr/resource_quota.cc', + 'deps/grpc/src/core/lib/iomgr/sockaddr_utils.cc', + 'deps/grpc/src/core/lib/iomgr/socket_factory_posix.cc', + 'deps/grpc/src/core/lib/iomgr/socket_mutator.cc', + 'deps/grpc/src/core/lib/iomgr/socket_utils_common_posix.cc', + 'deps/grpc/src/core/lib/iomgr/socket_utils_linux.cc', + 'deps/grpc/src/core/lib/iomgr/socket_utils_posix.cc', + 'deps/grpc/src/core/lib/iomgr/socket_utils_uv.cc', + 'deps/grpc/src/core/lib/iomgr/socket_utils_windows.cc', + 'deps/grpc/src/core/lib/iomgr/socket_windows.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_client_posix.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_client_uv.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_client_windows.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_posix.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_server_posix.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_server_utils_posix_common.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_server_utils_posix_ifaddrs.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_server_utils_posix_noifaddrs.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_server_uv.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_server_windows.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_uv.cc', + 'deps/grpc/src/core/lib/iomgr/tcp_windows.cc', + 'deps/grpc/src/core/lib/iomgr/time_averaged_stats.cc', + 'deps/grpc/src/core/lib/iomgr/timer_generic.cc', + 'deps/grpc/src/core/lib/iomgr/timer_heap.cc', + 'deps/grpc/src/core/lib/iomgr/timer_manager.cc', + 'deps/grpc/src/core/lib/iomgr/timer_uv.cc', + 'deps/grpc/src/core/lib/iomgr/udp_server.cc', + 'deps/grpc/src/core/lib/iomgr/unix_sockets_posix.cc', + 'deps/grpc/src/core/lib/iomgr/unix_sockets_posix_noop.cc', + 'deps/grpc/src/core/lib/iomgr/wakeup_fd_cv.cc', + 'deps/grpc/src/core/lib/iomgr/wakeup_fd_eventfd.cc', + 'deps/grpc/src/core/lib/iomgr/wakeup_fd_nospecial.cc', + 'deps/grpc/src/core/lib/iomgr/wakeup_fd_pipe.cc', + 'deps/grpc/src/core/lib/iomgr/wakeup_fd_posix.cc', + 'deps/grpc/src/core/lib/json/json.cc', + 'deps/grpc/src/core/lib/json/json_reader.cc', + 'deps/grpc/src/core/lib/json/json_string.cc', + 'deps/grpc/src/core/lib/json/json_writer.cc', + 'deps/grpc/src/core/lib/slice/b64.cc', + 'deps/grpc/src/core/lib/slice/percent_encoding.cc', + 'deps/grpc/src/core/lib/slice/slice.cc', + 'deps/grpc/src/core/lib/slice/slice_buffer.cc', + 'deps/grpc/src/core/lib/slice/slice_hash_table.cc', + 'deps/grpc/src/core/lib/slice/slice_intern.cc', + 'deps/grpc/src/core/lib/slice/slice_string_helpers.cc', + 'deps/grpc/src/core/lib/surface/api_trace.cc', + 'deps/grpc/src/core/lib/surface/byte_buffer.cc', + 'deps/grpc/src/core/lib/surface/byte_buffer_reader.cc', + 'deps/grpc/src/core/lib/surface/call.cc', + 'deps/grpc/src/core/lib/surface/call_details.cc', + 'deps/grpc/src/core/lib/surface/call_log_batch.cc', + 'deps/grpc/src/core/lib/surface/channel.cc', + 'deps/grpc/src/core/lib/surface/channel_init.cc', + 'deps/grpc/src/core/lib/surface/channel_ping.cc', + 'deps/grpc/src/core/lib/surface/channel_stack_type.cc', + 'deps/grpc/src/core/lib/surface/completion_queue.cc', + 'deps/grpc/src/core/lib/surface/completion_queue_factory.cc', + 'deps/grpc/src/core/lib/surface/event_string.cc', 'deps/grpc/src/core/lib/surface/lame_client.cc', - 'deps/grpc/src/core/lib/surface/metadata_array.c', - 'deps/grpc/src/core/lib/surface/server.c', - 'deps/grpc/src/core/lib/surface/validate_metadata.c', - 'deps/grpc/src/core/lib/surface/version.c', - 'deps/grpc/src/core/lib/transport/bdp_estimator.c', - 'deps/grpc/src/core/lib/transport/byte_stream.c', - 'deps/grpc/src/core/lib/transport/connectivity_state.c', - 'deps/grpc/src/core/lib/transport/error_utils.c', - 'deps/grpc/src/core/lib/transport/metadata.c', - 'deps/grpc/src/core/lib/transport/metadata_batch.c', - 'deps/grpc/src/core/lib/transport/pid_controller.c', - 'deps/grpc/src/core/lib/transport/service_config.c', - 'deps/grpc/src/core/lib/transport/static_metadata.c', - 'deps/grpc/src/core/lib/transport/status_conversion.c', - 'deps/grpc/src/core/lib/transport/timeout_encoding.c', - 'deps/grpc/src/core/lib/transport/transport.c', - 'deps/grpc/src/core/lib/transport/transport_op_string.c', - 'deps/grpc/src/core/lib/debug/trace.c', - 'deps/grpc/src/core/ext/transport/chttp2/server/secure/server_secure_chttp2.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/bin_decoder.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/bin_encoder.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/chttp2_plugin.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/chttp2_transport.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/flow_control.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_data.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_goaway.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_ping.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_rst_stream.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_settings.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_window_update.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/hpack_encoder.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/hpack_parser.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/hpack_table.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/http2_settings.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/huffsyms.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/incoming_metadata.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/parsing.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/stream_lists.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/stream_map.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/varint.c', - 'deps/grpc/src/core/ext/transport/chttp2/transport/writing.c', - 'deps/grpc/src/core/ext/transport/chttp2/alpn/alpn.c', - 'deps/grpc/src/core/ext/filters/http/client/http_client_filter.c', - 'deps/grpc/src/core/ext/filters/http/http_filters_plugin.c', - 'deps/grpc/src/core/ext/filters/http/message_compress/message_compress_filter.c', - 'deps/grpc/src/core/ext/filters/http/server/http_server_filter.c', - 'deps/grpc/src/core/lib/http/httpcli_security_connector.c', - 'deps/grpc/src/core/lib/security/context/security_context.c', - 'deps/grpc/src/core/lib/security/credentials/composite/composite_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/credentials.c', - 'deps/grpc/src/core/lib/security/credentials/credentials_metadata.c', - 'deps/grpc/src/core/lib/security/credentials/fake/fake_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/google_default/credentials_generic.c', - 'deps/grpc/src/core/lib/security/credentials/google_default/google_default_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/iam/iam_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/jwt/json_token.c', - 'deps/grpc/src/core/lib/security/credentials/jwt/jwt_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/jwt/jwt_verifier.c', - 'deps/grpc/src/core/lib/security/credentials/oauth2/oauth2_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/plugin/plugin_credentials.c', - 'deps/grpc/src/core/lib/security/credentials/ssl/ssl_credentials.c', - 'deps/grpc/src/core/lib/security/transport/client_auth_filter.c', - 'deps/grpc/src/core/lib/security/transport/lb_targets_info.c', - 'deps/grpc/src/core/lib/security/transport/secure_endpoint.c', - 'deps/grpc/src/core/lib/security/transport/security_connector.c', - 'deps/grpc/src/core/lib/security/transport/security_handshaker.c', - 'deps/grpc/src/core/lib/security/transport/server_auth_filter.c', - 'deps/grpc/src/core/lib/security/transport/tsi_error.c', - 'deps/grpc/src/core/lib/security/util/json_util.c', - 'deps/grpc/src/core/lib/surface/init_secure.c', - 'deps/grpc/src/core/tsi/fake_transport_security.c', - 'deps/grpc/src/core/tsi/gts_transport_security.c', - 'deps/grpc/src/core/tsi/ssl_transport_security.c', - 'deps/grpc/src/core/tsi/transport_security_grpc.c', - 'deps/grpc/src/core/tsi/transport_security.c', - 'deps/grpc/src/core/tsi/transport_security_adapter.c', - 'deps/grpc/src/core/ext/transport/chttp2/server/chttp2_server.c', - 'deps/grpc/src/core/ext/transport/chttp2/client/secure/secure_channel_create.c', - 'deps/grpc/src/core/ext/filters/client_channel/channel_connectivity.c', - 'deps/grpc/src/core/ext/filters/client_channel/client_channel.c', - 'deps/grpc/src/core/ext/filters/client_channel/client_channel_factory.c', - 'deps/grpc/src/core/ext/filters/client_channel/client_channel_plugin.c', - 'deps/grpc/src/core/ext/filters/client_channel/connector.c', - 'deps/grpc/src/core/ext/filters/client_channel/http_connect_handshaker.c', - 'deps/grpc/src/core/ext/filters/client_channel/http_proxy.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy_factory.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy_registry.c', - 'deps/grpc/src/core/ext/filters/client_channel/parse_address.c', - 'deps/grpc/src/core/ext/filters/client_channel/proxy_mapper.c', - 'deps/grpc/src/core/ext/filters/client_channel/proxy_mapper_registry.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver_factory.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver_registry.c', - 'deps/grpc/src/core/ext/filters/client_channel/retry_throttle.c', - 'deps/grpc/src/core/ext/filters/client_channel/subchannel.c', - 'deps/grpc/src/core/ext/filters/client_channel/subchannel_index.c', - 'deps/grpc/src/core/ext/filters/client_channel/uri_parser.c', - 'deps/grpc/src/core/ext/filters/deadline/deadline_filter.c', - 'deps/grpc/src/core/ext/transport/chttp2/client/chttp2_connector.c', - 'deps/grpc/src/core/ext/transport/chttp2/server/insecure/server_chttp2.c', - 'deps/grpc/src/core/ext/transport/chttp2/server/insecure/server_chttp2_posix.c', - 'deps/grpc/src/core/ext/transport/chttp2/client/insecure/channel_create.c', - 'deps/grpc/src/core/ext/transport/chttp2/client/insecure/channel_create_posix.c', - 'deps/grpc/src/core/ext/transport/inproc/inproc_plugin.c', - 'deps/grpc/src/core/ext/transport/inproc/inproc_transport.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/client_load_reporting_filter.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/grpclb.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/grpclb_channel_secure.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/grpclb_client_stats.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/load_balancer_api.c', + 'deps/grpc/src/core/lib/surface/metadata_array.cc', + 'deps/grpc/src/core/lib/surface/server.cc', + 'deps/grpc/src/core/lib/surface/validate_metadata.cc', + 'deps/grpc/src/core/lib/surface/version.cc', + 'deps/grpc/src/core/lib/transport/bdp_estimator.cc', + 'deps/grpc/src/core/lib/transport/byte_stream.cc', + 'deps/grpc/src/core/lib/transport/connectivity_state.cc', + 'deps/grpc/src/core/lib/transport/error_utils.cc', + 'deps/grpc/src/core/lib/transport/metadata.cc', + 'deps/grpc/src/core/lib/transport/metadata_batch.cc', + 'deps/grpc/src/core/lib/transport/pid_controller.cc', + 'deps/grpc/src/core/lib/transport/service_config.cc', + 'deps/grpc/src/core/lib/transport/static_metadata.cc', + 'deps/grpc/src/core/lib/transport/status_conversion.cc', + 'deps/grpc/src/core/lib/transport/timeout_encoding.cc', + 'deps/grpc/src/core/lib/transport/transport.cc', + 'deps/grpc/src/core/lib/transport/transport_op_string.cc', + 'deps/grpc/src/core/lib/debug/trace.cc', + 'deps/grpc/src/core/ext/transport/chttp2/server/secure/server_secure_chttp2.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/bin_decoder.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/bin_encoder.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/chttp2_plugin.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/chttp2_transport.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/flow_control.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_data.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_goaway.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_ping.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_rst_stream.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_settings.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/frame_window_update.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/hpack_encoder.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/hpack_parser.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/hpack_table.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/http2_settings.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/huffsyms.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/incoming_metadata.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/parsing.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/stream_lists.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/stream_map.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/varint.cc', + 'deps/grpc/src/core/ext/transport/chttp2/transport/writing.cc', + 'deps/grpc/src/core/ext/transport/chttp2/alpn/alpn.cc', + 'deps/grpc/src/core/ext/filters/http/client/http_client_filter.cc', + 'deps/grpc/src/core/ext/filters/http/http_filters_plugin.cc', + 'deps/grpc/src/core/ext/filters/http/message_compress/message_compress_filter.cc', + 'deps/grpc/src/core/ext/filters/http/server/http_server_filter.cc', + 'deps/grpc/src/core/lib/http/httpcli_security_connector.cc', + 'deps/grpc/src/core/lib/security/context/security_context.cc', + 'deps/grpc/src/core/lib/security/credentials/composite/composite_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/credentials_metadata.cc', + 'deps/grpc/src/core/lib/security/credentials/fake/fake_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/google_default/credentials_generic.cc', + 'deps/grpc/src/core/lib/security/credentials/google_default/google_default_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/iam/iam_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/jwt/json_token.cc', + 'deps/grpc/src/core/lib/security/credentials/jwt/jwt_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/jwt/jwt_verifier.cc', + 'deps/grpc/src/core/lib/security/credentials/oauth2/oauth2_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/plugin/plugin_credentials.cc', + 'deps/grpc/src/core/lib/security/credentials/ssl/ssl_credentials.cc', + 'deps/grpc/src/core/lib/security/transport/client_auth_filter.cc', + 'deps/grpc/src/core/lib/security/transport/lb_targets_info.cc', + 'deps/grpc/src/core/lib/security/transport/secure_endpoint.cc', + 'deps/grpc/src/core/lib/security/transport/security_connector.cc', + 'deps/grpc/src/core/lib/security/transport/security_handshaker.cc', + 'deps/grpc/src/core/lib/security/transport/server_auth_filter.cc', + 'deps/grpc/src/core/lib/security/transport/tsi_error.cc', + 'deps/grpc/src/core/lib/security/util/json_util.cc', + 'deps/grpc/src/core/lib/surface/init_secure.cc', + 'deps/grpc/src/core/tsi/alts_transport_security.cc', + 'deps/grpc/src/core/tsi/fake_transport_security.cc', + 'deps/grpc/src/core/tsi/ssl_transport_security.cc', + 'deps/grpc/src/core/tsi/transport_security_grpc.cc', + 'deps/grpc/src/core/tsi/transport_security.cc', + 'deps/grpc/src/core/tsi/transport_security_adapter.cc', + 'deps/grpc/src/core/ext/transport/chttp2/server/chttp2_server.cc', + 'deps/grpc/src/core/ext/transport/chttp2/client/secure/secure_channel_create.cc', + 'deps/grpc/src/core/ext/filters/client_channel/backup_poller.cc', + 'deps/grpc/src/core/ext/filters/client_channel/channel_connectivity.cc', + 'deps/grpc/src/core/ext/filters/client_channel/client_channel.cc', + 'deps/grpc/src/core/ext/filters/client_channel/client_channel_factory.cc', + 'deps/grpc/src/core/ext/filters/client_channel/client_channel_plugin.cc', + 'deps/grpc/src/core/ext/filters/client_channel/connector.cc', + 'deps/grpc/src/core/ext/filters/client_channel/http_connect_handshaker.cc', + 'deps/grpc/src/core/ext/filters/client_channel/http_proxy.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy_factory.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy_registry.cc', + 'deps/grpc/src/core/ext/filters/client_channel/parse_address.cc', + 'deps/grpc/src/core/ext/filters/client_channel/proxy_mapper.cc', + 'deps/grpc/src/core/ext/filters/client_channel/proxy_mapper_registry.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver_registry.cc', + 'deps/grpc/src/core/ext/filters/client_channel/retry_throttle.cc', + 'deps/grpc/src/core/ext/filters/client_channel/subchannel.cc', + 'deps/grpc/src/core/ext/filters/client_channel/subchannel_index.cc', + 'deps/grpc/src/core/ext/filters/client_channel/uri_parser.cc', + 'deps/grpc/src/core/ext/filters/deadline/deadline_filter.cc', + 'deps/grpc/src/core/ext/transport/chttp2/client/chttp2_connector.cc', + 'deps/grpc/src/core/ext/transport/chttp2/server/insecure/server_chttp2.cc', + 'deps/grpc/src/core/ext/transport/chttp2/server/insecure/server_chttp2_posix.cc', + 'deps/grpc/src/core/ext/transport/chttp2/client/insecure/channel_create.cc', + 'deps/grpc/src/core/ext/transport/chttp2/client/insecure/channel_create_posix.cc', + 'deps/grpc/src/core/ext/transport/inproc/inproc_plugin.cc', + 'deps/grpc/src/core/ext/transport/inproc/inproc_transport.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/client_load_reporting_filter.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/grpclb.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/grpclb_channel_secure.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/grpclb_client_stats.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/load_balancer_api.cc', 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/grpclb/proto/grpc/lb/v1/load_balancer.pb.c', 'deps/grpc/third_party/nanopb/pb_common.c', 'deps/grpc/third_party/nanopb/pb_decode.c', 'deps/grpc/third_party/nanopb/pb_encode.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/fake/fake_resolver.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/pick_first/pick_first.c', - 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/round_robin/round_robin.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/dns_resolver_ares.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_ev_driver_posix.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper_fallback.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/native/dns_resolver.c', - 'deps/grpc/src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.c', - 'deps/grpc/src/core/ext/filters/load_reporting/server_load_reporting_filter.c', - 'deps/grpc/src/core/ext/filters/load_reporting/server_load_reporting_plugin.c', - 'deps/grpc/src/core/ext/census/base_resources.c', - 'deps/grpc/src/core/ext/census/context.c', - 'deps/grpc/src/core/ext/census/gen/census.pb.c', - 'deps/grpc/src/core/ext/census/gen/trace_context.pb.c', - 'deps/grpc/src/core/ext/census/grpc_context.c', - 'deps/grpc/src/core/ext/census/grpc_filter.c', - 'deps/grpc/src/core/ext/census/grpc_plugin.c', - 'deps/grpc/src/core/ext/census/initialize.c', - 'deps/grpc/src/core/ext/census/intrusive_hash_map.c', - 'deps/grpc/src/core/ext/census/mlog.c', - 'deps/grpc/src/core/ext/census/operation.c', - 'deps/grpc/src/core/ext/census/placeholders.c', - 'deps/grpc/src/core/ext/census/resource.c', - 'deps/grpc/src/core/ext/census/trace_context.c', - 'deps/grpc/src/core/ext/census/tracing.c', - 'deps/grpc/src/core/ext/filters/max_age/max_age_filter.c', - 'deps/grpc/src/core/ext/filters/message_size/message_size_filter.c', - 'deps/grpc/src/core/ext/filters/workarounds/workaround_cronet_compression_filter.c', - 'deps/grpc/src/core/ext/filters/workarounds/workaround_utils.c', - 'deps/grpc/src/core/plugin_registry/grpc_plugin_registry.c', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/fake/fake_resolver.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/pick_first/pick_first.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/subchannel_list.cc', + 'deps/grpc/src/core/ext/filters/client_channel/lb_policy/round_robin/round_robin.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/dns_resolver_ares.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_ev_driver_posix.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper_fallback.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/dns/native/dns_resolver.cc', + 'deps/grpc/src/core/ext/filters/client_channel/resolver/sockaddr/sockaddr_resolver.cc', + 'deps/grpc/src/core/ext/filters/load_reporting/server_load_reporting_filter.cc', + 'deps/grpc/src/core/ext/filters/load_reporting/server_load_reporting_plugin.cc', + 'deps/grpc/src/core/ext/census/grpc_context.cc', + 'deps/grpc/src/core/ext/filters/max_age/max_age_filter.cc', + 'deps/grpc/src/core/ext/filters/message_size/message_size_filter.cc', + 'deps/grpc/src/core/ext/filters/workarounds/workaround_cronet_compression_filter.cc', + 'deps/grpc/src/core/ext/filters/workarounds/workaround_utils.cc', + 'deps/grpc/src/core/plugin_registry/grpc_plugin_registry.cc', ], 'conditions': [ ['OS == "mac"', { @@ -957,17 +903,7 @@ ], "target_name": "grpc_node", "sources": [ - "ext/byte_buffer.cc", - "ext/call.cc", - "ext/call_credentials.cc", - "ext/channel.cc", - "ext/channel_credentials.cc", - "ext/completion_queue.cc", - "ext/node_grpc.cc", - "ext/server.cc", - "ext/server_credentials.cc", - "ext/slice.cc", - "ext/timeval.cc", + "'ext/'+f).join(' ')\")" ], "dependencies": [ "grpc", diff --git a/packages/grpc-native-core/build.yaml b/packages/grpc-native-core/build.yaml new file mode 100644 index 00000000..505c94b5 --- /dev/null +++ b/packages/grpc-native-core/build.yaml @@ -0,0 +1,2 @@ +settings: + '#': It's possible to have node_version here as a key to override the core's version. \ No newline at end of file diff --git a/packages/grpc-native-core/deps/grpc b/packages/grpc-native-core/deps/grpc index cbe93839..34e8e0a6 160000 --- a/packages/grpc-native-core/deps/grpc +++ b/packages/grpc-native-core/deps/grpc @@ -1 +1 @@ -Subproject commit cbe93839937e4e6272407ab1b7ae7ef848b64894 +Subproject commit 34e8e0a6400d8b529125a3b83ec1facf71acf99b diff --git a/packages/grpc-native-core/ext/call_credentials.cc b/packages/grpc-native-core/ext/call_credentials.cc index 4cf3e565..2b1cb35f 100644 --- a/packages/grpc-native-core/ext/call_credentials.cc +++ b/packages/grpc-native-core/ext/call_credentials.cc @@ -238,9 +238,13 @@ NAUV_WORK_CB(SendPluginCallback) { } } -void plugin_get_metadata(void *state, grpc_auth_metadata_context context, - grpc_credentials_plugin_metadata_cb cb, - void *user_data) { +int plugin_get_metadata( + void *state, grpc_auth_metadata_context context, + grpc_credentials_plugin_metadata_cb cb, + void *user_data, + grpc_metadata creds_md[GRPC_METADATA_CREDENTIALS_PLUGIN_SYNC_MAX], + size_t *num_creds_md, grpc_status_code *status, + const char **error_details) { plugin_state *p_state = reinterpret_cast(state); plugin_callback_data *data = new plugin_callback_data; data->service_url = context.service_url; @@ -252,6 +256,7 @@ void plugin_get_metadata(void *state, grpc_auth_metadata_context context, uv_mutex_unlock(&p_state->plugin_mutex); uv_async_send(&p_state->plugin_async); + return 0; // Async processing. } void plugin_uv_close_cb(uv_handle_t *handle) { diff --git a/packages/grpc-native-core/ext/call_credentials.h b/packages/grpc-native-core/ext/call_credentials.h index adcff845..323eb4f2 100644 --- a/packages/grpc-native-core/ext/call_credentials.h +++ b/packages/grpc-native-core/ext/call_credentials.h @@ -75,9 +75,13 @@ typedef struct plugin_state { uv_async_t plugin_async; } plugin_state; -void plugin_get_metadata(void *state, grpc_auth_metadata_context context, - grpc_credentials_plugin_metadata_cb cb, - void *user_data); +int plugin_get_metadata( + void *state, grpc_auth_metadata_context context, + grpc_credentials_plugin_metadata_cb cb, + void *user_data, + grpc_metadata creds_md[GRPC_METADATA_CREDENTIALS_PLUGIN_SYNC_MAX], + size_t *num_creds_md, grpc_status_code *status, + const char **error_details); void plugin_destroy_state(void *state); diff --git a/packages/grpc-native-core/ext/completion_queue.cc b/packages/grpc-native-core/ext/completion_queue.cc index a08febbb..7a149532 100644 --- a/packages/grpc-native-core/ext/completion_queue.cc +++ b/packages/grpc-native-core/ext/completion_queue.cc @@ -35,7 +35,7 @@ grpc_completion_queue *queue; uv_prepare_t prepare; int pending_batches; -void drain_completion_queue(uv_prepare_t *handle) { +static void drain_completion_queue(uv_prepare_t *handle) { Nan::HandleScope scope; grpc_event event; (void)handle; @@ -53,9 +53,9 @@ void drain_completion_queue(uv_prepare_t *handle) { CompleteTag(event.tag, error_message); grpc::node::DestroyTag(event.tag); pending_batches--; - if (pending_batches == 0) { - uv_prepare_stop(&prepare); - } + } + if (pending_batches == 0) { + uv_prepare_stop(&prepare); } } while (event.type != GRPC_QUEUE_TIMEOUT); } @@ -64,7 +64,6 @@ grpc_completion_queue *GetCompletionQueue() { return queue; } void CompletionQueueNext() { if (pending_batches == 0) { - GPR_ASSERT(!uv_is_active((uv_handle_t *)&prepare)); uv_prepare_start(&prepare, drain_completion_queue); } pending_batches++; @@ -76,5 +75,15 @@ void CompletionQueueInit(Local exports) { pending_batches = 0; } +void CompletionQueueForcePoll() { + /* This sets the prepare object to poll on the completion queue the next time + * Node polls for IO. But it doesn't increment the number of pending batches, + * so it will immediately stop polling after that unless there is an + * intervening CompletionQueueNext call */ + if (pending_batches == 0) { + uv_prepare_start(&prepare, drain_completion_queue); + } +} + } // namespace node } // namespace grpc diff --git a/packages/grpc-native-core/ext/completion_queue.h b/packages/grpc-native-core/ext/completion_queue.h index f91d5ea8..3a56c327 100644 --- a/packages/grpc-native-core/ext/completion_queue.h +++ b/packages/grpc-native-core/ext/completion_queue.h @@ -28,5 +28,7 @@ void CompletionQueueNext(); void CompletionQueueInit(v8::Local exports); +void CompletionQueueForcePoll(); + } // namespace node } // namespace grpc diff --git a/packages/grpc-native-core/ext/node_grpc.cc b/packages/grpc-native-core/ext/node_grpc.cc index 11ed0838..6ba5f1a1 100644 --- a/packages/grpc-native-core/ext/node_grpc.cc +++ b/packages/grpc-native-core/ext/node_grpc.cc @@ -28,9 +28,7 @@ #include "grpc/support/time.h" // TODO(murgatroid99): Remove this when the endpoint API becomes public -extern "C" { #include "src/core/lib/iomgr/pollset_uv.h" -} #include "call.h" #include "call_credentials.h" @@ -265,6 +263,10 @@ NAN_METHOD(SetLogVerbosity) { gpr_set_log_verbosity(severity); } +NAN_METHOD(ForcePoll) { + grpc::node::CompletionQueueForcePoll(); +} + void init(Local exports) { Nan::HandleScope scope; grpc_init(); @@ -306,6 +308,9 @@ void init(Local exports) { Nan::Set(exports, Nan::New("setLogVerbosity").ToLocalChecked(), Nan::GetFunction(Nan::New(SetLogVerbosity)) .ToLocalChecked()); + Nan::Set(exports, Nan::New("forcePoll").ToLocalChecked(), + Nan::GetFunction(Nan::New(ForcePoll)) + .ToLocalChecked()); } NODE_MODULE(grpc_node, init) diff --git a/packages/grpc-native-core/ext/server.cc b/packages/grpc-native-core/ext/server.cc index c6ab5e18..d5b4763b 100644 --- a/packages/grpc-native-core/ext/server.cc +++ b/packages/grpc-native-core/ext/server.cc @@ -312,6 +312,9 @@ NAN_METHOD(Server::TryShutdown) { if (!HasInstance(info.This())) { return Nan::ThrowTypeError("tryShutdown can only be called on a Server"); } + if (!info[0]->IsFunction()) { + return Nan::ThrowError("tryShutdown's argument must be a callback"); + } Server *server = ObjectWrap::Unwrap(info.This()); if (server->wrapped_server == NULL) { // Server is already shut down. Call callback immediately. diff --git a/packages/grpc-native-core/gulpfile.js b/packages/grpc-native-core/gulpfile.js index 07fadde8..71a0b0ba 100644 --- a/packages/grpc-native-core/gulpfile.js +++ b/packages/grpc-native-core/gulpfile.js @@ -1,9 +1,27 @@ +/* + * 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'); // 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'); @@ -17,20 +35,20 @@ const testDir = path.resolve(nativeCoreDir, 'test'); const pkg = require('./package'); const jshintConfig = pkg.jshintConfig; -gulp.task('native.core.clean', 'Delete generated files', () => { +gulp.task('clean', 'Delete generated files', () => { return del([path.resolve(nativeCoreDir, 'build'), path.resolve(nativeCoreDir, 'ext/node')]); }); -gulp.task('native.core.clean.all', 'Delete all files created by tasks', - ['native.core.clean']); +gulp.task('clean.all', 'Delete all files created by tasks', + ['clean']); -gulp.task('native.core.install', 'Install native core dependencies', () => { +gulp.task('install', 'Install native core dependencies', () => { return execa('npm', ['install', '--build-from-source', '--unsafe-perm'], {cwd: nativeCoreDir, stdio: 'inherit'}); }); -gulp.task('native.core.install.windows', 'Install native core dependencies for MS Windows', () => { +gulp.task('install.windows', 'Install native core dependencies for MS Windows', () => { 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(() => @@ -39,20 +57,22 @@ execa('npm', ['install', '--build-from-source'], )) }); -gulp.task('native.core.link.create', 'Create npm link', () => { - return execa('npm', ['link'], {cwd: nativeCoreDir, stdio: 'inherit'}); -}); - -gulp.task('native.core.lint', 'Emits linting errors', () => { +gulp.task('lint', 'Emits linting errors', () => { return gulp.src([`${nativeCoreDir}/index.js`, `${srcDir}/*.js`, `${testDir}/*.js`]) .pipe(jshint(pkg.jshintConfig)) .pipe(jshint.reporter('default')); }); -gulp.task('native.core.build', 'Build native package', () => { +gulp.task('build', 'Build native package', () => { return execa('npm', ['run', 'build'], {cwd: nativeCoreDir, stdio: 'inherit'}); }); -gulp.task('native.core.test', 'Run all tests', ['native.core.build'], () => { +gulp.task('test', 'Run all tests', ['build'], () => { return gulp.src(`${testDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'})); }); + +gulp.task('doc.gen', 'Generate docs', (cb) => { + var config = require('./jsdoc_conf.json'); + gulp.src([`${nativeCoreDir}/README.md`, `${nativeCoreDir}/index.js`, `${srcDir}/*.js`], {read: false}) + .pipe(jsdoc(config, cb)); +}); diff --git a/packages/grpc-native-core/index.d.ts b/packages/grpc-native-core/index.d.ts new file mode 100644 index 00000000..73a685cd --- /dev/null +++ b/packages/grpc-native-core/index.d.ts @@ -0,0 +1,1262 @@ +declare module "grpc" { + import { Message, Service as ProtobufService } from "protobufjs"; + import { Duplex, Readable, Writable } from "stream"; + import { SecureContext } from "tls"; + + /** + * Load a ProtoBuf.js object as a gRPC object. + * @param value The ProtoBuf.js reflection object to load + * @param options Options to apply to the loaded file + * @return The resulting gRPC object. + */ + export function loadObject(value: object, options?: LoadObjectOptions): T; + + /** + * Options for loading proto object as gRPC object + * @param {(number|string)=} [options.protobufjsVersion='detect'] 5 and 6 + * respectively indicate that an object from the corresponding version of + * Protobuf.js is provided in the value argument. If the option is 'detect', + * gRPC will guess what the version is based on the structure of the value. + */ + export interface LoadObjectOptions { + /** + * Deserialize bytes values as base64 strings instead of Buffers. + * Defaults to `false`. + */ + binaryAsBase64?: boolean; + + /** + * Deserialize long values as strings instead of objects. + * Defaults to `true`. + */ + longsAsStrings?: boolean; + + /** + * Deserialize enum values as strings instead of numbers. Only works with + * Protobuf.js 6 values. + * Defaults to `true`. + */ + enumsAsStrings?: boolean; + + /** + * use the beta method argument order for client methods, with optional + * arguments after the callback. This option is only a temporary stopgap + * measure to smooth an API breakage. It is deprecated, and new code + * should not use it. + * Defaults to `false` + */ + deprecatedArgumentOrder?: boolean; + + /** + * 5 and 6 respectively indicate that an object from the corresponding + * version of Protobuf.js is provided in the value argument. If the option + * is 'detect', gRPC wll guess what the version is based on the structure + * of the value. + */ + protobufjsVersion?: 5 | 6 | "detect"; + } + + /** + * Map from `.proto` file. + * - Namespaces become maps from the names of their direct members to those member objects + * - Service definitions become client constructors for clients for that service. They also + * have a service member that can be used for constructing servers. + * - Message definitions become Message constructors like those that ProtoBuf.js would create + * - Enum definitions become Enum objects like those that ProtoBuf.js would create + * - Anything else becomes the relevant reflection object that ProtoBuf.js would create + */ + export interface GrpcObject { + [name: string]: GrpcObject | typeof Client | Message; + } + + /** + * Load a gRPC object from a .proto file. + * @param filename The file to load + * @param format The file format to expect. Defaults to 'proto' + * @param options Options to apply to the loaded file + * @return The resulting gRPC object + */ + export function load(filename: Filename, format?: "proto" | "json", options?: LoadOptions): T; + + /** + * A filename + */ + export type Filename = string | { root: string, file: string }; + + /** + * Options for loading proto file as gRPC object + */ + export interface LoadOptions { + /** + * Load this file with field names in camel case instead of their original case. + * Defaults to `false`. + */ + convertFieldsToCamelCase?: boolean; + + /** + * Deserialize bytes values as base64 strings instead of Buffers. + * Defaults to `false`. + */ + binaryAsBase64?: boolean; + + /** + * Deserialize long values as strings instead of objects. + * Defaults to `true`. + */ + longsAsStrings?: boolean; + + /** + * Use the beta method argument order for client methods, with optional + * arguments after the callback. This option is only a temporary stopgap + * measure to smooth an API breakage. It is deprecated, and new code + * should not use it. + * Defaults to `false` + */ + deprecatedArgumentOrder?: boolean; + } + + /** + * Sets the logger function for the gRPC module. For debugging purposes, the C + * core will log synchronously directly to stdout unless this function is + * called. Note: the output format here is intended to be informational, and + * is not guaranteed to stay the same in the future. + * Logs will be directed to logger.error. + * @param logger A Console-like object. + */ + export function setLogger(logger: Console): void; + + /** + * Sets the logger verbosity for gRPC module logging. The options are members + * of the grpc.logVerbosity map. + * @param verbosity The minimum severity to log + */ + export function setLogVerbosity(verbosity: logVerbosity): void; + + /** + * Server object that stores request handlers and delegates incoming requests to those handlers + */ + export class Server { + /** + * Constructs a server object that stores request handlers and delegates + * incoming requests to those handlers + * @param options Options that should be passed to the internal server + * implementation + * ``` + * var server = new grpc.Server(); + * server.addProtoService(protobuf_service_descriptor, service_implementation); + * server.bind('address:port', server_credential); + * server.start(); + * ``` + */ + constructor(options?: object); + + /** + * Start the server and begin handling requests + */ + start(): void; + + /** + * Registers a handler to handle the named method. Fails if there already is + * a handler for the given method. Returns true on success + * @param name The name of the method that the provided function should + * handle/respond to. + * @param handler Function that takes a stream of + * request values and returns a stream of response values + * @param serialize Serialization function for responses + * @param deserialize Deserialization function for requests + * @param type The streaming type of method that this handles + * @return True if the handler was set. False if a handler was already + * set for that name. + */ + register( + name: string, + handler: handleCall, + serialize: serialize, + deserialize: deserialize, + type: string + ): boolean; + + /** + * Gracefully shuts down the server. The server will stop receiving new calls, + * and any pending calls will complete. The callback will be called when all + * pending calls have completed and the server is fully shut down. This method + * is idempotent with itself and forceShutdown. + * @param {function()} callback The shutdown complete callback + */ + tryShutdown(callback: () => void): void; + + /** + * Forcibly shuts down the server. The server will stop receiving new calls + * and cancel all pending calls. When it returns, the server has shut down. + * This method is idempotent with itself and tryShutdown, and it will trigger + * any outstanding tryShutdown callbacks. + */ + forceShutdown(): void; + + /** + * Add a service to the server, with a corresponding implementation. + * @param service The service descriptor + * @param implementation Map of method names to method implementation + * for the provided service. + */ + addService( + service: ServiceDefinition, + implementation: ImplementationType + ): void; + + /** + * Add a proto service to the server, with a corresponding implementation + * @deprecated Use `Server#addService` instead + * @param service The proto service descriptor + * @param implementation Map of method names to method implementation + * for the provided service. + */ + addProtoService( + service: ServiceDefinition, + implementation: ImplementationType + ): void; + + /** + * Binds the server to the given port, with SSL disabled if creds is an + * insecure credentials object + * @param port The port that the server should bind on, in the format + * "address:port" + * @param creds Server credential object to be used for SSL. Pass an + * insecure credentials object for an insecure port. + * @return The bound port number or 0 if the opreation failed. + */ + bind(port: string, creds: ServerCredentials): number; + } + + /** + * A type that servers as a default for an untyped service. + */ + export type UntypedServiceImplementation = { [name: string]: handleCall }; + + /** + * An object that completely defines a service. + * @typedef {Object.} grpc~ServiceDefinition + */ + export type ServiceDefinition = { + readonly [I in keyof ImplementationType]: MethodDefinition; + } + + /** + * An object that completely defines a service method signature. + */ + export interface MethodDefinition { + /** + * The method's URL path + */ + path: string; + /** + * Indicates whether the method accepts a stream of requests + */ + requestStream: boolean; + /** + * Indicates whether the method returns a stream of responses + */ + responseStream: boolean; + /** + * Serialization function for request values + */ + requestSerialize: serialize; + /** + * Serialization function for response values + */ + responseSerialize: serialize; + /** + * Deserialization function for request data + */ + requestDeserialize: deserialize; + /** + * Deserialization function for repsonse data + */ + responseDeserialize: deserialize; + } + + type handleCall = + handleUnaryCall | + handleClientStreamingCall | + handleServerStreamingCall | + handleBidiStreamingCall; + + /** + * User-provided method to handle unary requests on a server + */ + type handleUnaryCall = + (call: ServerUnaryCall, callback: sendUnaryData) => void; + + /** + * An EventEmitter. Used for unary calls. + */ + export class ServerUnaryCall { + /** + * Indicates if the call has been cancelled + */ + cancelled: boolean; + + /** + * The request metadata from the client + */ + metadata: Metadata; + + /** + * The request message from the client + */ + request: RequestType; + + private constructor(); + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + + /** + * Send the initial metadata for a writable stream. + * @param responseMetadata Metadata to send + */ + sendMetadata(responseMetadata: Metadata): void; + } + + /** + * User provided method to handle client streaming methods on the server. + */ + type handleClientStreamingCall = + (call: ServerReadableStream, callback: sendUnaryData) => void; + + /** + * A stream that the server can read from. Used for calls that are streaming + * from the client side. + */ + export class ServerReadableStream extends Readable { + /** + * Indicates if the call has been cancelled + */ + cancelled: boolean; + + /** + * The request metadata from the client + */ + metadata: Metadata; + + private constructor(); + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + + /** + * Send the initial metadata for a writable stream. + * @param responseMetadata Metadata to send + */ + sendMetadata(responseMetadata: Metadata): void; + } + + /** + * User provided method to handle server streaming methods on the server. + */ + type handleServerStreamingCall = + (call: ServerWriteableStream) => void; + + /** + * A stream that the server can write to. Used for calls that are streaming + * from the server side. + */ + export class ServerWriteableStream extends Writable { + /** + * Indicates if the call has been cancelled + */ + cancelled: boolean; + + /** + * The request metadata from the client + */ + metadata: Metadata; + + /** + * The request message from the client + */ + request: RequestType; + + private constructor(); + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + + /** + * Send the initial metadata for a writable stream. + * @param responseMetadata Metadata to send + */ + sendMetadata(responseMetadata: Metadata): void; + } + + /** + * User provided method to handle bidirectional streaming calls on the server. + */ + type handleBidiStreamingCall = + (call: ServerDuplexStream) => void; + + /** + * A stream that the server can read from or write to. Used for calls + * with duplex streaming. + */ + export class ServerDuplexStream extends Duplex { + /** + * Indicates if the call has been cancelled + */ + cancelled: boolean; + + /** + * The request metadata from the client + */ + metadata: Metadata; + + private constructor(); + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + + /** + * Send the initial metadata for a writable stream. + * @param responseMetadata Metadata to send + */ + sendMetadata(responseMetadata: Metadata): void; + } + + /** + * A deserialization function + * @param data The byte sequence to deserialize + * @return The data deserialized as a value + */ + type deserialize = (data: Buffer) => T; + + /** + * A serialization function + * @param value The value to serialize + * @return The value serialized as a byte sequence + */ + type serialize = (value: T) => Buffer; + + /** + * Callback function passed to server handlers that handle methods with + * unary responses. + */ + type sendUnaryData = + (error: ServiceError | null, value: ResponseType | null, trailer?: Metadata, flags?: number) => void; + + /** + * A class for storing metadata. Keys are normalized to lowercase ASCII. + */ + export class Metadata { + /** + * Sets the given value for the given key by replacing any other values + * associated with that key. Normalizes the key. + * @param key The key to whose value should be set. + * @param value The value to set. Must be a buffer if and only + * if the normalized key ends with '-bin'. + */ + set(key: string, value: MetadataValue): void; + + /** + * Adds the given value for the given key by appending to a list of previous + * values associated with that key. Normalizes the key. + * @param key The key for which a new value should be appended. + * @param value The value to add. Must be a buffer if and only + * if the normalized key ends with '-bin'. + */ + add(key: string, value: MetadataValue): void; + + /** + * Removes the given key and any associated values. Normalizes the key. + * @param key The key whose values should be removed. + */ + remove(key: string): void; + + /** + * Gets a list of all values associated with the key. Normalizes the key. + * @param key The key whose value should be retrieved. + * @return A list of values associated with the given key. + */ + get(key: string): MetadataValue[]; + + /** + * Gets a plain object mapping each key to the first value associated with it. + * This reflects the most common way that people will want to see metadata. + * @return A key/value mapping of the metadata. + */ + getMap(): { [key: string]: MetadataValue }; + + /** + * Clones the metadata object. + * @return The newly cloned object. + */ + clone(): Metadata; + } + + export type MetadataValue = string | Buffer; + + /** + * Represents the status of a completed request. If `code` is + * `grpc.status.OK`, then the request has completed successfully. + * Otherwise, the request has failed, `details` will contain a description of + * the error. Either way, `metadata` contains the trailing response metadata + * sent by the server when it finishes processing the call. + */ + export interface StatusObject { + /** + * The error code, a key of `grpc.status` + */ + code: status; + /** + * Human-readable description of the status + */ + details: string; + /** + * Trailing metadata sent with the status, if applicable + */ + metadata: Metadata; + } + + /** + * Describes how a request has failed. The member `message` will be the same as + * `details` in `StatusObject`, and `code` and `metadata` are the + * same as in that object. + */ + export interface ServiceError extends Error { + /** + * The error code, a key of {@link grpc.status} that is not `grpc.status.OK` + */ + code?: status; + /** + * Trailing metadata sent with the status, if applicable + */ + metadata?: Metadata; + } + + /** + * ServerCredentials factories + */ + export class ServerCredentials { + /** + * Create insecure server credentials + * @return The ServerCredentials + */ + static createInsecure(): ServerCredentials; + /** + * Create SSL server credentials + * @param rootCerts Root CA certificates for validating client certificates + * @param keyCertPairs A list of private key and certificate chain pairs to + * be used for authenticating the server + * @param checkClientCertificate Indicates that the server should request + * and verify the client's certificates. + * Defaults to `false`. + * @return The ServerCredentials + */ + static createSsl(rootCerts: Buffer | null, keyCertPairs: KeyCertPair[], checkClientCertificate?: boolean): ServerCredentials; + } + + /** + * A private key and certificate pair + */ + export interface KeyCertPair { + /** + * The server's private key + */ + private_key: Buffer; + + /** + * The server's certificate chain + */ + cert_chain: Buffer; + } + + /** + * Enum of status codes that gRPC can return + */ + export enum status { + /** + * Not an error; returned on success + */ + OK = 0, + /** + * The operation was cancelled (typically by the caller). + */ + CANCELLED = 1, + /** + * Unknown error. An example of where this error may be returned is + * if a status value received from another address space belongs to + * an error-space that is not known in this address space. Also + * errors raised by APIs that do not return enough error information + * may be converted to this error. + */ + UNKNOWN = 2, + /** + * Client specified an invalid argument. Note that this differs + * from FAILED_PRECONDITION. INVALID_ARGUMENT indicates arguments + * that are problematic regardless of the state of the system + * (e.g., a malformed file name). + */ + INVALID_ARGUMENT = 3, + /** + * Deadline expired before operation could complete. For operations + * that change the state of the system, this error may be returned + * even if the operation has completed successfully. For example, a + * successful response from a server could have been delayed long + * enough for the deadline to expire. + */ + DEADLINE_EXCEEDED = 4, + /** + * Some requested entity (e.g., file or directory) was not found. + */ + NOT_FOUND = 5, + /** + * Some entity that we attempted to create (e.g., file or directory) + * already exists. + */ + ALREADY_EXISTS = 6, + /** + * The caller does not have permission to execute the specified + * operation. PERMISSION_DENIED must not be used for rejections + * caused by exhausting some resource (use RESOURCE_EXHAUSTED + * instead for those errors). PERMISSION_DENIED must not be + * used if the caller can not be identified (use UNAUTHENTICATED + * instead for those errors). + */ + PERMISSION_DENIED = 7, + /** + * Some resource has been exhausted, perhaps a per-user quota, or + * perhaps the entire file system is out of space. + */ + RESOURCE_EXHAUSTED = 8, + /** + * Operation was rejected because the system is not in a state + * required for the operation's execution. For example, directory + * to be deleted may be non-empty, an rmdir operation is applied to + * a non-directory, etc. + * + * A litmus test that may help a service implementor in deciding + * between FAILED_PRECONDITION, ABORTED, and UNAVAILABLE: + * + * - Use UNAVAILABLE if the client can retry just the failing call. + * - Use ABORTED if the client should retry at a higher-level + * (e.g., restarting a read-modify-write sequence). + * - Use FAILED_PRECONDITION if the client should not retry until + * the system state has been explicitly fixed. E.g., if an "rmdir" + * fails because the directory is non-empty, FAILED_PRECONDITION + * should be returned since the client should not retry unless + * they have first fixed up the directory by deleting files from it. + * - Use FAILED_PRECONDITION if the client performs conditional + * REST Get/Update/Delete on a resource and the resource on the + * server does not match the condition. E.g., conflicting + * read-modify-write on the same resource. + */ + FAILED_PRECONDITION = 9, + /** + * The operation was aborted, typically due to a concurrency issue + * like sequencer check failures, transaction aborts, etc. + * + * See litmus test above for deciding between FAILED_PRECONDITION, + * ABORTED, and UNAVAILABLE. + */ + ABORTED = 10, + /** + * Operation was attempted past the valid range. E.g., seeking or + * reading past end of file. + * + * Unlike INVALID_ARGUMENT, this error indicates a problem that may + * be fixed if the system state changes. For example, a 32-bit file + * system will generate INVALID_ARGUMENT if asked to read at an + * offset that is not in the range [0,2^32-1], but it will generate + * OUT_OF_RANGE if asked to read from an offset past the current + * file size. + * + * There is a fair bit of overlap between FAILED_PRECONDITION and + * OUT_OF_RANGE. We recommend using OUT_OF_RANGE (the more specific + * error) when it applies so that callers who are iterating through + * a space can easily look for an OUT_OF_RANGE error to detect when + * they are done. + */ + OUT_OF_RANGE = 11, + /** + * Operation is not implemented or not supported/enabled in this service. + */ + UNIMPLEMENTED = 12, + /** + * Internal errors. Means some invariants expected by underlying + * system has been broken. If you see one of these errors, + * something is very broken. + */ + INTERNAL = 13, + /** + * The service is currently unavailable. This is a most likely a + * transient condition and may be corrected by retrying with + * a backoff. + * + * See litmus test above for deciding between FAILED_PRECONDITION, + * ABORTED, and UNAVAILABLE. + */ + UNAVAILABLE = 14, + /** + * Unrecoverable data loss or corruption. + */ + DATA_LOSS = 15, + /** + * The request does not have valid authentication credentials for the + * operation. + */ + UNAUTHENTICATED = 16, + } + + /** + * Propagation flags: these can be bitwise or-ed to form the propagation option + * for calls. + * + * Users are encouraged to write propagation masks as deltas from the default. + * i.e. write `grpc.propagate.DEFAULTS & ~grpc.propagate.DEADLINE` to disable + * deadline propagation. + */ + export enum propagate { + DEADLINE, + CENSUS_STATS_CONTEXT, + CENSUS_TRACING_CONTEXT, + CANCELLATION, + DEFAULTS, + } + + /** + * Call error constants. Call errors almost always indicate bugs in the gRPC + * library, and these error codes are mainly useful for finding those bugs. + */ + export enum callError { + OK, + ERROR, + NOT_ON_SERVER, + NOT_ON_CLIENT, + ALREADY_INVOKED, + NOT_INVOKED, + ALREADY_FINISHED, + TOO_MANY_OPERATIONS, + INVALID_FLAGS, + INVALID_METADATA, + INVALID_MESSAGE, + NOT_SERVER_COMPLETION_QUEUE, + BATCH_TOO_BIG, + PAYLOAD_TYPE_MISMATCH, + } + + /** + * Write flags: these can be bitwise or-ed to form write options that modify + * how data is written. + */ + export enum writeFlags { + /** + * Hint that the write may be buffered and need not go out on the wire + * immediately. GRPC is free to buffer the message until the next non-buffered + * write, or until writes_done, but it need not buffer completely or at all. + */ + BUFFER_HINT = 1, + /** + * Force compression to be disabled for a particular write + */ + NO_COMPRESS, + } + + /** + * Log verbosity constants. Maps setting names to code numbers. + */ + export enum logVerbosity { + DEBUG, + INFO, + ERROR, + } + + /** + * Credentials module + * + * This module contains factory methods for two different credential types: + * CallCredentials and ChannelCredentials. ChannelCredentials are things like + * SSL credentials that can be used to secure a connection, and are used to + * construct a Client object. CallCredentials generally modify metadata, so they + * can be attached to an individual method call. + * + * CallCredentials can be composed with other CallCredentials to create + * CallCredentials. ChannelCredentials can be composed with CallCredentials + * to create ChannelCredentials. No combined credential can have more than + * one ChannelCredentials. + * + * For example, to create a client secured with SSL that uses Google + * default application credentials to authenticate: + * + * ``` + * var channel_creds = credentials.createSsl(root_certs); + * (new GoogleAuth()).getApplicationDefault(function(err, credential) { + * var call_creds = credentials.createFromGoogleCredential(credential); + * var combined_creds = credentials.combineChannelCredentials( + * channel_creds, call_creds); + * var client = new Client(address, combined_creds); + * }); + * ``` + */ + export const credentials: { + /** + * Create an SSL Credentials object. If using a client-side certificate, both + * the second and third arguments must be passed. + * @param rootCerts The root certificate data + * @param privateKey The client certificate private key, if applicable + * @param certChain The client certificate cert chain, if applicable + * @return The SSL Credentials object + */ + createSsl(rootCerts?: Buffer, privateKey?: Buffer, certChain?: Buffer): ChannelCredentials; + + /** + * Create a gRPC credentials object from a metadata generation function. This + * function gets the service URL and a callback as parameters. The error + * passed to the callback can optionally have a 'code' value attached to it, + * which corresponds to a status code that this library uses. + * @param metadataGenerator The function that generates metadata + * @return The credentials object + */ + createFromMetadataGenerator(metadataGenerator: metadataGenerator): CallCredentials; + + /** + * Create a gRPC credential from a Google credential object. + * @param googleCredential The Google credential object to use + * @return The resulting credentials object + */ + createFromGoogleCredential(googleCredential: GoogleOAuth2Client): CallCredentials; + + /** + * Combine a ChannelCredentials with any number of CallCredentials into a single + * ChannelCredentials object. + * @param channelCredential The ChannelCredentials to start with + * @param credentials The CallCredentials to compose + * @return A credentials object that combines all of the input credentials + */ + combineChannelCredentials(channelCredential: ChannelCredentials, ...credentials: CallCredentials[]): ChannelCredentials; + + /** + * Combine any number of CallCredentials into a single CallCredentials object + * @param credentials The CallCredentials to compose + * @return A credentials object that combines all of the input credentials + */ + combineCallCredentials(...credentials: CallCredentials[]): CallCredentials; + + /** + * Create an insecure credentials object. This is used to create a channel that + * does not use SSL. This cannot be composed with anything. + * @return The insecure credentials object + */ + createInsecure(): ChannelCredentials; + }; + + /** + * Metadata generator function. + */ + export type metadataGenerator = (params: { service_url: string }, callback: (error: Error | null, metadata?: Metadata) => void) => void; + + /** + * This cannot be constructed directly. Instead, instances of this class should + * be created using the factory functions in `grpc.credentials` + */ + export interface ChannelCredentials { + /** + * Returns a copy of this object with the included set of per-call credentials + * expanded to include callCredentials. + * @param callCredentials A CallCredentials object to associate with this + * 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; + } + + /** + * This cannot be constructed directly. Instead, instances of this class should + * be created using the factory functions in `grpc.credentials` + */ + export interface CallCredentials { + /** + * Asynchronously generates a new Metadata object. + * @param options Options used in generating the Metadata object. + */ + generateMetadata(options: object): Promise; + + /** + * Creates a new CallCredentials object from properties of both this and + * another CallCredentials object. This object's metadata generator will be + * called first. + * @param callCredentials The other CallCredentials object. + */ + compose(callCredentials: CallCredentials): CallCredentials; + } + + /** + * This is the required interface from the OAuth2Client object + * from https://github.com/google/google-auth-library-nodejs lib. + * The definition is copied from `ts/lib/auth/oauth2client.ts` + */ + export interface GoogleOAuth2Client { + getRequestMetadata(optUri: string, metadataCallback: (err: Error, headers: any) => void): void; + } + + /** + * Creates a constructor for a client with the given methods, as specified in + * the methods argument. The resulting class will have an instance method for + * each method in the service, which is a partial application of one of the + * `grpc.Client` request methods, depending on `requestSerialize` + * and `responseSerialize`, with the `method`, `serialize`, and `deserialize` + * arguments predefined. + * @param methods An object mapping method names to method attributes + * @param serviceName The fully qualified name of the service + * @param classOptions An options object. + * @return New client constructor, which is a subclass of `grpc.Client`, and + * has the same arguments as that constructor. + */ + export function makeGenericClientConstructor( + methods: ServiceDefinition, + serviceName: string, + classOptions: GenericClientOptions, + ): typeof Client; + + /** + * Options for generic client constructor. + */ + export interface GenericClientOptions { + /** + * Indicates that the old argument order should be used for methods, with + * optional arguments at the end instead of the callback at the end. This + * option is only a temporary stopgap measure to smooth an API breakage. + * It is deprecated, and new code should not use it. + */ + deprecatedArgumentOrder?: boolean; + } + + /** + * Create a client with the given methods + */ + export class Client { + /** + * A generic gRPC client. Primarily useful as a base class for generated clients + * @param address Server address to connect to + * @param credentials Credentials to use to connect to the server + * @param options Options to apply to channel creation + */ + constructor(address: string, credentials: ChannelCredentials, options?: object) + + /** + * Make a unary request to the given method, using the given serialize + * and deserialize functions, with the given argument. + * @param method The name of the method to request + * @param serialize The serialization function for inputs + * @param deserialize The deserialization function for outputs + * @param argument The argument to the call. Should be serializable with + * serialize + * @param metadata Metadata to add to the call + * @param options Options map + * @param callback The callback to for when the response is received + * @return An event emitter for stream related events + */ + makeUnaryRequest( + method: string, + serialize: serialize, + deserialize: deserialize, + argument: RequestType | null, + metadata: Metadata | null, + options: CallOptions | null, + callback: requestCallback, + ): ClientUnaryCall; + + /** + * Make a client stream request to the given method, using the given serialize + * and deserialize functions, with the given argument. + * @param method The name of the method to request + * @param serialize The serialization function for inputs + * @param deserialize The deserialization function for outputs + * @param metadata Array of metadata key/value pairs to add to the call + * @param options Options map + * @param callback The callback to for when the response is received + * @return An event emitter for stream related events + */ + makeClientStreamRequest( + method: string, + serialize: serialize, + deserialize: deserialize, + metadata: Metadata | null, + options: CallOptions | null, + callback: requestCallback, + ): ClientWritableStream; + + /** + * Make a server stream request to the given method, with the given serialize + * and deserialize function, using the given argument + * @param method The name of the method to request + * @param serialize The serialization function for inputs + * @param deserialize The deserialization function for outputs + * @param argument The argument to the call. Should be serializable with + * serialize + * @param metadata Array of metadata key/value pairs to add to the call + * @param options Options map + * @return An event emitter for stream related events + */ + makeServerStreamRequest( + method: string, + serialize: serialize, + deserialize: deserialize, + argument: RequestType, + metadata?: Metadata | null, + options?: CallOptions | null, + ): ClientReadableStream; + + /** + * Make a bidirectional stream request with this method on the given channel. + * @param method The name of the method to request + * @param serialize The serialization function for inputs + * @param deserialize The deserialization + * function for outputs + * @param metadata Array of metadata key/value + * pairs to add to the call + * @param options Options map + * @return An event emitter for stream related events + */ + makeBidiStreamRequest( + method: string, + serialize: serialize, + deserialize: deserialize, + metadata?: Metadata | null, + options?: CallOptions | null, + ): ClientDuplexStream; + + /** + * Close this client. + */ + close(): void; + + /** + * Return the underlying channel object for the specified client + * @return The channel + */ + getChannel(): Channel; + + /** + * Wait for the client to be ready. The callback will be called when the + * client has successfully connected to the server, and it will be called + * with an error if the attempt to connect to the server has unrecoverablly + * failed or if the deadline expires. This function will make the channel + * start connecting if it has not already done so. + * @param deadline When to stop waiting for a connection. + * @param callback The callback to call when done attempting to connect. + */ + waitForReady(deadline: Deadline, callback: (error: Error | null) => void): void; + } + + /** + * A gRPC channel. + */ + export type Channel = any; + + /** + * Options that can be set on a call. + */ + export interface CallOptions { + /** + * The deadline for the entire call to complete. + */ + deadline?: Deadline; + /** + * Server hostname to set on the call. Only meaningful if different from + * the server address used to construct the client. + */ + host?: string; + /** + * Parent call. Used in servers when making a call as part of the process + * of handling a call. Used to propagate some information automatically, + * as specified by propagate_flags. + */ + parent?: Call; + /** + * Indicates which properties of a parent call should propagate to this + * call. Bitwise combination of flags in `grpc.propagate`. + */ + propagate_flags: number; + /** + * The credentials that should be used to make this particular call. + */ + credentials: CallCredentials; + } + + /** + * The deadline of an operation. If it is a date, the deadline is reached at + * the date and time specified. If it is a finite number, it is treated as + * a number of milliseconds since the Unix Epoch. If it is Infinity, the + * deadline will never be reached. If it is -Infinity, the deadline has already + * passed. + */ + export type Deadline = number | Date; + + /** + * Any client call type + */ + type Call = + ClientUnaryCall | + ClientReadableStream | + ClientWritableStream | + ClientDuplexStream; + + /** + * An EventEmitter. Used for unary calls. + */ + export class ClientUnaryCall { + private constructor(); + + /** + * Cancel the ongoing call. Results in the call ending with a CANCELLED status, + * unless it has already ended with some other status. + */ + cancel(): void; + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + } + + /** + * A stream that the client can read from. Used for calls that are streaming + * from the server side. + */ + export class ClientReadableStream extends Readable { + private constructor(); + + /** + * Cancel the ongoing call. Results in the call ending with a CANCELLED status, + * unless it has already ended with some other status. + */ + cancel(): void; + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + } + + /** + * A stream that the client can write to. Used for calls that are streaming from + * the client side. + */ + export class ClientWritableStream extends Writable { + private constructor(); + + /** + * Write a message to the request stream. If serializing the argument fails, + * the call will be cancelled and the stream will end with an error. + * @param message The message to write. Must be a valid argument to the + * serialize function of the corresponding method + * @param flags Flags to modify how the message is written + * @param callback Callback for when this chunk of data is flushed + * @return As defined for [Writable]{@link external:Writable} + */ + write(message: RequestType, flags?: any&writeFlags, callback?: Function): boolean; + + /** + * Cancel the ongoing call. Results in the call ending with a CANCELLED status, + * unless it has already ended with some other status. + */ + cancel(): void; + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + } + + /** + * A stream that the client can read from or write to. Used for calls with + * duplex streaming. + */ + export class ClientDuplexStream extends Duplex { + private constructor(); + + /** + * Write a message to the request stream. If serializing the argument fails, + * the call will be cancelled and the stream will end with an error. + * @param message The message to write. Must be a valid argument to the + * serialize function of the corresponding method + * @param flags Flags to modify how the message is written + * @param callback Callback for when this chunk of data is flushed + * @return As defined for [Writable]{@link external:Writable} + */ + write(message: RequestType, flags?: any&writeFlags, callback?: Function): boolean; + + /** + * Cancel the ongoing call. Results in the call ending with a CANCELLED status, + * unless it has already ended with some other status. + */ + cancel(): void; + + /** + * Get the endpoint this call/stream is connected to. + * @return The URI of the endpoint + */ + getPeer(): string; + } + + /** + * Client request callback + * @param error The error, if the call failed + * @param value The response value, if the call succeeded + */ + export type requestCallback = + (error: ServiceError | null, value: ResponseType | undefined) => void; + + /** + * Return the underlying channel object for the specified client + * @see grpc.Client#getChannel + * @param client The client + * @return The channel + */ + export function getClientChannel(client: Client): Channel; + + /** + * Wait for the client to be ready. The callback will be called when the + * client has successfully connected to the server, and it will be called + * with an error if the attempt to connect to the server has unrecoverably + * failed or if the deadline expires. This function will make the channel + * start connecting if it has not already done so. + * @see grpc.Client#waitForReady + * @param client The client to wait on + * @param deadline When to stop waiting for a connection. Pass Infinity to + * wait forever. + * @param callback The callback to call when done attempting to connect. + */ + export function waitForClientReady(client: Client, deadline: Deadline, callback: (error: Error | null) => void): void; + + /** + * Close client. + * @param clientObj The client to close + */ + export function closeClient(clientObj: Client): void; +} diff --git a/packages/grpc-native-core/index.js b/packages/grpc-native-core/index.js index 0d814cc3..30a6ff3c 100644 --- a/packages/grpc-native-core/index.js +++ b/packages/grpc-native-core/index.js @@ -67,7 +67,7 @@ grpc.setDefaultRootsPem(fs.readFileSync(SSL_ROOTS_PATH, 'ascii')); * @param {(number|string)=} [options.protobufjsVersion='detect'] 5 and 6 * respectively indicate that an object from the corresponding version of * Protobuf.js is provided in the value argument. If the option is 'detect', - * gRPC wll guess what the version is based on the structure of the value. + * gRPC will guess what the version is based on the structure of the value. * @return {Object} The resulting gRPC object. */ exports.loadObject = function loadObject(value, options) { @@ -146,6 +146,29 @@ exports.load = function load(filename, format, options) { return loadObject(builder.ns, options); }; +/** + * Load a gRPC package definition as a gRPC object hierarchy + * @param packageDef grpc~PackageDefinition The package definition object + * @return {Object} The resulting gRPC object + */ +exports.loadPackageDefinition = function loadPackageDefintion(packageDef) { + const result = {}; + for (const serviceFqn in packageDef) { + const service = packageDef[serviceFqn]; + const nameComponents = serviceFqn.split('.'); + const serviceName = nameComponents[nameComponents.length-1]; + let current = result; + for (const packageName of nameComponents.slice(0, -1)) { + if (!current[packageName]) { + current[packageName] = {}; + } + current = current[packageName]; + } + current[serviceName] = client.makeClientConstructor(service, serviceName, {}); + } + return result; +}; + var log_template = _.template( '{severity} {timestamp}\t{file}:{line}]\t{message}', {interpolate: /{([\s\S]+?)}/g}); @@ -200,6 +223,8 @@ exports.writeFlags = constants.writeFlags; exports.logVerbosity = constants.logVerbosity; +exports.methodTypes = constants.methodTypes; + exports.credentials = require('./src/credentials.js'); /** @@ -213,19 +238,19 @@ exports.ServerCredentials = grpc.ServerCredentials; * Create insecure server credentials * @name grpc.ServerCredentials.createInsecure * @kind function - * @return grpc.ServerCredentials + * @return {grpc.ServerCredentials} */ /** * A private key and certificate pair * @typedef {Object} grpc.ServerCredentials~keyCertPair - * @property {Buffer} privateKey The server's private key - * @property {Buffer} certChain The server's certificate chain + * @property {Buffer} private_key The server's private key + * @property {Buffer} cert_chain The server's certificate chain */ /** * Create SSL server credentials - * @name grpc.ServerCredentials.createInsecure + * @name grpc.ServerCredentials.createSsl * @kind function * @param {?Buffer} rootCerts Root CA certificates for validating client * certificates @@ -234,7 +259,7 @@ exports.ServerCredentials = grpc.ServerCredentials; * the server * @param {boolean} [checkClientCertificate=false] Indicates that the server * should request and verify the client's certificates - * @return grpc.ServerCredentials + * @return {grpc.ServerCredentials} */ exports.makeGenericClientConstructor = client.makeClientConstructor; @@ -243,6 +268,11 @@ exports.getClientChannel = client.getClientChannel; exports.waitForClientReady = client.waitForClientReady; +exports.StatusBuilder = client.StatusBuilder; +exports.ListenerBuilder = client.ListenerBuilder; +exports.RequesterBuilder = client.RequesterBuilder; +exports.InterceptingCall = client.InterceptingCall; + /** * @memberof grpc * @alias grpc.closeClient diff --git a/packages/grpc-native-core/jsdoc_conf.json b/packages/grpc-native-core/jsdoc_conf.json index 2d967753..92de2990 100644 --- a/packages/grpc-native-core/jsdoc_conf.json +++ b/packages/grpc-native-core/jsdoc_conf.json @@ -2,14 +2,8 @@ "tags": { "allowUnknownTags": true }, - "source": { - "include": [ "src/node/index.js", "src/node/src" ], - "includePattern": "src/node/.+\\.js(doc)?$", - "excludePattern": "(^|\\/|\\\\)_" - }, "opts": { - "package": "package.json", - "readme": "src/node/README.md" + "destination": "docs/gen/native/core" }, "plugins": ["plugins/markdown"], "templates": { diff --git a/packages/grpc-native-core/package.json b/packages/grpc-native-core/package.json index 6ad9013b..3eaa3a83 100644 --- a/packages/grpc-native-core/package.json +++ b/packages/grpc-native-core/package.json @@ -1,6 +1,6 @@ { "name": "grpc", - "version": "1.7.0-dev", + "version": "1.10.0-dev", "author": "Google Inc.", "description": "gRPC Library for Node", "homepage": "https://grpc.io/", @@ -16,14 +16,11 @@ } ], "directories": { - "lib": "src/node/src" + "lib": "src" }, "scripts": { - "lint": "node ./node_modules/jshint/bin/jshint src test index.js --exclude-path=.jshintignore", - "test": "./node_modules/.bin/mocha test && npm run-script lint", "build": "./node_modules/.bin/node-pre-gyp build", "electron-build": "./node_modules/.bin/node-pre-gyp configure build --runtime=electron --disturl=https://atom.io/download/atom-shell", - "gen_docs": "./node_modules/.bin/jsdoc -c jsdoc_conf.json", "coverage": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha test", "install": "./node_modules/.bin/node-pre-gyp install --fallback-to-build --library=static_library" }, @@ -31,10 +28,9 @@ "node-pre-gyp" ], "dependencies": { - "arguejs": "^0.2.3", "lodash": "^4.15.0", "nan": "^2.0.0", - "node-pre-gyp": "^0.6.35", + "node-pre-gyp": "^0.9.0", "protobufjs": "^5.0.0" }, "devDependencies": { @@ -45,7 +41,7 @@ "google-auth-library": "^0.9.2", "google-protobuf": "^3.0.0", "istanbul": "^0.4.4", - "jsdoc": "^3.3.2", + "lodash": "^4.17.4", "minimist": "^1.1.0", "poisson-process": "^0.2.1" }, @@ -54,29 +50,32 @@ }, "binary": { "module_name": "grpc_node", - "module_path": "src/node/extension_binary/{node_abi}-{platform}-{arch}", + "module_path": "src/node/extension_binary/{node_abi}-{platform}-{arch}-{libc}", "host": "https://storage.googleapis.com/", "remote_path": "grpc-precompiled-binaries/node/{name}/v{version}", - "package_name": "{node_abi}-{platform}-{arch}.tar.gz" + "package_name": "{node_abi}-{platform}-{arch}-{libc}.tar.gz" }, "files": [ "LICENSE", "README.md", - "deps/grpc/src/proto", - "deps/grpc/etc", + "deps/grpc/etc/", "index.js", - "src", - "ext", - "deps/grpc/include/grpc", - "deps/grpc/src/core", - "deps/grpc/src/boringssl", - "deps/grpc/src/zlib", - "deps/grpc/third_party/nanopb", - "deps/grpc/third_party/zlib", - "deps/grpc/third_party/boringssl", + "index.d.ts", + "src/*.js", + "ext/*.{cc,h}", + "deps/grpc/include/grpc/**/*.h", + "deps/grpc/src/core/**/*.{c,cc,h}", + "deps/grpc/src/boringssl/*.{c,cc,h}", + "deps/grpc/third_party/nanopb/*.{c,cc,h}", + "deps/grpc/third_party/zlib/**/*.{c,cc,h}", + "deps/grpc/third_party/boringssl/crypto/**/*.{c,cc,h}", + "deps/grpc/third_party/boringssl/include/**/*.{c,cc,h}", + "deps/grpc/third_party/boringssl/ssl/**/*.{c,cc,h}", + "deps/grpc/third_party/abseil-cpp/absl/**/*.{h,hh}", "binding.gyp" ], "main": "index.js", + "typings": "index.d.ts", "license": "Apache-2.0", "jshintConfig": { "bitwise": true, diff --git a/packages/grpc-native-core/src/client.js b/packages/grpc-native-core/src/client.js index 4208da11..e5ce8a3e 100644 --- a/packages/grpc-native-core/src/client.js +++ b/packages/grpc-native-core/src/client.js @@ -33,8 +33,8 @@ 'use strict'; var _ = require('lodash'); -var arguejs = require('arguejs'); +var client_interceptors = require('./client_interceptors'); var grpc = require('./grpc_extension'); var common = require('./common'); @@ -50,6 +50,7 @@ var stream = require('stream'); var Readable = stream.Readable; var Writable = stream.Writable; var Duplex = stream.Duplex; +var methodTypes = constants.methodTypes; var util = require('util'); var version = require('../package.json').version; @@ -92,18 +93,15 @@ util.inherits(ClientWritableStream, Writable); * grpc~ClientWritableStream#metadata * @borrows grpc~ClientUnaryCall#event:status as * grpc~ClientWritableStream#status - * @param {grpc.internal~Call} call The call object to send data with - * @param {grpc~serialize=} [serialize=identity] Serialization - * function for writes. + * @param {InterceptingCall} call Exposes gRPC request operations, processed by + * an interceptor stack. */ -function ClientWritableStream(call, serialize) { +function ClientWritableStream(call) { Writable.call(this, {objectMode: true}); this.call = call; - this.serialize = common.wrapIgnoreNull(serialize); + var self = this; this.on('finish', function() { - var batch = {}; - batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; - call.startBatch(batch, function() {}); + self.call.halfClose(); }); } @@ -130,8 +128,6 @@ function ClientWritableStream(call, serialize) { */ function _write(chunk, encoding, callback) { /* jshint validthis: true */ - var batch = {}; - var message; var self = this; if (this.writeFailed) { /* Once a write fails, just call the callback immediately to let the caller @@ -139,26 +135,7 @@ function _write(chunk, encoding, callback) { setImmediate(callback); return; } - try { - message = this.serialize(chunk); - } catch (e) { - /* Sending this error to the server and emitting it immediately on the - client may put the call in a slightly weird state on the client side, - but passing an object that causes a serialization failure is a misuse - of the API anyway, so that's OK. The primary purpose here is to give the - programmer a useful error and to stop the stream properly */ - this.call.cancelWithStatus(constants.status.INTERNAL, - 'Serialization failure'); - callback(e); - return; - } - if (_.isFinite(encoding)) { - /* Attach the encoding if it is a finite number. This is the closest we - * can get to checking that it is valid flags */ - message.grpcWriteFlags = encoding; - } - batch[grpc.opType.SEND_MESSAGE] = message; - this.call.startBatch(batch, function(err, event) { + var outerCallback = function(err, event) { if (err) { /* Assume that the call is complete and that writing failed because a status was received. In that case, set a flag to discard all future @@ -166,7 +143,12 @@ function _write(chunk, encoding, callback) { self.writeFailed = true; } callback(); - }); + }; + var context = { + encoding: encoding, + callback: outerCallback + }; + this.call.sendMessageWithContext(context, chunk); } ClientWritableStream.prototype._write = _write; @@ -184,16 +166,14 @@ util.inherits(ClientReadableStream, Readable); * grpc~ClientReadableStream#metadata * @borrows grpc~ClientUnaryCall#event:status as * grpc~ClientReadableStream#status - * @param {grpc.internal~Call} call The call object to read data with - * @param {grpc~deserialize=} [deserialize=identity] - * Deserialization function for reads + * @param {InterceptingCall} call Exposes gRPC request operations, processed by + * an interceptor stack. */ -function ClientReadableStream(call, deserialize) { +function ClientReadableStream(call) { Readable.call(this, {objectMode: true}); this.call = call; this.finished = false; this.reading = false; - this.deserialize = common.wrapIgnoreNull(deserialize); /* Status generated from reading messages from the server. Overrides the * status from the server if not OK */ this.read_status = null; @@ -252,9 +232,7 @@ function _emitStatusIfDone() { if (status.code === constants.status.OK) { this.push(null); } else { - var error = new Error(status.details); - error.code = status.code; - error.metadata = status.metadata; + var error = common.createStatusError(status); this.emit('error', error); } this.emit('status', status); @@ -270,48 +248,15 @@ ClientReadableStream.prototype._emitStatusIfDone = _emitStatusIfDone; */ function _read(size) { /* jshint validthis: true */ - var self = this; - /** - * Callback to be called when a READ event is received. Pushes the data onto - * the read queue and starts reading again if applicable - * @param {grpc.Event} event READ event object - */ - function readCallback(err, event) { - if (err) { - // Something has gone wrong. Stop reading and wait for status - self.finished = true; - self._readsDone(); - return; - } - var data = event.read; - var deserialized; - try { - deserialized = self.deserialize(data); - } catch (e) { - self._readsDone({code: constants.status.INTERNAL, - details: 'Failed to parse server response'}); - return; - } - if (data === null) { - self._readsDone(); - return; - } - if (self.push(deserialized) && data !== null) { - var read_batch = {}; - read_batch[grpc.opType.RECV_MESSAGE] = true; - self.call.startBatch(read_batch, readCallback); - } else { - self.reading = false; - } - } - if (self.finished) { - self.push(null); + if (this.finished) { + this.push(null); } else { - if (!self.reading) { - self.reading = true; - var read_batch = {}; - read_batch[grpc.opType.RECV_MESSAGE] = true; - self.call.startBatch(read_batch, readCallback); + if (!this.reading) { + this.reading = true; + var context = { + stream: this + }; + this.call.recvMessageWithContext(context); } } } @@ -332,26 +277,20 @@ util.inherits(ClientDuplexStream, Duplex); * grpc~ClientDuplexStream#metadata * @borrows grpc~ClientUnaryCall#event:status as * grpc~ClientDuplexStream#status - * @param {grpc.internal~Call} call Call object to proxy - * @param {grpc~serialize=} [serialize=identity] Serialization - * function for requests - * @param {grpc~deserialize=} [deserialize=identity] - * Deserialization function for responses + * @param {InterceptingCall} call Exposes gRPC request operations, processed by + * an interceptor stack. */ -function ClientDuplexStream(call, serialize, deserialize) { +function ClientDuplexStream(call) { Duplex.call(this, {objectMode: true}); - this.serialize = common.wrapIgnoreNull(serialize); - this.deserialize = common.wrapIgnoreNull(deserialize); this.call = call; /* Status generated from reading messages from the server. Overrides the * status from the server if not OK */ this.read_status = null; /* Status received from the server. */ this.received_status = null; + var self = this; this.on('finish', function() { - var batch = {}; - batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; - call.startBatch(batch, function() {}); + self.call.halfClose(); }); } @@ -393,8 +332,8 @@ ClientDuplexStream.prototype.getPeer = getPeer; /** * Any client call type - * @typedef {(ClientUnaryCall|ClientReadableStream| - * ClientWritableStream|ClientDuplexStream)} + * @typedef {(grpc~ClientUnaryCall|grpc~ClientReadableStream| + * grpc~ClientWritableStream|grpc~ClientDuplexStream)} * grpc.Client~Call */ @@ -416,45 +355,17 @@ ClientDuplexStream.prototype.getPeer = getPeer; * should be used to make this particular call. */ -/** - * Get a call object built with the provided options. - * @access private - * @param {grpc.Client~CallOptions=} options Options object. - */ -function getCall(channel, method, options) { - var deadline; - var host; - var parent; - var propagate_flags; - var credentials; - if (options) { - deadline = options.deadline; - host = options.host; - parent = _.get(options, 'parent.call'); - propagate_flags = options.propagate_flags; - credentials = options.credentials; - } - if (deadline === undefined) { - deadline = Infinity; - } - var call = new grpc.Call(channel, method, deadline, host, - parent, propagate_flags); - if (credentials) { - call.setCredentials(credentials); - } - return call; -} - /** * A generic gRPC client. Primarily useful as a base class for generated clients * @memberof grpc * @constructor * @param {string} address Server address to connect to - * @param {grpc~ChannelCredentials} credentials Credentials to use to connect to - * the server + * @param {grpc.credentials~ChannelCredentials} credentials Credentials to use + * to connect to the server * @param {Object} options Options to apply to channel creation */ function Client(address, credentials, options) { + var self = this; if (!options) { options = {}; } @@ -467,9 +378,27 @@ function Client(address, credentials, options) { options['grpc.primary_user_agent'] = ''; } options['grpc.primary_user_agent'] += 'grpc-node/' + version; + + // Resolve interceptor options and assign interceptors to each method + var interceptor_providers = options.interceptor_providers || []; + var interceptors = options.interceptors || []; + if (interceptor_providers.length && interceptors.length) { + throw new client_interceptors.InterceptorConfigurationError( + 'Both interceptors and interceptor_providers were passed as options ' + + 'to the client constructor. Only one of these is allowed.'); + } + _.each(self.$method_definitions, function(method_definition, method_name) { + self[method_name].interceptors = client_interceptors + .resolveInterceptorProviders(interceptor_providers, method_definition) + .concat(interceptors); + }); + + // Exclude interceptor options which have already been consumed + var channel_options = _.omit(options, + ['interceptors', 'interceptor_providers']); /* Private fields use $ as a prefix instead of _ because it is an invalid * prefix of a method name */ - this.$channel = new grpc.Channel(address, credentials, options); + this.$channel = new grpc.Channel(address, credentials, channel_options); } exports.Client = Client; @@ -484,7 +413,7 @@ exports.Client = Client; /** * Make a unary request to the given method, using the given serialize * and deserialize functions, with the given argument. - * @param {string} method The name of the method to request + * @param {string} path The path of the method to request * @param {grpc~serialize} serialize The serialization function for * inputs * @param {grpc~deserialize} deserialize The deserialization @@ -493,80 +422,77 @@ exports.Client = Client; * serialize * @param {grpc.Metadata=} metadata Metadata to add to the call * @param {grpc.Client~CallOptions=} options Options map - * @param {grpc.Client~requestCallback} callback The callback to + * @param {grpc.Client~requestCallback} callback The callback * for when the response is received * @return {grpc~ClientUnaryCall} An event emitter for stream related events */ -Client.prototype.makeUnaryRequest = function(method, serialize, deserialize, +Client.prototype.makeUnaryRequest = function(path, serialize, deserialize, argument, metadata, options, callback) { - /* While the arguments are listed in the function signature, those variables - * are not used directly. Instead, ArgueJS processes the arguments - * object. This allows for simple handling of optional arguments in the - * middle of the argument list, and also provides type checking. */ - var args = arguejs({method: String, serialize: Function, - deserialize: Function, - argument: null, metadata: [Metadata, new Metadata()], - options: [Object], callback: Function}, arguments); - var call = getCall(this.$channel, method, args.options); - var emitter = new ClientUnaryCall(call); - metadata = args.metadata.clone(); - var client_batch = {}; - var message = serialize(args.argument); - if (args.options) { - message.grpcWriteFlags = args.options.flags; + if (_.isFunction(options)) { + callback = options; + if (metadata instanceof Metadata) { + options = {}; + } else { + options = metadata; + metadata = new Metadata(); + } + } else if (_.isFunction(metadata)) { + callback = metadata; + metadata = new Metadata(); + options = {}; + } + if (!metadata) { + metadata = new Metadata(); + } + if (!options) { + options = {}; + } + if (!((metadata instanceof Metadata) && + (options instanceof Object) && + (_.isFunction(callback)))) { + throw new Error('Argument mismatch in makeUnaryRequest'); } - client_batch[grpc.opType.SEND_INITIAL_METADATA] = - metadata._getCoreRepresentation(); - client_batch[grpc.opType.SEND_MESSAGE] = message; - client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; - client_batch[grpc.opType.RECV_INITIAL_METADATA] = true; - client_batch[grpc.opType.RECV_MESSAGE] = true; - client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; - call.startBatch(client_batch, function(err, response) { - response.status.metadata = Metadata._fromCoreRepresentation( - response.status.metadata); - var status = response.status; - var error; - var deserialized; - emitter.emit('metadata', Metadata._fromCoreRepresentation( - response.metadata)); - if (status.code === constants.status.OK) { - if (err) { - // Got a batch error, but OK status. Something went wrong - args.callback(err); - return; - } else { - try { - deserialized = deserialize(response.read); - } catch (e) { - /* Change status to indicate bad server response. This will result - * in passing an error to the callback */ - status = { - code: constants.status.INTERNAL, - details: 'Failed to parse server response' - }; - } - } - } - if (status.code !== constants.status.OK) { - error = new Error(status.details); - error.code = status.code; - error.metadata = status.metadata; - args.callback(error); - } else { - args.callback(null, deserialized); - } - emitter.emit('status', status); - }); + var method_name = this.$method_names[path]; + var constructor_interceptors = this[method_name] ? + this[method_name].interceptors : + null; + var method_definition = options.method_definition = { + path: path, + requestStream: false, + responseStream: false, + requestSerialize: serialize, + responseDeserialize: deserialize + }; + + metadata = metadata.clone(); + + var intercepting_call = client_interceptors.getInterceptingCall( + method_definition, + options, + constructor_interceptors, + this.$channel, + callback + ); + var emitter = new ClientUnaryCall(intercepting_call); + var last_listener = client_interceptors.getLastListener( + method_definition, + emitter, + callback + ); + + intercepting_call.start(metadata, last_listener); + intercepting_call.sendMessage(argument); + intercepting_call.halfClose(); + return emitter; }; /** * Make a client stream request to the given method, using the given serialize * and deserialize functions, with the given argument. - * @param {string} method The name of the method to request + * @param {string} path The path of the method to request * @param {grpc~serialize} serialize The serialization function for * inputs * @param {grpc~deserialize} deserialize The deserialization @@ -574,82 +500,76 @@ Client.prototype.makeUnaryRequest = function(method, serialize, deserialize, * @param {grpc.Metadata=} metadata Array of metadata key/value pairs to add to * the call * @param {grpc.Client~CallOptions=} options Options map - * @param {grpc.Client~requestCallback} callback The callback to for when the + * @param {grpc.Client~requestCallback} callback The callback for when the * response is received * @return {grpc~ClientWritableStream} An event emitter for stream related * events */ -Client.prototype.makeClientStreamRequest = function(method, serialize, - deserialize, metadata, - options, callback) { - /* While the arguments are listed in the function signature, those variables - * are not used directly. Instead, ArgueJS processes the arguments - * object. This allows for simple handling of optional arguments in the - * middle of the argument list, and also provides type checking. */ - var args = arguejs({method:String, serialize: Function, - deserialize: Function, - metadata: [Metadata, new Metadata()], - options: [Object], callback: Function}, arguments); - var call = getCall(this.$channel, method, args.options); - metadata = args.metadata.clone(); - var stream = new ClientWritableStream(call, serialize); - var metadata_batch = {}; - metadata_batch[grpc.opType.SEND_INITIAL_METADATA] = - metadata._getCoreRepresentation(); - metadata_batch[grpc.opType.RECV_INITIAL_METADATA] = true; - call.startBatch(metadata_batch, function(err, response) { - if (err) { - // The call has stopped for some reason. A non-OK status will arrive - // in the other batch. - return; - } - stream.emit('metadata', Metadata._fromCoreRepresentation( - response.metadata)); - }); - var client_batch = {}; - client_batch[grpc.opType.RECV_MESSAGE] = true; - client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; - call.startBatch(client_batch, function(err, response) { - response.status.metadata = Metadata._fromCoreRepresentation( - response.status.metadata); - var status = response.status; - var error; - var deserialized; - if (status.code === constants.status.OK) { - if (err) { - // Got a batch error, but OK status. Something went wrong - args.callback(err); - return; - } else { - try { - deserialized = deserialize(response.read); - } catch (e) { - /* Change status to indicate bad server response. This will result - * in passing an error to the callback */ - status = { - code: constants.status.INTERNAL, - details: 'Failed to parse server response' - }; - } - } - } - if (status.code !== constants.status.OK) { - error = new Error(response.status.details); - error.code = status.code; - error.metadata = status.metadata; - args.callback(error); +Client.prototype.makeClientStreamRequest = function(path, serialize, + deserialize, metadata, + options, callback) { + if (_.isFunction(options)) { + callback = options; + if (metadata instanceof Metadata) { + options = {}; } else { - args.callback(null, deserialized); + options = metadata; + metadata = new Metadata(); } - stream.emit('status', status); - }); - return stream; + } else if (_.isFunction(metadata)) { + callback = metadata; + metadata = new Metadata(); + options = {}; + } + if (!metadata) { + metadata = new Metadata(); + } + if (!options) { + options = {}; + } + if (!((metadata instanceof Metadata) && + (options instanceof Object) && + (_.isFunction(callback)))) { + throw new Error('Argument mismatch in makeClientStreamRequest'); + } + + var method_name = this.$method_names[path]; + var constructor_interceptors = this[method_name] ? + this[method_name].interceptors : + null; + var method_definition = options.method_definition = { + path: path, + requestStream: true, + responseStream: false, + requestSerialize: serialize, + responseDeserialize: deserialize + }; + + metadata = metadata.clone(); + + var intercepting_call = client_interceptors.getInterceptingCall( + method_definition, + options, + constructor_interceptors, + this.$channel, + callback + ); + var emitter = new ClientWritableStream(intercepting_call); + var last_listener = client_interceptors.getLastListener( + method_definition, + emitter, + callback + ); + + intercepting_call.start(metadata, last_listener); + + return emitter; }; /** * Make a server stream request to the given method, with the given serialize * and deserialize function, using the given argument - * @param {string} method The name of the method to request + * @param {string} path The path of the method to request * @param {grpc~serialize} serialize The serialization function for inputs * @param {grpc~deserialize} deserialize The deserialization * function for outputs @@ -661,56 +581,58 @@ Client.prototype.makeClientStreamRequest = function(method, serialize, * @return {grpc~ClientReadableStream} An event emitter for stream related * events */ -Client.prototype.makeServerStreamRequest = function(method, serialize, +Client.prototype.makeServerStreamRequest = function(path, serialize, deserialize, argument, metadata, options) { - /* While the arguments are listed in the function signature, those variables - * are not used directly. Instead, ArgueJS processes the arguments - * object. */ - var args = arguejs({method:String, serialize: Function, - deserialize: Function, - argument: null, metadata: [Metadata, new Metadata()], - options: [Object]}, arguments); - var call = getCall(this.$channel, method, args.options); - metadata = args.metadata.clone(); - var stream = new ClientReadableStream(call, deserialize); - var start_batch = {}; - var message = serialize(args.argument); - if (args.options) { - message.grpcWriteFlags = args.options.flags; + if (!(metadata instanceof Metadata)) { + options = metadata; + metadata = new Metadata(); + } + if (!(options instanceof Object)) { + options = {}; + } + if (!((metadata instanceof Metadata) && (options instanceof Object))) { + throw new Error('Argument mismatch in makeServerStreamRequest'); } - start_batch[grpc.opType.SEND_INITIAL_METADATA] = - metadata._getCoreRepresentation(); - start_batch[grpc.opType.RECV_INITIAL_METADATA] = true; - start_batch[grpc.opType.SEND_MESSAGE] = message; - start_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; - call.startBatch(start_batch, function(err, response) { - if (err) { - // The call has stopped for some reason. A non-OK status will arrive - // in the other batch. - return; - } - stream.emit('metadata', Metadata._fromCoreRepresentation( - response.metadata)); - }); - var status_batch = {}; - status_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; - call.startBatch(status_batch, function(err, response) { - if (err) { - stream.emit('error', err); - return; - } - response.status.metadata = Metadata._fromCoreRepresentation( - response.status.metadata); - stream._receiveStatus(response.status); - }); - return stream; -}; + var method_name = this.$method_names[path]; + var constructor_interceptors = this[method_name] ? + this[method_name].interceptors : + null; + var method_definition = options.method_definition = { + path: path, + requestStream: false, + responseStream: true, + requestSerialize: serialize, + responseDeserialize: deserialize + }; + + metadata = metadata.clone(); + + var emitter = new ClientReadableStream(); + var intercepting_call = client_interceptors.getInterceptingCall( + method_definition, + options, + constructor_interceptors, + this.$channel, + emitter + ); + emitter.call = intercepting_call; + var last_listener = client_interceptors.getLastListener( + method_definition, + emitter + ); + + intercepting_call.start(metadata, last_listener); + intercepting_call.sendMessage(argument); + intercepting_call.halfClose(); + + return emitter; +}; /** * Make a bidirectional stream request with this method on the given channel. - * @param {string} method The name of the method to request + * @param {string} path The path of the method to request * @param {grpc~serialize} serialize The serialization function for inputs * @param {grpc~deserialize} deserialize The deserialization * function for outputs @@ -719,44 +641,51 @@ Client.prototype.makeServerStreamRequest = function(method, serialize, * @param {grpc.Client~CallOptions=} options Options map * @return {grpc~ClientDuplexStream} An event emitter for stream related events */ -Client.prototype.makeBidiStreamRequest = function(method, serialize, +Client.prototype.makeBidiStreamRequest = function(path, serialize, deserialize, metadata, options) { - /* While the arguments are listed in the function signature, those variables - * are not used directly. Instead, ArgueJS processes the arguments - * object. */ - var args = arguejs({method:String, serialize: Function, - deserialize: Function, - metadata: [Metadata, new Metadata()], - options: [Object]}, arguments); - var call = getCall(this.$channel, method, args.options); - metadata = args.metadata.clone(); - var stream = new ClientDuplexStream(call, serialize, deserialize); - var start_batch = {}; - start_batch[grpc.opType.SEND_INITIAL_METADATA] = - metadata._getCoreRepresentation(); - start_batch[grpc.opType.RECV_INITIAL_METADATA] = true; - call.startBatch(start_batch, function(err, response) { - if (err) { - // The call has stopped for some reason. A non-OK status will arrive - // in the other batch. - return; - } - stream.emit('metadata', Metadata._fromCoreRepresentation( - response.metadata)); - }); - var status_batch = {}; - status_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; - call.startBatch(status_batch, function(err, response) { - if (err) { - stream.emit('error', err); - return; - } - response.status.metadata = Metadata._fromCoreRepresentation( - response.status.metadata); - stream._receiveStatus(response.status); - }); - return stream; + if (!(metadata instanceof Metadata)) { + options = metadata; + metadata = new Metadata(); + } + if (!(options instanceof Object)) { + options = {}; + } + if (!((metadata instanceof Metadata) && (options instanceof Object))) { + throw new Error('Argument mismatch in makeBidiStreamRequest'); + } + + var method_name = this.$method_names[path]; + var constructor_interceptors = this[method_name] ? + this[method_name].interceptors : + null; + var method_definition = options.method_definition = { + path: path, + requestStream: true, + responseStream: true, + requestSerialize: serialize, + responseDeserialize: deserialize + }; + + metadata = metadata.clone(); + + var emitter = new ClientDuplexStream(); + var intercepting_call = client_interceptors.getInterceptingCall( + method_definition, + options, + constructor_interceptors, + this.$channel, + emitter + ); + emitter.call = intercepting_call; + var last_listener = client_interceptors.getLastListener( + method_definition, + emitter + ); + + intercepting_call.start(metadata, last_listener); + + return emitter; }; /** @@ -800,7 +729,10 @@ Client.prototype.waitForReady = function(deadline, callback) { self.$channel.watchConnectivityState(new_state, deadline, checkState); } }; - checkState(); + /* Force a single round of polling to ensure that the channel state is up + * to date */ + grpc.forcePoll(); + setImmediate(checkState); }; /** @@ -809,10 +741,10 @@ Client.prototype.waitForReady = function(deadline, callback) { * @private */ var requester_funcs = { - unary: Client.prototype.makeUnaryRequest, - server_stream: Client.prototype.makeServerStreamRequest, - client_stream: Client.prototype.makeClientStreamRequest, - bidi: Client.prototype.makeBidiStreamRequest + [methodTypes.UNARY]: Client.prototype.makeUnaryRequest, + [methodTypes.CLIENT_STREAMING]: Client.prototype.makeClientStreamRequest, + [methodTypes.SERVER_STREAMING]: Client.prototype.makeServerStreamRequest, + [methodTypes.BIDI_STREAMING]: Client.prototype.makeBidiStreamRequest }; function getDefaultValues(metadata, options) { @@ -828,7 +760,7 @@ function getDefaultValues(metadata, options) { * @access private */ var deprecated_request_wrap = { - unary: function(makeUnaryRequest) { + [methodTypes.UNARY]: function(makeUnaryRequest) { return function makeWrappedUnaryRequest(argument, callback, metadata, options) { /* jshint validthis: true */ @@ -837,7 +769,7 @@ var deprecated_request_wrap = { opt_args.options, callback); }; }, - client_stream: function(makeServerStreamRequest) { + [methodTypes.CLIENT_STREAMING]: function(makeServerStreamRequest) { return function makeWrappedClientStreamRequest(callback, metadata, options) { /* jshint validthis: true */ @@ -846,8 +778,8 @@ var deprecated_request_wrap = { opt_args.options, callback); }; }, - server_stream: _.identity, - bidi: _.identity + [methodTypes.SERVER_STREAMING]: _.identity, + [methodTypes.BIDI_STREAMING]: _.identity }; /** @@ -882,38 +814,29 @@ exports.makeClientConstructor = function(methods, serviceName, } util.inherits(ServiceClient, Client); + ServiceClient.prototype.$method_definitions = methods; + ServiceClient.prototype.$method_names = {}; _.each(methods, function(attrs, name) { - var method_type; if (_.startsWith(name, '$')) { throw new Error('Method names cannot start with $'); } - if (attrs.requestStream) { - if (attrs.responseStream) { - method_type = 'bidi'; - } else { - method_type = 'client_stream'; - } - } else { - if (attrs.responseStream) { - method_type = 'server_stream'; - } else { - method_type = 'unary'; - } - } - var serialize = attrs.requestSerialize; - var deserialize = attrs.responseDeserialize; + var method_type = common.getMethodType(attrs); var method_func = _.partial(requester_funcs[method_type], attrs.path, - serialize, deserialize); + attrs.requestSerialize, + attrs.responseDeserialize); if (class_options.deprecatedArgumentOrder) { - ServiceClient.prototype[name] = deprecated_request_wrap(method_func); + ServiceClient.prototype[name] = + deprecated_request_wrap[method_type](method_func); } else { ServiceClient.prototype[name] = method_func; } + ServiceClient.prototype.$method_names[attrs.path] = name; // Associate all provided attributes with the method _.assign(ServiceClient.prototype[name], attrs); if (attrs.originalName) { - ServiceClient.prototype[attrs.originalName] = ServiceClient.prototype[name]; + ServiceClient.prototype[attrs.originalName] = + ServiceClient.prototype[name]; } }); @@ -926,7 +849,7 @@ exports.makeClientConstructor = function(methods, serviceName, * Return the underlying channel object for the specified client * @memberof grpc * @alias grpc~getClientChannel - * @param {Client} client + * @param {grpc.Client} client The client * @return {Channel} The channel * @see grpc.Client#getChannel */ @@ -934,6 +857,17 @@ exports.getClientChannel = function(client) { return Client.prototype.getChannel.call(client); }; +/** + * Gets a map of client method names to interceptor stacks. + * @param {grpc.Client} client + * @returns {Object.} + */ +exports.getClientInterceptors = function(client) { + return _.mapValues(client.$method_definitions, function(def, name) { + return client[name].interceptors; + }); +}; + /** * Wait for the client to be ready. The callback will be called when the * client has successfully connected to the server, and it will be called @@ -942,7 +876,7 @@ exports.getClientChannel = function(client) { * start connecting if it has not already done so. * @memberof grpc * @alias grpc~waitForClientReady - * @param {Client} client The client to wait on + * @param {grpc.Client} client The client to wait on * @param {grpc~Deadline} deadline When to stop waiting for a connection. Pass * Infinity to wait forever. * @param {function(Error)} callback The callback to call when done attempting @@ -952,3 +886,8 @@ exports.getClientChannel = function(client) { exports.waitForClientReady = function(client, deadline, callback) { Client.prototype.waitForReady.call(client, deadline, callback); }; + +exports.StatusBuilder = client_interceptors.StatusBuilder; +exports.ListenerBuilder = client_interceptors.ListenerBuilder; +exports.RequesterBuilder = client_interceptors.RequesterBuilder; +exports.InterceptingCall = client_interceptors.InterceptingCall; diff --git a/packages/grpc-native-core/src/client_interceptors.js b/packages/grpc-native-core/src/client_interceptors.js new file mode 100644 index 00000000..4f45b752 --- /dev/null +++ b/packages/grpc-native-core/src/client_interceptors.js @@ -0,0 +1,1494 @@ +/** + * @license + * Copyright 2018 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. + * + */ + +/** + * Client Interceptors + * + * This module describes the interceptor framework for clients. + * An interceptor is a function which takes an options object and a nextCall + * function and returns an InterceptingCall: + * + * ``` + * var interceptor = function(options, nextCall) { + * return new InterceptingCall(nextCall(options)); + * } + * ``` + * + * The interceptor function must return an InterceptingCall object. Returning + * `new InterceptingCall(nextCall(options))` will satisfy the contract (but + * provide no interceptor functionality). `nextCall` is a function which will + * generate the next interceptor in the chain. + * + * To implement interceptor functionality, create a requester and pass it to + * the InterceptingCall constructor: + * + * `return new InterceptingCall(nextCall(options), requester);` + * + * A requester is a POJO with zero or more of the following methods: + * + * `start(metadata, listener, next)` + * * To continue, call next(metadata, listener). Listeners are described + * * below. + * + * `sendMessage(message, next)` + * * To continue, call next(message). + * + * `halfClose(next)` + * * To continue, call next(). + * + * `cancel(message, next)` + * * To continue, call next(). + * + * A listener is a POJO with one or more of the following methods: + * + * `onReceiveMetadata(metadata, next)` + * * To continue, call next(metadata) + * + * `onReceiveMessage(message, next)` + * * To continue, call next(message) + * + * `onReceiveStatus(status, next)` + * * To continue, call next(status) + * + * A listener is provided by the requester's `start` method. The provided + * listener implements all the inbound interceptor methods, which can be called + * to short-circuit the gRPC call. + * + * Three usage patterns are supported for listeners: + * 1) Pass the listener along without modification: `next(metadata, listener)`. + * In this case the interceptor declines to intercept any inbound operations. + * 2) Create a new listener with one or more inbound interceptor methods and + * pass it to `next`. In this case the interceptor will fire on the inbound + * operations implemented in the new listener. + * 3) Make direct inbound calls to the provided listener's methods. This + * short-circuits the interceptor stack. + * + * Do not modify the listener passed in. Either pass it along unmodified, + * ignore it, or call methods on it to short-circuit the call. + * + * To intercept errors, implement the `onReceiveStatus` method and test for + * `status.code !== grpc.status.OK`. + * + * To intercept trailers, examine `status.metadata` in the `onReceiveStatus` + * method. + * + * This is a trivial implementation of all interceptor methods: + * var interceptor = function(options, nextCall) { + * return new InterceptingCall(nextCall(options), { + * start: function(metadata, listener, next) { + * next(metadata, { + * onReceiveMetadata: function (metadata, next) { + * next(metadata); + * }, + * onReceiveMessage: function (message, next) { + * next(message); + * }, + * onReceiveStatus: function (status, next) { + * next(status); + * }, + * }); + * }, + * sendMessage: function(message, next) { + * next(message); + * }, + * halfClose: function(next) { + * next(); + * }, + * cancel: function(message, next) { + * next(); + * } + * }); + * }; + * + * This is an interceptor with a single method: + * var interceptor = function(options, nextCall) { + * return new InterceptingCall(nextCall(options), { + * sendMessage: function(message, next) { + * next(message); + * } + * }); + * }; + * + * Builders are provided for convenience: StatusBuilder, ListenerBuilder, + * and RequesterBuilder + * + * gRPC client operations use this mapping to interceptor methods: + * + * grpc.opType.SEND_INITIAL_METADATA -> start + * grpc.opType.SEND_MESSAGE -> sendMessage + * grpc.opType.SEND_CLOSE_FROM_CLIENT -> halfClose + * grpc.opType.RECV_INITIAL_METADATA -> onReceiveMetadata + * grpc.opType.RECV_MESSAGE -> onReceiveMessage + * grpc.opType.RECV_STATUS_ON_CLIENT -> onReceiveStatus + * + * @module + */ + +'use strict'; + +var _ = require('lodash'); +var grpc = require('./grpc_extension'); +var Metadata = require('./metadata'); +var constants = require('./constants'); +var common = require('./common'); +var methodTypes = constants.methodTypes; +var EventEmitter = require('events').EventEmitter; + +/** + * A custom error thrown when interceptor configuration fails. + * @param {string} message The error message + * @param {object=} extra + * @constructor + */ +var InterceptorConfigurationError = + function InterceptorConfigurationError(message, extra) { + Error.captureStackTrace(this, this.constructor); + this.name = this.constructor.name; + this.message = message; + this.extra = extra; + }; + +require('util').inherits(InterceptorConfigurationError, Error); + +/** + * A builder for gRPC status objects. + * @constructor + */ +function StatusBuilder() { + this.code = null; + this.details = null; + this.metadata = null; +} + +/** + * Adds a status code to the builder. + * @param {number} code The status code. + * @return {StatusBuilder} + */ +StatusBuilder.prototype.withCode = function(code) { + this.code = code; + return this; +}; + +/** + * Adds details to the builder. + * @param {string} details A status message. + * @return {StatusBuilder} + */ +StatusBuilder.prototype.withDetails = function(details) { + this.details = details; + return this; +}; + +/** + * Adds metadata to the builder. + * @param {Metadata} metadata The gRPC status metadata. + * @return {StatusBuilder} + */ +StatusBuilder.prototype.withMetadata = function(metadata) { + this.metadata = metadata; + return this; +}; + +/** + * Builds the status object. + * @return {grpc~StatusObject} A gRPC status. + */ +StatusBuilder.prototype.build = function() { + var status = {}; + if (this.code !== undefined) { + status.code = this.code; + } + if (this.details) { + status.details = this.details; + } + if (this.metadata) { + status.metadata = this.metadata; + } + return status; +}; + +/** + * A builder for listener interceptors. + * @constructor + */ +function ListenerBuilder() { + this.metadata = null; + this.message = null; + this.status = null; +} + +/** + * Adds an onReceiveMetadata method to the builder. + * @param {MetadataListener} on_receive_metadata A listener method for + * receiving metadata. + * @return {ListenerBuilder} + */ +ListenerBuilder.prototype.withOnReceiveMetadata = + function(on_receive_metadata) { + this.metadata = on_receive_metadata; + return this; + }; + +/** + * Adds an onReceiveMessage method to the builder. + * @param {MessageListener} on_receive_message A listener method for receiving + * messages. + * @return {ListenerBuilder} + */ +ListenerBuilder.prototype.withOnReceiveMessage = function(on_receive_message) { + this.message = on_receive_message; + return this; +}; + +/** + * Adds an onReceiveStatus method to the builder. + * @param {StatusListener} on_receive_status A listener method for receiving + * status. + * @return {ListenerBuilder} + */ +ListenerBuilder.prototype.withOnReceiveStatus = function(on_receive_status) { + this.status = on_receive_status; + return this; +}; + +/** + * Builds the call listener. + * @return {grpc~Listener} + */ +ListenerBuilder.prototype.build = function() { + var self = this; + var listener = {}; + listener.onReceiveMetadata = self.metadata; + listener.onReceiveMessage = self.message; + listener.onReceiveStatus = self.status; + return listener; +}; + +/** + * A builder for the outbound methods of an interceptor. + * @constructor + */ +function RequesterBuilder() { + this.start = null; + this.message = null; + this.half_close = null; + this.cancel = null; +} + +/** + * Add a metadata requester to the builder. + * @param {MetadataRequester} start A requester method for handling metadata. + * @return {RequesterBuilder} + */ +RequesterBuilder.prototype.withStart = function(start) { + this.start = start; + return this; +}; + +/** + * Add a message requester to the builder. + * @param {MessageRequester} send_message A requester method for handling + * messages. + * @return {RequesterBuilder} + */ +RequesterBuilder.prototype.withSendMessage = function(send_message) { + this.message = send_message; + return this; +}; + +/** + * Add a close requester to the builder. + * @param {CloseRequester} half_close A requester method for handling client + * close. + * @return {RequesterBuilder} + */ +RequesterBuilder.prototype.withHalfClose = function(half_close) { + this.half_close = half_close; + return this; +}; + +/** + * Add a cancel requester to the builder. + * @param {CancelRequester} cancel A requester method for handling `cancel` + * @return {RequesterBuilder} + */ +RequesterBuilder.prototype.withCancel = function(cancel) { + this.cancel = cancel; + return this; +}; + +/** + * Builds the requester's interceptor methods. + * @return {grpc~Requester} + */ +RequesterBuilder.prototype.build = function() { + var requester = {}; + requester.start = this.start; + requester.sendMessage = this.message; + requester.halfClose = this.half_close; + requester.cancel = this.cancel; + return requester; +}; + +/** + * Transforms a list of interceptor providers into interceptors. + * @param {InterceptorProvider[]} providers + * @param {grpc~MethodDefinition} method_definition + * @return {null|Interceptor[]} + */ +var resolveInterceptorProviders = function(providers, method_definition) { + if (!_.isArray(providers)) { + return null; + } + var interceptors = []; + for (var i = 0; i < providers.length; i++) { + var provider = providers[i]; + var interceptor = provider(method_definition); + if (interceptor) { + interceptors.push(interceptor); + } + } + return interceptors; +}; + +/** + * Resolves interceptor options at call invocation time + * @param {grpc.Client~CallOptions} options The call options passed to a gRPC + * call. + * @param {Interceptor[]} [options.interceptors] + * @param {InterceptorProvider[]} [options.interceptor_providers] + * @param {grpc~MethodDefinition} method_definition + * @return {null|function[]} + */ +var resolveInterceptorOptions = function(options, method_definition) { + var provided = resolveInterceptorProviders(options.interceptor_providers, + method_definition); + if (_.isArray(options.interceptors) && _.isArray(provided)) { + throw new InterceptorConfigurationError( + 'Both interceptors and interceptor_providers were passed as options ' + + 'to the call invocation. Only one of these is allowed.'); + } + if (_.isArray(options.interceptors)) { + return options.interceptors; + } + if (_.isArray(provided)) { + return provided; + } + return null; +}; + +/** + * A chainable gRPC call proxy which will delegate to an optional requester + * object. By default, interceptor methods will chain to next_call. If a + * requester is provided which implements an interceptor method, that + * requester method will be executed as part of the chain. + * @param {InterceptingCall|null} next_call The next call in the chain + * @param {grpc~Requester=} requester Interceptor methods to handle request + * operations. + * @constructor + */ +function InterceptingCall(next_call, requester) { + this.next_call = next_call; + this.requester = requester; +} + +/** + * Get the next method in the chain or a no-op function if we are at the end + * of the chain + * @param {string} method_name + * @return {function} The next method in the chain + * @private + */ +InterceptingCall.prototype._getNextCall = function(method_name) { + return this.next_call ? + this.next_call[method_name].bind(this.next_call) : + function(){}; +}; + +/** + * Call the next method in the chain. This will either be on the next + * InterceptingCall (next_call), or the requester if the requester + * implements the method. + * @param {string} method_name The name of the interceptor method + * @param {array=} args Payload arguments for the operation + * @param {function=} next The next InterceptingCall's method + * @return {null} + * @private + */ +InterceptingCall.prototype._callNext = function(method_name, args, next) { + var args_array = args || []; + var next_call = next ? next : this._getNextCall(method_name); + if (this.requester && this.requester[method_name]) { + // Avoid using expensive `apply` calls + var num_args = args_array.length; + switch (num_args) { + case 0: + return this.requester[method_name](next_call); + case 1: + return this.requester[method_name](args_array[0], next_call); + case 2: + return this.requester[method_name](args_array[0], args_array[1], + next_call); + } + } else { + return next_call(args_array[0], args_array[1]); + } +}; + +/** + * Starts a call through the outbound interceptor chain and adds an element to + * the reciprocal inbound listener chain. + * @param {grpc.Metadata} metadata The outgoing metadata. + * @param {grpc~Listener} listener An intercepting listener for inbound + * operations. + */ +InterceptingCall.prototype.start = function(metadata, listener) { + var self = this; + + // If the listener provided is an InterceptingListener, use it. Otherwise, we + // must be at the end of the listener chain, and any listener operations + // should be terminated in an EndListener. + var next_listener = _getInterceptingListener(listener, new EndListener()); + + // Build the next method in the interceptor chain + var next = function(metadata, current_listener) { + // If there is a next call in the chain, run it. Otherwise do nothing. + if (self.next_call) { + // Wire together any listener provided with the next listener + var listener = _getInterceptingListener(current_listener, next_listener); + self.next_call.start(metadata, listener); + } + }; + this._callNext('start', [metadata, next_listener], next); +}; + +/** + * Pass a message through the interceptor chain. + * @param {jspb.Message} message + */ +InterceptingCall.prototype.sendMessage = function(message) { + this._callNext('sendMessage', [message]); +}; + +/** + * Run a close operation through the interceptor chain + */ +InterceptingCall.prototype.halfClose = function() { + this._callNext('halfClose'); +}; + +/** + * Run a cancel operation through the interceptor chain + */ +InterceptingCall.prototype.cancel = function() { + this._callNext('cancel'); +}; + +/** + * Run a cancelWithStatus operation through the interceptor chain. + * @param {grpc~StatusObject} status + * @param {string} message + */ +InterceptingCall.prototype.cancelWithStatus = function(status, message) { + this._callNext('cancelWithStatus', [status, message]); +}; + +/** + * Pass a getPeer call down to the base gRPC call (should not be intercepted) + * @return {object} + */ +InterceptingCall.prototype.getPeer = function() { + return this._callNext('getPeer'); +}; + +/** + * For streaming calls, we need to transparently pass the stream's context + * through the interceptor chain. Passes the context between InterceptingCalls + * but hides it from any requester implementations. + * @param {object} context Carries objects needed for streaming operations. + * @param {jspb.Message} message The message to send. + */ +InterceptingCall.prototype.sendMessageWithContext = function(context, message) { + var next = this.next_call ? + this.next_call.sendMessageWithContext.bind(this.next_call, context) : + context; + this._callNext('sendMessage', [message], next); +}; + +/** + * For receiving streaming messages, we need to seed the base interceptor with + * the streaming context to create a RECV_MESSAGE batch. + * @param {object} context Carries objects needed for streaming operations + */ +InterceptingCall.prototype.recvMessageWithContext = function(context) { + this._callNext('recvMessageWithContext', [context]); +}; + +/** + * A chain-able listener object which will delegate to a custom listener when + * appropriate. + * @param {InterceptingListener|null} next_listener The next + * InterceptingListener in the chain + * @param {grpc~Listener=} delegate A custom listener object which may implement + * specific operations + * @constructor + */ +function InterceptingListener(next_listener, delegate) { + this.delegate = delegate || {}; + this.next_listener = next_listener; +} + +/** + * Get the next method in the chain or a no-op function if we are at the end + * of the chain. + * @param {string} method_name The name of the listener method. + * @return {function} The next method in the chain + * @private + */ +InterceptingListener.prototype._getNextListener = function(method_name) { + return this.next_listener ? + this.next_listener[method_name].bind(this.next_listener) : + function(){}; +}; + +/** + * Call the next method in the chain. This will either be on the next + * InterceptingListener (next_listener), or the requester if the requester + * implements the method. + * @param {string} method_name The name of the interceptor method + * @param {array=} args Payload arguments for the operation + * @param {function=} next The next InterceptingListener's method + * @return {null} + * @private + */ +InterceptingListener.prototype._callNext = function(method_name, args, next) { + var args_array = args || []; + var next_listener = next ? next : this._getNextListener(method_name); + if (this.delegate && this.delegate[method_name]) { + // Avoid using expensive `apply` calls + var num_args = args_array.length; + switch (num_args) { + case 0: + return this.delegate[method_name](next_listener); + case 1: + return this.delegate[method_name](args_array[0], next_listener); + case 2: + return this.delegate[method_name](args_array[0], args_array[1], + next_listener); + } + } else { + return next_listener(args_array[0], args_array[1]); + } +}; +/** + * Inbound metadata receiver. + * @param {Metadata} metadata + */ +InterceptingListener.prototype.onReceiveMetadata = function(metadata) { + this._callNext('onReceiveMetadata', [metadata]); +}; + +/** + * Inbound message receiver. + * @param {jspb.Message} message + */ +InterceptingListener.prototype.onReceiveMessage = function(message) { + this._callNext('onReceiveMessage', [message]); +}; + +/** + * When intercepting streaming message, we need to pass the streaming context + * transparently along the chain. Hides the context from the delegate listener + * methods. + * @param {object} context Carries objects needed for streaming operations. + * @param {jspb.Message} message The message received. + */ +InterceptingListener.prototype.recvMessageWithContext = function(context, + message) { + var fallback = this.next_listener.recvMessageWithContext; + var next_method = this.next_listener ? + fallback.bind(this.next_listener, context) : + context; + if (this.delegate.onReceiveMessage) { + this.delegate.onReceiveMessage(message, next_method, context); + } else { + next_method(message); + } +}; + +/** + * Inbound status receiver. + * @param {grpc~StatusObject} status + */ +InterceptingListener.prototype.onReceiveStatus = function(status) { + this._callNext('onReceiveStatus', [status]); +}; + +/** + * A dead-end listener used to terminate a call chain. Used when an interceptor + * creates a branch chain, when the branch returns the listener chain will + * terminate here. + * @constructor + */ +function EndListener() {} +EndListener.prototype.onReceiveMetadata = function(){}; +EndListener.prototype.onReceiveMessage = function(){}; +EndListener.prototype.onReceiveStatus = function(){}; +EndListener.prototype.recvMessageWithContext = function(){}; + +/** + * Get a call object built with the provided options. + * @param {grpc.Channel} channel + * @param {string} path + * @param {grpc.Client~CallOptions=} options Options object. + */ +function getCall(channel, path, options) { + var deadline; + var host; + var parent; + var propagate_flags; + var credentials; + if (options) { + deadline = options.deadline; + host = options.host; + parent = _.get(options, 'parent.call'); + propagate_flags = options.propagate_flags; + credentials = options.credentials; + } + if (deadline === undefined) { + deadline = Infinity; + } + var call = new grpc.Call(channel, path, deadline, host, + parent, propagate_flags); + if (credentials) { + call.setCredentials(credentials); + } + return call; +} + +var OP_DEPENDENCIES = { + [grpc.opType.SEND_MESSAGE]: [grpc.opType.SEND_INITIAL_METADATA], + [grpc.opType.SEND_CLOSE_FROM_CLIENT]: [grpc.opType.SEND_MESSAGE], + [grpc.opType.RECV_MESSAGE]: [grpc.opType.SEND_INITIAL_METADATA] +}; + +/** + * Produces a callback triggered by streaming response messages. + * @private + * @param {EventEmitter} emitter + * @param {grpc.internal~Call} call + * @param {function} get_listener Returns a grpc~Listener. + * @param {grpc~deserialize} deserialize + * @return {Function} + */ +function _getStreamReadCallback(emitter, call, get_listener, deserialize) { + return function (err, response) { + if (err) { + // Something has gone wrong. Stop reading and wait for status + emitter.finished = true; + emitter._readsDone(); + return; + } + var data = response.read; + var deserialized; + try { + deserialized = deserialize(data); + } catch (e) { + emitter._readsDone({ + code: constants.status.INTERNAL, + details: 'Failed to parse server response' + }); + return; + } + if (data === null) { + emitter._readsDone(); + return; + } + var listener = get_listener(); + var context = { + call: call, + listener: listener + }; + listener.recvMessageWithContext(context, deserialized); + }; +} + +/** + * Tests whether a batch can be started. + * @private + * @param {number[]} batch_ops The operations in the batch we are checking. + * @param {number[]} completed_ops Previously completed operations. + * @return {boolean} + */ +function _areBatchRequirementsMet(batch_ops, completed_ops) { + var dependencies = _.flatMap(batch_ops, function(op) { + return OP_DEPENDENCIES[op] || []; + }); + for (var i = 0; i < dependencies.length; i++) { + var required_dep = dependencies[i]; + if (batch_ops.indexOf(required_dep) === -1 && + completed_ops.indexOf(required_dep) === -1) { + return false; + } + } + return true; +} + +/** + * Enforces the order of operations for synchronous requests. If a batch's + * operations cannot be started because required operations have not started + * yet, the batch is deferred until requirements are met. + * @private + * @param {grpc.Client~Call} call + * @param {object} batch + * @param {object} batch_state + * @param {number[]} [batch_state.completed_ops] The ops already sent. + * @param {object} [batch_state.deferred_batches] Batches to be sent after + * their dependencies are fulfilled. + * @param {function} callback + * @return {object} + */ +function _startBatchIfReady(call, batch, batch_state, callback) { + var completed_ops = batch_state.completed_ops; + var deferred_batches = batch_state.deferred_batches; + var batch_ops = _.map(_.keys(batch), Number); + if (_areBatchRequirementsMet(batch_ops, completed_ops)) { + // Dependencies are met, start the batch and any deferred batches whose + // dependencies are met as a result. + call.startBatch(batch, callback); + completed_ops = _.union(completed_ops, batch_ops); + deferred_batches = _.flatMap(deferred_batches, function(deferred_batch) { + var deferred_batch_ops = _.map(_.keys(deferred_batch), Number); + if (_areBatchRequirementsMet(deferred_batch_ops, completed_ops)) { + call.startBatch(deferred_batch.batch, deferred_batch.callback); + return []; + } + return [deferred_batch]; + }); + } else { + // Dependencies are not met, defer the batch + deferred_batches = deferred_batches.concat({ + batch: batch, + callback: callback + }); + } + return { + completed_ops: completed_ops, + deferred_batches: deferred_batches + }; +} + +/** + * Produces an interceptor which will start gRPC batches for unary calls. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {grpc.Channel} channel + * @param {EventEmitter} emitter + * @param {function} callback + * @return {Interceptor} + */ +function _getUnaryInterceptor(method_definition, channel, emitter, callback) { + var serialize = method_definition.requestSerialize; + var deserialize = method_definition.responseDeserialize; + return function (options) { + var call = getCall(channel, method_definition.path, options); + var first_listener; + var final_requester = {}; + var batch_state = { + completed_ops: [], + deferred_batches: [] + }; + final_requester.start = function (metadata, listener) { + var batch = { + [grpc.opType.SEND_INITIAL_METADATA]: + metadata._getCoreRepresentation(), + }; + first_listener = listener; + batch_state = _startBatchIfReady(call, batch, batch_state, + function() {}); + }; + final_requester.sendMessage = function (message) { + var batch = { + [grpc.opType.SEND_MESSAGE]: serialize(message), + }; + batch_state = _startBatchIfReady(call, batch, batch_state, + function() {}); + }; + final_requester.halfClose = function () { + var batch = { + [grpc.opType.SEND_CLOSE_FROM_CLIENT]: true, + [grpc.opType.RECV_INITIAL_METADATA]: true, + [grpc.opType.RECV_MESSAGE]: true, + [grpc.opType.RECV_STATUS_ON_CLIENT]: true + }; + var callback = function (err, response) { + response.status.metadata = Metadata._fromCoreRepresentation( + response.status.metadata); + var status = response.status; + var deserialized; + if (status.code === constants.status.OK) { + if (err) { + // Got a batch error, but OK status. Something went wrong + callback(err); + return; + } else { + try { + deserialized = deserialize(response.read); + } catch (e) { + /* Change status to indicate bad server response. This + * will result in passing an error to the callback */ + status = { + code: constants.status.INTERNAL, + details: 'Failed to parse server response' + }; + } + } + } + response.metadata = + Metadata._fromCoreRepresentation(response.metadata); + first_listener.onReceiveMetadata(response.metadata); + first_listener.onReceiveMessage(deserialized); + first_listener.onReceiveStatus(status); + }; + batch_state = _startBatchIfReady(call, batch, batch_state, callback); + }; + final_requester.cancel = function () { + call.cancel(); + }; + final_requester.getPeer = function () { + return call.getPeer(); + }; + return new InterceptingCall(null, final_requester); + }; +} + +/** + * Produces an interceptor which will start gRPC batches for client streaming + * calls. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {grpc.Channel} channel + * @param {EventEmitter} emitter + * @param {function} callback + * @return {Interceptor} + */ +function _getClientStreamingInterceptor(method_definition, channel, emitter, + callback) { + var serialize = common.wrapIgnoreNull(method_definition.requestSerialize); + var deserialize = method_definition.responseDeserialize; + return function (options) { + var first_listener; + var call = getCall(channel, method_definition.path, options); + var final_requester = {}; + final_requester.start = function (metadata, listener) { + var metadata_batch = { + [grpc.opType.SEND_INITIAL_METADATA]: metadata._getCoreRepresentation(), + [grpc.opType.RECV_INITIAL_METADATA]: true + }; + first_listener = listener; + call.startBatch(metadata_batch, function (err, response) { + if (err) { + // The call has stopped for some reason. A non-OK status will arrive + // in the other batch. + return; + } + response.metadata = Metadata._fromCoreRepresentation(response.metadata); + listener.onReceiveMetadata(response.metadata); + }); + var recv_batch = {}; + recv_batch[grpc.opType.RECV_MESSAGE] = true; + recv_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; + call.startBatch(recv_batch, function (err, response) { + response.status.metadata = Metadata._fromCoreRepresentation( + response.status.metadata); + var status = response.status; + var deserialized; + if (status.code === constants.status.OK) { + if (err) { + // Got a batch error, but OK status. Something went wrong + callback(err); + return; + } else { + try { + deserialized = deserialize(response.read); + } catch (e) { + /* Change status to indicate bad server response. This will result + * in passing an error to the callback */ + status = { + code: constants.status.INTERNAL, + details: 'Failed to parse server response' + }; + } + } + } + listener.onReceiveMessage(deserialized); + listener.onReceiveStatus(status); + }); + }; + final_requester.sendMessage = function (chunk, context) { + var message; + var callback = (context && context.callback) ? + context.callback : + function () { }; + var encoding = (context && context.encoding) ? + context.encoding : + ''; + try { + message = serialize(chunk); + } catch (e) { + /* Sending this error to the server and emitting it immediately on the + client may put the call in a slightly weird state on the client side, + but passing an object that causes a serialization failure is a misuse + of the API anyway, so that's OK. The primary purpose here is to give + the programmer a useful error and to stop the stream properly */ + call.cancelWithStatus(constants.status.INTERNAL, + 'Serialization failure'); + callback(e); + return; + } + if (_.isFinite(encoding)) { + /* Attach the encoding if it is a finite number. This is the closest we + * can get to checking that it is valid flags */ + message.grpcWriteFlags = encoding; + } + var batch = { + [grpc.opType.SEND_MESSAGE]: message + }; + call.startBatch(batch, function (err, event) { + callback(err, event); + }); + }; + final_requester.halfClose = function () { + var batch = { + [grpc.opType.SEND_CLOSE_FROM_CLIENT]: true + }; + call.startBatch(batch, function () { }); + }; + final_requester.cancel = function () { + call.cancel(); + }; + final_requester.getPeer = function() { + return call.getPeer(); + }; + return new InterceptingCall(null, final_requester); + }; +} + +/** + * Produces an interceptor which will start gRPC batches for server streaming + * calls. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {grpc.Channel} channel + * @param {EventEmitter} emitter + * @return {Interceptor} + */ +function _getServerStreamingInterceptor(method_definition, channel, emitter) { + var deserialize = common.wrapIgnoreNull( + method_definition.responseDeserialize); + var serialize = method_definition.requestSerialize; + return function (options) { + var batch_state = { + completed_ops: [], + deferred_batches: [] + }; + var call = getCall(channel, method_definition.path, options); + var final_requester = {}; + var first_listener; + var get_listener = function() { + return first_listener; + }; + final_requester.start = function(metadata, listener) { + first_listener = listener; + metadata = metadata.clone(); + var metadata_batch = { + [grpc.opType.SEND_INITIAL_METADATA]: metadata._getCoreRepresentation(), + [grpc.opType.RECV_INITIAL_METADATA]: true + }; + var callback = function(err, response) { + if (err) { + // The call has stopped for some reason. A non-OK status will arrive + // in the other batch. + return; + } + first_listener.onReceiveMetadata( + Metadata._fromCoreRepresentation(response.metadata)); + }; + batch_state = _startBatchIfReady(call, metadata_batch, batch_state, + callback); + var status_batch = { + [grpc.opType.RECV_STATUS_ON_CLIENT]: true + }; + call.startBatch(status_batch, function(err, response) { + if (err) { + emitter.emit('error', err); + return; + } + response.status.metadata = Metadata._fromCoreRepresentation( + response.status.metadata); + first_listener.onReceiveStatus(response.status); + }); + }; + final_requester.sendMessage = function(argument) { + var message = serialize(argument); + if (options) { + message.grpcWriteFlags = options.flags; + } + var send_batch = { + [grpc.opType.SEND_MESSAGE]: message + }; + var callback = function(err, response) { + if (err) { + // The call has stopped for some reason. A non-OK status will arrive + // in the other batch. + return; + } + }; + batch_state = _startBatchIfReady(call, send_batch, batch_state, callback); + }; + final_requester.halfClose = function() { + var batch = { + [grpc.opType.SEND_CLOSE_FROM_CLIENT]: true + }; + batch_state = _startBatchIfReady(call, batch, batch_state, function() {}); + }; + final_requester.recvMessageWithContext = function(context) { + var recv_batch = { + [grpc.opType.RECV_MESSAGE]: true + }; + var callback = _getStreamReadCallback(emitter, call, + get_listener, deserialize); + batch_state = _startBatchIfReady(call, recv_batch, batch_state, callback); + }; + final_requester.cancel = function() { + call.cancel(); + }; + final_requester.getPeer = function() { + return call.getPeer(); + }; + return new InterceptingCall(null, final_requester); + }; +} + +/** + * Produces an interceptor which will start gRPC batches for bi-directional + * calls. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {grpc.Channel} channel + * @param {EventEmitter} emitter + * @return {Interceptor} + */ +function _getBidiStreamingInterceptor(method_definition, channel, emitter) { + var serialize = common.wrapIgnoreNull(method_definition.requestSerialize); + var deserialize = common.wrapIgnoreNull( + method_definition.responseDeserialize); + return function (options) { + var first_listener; + var get_listener = function() { + return first_listener; + }; + var call = getCall(channel, method_definition.path, options); + var final_requester = {}; + final_requester.start = function (metadata, listener) { + var metadata_batch = { + [grpc.opType.SEND_INITIAL_METADATA]: metadata._getCoreRepresentation(), + [grpc.opType.RECV_INITIAL_METADATA]: true + }; + first_listener = listener; + call.startBatch(metadata_batch, function (err, response) { + if (err) { + // The call has stopped for some reason. A non-OK status will arrive + // in the other batch. + return; + } + response.metadata = Metadata._fromCoreRepresentation(response.metadata); + listener.onReceiveMetadata(response.metadata); + }); + var recv_batch = {}; + recv_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; + call.startBatch(recv_batch, function (err, response) { + var status = response.status; + if (status.code === constants.status.OK) { + if (err) { + emitter.emit('error', err); + return; + } + } + response.status.metadata = Metadata._fromCoreRepresentation( + response.status.metadata); + listener.onReceiveStatus(status); + }); + }; + final_requester.sendMessage = function (chunk, context) { + var message; + var callback = (context && context.callback) ? + context.callback : + function() {}; + var encoding = (context && context.encoding) ? + context.encoding : + ''; + try { + message = serialize(chunk); + } catch (e) { + /* Sending this error to the server and emitting it immediately on the + client may put the call in a slightly weird state on the client side, + but passing an object that causes a serialization failure is a misuse + of the API anyway, so that's OK. The primary purpose here is to give + the programmer a useful error and to stop the stream properly */ + call.cancelWithStatus(constants.status.INTERNAL, + 'Serialization failure'); + callback(e); + return; + } + if (_.isFinite(encoding)) { + /* Attach the encoding if it is a finite number. This is the closest we + * can get to checking that it is valid flags */ + message.grpcWriteFlags = encoding; + } + var batch = { + [grpc.opType.SEND_MESSAGE]: message + }; + call.startBatch(batch, function (err, event) { + callback(err, event); + }); + }; + final_requester.halfClose = function () { + var batch = { + [grpc.opType.SEND_CLOSE_FROM_CLIENT]: true + }; + call.startBatch(batch, function () { }); + }; + final_requester.recvMessageWithContext = function(context) { + var recv_batch = { + [grpc.opType.RECV_MESSAGE]: true + }; + call.startBatch(recv_batch, _getStreamReadCallback(emitter, call, + get_listener, deserialize)); + }; + final_requester.cancel = function() { + call.cancel(); + }; + final_requester.getPeer = function() { + return call.getPeer(); + }; + return new InterceptingCall(null, final_requester); + }; +} + +/** + * Produces a listener for responding to callers of unary RPCs. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {EventEmitter} emitter + * @param {function} callback + * @return {grpc~Listener} + */ +function _getUnaryListener(method_definition, emitter, callback) { + var resultMessage; + return { + onReceiveMetadata: function (metadata) { + emitter.emit('metadata', metadata); + }, + onReceiveMessage: function (message) { + resultMessage = message; + }, + onReceiveStatus: function (status) { + if (status.code !== constants.status.OK) { + var error = common.createStatusError(status); + callback(error); + } else { + callback(null, resultMessage); + } + emitter.emit('status', status); + } + }; +} + +/** + * Produces a listener for responding to callers of client streaming RPCs. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {EventEmitter} emitter + * @param {function} callback + * @return {grpc~Listener} + */ +function _getClientStreamingListener(method_definition, emitter, callback) { + var resultMessage; + return { + onReceiveMetadata: function (metadata) { + emitter.emit('metadata', metadata); + }, + onReceiveMessage: function (message) { + resultMessage = message; + }, + onReceiveStatus: function (status) { + if (status.code !== constants.status.OK) { + var error = common.createStatusError(status); + callback(error); + } else { + callback(null, resultMessage); + } + emitter.emit('status', status); + } + }; +} + +/** + * Produces a listener for responding to callers of server streaming RPCs. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {EventEmitter} emitter + * @return {grpc~Listener} + */ +function _getServerStreamingListener(method_definition, emitter) { + var deserialize = common.wrapIgnoreNull( + method_definition.responseDeserialize); + return { + onReceiveMetadata: function (metadata) { + emitter.emit('metadata', metadata); + }, + onReceiveMessage: function(message, next, context) { + if (emitter.push(message) && message !== null) { + var call = context.call; + var get_listener = function() { + return context.listener; + }; + var read_batch = {}; + read_batch[grpc.opType.RECV_MESSAGE] = true; + call.startBatch(read_batch, _getStreamReadCallback(emitter, call, + get_listener, deserialize)); + } else { + emitter.reading = false; + } + }, + onReceiveStatus: function (status) { + emitter._receiveStatus(status); + } + }; +} + +/** + * Produces a listener for responding to callers of bi-directional RPCs. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {EventEmitter} emitter + * @return {grpc~Listener} + */ +function _getBidiStreamingListener(method_definition, emitter) { + var deserialize = common.wrapIgnoreNull( + method_definition.responseDeserialize); + return { + onReceiveMetadata: function (metadata) { + emitter.emit('metadata', metadata); + }, + onReceiveMessage: function(message, next, context) { + if (emitter.push(message) && message !== null) { + var call = context.call; + var get_listener = function() { + return context.listener; + }; + var read_batch = {}; + read_batch[grpc.opType.RECV_MESSAGE] = true; + call.startBatch(read_batch, _getStreamReadCallback(emitter, call, + get_listener, deserialize)); + } else { + emitter.reading = false; + } + }, + onReceiveStatus: function (status) { + emitter._receiveStatus(status); + } + }; +} + +var interceptorGenerators = { + [methodTypes.UNARY]: _getUnaryInterceptor, + [methodTypes.CLIENT_STREAMING]: _getClientStreamingInterceptor, + [methodTypes.SERVER_STREAMING]: _getServerStreamingInterceptor, + [methodTypes.BIDI_STREAMING]: _getBidiStreamingInterceptor +}; + +var listenerGenerators = { + [methodTypes.UNARY]: _getUnaryListener, + [methodTypes.CLIENT_STREAMING]: _getClientStreamingListener, + [methodTypes.SERVER_STREAMING]: _getServerStreamingListener, + [methodTypes.BIDI_STREAMING]: _getBidiStreamingListener +}; + +/** + * Creates the last listener in an interceptor stack. + * @param {grpc~MethodDefinition} method_definition + * @param {EventEmitter} emitter + * @param {function=} callback + * @return {grpc~Listener} + */ +function getLastListener(method_definition, emitter, callback) { + if (emitter instanceof Function) { + callback = emitter; + callback = function() {}; + } + if (!(callback instanceof Function)) { + callback = function() {}; + } + if (!((emitter instanceof EventEmitter) && + (callback instanceof Function))) { + throw new Error('Argument mismatch in getLastListener'); + } + var method_type = common.getMethodType(method_definition); + var generator = listenerGenerators[method_type]; + return generator(method_definition, emitter, callback); +} + +/** + * + * @param {grpc~MethodDefinition} method_definition + * @param {grpc.Client~CallOptions} options + * @param {Interceptor[]} constructor_interceptors + * @param {grpc.Channel} channel + * @param {function|EventEmitter} responder + */ +function getInterceptingCall(method_definition, options, + constructor_interceptors, channel, responder) { + var interceptors = _processInterceptorLayers( + options, + constructor_interceptors, + method_definition + ); + var last_interceptor = _getLastInterceptor(method_definition, channel, + responder); + var all_interceptors = interceptors.concat(last_interceptor); + return _buildChain(all_interceptors, options); +} + +/** + * Creates the last interceptor in an interceptor stack. + * @private + * @param {grpc~MethodDefinition} method_definition + * @param {grpc.Channel} channel + * @param {function|EventEmitter} responder + * @return {Interceptor} + */ +function _getLastInterceptor(method_definition, channel, responder) { + var callback = (responder instanceof Function) ? responder : function() {}; + var emitter = (responder instanceof EventEmitter) ? responder : + new EventEmitter(); + var method_type = common.getMethodType(method_definition); + var generator = interceptorGenerators[method_type]; + return generator(method_definition, channel, emitter, callback); +} + +/** + * Chain a list of interceptors together and return the first InterceptingCall. + * @private + * @param {Interceptor[]} interceptors An interceptor stack. + * @param {grpc.Client~CallOptions} options Call options. + * @return {InterceptingCall} + */ +function _buildChain(interceptors, options) { + var next = function(interceptors) { + if (interceptors.length === 0) { + return function (options) {}; + } + var head_interceptor = interceptors[0]; + var rest_interceptors = interceptors.slice(1); + return function (options) { + return head_interceptor(options, next(rest_interceptors)); + }; + }; + var chain = next(interceptors)(options); + return new InterceptingCall(chain); +} + +/** + * Process call options and the interceptor override layers to get the final set + * of interceptors. + * @private + * @param {grpc.Client~CallOptions} call_options The options passed to the gRPC + * call. + * @param {Interceptor[]} constructor_interceptors Interceptors passed to the + * client constructor. + * @param {grpc~MethodDefinition} method_definition Details of the RPC method. + * @return {Interceptor[]|null} The final set of interceptors. + */ +function _processInterceptorLayers(call_options, + constructor_interceptors, + method_definition) { + var calltime_interceptors = resolveInterceptorOptions(call_options, + method_definition); + var interceptor_overrides = [ + calltime_interceptors, + constructor_interceptors + ]; + return _resolveInterceptorOverrides(interceptor_overrides); +} + +/** + * Wraps a plain listener object in an InterceptingListener if it isn't an + * InterceptingListener already. + * @param {InterceptingListener|object|null} current_listener + * @param {InterceptingListener|EndListener} next_listener + * @return {InterceptingListener|null} + * @private + */ +function _getInterceptingListener(current_listener, next_listener) { + if (!_isInterceptingListener(current_listener)) { + return new InterceptingListener(next_listener, current_listener); + } + return current_listener; +} + +/** + * Test if the listener exists and is an InterceptingListener. + * @param listener + * @return {boolean} + * @private + */ +function _isInterceptingListener(listener) { + return listener && listener.constructor.name === 'InterceptingListener'; +} + +/** + * Chooses the first valid array of interceptors or returns null. + * @param {Interceptor[][]} interceptor_lists A list of interceptor lists in + * descending override priority order. + * @return {Interceptor[]|null} The resulting interceptors + * @private + */ +function _resolveInterceptorOverrides(interceptor_lists) { + for (var i = 0; i < interceptor_lists.length; i++) { + var interceptor_list = interceptor_lists[i]; + if (_.isArray(interceptor_list)) { + return interceptor_list; + } + } + return null; +} + +exports.resolveInterceptorProviders = resolveInterceptorProviders; + +exports.InterceptingCall = InterceptingCall; +exports.ListenerBuilder = ListenerBuilder; +exports.RequesterBuilder = RequesterBuilder; +exports.StatusBuilder = StatusBuilder; + +exports.InterceptorConfigurationError = InterceptorConfigurationError; + +exports.getInterceptingCall = getInterceptingCall; +exports.getLastListener = getLastListener; diff --git a/packages/grpc-native-core/src/common.js b/packages/grpc-native-core/src/common.js index 5a444f5e..0c35296a 100644 --- a/packages/grpc-native-core/src/common.js +++ b/packages/grpc-native-core/src/common.js @@ -19,6 +19,7 @@ 'use strict'; var _ = require('lodash'); +var constants = require('./constants'); /** * Wrap a function to pass null-like values through without calling it. If no @@ -75,6 +76,42 @@ exports.defaultGrpcOptions = { deprecatedArgumentOrder: false }; +/** + * Create an Error object from a status object + * @param {grpc~StatusObject} status The status object + * @return {Error} The resulting Error + */ +exports.createStatusError = function(status) { + let statusName = _.invert(constants.status)[status.code]; + let message = `${status.code} ${statusName}: ${status.details}`; + let error = new Error(message); + error.code = status.code; + error.metadata = status.metadata; + error.details = status.details; + return error; +}; + +/** + * Get a method's type from its definition + * @param {grpc~MethodDefinition} method_definition + * @return {number} + */ +exports.getMethodType = function(method_definition) { + if (method_definition.requestStream) { + if (method_definition.responseStream) { + return constants.methodTypes.BIDI_STREAMING; + } else { + return constants.methodTypes.CLIENT_STREAMING; + } + } else { + if (method_definition.responseStream) { + return constants.methodTypes.SERVER_STREAMING; + } else { + return constants.methodTypes.UNARY; + } + } +}; + // JSDoc definitions that are used in multiple other modules /** @@ -145,7 +182,7 @@ exports.defaultGrpcOptions = { * a number of milliseconds since the Unix Epoch. If it is Infinity, the * deadline will never be reached. If it is -Infinity, the deadline has already * passed. - * @typedef {(number|date)} grpc~Deadline + * @typedef {(number|Date)} grpc~Deadline */ /** @@ -166,7 +203,109 @@ exports.defaultGrpcOptions = { * function for repsonse data */ +/** + * @function MetadataListener + * @param {grpc.Metadata} metadata The response metadata. + * @param {function} next Passes metadata to the next interceptor. + */ + +/** + * @function MessageListener + * @param {jspb.Message} message The response message. + * @param {function} next Passes a message to the next interceptor. + */ + +/** + * @function StatusListener + * @param {grpc~StatusObject} status The response status. + * @param {function} next Passes a status to the next interceptor. + */ + +/** + * A set of interceptor functions triggered by responses + * @typedef {object} grpc~Listener + * @property {MetadataListener=} onReceiveMetadata A function triggered by + * response metadata. + * @property {MessageListener=} onReceiveMessage A function triggered by a + * response message. + * @property {StatusListener=} onReceiveStatus A function triggered by a + * response status. + */ + +/** + * @function MetadataRequester + * @param {grpc.Metadata} metadata The request metadata. + * @param {grpc~Listener} listener A listener wired to the previous layers + * in the interceptor stack. + * @param {function} next Passes metadata and a listener to the next + * interceptor. + */ + +/** + * @function MessageRequester + * @param {jspb.Message} message The request message. + * @param {function} next Passes a message to the next interceptor. + */ + +/** + * @function CloseRequester + * @param {function} next Calls the next interceptor. + */ + +/** + * @function CancelRequester + * @param {function} next Calls the next interceptor. + */ + +/** + * @function GetPeerRequester + * @param {function} next Calls the next interceptor. + * @return {string} + */ + +/** + * @typedef {object} grpc~Requester + * @param {MetadataRequester=} start A function triggered when the call begins. + * @param {MessageRequester=} sendMessage A function triggered by the request + * message. + * @param {CloseRequester=} halfClose A function triggered when the client + * closes the call. + * @param {CancelRequester=} cancel A function triggered when the call is + * cancelled. + * @param {GetPeerRequester=} getPeer A function triggered when the endpoint is + * requested. + */ + /** * An object that completely defines a service. * @typedef {Object.} grpc~ServiceDefinition */ + +/** + * An object that defines a package hierarchy with multiple services + * @typedef {Object.} grpc~PackageDefinition + */ + +/** + * A function for dynamically assigning an interceptor to a call. + * @function InterceptorProvider + * @param {grpc~MethodDefinition} method_definition The method to provide + * an interceptor for. + * @return {Interceptor|null} The interceptor to provide or nothing + */ + +/** + * A function which can modify call options and produce methods to intercept + * RPC operations. + * @function Interceptor + * @param {object} options The grpc call options + * @param {NextCall} nextCall + * @return {InterceptingCall} + */ + +/** + * A function which produces the next InterceptingCall. + * @function NextCall + * @param {object} options The grpc call options + * @return {InterceptingCall|null} + */ diff --git a/packages/grpc-native-core/src/constants.js b/packages/grpc-native-core/src/constants.js index c90e44d0..e9df9a31 100644 --- a/packages/grpc-native-core/src/constants.js +++ b/packages/grpc-native-core/src/constants.js @@ -139,7 +139,8 @@ exports.status = { * a backoff. * * See litmus test above for deciding between FAILED_PRECONDITION, - * ABORTED, and UNAVAILABLE. */ + * ABORTED, and UNAVAILABLE. + */ UNAVAILABLE: 14, /** Unrecoverable data loss or corruption. */ DATA_LOSS: 15, @@ -150,7 +151,7 @@ exports.status = { UNAUTHENTICATED: 16 }; -/* The comments about propagation bit flags are copied rom +/* The comments about propagation bit flags are copied from * include/grpc/impl/codegen/propagation_bits.h for the purpose of including * them in generated documentation. */ @@ -234,3 +235,17 @@ exports.logVerbosity = { INFO: 1, ERROR: 2 }; + +/** + * Method types: the supported RPC types + * @memberof grpc + * @alias grpc.methodTypes + * @readonly + * @enum {number} + */ +exports.methodTypes = { + UNARY: 0, + CLIENT_STREAMING: 1, + SERVER_STREAMING: 2, + BIDI_STREAMING: 3 +}; diff --git a/packages/grpc-native-core/src/credentials.js b/packages/grpc-native-core/src/credentials.js index d68d888e..a2312966 100644 --- a/packages/grpc-native-core/src/credentials.js +++ b/packages/grpc-native-core/src/credentials.js @@ -86,7 +86,7 @@ var _ = require('lodash'); * @param {Buffer=} private_key The client certificate private key, if * applicable * @param {Buffer=} cert_chain The client certificate cert chain, if applicable - * @return {grpc.credentials.ChannelCredentials} The SSL Credentials object + * @return {grpc.credentials~ChannelCredentials} The SSL Credentials object */ exports.createSsl = ChannelCredentials.createSsl; @@ -113,7 +113,7 @@ exports.createSsl = ChannelCredentials.createSsl; * @alias grpc.credentials.createFromMetadataGenerator * @param {grpc.credentials~generateMetadata} metadata_generator The function * that generates metadata - * @return {grpc.credentials.CallCredentials} The credentials object + * @return {grpc.credentials~CallCredentials} The credentials object */ exports.createFromMetadataGenerator = function(metadata_generator) { return CallCredentials.createFromPlugin(function(service_url, cb_data, @@ -143,7 +143,7 @@ exports.createFromMetadataGenerator = function(metadata_generator) { * @alias grpc.credentials.createFromGoogleCredential * @param {external:GoogleCredential} google_credential The Google credential * object to use - * @return {grpc.credentials.CallCredentials} The resulting credentials object + * @return {grpc.credentials~CallCredentials} The resulting credentials object */ exports.createFromGoogleCredential = function(google_credential) { return exports.createFromMetadataGenerator(function(auth_context, callback) { @@ -166,10 +166,10 @@ exports.createFromGoogleCredential = function(google_credential) { * ChannelCredentials object. * @memberof grpc.credentials * @alias grpc.credentials.combineChannelCredentials - * @param {ChannelCredentials} channel_credential The ChannelCredentials to + * @param {grpc.credentials~ChannelCredentials} channel_credential The ChannelCredentials to * start with - * @param {...CallCredentials} credentials The CallCredentials to compose - * @return ChannelCredentials A credentials object that combines all of the + * @param {...grpc.credentials~CallCredentials} credentials The CallCredentials to compose + * @return {grpc.credentials~ChannelCredentials} A credentials object that combines all of the * input credentials */ exports.combineChannelCredentials = function(channel_credential) { @@ -184,8 +184,8 @@ exports.combineChannelCredentials = function(channel_credential) { * Combine any number of CallCredentials into a single CallCredentials object * @memberof grpc.credentials * @alias grpc.credentials.combineCallCredentials - * @param {...CallCredentials} credentials the CallCredentials to compose - * @return CallCredentials A credentials object that combines all of the input + * @param {...grpc.credentials~CallCredentials} credentials The CallCredentials to compose + * @return {grpc.credentials~CallCredentials} A credentials object that combines all of the input * credentials */ exports.combineCallCredentials = function() { @@ -202,6 +202,6 @@ exports.combineCallCredentials = function() { * @memberof grpc.credentials * @alias grpc.credentials.createInsecure * @kind function - * @return {ChannelCredentials} The insecure credentials object + * @return {grpc.credentials~ChannelCredentials} The insecure credentials object */ exports.createInsecure = ChannelCredentials.createInsecure; diff --git a/packages/grpc-native-core/src/grpc_extension.js b/packages/grpc-native-core/src/grpc_extension.js index 77476f01..9a023b7c 100644 --- a/packages/grpc-native-core/src/grpc_extension.js +++ b/packages/grpc-native-core/src/grpc_extension.js @@ -27,6 +27,35 @@ var binary = require('node-pre-gyp/lib/pre-binding'); var path = require('path'); var binding_path = binary.find(path.resolve(path.join(__dirname, '../package.json'))); -var binding = require(binding_path); +var binding; +try { + binding = require(binding_path); +} catch (e) { + let fs = require('fs'); + let searchPath = path.dirname(path.dirname(binding_path)); + let searchName = path.basename(path.dirname(binding_path)); + let foundNames; + try { + foundNames = fs.readdirSync(searchPath); + } catch (readDirError) { + let message = `The gRPC binary module was not installed. This may be fixed by running "npm rebuild" +Original error: ${e.message}`; + let error = new Error(message); + error.code = e.code; + throw error; + } + if (foundNames.indexOf(searchName) === -1) { + let message = `Failed to load gRPC binary module because it was not installed for the current system +Expected directory: ${searchName} +Found: [${foundNames.join(', ')}] +This problem can often be fixed by running "npm rebuild" on the current system +Original error: ${e.message}`; + let error = new Error(message); + error.code = e.code; + throw error; + } else { + throw e; + } +} module.exports = binding; diff --git a/packages/grpc-native-core/src/metadata.js b/packages/grpc-native-core/src/metadata.js index 46f9e0fe..63bbed76 100644 --- a/packages/grpc-native-core/src/metadata.js +++ b/packages/grpc-native-core/src/metadata.js @@ -132,7 +132,7 @@ Metadata.prototype.getMap = function() { /** * Clone the metadata object. - * @return {Metadata} The new cloned object + * @return {grpc.Metadata} The new cloned object */ Metadata.prototype.clone = function() { var copy = new Metadata(); diff --git a/packages/grpc-native-core/src/server.js b/packages/grpc-native-core/src/server.js index 8b7c0b68..ec5a8067 100644 --- a/packages/grpc-native-core/src/server.js +++ b/packages/grpc-native-core/src/server.js @@ -470,7 +470,7 @@ ServerDuplexStream.prototype._write = _write; /** * Send the initial metadata for a writable stream. * @alias grpc~ServerUnaryCall#sendMetadata - * @param {Metadata} responseMetadata Metadata to send + * @param {grpc.Metadata} responseMetadata Metadata to send */ function sendMetadata(responseMetadata) { /* jshint validthis: true */ @@ -773,7 +773,7 @@ Server.prototype.start = function() { (new Metadata())._getCoreRepresentation(); batch[grpc.opType.SEND_STATUS_FROM_SERVER] = { code: constants.status.UNIMPLEMENTED, - details: '', + details: 'RPC method not implemented ' + method, metadata: {} }; batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true; diff --git a/packages/grpc-native-core/templates/binding.gyp.template b/packages/grpc-native-core/templates/binding.gyp.template index a74066df..e5eebfed 100644 --- a/packages/grpc-native-core/templates/binding.gyp.template +++ b/packages/grpc-native-core/templates/binding.gyp.template @@ -74,9 +74,11 @@ ], 'include_dirs': [ 'deps/grpc', - 'deps/grpc/include' + 'deps/grpc/include', + 'deps/grpc/third_party/abseil-cpp' ], 'defines': [ + 'PB_FIELD_16BIT', 'GPR_BACKWARDS_COMPATIBILITY_MODE', 'GRPC_ARES=0', 'GRPC_UV' @@ -187,13 +189,15 @@ 'conditions': [ ['OS=="win" or runtime=="electron"', { 'targets': [ - % for module in node_modules: % for lib in libs: - % if lib.name in module.transitive_deps and lib.name == 'boringssl': + % if lib.name == 'boringssl': { 'target_name': '${lib.name}', 'product_prefix': 'lib', 'type': 'static_library', + 'cflags': [ + '-Wno-implicit-fallthrough' + ], 'dependencies': [ % for dep in getattr(lib, 'deps', []): '${dep}', @@ -214,7 +218,6 @@ }, % endif % endfor - % endfor ], }], ['OS == "win" and runtime!="electron"', { @@ -250,9 +253,8 @@ ['OS == "win"', { 'targets': [ # Only want to compile zlib under Windows - % for module in node_modules: % for lib in libs: - % if lib.name in module.transitive_deps and lib.name == 'z': + % if lib.name == 'z': { 'target_name': '${lib.name}', 'product_prefix': 'lib', @@ -270,14 +272,14 @@ }, % endif % endfor - % endfor ] }] ], 'targets': [ - % for module in node_modules: + % for core in libs: + % if core.name == 'grpc': % for lib in libs: - % if lib.name in module.transitive_deps and lib.name not in ('boringssl', 'z'): + % if lib.name == core.name or (lib.name in core.transitive_deps and lib.name not in ('boringssl', 'z')): { 'target_name': '${lib.name}', 'product_prefix': 'lib', @@ -302,6 +304,8 @@ }, % endif % endfor + % endif + % endfor { 'include_dirs': [ "'ext/'+f).join(' ')\")" ], "dependencies": [ - % for dep in getattr(module, 'deps', []): - % if dep not in ('boringssl', 'z'): - "${dep}", - % endif - % endfor + "grpc", + "gpr", ] }, - % endfor { "target_name": "action_after_build", "type": "none", diff --git a/packages/grpc-native-core/templates/package.json.template b/packages/grpc-native-core/templates/package.json.template index 45f52240..b2da4098 100644 --- a/packages/grpc-native-core/templates/package.json.template +++ b/packages/grpc-native-core/templates/package.json.template @@ -2,7 +2,7 @@ --- | { "name": "grpc", - "version": "${settings.node_version}", + "version": "${settings.get('node_version', settings.version)}", "author": "Google Inc.", "description": "gRPC Library for Node", "homepage": "https://grpc.io/", @@ -18,13 +18,11 @@ } ], "directories": { - "lib": "src/node/src" + "lib": "src" }, "scripts": { - "lint": "node ./node_modules/jshint/bin/jshint src test index.js --exclude-path=.jshintignore", - "test": "./node_modules/.bin/mocha test && npm run-script lint", + "build": "./node_modules/.bin/node-pre-gyp build", "electron-build": "./node_modules/.bin/node-pre-gyp configure build --runtime=electron --disturl=https://atom.io/download/atom-shell", - "gen_docs": "./node_modules/.bin/jsdoc -c jsdoc_conf.json", "coverage": "./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha test", "install": "./node_modules/.bin/node-pre-gyp install --fallback-to-build --library=static_library" }, @@ -32,10 +30,9 @@ "node-pre-gyp" ], "dependencies": { - "arguejs": "^0.2.3", "lodash": "^4.15.0", "nan": "^2.0.0", - "node-pre-gyp": "^0.6.35", + "node-pre-gyp": "^0.9.0", "protobufjs": "^5.0.0" }, "devDependencies": { @@ -46,11 +43,8 @@ "google-auth-library": "^0.9.2", "google-protobuf": "^3.0.0", "istanbul": "^0.4.4", - "jsdoc": "^3.3.2", - "jshint": "^2.5.0", + "lodash": "^4.17.4", "minimist": "^1.1.0", - "mocha": "^3.0.2", - "mocha-jenkins-reporter": "^0.2.3", "poisson-process": "^0.2.1" }, "engines": { @@ -58,29 +52,32 @@ }, "binary": { "module_name": "grpc_node", - "module_path": "src/node/extension_binary/{node_abi}-{platform}-{arch}", + "module_path": "src/node/extension_binary/{node_abi}-{platform}-{arch}-{libc}", "host": "https://storage.googleapis.com/", "remote_path": "grpc-precompiled-binaries/node/{name}/v{version}", - "package_name": "{node_abi}-{platform}-{arch}.tar.gz" + "package_name": "{node_abi}-{platform}-{arch}-{libc}.tar.gz" }, "files": [ "LICENSE", "README.md", - "deps/grpc/src/proto", - "deps/grpc/etc", + "deps/grpc/etc/", "index.js", - "src", - "ext", - "deps/grpc/include/grpc", - "deps/grpc/src/core", - "deps/grpc/src/boringssl", - "deps/grpc/src/zlib", - "deps/grpc/third_party/nanopb", - "deps/grpc/third_party/zlib", - "deps/grpc/third_party/boringssl", + "index.d.ts", + "src/*.js", + "ext/*.{cc,h}", + "deps/grpc/include/grpc/**/*.h", + "deps/grpc/src/core/**/*.{c,cc,h}", + "deps/grpc/src/boringssl/*.{c,cc,h}", + "deps/grpc/third_party/nanopb/*.{c,cc,h}", + "deps/grpc/third_party/zlib/**/*.{c,cc,h}", + "deps/grpc/third_party/boringssl/crypto/**/*.{c,cc,h}", + "deps/grpc/third_party/boringssl/include/**/*.{c,cc,h}", + "deps/grpc/third_party/boringssl/ssl/**/*.{c,cc,h}", + "deps/grpc/third_party/abseil-cpp/absl/**/*.{h,hh}", "binding.gyp" ], "main": "index.js", + "typings": "index.d.ts", "license": "Apache-2.0", "jshintConfig": { "bitwise": true, diff --git a/packages/grpc-native-core/test/async_test.js b/packages/grpc-native-core/test/async_test.js index e2f124e5..786e50bf 100644 --- a/packages/grpc-native-core/test/async_test.js +++ b/packages/grpc-native-core/test/async_test.js @@ -21,7 +21,8 @@ var assert = require('assert'); var grpc = require('..'); -var math = grpc.load(__dirname + '/../deps/grpc/src/proto/math/math.proto').math; +var math = grpc.load( + __dirname + '/../deps/grpc/src/proto/math/math.proto').math; /** diff --git a/packages/grpc-native-core/test/client_interceptors_test.js b/packages/grpc-native-core/test/client_interceptors_test.js new file mode 100644 index 00000000..b14ec156 --- /dev/null +++ b/packages/grpc-native-core/test/client_interceptors_test.js @@ -0,0 +1,1795 @@ +/** + * @license + * Copyright 2018 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. + * + */ + +'use strict'; + +var _ = require('lodash'); +var assert = require('assert'); +var grpc = require('..'); +var grpc_client = require('../src/client.js'); +var Metadata = require('../src/metadata'); + +var insecureCreds = grpc.credentials.createInsecure(); + +var echo_proto = grpc.load(__dirname + '/echo_service.proto'); +var echo_service = echo_proto.EchoService.service; + +var StatusBuilder = grpc_client.StatusBuilder; +var ListenerBuilder = grpc_client.ListenerBuilder; +var InterceptingCall = grpc_client.InterceptingCall; +var RequesterBuilder = grpc_client.RequesterBuilder; + +var CallRegistry = function(done, expectation, is_ordered, is_verbose) { + this.call_map = {}; + this.call_array = []; + this.done = done; + this.expectation = expectation; + this.expectation_is_array = _.isArray(this.expectation); + this.is_ordered = is_ordered; + this.is_verbose = is_verbose; + if (is_verbose) { + console.log('Expectation: ', expectation); + } +}; + +CallRegistry.prototype.addCall = function(call_name) { + if (this.expectation_is_array) { + this.call_array.push(call_name); + if (this.is_verbose) { + console.log(this.call_array); + } + } else { + if (!this.call_map[call_name]) { + this.call_map[call_name] = 0; + } + this.call_map[call_name]++; + if (this.is_verbose) { + console.log(this.call_map); + } + } + this.maybeCallDone(); +}; + +CallRegistry.prototype.maybeCallDone = function() { + if (this.expectation_is_array) { + if (this.is_ordered) { + if (this.expectation && _.isEqual(this.expectation, this.call_array)) { + this.done(); + } + } else { + var intersection = _.intersectionWith(this.expectation, this.call_array, + _.isEqual); + if (intersection.length === this.expectation.length) { + this.done(); + } + } + } else if (this.expectation && _.isEqual(this.expectation, this.call_map)) { + this.done(); + } +}; + +describe('Client interceptors', function() { + var echo_server; + var echo_port; + var client; + + function startServer() { + echo_server = new grpc.Server(); + echo_server.addService(echo_service, { + echo: function(call, callback) { + call.sendMetadata(call.metadata); + if (call.request.value === 'error') { + var status = { + code: 2, + message: 'test status message' + }; + status.metadata = call.metadata; + callback(status, null); + return; + } + callback(null, call.request); + }, + echoClientStream: function(call, callback){ + call.sendMetadata(call.metadata); + var payload; + var err = null; + call.on('data', function(data) { + if (data.value === 'error') { + err = { + code: 2, + message: 'test status message' + }; + err.metadata = call.metadata; + return; + } + payload = data; + }); + call.on('end', function() { + callback(err, payload, call.metadata); + }); + }, + echoServerStream: function(call) { + call.sendMetadata(call.metadata); + if (call.request.value === 'error') { + var status = { + code: 2, + message: 'test status message' + }; + status.metadata = call.metadata; + call.emit('error', status); + return; + } + call.write(call.request); + call.end(call.metadata); + }, + echoBidiStream: function(call) { + call.sendMetadata(call.metadata); + call.on('data', function(data) { + if (data.value === 'error') { + var status = { + code: 2, + message: 'test status message' + }; + call.emit('error', status); + return; + } + call.write(data); + }); + call.on('end', function() { + call.end(call.metadata); + }); + } + }); + var server_credentials = grpc.ServerCredentials.createInsecure(); + echo_port = echo_server.bind('localhost:0', server_credentials); + echo_server.start(); + } + + function stopServer() { + echo_server.forceShutdown(); + } + + function resetClient() { + var EchoClient = grpc_client.makeClientConstructor(echo_service); + client = new EchoClient('localhost:' + echo_port, insecureCreds); + } + + before(function() { + startServer(); + }); + beforeEach(function() { + resetClient(); + }); + after(function() { + stopServer(); + }); + describe('pass calls through when no interceptors provided', function() { + it('with unary call', function(done) { + var expected_value = 'foo'; + var message = {value: expected_value}; + client.echo(message, function(err, response) { + assert.strictEqual(response.value, expected_value); + done(); + }); + assert(_.isEqual(grpc_client.getClientInterceptors(client), { + echo: [], + echoClientStream: [], + echoServerStream: [], + echoBidiStream: [] + })); + }); + }); + + describe('execute downstream interceptors when a new call is made outbound', + function() { + var registry; + var options; + before(function() { + var stored_listener; + var stored_metadata; + var interceptor_a = function (options, nextCall) { + options.call_number = 1; + registry.addCall('construct a ' + options.call_number); + return new InterceptingCall(nextCall(options), { + start: function (metadata, listener, next) { + registry.addCall('start a ' + options.call_number); + stored_listener = listener; + stored_metadata = metadata; + next(metadata, listener); + }, + sendMessage: function (message, next) { + registry.addCall('send a ' + options.call_number); + var options2 = _.clone(options); + options2.call_number = 2; + var second_call = nextCall(options2); + second_call.start(stored_metadata); + second_call.sendMessage(message); + second_call.halfClose(); + next(message); + }, + halfClose: function (next) { + registry.addCall('close a ' + options.call_number); + next(); + } + }); + }; + + var interceptor_b = function (options, nextCall) { + registry.addCall('construct b ' + options.call_number); + return new InterceptingCall(nextCall(options), { + start: function (metadata, listener, next) { + registry.addCall('start b ' + options.call_number); + next(metadata, listener); + }, + sendMessage: function (message, next) { + registry.addCall('send b ' + options.call_number); + next(message); + }, + halfClose: function (next) { + registry.addCall('close b ' + options.call_number); + next(); + } + }); + }; + options = { + interceptors: [interceptor_a, interceptor_b] + }; + }); + var expected_calls = [ + 'construct a 1', + 'construct b 1', + 'start a 1', + 'start b 1', + 'send a 1', + 'construct b 2', + 'start b 2', + 'send b 2', + 'close b 2', + 'send b 1', + 'close a 1', + 'close b 1', + 'response' + ]; + + it('with unary call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {}; + message.value = 'foo'; + client.echo(message, options, function(err, response){ + if (!err) { + registry.addCall('response'); + } + }); + }); + it('with client streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, false); + var message = {}; + message.value = 'foo'; + var stream = client.echoClientStream(options, function(err, response) { + if (!err) { + registry.addCall('response'); + } + }); + stream.write(message); + stream.end(); + }); + it('with server streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {}; + message.value = 'foo'; + var stream = client.echoServerStream(message, options); + stream.on('data', function(data) { + registry.addCall('response'); + }); + }); + it('with bidi streaming call', function(done) { + registry = new CallRegistry( done, expected_calls, true); + var message = {}; + message.value = 'foo'; + var stream = client.echoBidiStream(options); + stream.on('data', function(data) { + registry.addCall('response'); + }); + stream.write(message); + stream.end(); + }); + }); + + + describe('execute downstream interceptors when a new call is made inbound', + function() { + var registry; + var options; + before(function() { + var interceptor_a = function (options, nextCall) { + return new InterceptingCall(nextCall(options), { + start: function (metadata, listener, next) { + next(metadata, { + onReceiveMetadata: function () { }, + onReceiveMessage: function (message, next) { + registry.addCall('interceptor_a'); + var second_call = nextCall(options); + second_call.start(metadata, listener); + second_call.sendMessage(message); + second_call.halfClose(); + }, + onReceiveStatus: function () { } + }); + } + }); + }; + + var interceptor_b = function (options, nextCall) { + return new InterceptingCall(nextCall(options), { + start: function (metadata, listener, next) { + next(metadata, { + onReceiveMessage: function (message, next) { + registry.addCall('interceptor_b'); + next(message); + } + }); + } + }); + }; + + options = { + interceptors: [interceptor_a, interceptor_b] + }; + + }); + var expected_calls = ['interceptor_b', 'interceptor_a', + 'interceptor_b', 'response']; + it('with unary call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {}; + message.value = 'foo'; + client.echo(message, options, function(err) { + if (!err) { + registry.addCall('response'); + } + }); + }); + it('with client streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {}; + message.value = 'foo'; + var stream = client.echoClientStream(options, function(err, response) { + if (!err) { + registry.addCall('response'); + } + }); + stream.write(message); + stream.end(); + }); + }); + + it('will delay operations and short circuit unary requests', function(done) { + var registry = new CallRegistry(done, ['foo_miss', 'foo_hit', 'bar_miss', + 'foo_hit_done', 'foo_miss_done', 'bar_miss_done']); + var cache = {}; + var _getCachedResponse = function(value) { + return cache[value]; + }; + var _store = function(key, value) { + cache[key] = value; + }; + + var interceptor = function(options, nextCall) { + var savedMetadata; + var startNext; + var storedListener; + var storedMessage; + var messageNext; + var requester = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + savedMetadata = metadata; + storedListener = listener; + startNext = next; + }) + .withSendMessage(function(message, next) { + storedMessage = message; + messageNext = next; + }) + .withHalfClose(function(next) { + var cachedValue = _getCachedResponse(storedMessage.value); + if (cachedValue) { + var cachedMessage = {}; + cachedMessage.value = cachedValue; + registry.addCall(storedMessage.value + '_hit'); + storedListener.onReceiveMetadata(new Metadata()); + storedListener.onReceiveMessage(cachedMessage); + storedListener.onReceiveStatus( + (new StatusBuilder()).withCode(grpc.status.OK).build()); + } else { + registry.addCall(storedMessage.value + '_miss'); + var newListener = (new ListenerBuilder()).withOnReceiveMessage( + function(message, next) { + _store(storedMessage.value, message.value); + next(message); + }).build(); + startNext(savedMetadata, newListener); + messageNext(storedMessage); + next(); + } + }) + .withCancel(function(message, next) { + next(); + }).build(); + + return new InterceptingCall(nextCall(options), requester); + }; + + var options = { + interceptors: [interceptor] + }; + + var foo_message = {}; + foo_message.value = 'foo'; + client.echo(foo_message, options, function(err, response){ + assert.equal(response.value, 'foo'); + registry.addCall('foo_miss_done'); + client.echo(foo_message, options, function(err, response){ + assert.equal(response.value, 'foo'); + registry.addCall('foo_hit_done'); + }); + }); + + var bar_message = {}; + bar_message.value = 'bar'; + client.echo(bar_message, options, function(err, response) { + assert.equal(response.value, 'bar'); + registry.addCall('bar_miss_done'); + }); + }); + + it('can retry failed messages and handle eventual success', function(done) { + var registry = new CallRegistry(done, + ['retry_foo_1', 'retry_foo_2', 'retry_foo_3', 'foo_result', + 'retry_bar_1', 'bar_result']); + var maxRetries = 3; + var retry_interceptor = function(options, nextCall) { + var savedMetadata; + var savedSendMessage; + var savedReceiveMessage; + var savedMessageNext; + var requester = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + savedMetadata = metadata; + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + savedReceiveMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function(status, next) { + var retries = 0; + var retry = function(message, metadata) { + retries++; + var newCall = nextCall(options); + var receivedMessage; + newCall.start(metadata, { + onReceiveMessage: function(message) { + receivedMessage = message; + }, + onReceiveStatus: function(status) { + registry.addCall('retry_' + savedMetadata.get('name') + + '_' + retries); + if (status.code !== grpc.status.OK) { + if (retries <= maxRetries) { + retry(message, metadata); + } else { + savedMessageNext(receivedMessage); + next(status); + } + } else { + registry.addCall('success_call'); + var new_status = (new StatusBuilder()) + .withCode(grpc.status.OK).build(); + savedMessageNext(receivedMessage); + next(new_status); + } + } + }); + newCall.sendMessage(message); + newCall.halfClose(); + }; + if (status.code !== grpc.status.OK) { + // Change the message we're sending only for test purposes + // so the server will respond without error + var newMessage = (savedMetadata.get('name')[0] === 'bar') ? + {value: 'bar'} : savedSendMessage; + retry(newMessage, savedMetadata); + } else { + savedMessageNext(savedReceiveMessage); + next(status); + } + } + ).build(); + next(metadata, new_listener); + }) + .withSendMessage(function(message, next) { + savedSendMessage = message; + next(message); + }).build(); + return new InterceptingCall(nextCall(options), requester); + }; + + var options = { + interceptors: [retry_interceptor] + }; + + // Make a call which the server will return a non-OK status for + var foo_message = {value: 'error'}; + var foo_metadata = new Metadata(); + foo_metadata.set('name', 'foo'); + client.echo(foo_message, foo_metadata, options, function(err, response) { + assert.strictEqual(err.code, 2); + registry.addCall('foo_result'); + }); + + // Make a call which will fail the first time and succeed on the first + // retry + var bar_message = {value: 'error'}; + var bar_metadata = new Metadata(); + bar_metadata.set('name', 'bar'); + client.echo(bar_message, bar_metadata, options, function(err, response) { + assert.strictEqual(response.value, 'bar'); + registry.addCall('bar_result'); + }); + }); + + it('can retry and preserve interceptor order on success', function(done) { + var registry = new CallRegistry(done, + ['interceptor_c', 'retry_interceptor', 'fail_call', 'interceptor_c', + 'success_call', 'interceptor_a', 'result'], true); + var interceptor_a = function(options, nextCall) { + var requester = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + registry.addCall('interceptor_a'); + next(message); + }).build(); + next(metadata, new_listener); + }).build(); + return new InterceptingCall(nextCall(options), requester); + }; + + var retry_interceptor = function(options, nextCall) { + var savedMetadata; + var savedMessage; + var savedMessageNext; + var sendMessageNext; + var originalMessage; + var startNext; + var originalListener; + var requester = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + startNext = next; + savedMetadata = metadata; + originalListener = listener; + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + savedMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function(status, next) { + var retries = 0; + var maxRetries = 1; + var receivedMessage; + var retry = function(message, metadata) { + retries++; + var new_call = nextCall(options); + new_call.start(metadata, { + onReceiveMessage: function(message) { + receivedMessage = message; + }, + onReceiveStatus: function(status) { + if (status.code !== grpc.status.OK) { + if (retries <= maxRetries) { + retry(message, metadata); + } else { + savedMessageNext(receivedMessage); + next(status); + } + } else { + registry.addCall('success_call'); + var new_status = (new StatusBuilder()) + .withCode(grpc.status.OK).build(); + savedMessageNext(receivedMessage); + next(new_status); + } + } + }); + new_call.sendMessage(message); + new_call.halfClose(); + }; + registry.addCall('retry_interceptor'); + if (status.code !== grpc.status.OK) { + registry.addCall('fail_call'); + var newMessage = {value: 'foo'}; + retry(newMessage, savedMetadata); + } else { + savedMessageNext(savedMessage); + next(status); + } + }).build(); + next(metadata, new_listener); + }) + .withSendMessage(function(message, next) { + sendMessageNext = next; + originalMessage = message; + next(message); + }) + .build(); + return new InterceptingCall(nextCall(options), requester); + }; + + var interceptor_c = function(options, nextCall) { + var requester = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + registry.addCall('interceptor_c'); + next(message); + }).build(); + next(metadata, new_listener); + }).build(); + return new InterceptingCall(nextCall(options), requester); + }; + + var options = { + interceptors: [interceptor_a, retry_interceptor, interceptor_c] + }; + + var message = {value: 'error'}; + client.echo(message, options, function(err, response) { + assert.strictEqual(response.value, 'foo'); + registry.addCall('result'); + }); + }); + + describe('handle interceptor errors', function (doneOuter) { + var options; + before(function () { + var foo_interceptor = function (options, nextCall) { + var savedListener; + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + savedListener = listener; + next(metadata, listener); + }) + .withSendMessage(function (message, next) { + savedListener.onReceiveMetadata(new Metadata()); + savedListener.onReceiveMessage({ value: 'failed' }); + var error_status = (new StatusBuilder()) + .withCode(16) + .withDetails('Error in foo interceptor') + .build(); + savedListener.onReceiveStatus(error_status); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + it('with unary call', function(done) { + var message = {}; + client.echo(message, options, function(err, response) { + assert.strictEqual(err.code, 16); + assert.strictEqual(err.message, + '16 UNAUTHENTICATED: Error in foo interceptor'); + done(); + doneOuter(); + }); + }); + }); + + describe('implement fallbacks for streaming RPCs', function() { + + var options; + before(function () { + var fallback_response = { value: 'fallback' }; + var savedMessage; + var savedMessageNext; + var interceptor = function (options, nextCall) { + var requester = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function (message, next) { + savedMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function (status, next) { + if (status.code !== grpc.status.OK) { + savedMessageNext(fallback_response); + next((new StatusBuilder()).withCode(grpc.status.OK)); + } else { + savedMessageNext(savedMessage); + next(status); + } + }).build(); + next(metadata, new_listener); + }).build(); + return new InterceptingCall(nextCall(options), requester); + }; + options = { + interceptors: [interceptor] + }; + }); + it('with client streaming call', function (done) { + var registry = new CallRegistry(done, ['foo_result', 'fallback_result']); + var stream = client.echoClientStream(options, function (err, response) { + assert.strictEqual(response.value, 'foo'); + registry.addCall('foo_result'); + }); + stream.write({ value: 'foo' }); + stream.end(); + + stream = client.echoClientStream(options, function(err, response) { + assert.strictEqual(response.value, 'fallback'); + registry.addCall('fallback_result'); + }); + stream.write({value: 'error'}); + stream.end(); + }); + }); + + describe('allows the call options to be modified for downstream interceptors', + function() { + var done; + var options; + var method_name; + var method_path_last; + before(function() { + var interceptor_a = function (options, nextCall) { + options.deadline = 10; + return new InterceptingCall(nextCall(options)); + }; + var interceptor_b = function (options, nextCall) { + assert.equal(options.method_definition.path, '/EchoService/' + + method_path_last); + assert.equal(options.deadline, 10); + done(); + return new InterceptingCall(nextCall(options)); + }; + + options = { + interceptors: [interceptor_a, interceptor_b], + deadline: 100 + }; + }); + + it('with unary call', function(cb) { + done = cb; + var metadata = new Metadata(); + var message = {}; + method_name = 'echo'; + method_path_last = 'Echo'; + + client.echo(message, metadata, options, function(){}); + }); + + it('with client streaming call', function(cb) { + done = cb; + var metadata = new Metadata(); + method_name = 'echoClientStream'; + method_path_last = 'EchoClientStream'; + + client.echoClientStream(metadata, options, function() {}); + }); + + it('with server streaming call', function(cb) { + done = cb; + var metadata = new Metadata(); + var message = {}; + method_name = 'echoServerStream'; + method_path_last = 'EchoServerStream'; + + client.echoServerStream(message, metadata, options); + }); + + it('with bidi streaming call', function(cb) { + done = cb; + var metadata = new Metadata(); + method_name = 'echoBidiStream'; + method_path_last = 'EchoBidiStream'; + + client.echoBidiStream(metadata, options); + }); + }); + + describe('pass accurate MethodDefinitions', function() { + var registry; + var initial_value = 'broken'; + var expected_value = 'working'; + var options; + before(function() { + var interceptor = function (options, nextCall) { + registry.addCall({ + path: options.method_definition.path, + requestStream: options.method_definition.requestStream, + responseStream: options.method_definition.responseStream + }); + var outbound = (new RequesterBuilder()) + .withSendMessage(function (message, next) { + message.value = expected_value; + next(message); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + options = { interceptors: [interceptor] }; + }); + + it('with unary call', function(done) { + var unary_definition = { + path: '/EchoService/Echo', + requestStream: false, + responseStream: false + }; + registry = new CallRegistry(done, [ + unary_definition, + 'result_unary' + ]); + + var metadata = new Metadata(); + + var message = {value: initial_value}; + + client.echo(message, metadata, options, function(err, response){ + assert.equal(response.value, expected_value); + registry.addCall('result_unary'); + }); + + }); + it('with client streaming call', function(done) { + + var client_stream_definition = { + path: '/EchoService/EchoClientStream', + requestStream: true, + responseStream: false + }; + registry = new CallRegistry(done, [ + client_stream_definition, + 'result_client_stream' + ], false, true); + var metadata = new Metadata(); + var message = {value: initial_value}; + var client_stream = client.echoClientStream(metadata, options, + function(err, response) { + assert.strictEqual(response.value, expected_value); + registry.addCall('result_client_stream'); + }); + client_stream.write(message); + client_stream.end(); + + }); + it('with server streaming call', function(done) { + var server_stream_definition = { + path: '/EchoService/EchoServerStream', + responseStream: true, + requestStream: false, + }; + registry = new CallRegistry(done, [ + server_stream_definition, + 'result_server_stream' + ]); + + var metadata = new Metadata(); + var message = {value: initial_value}; + var server_stream = client.echoServerStream(message, metadata, options); + server_stream.on('data', function(data) { + assert.strictEqual(data.value, expected_value); + registry.addCall('result_server_stream'); + }); + + }); + it('with bidi streaming call', function(done) { + var bidi_stream_definition = { + path: '/EchoService/EchoBidiStream', + requestStream: true, + responseStream: true + }; + registry = new CallRegistry(done, [ + bidi_stream_definition, + 'result_bidi_stream' + ]); + + var metadata = new Metadata(); + var message = {value: initial_value}; + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('data', function(data) { + assert.strictEqual(data.value, expected_value); + registry.addCall('result_bidi_stream'); + }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + it('uses interceptors passed to the client constructor', function(done) { + var registry = new CallRegistry(done, { + 'constructor_interceptor_a_echo': 1, + 'constructor_interceptor_b_echoServerStream': 1, + 'invocation_interceptor': 1, + 'result_unary': 1, + 'result_stream': 1, + 'result_invocation': 1 + }); + + var constructor_interceptor_a = function(options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + registry.addCall('constructor_interceptor_a_' + + client.$method_names[options.method_definition.path]); + next(metadata, listener); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + var constructor_interceptor_b = function(options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + registry.addCall('constructor_interceptor_b_' + + client.$method_names[options.method_definition.path]); + next(metadata, listener); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + var invocation_interceptor = function(options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function(metadata, listener, next) { + registry.addCall('invocation_interceptor'); + next(metadata, listener); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + + var interceptor_providers = [ + function(method_definition) { + if (!method_definition.requestStream && + !method_definition.responseStream) { + return constructor_interceptor_a; + } + }, + function(method_definition) { + if (!method_definition.requestStream && + method_definition.responseStream) { + return constructor_interceptor_b; + } + } + ]; + var constructor_options = { + interceptor_providers: interceptor_providers + }; + var IntClient = grpc_client.makeClientConstructor(echo_service); + var int_client = new IntClient('localhost:' + echo_port, insecureCreds, + constructor_options); + var message = {}; + int_client.echo(message, function() { + registry.addCall('result_unary'); + }); + var stream = int_client.echoServerStream(message); + stream.on('data', function() { + registry.addCall('result_stream'); + }); + + var options = { interceptors: [invocation_interceptor] }; + int_client.echo(message, options, function() { + registry.addCall('result_invocation'); + }); + + assert(_.isEqual(grpc_client.getClientInterceptors(int_client), { + echo: [constructor_interceptor_a], + echoClientStream: [], + echoServerStream: [constructor_interceptor_b], + echoBidiStream: [] + })); + }); + + it('will reject conflicting interceptor options at invocation', + function(done) { + try { + client.echo('message', { + interceptors: [], + interceptor_providers: [] + }, function () {}); + } catch (e) { + assert.equal(e.name, 'InterceptorConfigurationError'); + done(); + } + }); + + it('will resolve interceptor providers at invocation', function(done) { + var constructor_interceptor = function(options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function() { + assert(false); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + var invocation_interceptor = function(options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function() { + done(); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + var constructor_interceptor_providers = [ + function() { + return constructor_interceptor; + } + ]; + var invocation_interceptor_providers = [ + function() { + return invocation_interceptor; + } + ]; + var constructor_options = { + interceptor_providers: constructor_interceptor_providers + }; + var IntClient = grpc_client.makeClientConstructor(echo_service); + var int_client = new IntClient('localhost:' + echo_port, insecureCreds, + constructor_options); + var message = {}; + var options = { interceptor_providers: invocation_interceptor_providers }; + int_client.echo(message, options, function() {}); + }); + + describe('trigger a stack of interceptors in nested order', function() { + var registry; + var expected_calls = ['constructA', 'constructB', 'outboundA', 'outboundB', + 'inboundB', 'inboundA']; + var options; + before(function() { + var interceptor_a = function (options, nextCall) { + registry.addCall('constructA'); + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + registry.addCall('outboundA'); + var new_listener = (new ListenerBuilder()).withOnReceiveMessage( + function (message, next) { + registry.addCall('inboundA'); + next(message); + }).build(); + next(metadata, new_listener); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + var interceptor_b = function (options, nextCall) { + registry.addCall('constructB'); + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + registry.addCall('outboundB'); + var new_listener = (new ListenerBuilder()).withOnReceiveMessage( + function (message, next) { + registry.addCall('inboundB'); + next(message); + }).build(); + next(metadata, new_listener); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [interceptor_a, interceptor_b] }; + }); + var metadata = new Metadata(); + var message = {}; + + it('with unary call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + client.echo(message, metadata, options, function(){}); + }); + + it('with client streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var client_stream = client.echoClientStream(metadata, options, + function() {}); + client_stream.write(message); + client_stream.end(); + }); + + it('with server streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var stream = client.echoServerStream(message, metadata, options); + stream.on('data', function() {}); + }); + + it('with bidi streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('data', function(){}); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('trigger interceptors horizontally', function() { + var expected_calls = [ + 'interceptor_a_start', + 'interceptor_b_start', + 'interceptor_a_send', + 'interceptor_b_send' + ]; + var registry; + var options; + var metadata = new Metadata(); + var message = {}; + + before(function() { + var interceptor_a = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + registry.addCall('interceptor_a_start'); + next(metadata, listener); + }) + .withSendMessage(function (message, next) { + registry.addCall('interceptor_a_send'); + next(message); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + var interceptor_b = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + registry.addCall('interceptor_b_start'); + next(metadata, listener); + }) + .withSendMessage(function (message, next) { + registry.addCall('interceptor_b_send'); + next(message); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [interceptor_a, interceptor_b] }; + }); + + it('with unary call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + client.echo(message, metadata, options, function(){}); + }); + + it('with client streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var client_stream = client.echoClientStream(metadata, options, + function() {}); + client_stream.write(message); + client_stream.end(); + }); + + it('with server streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var stream = client.echoServerStream(message, metadata, options); + stream.on('data', function() {}); + }); + + it('with bidi streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('data', function(){}); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('trigger when sending metadata', function() { + var registry; + + var message = {}; + var key_names = ['original', 'foo', 'bar']; + var keys = { + original: 'originalkey', + foo: 'fookey', + bar: 'barkey' + }; + var values = { + original: 'originalvalue', + foo: 'foovalue', + bar: 'barvalue' + }; + var expected_calls = ['foo', 'bar', 'response']; + var options; + before(function () { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + metadata.add(keys.foo, values.foo); + registry.addCall('foo'); + next(metadata, listener); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + var bar_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + metadata.add(keys.bar, values.bar); + registry.addCall('bar'); + next(metadata, listener); + }).build(); + return new InterceptingCall(nextCall(options), outbound); + }; + options = { interceptors: [foo_interceptor, bar_interceptor] }; + }); + + it('with unary call', function (done) { + registry = new CallRegistry(done, expected_calls, true); + var metadata = new Metadata(); + metadata.add(keys.original, values.original); + + var unary_call = client.echo(message, metadata, options, function () {}); + unary_call.on('metadata', function (metadata) { + var has_expected_values = _.every(key_names, function (key_name) { + return _.isEqual(metadata.get(keys[key_name]), [values[key_name]]); + }); + assert(has_expected_values); + registry.addCall('response'); + }); + }); + it('with client streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var metadata = new Metadata(); + metadata.add(keys.original, values.original); + + var client_stream = client.echoClientStream(metadata, options, + function () { + }); + client_stream.write(message); + client_stream.on('metadata', function (metadata) { + var has_expected_values = _.every(key_names, function (key_name) { + return _.isEqual(metadata.get(keys[key_name]), [values[key_name]]); + }); + assert(has_expected_values); + registry.addCall('response'); + }); + client_stream.end(); + }); + it('with server streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var metadata = new Metadata(); + metadata.add(keys.original, values.original); + var server_stream = client.echoServerStream(message, metadata, options); + server_stream.on('metadata', function (metadata) { + var has_expected_values = _.every(key_names, function (key_name) { + return _.isEqual(metadata.get(keys[key_name]), [values[key_name]]); + }); + assert(has_expected_values); + registry.addCall('response'); + }); + server_stream.on('data', function() { }); + }); + it('with bidi streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var metadata = new Metadata(); + metadata.add(keys.original, values.original); + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('metadata', function(metadata) { + var has_expected_values = _.every(key_names, function(key_name) { + return _.isEqual(metadata.get(keys[key_name]),[values[key_name]]); + }); + assert(has_expected_values); + bidi_stream.end(); + registry.addCall('response'); + }); + bidi_stream.on('data', function() { }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('trigger when sending messages', function() { + var registry; + var originalValue = 'foo'; + var expectedValue = 'bar'; + var options; + var metadata = new Metadata(); + var expected_calls = ['messageIntercepted', 'response']; + + before(function() { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withSendMessage(function (message, next) { + assert.strictEqual(message.value, originalValue); + registry.addCall('messageIntercepted'); + next({ value: expectedValue }); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + + it('with unary call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {value: originalValue}; + + client.echo(message, metadata, options, function (err, response) { + assert.strictEqual(response.value, expectedValue); + registry.addCall('response'); + }); + }); + it('with client streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {value: originalValue}; + var client_stream = client.echoClientStream(metadata, options, + function (err, response) { + assert.strictEqual(response.value, expectedValue); + registry.addCall('response'); + }); + client_stream.write(message); + client_stream.end(); + }); + it('with server streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {value: originalValue}; + var server_stream = client.echoServerStream(message, metadata, options); + server_stream.on('data', function (data) { + assert.strictEqual(data.value, expectedValue); + registry.addCall('response'); + }); + }); + it('with bidi streaming call', function(done) { + registry = new CallRegistry(done, expected_calls, true); + var message = {value: originalValue}; + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('data', function(data) { + assert.strictEqual(data.value, expectedValue); + registry.addCall('response'); + }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('trigger when client closes the call', function() { + var registry; + var expected_calls = [ + 'response', 'halfClose' + ]; + var message = {}; + var options; + before(function() { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withHalfClose(function (next) { + registry.addCall('halfClose'); + next(); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + it('with unary call', function (done) { + registry = new CallRegistry(done, expected_calls); + client.echo(message, options, function (err, response) { + if (!err) { + registry.addCall('response'); + } + }); + }); + it('with client streaming call', function (done) { + registry = new CallRegistry(done, expected_calls); + var client_stream = client.echoClientStream(options, + function (err, response) { }); + client_stream.write(message, function (err) { + if (!err) { + registry.addCall('response'); + } + }); + client_stream.end(); + }); + it('with server streaming call', function (done) { + registry = new CallRegistry(done, expected_calls); + var server_stream = client.echoServerStream(message, options); + server_stream.on('data', function (data) { + registry.addCall('response'); + }); + }); + it('with bidi streaming call', function (done) { + registry = new CallRegistry(done, expected_calls); + var bidi_stream = client.echoBidiStream(options); + bidi_stream.on('data', function (data) { + registry.addCall('response'); + }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('trigger when the stream is canceled', function() { + var done; + var message = {}; + var options; + before(function() { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withCancel(function (next) { + done(); + next(); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + + it('with unary call', function(cb) { + done = cb; + var stream = client.echo(message, options, function() {}); + stream.cancel(); + }); + + it('with client streaming call', function(cb) { + done = cb; + var stream = client.echoClientStream(options, function() {}); + stream.cancel(); + }); + it('with server streaming call', function(cb) { + done = cb; + var stream = client.echoServerStream(message, options); + stream.cancel(); + }); + it('with bidi streaming call', function(cb) { + done = cb; + var stream = client.echoBidiStream(options); + stream.cancel(); + }); + }); + + describe('trigger when receiving metadata', function() { + var message = {}; + var expectedKey = 'foo'; + var expectedValue = 'bar'; + var options; + before(function() { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + var new_listener = (new ListenerBuilder()).withOnReceiveMetadata( + function (metadata, next) { + metadata.add(expectedKey, expectedValue); + next(metadata); + }).build(); + next(metadata, new_listener); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + + it('with unary call', function(done) { + var metadata = new Metadata(); + var unary_call = client.echo(message, metadata, options, function () {}); + unary_call.on('metadata', function (metadata) { + assert.strictEqual(metadata.get(expectedKey)[0], expectedValue); + done(); + }); + }); + it('with client streaming call', function(done) { + var metadata = new Metadata(); + var client_stream = client.echoClientStream(metadata, options, + function () {}); + client_stream.write(message); + client_stream.on('metadata', function (metadata) { + assert.strictEqual(metadata.get(expectedKey)[0], expectedValue); + done(); + }); + client_stream.end(); + }); + it('with server streaming call', function(done) { + var metadata = new Metadata(); + var server_stream = client.echoServerStream(message, metadata, options); + server_stream.on('metadata', function (metadata) { + assert.strictEqual(metadata.get(expectedKey)[0], expectedValue); + done(); + }); + server_stream.on('data', function() { }); + }); + it('with bidi streaming call', function(done) { + var metadata = new Metadata(); + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('metadata', function(metadata) { + assert.strictEqual(metadata.get(expectedKey)[0], expectedValue); + bidi_stream.end(); + done(); + }); + bidi_stream.on('data', function() { }); + bidi_stream.write(message); + }); + }); + + describe('trigger when sending messages', function() { + var originalValue = 'foo'; + var expectedValue = 'bar'; + var options; + var metadata = new Metadata(); + before(function() { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + var new_listener = (new ListenerBuilder()).withOnReceiveMessage( + function (message, next) { + if (!message) { + next(message); + return; + } + assert.strictEqual(message.value, originalValue); + message.value = expectedValue; + next(message); + }).build(); + next(metadata, new_listener); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + + it('with unary call', function (done) { + var message = { value: originalValue }; + client.echo(message, metadata, options, function (err, response) { + assert.strictEqual(response.value, expectedValue); + done(); + }); + }); + it('with client streaming call', function (done) { + var message = { value: originalValue }; + var client_stream = client.echoClientStream(metadata, options, + function (err, response) { + assert.strictEqual(response.value, expectedValue); + done(); + }); + client_stream.write(message); + client_stream.end(); + }); + it('with server streaming call', function (done) { + var message = { value: originalValue }; + var server_stream = client.echoServerStream(message, metadata, options); + server_stream.on('data', function (data) { + assert.strictEqual(data.value, expectedValue); + done(); + }); + }); + it('with bidi streaming call', function (done) { + var message = { value: originalValue }; + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('data', function (data) { + assert.strictEqual(data.value, expectedValue); + done(); + }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('trigger when receiving status', function() { + var expectedStatus = 'foo'; + var options; + var metadata = new Metadata(); + before(function() { + var foo_interceptor = function (options, nextCall) { + var outbound = (new RequesterBuilder()) + .withStart(function (metadata, listener, next) { + var new_listener = (new ListenerBuilder()).withOnReceiveStatus( + function (status, next) { + assert.strictEqual(status.code, 2); + assert.strictEqual(status.details, 'test status message'); + var new_status = { + code: 1, + details: expectedStatus, + metadata: {} + }; + next(new_status); + }).build(); + next(metadata, new_listener); + }).build(); + return new grpc_client.InterceptingCall(nextCall(options), + outbound); + }; + options = { interceptors: [foo_interceptor] }; + }); + it('with unary call', function (done) { + var message = { value: 'error' }; + var unary_call = client.echo(message, metadata, options, function () { + }); + unary_call.on('status', function (status) { + assert.strictEqual(status.code, 1); + assert.strictEqual(status.details, expectedStatus); + done(); + }); + }); + it('with client streaming call', function (done) { + var message = { value: 'error' }; + var client_stream = client.echoClientStream(metadata, options, + function () { + }); + client_stream.on('status', function (status) { + assert.strictEqual(status.code, 1); + assert.strictEqual(status.details, expectedStatus); + done(); + }); + client_stream.write(message); + client_stream.end(); + }); + + it('with server streaming call', function(done) { + var message = {value: 'error'}; + var server_stream = client.echoServerStream(message, metadata, options); + server_stream.on('error', function (err) { + }); + server_stream.on('data', function (data) { + }); + server_stream.on('status', function (status) { + assert.strictEqual(status.code, 1); + assert.strictEqual(status.details, expectedStatus); + done(); + }); + }); + + it('with bidi streaming call', function(done) { + var message = {value: 'error'}; + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('error', function(err) {}); + bidi_stream.on('data', function(data) {}); + bidi_stream.on('status', function(status) { + assert.strictEqual(status.code, 1); + assert.strictEqual(status.details, expectedStatus); + done(); + }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + describe('delay streaming headers', function() { + var options; + var metadata = new Metadata(); + before(function() { + var foo_interceptor = function (options, nextCall) { + var startNext; + var startListener; + var startMetadata; + var methods = { + start: function (metadata, listener, next) { + startNext = next; + startListener = listener; + startMetadata = metadata; + }, + sendMessage: function (message, next) { + startMetadata.set('fromMessage', message.value); + startNext(startMetadata, startListener); + next(message); + } + }; + return new grpc_client.InterceptingCall(nextCall(options), methods); + }; + options = { interceptors: [foo_interceptor] }; + }); + + it('with client streaming call', function (done) { + var message = { value: 'foo' }; + var client_stream = client.echoClientStream(metadata, options, + function () { }); + client_stream.on('metadata', function (metadata) { + assert.equal(metadata.get('fromMessage'), 'foo'); + done(); + }); + client_stream.write(message); + client_stream.end(); + }); + it('with bidi streaming call', function (done) { + var message = { value: 'foo' }; + var bidi_stream = client.echoBidiStream(metadata, options); + bidi_stream.on('metadata', function (metadata) { + assert.equal(metadata.get('fromMessage'), 'foo'); + done(); + }); + bidi_stream.write(message); + bidi_stream.end(); + }); + }); + + describe('order of operations enforced for async interceptors', function() { + it('with unary call', function(done) { + var expected_calls = [ + 'close_b', + 'message_b', + 'start_b', + 'done' + ]; + var registry = new CallRegistry(done, expected_calls, true); + var message = {value: 'foo'}; + var interceptor_a = function(options, nextCall) { + return new InterceptingCall(nextCall(options), { + start: function(metadata, listener, next) { + setTimeout(function() { next(metadata, listener); }, 50); + }, + sendMessage: function(message, next) { + setTimeout(function () { next(message); }, 10); + } + }); + }; + var interceptor_b = function(options, nextCall) { + return new InterceptingCall(nextCall(options), { + start: function(metadata, listener, next) { + registry.addCall('start_b'); + next(metadata, listener); + }, + sendMessage: function(message, next) { + registry.addCall('message_b'); + next(message); + }, + halfClose: function(next) { + registry.addCall('close_b'); + next(); + } + }); + }; + var options = { + interceptors: [interceptor_a, interceptor_b] + }; + client.echo(message, options, function(err, response) { + assert.strictEqual(err, null); + registry.addCall('done'); + }); + }); + it('with serverStreaming call', function(done) { + var expected_calls = [ + 'close_b', + 'message_b', + 'start_b', + 'done' + ]; + var registry = new CallRegistry(done, expected_calls, true); + var message = {value: 'foo'}; + var interceptor_a = function(options, nextCall) { + return new InterceptingCall(nextCall(options), { + start: function(metadata, listener, next) { + setTimeout(function() { next(metadata, listener); }, 50); + }, + sendMessage: function(message, next) { + setTimeout(function () { next(message); }, 10); + } + }); + }; + var interceptor_b = function(options, nextCall) { + return new InterceptingCall(nextCall(options), { + start: function(metadata, listener, next) { + registry.addCall('start_b'); + next(metadata, listener); + }, + sendMessage: function(message, next) { + registry.addCall('message_b'); + next(message); + }, + halfClose: function(next) { + registry.addCall('close_b'); + next(); + } + }); + }; + var options = { + interceptors: [interceptor_a, interceptor_b] + }; + var stream = client.echoServerStream(message, options); + stream.on('data', function(response) { + assert.strictEqual(response.value, 'foo'); + registry.addCall('done'); + }); + }); + }); +}); diff --git a/test/api/credentials_test.js b/packages/grpc-native-core/test/credentials_test.js similarity index 97% rename from test/api/credentials_test.js rename to packages/grpc-native-core/test/credentials_test.js index 96879994..58dddb50 100644 --- a/test/api/credentials_test.js +++ b/packages/grpc-native-core/test/credentials_test.js @@ -22,7 +22,7 @@ var assert = require('assert'); var fs = require('fs'); var path = require('path'); -var grpc = require('grpc'); +var grpc = require('..'); /** * This is used for testing functions with multiple asynchronous calls that @@ -67,9 +67,9 @@ var fakeFailingGoogleCredentials = { var key_data, pem_data, ca_data; before(function() { - var key_path = path.join(__dirname, '../data/server1.key'); - var pem_path = path.join(__dirname, '../data/server1.pem'); - var ca_path = path.join(__dirname, '../data/ca.pem'); + var key_path = path.join(__dirname, '/data/server1.key'); + var pem_path = path.join(__dirname, '/data/server1.pem'); + var ca_path = path.join(__dirname, '/data/ca.pem'); key_data = fs.readFileSync(key_path); pem_data = fs.readFileSync(pem_path); ca_data = fs.readFileSync(ca_path); @@ -306,7 +306,7 @@ describe('client credentials', function() { done(); }); }); - it('should propagate errors that the updater emits', function(done) { + it('should fail the call if the updater fails', function(done) { var metadataUpdater = function(service_url, callback) { var error = new Error('Authentication error'); error.code = grpc.status.UNAUTHENTICATED; @@ -319,10 +319,10 @@ describe('client credentials', function() { client_options); client.unary({}, function(err, data) { assert(err); - assert.strictEqual(err.message, + assert.strictEqual(err.details, 'Getting metadata from plugin failed with error: ' + 'Authentication error'); - assert.strictEqual(err.code, grpc.status.UNAUTHENTICATED); + assert.notStrictEqual(err.code, grpc.status.OK); done(); }); }); @@ -369,7 +369,7 @@ describe('client credentials', function() { client_options); client.unary({}, function(err, data) { assert(err); - assert.strictEqual(err.message, + assert.strictEqual(err.details, 'Getting metadata from plugin failed with error: ' + 'Authentication failure'); done(); diff --git a/packages/grpc-native-core/test/data/README b/packages/grpc-native-core/test/data/README new file mode 100644 index 00000000..888d95b9 --- /dev/null +++ b/packages/grpc-native-core/test/data/README @@ -0,0 +1 @@ +CONFIRMEDTESTKEY diff --git a/packages/grpc-native-core/test/data/ca.pem b/packages/grpc-native-core/test/data/ca.pem new file mode 100644 index 00000000..6c8511a7 --- /dev/null +++ b/packages/grpc-native-core/test/data/ca.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/packages/grpc-native-core/test/data/server1.key b/packages/grpc-native-core/test/data/server1.key new file mode 100644 index 00000000..143a5b87 --- /dev/null +++ b/packages/grpc-native-core/test/data/server1.key @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD +M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf +3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY +AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm +V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY +tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p +dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q +K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR +81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff +DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd +aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 +ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 +XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe +F98XJ7tIFfJq +-----END PRIVATE KEY----- diff --git a/packages/grpc-native-core/test/data/server1.pem b/packages/grpc-native-core/test/data/server1.pem new file mode 100644 index 00000000..f3d43fcc --- /dev/null +++ b/packages/grpc-native-core/test/data/server1.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET +MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ +dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx +MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV +BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 +ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco +LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg +zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd +9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw +CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy +em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G +CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 +hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh +y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 +-----END CERTIFICATE----- diff --git a/packages/grpc-native-core/test/echo_service.proto b/packages/grpc-native-core/test/echo_service.proto new file mode 100644 index 00000000..414b421e --- /dev/null +++ b/packages/grpc-native-core/test/echo_service.proto @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2018 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-native-core/test/math/math_grpc_pb.js b/packages/grpc-native-core/test/math/math_grpc_pb.js index afd08a34..82105c0f 100644 --- a/packages/grpc-native-core/test/math/math_grpc_pb.js +++ b/packages/grpc-native-core/test/math/math_grpc_pb.js @@ -16,8 +16,8 @@ // limitations under the License. // 'use strict'; -var grpc = require('grpc'); -var math_math_pb = require('../math/math_pb.js'); +var grpc = require('../..'); +var math_math_pb = require('./math_pb.js'); function serialize_DivArgs(arg) { if (!(arg instanceof math_math_pb.DivArgs)) { diff --git a/packages/grpc-native-core/test/metadata_test.js b/packages/grpc-native-core/test/metadata_test.js index 4ba54e0a..6063b31f 100644 --- a/packages/grpc-native-core/test/metadata_test.js +++ b/packages/grpc-native-core/test/metadata_test.js @@ -18,7 +18,7 @@ 'use strict'; -var Metadata = require('../src/metadata.js'); +var Metadata = require('..').Metadata; var assert = require('assert'); diff --git a/packages/grpc-native-core/test/server_test.js b/packages/grpc-native-core/test/server_test.js index c1b94194..5351c138 100644 --- a/packages/grpc-native-core/test/server_test.js +++ b/packages/grpc-native-core/test/server_test.js @@ -72,8 +72,8 @@ describe('server', function() { }); it('should bind to an unused port with ssl credentials', function() { var port; - var key_path = path.join(__dirname, '../../../test/data/server1.key'); - var pem_path = path.join(__dirname, '../../../test/data/server1.pem'); + var key_path = path.join(__dirname, '/data/server1.key'); + var pem_path = path.join(__dirname, '/data/server1.pem'); var key_data = fs.readFileSync(key_path); var pem_data = fs.readFileSync(pem_path); var creds = grpc.ServerCredentials.createSsl(null, diff --git a/test/api/surface_test.js b/packages/grpc-native-core/test/surface_test.js similarity index 91% rename from test/api/surface_test.js rename to packages/grpc-native-core/test/surface_test.js index b2072fdd..36179738 100644 --- a/test/api/surface_test.js +++ b/packages/grpc-native-core/test/surface_test.js @@ -21,10 +21,10 @@ var assert = require('assert'); var _ = require('lodash'); -var grpc = require('grpc'); +var grpc = require('..'); var MathClient = grpc.load( - __dirname + '/../../packages/grpc-native-core/deps/grpc/src/proto/math/math.proto').math.Math; + __dirname + '/../deps/grpc/src/proto/math/math.proto').math.Math; var mathServiceAttrs = MathClient.service; /** @@ -485,7 +485,7 @@ describe('Echo metadata', function() { call.end(); }); it('shows the correct user-agent string', function(done) { - var version = require('grpc/package.json').version; + var version = require('../package.json').version; var call = client.unary({}, metadata, function(err, data) { assert.ifError(err); }); call.on('metadata', function(metadata) { @@ -506,6 +506,116 @@ describe('Echo metadata', function() { done(); }); }); + describe('Call argument handling', function() { + describe('Unary call', function() { + it('Should handle undefined options', function(done) { + var call = client.unary({}, metadata, undefined, function(err, data) { + assert.ifError(err); + }); + call.on('metadata', function(metadata) { + assert.deepEqual(metadata.get('key'), ['value']); + done(); + }); + }); + it('Should handle two undefined arguments', function(done) { + var call = client.unary({}, undefined, undefined, function(err, data) { + assert.ifError(err); + }); + call.on('metadata', function(metadata) { + done(); + }); + }); + it('Should handle one undefined argument', function(done) { + var call = client.unary({}, undefined, function(err, data) { + assert.ifError(err); + }); + call.on('metadata', function(metadata) { + done(); + }); + }); + }); + describe('Client stream call', function() { + it('Should handle undefined options', function(done) { + var call = client.clientStream(metadata, undefined, function(err, data) { + assert.ifError(err); + }); + call.on('metadata', function(metadata) { + assert.deepEqual(metadata.get('key'), ['value']); + done(); + }); + call.end(); + }); + it('Should handle two undefined arguments', function(done) { + var call = client.clientStream(undefined, undefined, function(err, data) { + assert.ifError(err); + }); + call.on('metadata', function(metadata) { + done(); + }); + call.end(); + }); + it('Should handle one undefined argument', function(done) { + var call = client.clientStream(undefined, function(err, data) { + assert.ifError(err); + }); + call.on('metadata', function(metadata) { + done(); + }); + call.end(); + }); + }); + describe('Server stream call', function() { + it('Should handle undefined options', function(done) { + var call = client.serverStream({}, metadata, undefined); + call.on('data', function() {}); + call.on('metadata', function(metadata) { + assert.deepEqual(metadata.get('key'), ['value']); + done(); + }); + }); + it('Should handle two undefined arguments', function(done) { + var call = client.serverStream({}, undefined, undefined); + call.on('data', function() {}); + call.on('metadata', function(metadata) { + done(); + }); + }); + it('Should handle one undefined argument', function(done) { + var call = client.serverStream({}, undefined); + call.on('data', function() {}); + call.on('metadata', function(metadata) { + done(); + }); + }); + }); + describe('Bidi stream call', function() { + it('Should handle undefined options', function(done) { + var call = client.bidiStream(metadata, undefined); + call.on('data', function() {}); + call.on('metadata', function(metadata) { + assert.deepEqual(metadata.get('key'), ['value']); + done(); + }); + call.end(); + }); + it('Should handle two undefined arguments', function(done) { + var call = client.bidiStream(undefined, undefined); + call.on('data', function() {}); + call.on('metadata', function(metadata) { + done(); + }); + call.end(); + }); + it('Should handle one undefined argument', function(done) { + var call = client.bidiStream(undefined); + call.on('data', function() {}); + call.on('metadata', function(metadata) { + done(); + }); + call.end(); + }); + }); + }); }); describe('Client malformed response handling', function() { var server; @@ -981,7 +1091,7 @@ describe('Other conditions', function() { client.unary({error: true}, function(err, data) { assert(err); assert.strictEqual(err.code, grpc.status.UNKNOWN); - assert.strictEqual(err.message, 'Requested error'); + assert.strictEqual(err.details, 'Requested error'); done(); }); }); @@ -989,7 +1099,7 @@ describe('Other conditions', function() { var call = client.clientStream(function(err, data) { assert(err); assert.strictEqual(err.code, grpc.status.UNKNOWN); - assert.strictEqual(err.message, 'Requested error'); + assert.strictEqual(err.details, 'Requested error'); done(); }); call.write({error: false}); @@ -1001,7 +1111,7 @@ describe('Other conditions', function() { call.on('data', function(){}); call.on('error', function(error) { assert.strictEqual(error.code, grpc.status.UNKNOWN); - assert.strictEqual(error.message, 'Requested error'); + assert.strictEqual(error.details, 'Requested error'); done(); }); }); @@ -1013,7 +1123,7 @@ describe('Other conditions', function() { call.on('data', function(){}); call.on('error', function(error) { assert.strictEqual(error.code, grpc.status.UNKNOWN); - assert.strictEqual(error.message, 'Requested error'); + assert.strictEqual(error.details, 'Requested error'); done(); }); }); diff --git a/test/api/test_service.json b/packages/grpc-native-core/test/test_service.json similarity index 100% rename from test/api/test_service.json rename to packages/grpc-native-core/test/test_service.json diff --git a/test/api/test_service.proto b/packages/grpc-native-core/test/test_service.proto similarity index 100% rename from test/api/test_service.proto rename to packages/grpc-native-core/test/test_service.proto diff --git a/packages/grpc-native-core/tools/buildgen/generate_projects.sh b/packages/grpc-native-core/tools/buildgen/generate_projects.sh index 41a228da..d68c467f 100755 --- a/packages/grpc-native-core/tools/buildgen/generate_projects.sh +++ b/packages/grpc-native-core/tools/buildgen/generate_projects.sh @@ -18,4 +18,4 @@ set -e cd `dirname $0`/../.. root=`pwd` -./deps/grpc/tools/buildgen/generate_projects.sh --base=$root --templates `find templates -type f` +./deps/grpc/tools/buildgen/generate_projects.sh $root/build.yaml --base=$root --templates `find templates -type f` diff --git a/packages/grpc-native-core/tools/docker/alpine_artifact/Dockerfile b/packages/grpc-native-core/tools/docker/alpine_artifact/Dockerfile new file mode 100644 index 00000000..0674654a --- /dev/null +++ b/packages/grpc-native-core/tools/docker/alpine_artifact/Dockerfile @@ -0,0 +1,2 @@ +FROM node:8-alpine +RUN apk add --no-cache python curl bash build-base diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_all_linux_artifacts.sh b/packages/grpc-native-core/tools/run_tests/artifacts/build_all_linux_artifacts.sh new file mode 100755 index 00000000..59f62a2f --- /dev/null +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_all_linux_artifacts.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# 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. + +set -ex + +cd $(dirname $0) +tool_dir=$(pwd) +cd $tool_dir/../../.. +base_dir=$(pwd) + +export ARTIFACTS_OUT=$base_dir/artifacts +export JOBS=8 + +rm -rf build || true + +mkdir -p "${ARTIFACTS_OUT}" + +docker build -t alpine_node_artifact $base_dir/tools/docker/alpine_artifact + +$tool_dir/build_artifact_node.sh + +$tool_dir/build_artifact_node_arm.sh + +docker run -e JOBS=8 -e ARTIFACTS_OUT=/var/grpc/artifacts -v $base_dir:/var/grpc alpine_node_artifact /var/grpc/tools/run_tests/artifacts/build_artifact_node.sh --with-alpine 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 new file mode 100644 index 00000000..e84dd896 --- /dev/null +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.bat @@ -0,0 +1,54 @@ +@rem Copyright 2016 gRPC authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. + +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 + +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 + +set PATH=%PATH%;C:\Program Files\nodejs\;%APPDATA%\npm + +set JOBS=8 + +del /f /q BUILD || rmdir build /s /q + +call npm update || goto :error + +mkdir -p %ARTIFACTS_OUT% + +for %%a in (%arch_list%) do ( + for %%v in (%node_versions%) do ( + call .\node_modules\.bin\node-pre-gyp.cmd configure build --target=%%v --target_arch=%%a + + @rem Try again after removing openssl headers + rmdir "%USERPROFILE%\.node-gyp\%%v\include\node\openssl" /S /Q + rmdir "%USERPROFILE%\.node-gyp\iojs-%%v\include\node\openssl" /S /Q + call .\node_modules\.bin\node-pre-gyp.cmd build package --target=%%v --target_arch=%%a || goto :error + + xcopy /Y /I /S build\stage\* %ARTIFACTS_OUT%\ || goto :error + ) + + for %%v in (%electron_versions%) do ( + cmd /V /C "set "HOME=%USERPROFILE%\electron-gyp" && call .\node_modules\.bin\node-pre-gyp.cmd configure rebuild package --runtime=electron --target=%%v --target_arch=%%a --disturl=https://atom.io/download/electron" || goto :error + + xcopy /Y /I /S build\stage\* %ARTIFACTS_OUT%\ || goto :error + ) +) +if %errorlevel% neq 0 exit /b %errorlevel% + +goto :EOF + +:error +exit /b 1 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 new file mode 100755 index 00000000..006370da --- /dev/null +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Copyright 2016 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. + + +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 ) +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 ) + +while true ; do + case $1 in + --with-alpine) + arch_list=( x64 ) + electron_versions=( ) + ;; + "") + ;; + *) + echo "Unknown parameter: $1" + exit 1 + ;; + esac + shift || break +done + +umask 022 + +cd $(dirname $0)/../../.. + +rm -rf build || true + +mkdir -p "${ARTIFACTS_OUT}" + +npm update + +for arch in ${arch_list[@]} +do + for version in ${node_versions[@]} + do + ./node_modules/.bin/node-pre-gyp configure rebuild package --target=$version --target_arch=$arch + cp -r build/stage/* "${ARTIFACTS_OUT}"/ + done + + for version in ${electron_versions[@]} + do + HOME=~/.electron-gyp ./node_modules/.bin/node-pre-gyp configure rebuild package --runtime=electron --target=$version --target_arch=$arch --disturl=https://atom.io/download/electron + cp -r build/stage/* "${ARTIFACTS_OUT}"/ + done +done + +rm -rf build || true 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 new file mode 100755 index 00000000..7faf15cc --- /dev/null +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node_arm.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# 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. + +set -ex + +cd $(dirname $0)/../../.. + +rm -rf build || true + +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 ) + +for version in ${node_versions[@]} +do + # Cross compile for ARM on x64 + # Requires debian or ubuntu packages "g++-aarch64-linux-gnu" and "g++-arm-linux-gnueabihf". + CC=arm-linux-gnueabihf-gcc CXX=arm-linux-gnueabihf-g++ LD=arm-linux-gnueabihf-g++ ./node_modules/.bin/node-pre-gyp configure rebuild package testpackage --target=$version --target_arch=arm + CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ LD=aarch64-linux-gnu-g++ ./node_modules/.bin/node-pre-gyp configure rebuild package testpackage --target=$version --target_arch=arm64 + cp -r build/stage/* "${ARTIFACTS_OUT}"/ +done diff --git a/packages/grpc-native-core/tools/run_tests/artifacts/build_package_node.sh b/packages/grpc-native-core/tools/run_tests/artifacts/build_package_node.sh new file mode 100755 index 00000000..412d0be2 --- /dev/null +++ b/packages/grpc-native-core/tools/run_tests/artifacts/build_package_node.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Copyright 2016 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. + +set -ex + +cd $(dirname $0)/../../.. + +base=$(pwd) + +artifacts=$base/artifacts + +mkdir -p $artifacts +cp -r $EXTERNAL_GIT_ROOT/platform={windows,linux,macos}/artifacts/node_ext_*/* $artifacts/ || true + +npm update +npm pack + +cp grpc-*.tgz $artifacts/grpc.tgz + +mkdir -p bin + +cd $base/src/node/health_check +npm pack +cp grpc-health-check-*.tgz $artifacts/ + +cd $base/src/node/tools +npm update +npm pack +cp grpc-tools-*.tgz $artifacts/ +tools_version=$(npm list | grep -oP '(?<=grpc-tools@)\S+') + +output_dir=$artifacts/grpc-precompiled-binaries/node/grpc-tools/v$tools_version +mkdir -p $output_dir + +well_known_protos=( any api compiler/plugin descriptor duration empty field_mask source_context struct timestamp type wrappers ) + +for arch in {x86,x64}; do + case $arch in + x86) + node_arch=ia32 + ;; + *) + node_arch=$arch + ;; + esac + for plat in {windows,linux,macos}; do + case $plat in + windows) + node_plat=win32 + ;; + macos) + node_plat=darwin + ;; + *) + node_plat=$plat + ;; + esac + rm -r bin/* + input_dir="$EXTERNAL_GIT_ROOT/platform=${plat}/artifacts/protoc_${plat}_${arch}" + cp $input_dir/protoc* bin/ + cp $input_dir/grpc_node_plugin* bin/ + mkdir -p bin/google/protobuf + mkdir -p bin/google/protobuf/compiler # needed for plugin.proto + for proto in "${well_known_protos[@]}"; do + cp $base/third_party/protobuf/src/google/protobuf/$proto.proto bin/google/protobuf/$proto.proto + done + tar -czf $output_dir/$node_plat-$node_arch.tar.gz bin/ + done +done diff --git a/packages/grpc-native/gulpfile.js b/packages/grpc-native/gulpfile.js new file mode 100644 index 00000000..37fd6af9 --- /dev/null +++ b/packages/grpc-native/gulpfile.js @@ -0,0 +1,46 @@ +/* + * 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'); + +// gulp-help monkeypatches tasks to have an additional description parameter +const gulp = help(_gulp); + +const execa = require('execa'); +const path = require('path'); +const del = require('del'); +const linkSync = require('../../util').linkSync; + +const nativeDir = __dirname; + +gulp.task('clean.links', 'Delete npm links', () => { + return del([path.resolve(nativeDir, 'node_modules/grpc'), + path.resolve(nativeDir, 'node_modules/@grpc/surface')]); +}); + +gulp.task('clean.all', 'Delete all files created by tasks', + ['clean.links']); + +gulp.task('install', 'Install dependencies', () => { + return execa('npm', ['install', '--unsafe-perm'], {cwd: nativeDir, stdio: 'inherit'}); +}); + +gulp.task('link.add', 'Link local copies of dependencies', () => { + linkSync(nativeDir, './node_modules/grpc', '../grpc-native-core'); + linkSync(nativeDir, './node_modules/@grpc/surface', '../grpc-surface'); +}); diff --git a/packages/grpc-native/index.js b/packages/grpc-native/index.js new file mode 100644 index 00000000..692cf835 --- /dev/null +++ b/packages/grpc-native/index.js @@ -0,0 +1,22 @@ +/** + * @license + * 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. + * + */ + +'use strict'; + +// TODO(mlumish): This should eventually be @grpc/native-core instead of grpc +module.exports = require('@grpc/surface')(require('grpc')); diff --git a/packages/grpc-native/package.json b/packages/grpc-native/package.json new file mode 100644 index 00000000..d141a2f9 --- /dev/null +++ b/packages/grpc-native/package.json @@ -0,0 +1,23 @@ +{ + "name": "@grpc/native", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/grpc/grpc-node.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/grpc/grpc-node/issues" + }, + "homepage": "https://grpc.io", + "dependencies": { + "grpc": "^1.6.0", + "@grpc/surface": "^0.1.0" + } +} diff --git a/packages/grpc-protobufjs/gulpfile.ts b/packages/grpc-protobufjs/gulpfile.ts new file mode 100644 index 00000000..550ce364 --- /dev/null +++ b/packages/grpc-protobufjs/gulpfile.ts @@ -0,0 +1,60 @@ +/* + * 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. + * + */ + +import * as _gulp from 'gulp'; +import * as help from 'gulp-help'; + +import * as fs from 'fs'; +import * as mocha from 'gulp-mocha'; +import * as path from 'path'; +import * as execa from 'execa'; + +// gulp-help monkeypatches tasks to have an additional description parameter +const gulp = help(_gulp); + +Error.stackTraceLimit = Infinity; + +const protojsDir = __dirname; +const tslintPath = path.resolve(protojsDir, 'node_modules/google-ts-style/tslint.json'); +const tsconfigPath = path.resolve(protojsDir, 'tsconfig.json'); +const outDir = path.resolve(protojsDir, 'build'); +const srcDir = path.resolve(protojsDir, 'src'); +const testDir = path.resolve(protojsDir, 'test'); + +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')); + +/** + * 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')); + +gulp.task('clean', 'Deletes transpiled code.', ['install'], + () => execNpmCommand('clean')); + +gulp.task('clean.all', 'Deletes all files added by targets', ['clean']); + +/** + * Transpiles TypeScript files in src/ to JavaScript according to the settings + * found in tsconfig.json. + */ +gulp.task('compile', 'Transpiles src/.', () => execNpmCommand('compile')); diff --git a/packages/grpc-protobufjs/package.json b/packages/grpc-protobufjs/package.json new file mode 100644 index 00000000..bcf53e5f --- /dev/null +++ b/packages/grpc-protobufjs/package.json @@ -0,0 +1,41 @@ +{ + "name": "@grpc/protobufjs", + "version": "0.1.0", + "description": "", + "main": "build/src/index.js", + "scripts": { + "build": "npm run compile", + "clean": "gts clean", + "compile": "tsc -p .", + "format": "clang-format -i -style=\"{Language: JavaScript, BasedOnStyle: Google, ColumnLimit: 80}\" src/*.ts test/*.ts", + "lint": "tslint -c node_modules/google-ts-style/tslint.json -p . -t codeFrame --type-check", + "prepare": "npm run compile", + "test": "gulp test", + "check": "gts check", + "fix": "gts fix", + "pretest": "npm run compile", + "posttest": "npm run check" + }, + "repository": { + "type": "git", + "url": "https://github.com/grpc/grpc-node.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/grpc/grpc-node/issues" + }, + "files": [ + "build/src/*.d.ts", + "build/src/*.js" + ], + "dependencies": { + "@types/lodash": "^4.14.104", + "@types/node": "^9.4.6", + "clang-format": "^1.2.2", + "gts": "^0.5.3", + "lodash": "^4.17.5", + "protobufjs": "^6.8.5", + "typescript": "~2.7.2" + } +} diff --git a/packages/grpc-protobufjs/src/index.ts b/packages/grpc-protobufjs/src/index.ts new file mode 100644 index 00000000..85c2c941 --- /dev/null +++ b/packages/grpc-protobufjs/src/index.ts @@ -0,0 +1,184 @@ +/** + * @license + * Copyright 2018 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 Protobuf from 'protobufjs'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as _ from 'lodash'; + +export interface Serialize { + (value: T): Buffer; +} + +export interface Deserialize { + (bytes: Buffer): T; +} + +export interface MethodDefinition { + path: string; + requestStream: boolean; + responseStream: boolean; + requestSerialize: Serialize; + responseSerialize: Serialize; + requestDeserialize: Deserialize; + responseDeserialize: Deserialize; + originalName?: string; +} + +export interface ServiceDefinition { + [index: string]: MethodDefinition; +} + +export interface PackageDefinition { + [index: string]: ServiceDefinition; +} + +export type Options = Protobuf.IParseOptions & Protobuf.IConversionOptions & { + include?: string[]; +}; + +function joinName(baseName: string, name: string): string { + if (baseName === '') { + return name; + } else { + return baseName + '.' + name; + } +} + +function getAllServices(obj: Protobuf.NamespaceBase, parentName: string): Array<[string, Protobuf.Service]> { + const objName = joinName(parentName, obj.name); + if (obj.hasOwnProperty('methods')) { + return [[objName, obj as Protobuf.Service]]; + } else { + return obj.nestedArray.map((child) => { + if (child.hasOwnProperty('nested')) { + return getAllServices(child as Protobuf.NamespaceBase, objName); + } else { + return []; + } + }).reduce((accumulator, currentValue) => accumulator.concat(currentValue), []); + } +} + +function createDeserializer(cls: Protobuf.Type, options: Options): Deserialize { + return function deserialize(argBuf: Buffer): object { + return cls.toObject(cls.decode(argBuf), options); + }; +} + +function createSerializer(cls: Protobuf.Type): Serialize { + return function serialize(arg: object): Buffer { + const message = cls.fromObject(arg); + return cls.encode(message).finish() as Buffer; + }; +} + +function createMethodDefinition(method: Protobuf.Method, serviceName: string, options: Options): MethodDefinition { + return { + path: '/' + serviceName + '/' + method.name, + requestStream: !!method.requestStream, + responseStream: !!method.responseStream, + requestSerialize: createSerializer(method.resolvedRequestType as Protobuf.Type), + requestDeserialize: createDeserializer(method.resolvedRequestType as Protobuf.Type, options), + responseSerialize: createSerializer(method.resolvedResponseType as Protobuf.Type), + responseDeserialize: createDeserializer(method.resolvedResponseType as Protobuf.Type, options), + // TODO(murgatroid99): Find a better way to handle this + originalName: _.camelCase(method.name) + }; +} + +function createServiceDefinition(service: Protobuf.Service, name: string, options: Options): ServiceDefinition { + const def: ServiceDefinition = {}; + for (const method of service.methodsArray) { + def[method.name] = createMethodDefinition(method, name, options); + } + return def; +} + +function createPackageDefinition(root: Protobuf.Root, options: Options): PackageDefinition { + const def: PackageDefinition = {}; + for (const [name, service] of getAllServices(root, '')) { + def[name] = createServiceDefinition(service, name, options); + } + return def; +} + +function addIncludePathResolver(root: Protobuf.Root, includePaths: string[]) { + root.resolvePath = (origin: string, target: string) => { + for (const directory of includePaths) { + const fullPath: string = path.join(directory, target); + try { + fs.accessSync(fullPath, fs.constants.R_OK); + return fullPath; + } catch (err) { + continue; + } + } + return null; + }; +} + +/** + * Load a .proto file with the specified options. + * @param filename The file path to load. Can be an absolute path or relative to + * an include path. + * @param options.keepCase Preserve field names. The default is to change them + * to camel case. + * @param options.longs The type that should be used to represent `long` values. + * Valid options are `Number` and `String`. Defaults to a `Long` object type + * from a library. + * @param options.enums The type that should be used to represent `enum` values. + * The only valid option is `String`. Defaults to the numeric value. + * @param options.bytes The type that should be used to represent `bytes` + * values. Valid options are `Array` and `String`. The default is to use + * `Buffer`. + * @param options.defaults Set default values on output objects. Defaults to + * `false`. + * @param options.arrays Set empty arrays for missing array values even if + * `defaults` is `false`. Defaults to `false`. + * @param options.objects Set empty objects for missing object values even if + * `defaults` is `false`. Defaults to `false`. + * @param options.oneofs Set virtual oneof properties to the present field's + * name + * @param options.include Paths to search for imported `.proto` files. + */ +export function load(filename: string, options: Options): Promise { + const root: Protobuf.Root = new Protobuf.Root(); + if (!!options.include) { + if (!(options.include instanceof Array)) { + return Promise.reject(new Error('The include option must be an array')); + } + addIncludePathResolver(root, options.include as string[]); + } + return root.load(filename, options).then((loadedRoot) => { + loadedRoot.resolveAll(); + return createPackageDefinition(root, options); + }); +} + +export function loadSync(filename: string, options: Options): PackageDefinition { + const root: Protobuf.Root = new Protobuf.Root(); + if (!!options.include) { + if (!(options.include instanceof Array)) { + throw new Error('The include option must be an array'); + } + addIncludePathResolver(root, options.include as string[]); + } + const loadedRoot = root.loadSync(filename, options); + loadedRoot.resolveAll(); + return createPackageDefinition(root, options); +} diff --git a/packages/grpc-protobufjs/tsconfig.json b/packages/grpc-protobufjs/tsconfig.json new file mode 100644 index 00000000..9218530c --- /dev/null +++ b/packages/grpc-protobufjs/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/*.ts", + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/grpc-surface/gulpfile.js b/packages/grpc-surface/gulpfile.js new file mode 100644 index 00000000..9089587a --- /dev/null +++ b/packages/grpc-surface/gulpfile.js @@ -0,0 +1,30 @@ +/* + * 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'); + +// gulp-help monkeypatches tasks to have an additional description parameter +const gulp = help(_gulp); + +const execa = require('execa'); + +const surfaceDir = __dirname; + +gulp.task('install', 'Install surface dependencies', () => { + return execa('npm', ['install', '--unsafe-perm'], {cwd: surfaceDir, stdio: 'inherit'}); +}); diff --git a/packages/grpc-surface/index.js b/packages/grpc-surface/index.js new file mode 100644 index 00000000..d8bcef5e --- /dev/null +++ b/packages/grpc-surface/index.js @@ -0,0 +1,147 @@ +/** + * @license + * 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. + * + */ + +'use strict'; + +const util = require('util'); + +const _ = require('lodash'); + +module.exports = function(grpc) { + + let exports = {}; + + const Client = grpc.Client; + + function getDefaultValues(metadata, options) { + var res = {}; + res.metadata = metadata || new grpc.Metadata(); + res.options = options || {}; + return res; + } + + /** + * Map with wrappers for each type of requester function to make it use the old + * argument order with optional arguments after the callback. + * @access private + */ + var deprecated_request_wrap = { + unary: function(makeUnaryRequest) { + return function makeWrappedUnaryRequest(argument, callback, + metadata, options) { + /* jshint validthis: true */ + var opt_args = getDefaultValues(metadata, metadata); + return makeUnaryRequest.call(this, argument, opt_args.metadata, + opt_args.options, callback); + }; + }, + client_stream: function(makeServerStreamRequest) { + return function makeWrappedClientStreamRequest(callback, metadata, + options) { + /* jshint validthis: true */ + var opt_args = getDefaultValues(metadata, options); + return makeServerStreamRequest.call(this, opt_args.metadata, + opt_args.options, callback); + }; + }, + server_stream: _.identity, + bidi: _.identity + }; + + /** + * Map with short names for each of the requester maker functions. Used in + * makeClientConstructor + * @private + */ + const requester_funcs = { + unary: Client.prototype.makeUnaryRequest, + server_stream: Client.prototype.makeServerStreamRequest, + client_stream: Client.prototype.makeClientStreamRequest, + bidi: Client.prototype.makeBidiStreamRequest + }; + + /** + * Creates a constructor for a client with the given methods, as specified in + * the methods argument. The resulting class will have an instance method for + * each method in the service, which is a partial application of one of the + * [Client]{@link grpc.Client} request methods, depending on `requestSerialize` + * and `responseSerialize`, with the `method`, `serialize`, and `deserialize` + * arguments predefined. + * @memberof grpc + * @alias grpc~makeGenericClientConstructor + * @param {grpc~ServiceDefinition} methods An object mapping method names to + * method attributes + * @param {string} serviceName The fully qualified name of the service + * @param {Object} class_options An options object. + * @return {function} New client constructor, which is a subclass of + * {@link grpc.Client}, and has the same arguments as that constructor. + */ + exports.makeClientConstructor = function(methods, serviceName, + class_options) { + if (!class_options) { + class_options = {}; + } + + function ServiceClient(address, credentials, options) { + Client.call(this, address, credentials, options); + } + + util.inherits(ServiceClient, Client); + + _.each(methods, function(attrs, name) { + var method_type; + // TODO(murgatroid99): Verify that we don't need this anymore + if (_.startsWith(name, '$')) { + throw new Error('Method names cannot start with $'); + } + if (attrs.requestStream) { + if (attrs.responseStream) { + method_type = 'bidi'; + } else { + method_type = 'client_stream'; + } + } else { + if (attrs.responseStream) { + method_type = 'server_stream'; + } else { + method_type = 'unary'; + } + } + var serialize = attrs.requestSerialize; + var deserialize = attrs.responseDeserialize; + var method_func = _.partial(requester_funcs[method_type], attrs.path, + serialize, deserialize); + if (class_options.deprecatedArgumentOrder) { + ServiceClient.prototype[name] = deprecated_request_wrap(method_func); + } else { + ServiceClient.prototype[name] = method_func; + } + // Associate all provided attributes with the method + _.assign(ServiceClient.prototype[name], attrs); + if (attrs.originalName) { + ServiceClient.prototype[attrs.originalName] = ServiceClient.prototype[name]; + } + }); + + ServiceClient.service = methods; + + return ServiceClient; + }; + + return Object.assign(exports, grpc); +}; diff --git a/packages/grpc-surface/package.json b/packages/grpc-surface/package.json new file mode 100644 index 00000000..7abcfd53 --- /dev/null +++ b/packages/grpc-surface/package.json @@ -0,0 +1,21 @@ +{ + "name": "@grpc/surface", + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/grpc/grpc-node.git" + }, + "author": "", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/grpc/grpc-node/issues" + }, + "dependencies": { + "lodash": "^4.17.4" + } +} diff --git a/packages/grpc-tools/package.json b/packages/grpc-tools/package.json index 7159010a..c1062c10 100644 --- a/packages/grpc-tools/package.json +++ b/packages/grpc-tools/package.json @@ -1,6 +1,6 @@ { "name": "grpc-tools", - "version": "1.7.0-dev", + "version": "1.7.0-pre1", "author": "Google Inc.", "description": "Tools for developing with gRPC on Node.js", "homepage": "https://grpc.io/", diff --git a/run-tests.bat b/run-tests.bat index 7e792bbd..08daf796 100644 --- a/run-tests.bat +++ b/run-tests.bat @@ -20,6 +20,7 @@ PowerShell -Command .\install-nvm-windows.ps1 SET NVM_HOME=%ROOT%nvm SET NVM_SYMLINK=%ROOT%nvm\nodejs SET PATH=%NVM_HOME%;%NVM_SYMLINK%;%PATH% +SET JOBS=8 nvm version @@ -34,6 +35,7 @@ SET FAILED=0 for %%v in (4.8.4 6.11.3 7.9.0 8.5.0) do ( nvm install %%v nvm use %%v + npm install -g npm node -e "console.log(process.versions)" SET JUNIT_REPORT_PATH=reports/node%%v/ diff --git a/run-tests.sh b/run-tests.sh index c1a76889..5df88186 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -38,6 +38,7 @@ set -ex npm install --unsafe-perm mkdir -p reports +export JOBS=8 # TODO(mlumish): Add electron tests diff --git a/setup.sh b/setup.sh index b6ecf6d0..896def2a 100755 --- a/setup.sh +++ b/setup.sh @@ -1,3 +1,18 @@ +#!/bin/sh +# 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. + if [ -z $NODE_H2 ]; then echo "\$NODE_H2 must point to a node binary" exit 1 diff --git a/test/any_grpc.js b/test/any_grpc.js new file mode 100644 index 00000000..250f0098 --- /dev/null +++ b/test/any_grpc.js @@ -0,0 +1,24 @@ +// TODO: Instead of attempting to expose both implementations of gRPC in +// a single object, the tests should be re-written in a way that makes it clear +// that two separate implementations are being tested against one another. + +const _ = require('lodash'); + +function getImplementation(globalField) { + if (global[globalField] !== 'js' && global[globalField] !== 'native') { + throw new Error([ + `Invalid value for global.${globalField}: ${global.globalField}.`, + 'If running from the command line, please --require a fixture first.' + ].join(' ')); + } + const impl = global[globalField]; + return require(`../packages/grpc-${impl}-core`); +} + +const clientImpl = getImplementation('_client_implementation'); +const serverImpl = getImplementation('_server_implementation'); + +module.exports = { + client: clientImpl, + server: serverImpl +}; diff --git a/test/api/echo_service.proto b/test/api/echo_service.proto deleted file mode 100644 index 0b27c8b1..00000000 --- a/test/api/echo_service.proto +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2015 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); -} diff --git a/test/api/test_messages.proto b/test/api/test_messages.proto deleted file mode 100644 index da1ef565..00000000 --- a/test/api/test_messages.proto +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2015 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 LongValues { - int64 int_64 = 1; - uint64 uint_64 = 2; - sint64 sint_64 = 3; - fixed64 fixed_64 = 4; - sfixed64 sfixed_64 = 5; -} - -message SequenceValues { - bytes bytes_field = 1; - repeated int32 repeated_field = 2; -} - -message OneOfValues { - oneof oneof_choice { - int32 int_choice = 1; - string string_choice = 2; - } -} - -enum TestEnum { - ZERO = 0; - ONE = 1; - TWO = 2; -} - -message EnumValues { - TestEnum enum_value = 1; -} diff --git a/test/client-libraries-integration/.gitignore b/test/client-libraries-integration/.gitignore new file mode 100644 index 00000000..5fe2e366 --- /dev/null +++ b/test/client-libraries-integration/.gitignore @@ -0,0 +1 @@ +nodejs-*/ \ No newline at end of file diff --git a/test/client-libraries-integration/init.sh b/test/client-libraries-integration/init.sh new file mode 100755 index 00000000..c5834a77 --- /dev/null +++ b/test/client-libraries-integration/init.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +npm install + +for dir in $(node -p "require('./repositories.json').join('\n')"); do + if [ ! -d $dir ]; then + git clone https://github.com/googleapis/$dir + fi + pushd $dir + npm install + popd + node --require ./use-grpc-js.js $(npm bin)/_mocha --timeout 60000 $dir/system-test/*.js +done diff --git a/test/client-libraries-integration/package.json b/test/client-libraries-integration/package.json new file mode 100644 index 00000000..ea639a06 --- /dev/null +++ b/test/client-libraries-integration/package.json @@ -0,0 +1,9 @@ +{ + "name": "grpc-client-libraries-integration", + "version": "0.0.1", + "description": "", + "dependencies": { + "mocha": "^5.0.4", + "shimmer": "^1.2.0" + } +} diff --git a/test/client-libraries-integration/repositories.json b/test/client-libraries-integration/repositories.json new file mode 100644 index 00000000..31a937e9 --- /dev/null +++ b/test/client-libraries-integration/repositories.json @@ -0,0 +1,20 @@ +[ + "nodejs-datastore", + "nodejs-language", + "nodejs-storage", + "nodejs-translate", + "nodejs-logging", + "nodejs-video-intelligence", + "nodejs-dlp", + "nodejs-firestore", + "nodejs-pubsub", + "nodejs-spanner", + "nodejs-speech", + "nodejs-vision", + "nodejs-bigquery", + "nodejs-monitoring", + "nodejs-bigtable", + "nodejs-dns", + "nodejs-resource", + "nodejs-compute" +] \ No newline at end of file diff --git a/test/client-libraries-integration/use-grpc-js.js b/test/client-libraries-integration/use-grpc-js.js new file mode 100644 index 00000000..05273944 --- /dev/null +++ b/test/client-libraries-integration/use-grpc-js.js @@ -0,0 +1,18 @@ +const Module = require('module'); +const shimmer = require('shimmer'); + +const grpcImpl = require('../../packages/grpc-js-core'); +const grpcPJson = require('../../packages/grpc-js-core/package'); + +shimmer.wrap(Module, '_load', (moduleLoad) => { + return function Module_load(path, parent) { + if (path === 'grpc') { + return grpcImpl; + } else if (path.startsWith('grpc/package')) { + return grpcPJson; + } else { + const result = moduleLoad.apply(this, arguments); + return result; + } + }; +}); diff --git a/test/fixtures/js_js.js b/test/fixtures/js_js.js new file mode 100644 index 00000000..7d72dc49 --- /dev/null +++ b/test/fixtures/js_js.js @@ -0,0 +1,2 @@ +global._server_implementation = 'js'; +global._client_implementation = 'js'; diff --git a/test/fixtures/js_native.js b/test/fixtures/js_native.js new file mode 100644 index 00000000..5088df96 --- /dev/null +++ b/test/fixtures/js_native.js @@ -0,0 +1,2 @@ +global._server_implementation = 'js'; +global._client_implementation = 'native'; \ No newline at end of file diff --git a/test/fixtures/native_js.js b/test/fixtures/native_js.js new file mode 100644 index 00000000..3da4106e --- /dev/null +++ b/test/fixtures/native_js.js @@ -0,0 +1,2 @@ +global._server_implementation = 'native'; +global._client_implementation = 'js'; diff --git a/test/fixtures/native_native.js b/test/fixtures/native_native.js new file mode 100644 index 00000000..5b005e48 --- /dev/null +++ b/test/fixtures/native_native.js @@ -0,0 +1,2 @@ +global._server_implementation = 'native'; +global._client_implementation = 'native'; \ No newline at end of file diff --git a/test/gulpfile.js b/test/gulpfile.js index b66b6e01..61ff1658 100644 --- a/test/gulpfile.js +++ b/test/gulpfile.js @@ -1,9 +1,27 @@ +/* + * 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; // gulp-help monkeypatches tasks to have an additional description parameter const gulp = help(_gulp); @@ -11,17 +29,35 @@ const gulp = help(_gulp); const testDir = __dirname; const apiTestDir = path.resolve(testDir, 'api'); -gulp.task('internal.test.clean.links', 'Delete npm links', () => { - return del(path.resolve(testDir, 'node_modules/grpc')); +gulp.task('install', 'Install test dependencies', () => { + return execa('npm', ['install'], {cwd: testDir, stdio: 'inherit'}); }); -gulp.task('internal.test.clean.all', 'Delete all files created by tasks', - ['internal.test.clean.links']); +gulp.task('clean.all', 'Delete all files created by tasks', () => {}); -gulp.task('internal.test.link.add', 'Link local copies of grpc packages', () => { - return execa('npm', ['link', 'grpc'], {cwd: testDir, stdio: 'inherit'}); -}); - -gulp.task('internal.test.test', 'Run API-level tests', () => { - return gulp.src(`${apiTestDir}/*.js`).pipe(mocha({reporter: 'mocha-jenkins-reporter'})); +gulp.task('test', 'Run API-level tests', () => { + // run mocha tests matching a glob with a pre-required fixture, + // returning the associated gulp stream + const apiTestGlob = `${apiTestDir}/*.js`; + const runTestsWithFixture = (server, client) => new Promise((resolve, reject) => { + const fixture = `${server}_${client}`; + console.log(`Running ${apiTestGlob} with ${server} server + ${client} client`); + gulp.src(apiTestGlob) + .pipe(mocha({ + reporter: 'mocha-jenkins-reporter', + require: `${testDir}/fixtures/${fixture}.js` + })) + .resume() // put the stream in flowing mode + .on('end', resolve) + .on('error', reject); + }); + const runTestsArgPairs = [ + ['native', 'native'], + ['native', 'js'], + // ['js', 'native'], + // ['js', 'js'] + ]; + return runTestsArgPairs.reduce((previousPromise, argPair) => { + return previousPromise.then(runTestsWithFixture.bind(null, argPair[0], argPair[1])); + }, Promise.resolve()); }); diff --git a/test/interop/interop_client.js b/test/interop/interop_client.js index 5fad82bf..da3f6c33 100644 --- a/test/interop/interop_client.js +++ b/test/interop/interop_client.js @@ -20,13 +20,18 @@ var fs = require('fs'); var path = require('path'); -// TODO(murgatroid99): use multiple grpc implementations -var grpc = require('grpc'); -var testProto = grpc.load({ - root: __dirname + '/../../packages/grpc-native-core/deps/grpc', - file: 'src/proto/grpc/testing/test.proto'}).grpc.testing; +var grpc = require('../any_grpc').client; +var protoLoader = require('../../packages/grpc-protobufjs'); var GoogleAuth = require('google-auth-library'); +var protoPackage = protoLoader.loadSync( + 'src/proto/grpc/testing/test.proto', + {keepCase: true, + defaults: true, + enums: String, + include: [__dirname + '/../../packages/grpc-native-core/deps/grpc']}); +var testProto = grpc.loadPackageDefinition(protoPackage).grpc.testing; + var assert = require('assert'); var SERVICE_ACCOUNT_EMAIL; @@ -348,7 +353,7 @@ function statusCodeAndMessage(client, done) { client.unaryCall(arg, function(err, resp) { assert(err); assert.strictEqual(err.code, 2); - assert.strictEqual(err.message, 'test status message'); + assert.strictEqual(err.details, 'test status message'); done(); }); var duplex = client.fullDuplexCall(); diff --git a/test/interop/interop_server.js b/test/interop/interop_server.js index 65bb088d..a0b80cc9 100644 --- a/test/interop/interop_server.js +++ b/test/interop/interop_server.js @@ -22,11 +22,16 @@ var fs = require('fs'); var path = require('path'); var _ = require('lodash'); var AsyncDelayQueue = require('./async_delay_queue'); -// TODO(murgatroid99): use multiple grpc implementations -var grpc = require('grpc'); -var testProto = grpc.load({ - root: __dirname + '/../../packages/grpc-native-core/deps/grpc', - file: 'src/proto/grpc/testing/test.proto'}).grpc.testing; +var grpc = require('../any_grpc').server; +// TODO(murgatroid99): do this import more cleanly +var protoLoader = require('../../packages/grpc-protobufjs'); +var protoPackage = protoLoader.loadSync( + 'src/proto/grpc/testing/test.proto', + {keepCase: true, + defaults: true, + enums: String, + include: [__dirname + '/../../packages/grpc-native-core/deps/grpc']}); +var testProto = grpc.loadPackageDefinition(protoPackage).grpc.testing; var ECHO_INITIAL_KEY = 'x-grpc-test-echo-initial'; var ECHO_TRAILING_KEY = 'x-grpc-test-echo-trailing-bin'; diff --git a/kokoro.bat b/test/kokoro.bat similarity index 99% rename from kokoro.bat rename to test/kokoro.bat index fc5fd8b2..1f0fabfc 100644 --- a/kokoro.bat +++ b/test/kokoro.bat @@ -15,6 +15,7 @@ @echo "Starting Windows test" cd /d %~dp0 +cd .. git submodule update --init git submodule foreach --recursive git submodule update --init diff --git a/kokoro.sh b/test/kokoro.sh similarity index 89% rename from kokoro.sh rename to test/kokoro.sh index 3e51749c..c4b9373b 100755 --- a/kokoro.sh +++ b/test/kokoro.sh @@ -14,10 +14,12 @@ # limitations under the License. set -e -cd $(dirname $0) +cd $(dirname $0)/.. # Install gRPC and its submodules. git submodule update --init git submodule foreach --recursive git submodule update --init +./packages/grpc-native-core/tools/buildgen/generate_projects.sh + ./run-tests.sh diff --git a/test/kokoro/linux-build.cfg b/test/kokoro/linux-build.cfg new file mode 100644 index 00000000..b5e6d6e6 --- /dev/null +++ b/test/kokoro/linux-build.cfg @@ -0,0 +1,19 @@ +# Copyright 2018 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. + +# Config file for Kokoro (in protobuf text format) + +# Location of the continuous shell script in repository. +build_file: "grpc-node/tools/release/kokoro.sh" +timeout_mins: 60 diff --git a/test/kokoro/linux.cfg b/test/kokoro/linux.cfg index c6b2c88f..f40e6db4 100644 --- a/test/kokoro/linux.cfg +++ b/test/kokoro/linux.cfg @@ -15,7 +15,7 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/kokoro.sh" +build_file: "grpc-node/test/kokoro.sh" timeout_mins: 60 action { define_artifacts { diff --git a/test/kokoro/macos-build.cfg b/test/kokoro/macos-build.cfg new file mode 100644 index 00000000..b5e6d6e6 --- /dev/null +++ b/test/kokoro/macos-build.cfg @@ -0,0 +1,19 @@ +# Copyright 2018 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. + +# Config file for Kokoro (in protobuf text format) + +# Location of the continuous shell script in repository. +build_file: "grpc-node/tools/release/kokoro.sh" +timeout_mins: 60 diff --git a/test/kokoro/macos.cfg b/test/kokoro/macos.cfg index c6b2c88f..f40e6db4 100644 --- a/test/kokoro/macos.cfg +++ b/test/kokoro/macos.cfg @@ -15,7 +15,7 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/kokoro.sh" +build_file: "grpc-node/test/kokoro.sh" timeout_mins: 60 action { define_artifacts { diff --git a/test/kokoro/windows-build.cfg b/test/kokoro/windows-build.cfg new file mode 100644 index 00000000..1885ef39 --- /dev/null +++ b/test/kokoro/windows-build.cfg @@ -0,0 +1,19 @@ +# Copyright 2018 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. + +# Config file for Kokoro (in protobuf text format) + +# Location of the continuous shell script in repository. +build_file: "grpc-node/tools/release/kokoro.bat" +timeout_mins: 60 diff --git a/test/kokoro/windows.cfg b/test/kokoro/windows.cfg index e4a4524b..2b9d0906 100644 --- a/test/kokoro/windows.cfg +++ b/test/kokoro/windows.cfg @@ -15,5 +15,5 @@ # Config file for Kokoro (in protobuf text format) # Location of the continuous shell script in repository. -build_file: "grpc-node/kokoro.bat" +build_file: "grpc-node/test/kokoro.bat" timeout_mins: 60 diff --git a/test/package.json b/test/package.json new file mode 100644 index 00000000..306e3f73 --- /dev/null +++ b/test/package.json @@ -0,0 +1,20 @@ +{ + "name": "grpc-node-test", + "version": "0.1.0", + "description": "Dummy package for the grpc-node repository tests", + "private": true, + "keywords": [], + "author": { + "name": "Google Inc." + }, + "license": "Apache-2.0", + "contributors": [ + { + "name": "Google Inc." + } + ], + "dependencies": { + "google-auth-library": "^0.9.2", + "lodash": "^4.17.4" + } +} diff --git a/tools/release/kokoro.bat b/tools/release/kokoro.bat new file mode 100644 index 00000000..e86e01bd --- /dev/null +++ b/tools/release/kokoro.bat @@ -0,0 +1,28 @@ +@rem Copyright 2018 gRPC authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. + +@echo "Starting Windows build" + +cd /d %~dp0 +cd ..\.. + +git submodule update --init +git submodule foreach --recursive git submodule update --init + +set ARTIFACTS_OUT=artifacts +cd packages\grpc-native-core +call tools\run_tests\artifacts\build_artifact_node.bat +cd ..\.. + +move packages\grpc-native-core\artifacts . diff --git a/tools/release/kokoro.sh b/tools/release/kokoro.sh new file mode 100755 index 00000000..e5b578f7 --- /dev/null +++ b/tools/release/kokoro.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Copyright 2018 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. + +set -ex +cd $(dirname $0)/../.. +base_dir=$(pwd) + +# Install gRPC and its submodules. +git submodule update --init +git submodule foreach --recursive git submodule update --init + +pip install mako +./packages/grpc-native-core/tools/buildgen/generate_projects.sh + +OS=`uname` + +case $OS in +Linux) + sudo apt-get update + sudo apt-get install -y linux-libc-dev:i386 g++-aarch64-linux-gnu g++-arm-linux-gnueabihf + ./packages/grpc-native-core/tools/run_tests/artifacts/build_all_linux_artifacts.sh + mv packages/grpc-native-core/artifacts . + ;; +Darwin) + JOBS=8 ARTIFACTS_OUT=$base_dir/artifacts ./packages/grpc-native-core/tools/run_tests/artifacts/build_artifact_node.sh + ;; +esac diff --git a/tools/release/kokoro/linux.cfg b/tools/release/kokoro/linux.cfg new file mode 100644 index 00000000..b3348f8d --- /dev/null +++ b/tools/release/kokoro/linux.cfg @@ -0,0 +1,25 @@ +# Copyright 2018 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. + +# Config file for Kokoro (in protobuf text format) + +# Location of the continuous shell script in repository. +build_file: "grpc-node/tools/release/kokoro.sh" +timeout_mins: 180 +action { + define_artifacts { + regex: "github/grpc-node/artifacts/**", + strip_prefix: "github/grpc-node/artifacts" + } +} diff --git a/tools/release/kokoro/macos.cfg b/tools/release/kokoro/macos.cfg new file mode 100644 index 00000000..73a539fa --- /dev/null +++ b/tools/release/kokoro/macos.cfg @@ -0,0 +1,25 @@ +# Copyright 2018 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. + +# Config file for Kokoro (in protobuf text format) + +# Location of the continuous shell script in repository. +build_file: "grpc-node/tools/release/kokoro.sh" +timeout_mins: 120 +action { + define_artifacts { + regex: "github/grpc-node/artifacts/**", + strip_prefix: "github/grpc-node/artifacts" + } +} diff --git a/tools/release/kokoro/windows.cfg b/tools/release/kokoro/windows.cfg new file mode 100644 index 00000000..1ba6123f --- /dev/null +++ b/tools/release/kokoro/windows.cfg @@ -0,0 +1,25 @@ +# Copyright 2018 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. + +# Config file for Kokoro (in protobuf text format) + +# Location of the continuous shell script in repository. +build_file: "grpc-node/tools/release/kokoro.bat" +timeout_mins: 120 +action { + define_artifacts { + regex: "github/grpc-node/artifacts/**", + strip_prefix: "github/grpc-node/artifacts" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..dbc5c898 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "lib": [ + "es2015" + ] + } +} diff --git a/util.js b/util.js new file mode 100644 index 00000000..d9193347 --- /dev/null +++ b/util.js @@ -0,0 +1,23 @@ +const path = require('path'); +const del = require('del'); +const fs = require('fs'); +const makeDir = require('make-dir'); + +// synchronously link a module +const linkSync = (base, from, to) => { + from = path.resolve(base, from); + to = path.resolve(base, to); + try { + fs.lstatSync(from); + console.log('link: deleting', from); + del.sync(from); + } catch (e) { + makeDir.sync(path.dirname(from)); + } + console.log('link: linking', from, '->', to); + fs.symlinkSync(to, from, 'junction'); +}; + +module.exports = { + linkSync +}; \ No newline at end of file