Merge branch 'gpgmepp'
[gpgme.git] / src / engine-assuan.c
1 /* engine-assuan.c - Low-level Assuan protocol engine
2  * Copyright (C) 2009 g10 Code GmbH
3  *
4  * This file is part of GPGME.
5  *
6  * GPGME is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as
8  * published by the Free Software Foundation; either version 2.1 of
9  * the License, or (at your option) any later version.
10  *
11  * GPGME is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this program; if not, see <http://www.gnu.org/licenses/>.
18  */
19
20 /*
21    Note: This engine requires a modern Assuan server which uses
22    gpg-error codes.  In particular there is no backward compatible
23    mapping of old Assuan error codes implemented.
24 */
25
26
27 #if HAVE_CONFIG_H
28 #include <config.h>
29 #endif
30
31 #include <stdlib.h>
32 #include <string.h>
33 #ifdef HAVE_SYS_TYPES_H
34 # include <sys/types.h>
35 #endif
36 #include <assert.h>
37 #ifdef HAVE_UNISTD_H
38 # include <unistd.h>
39 #endif
40 #ifdef HAVE_LOCALE_H
41 #include <locale.h>
42 #endif
43 #include <errno.h>
44
45 #include "gpgme.h"
46 #include "util.h"
47 #include "ops.h"
48 #include "wait.h"
49 #include "priv-io.h"
50 #include "sema.h"
51
52 #include "assuan.h"
53 #include "debug.h"
54
55 #include "engine-backend.h"
56
57 \f
58 typedef struct
59 {
60   int fd;       /* FD we talk about.  */
61   int server_fd;/* Server FD for this connection.  */
62   int dir;      /* Inbound/Outbound, maybe given implicit?  */
63   void *data;   /* Handler-specific data.  */
64   void *tag;    /* ID from the user for gpgme_remove_io_callback.  */
65 } iocb_data_t;
66
67 /* Engine instance data.  */
68 struct engine_llass
69 {
70   assuan_context_t assuan_ctx;
71
72   int lc_ctype_set;
73   int lc_messages_set;
74
75   iocb_data_t status_cb;
76
77   struct gpgme_io_cbs io_cbs;
78
79   /* Hack for old opassuan.c interface, see there the result struct.  */
80   gpg_error_t last_op_err;
81
82   /* User provided callbacks.  */
83   struct {
84     gpgme_assuan_data_cb_t data_cb;
85     void *data_cb_value;
86
87     gpgme_assuan_inquire_cb_t inq_cb;
88     void *inq_cb_value;
89
90     gpgme_assuan_status_cb_t status_cb;
91     void *status_cb_value;
92   } user;
93
94   /* Option flags.  */
95   struct {
96     int gpg_agent:1;  /* Assume this is a gpg-agent connection.  */
97   } opt;
98
99 };
100 typedef struct engine_llass *engine_llass_t;
101
102
103 gpg_error_t _gpgme_engine_assuan_last_op_err (void *engine)
104 {
105   engine_llass_t llass = engine;
106   return llass->last_op_err;
107 }
108
109
110 /* Prototypes.  */
111 static void llass_io_event (void *engine,
112                             gpgme_event_io_t type, void *type_data);
113
114
115
116
117 \f
118 /* return the default home directory.  */
119 static const char *
120 llass_get_home_dir (void)
121 {
122   /* For this engine the home directory is not a filename but a string
123      used to convey options.  The exclamation mark is a marker to show
124      that this is not a directory name. Options are strings delimited
125      by a space.  The only option defined for now is GPG_AGENT to
126      enable GPG_AGENT specific commands to send to the server at
127      connection startup.  */
128   return "!GPG_AGENT";
129 }
130
131 static char *
132 llass_get_version (const char *file_name)
133 {
134   return strdup ("1.0");
135 }
136
137
138 static const char *
139 llass_get_req_version (void)
140 {
141   return "1.0";
142 }
143
144 \f
145 static void
146 close_notify_handler (int fd, void *opaque)
147 {
148   engine_llass_t llass = opaque;
149
150   assert (fd != -1);
151   if (llass->status_cb.fd == fd)
152     {
153       if (llass->status_cb.tag)
154         llass->io_cbs.remove (llass->status_cb.tag);
155       llass->status_cb.fd = -1;
156       llass->status_cb.tag = NULL;
157     }
158 }
159
160
161
162 static gpgme_error_t
163 llass_cancel (void *engine)
164 {
165   engine_llass_t llass = engine;
166
167   if (!llass)
168     return gpg_error (GPG_ERR_INV_VALUE);
169
170   if (llass->status_cb.fd != -1)
171     _gpgme_io_close (llass->status_cb.fd);
172
173   if (llass->assuan_ctx)
174     {
175       assuan_release (llass->assuan_ctx);
176       llass->assuan_ctx = NULL;
177     }
178
179   return 0;
180 }
181
182
183 static gpgme_error_t
184 llass_cancel_op (void *engine)
185 {
186   engine_llass_t llass = engine;
187
188   if (!llass)
189     return gpg_error (GPG_ERR_INV_VALUE);
190
191   if (llass->status_cb.fd != -1)
192     _gpgme_io_close (llass->status_cb.fd);
193
194   return 0;
195 }
196
197
198 static void
199 llass_release (void *engine)
200 {
201   engine_llass_t llass = engine;
202
203   if (!llass)
204     return;
205
206   llass_cancel (engine);
207
208   free (llass);
209 }
210
211
212 /* Create a new instance. If HOME_DIR is NULL standard options for use
213    with gpg-agent are issued.  */
214 static gpgme_error_t
215 llass_new (void **engine, const char *file_name, const char *home_dir)
216 {
217   gpgme_error_t err = 0;
218   engine_llass_t llass;
219   char *optstr;
220
221   llass = calloc (1, sizeof *llass);
222   if (!llass)
223     return gpg_error_from_syserror ();
224
225   llass->status_cb.fd = -1;
226   llass->status_cb.dir = 1;
227   llass->status_cb.tag = 0;
228   llass->status_cb.data = llass;
229
230   /* Parse_options.  */
231   if (home_dir && *home_dir == '!')
232     {
233       home_dir++;
234       /* Very simple parser only working for the one option we support.  */
235       /* Note that wk promised to write a regression test if this
236          parser will be extended.  */
237       if (!strncmp (home_dir, "GPG_AGENT", 9)
238           && (!home_dir[9] || home_dir[9] == ' '))
239         llass->opt.gpg_agent = 1;
240     }
241
242   err = assuan_new_ext (&llass->assuan_ctx, GPG_ERR_SOURCE_GPGME,
243                         &_gpgme_assuan_malloc_hooks, _gpgme_assuan_log_cb,
244                         NULL);
245   if (err)
246     goto leave;
247   assuan_ctx_set_system_hooks (llass->assuan_ctx, &_gpgme_assuan_system_hooks);
248
249   err = assuan_socket_connect (llass->assuan_ctx, file_name, 0, 0);
250   if (err)
251     goto leave;
252
253   if (llass->opt.gpg_agent)
254     {
255       char *dft_display = NULL;
256
257       err = _gpgme_getenv ("DISPLAY", &dft_display);
258       if (err)
259         goto leave;
260       if (dft_display)
261         {
262           if (asprintf (&optstr, "OPTION display=%s", dft_display) < 0)
263             {
264               err = gpg_error_from_syserror ();
265               free (dft_display);
266               goto leave;
267             }
268           free (dft_display);
269
270           err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL, NULL,
271                                  NULL, NULL, NULL);
272           free (optstr);
273           if (err)
274             goto leave;
275         }
276     }
277
278   if (llass->opt.gpg_agent && isatty (1))
279     {
280       int rc;
281       char dft_ttyname[64];
282       char *dft_ttytype = NULL;
283
284       rc = ttyname_r (1, dft_ttyname, sizeof (dft_ttyname));
285
286       /* Even though isatty() returns 1, ttyname_r() may fail in many
287          ways, e.g., when /dev/pts is not accessible under chroot.  */
288       if (!rc)
289         {
290           if (asprintf (&optstr, "OPTION ttyname=%s", dft_ttyname) < 0)
291             {
292               err = gpg_error_from_syserror ();
293               goto leave;
294             }
295           err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL, NULL,
296                                  NULL, NULL, NULL);
297           free (optstr);
298           if (err)
299             goto leave;
300
301           err = _gpgme_getenv ("TERM", &dft_ttytype);
302           if (err)
303             goto leave;
304           if (dft_ttytype)
305             {
306               if (asprintf (&optstr, "OPTION ttytype=%s", dft_ttytype) < 0)
307                 {
308                   err = gpg_error_from_syserror ();
309                   free (dft_ttytype);
310                   goto leave;
311                 }
312               free (dft_ttytype);
313
314               err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL,
315                                      NULL, NULL, NULL, NULL);
316               free (optstr);
317               if (err)
318                 goto leave;
319             }
320         }
321     }
322
323
324 #ifdef HAVE_W32_SYSTEM
325   /* Under Windows we need to use AllowSetForegroundWindow.  Tell
326      llass to tell us when it needs it.  */
327   if (!err && llass->opt.gpg_agent)
328     {
329       err = assuan_transact (llass->assuan_ctx, "OPTION allow-pinentry-notify",
330                              NULL, NULL, NULL, NULL, NULL, NULL);
331       if (gpg_err_code (err) == GPG_ERR_UNKNOWN_OPTION)
332         err = 0; /* This work only with recent gpg-agents.  */
333     }
334 #endif /*HAVE_W32_SYSTEM*/
335
336
337  leave:
338   /* Close the server ends of the pipes (because of this, we must use
339      the stored server_fd_str in the function start).  Our ends are
340      closed in llass_release().  */
341
342   if (err)
343     llass_release (llass);
344   else
345     *engine = llass;
346
347   return err;
348 }
349
350
351 static gpgme_error_t
352 llass_set_locale (void *engine, int category, const char *value)
353 {
354   gpgme_error_t err;
355   engine_llass_t llass = engine;
356   char *optstr;
357   char *catstr;
358
359   if (!llass->opt.gpg_agent)
360     return 0;
361
362   /* FIXME: If value is NULL, we need to reset the option to default.
363      But we can't do this.  So we error out here.  gpg-agent needs
364      support for this.  */
365   if (0)
366     ;
367 #ifdef LC_CTYPE
368   else if (category == LC_CTYPE)
369     {
370       catstr = "lc-ctype";
371       if (!value && llass->lc_ctype_set)
372         return gpg_error (GPG_ERR_INV_VALUE);
373       if (value)
374         llass->lc_ctype_set = 1;
375     }
376 #endif
377 #ifdef LC_MESSAGES
378   else if (category == LC_MESSAGES)
379     {
380       catstr = "lc-messages";
381       if (!value && llass->lc_messages_set)
382         return gpg_error (GPG_ERR_INV_VALUE);
383       if (value)
384         llass->lc_messages_set = 1;
385     }
386 #endif /* LC_MESSAGES */
387   else
388     return gpg_error (GPG_ERR_INV_VALUE);
389
390   /* FIXME: Reset value to default.  */
391   if (!value)
392     return 0;
393
394   if (asprintf (&optstr, "OPTION %s=%s", catstr, value) < 0)
395     err = gpg_error_from_syserror ();
396   else
397     {
398       err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL,
399                              NULL, NULL, NULL, NULL);
400       free (optstr);
401     }
402   return err;
403 }
404
405
406 /* This is the inquiry callback.  It handles stuff which ee need to
407    handle here and passes everything on to the user callback.  */
408 static gpgme_error_t
409 inquire_cb (engine_llass_t llass, const char *keyword, const char *args)
410 {
411   gpg_error_t err;
412
413   if (llass->opt.gpg_agent && !strcmp (keyword, "PINENTRY_LAUNCHED"))
414     {
415       _gpgme_allow_set_foreground_window ((pid_t)strtoul (args, NULL, 10));
416     }
417
418   if (llass->user.inq_cb)
419     {
420       gpgme_data_t data = NULL;
421
422       err = llass->user.inq_cb (llass->user.inq_cb_value,
423                                 keyword, args, &data);
424       if (!err && data)
425         {
426           /* FIXME: Returning data is not yet implemented.  However we
427              need to allow the caller to cleanup his data object.
428              Thus we run the callback in finish mode immediately.  */
429           err = llass->user.inq_cb (llass->user.inq_cb_value,
430                                     NULL, NULL, &data);
431         }
432     }
433   else
434     err = 0;
435
436   return err;
437 }
438
439
440 static gpgme_error_t
441 llass_status_handler (void *opaque, int fd)
442 {
443   struct io_cb_data *data = (struct io_cb_data *) opaque;
444   engine_llass_t llass = (engine_llass_t) data->handler_value;
445   gpgme_error_t err = 0;
446   char *line;
447   size_t linelen;
448
449   do
450     {
451       err = assuan_read_line (llass->assuan_ctx, &line, &linelen);
452       if (err)
453         {
454           /* Reading a full line may not be possible when
455              communicating over a socket in nonblocking mode.  In this
456              case, we are done for now.  */
457           if (gpg_err_code (err) == GPG_ERR_EAGAIN)
458             {
459               TRACE1 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
460                       "fd 0x%x: EAGAIN reading assuan line (ignored)", fd);
461               err = 0;
462               continue;
463             }
464
465           TRACE2 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
466                   "fd 0x%x: error reading assuan line: %s",
467                   fd, gpg_strerror (err));
468         }
469       else if (linelen >= 2 && line[0] == 'D' && line[1] == ' ')
470         {
471           char *src = line + 2;
472           char *end = line + linelen;
473           char *dst = src;
474
475           linelen = 0;
476           while (src < end)
477             {
478               if (*src == '%' && src + 2 < end)
479                 {
480                   /* Handle escaped characters.  */
481                   ++src;
482                   *dst++ = _gpgme_hextobyte (src);
483                   src += 2;
484                 }
485               else
486                 *dst++ = *src++;
487
488               linelen++;
489             }
490
491           src = line + 2;
492           if (linelen && llass->user.data_cb)
493             err = llass->user.data_cb (llass->user.data_cb_value,
494                                        src, linelen);
495
496           TRACE2 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
497                   "fd 0x%x: D inlinedata; status from cb: %s",
498                   fd, (llass->user.data_cb ?
499                        (err? gpg_strerror (err):"ok"):"no callback"));
500         }
501       else if (linelen >= 3
502                && line[0] == 'E' && line[1] == 'N' && line[2] == 'D'
503                && (line[3] == '\0' || line[3] == ' '))
504         {
505           /* END received.  Tell the data callback.  */
506           if (llass->user.data_cb)
507             err = llass->user.data_cb (llass->user.data_cb_value, NULL, 0);
508
509           TRACE2 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
510                   "fd 0x%x: END line; status from cb: %s",
511                   fd, (llass->user.data_cb ?
512                        (err? gpg_strerror (err):"ok"):"no callback"));
513         }
514       else if (linelen > 2 && line[0] == 'S' && line[1] == ' ')
515         {
516           char *args;
517           char *src;
518
519           for (src=line+2; *src == ' '; src++)
520             ;
521
522           args = strchr (src, ' ');
523           if (!args)
524             args = line + linelen; /* Let it point to an empty string.  */
525           else
526             *(args++) = 0;
527
528           while (*args == ' ')
529             args++;
530
531           if (llass->user.status_cb)
532             err = llass->user.status_cb (llass->user.status_cb_value,
533                                          src, args);
534
535           TRACE3 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
536                   "fd 0x%x: S line (%s) - status from cb: %s",
537                   fd, line+2, (llass->user.status_cb ?
538                                (err? gpg_strerror (err):"ok"):"no callback"));
539         }
540       else if (linelen >= 7
541                && line[0] == 'I' && line[1] == 'N' && line[2] == 'Q'
542                && line[3] == 'U' && line[4] == 'I' && line[5] == 'R'
543                && line[6] == 'E'
544                && (line[7] == '\0' || line[7] == ' '))
545         {
546           char *src;
547           char *args;
548
549           for (src=line+7; *src == ' '; src++)
550             ;
551
552           args = strchr (src, ' ');
553           if (!args)
554             args = line + linelen; /* Let it point to an empty string.  */
555           else
556             *(args++) = 0;
557
558           while (*args == ' ')
559             args++;
560
561           err = inquire_cb (llass, src, args);
562           if (!err)
563             {
564               /* Flush and send END.  */
565               err = assuan_send_data (llass->assuan_ctx, NULL, 0);
566             }
567           else if (gpg_err_code (err) == GPG_ERR_ASS_CANCELED)
568             {
569               /* Flush and send CANcel.  */
570               err = assuan_send_data (llass->assuan_ctx, NULL, 1);
571             }
572         }
573       else if (linelen >= 3
574                && line[0] == 'E' && line[1] == 'R' && line[2] == 'R'
575                && (line[3] == '\0' || line[3] == ' '))
576         {
577           if (line[3] == ' ')
578             err = atoi (line+4);
579           else
580             err = gpg_error (GPG_ERR_GENERAL);
581           TRACE2 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
582                   "fd 0x%x: ERR line: %s",
583                   fd, err ? gpg_strerror (err) : "ok");
584
585           /* Command execution errors are not fatal, as we use
586              a session based protocol.  */
587           data->op_err = err;
588           llass->last_op_err = err;
589
590           /* The caller will do the rest (namely, call cancel_op,
591              which closes status_fd).  */
592           return 0;
593         }
594       else if (linelen >= 2
595                && line[0] == 'O' && line[1] == 'K'
596                && (line[2] == '\0' || line[2] == ' '))
597         {
598           TRACE1 (DEBUG_CTX, "gpgme:llass_status_handler", llass,
599                   "fd 0x%x: OK line", fd);
600
601           llass->last_op_err = 0;
602
603           _gpgme_io_close (llass->status_cb.fd);
604           return 0;
605         }
606       else
607         {
608           /* Comment line or invalid line.  */
609         }
610
611     }
612   while (!err && assuan_pending_line (llass->assuan_ctx));
613
614   return err;
615 }
616
617
618 static gpgme_error_t
619 add_io_cb (engine_llass_t llass, iocb_data_t *iocbd, gpgme_io_cb_t handler)
620 {
621   gpgme_error_t err;
622
623   TRACE_BEG2 (DEBUG_ENGINE, "engine-assuan:add_io_cb", llass,
624               "fd %d, dir %d", iocbd->fd, iocbd->dir);
625   err = (*llass->io_cbs.add) (llass->io_cbs.add_priv,
626                               iocbd->fd, iocbd->dir,
627                               handler, iocbd->data, &iocbd->tag);
628   if (err)
629     return TRACE_ERR (err);
630   if (!iocbd->dir)
631     /* FIXME Kludge around poll() problem.  */
632     err = _gpgme_io_set_nonblocking (iocbd->fd);
633   return TRACE_ERR (err);
634 }
635
636
637 static gpgme_error_t
638 start (engine_llass_t llass, const char *command)
639 {
640   gpgme_error_t err;
641   assuan_fd_t afdlist[5];
642   int fdlist[5];
643   int nfds;
644   int i;
645
646   /* We need to know the fd used by assuan for reads.  We do this by
647      using the assumption that the first returned fd from
648      assuan_get_active_fds() is always this one.  */
649   nfds = assuan_get_active_fds (llass->assuan_ctx, 0 /* read fds */,
650                                 afdlist, DIM (afdlist));
651   if (nfds < 1)
652     return gpg_error (GPG_ERR_GENERAL); /* FIXME */
653   /* For now... */
654   for (i = 0; i < nfds; i++)
655     fdlist[i] = (int) afdlist[i];
656
657   /* We "duplicate" the file descriptor, so we can close it here (we
658      can't close fdlist[0], as that is closed by libassuan, and
659      closing it here might cause libassuan to close some unrelated FD
660      later).  Alternatively, we could special case status_fd and
661      register/unregister it manually as needed, but this increases
662      code duplication and is more complicated as we can not use the
663      close notifications etc.  A third alternative would be to let
664      Assuan know that we closed the FD, but that complicates the
665      Assuan interface.  */
666
667   llass->status_cb.fd = _gpgme_io_dup (fdlist[0]);
668   if (llass->status_cb.fd < 0)
669     return gpg_error_from_syserror ();
670
671   if (_gpgme_io_set_close_notify (llass->status_cb.fd,
672                                   close_notify_handler, llass))
673     {
674       _gpgme_io_close (llass->status_cb.fd);
675       llass->status_cb.fd = -1;
676       return gpg_error (GPG_ERR_GENERAL);
677     }
678
679   err = add_io_cb (llass, &llass->status_cb, llass_status_handler);
680   if (!err)
681     err = assuan_write_line (llass->assuan_ctx, command);
682
683   /* FIXME: If *command == '#' no answer is expected.  */
684
685   if (!err)
686     llass_io_event (llass, GPGME_EVENT_START, NULL);
687
688   return err;
689 }
690
691
692
693 static gpgme_error_t
694 llass_transact (void *engine,
695                 const char *command,
696                 gpgme_assuan_data_cb_t data_cb,
697                 void *data_cb_value,
698                 gpgme_assuan_inquire_cb_t inq_cb,
699                 void *inq_cb_value,
700                 gpgme_assuan_status_cb_t status_cb,
701                 void *status_cb_value)
702 {
703   engine_llass_t llass = engine;
704   gpgme_error_t err;
705
706   if (!llass || !command || !*command)
707     return gpg_error (GPG_ERR_INV_VALUE);
708
709   llass->user.data_cb = data_cb;
710   llass->user.data_cb_value = data_cb_value;
711   llass->user.inq_cb = inq_cb;
712   llass->user.inq_cb_value = inq_cb_value;
713   llass->user.status_cb = status_cb;
714   llass->user.status_cb_value = status_cb_value;
715
716   err = start (llass, command);
717   return err;
718 }
719
720
721
722 static void
723 llass_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs)
724 {
725   engine_llass_t llass = engine;
726   llass->io_cbs = *io_cbs;
727 }
728
729
730 static void
731 llass_io_event (void *engine, gpgme_event_io_t type, void *type_data)
732 {
733   engine_llass_t llass = engine;
734
735   TRACE3 (DEBUG_ENGINE, "gpgme:llass_io_event", llass,
736           "event %p, type %d, type_data %p",
737           llass->io_cbs.event, type, type_data);
738   if (llass->io_cbs.event)
739     (*llass->io_cbs.event) (llass->io_cbs.event_priv, type, type_data);
740 }
741
742
743 struct engine_ops _gpgme_engine_ops_assuan =
744   {
745     /* Static functions.  */
746     _gpgme_get_default_agent_socket,
747     llass_get_home_dir,
748     llass_get_version,
749     llass_get_req_version,
750     llass_new,
751
752     /* Member functions.  */
753     llass_release,
754     NULL,               /* reset */
755     NULL,               /* set_status_handler */
756     NULL,               /* set_command_handler */
757     NULL,               /* set_colon_line_handler */
758     llass_set_locale,
759     NULL,               /* set_protocol */
760     NULL,               /* decrypt */
761     NULL,               /* decrypt_verify */
762     NULL,               /* delete */
763     NULL,               /* edit */
764     NULL,               /* encrypt */
765     NULL,               /* encrypt_sign */
766     NULL,               /* export */
767     NULL,               /* export_ext */
768     NULL,               /* genkey */
769     NULL,               /* import */
770     NULL,               /* keylist */
771     NULL,               /* keylist_ext */
772     NULL,               /* sign */
773     NULL,               /* trustlist */
774     NULL,               /* verify */
775     NULL,               /* getauditlog */
776     llass_transact,     /* opassuan_transact */
777     NULL,               /* conf_load */
778     NULL,               /* conf_save */
779     llass_set_io_cbs,
780     llass_io_event,
781     llass_cancel,
782     llass_cancel_op,
783     NULL,               /* passwd */
784     NULL,               /* set_pinentry_mode */
785     NULL                /* opspawn */
786   };