dot/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/daemon.js

1127 lines
32 KiB
JavaScript
Executable File

#!/usr/bin/env gjs
'use strict';
imports.gi.versions.Gdk = '3.0';
imports.gi.versions.GdkPixbuf = '2.0';
imports.gi.versions.Gio = '2.0';
imports.gi.versions.GIRepository = '2.0';
imports.gi.versions.GLib = '2.0';
imports.gi.versions.GObject = '2.0';
imports.gi.versions.Gtk = '3.0';
imports.gi.versions.Pango = '1.0';
imports.gi.versions.UPowerGlib = '1.0';
const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
// Find the root datadir of the extension
function get_datadir() {
let m = /@(.+):\d+/.exec((new Error()).stack.split('\n')[1]);
return Gio.File.new_for_path(m[1]).get_parent().get_parent().get_path();
}
window.gsconnect = {extdatadir: get_datadir()};
imports.searchPath.unshift(gsconnect.extdatadir);
imports._gsconnect;
// Local Imports
const Core = imports.service.protocol.core;
const Device = imports.service.device;
const ServiceUI = imports.service.ui.service;
const _GITHUB = 'https://github.com/andyholmes/gnome-shell-extension-gsconnect';
const Service = GObject.registerClass({
GTypeName: 'GSConnectService',
Properties: {
'discoverable': GObject.ParamSpec.boolean(
'discoverable',
'Discoverable',
'Whether the service responds to discovery requests',
GObject.ParamFlags.READWRITE,
false
),
'id': GObject.ParamSpec.string(
'id',
'Id',
'The service id',
GObject.ParamFlags.READWRITE,
null
),
'name': GObject.ParamSpec.string(
'name',
'deviceName',
'The name announced to the network',
GObject.ParamFlags.READWRITE,
'GSConnect'
)
}
}, class Service extends Gtk.Application {
_init() {
super._init({
application_id: gsconnect.app_id,
flags: Gio.ApplicationFlags.HANDLES_OPEN
});
GLib.set_prgname('GSConnect');
GLib.set_application_name('GSConnect');
// Track devices with id as key
this._devices = new Map();
// Command-line
this._initOptions();
}
get backends() {
if (this._backends === undefined) {
this._backends = new Map();
}
return this._backends;
}
get components() {
if (this._components === undefined) {
this._components = new Map();
}
return this._components;
}
get devices() {
return Array.from(this._devices.values());
}
get identity() {
if (this._identity === undefined) {
this._identity = new Core.Packet({
id: 0,
type: 'kdeconnect.identity',
body: {
deviceId: this.id,
deviceName: this.name,
deviceType: this._getDeviceType(),
protocolVersion: 7,
incomingCapabilities: [],
outgoingCapabilities: []
}
});
for (let name in imports.service.plugins) {
// Don't report mousepad support in Ubuntu Wayland sessions
if (name === 'mousepad' && !HAVE_REMOTEINPUT) continue;
let meta = imports.service.plugins[name].Metadata;
if (!meta) continue;
meta.incomingCapabilities.map(type => {
this._identity.body.incomingCapabilities.push(type);
});
meta.outgoingCapabilities.map(type => {
this._identity.body.outgoingCapabilities.push(type);
});
}
}
return this._identity;
}
/**
* Helpers
*/
_getDeviceType() {
try {
let type = GLib.file_get_contents('/sys/class/dmi/id/chassis_type')[1];
type = Number(imports.byteArray.toString(type));
if ([8, 9, 10, 14].includes(type)) {
return 'laptop';
}
return 'desktop';
} catch (e) {
return 'desktop';
}
}
/**
* Return a device for @packet, creating it and adding it to the list of
* of known devices if it doesn't exist.
*
* @param {kdeconnect.identity} packet - An identity packet for the device
* @return {Device.Device} - A device object
*/
_ensureDevice(packet) {
let device = this._devices.get(packet.body.deviceId);
if (device === undefined) {
debug(`Adding ${packet.body.deviceName}`);
// TODO: Remove when all clients support bluetooth-like discovery
//
// If this is the third unpaired device to connect, we disable
// discovery to avoid choking on networks with many devices
let unpaired = Array.from(this._devices.values()).filter(dev => {
return !dev.paired;
});
if (unpaired.length === 3 && this.discoverable) {
this.discoverable = false;
let error = new Error();
error.name = 'DiscoveryWarning';
this.notify_error(error);
}
device = new Device.Device(packet);
this._devices.set(device.id, device);
// Notify
this.settings.set_strv(
'devices',
Array.from(this._devices.keys())
);
}
return device;
}
/**
* Permanently remove a device.
*
* Removes the device from the list of known devices, deletes all GSettings
* and files.
*
* @param {String} id - The id of the device to delete
*/
_removeDevice(id) {
// Delete all GSettings
let settings_path = '/org/gnome/shell/extensions/gsconnect/' + id + '/';
GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`);
// Delete the cache
let cache = GLib.build_filenamev([gsconnect.cachedir, id]);
Gio.File.rm_rf(cache);
// Forget the device
this._devices.delete(id);
this.settings.set_strv(
'devices',
Array.from(this._devices.keys())
);
}
/**
* GSettings
*/
_initSettings() {
this.settings = new Gio.Settings({
settings_schema: gsconnect.gschema.lookup(gsconnect.app_id, true)
});
// Bound Properties
this.settings.bind('discoverable', this, 'discoverable', 0);
this.settings.bind('id', this, 'id', 0);
this.settings.bind('name', this, 'name', 0);
// Set the default name to the computer's hostname
if (this.name.length === 0) {
this.settings.set_string('name', GLib.get_host_name());
}
// Keep identity updated and broadcast any name changes
this._nameChangedId = this.settings.connect(
'changed::name',
this._onNameChanged.bind(this)
);
}
_onNameChanged(settings, key) {
this.identity.body.deviceName = this.name;
this._identify();
}
/**
* GActions
*/
_initActions() {
let actions = [
['connect', this._identify.bind(this), 's'],
['device', this._device.bind(this), '(ssbv)'],
['error', this._error.bind(this), 'a{ss}'],
['preferences', this._preferences],
['quit', () => this.quit()],
['refresh', this._identify.bind(this)],
['wiki', this._wiki.bind(this), 's']
];
for (let [name, callback, type] of actions) {
let action = new Gio.SimpleAction({
name: name,
parameter_type: (type) ? new GLib.VariantType(type) : null
});
action.connect('activate', callback);
this.add_action(action);
}
}
/**
* A wrapper for Device GActions. This is used to route device notification
* actions to their device, since GNotifications need an 'app' level action.
*
* @param {Gio.Action} action - ...
* @param {GLib.Variant(av)} parameter - ...
* @param {GLib.Variant(s)} parameter[0] - Device Id or '*' for all
* @param {GLib.Variant(s)} parameter[1] - GAction name
* @param {GLib.Variant(b)} parameter[2] - %false if the parameter is null
* @param {GLib.Variant(v)} parameter[3] - GAction parameter
*/
_device(action, parameter) {
try {
parameter = parameter.unpack();
// Select the appropriate device(s)
let devices;
let id = parameter[0].unpack();
if (id === '*') {
devices = this._devices.values();
} else {
devices = [this._devices.get(id)];
}
// Unpack the action data
let name = parameter[1].unpack();
let target = parameter[2].unpack() ? parameter[3].unpack() : null;
// Activate the action on each available device
for (let device of devices) {
if (device) {
device.activate_action(name, target);
}
}
} catch (e) {
logError(e);
}
}
_error(action, parameter) {
try {
let error = parameter.deepUnpack();
let dialog = new Gtk.MessageDialog({
text: error.message,
secondary_text: error.stack,
buttons: Gtk.ButtonsType.CLOSE,
message_type: Gtk.MessageType.ERROR,
});
dialog.add_button(_('Report'), Gtk.ResponseType.OK);
dialog.set_keep_above(true);
let [message, stack] = dialog.get_message_area().get_children();
message.halign = Gtk.Align.START;
message.selectable = true;
stack.selectable = true;
dialog.connect('response', (dialog, response_id) => {
if (response_id === Gtk.ResponseType.OK) {
let query = encodeURIComponent(dialog.text).replace('%20', '+');
this._github(`issues?q=is%3Aissue+"${query}"`);
} else {
dialog.destroy();
}
});
dialog.show();
} catch (e) {
logError(e);
}
}
_identify(action, parameter) {
try {
// If we're passed a parameter, try and find a backend for it
if (parameter instanceof GLib.Variant) {
let uri = parameter.unpack();
let [scheme, address] = uri.split('://');
let backend = this.backends.get(scheme);
if (backend) {
backend.broadcast(address);
}
// If we're not discoverable, only try to reconnect known devices
} else if (!this.discoverable) {
this._reconnect();
// Otherwise have each backend broadcast to it's network
} else {
for (let backend of this.backends.values()) {
backend.broadcast();
}
}
} catch (e) {
logError(e);
}
}
_preferences() {
let proc = new Gio.Subprocess({
argv: [gsconnect.extdatadir + '/gsconnect-preferences']
});
proc.init(null);
proc.wait_async(null, null);
}
/**
* A GSourceFunc that tries to reconnect to each paired device, while
* pruning unpaired devices that have disconnected.
*/
_reconnect() {
for (let [id, device] of this._devices.entries()) {
switch (true) {
case device.connected:
break;
case device.paired:
device.activate();
break;
default:
this._removeDevice(id);
device.destroy();
}
}
return GLib.SOURCE_CONTINUE;
}
_wiki(action, parameter) {
this._github(`wiki/${parameter.unpack()}`);
}
_github(path = []) {
let uri = [_GITHUB].concat(path.split('/')).join('/');
Gio.AppInfo.launch_default_for_uri_async(uri, null, null, null);
}
/**
* Components
*/
_initComponents() {
for (let name in imports.service.components) {
try {
let module = imports.service.components[name];
if (module.hasOwnProperty('Component')) {
let component = new module.Component();
this.components.set(name, component);
}
} catch (e) {
logError(e, `'${name}' Component`);
}
}
}
/**
* Backends
*
* These are the implementations of Core.ChannelService that emit
* Core.ChannelService::channel with objects implementing Core.Channel.
*/
_onChannel(backend, channel) {
try {
let device = this._devices.get(channel.identity.body.deviceId);
switch (true) {
// Proceed if this is an existing device...
case (device !== undefined):
break;
// Or the service is discoverable...
case this.discoverable:
device = this._ensureDevice(channel.identity);
break;
// ...otherwise bail
default:
debug(`${channel.identity.body.deviceName}: not allowed`);
return false;
}
channel.attach(device);
return true;
} catch (e) {
logError(e, backend.name);
return false;
}
}
_initBackends() {
let backends = [
'lan'
];
for (let name of backends) {
try {
// Try to create the backend and track it if successful
let module = imports.service.protocol[name];
let backend = new module.ChannelService();
this.backends.set(name, backend);
// Connect to the backend
backend.__channelId = backend.connect(
'channel',
this._onChannel.bind(this)
);
// Now try to start the backend, allowing us to retry if we fail
backend.start();
} catch (e) {
this.notify_error(e);
}
}
}
/**
* Remove a local libnotify or Gtk notification.
*
* @param {String|Number} id - Gtk (string) or libnotify id (uint32)
* @param {String|null} application - Application Id if Gtk or null
*/
remove_notification(id, application = null) {
let name, path, method, variant;
if (application !== null) {
name = 'org.gtk.Notifications';
method = 'RemoveNotification';
path = '/org/gtk/Notifications';
variant = new GLib.Variant('(ss)', [application, id]);
} else {
name = 'org.freedesktop.Notifications';
path = '/org/freedesktop/Notifications';
method = 'CloseNotification';
variant = new GLib.Variant('(u)', [id]);
}
Gio.DBus.session.call(
name, path, name, method, variant, null,
Gio.DBusCallFlags.NONE, -1, null,
(connection, res) => {
try {
connection.call_finish(res);
} catch (e) {
logError(e);
}
}
);
}
/**
* Report a service-level error
*
* @param {object} error - An Error or object with name, message and stack
*/
notify_error(error) {
try {
// Always log the error
logError(error);
// Create an new notification
let id, title, body, icon, priority;
let notif = new Gio.Notification();
switch (error.name) {
case 'LanError':
id = error.name;
title = _('Network Error');
body = _('Click for help troubleshooting');
icon = new Gio.ThemedIcon({name: 'network-error'});
priority = Gio.NotificationPriority.URGENT;
notif.set_default_action(`app.wiki('Help#${error.name}')`);
break;
case 'DiscoveryWarning':
id = 'discovery-warning';
title = _('Discovery Disabled');
body = _('Discovery has been disabled due to the number of devices on this network.');
icon = new Gio.ThemedIcon({name: 'dialog-warning'});
priority = Gio.NotificationPriority.NORMAL;
notif.set_default_action('app.preferences');
break;
default:
id = `${Date.now()}`;
title = error.name.trim();
body = _('Click for more information');
icon = new Gio.ThemedIcon({name: 'dialog-error'});
error = new GLib.Variant('a{ss}', {
name: error.name.trim(),
message: error.message.trim(),
stack: error.stack.trim()
});
notif.set_default_action_and_target('app.error', error);
priority = Gio.NotificationPriority.HIGH;
}
// Create an urgent notification
notif.set_title(`GSConnect: ${title}`);
notif.set_body(body);
notif.set_icon(icon);
notif.set_priority(priority);
// Bypass override
super.send_notification(id, notif);
} catch (e) {
logError(e);
}
}
vfunc_activate() {
super.vfunc_activate();
}
vfunc_startup() {
super.vfunc_startup();
this.hold();
// Watch *this* file and stop the service if it's updated/uninstalled
this._serviceMonitor = Gio.File.new_for_path(
gsconnect.extdatadir + '/service/daemon.js'
).monitor(Gio.FileMonitorFlags.WATCH_MOVES, null);
this._serviceMonitor.connect('changed', () => this.quit());
// Init some resources
let provider = new Gtk.CssProvider();
provider.load_from_resource(gsconnect.app_path + '/application.css');
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
);
// Ensure our handlers are registered
try {
let appInfo = Gio.DesktopAppInfo.new(`${gsconnect.app_id}.desktop`);
appInfo.add_supports_type('x-scheme-handler/sms');
appInfo.add_supports_type('x-scheme-handler/tel');
} catch (e) {
logError(e);
}
// GActions & GSettings
this._initSettings();
this._initActions();
this._initComponents();
this._initBackends();
// Load cached devices
for (let id of this.settings.get_strv('devices')) {
let device = new Device.Device({body: {deviceId: id}});
this._devices.set(id, device);
}
// Reconnect to paired devices every 5 seconds
GLib.timeout_add_seconds(300, 5, this._reconnect.bind(this));
}
vfunc_dbus_register(connection, object_path) {
if (!super.vfunc_dbus_register(connection, object_path))
return false;
this.objectManager = new Gio.DBusObjectManagerServer({
connection: connection,
object_path: object_path
});
return true;
}
vfunc_open(files, hint) {
super.vfunc_open(files, hint);
for (let file of files) {
let action, parameter, title;
try {
switch (file.get_uri_scheme()) {
case 'sms':
title = _('Send SMS');
action = 'uriSms';
parameter = new GLib.Variant('s', file.get_uri());
break;
case 'tel':
title = _('Dial Number');
action = 'shareUri';
parameter = new GLib.Variant('s', file.get_uri());
break;
case 'file':
title = _('Share File');
action = 'shareFile';
parameter = new GLib.Variant('(sb)', [file.get_uri(), false]);
break;
default:
throw new Error(`Unsupported URI: ${file.get_uri()}`);
}
// Show chooser dialog
new ServiceUI.DeviceChooserDialog({
title: title,
action: action,
parameter: parameter
});
} catch (e) {
logError(e, `GSConnect: Opening ${file.get_uri()}`);
}
}
}
vfunc_shutdown() {
// Dispose GSettings
this.settings.disconnect(this._nameChangedId);
this.settings.run_dispose();
// Destroy the backends first to avoid any further connections
for (let [name, backend] of this.backends) {
try {
backend.destroy();
} catch (e) {
logError(e, `'${name}' Backend`);
}
}
// We must unexport the devices before ::dbus-unregister is emitted
this._devices.forEach(device => device.destroy());
// Destroy the components last
for (let [name, component] of this.components) {
try {
component.destroy();
} catch (e) {
logError(e, `'${name}' Component`);
}
}
// Chain up last (application->priv->did_shutdown)
super.vfunc_shutdown();
}
/*
* CLI
*/
_initOptions() {
/*
* Device Listings
*/
this.add_main_option(
'list-devices',
'l'.charCodeAt(0),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('List available devices'),
null
);
this.add_main_option(
'list-all',
'a'.charCodeAt(0),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('List all devices'),
null
);
this.add_main_option(
'device',
'd'.charCodeAt(0),
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Target Device'),
'<device-id>'
);
/**
* Pairing
*/
this.add_main_option(
'pair',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Pair'),
null
);
this.add_main_option(
'unpair',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Unpair'),
null
);
/*
* Messaging
*/
this.add_main_option(
'message',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING_ARRAY,
_('Send SMS'),
'<phone-number>'
);
this.add_main_option(
'message-body',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Message Body'),
'<text>'
);
/*
* Notifications
*/
this.add_main_option(
'notification',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Send Notification'),
'<title>'
);
this.add_main_option(
'notification-appname',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Notification App Name'),
'<name>'
);
this.add_main_option(
'notification-body',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Notification Body'),
'<text>'
);
this.add_main_option(
'notification-icon',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Notification Icon'),
'<icon-name>'
);
this.add_main_option(
'notification-id',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Notification ID'),
'<id>'
);
this.add_main_option(
'photo',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Photo'),
null
);
this.add_main_option(
'ping',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Ping'),
null
);
this.add_main_option(
'ring',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Ring'),
null
);
/*
* Sharing
*/
this.add_main_option(
'share-file',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.FILENAME_ARRAY,
_('Share File'),
'<filepath|URI>'
);
this.add_main_option(
'share-link',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING_ARRAY,
_('Share Link'),
'<URL>'
);
this.add_main_option(
'share-text',
null,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
_('Share Text'),
'<text>'
);
/*
* Misc
*/
this.add_main_option(
'version',
'v'.charCodeAt(0),
GLib.OptionFlags.NONE,
GLib.OptionArg.NONE,
_('Show release version'),
null
);
}
_cliAction(id, name, parameter = null) {
let parameters = [];
if (parameter instanceof GLib.Variant) {
parameters[0] = parameter;
}
id = id.replace(/\W+/g, '_');
Gio.DBus.session.call_sync(
'org.gnome.Shell.Extensions.GSConnect',
`/org/gnome/Shell/Extensions/GSConnect/Device/${id}`,
'org.gtk.Actions',
'Activate',
GLib.Variant.new('(sava{sv})', [name, parameters, {}]),
null,
Gio.DBusCallFlags.NONE,
-1,
null
);
}
_cliListDevices(full = true) {
let result = Gio.DBus.session.call_sync(
'org.gnome.Shell.Extensions.GSConnect',
'/org/gnome/Shell/Extensions/GSConnect',
'org.freedesktop.DBus.ObjectManager',
'GetManagedObjects',
null,
null,
Gio.DBusCallFlags.NONE,
-1,
null
);
let variant = result.unpack()[0].unpack();
let device;
for (let object of Object.values(variant)) {
object = object.recursiveUnpack();
device = object['org.gnome.Shell.Extensions.GSConnect.Device'];
if (full) {
print(`${device.Id}\t${device.Name}\t${device.Connected}\t${device.Paired}`);
} else if (device.Connected && device.Paired) {
print(device.Id);
}
}
}
_cliMessage(id, options) {
if (!options.contains('message-body')) {
throw new TypeError('missing --message-body option');
}
// TODO: currently we only support single-recipient messaging
let addresses = options.lookup_value('message', null).deepUnpack();
let body = options.lookup_value('message-body', null).deepUnpack();
this._cliAction(
id,
'sendSms',
GLib.Variant.new('(ss)', [addresses[0], body])
);
}
_cliNotify(id, options) {
let title = options.lookup_value('notification', null).unpack();
let body = '';
let icon = null;
let nid = `${Date.now()}`;
let appName = gsconnect.settings.get_string('name');
if (options.contains('notification-id')) {
nid = options.lookup_value('notification-id', null).unpack();
}
if (options.contains('notification-body')) {
body = options.lookup_value('notification-body', null).unpack();
}
if (options.contains('notification-appname')) {
appName = options.lookup_value('notification-appname', null).unpack();
}
if (options.contains('notification-icon')) {
icon = options.lookup_value('notification-icon', null).unpack();
icon = Gio.Icon.new_for_string(icon);
}
let notif = {
appName: appName,
id: nid,
title: title,
text: body,
ticker: `${title}: ${body}`,
time: `${Date.now()}`,
isClearable: true,
icon: icon
};
let parameter = GLib.Variant.full_pack(notif);
this._cliAction(id, 'sendNotification', parameter);
}
_cliShareFile(device, options) {
let files = options.lookup_value('share-file', null);
files = files.deepUnpack();
files.map(file => {
file = imports.byteArray.toString(file);
this._cliAction(device, 'shareFile', GLib.Variant.new('(sb)', [file, false]));
});
}
_cliShareLink(device, options) {
let uris = options.lookup_value('share-link', null).deepUnpack();
uris.map(uri => {
uri = imports.byteArray.toString(uri);
this._cliAction(device, 'shareUri', GLib.Variant.new_string(uri));
});
}
_cliShareText(device, options) {
let text = options.lookup_value('share-text', null).unpack();
this._cliAction(device, 'shareText', GLib.Variant.new_string(text));
}
vfunc_handle_local_options(options) {
try {
if (options.contains('version')) {
print(`GSConnect ${gsconnect.metadata.version}`);
return 0;
}
this.register(null);
if (options.contains('list-devices')) {
this._cliListDevices(false);
return 0;
}
if (options.contains('list-all')) {
this._cliListDevices(true);
return 0;
}
// We need a device for anything else; exit since this is probably
// the daemon being started.
if (!options.contains('device')) {
return -1;
}
let id = options.lookup_value('device', null).unpack();
// Pairing
if (options.contains('pair')) {
this._cliAction(id, 'pair');
}
if (options.contains('unpair')) {
this._cliAction(id, 'unpair');
return 0;
}
// Plugins
if (options.contains('message')) {
this._cliMessage(id, options);
}
if (options.contains('notification')) {
this._cliNotify(id, options);
}
if (options.contains('photo')) {
this._cliAction(id, 'photo');
}
if (options.contains('ping')) {
this._cliAction(id, 'ping', GLib.Variant.new_string(''));
}
if (options.contains('ring')) {
this._cliAction(id, 'ring');
}
if (options.contains('share-file')) {
this._cliShareFile(id, options);
}
if (options.contains('share-link')) {
this._cliShareLink(id, options);
}
return 0;
} catch (e) {
logError(e);
return 1;
}
}
});
(new Service()).run([imports.system.programInvocationName].concat(ARGV));