daemon.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. #!/usr/bin/env -S gjs -m
  2. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  3. //
  4. // SPDX-License-Identifier: GPL-2.0-or-later
  5. import Gdk from 'gi://Gdk?version=3.0';
  6. import 'gi://GdkPixbuf?version=2.0';
  7. import Gio from 'gi://Gio?version=2.0';
  8. import GLib from 'gi://GLib?version=2.0';
  9. import GObject from 'gi://GObject?version=2.0';
  10. import Gtk from 'gi://Gtk?version=3.0';
  11. import 'gi://Pango?version=1.0';
  12. // GNOME 49 uses GIRepository 3.0
  13. import('gi://GIRepository?version=3.0').catch(() => {
  14. import('gi://GIRepository?version=2.0').catch(() => {});
  15. });
  16. import('gi://GioUnix?version=2.0').catch(() => {}); // Set version for optional dependency
  17. import system from 'system';
  18. import './init.js';
  19. import Config from '../config.js';
  20. import Device from './device.js';
  21. import Manager from './manager.js';
  22. import * as ServiceUI from './ui/service.js';
  23. import {MissingOpensslError} from '../utils/exceptions.js';
  24. /**
  25. * Class representing the GSConnect service daemon.
  26. */
  27. const Service = GObject.registerClass({
  28. GTypeName: 'GSConnectService',
  29. }, class Service extends Gtk.Application {
  30. _init() {
  31. super._init({
  32. application_id: 'org.gnome.Shell.Extensions.GSConnect',
  33. flags: Gio.ApplicationFlags.HANDLES_OPEN,
  34. resource_base_path: '/org/gnome/Shell/Extensions/GSConnect',
  35. });
  36. GLib.set_prgname('gsconnect');
  37. GLib.set_application_name('GSConnect');
  38. // Command-line
  39. this._initOptions();
  40. }
  41. _migrateConfiguration() {
  42. if (!Device.validateName(this.settings.get_string('name')))
  43. this.settings.set_string('name', GLib.get_host_name().slice(0, 32));
  44. const [certPath, keyPath] = [
  45. GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
  46. GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
  47. ];
  48. const certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath, null);
  49. if (Device.validateId(certificate.common_name))
  50. return;
  51. // Remove the old certificate, serving as the single source of truth
  52. // for the device ID
  53. try {
  54. Gio.File.new_for_path(certPath).delete(null);
  55. Gio.File.new_for_path(keyPath).delete(null);
  56. } catch {
  57. // Silence errors
  58. }
  59. // For each device, remove it entirely if it violates the device ID
  60. // constraints, otherwise mark it unpaired to preserve the settings.
  61. const deviceList = this.settings.get_strv('devices').filter(id => {
  62. const settingsPath = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
  63. if (!Device.validateId(id)) {
  64. GLib.spawn_command_line_async(`dconf reset -f ${settingsPath}`);
  65. Gio.File.rm_rf(GLib.build_filenamev([Config.CACHEDIR, id]));
  66. debug(`Invalid device ID ${id} removed.`);
  67. return false;
  68. }
  69. const settings = new Gio.Settings({
  70. settings_schema: Config.GSCHEMA.lookup(
  71. 'org.gnome.Shell.Extensions.GSConnect.Device', true),
  72. path: settingsPath,
  73. });
  74. settings.set_boolean('paired', false);
  75. return true;
  76. });
  77. this.settings.set_strv('devices', deviceList);
  78. // Notify the user
  79. const notification = Gio.Notification.new(_('Settings Migrated'));
  80. notification.set_body(_('GSConnect has updated to support changes to the KDE Connect protocol. Some devices may need to be re-paired.'));
  81. notification.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'}));
  82. notification.set_priority(Gio.NotificationPriority.HIGH);
  83. this.send_notification('settings-migrated', notification);
  84. // Finally, reset the service ID to trigger re-generation.
  85. this.settings.reset('id');
  86. }
  87. get settings() {
  88. if (this._settings === undefined) {
  89. this._settings = new Gio.Settings({
  90. settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true),
  91. });
  92. }
  93. return this._settings;
  94. }
  95. /*
  96. * GActions
  97. */
  98. _initActions() {
  99. const actions = [
  100. ['connect', this._identify.bind(this), new GLib.VariantType('s')],
  101. ['device', this._device.bind(this), new GLib.VariantType('(ssbv)')],
  102. ['error', this._error.bind(this), new GLib.VariantType('a{ss}')],
  103. ['preferences', this._preferences, null],
  104. ['quit', () => this.quit(), null],
  105. ['refresh', this._identify.bind(this), null],
  106. ];
  107. for (const [name, callback, type] of actions) {
  108. const action = new Gio.SimpleAction({
  109. name: name,
  110. parameter_type: type,
  111. });
  112. action.connect('activate', callback);
  113. this.add_action(action);
  114. }
  115. }
  116. /**
  117. * A wrapper for Device GActions. This is used to route device notification
  118. * actions to their device, since GNotifications need an 'app' level action.
  119. *
  120. * @param {Gio.Action} action - The GAction
  121. * @param {GLib.Variant} parameter - The activation parameter
  122. */
  123. _device(action, parameter) {
  124. try {
  125. parameter = parameter.unpack();
  126. // Select the appropriate device(s)
  127. let devices;
  128. const id = parameter[0].unpack();
  129. if (id === '*')
  130. devices = this.manager.devices.values();
  131. else
  132. devices = [this.manager.devices.get(id)];
  133. // Unpack the action data and activate the action
  134. const name = parameter[1].unpack();
  135. const target = parameter[2].unpack() ? parameter[3].unpack() : null;
  136. for (const device of devices)
  137. device.activate_action(name, target);
  138. } catch (e) {
  139. logError(e);
  140. }
  141. }
  142. _error(action, parameter) {
  143. try {
  144. const error = parameter.deepUnpack();
  145. // If there's a URL, we have better information in the Wiki
  146. if (error.url !== undefined) {
  147. Gio.AppInfo.launch_default_for_uri_async(
  148. error.url,
  149. null,
  150. null,
  151. null
  152. );
  153. return;
  154. }
  155. const dialog = new ServiceUI.ErrorDialog(error);
  156. dialog.present();
  157. } catch (e) {
  158. logError(e);
  159. }
  160. }
  161. _identify(action, parameter) {
  162. try {
  163. let uri = null;
  164. if (parameter instanceof GLib.Variant)
  165. uri = parameter.unpack();
  166. this.manager.identify(uri);
  167. } catch (e) {
  168. logError(e);
  169. }
  170. }
  171. _preferences() {
  172. Gio.Subprocess.new(
  173. [`${Config.PACKAGE_DATADIR}/gsconnect-preferences`],
  174. Gio.SubprocessFlags.NONE
  175. );
  176. }
  177. /**
  178. * Report a service-level error
  179. *
  180. * @param {object} error - An Error or object with name, message and stack
  181. * @param {string} [notification_id] - An optional id for the notification
  182. */
  183. notify_error(error, notification_id) {
  184. try {
  185. // Always log the error
  186. logError(error);
  187. // Create an new notification
  188. let id, body, priority;
  189. const notif = new Gio.Notification();
  190. const icon = new Gio.ThemedIcon({name: 'dialog-error'});
  191. let target = null;
  192. if (error.name === undefined)
  193. error.name = 'Error';
  194. if (notification_id !== undefined)
  195. id = notification_id;
  196. else
  197. id = error.url || error.message.trim();
  198. if (error.url !== undefined) {
  199. body = _('Click for help troubleshooting');
  200. priority = Gio.NotificationPriority.URGENT;
  201. target = new GLib.Variant('a{ss}', {
  202. name: error.name.trim(),
  203. message: error.message.trim(),
  204. stack: error.stack.trim(),
  205. url: error.url,
  206. });
  207. } else {
  208. body = _('Click for more information');
  209. priority = Gio.NotificationPriority.HIGH;
  210. target = new GLib.Variant('a{ss}', {
  211. name: error.name.trim(),
  212. message: error.message.trim(),
  213. stack: error.stack.trim(),
  214. });
  215. }
  216. notif.set_title(`GSConnect: ${error.name.trim()}`);
  217. notif.set_body(body);
  218. notif.set_icon(icon);
  219. notif.set_priority(priority);
  220. notif.set_default_action_and_target('app.error', target);
  221. this.send_notification(id, notif);
  222. } catch (e) {
  223. logError(e);
  224. }
  225. }
  226. vfunc_activate() {
  227. super.vfunc_activate();
  228. }
  229. vfunc_startup() {
  230. super.vfunc_startup();
  231. this.hold();
  232. // Watch *this* file and stop the service if it's updated/uninstalled
  233. this._serviceMonitor = Gio.File.new_for_path(
  234. `${Config.PACKAGE_DATADIR}/service/daemon.js`
  235. ).monitor(Gio.FileMonitorFlags.WATCH_MOVES, null);
  236. this._serviceMonitor.connect('changed', () => this.quit());
  237. // Init some resources
  238. const provider = new Gtk.CssProvider();
  239. provider.load_from_resource(`${Config.APP_PATH}/application.css`);
  240. Gtk.StyleContext.add_provider_for_screen(
  241. Gdk.Screen.get_default(),
  242. provider,
  243. Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
  244. );
  245. // Ensure our handlers are registered
  246. try {
  247. const appInfo = Gio.DesktopAppInfo.new(`${Config.APP_ID}.desktop`);
  248. appInfo.add_supports_type('x-scheme-handler/sms');
  249. appInfo.add_supports_type('x-scheme-handler/tel');
  250. } catch (e) {
  251. debug(e);
  252. }
  253. // GActions & GSettings
  254. this._initActions();
  255. // TODO: remove after a reasonable period of time
  256. try {
  257. this._migrateConfiguration();
  258. if (this.settings.get_boolean('missing-openssl'))
  259. this.withdraw_notification('gsconnect-missing-openssl');
  260. this.settings.set_boolean('missing-openssl', false);
  261. } catch (e) {
  262. if (e instanceof MissingOpensslError) {
  263. this.settings.set_boolean('missing-openssl', true);
  264. this.notify_error(e, 'gsconnect-missing-openssl');
  265. }
  266. throw e;
  267. }
  268. this.manager.start();
  269. }
  270. vfunc_dbus_register(connection, object_path) {
  271. if (!super.vfunc_dbus_register(connection, object_path))
  272. return false;
  273. this.manager = new Manager({
  274. connection: connection,
  275. object_path: object_path,
  276. });
  277. return true;
  278. }
  279. vfunc_dbus_unregister(connection, object_path) {
  280. this.manager.destroy();
  281. super.vfunc_dbus_unregister(connection, object_path);
  282. }
  283. vfunc_open(files, hint) {
  284. super.vfunc_open(files, hint);
  285. for (const file of files) {
  286. let action, parameter, title;
  287. try {
  288. switch (file.get_uri_scheme()) {
  289. case 'sms':
  290. title = _('Send SMS');
  291. action = 'uriSms';
  292. parameter = new GLib.Variant('s', file.get_uri());
  293. break;
  294. case 'tel':
  295. title = _('Dial Number');
  296. action = 'shareUri';
  297. parameter = new GLib.Variant('s', file.get_uri());
  298. break;
  299. case 'file':
  300. title = _('Share File');
  301. action = 'shareFile';
  302. parameter = new GLib.Variant('(sb)', [file.get_uri(), false]);
  303. break;
  304. default:
  305. throw new Error(`Unsupported URI: ${file.get_uri()}`);
  306. }
  307. // Show chooser dialog
  308. new ServiceUI.DeviceChooser({
  309. title: title,
  310. action_name: action,
  311. action_target: parameter,
  312. });
  313. } catch (e) {
  314. logError(e, `GSConnect: Opening ${file.get_uri()}`);
  315. }
  316. }
  317. }
  318. vfunc_shutdown() {
  319. // Dispose GSettings
  320. if (this._settings !== undefined)
  321. this.settings.run_dispose();
  322. this.manager.stop();
  323. // Exhaust the event loop to ensure any pending operations complete
  324. const context = GLib.MainContext.default();
  325. while (context.iteration(false))
  326. continue;
  327. // Force a GC to prevent any more calls back into JS, then chain-up
  328. system.gc();
  329. super.vfunc_shutdown();
  330. }
  331. /*
  332. * CLI
  333. */
  334. _initOptions() {
  335. /*
  336. * Device Listings
  337. */
  338. this.add_main_option(
  339. 'list-devices',
  340. 'l'.charCodeAt(0),
  341. GLib.OptionFlags.NONE,
  342. GLib.OptionArg.NONE,
  343. _('List available devices'),
  344. null
  345. );
  346. this.add_main_option(
  347. 'list-all',
  348. 'a'.charCodeAt(0),
  349. GLib.OptionFlags.NONE,
  350. GLib.OptionArg.NONE,
  351. _('List all devices'),
  352. null
  353. );
  354. this.add_main_option(
  355. 'device',
  356. 'd'.charCodeAt(0),
  357. GLib.OptionFlags.NONE,
  358. GLib.OptionArg.STRING,
  359. _('Target Device'),
  360. '<device-id>'
  361. );
  362. /**
  363. * Pairing
  364. */
  365. this.add_main_option(
  366. 'pair',
  367. 0,
  368. GLib.OptionFlags.NONE,
  369. GLib.OptionArg.NONE,
  370. _('Pair'),
  371. null
  372. );
  373. this.add_main_option(
  374. 'unpair',
  375. 0,
  376. GLib.OptionFlags.NONE,
  377. GLib.OptionArg.NONE,
  378. _('Unpair'),
  379. null
  380. );
  381. /*
  382. * Messaging
  383. */
  384. this.add_main_option(
  385. 'message',
  386. 0,
  387. GLib.OptionFlags.NONE,
  388. GLib.OptionArg.STRING_ARRAY,
  389. _('Send SMS'),
  390. '<phone-number>'
  391. );
  392. this.add_main_option(
  393. 'message-body',
  394. 0,
  395. GLib.OptionFlags.NONE,
  396. GLib.OptionArg.STRING,
  397. _('Message Body'),
  398. '<text>'
  399. );
  400. /*
  401. * Notifications
  402. */
  403. this.add_main_option(
  404. 'notification',
  405. 0,
  406. GLib.OptionFlags.NONE,
  407. GLib.OptionArg.STRING,
  408. _('Send Notification'),
  409. '<title>'
  410. );
  411. this.add_main_option(
  412. 'notification-appname',
  413. 0,
  414. GLib.OptionFlags.NONE,
  415. GLib.OptionArg.STRING,
  416. _('Notification App Name'),
  417. '<name>'
  418. );
  419. this.add_main_option(
  420. 'notification-body',
  421. 0,
  422. GLib.OptionFlags.NONE,
  423. GLib.OptionArg.STRING,
  424. _('Notification Body'),
  425. '<text>'
  426. );
  427. this.add_main_option(
  428. 'notification-icon',
  429. 0,
  430. GLib.OptionFlags.NONE,
  431. GLib.OptionArg.STRING,
  432. _('Notification Icon'),
  433. '<icon-name>'
  434. );
  435. this.add_main_option(
  436. 'notification-id',
  437. 0,
  438. GLib.OptionFlags.NONE,
  439. GLib.OptionArg.STRING,
  440. _('Notification ID'),
  441. '<id>'
  442. );
  443. this.add_main_option(
  444. 'ping',
  445. 0,
  446. GLib.OptionFlags.NONE,
  447. GLib.OptionArg.NONE,
  448. _('Ping'),
  449. null
  450. );
  451. this.add_main_option(
  452. 'ring',
  453. 0,
  454. GLib.OptionFlags.NONE,
  455. GLib.OptionArg.NONE,
  456. _('Ring'),
  457. null
  458. );
  459. /*
  460. * Sharing
  461. */
  462. this.add_main_option(
  463. 'share-file',
  464. 0,
  465. GLib.OptionFlags.NONE,
  466. GLib.OptionArg.FILENAME_ARRAY,
  467. _('Share File'),
  468. '<filepath|URI>'
  469. );
  470. this.add_main_option(
  471. 'share-link',
  472. 0,
  473. GLib.OptionFlags.NONE,
  474. GLib.OptionArg.STRING_ARRAY,
  475. _('Share Link'),
  476. '<URL>'
  477. );
  478. this.add_main_option(
  479. 'share-text',
  480. 0,
  481. GLib.OptionFlags.NONE,
  482. GLib.OptionArg.STRING,
  483. _('Share Text'),
  484. '<text>'
  485. );
  486. /*
  487. * Misc
  488. */
  489. this.add_main_option(
  490. 'version',
  491. 'v'.charCodeAt(0),
  492. GLib.OptionFlags.NONE,
  493. GLib.OptionArg.NONE,
  494. _('Show release version'),
  495. null
  496. );
  497. }
  498. _cliAction(id, name, parameter = null) {
  499. const parameters = [];
  500. if (parameter instanceof GLib.Variant)
  501. parameters[0] = parameter;
  502. id = id.replace(/\W+/g, '_');
  503. Gio.DBus.session.call_sync(
  504. 'org.gnome.Shell.Extensions.GSConnect',
  505. `/org/gnome/Shell/Extensions/GSConnect/Device/${id}`,
  506. 'org.gtk.Actions',
  507. 'Activate',
  508. GLib.Variant.new('(sava{sv})', [name, parameters, {}]),
  509. null,
  510. Gio.DBusCallFlags.NONE,
  511. -1,
  512. null
  513. );
  514. }
  515. _cliListDevices(full = true) {
  516. const result = Gio.DBus.session.call_sync(
  517. 'org.gnome.Shell.Extensions.GSConnect',
  518. '/org/gnome/Shell/Extensions/GSConnect',
  519. 'org.freedesktop.DBus.ObjectManager',
  520. 'GetManagedObjects',
  521. null,
  522. null,
  523. Gio.DBusCallFlags.NONE,
  524. -1,
  525. null
  526. );
  527. const variant = result.unpack()[0].unpack();
  528. let device;
  529. for (let object of Object.values(variant)) {
  530. object = object.recursiveUnpack();
  531. device = object['org.gnome.Shell.Extensions.GSConnect.Device'];
  532. if (full)
  533. print(`${device.Id}\t${device.Name}\t${device.Connected}\t${device.Paired}`);
  534. else if (device.Connected && device.Paired)
  535. print(device.Id);
  536. }
  537. }
  538. _cliMessage(id, options) {
  539. if (!options.contains('message-body'))
  540. throw new TypeError('missing --message-body option');
  541. // TODO: currently we only support single-recipient messaging
  542. const addresses = options.lookup_value('message', null).deepUnpack();
  543. const body = options.lookup_value('message-body', null).deepUnpack();
  544. this._cliAction(
  545. id,
  546. 'sendSms',
  547. GLib.Variant.new('(ss)', [addresses[0], body])
  548. );
  549. }
  550. _cliNotify(id, options) {
  551. const title = options.lookup_value('notification', null).unpack();
  552. let body = '';
  553. let icon = null;
  554. let nid = `${Date.now()}`;
  555. let appName = 'GSConnect CLI';
  556. if (options.contains('notification-id'))
  557. nid = options.lookup_value('notification-id', null).unpack();
  558. if (options.contains('notification-body'))
  559. body = options.lookup_value('notification-body', null).unpack();
  560. if (options.contains('notification-appname'))
  561. appName = options.lookup_value('notification-appname', null).unpack();
  562. if (options.contains('notification-icon')) {
  563. icon = options.lookup_value('notification-icon', null).unpack();
  564. icon = Gio.Icon.new_for_string(icon);
  565. } else {
  566. icon = new Gio.ThemedIcon({
  567. name: 'org.gnome.Shell.Extensions.GSConnect',
  568. });
  569. }
  570. const notification = new GLib.Variant('a{sv}', {
  571. appName: GLib.Variant.new_string(appName),
  572. id: GLib.Variant.new_string(nid),
  573. title: GLib.Variant.new_string(title),
  574. text: GLib.Variant.new_string(body),
  575. ticker: GLib.Variant.new_string(`${title}: ${body}`),
  576. time: GLib.Variant.new_string(`${Date.now()}`),
  577. isClearable: GLib.Variant.new_boolean(true),
  578. icon: icon.serialize(),
  579. });
  580. this._cliAction(id, 'sendNotification', notification);
  581. }
  582. _cliShareFile(device, options) {
  583. const files = options.lookup_value('share-file', null).deepUnpack();
  584. for (let file of files) {
  585. file = new TextDecoder().decode(file);
  586. this._cliAction(device, 'shareFile', GLib.Variant.new('(sb)', [file, false]));
  587. }
  588. }
  589. _cliShareLink(device, options) {
  590. const uris = options.lookup_value('share-link', null).unpack();
  591. for (const uri of uris)
  592. this._cliAction(device, 'shareUri', uri);
  593. }
  594. _cliShareText(device, options) {
  595. const text = options.lookup_value('share-text', null).unpack();
  596. this._cliAction(device, 'shareText', GLib.Variant.new_string(text));
  597. }
  598. vfunc_handle_local_options(options) {
  599. try {
  600. if (options.contains('version')) {
  601. print(`GSConnect ${Config.PACKAGE_VERSION}`);
  602. return 0;
  603. }
  604. this.register(null);
  605. if (options.contains('list-devices')) {
  606. this._cliListDevices(false);
  607. return 0;
  608. }
  609. if (options.contains('list-all')) {
  610. this._cliListDevices(true);
  611. return 0;
  612. }
  613. // We need a device for anything else; exit since this is probably
  614. // the daemon being started.
  615. if (!options.contains('device'))
  616. return -1;
  617. const id = options.lookup_value('device', null).unpack();
  618. // Pairing
  619. if (options.contains('pair')) {
  620. this._cliAction(id, 'pair');
  621. return 0;
  622. }
  623. if (options.contains('unpair')) {
  624. this._cliAction(id, 'unpair');
  625. return 0;
  626. }
  627. // Plugins
  628. if (options.contains('message'))
  629. this._cliMessage(id, options);
  630. if (options.contains('notification'))
  631. this._cliNotify(id, options);
  632. if (options.contains('ping'))
  633. this._cliAction(id, 'ping', GLib.Variant.new_string(''));
  634. if (options.contains('ring'))
  635. this._cliAction(id, 'ring');
  636. if (options.contains('share-file'))
  637. this._cliShareFile(id, options);
  638. if (options.contains('share-link'))
  639. this._cliShareLink(id, options);
  640. if (options.contains('share-text'))
  641. this._cliShareText(id, options);
  642. return 0;
  643. } catch (e) {
  644. logError(e);
  645. return 1;
  646. }
  647. }
  648. });
  649. await (new Service()).runAsync([system.programInvocationName].concat(ARGV));