vrpc 3.0.1 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vrpc",
3
- "version": "3.0.1",
3
+ "version": "3.0.4",
4
4
  "description": "Variadic Remote Procedure Calls",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -390,7 +390,7 @@ class VrpcAdapter {
390
390
  }
391
391
 
392
392
  static _call (json) {
393
- const before = Object.keys(VrpcAdapter._listeners).length
393
+ this._mustTrackClient = false // reset client tracking flag
394
394
  switch (json.f) {
395
395
  case '__createIsolated__':
396
396
  VrpcAdapter._handleCreateIsolated(json)
@@ -408,10 +408,9 @@ class VrpcAdapter {
408
408
  // this call may have the side-effect of taking a listener on board
409
409
  VrpcAdapter._handleCall(json)
410
410
  }
411
- const after = Object.keys(VrpcAdapter._listeners).length
412
411
  // if we took a listener on board we must tell our caller such that
413
412
  // things can be cleaned up later.
414
- return after > before
413
+ return this._mustTrackClient
415
414
  }
416
415
 
417
416
  static _handleCreateIsolated (json) {
@@ -593,13 +592,7 @@ class VrpcAdapter {
593
592
  if (VrpcAdapter._isFunction(entry.removeAllListeners)) {
594
593
  entry.removeAllListeners()
595
594
  }
596
- for (const [key, { clients, instance }] of Object.entries(
597
- VrpcAdapter._listeners
598
- )) {
599
- if (instance === instanceId) {
600
- VrpcAdapter._listeners[key] = { clients }
601
- }
602
- }
595
+ delete VrpcAdapter._listeners[instanceId]
603
596
  VrpcAdapter._instances.delete(instanceId)
604
597
  return true
605
598
  }
@@ -607,16 +600,18 @@ class VrpcAdapter {
607
600
  }
608
601
 
609
602
  static _unregisterClient (clientId) {
610
- for (const [key, value] of Object.entries(VrpcAdapter._listeners)) {
611
- const { instance, event, listener, clients } = value
612
- clients.delete(clientId)
613
- if (clients.size === 0) {
614
- if (event) {
615
- VrpcAdapter.getInstance(instance).removeListener(event, listener)
603
+ Object.entries(VrpcAdapter._listeners).forEach(([ik, iv]) => {
604
+ Object.entries(iv).forEach(([ek, ev]) => {
605
+ const { clients, listener, event } = ev
606
+ if (clients.has(clientId)) {
607
+ clients.delete(clientId)
608
+ if (clients.size === 0) {
609
+ VrpcAdapter.getInstance(ik).removeListener(event, listener)
610
+ delete VrpcAdapter._listeners[ik][ek]
611
+ }
616
612
  }
617
- delete VrpcAdapter._listeners[key]
618
- }
619
- }
613
+ })
614
+ })
620
615
  }
621
616
 
622
617
  static _validate (schema, params) {
@@ -628,102 +623,162 @@ class VrpcAdapter {
628
623
  }
629
624
 
630
625
  static _unwrapArguments (json, instanceId) {
631
- const sender = json.s
632
- let context = json.c
633
- let func = json.f
634
- let args = json.a
635
- // things are different if we came from callAll (instanceId !== undefined)
636
- if (instanceId) {
637
- context = instanceId
638
- func = args[0]
639
- args = args.slice(1)
640
- }
641
- const unwrapped = instanceId ? [func] : []
626
+ const clientId = json.s
627
+ // if we came from a "callAll" we will have an instanceId defined
628
+ // things are different for callAll as we have to dispatch the instanceIds
629
+ const isCallAll = instanceId !== undefined
630
+ const context = isCallAll ? instanceId : json.c
631
+ const args = isCallAll ? json.a.slice(1) : json.a
632
+ const func = isCallAll ? json.a[0] : json.f
633
+ const unwrapped = isCallAll ? [func] : []
642
634
  for (const arg of args) {
643
- // find those args that actually need to be functions
644
- if (typeof arg === 'string' && arg.slice(0, 5) === '__f__') {
645
- const callback = this._generateCallback(arg, sender, instanceId)
646
- unwrapped.push(callback)
647
- } else if (typeof arg === 'string' && arg.slice(0, 5) === '__e__') {
648
- const listener = this._generateListener(arg, sender, instanceId)
649
- unwrapped.push(listener)
650
- // start tracking clients that are registering event listeners
651
- if (!VrpcAdapter._listeners[arg]) {
652
- VrpcAdapter._listeners[arg] = {
653
- listener,
654
- clients: new Set([sender]),
655
- instances: new Set(),
656
- event: null
635
+ if (typeof arg === 'string') {
636
+ if (arg.startsWith('__f__')) {
637
+ // function callback
638
+ unwrapped.push(
639
+ this._generateCallback({
640
+ clientId,
641
+ isCallAll,
642
+ instanceId: context,
643
+ eventId: arg
644
+ })
645
+ )
646
+ } else if (arg.startsWith('__e__')) {
647
+ // event registration
648
+ if (func === 'on' || func === 'addListener') {
649
+ this._mustTrackClient = true
650
+ const listener = VrpcAdapter._registerListener({
651
+ clientId,
652
+ isCallAll,
653
+ instanceId: context,
654
+ eventId: arg,
655
+ event: args[0]
656
+ })
657
+ if (
658
+ VrpcAdapter.getInstance(context)
659
+ .listeners(arg)
660
+ .includes(listener)
661
+ ) {
662
+ return null // skip call as listener is already registered
663
+ }
664
+ unwrapped.push(listener)
665
+ // event un-registration
666
+ } else if (func === 'off' || func === 'removeListener') {
667
+ const listener = VrpcAdapter._unregisterListener({
668
+ clientId,
669
+ instanceId: context,
670
+ eventId: arg
671
+ })
672
+ if (!listener) {
673
+ return null // skip call as others are still interested
674
+ }
675
+ unwrapped.push(listener)
676
+ } else {
677
+ this._mustTrackClient = true
678
+ const listener = VrpcAdapter._registerListener({
679
+ clientId,
680
+ isCallAll,
681
+ instanceId: context,
682
+ eventId: arg,
683
+ event: null
684
+ })
685
+ unwrapped.push(listener)
657
686
  }
658
687
  } else {
659
- VrpcAdapter._listeners[arg].clients.add(sender)
660
- }
661
- // see whether the requested listener is an EventEmitter registration
662
- if (func === 'on' || func === 'addListener') {
663
- // if this event is already registered on the given instance
664
- // avoid a second registration
665
- if (
666
- VrpcAdapter._listeners[arg].event &&
667
- VrpcAdapter._listeners[arg].instances.has(context)
668
- ) {
669
- return null // means that this call won't be forwarded
670
- }
671
- VrpcAdapter._listeners[arg].event = args[0]
672
- VrpcAdapter._listeners[arg].instances.add(context)
673
- }
674
- if (func === 'off' || func === 'removeListener') {
675
- const {
676
- clients,
677
- instances,
678
- listener,
679
- event
680
- } = VrpcAdapter._listeners[arg]
681
- clients.delete(sender)
682
- instances.delete(context)
683
- if (clients.size === 0) {
684
- delete VrpcAdapter._listeners[arg]
685
- VrpcAdapter.getInstance(context).off(event, listener)
686
- }
687
- return null // means that this call won't be forwarded
688
+ unwrapped.push(arg)
688
689
  }
689
690
  } else {
690
- // default behavior is to not touch
691
+ // regular argument
691
692
  unwrapped.push(arg)
692
693
  }
693
694
  }
694
695
  return unwrapped
695
696
  }
696
697
 
697
- static _removeAllListeners (event, sender, context) {
698
- for (const [key, value] of Object.entries(VrpcAdapter._listeners)) {
699
- if (value.event === event) {
700
- value.clients.delete(sender)
701
- if (value.clients.size === 0) {
702
- delete VrpcAdapter._listeners[key]
703
- VrpcAdapter.getInstance(context).off(event, value.listener)
698
+ static _generateCallback ({ eventId, clientId, instanceId, isCallAll }) {
699
+ return (...innerArgs) => {
700
+ // "callAll" event notifications expect the instanceId as first argument
701
+ const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
702
+ VrpcAdapter._callback({ a, s: clientId, i: eventId })
703
+ }
704
+ }
705
+
706
+ static _registerListener ({
707
+ eventId,
708
+ clientId,
709
+ instanceId,
710
+ event,
711
+ isCallAll
712
+ }) {
713
+ if (!VrpcAdapter._listeners[instanceId]) {
714
+ VrpcAdapter._listeners[instanceId] = {
715
+ [eventId]: {
716
+ event,
717
+ clients: new Set([clientId]),
718
+ listener: VrpcAdapter._generateListener({
719
+ eventId,
720
+ instanceId,
721
+ isCallAll
722
+ })
704
723
  }
705
724
  }
725
+ } else if (!VrpcAdapter._listeners[instanceId][eventId]) {
726
+ VrpcAdapter._listeners[instanceId][eventId] = {
727
+ event,
728
+ clients: new Set([clientId]),
729
+ listener: VrpcAdapter._generateListener({
730
+ eventId,
731
+ instanceId,
732
+ isCallAll
733
+ })
734
+ }
735
+ } else {
736
+ VrpcAdapter._listeners[instanceId][eventId].clients.add(clientId)
706
737
  }
738
+ return VrpcAdapter._listeners[instanceId][eventId].listener
707
739
  }
708
740
 
709
- static _generateCallback (id, sender, instanceId) {
741
+ static _generateListener ({ eventId, instanceId, isCallAll }) {
710
742
  return (...innerArgs) => {
711
- // callAll expects the instanceId as first argument
712
- const a = instanceId ? [instanceId, ...innerArgs] : [...innerArgs]
713
- VrpcAdapter._callback({ a, s: sender, i: id })
743
+ const a = isCallAll ? [instanceId, ...innerArgs] : [...innerArgs]
744
+ VrpcAdapter._callback({ a, i: eventId })
714
745
  }
715
746
  }
716
747
 
717
- static _generateListener (id, sender, instanceId) {
718
- return (...innerArgs) => {
719
- // do not even emit for already offline clients
720
- if (!VrpcAdapter._listeners[id]) return
721
- // callAll expects the instanceId as first argument
722
- const a = instanceId ? [instanceId, ...innerArgs] : [...innerArgs]
723
- VrpcAdapter._callback({ a, s: sender, i: id })
748
+ static _unregisterListener ({ eventId, clientId, instanceId }) {
749
+ if (
750
+ VrpcAdapter._listeners[instanceId] &&
751
+ VrpcAdapter._listeners[instanceId][eventId]
752
+ ) {
753
+ const { listener, clients } = VrpcAdapter._listeners[instanceId][eventId]
754
+ clients.delete(clientId)
755
+ if (clients.size === 0) {
756
+ delete VrpcAdapter._listeners[instanceId][eventId]
757
+ if (Object.keys(VrpcAdapter._listeners[instanceId]).length === 0) {
758
+ delete VrpcAdapter._listeners[instanceId]
759
+ }
760
+ return listener
761
+ }
724
762
  }
725
763
  }
726
764
 
765
+ static _removeAllListeners (event, clientId, instanceId) {
766
+ const eventIds = VrpcAdapter._listeners[instanceId]
767
+ if (!eventIds) return
768
+ Object.entries(eventIds).forEach(([ek, ev]) => {
769
+ if (ev.event === event && ev.clients.has(clientId)) {
770
+ ev.clients.delete(clientId)
771
+ if (ev.clients.size === 0) {
772
+ VrpcAdapter.getInstance(instanceId).removeListener(
773
+ ev.event,
774
+ ev.listener
775
+ )
776
+ delete VrpcAdapter._listeners[instanceId][ek]
777
+ }
778
+ }
779
+ })
780
+ }
781
+
727
782
  static _handlePromise (json, promise) {
728
783
  try {
729
784
  const cid = VrpcAdapter._correlationId++ % Number.MAX_SAFE_INTEGER
package/vrpc/VrpcAgent.js CHANGED
@@ -48,7 +48,7 @@ class VrpcAgent extends EventEmitter {
48
48
  })
49
49
  parser.add_argument('-d', '--domain', {
50
50
  help: 'Domain name',
51
- default: domain || 'public.vrpc'
51
+ default: domain || 'vrpc'
52
52
  })
53
53
  parser.add_argument('-t', '--token', {
54
54
  help: 'access token',
@@ -92,10 +92,10 @@ class VrpcAgent extends EventEmitter {
92
92
  *
93
93
  * @constructor
94
94
  * @param {Object} obj
95
- * @param {String} [obj.username] MQTT username (if no token is used)
95
+ * @param {String} [obj.username] MQTT username
96
96
  * @param {String} [obj.password] MQTT password (if no token is provided)
97
- * @param {String} [obj.token] Access token as generated by: https://app.vrpc.io (only optional when using default domain and broker)
98
- * @param {String} [obj.domain='public.vrpc'] The domain under which the agent-provided code is reachable
97
+ * @param {String} [obj.token] Access token
98
+ * @param {String} [obj.domain='vrpc'] The domain under which the agent-provided code is reachable
99
99
  * @param {String} [obj.agent='<user>-<pathId>@<hostname>-<platform>-js'] This agent's name
100
100
  * @param {String} [obj.broker='mqtts://vrpc.io:8883'] Broker url in form: `<scheme>://<host>:<port>`
101
101
  * @param {Object} [obj.log=console] Log object (must support debug, info, warn, and error level)
@@ -104,7 +104,7 @@ class VrpcAgent extends EventEmitter {
104
104
  *
105
105
  * @example
106
106
  * const agent = new Agent({
107
- * domain: 'public.vrpc'
107
+ * domain: 'vrpc'
108
108
  * agent: 'myAgent'
109
109
  * })
110
110
  */
@@ -112,7 +112,7 @@ class VrpcAgent extends EventEmitter {
112
112
  username,
113
113
  password,
114
114
  token,
115
- domain = 'public.vrpc',
115
+ domain = 'vrpc',
116
116
  agent = VrpcAgent._generateAgentName(),
117
117
  broker = 'mqtts://vrpc.io:8883',
118
118
  log = 'console',
@@ -130,7 +130,6 @@ class VrpcAgent extends EventEmitter {
130
130
  this._broker = broker
131
131
  this._qos = bestEffort ? 0 : 1
132
132
  this._version = version
133
- this._isReconnect = false
134
133
  if (log === 'console') {
135
134
  this._log = console
136
135
  this._log.debug = () => {}
@@ -169,12 +168,15 @@ class VrpcAgent extends EventEmitter {
169
168
  .createHash('md5')
170
169
  .update(this._domain + this._agent)
171
170
  .digest('hex')
172
- .substr(0, 20)
171
+ .substring(0, 20)
173
172
  let username = this._username
174
173
  let password = this._password
175
174
  if (this._token) {
176
- username = '__token__'
175
+ username = `${this._domain}:${this._agent}`
177
176
  password = this._token
177
+ } else if (!this._password) {
178
+ username = `${this._domain}:${this._agent}`
179
+ password = this._generateToken()
178
180
  }
179
181
  this._options = {
180
182
  username,
@@ -194,12 +196,7 @@ class VrpcAgent extends EventEmitter {
194
196
  this._log.info(`Agent : ${this._agent}`)
195
197
  this._log.info(`Broker : ${this._broker}`)
196
198
  this._log.info('Connecting to MQTT server...')
197
- const wasEnded = await this._clearPersistedSession()
198
- if (wasEnded) return
199
- this._client = mqtt.connect(this._broker, {
200
- ...this._options,
201
- clean: false
202
- })
199
+ this._client = mqtt.connect(this._broker, this._options)
203
200
  this._client.on('connect', this._handleConnect.bind(this))
204
201
  this._client.on('reconnect', this._handleReconnect.bind(this))
205
202
  this._client.on('error', this._handleError.bind(this))
@@ -238,7 +235,6 @@ class VrpcAgent extends EventEmitter {
238
235
  }
239
236
  }
240
237
  await new Promise(resolve => this._client.end(false, {}, resolve))
241
- await this._clearPersistedSession()
242
238
  } catch (err) {
243
239
  this._log.error(err, `Problem during disconnecting agent: ${err.message}`)
244
240
  }
@@ -250,10 +246,31 @@ class VrpcAgent extends EventEmitter {
250
246
  .createHash('md5')
251
247
  .update(path.resolve())
252
248
  .digest('hex')
253
- .substr(0, 4)
249
+ .substring(0, 4)
254
250
  return `${username}-${pathId}@${os.hostname()}-${os.platform()}-js`
255
251
  }
256
252
 
253
+ _generateToken () {
254
+ const uid =
255
+ this._domain +
256
+ this._agent +
257
+ os.userInfo().username +
258
+ path.resolve() +
259
+ os.arch() +
260
+ os.homedir() +
261
+ os.hostname() +
262
+ os.platform() +
263
+ os.release() +
264
+ os.totalmem() +
265
+ os.type() +
266
+ JSON.stringify(os.networkInterfaces()) +
267
+ JSON.stringify(os.cpus().map(({ model }) => model))
268
+ return crypto
269
+ .createHash('md5')
270
+ .update(uid)
271
+ .digest('hex')
272
+ }
273
+
257
274
  _validateDomain (domain) {
258
275
  if (!domain) throw new Error('The domain must be specified')
259
276
  if (domain.match(/[+/#*]/)) {
@@ -281,27 +298,6 @@ class VrpcAgent extends EventEmitter {
281
298
  })
282
299
  }
283
300
 
284
- /**
285
- * @return {Boolean} true if `agent.end()` was explicitly called
286
- */
287
- async _clearPersistedSession () {
288
- // Clear potentially existing persisted sessions
289
- const client = mqtt.connect(this._broker, { ...this._options, clean: true })
290
- client.on('error', err => this.emit('error', err))
291
- client.on('offline', () => this.emit('offline'))
292
- client.on('reconnect', () => this.emit('reconnect'))
293
- return new Promise(resolve => {
294
- client.on('connect', () => {
295
- client.end(false, {}, () => resolve(false))
296
- })
297
- // Will be triggered if the user called 'agent.end()'
298
- this.once('end', () => {
299
- client.end()
300
- resolve(true)
301
- })
302
- })
303
- }
304
-
305
301
  _mqttPublish (topic, message, options) {
306
302
  this._client.publish(
307
303
  topic,
@@ -371,6 +367,8 @@ class VrpcAgent extends EventEmitter {
371
367
  resolve()
372
368
  } else {
373
369
  this._client.once('connect', resolve)
370
+ // Will be triggered if the user called 'agent.end()'
371
+ this.once('end', resolve)
374
372
  }
375
373
  })
376
374
  }
@@ -409,16 +407,6 @@ class VrpcAgent extends EventEmitter {
409
407
 
410
408
  _handleConnect () {
411
409
  this._log.info('[OK]')
412
- if (this._isReconnect) {
413
- this._publishAgentInfoMessage()
414
- const classes = this._getClasses()
415
- classes.forEach(className => {
416
- this._publishClassInfoMessage(className)
417
- })
418
- this.emit('connect')
419
- this._isReconnect = false
420
- return
421
- }
422
410
  try {
423
411
  const topics = this._generateTopics()
424
412
  if (topics.length > 0) this._mqttSubscribe(topics)
@@ -666,7 +654,6 @@ class VrpcAgent extends EventEmitter {
666
654
  }
667
655
 
668
656
  _handleReconnect () {
669
- this._isReconnect = true
670
657
  this._log.warn(`Reconnecting to ${this._broker}`)
671
658
  this.emit('reconnect')
672
659
  }
@@ -59,9 +59,9 @@ class VrpcClient extends EventEmitter {
59
59
  *
60
60
  * @constructor
61
61
  * @param {Object} options
62
- * @param {String} options.token Access token as generated by: https://app.vrpc.io
63
- * @param {String} options.username MQTT username (if no token is used)
64
- * @param {String} options.password MQTT password (if no token is provided)
62
+ * @param {String} [options.username] MQTT username
63
+ * @param {String} [options.password] MQTT password (if no token is provided)
64
+ * @param {String} [options.token] Access token
65
65
  * @param {String} options.domain Sets the domain
66
66
  * @param {String} [options.agent="*"] Sets default agent
67
67
  * @param {String} [options.broker="mqtts://vrpc.io:8883"] Broker url in form: `<scheme>://<host>:<port>`
@@ -70,14 +70,14 @@ class VrpcClient extends EventEmitter {
70
70
  * @param {Boolean} [options.bestEffort=false] If true, message will be sent with best effort, i.e. no caching if offline
71
71
  * @example
72
72
  * const client = new VrpcClient({
73
- * domain: 'public.vrpc',
73
+ * domain: 'vrpc',
74
74
  * broker: 'mqtt://vrpc.io'
75
75
  * })
76
76
  */
77
77
  constructor ({
78
78
  token,
79
- username,
80
79
  password,
80
+ username,
81
81
  domain,
82
82
  agent = '*',
83
83
  broker = 'mqtts://vrpc.io:8883',
@@ -152,7 +152,7 @@ class VrpcClient extends EventEmitter {
152
152
  let username = this._username
153
153
  let password = this._password
154
154
  if (this._token) {
155
- username = '__token__'
155
+ username = username || this._generateUserName()
156
156
  password = this._token
157
157
  }
158
158
  const options = {
@@ -660,10 +660,13 @@ class VrpcClient extends EventEmitter {
660
660
  return new Promise(resolve => this._client.end(false, {}, resolve))
661
661
  }
662
662
 
663
+ _generateUserName () {
664
+ return `${this._domain}:client@${os.hostname()}-${os.platform()}-js`
665
+ }
666
+
663
667
  _createMqttClientId () {
664
668
  const clientInfo =
665
669
  os.arch() +
666
- JSON.stringify(os.cpus()) +
667
670
  os.homedir() +
668
671
  os.hostname() +
669
672
  JSON.stringify(os.networkInterfaces()) +
@@ -694,6 +697,12 @@ class VrpcClient extends EventEmitter {
694
697
  this._log.warn(
695
698
  `Could not publish MQTT message because: ${err.message}`
696
699
  )
700
+ if (err.message === 'client disconnecting') {
701
+ // Workaround for an already reported bug in MQTT.js (#1284)
702
+ this._log.info('Forcing reconnect now...')
703
+ this._client.disconnecting = false
704
+ this._client.end(true, {}, () => this._client.reconnect())
705
+ }
697
706
  }
698
707
  }
699
708
  )
@@ -708,6 +717,12 @@ class VrpcClient extends EventEmitter {
708
717
  this._log.warn(
709
718
  `Could not subscribe to topic: ${topic} because: ${err.message}`
710
719
  )
720
+ if (err.message === 'client disconnecting') {
721
+ // Workaround for an already reported bug in MQTT.js (#1284)
722
+ this._log.info('Forcing reconnect now...')
723
+ this._client.disconnecting = false
724
+ this._client.end(true, {}, () => this._client.reconnect())
725
+ }
711
726
  } else {
712
727
  if (granted.length === 0) {
713
728
  this._log.debug(`Already subscribed to: ${topic}`)
@@ -933,7 +948,8 @@ class VrpcClient extends EventEmitter {
933
948
  if (typeof ret === 'string' && ret.substring(0, 5) === '__p__') {
934
949
  this._eventEmitter.once(ret, promiseData => {
935
950
  // TODO improve error message here, like above
936
- if (promiseData.e) reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${promiseData.e}`))
951
+ if (promiseData.e)
952
+ reject(new Error(`[vrpc ${agent}-${c}-${f}]: ${promiseData.e}`))
937
953
  else resolve(promiseData.r)
938
954
  })
939
955
  } else {
@@ -1068,7 +1084,9 @@ class VrpcClient extends EventEmitter {
1068
1084
  : `${this._domain}/${agent}/${className}/${event}`
1069
1085
  const id = `__e__${topic}`
1070
1086
  if (this._cachedSubscriptions[topic]) {
1071
- const index = this._cachedSubscriptions[topic].findIndex(x => x.callback === callback)
1087
+ const index = this._cachedSubscriptions[topic].findIndex(
1088
+ x => x.callback === callback
1089
+ )
1072
1090
  if (index === -1) {
1073
1091
  this._log.warn(`Failed removing event listener ${id}`)
1074
1092
  return null
@@ -23,7 +23,7 @@ parser.add_argument(
23
23
  {
24
24
  help: 'The prefix for all vrpc communication',
25
25
  type: 'string',
26
- default: 'public.vrpc'
26
+ default: 'vrpc'
27
27
  }
28
28
  )
29
29
  parser.add_argument(