diff --git a/index.js b/index.js index efcea2e..6ac4ab2 100755 --- a/index.js +++ b/index.js @@ -29,7 +29,6 @@ var TChannel = require('tchannel'); var TChannelAsThrift = require('tchannel/as/thrift'); var TChannelAsJSON = require('tchannel/as/json'); var minimist = require('minimist'); -var myLocalIp = require('my-local-ip'); var DebugLogtron = require('debug-logtron'); var fmt = require('util').format; @@ -37,7 +36,6 @@ var console = require('console'); var process = require('process'); var fs = require('fs'); var path = require('path'); -var url = require('url'); var assert = require('assert'); var safeJsonParse = require('safe-json-parse/tuple'); @@ -51,12 +49,13 @@ var packageJson = require('./package.json'); module.exports = main; var minimistArgs = { - boolean: ['raw', 'strict'], + boolean: ['raw', 'json', 'strict'], alias: { h: 'help', p: 'peer', H: 'hostlist', t: 'thrift', + j: 'json', 2: ['arg2', 'head'], 3: ['arg3', 'body'] }, @@ -94,16 +93,21 @@ main.exec = function execMain(str, delegate) { function help() { var helpMessage = [ 'tcurl [-H | -p host:port] [options]', - ' ', + '', ' Version: ' + packageJson.version, + '', ' Options: ', // TODO @file; @- stdin. - ' -2 [data] send an arg2 blob', - ' -3 [data] send an arg3 blob', + ' --head (-2) [data] JSON or raw', + ' --body (-3) [data] JSON or raw', + ' (JSON promoted to Thrift via IDL when applicable)', ' --shardKey send ringpop shardKey transport header', ' --depth=n configure inspect printing depth', - ' -t [dir] directory containing Thrift files', + ' --thrift (-t) [dir] directory containing Thrift files', + ' (obtained from Meta::thriftIDL endpoint if omitted)', ' --no-strict parse Thrift loosely', + ' --json (-j) Use JSON argument scheme', + ' (default unless endpoint has ::)', ' --http method', ' --raw encode arg2 & arg3 raw', ' --health', @@ -118,32 +122,36 @@ function parseArgs(argv) { var endpoint = argv._[1]; var health = argv.health; - var uri = argv.hostlist ? - JSON.parse(fs.readFileSync(argv.hostlist))[0] : argv.peer; + var peers = argv.hostlist ? + JSON.parse(fs.readFileSync(argv.hostlist)) : [argv.peer]; - var parsedUri = url.parse('tchannel://' + uri); - - if (parsedUri.hostname === 'localhost') { - parsedUri.hostname = myLocalIp(); - } - - assert(parsedUri.hostname, 'host required'); - assert(parsedUri.port, 'port required'); assert(health || endpoint, 'endpoint required'); assert(service, 'service required'); + var argScheme; + if (argv.raw) { + argScheme = 'raw'; + } else if (argv.http) { + argScheme = 'http'; + } else if (argv.json) { + argScheme = 'json'; + } else if (argv.thrift || health || endpoint.indexOf('::') >= 0) { + argScheme = 'thrift'; + } else { + argScheme = 'json'; + } + return { head: argv.head, body: argv.body, shardKey: argv.shardKey, service: service, endpoint: endpoint, - hostname: parsedUri.hostname, - port: parsedUri.port, + peers: peers, + argScheme: argScheme, thrift: argv.thrift, strict: argv.strict, http: argv.http, - json: argv.json, raw: argv.raw, timeout: argv.timeout, depth: argv.depth, @@ -182,6 +190,50 @@ TCurl.prototype.parseJsonArgs = function parseJsonArgs(opts, delegate) { return null; }; +TCurl.prototype.getThriftSource = function getThriftSource(opts, channel, delegate, callback) { + var self = this; + if (opts.thrift) { + var source = self.readThrift(opts, delegate); + if (source === null) { + return callback(new Error('Unabled to find thrift source')); + } else { + return callback(null, source); + } + } else { + return self.requestThriftSource(opts, channel, delegate, callback); + } +}; + +TCurl.prototype.requestThriftSource = function requestThriftSource(opts, channel, delegate, callback) { + var source = fs.readFileSync(path.join(__dirname, 'meta.thrift'), 'ascii'); + var sender = new TChannelAsThrift({source: source}); + + var request = channel.request({ + timeout: opts.timeout || 100, + hasNoParent: true, + serviceName: opts.service, + headers: {} + }); + + sender.send(request, 'Meta::thriftIDL', null, null, onResponse); + + function onResponse(err, res) { + if (err) { + delegate.error('Can\'t infer Thrift IDL from Meta::thriftIDL endpoint'); + delegate.error(err); + delegate.error('Consider passing --thrift [dir/file] or --json'); + return callback(err); + } else if (!res.ok) { + delegate.error('Can\'t infer Thrift IDL from Meta::thriftIDL endpoint'); + delegate.error('Service returned unexpected Thrift exception'); + delegate.error(res.body); + delegate.error('Consider passing --thrift [dir/file] or --json'); + return callback(new Error('Unexpected Thrift exception')); + } + return callback(null, res.body); + } +}; + TCurl.prototype.readThrift = function readThrift(opts, delegate) { var self = this; try { @@ -233,7 +285,7 @@ TCurl.prototype.request = function tcurlRequest(opts, delegate) { var subChan = client.makeSubChannel({ serviceName: opts.service, - peers: [opts.hostname + ':' + opts.port], + peers: opts.peers, requestDefaults: { serviceName: opts.service, headers: { @@ -242,9 +294,10 @@ TCurl.prototype.request = function tcurlRequest(opts, delegate) { } }); - client.waitForIdentified({ - host: opts.hostname + ':' + opts.port - }, onIdentified); + var peer = subChan.peers.choosePeer(); + assert(peer, 'peer required'); + // TODO: the host option should be called peer, hostPort, or address + client.waitForIdentified({host: peer.hostPort}, onIdentified); function onIdentified(err) { if (err) { @@ -261,15 +314,15 @@ TCurl.prototype.request = function tcurlRequest(opts, delegate) { headers: headers }); - if (opts.thrift) { + if (opts.argScheme === 'thrift') { self.asThrift(opts, request, delegate, done); - } else if (opts.http) { + } else if (opts.argScheme === 'http') { self.asHTTP(opts, client, subChan, delegate, done); - } else if (opts.raw) { - self.asRaw(opts, request, delegate, done); - } else { + } else if (opts.argScheme === 'json') { self.asJSON(opts, request, delegate, done); // TODO fix argument order for each of these + } else { + self.asRaw(opts, request, delegate, done); } } @@ -281,44 +334,46 @@ TCurl.prototype.request = function tcurlRequest(opts, delegate) { TCurl.prototype.asThrift = function asThrift(opts, request, delegate, done) { var self = this; - var source = self.readThrift(opts, delegate); - - if (source === null) { - done(); - return delegate.exit(); - } - - var sender; - try { - sender = new TChannelAsThrift({source: source, strict: opts.strict}); - } catch (err) { - delegate.error('Error parsing Thrift IDL'); - delegate.error(err); - delegate.error('Consider using --no-strict to bypass mandatory optional/required fields'); - done(); - return delegate.exit(); - } + self.getThriftSource(opts, request.channel, delegate, onThriftSource); - // The following is a hack to produce a nice error message when - // the endpoint does not exist. It is a temporary solution based - // on the thriftify interface. How the existence of this endpoint - // is checked and this error thrown will change when we move to - // the thriftrw rewrite. - try { - sender.send(request, opts.endpoint, opts.head, - opts.body, onResponse); - } catch (err) { - // TODO untangle this mess - if (err.message === fmt('type %s_args not found', opts.endpoint)) { - delegate.error(fmt('%s endpoint does not exist', opts.endpoint)); + function onThriftSource(err, source) { + if (err) { done(); return delegate.exit(); - } else { - delegate.error('Error response received for the as-thrift request.'); + } + + var sender; + try { + sender = new TChannelAsThrift({source: source, strict: opts.strict}); + } catch (err) { + delegate.error('Error parsing Thrift IDL'); delegate.error(err); + delegate.error('Consider using --no-strict to bypass mandatory optional/required fields'); done(); return delegate.exit(); } + + // The following is a hack to produce a nice error message when + // the endpoint does not exist. It is a temporary solution based + // on the thriftify interface. How the existence of this endpoint + // is checked and this error thrown will change when we move to + // the thriftrw rewrite. + try { + sender.send(request, opts.endpoint, opts.head, + opts.body, onResponse); + } catch (err) { + // TODO untangle this mess + if (err.message === fmt('type %s_args not found', opts.endpoint)) { + delegate.error(fmt('%s endpoint does not exist', opts.endpoint)); + done(); + return delegate.exit(); + } else { + delegate.error('Error response received for the as-thrift request.'); + delegate.error(err); + done(); + return delegate.exit(); + } + } } function onResponse(err, res, arg2, arg3) { diff --git a/package.json b/package.json index e575839..80ca880 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,10 @@ "dependencies": { "debug-logtron": "^3.2.0", "minimist": "^1.1.1", - "my-local-ip": "^1.0.0", "process": "^0.11.0", "readable-stream": "^1.0.33", "safe-json-parse": "^4.0.0", - "tchannel": "2.10.1" + "tchannel": "3.3.0" }, "devDependencies": { "coveralls": "^2.10.0", diff --git a/test/as-thrift.js b/test/as-thrift.js index c5c8b9d..bdf608f 100644 --- a/test/as-thrift.js +++ b/test/as-thrift.js @@ -101,6 +101,56 @@ test('getting an ok response', function t(assert) { }); +test.only('getting an ok response using remote IDL', function t(assert) { + var serviceName = 'meta'; + var server = new TChannel({ + serviceName: serviceName + }); + + var hostname = '127.0.0.1'; + var port = 4040; + + // Register Meta::health and Meta::thriftIDL endpoints + /* eslint no-unused-vars: [0] */ + var tchannelAsThrift = new TChannelAsThrift({ + source: meta, + isHealthy: isHealthy, + channel: server + }); + + server.listen(port, hostname, onListening); + function onListening() { + var cmd = [ + '-p', hostname + ':' + port, + serviceName, + 'Meta::health', + '--body', JSON.stringify({}) + ]; + + tcurl.exec(cmd, { + error: function error(err) { + assert.ifError(err); + }, + response: function response(res) { + assert.deepEqual(res.body, { + ok: true, + message: null + }, 'caller receives thrift body from handler'); + }, + exit: function exit() { + server.close(); + assert.end(); + } + }); + } + + function isHealthy() { + return { + ok: true + }; + } +}); + test('hitting non-existent endpoint', function t(assert) { var serviceName = 'meta';