source: roaraudio/libroar/basic.c @ 5369:dfc6cbfe8025

Last change on this file since 5369:dfc6cbfe8025 was 5369:dfc6cbfe8025, checked in by phi, 12 years ago

moved +fork out of socket.c into basic.c, now also support +fork=d:daemon and +fork=!daemon_command

File size: 16.0 KB
RevLine 
[0]1//basic.c:
2
[690]3/*
[4708]4 *      Copyright (C) Philipp 'ph3-der-loewe' Schafft - 2008-2011
[690]5 *
6 *  This file is part of libroar a part of RoarAudio,
7 *  a cross-platform sound system for both, home and professional use.
8 *  See README for details.
9 *
10 *  This file is free software; you can redistribute it and/or modify
11 *  it under the terms of the GNU General Public License version 3
12 *  as published by the Free Software Foundation.
13 *
14 *  libroar is distributed in the hope that it will be useful,
15 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
16 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 *  GNU General Public License for more details.
18 *
19 *  You should have received a copy of the GNU General Public License
20 *  along with this software; see the file COPYING.  If not, write to
[3517]21 *  the Free Software Foundation, 51 Franklin Street, Fifth Floor,
22 *  Boston, MA 02110-1301, USA.
[690]23 *
24 *  NOTE for everyone want's to change something and send patches:
25 *  read README and HACKING! There a addition information on
26 *  the license of this document you need to read before you send
27 *  any patches.
28 *
29 *  NOTE for uses of non-GPL (LGPL,...) software using libesd, libartsc
30 *  or libpulse*:
31 *  The libs libroaresd, libroararts and libroarpulse link this lib
32 *  and are therefore GPL. Because of this it may be illigal to use
33 *  them with any software that uses libesd, libartsc or libpulse*.
34 */
35
[0]36#include "libroar.h"
37
[5369]38static int _start_server(struct roar_connection * con, const char * server, int type, int flags, uint_least32_t timeout) {
39#if !defined(ROAR_TARGET_WIN32) && !defined(ROAR_TARGET_MICROCONTROLLER)
40 enum {
41  NORMAL = 0,
42  SYSTEM = 1
43 } mode = NORMAL;
44 const char * daemonimage = NULL;
45 int socks[2];
46 int r;
47 char fhstr[12];
48
49 if ( !strncmp(server, "+fork=", 6) ) {
50  server += 6;
51  if ( server[0] == 0 ) {
52   // no special case, we just ignore it.
53  } else if ( server[0] == 'd' && server[1] == ':' ) {
54   server += 2;
55   daemonimage = server;
56  } else if ( server[0] == '!' ) {
57   server += 1;
58   daemonimage = server;
59   mode = SYSTEM;
60  } else {
61   roar_err_set(ROAR_ERROR_ILLSEQ);
62   return -1;
63  }
64 }
65
66 // TODO: FIXME: we should move this into the config structure.
67 if ( daemonimage == NULL )
68  daemonimage = getenv("ROAR_DAEMONIMAGE");
69
70 if ( daemonimage == NULL || *daemonimage == 0 )
71  daemonimage = "roard";
72
73 if ( socketpair(AF_UNIX, SOCK_STREAM, 0, socks) == -1 ) {
74  roar_err_from_errno();
75  return -1;
76 }
77
78 r = fork();
79
80 if ( r == -1 ) { // error!
81  roar_err_from_errno();
82  ROAR_ERR("roar_socket_open_fork(*): Can not fork: %s", strerror(errno));
83  close(socks[0]);
84  close(socks[1]);
85  return -1;
86 } else if ( r == 0 ) { // we are the child
87  close(socks[0]);
88
89  close(ROAR_STDIN ); // we do not want roard to have any standard input
90  close(ROAR_STDOUT); // STDOUT is also not needed, so we close it,
91                      // but STDERR we keep open for error messages.
92
93  snprintf(fhstr, sizeof(fhstr), "%i", socks[1]);
94
95  switch (mode) {
96   case NORMAL:
97     execlp(daemonimage, daemonimage, "--no-listen", "--client-fh", fhstr, (_LIBROAR_GOOD_CAST char*)NULL);
98    break;
99   case SYSTEM:
100     dup2(socks[1], ROAR_STDIN );
101     dup2(socks[1], ROAR_STDOUT);
102     execl("/bin/sh", "/bin/sh", "-c", daemonimage, (_LIBROAR_GOOD_CAST char*)NULL);
103     execlp("sh", "sh", "-c", daemonimage, (_LIBROAR_GOOD_CAST char*)NULL);
104    break;
105  }
106
107  // we are still alive?
108  ROAR_ERR("roar_socket_open_fork(*): alive after exec(), that's bad!");
109  _exit(1);
110 } else { // we are the parent
111  close(socks[1]);
112  if ( roar_vio_open_fh_socket(con->viocon, socks[0]) == -1 ) {
113   close(socks[0]);
114   return -1;
115  } else {
116   con->flags |= ROAR_CON_FLAGS_VIO;
117  }
118  return 0;
119 }
120
121 return -1;
122#else
123 ROAR_ERR("roar_socket_open_fork(*): There is no UNIX Domain Socket support in win32, download a real OS.");
124 return -1;
125#endif
126}
127
[5368]128static int _connect_server(struct roar_connection * con, const char * server, int type, int flags, uint_least32_t timeout) {
129#if defined(ROAR_HAVE_STAT) && defined(ROAR_HAVE_H_SYS_STAT)
130 struct stat sockstat;
131#endif
132 const char * obj = NULL;
133 char user_sock[128];
134 int is_decnet = 0;
135 int port = 0;
136 int i = 0;
137 int fh = -1;
138 int err;
139
140 if ( con == NULL || server == NULL ) {
141  roar_err_set(ROAR_ERROR_FAULT);
142  return -1;
143 }
144
145 if ( !strcmp(server, "+invalid") ) {
146  roar_err_set(ROAR_ERROR_CANCELED);
147  return -1;
148 } else if ( !strncmp(server, "+dstr=", 6) ) {
149  if ( roar_vio_open_dstr_simple(con->viocon, server+6, ROAR_VIOF_READWRITE) == -1 )
150   return -1;
151  con->flags |= ROAR_CON_FLAGS_VIO;
152  return 0;
[5369]153 } else if ( !strcmp(server, "+fork") || !strncmp(server, "+fork=", 6) ) {
154  return _start_server(con, server, type, flags, timeout);
[5368]155 }
156
157 strncpy(user_sock, server, sizeof(user_sock)-1);
158 user_sock[sizeof(user_sock)-1] = 0;
159
160
161 if ( *user_sock != '/' ) { // don't test AF_UNIX sockets for ports
162  for (i = 0; user_sock[i] != 0; i++) {
163   if ( user_sock[i] == ':' ) {
164    if ( user_sock[i+1] == ':' ) { // DECnet, leave unchanged
165     is_decnet = 1;
166     obj = &user_sock[i+2];
167     break;
168    }
169
170    port = atoi(&(user_sock[i+1]));
171    user_sock[i] = 0;
172    break;
173   }
174  }
175 }
176
177 if ( is_decnet ) {
178  if ( *user_sock == ':' ) {
179   if ( roar_socket_get_local_nodename() != NULL ) {
180    strncpy(user_sock, roar_socket_get_local_nodename(), sizeof(user_sock)-1);
181    user_sock[sizeof(user_sock)-1] = 0;
182    roar_mm_strlcat(user_sock, server, sizeof(user_sock)-1);
183    user_sock[sizeof(user_sock)-1] = 0;
184    obj  = strstr(user_sock, "::");
185    obj += 2;
186   }
187  }
188
189  if ( *obj == 0 ) {
190#ifdef DN_MAXOBJL
191   roar_mm_strlcat(user_sock, ROAR_DEFAULT_OBJECT, sizeof(user_sock)-1);
192   user_sock[sizeof(user_sock)-1] = 0;
193#else
194   ROAR_ERR("roar_connect_raw(*): size of DECnet object unknown.");
195#endif
196  }
197   ROAR_DBG("roar_connect_raw(*): user_sock='%s'", user_sock);
198 }
199
200 if ( port || is_decnet ) {
201  fh = roar_socket_connect(user_sock, port);
202  // restore the original string
203  user_sock[i] = ':';
204 } else {
205#if defined(ROAR_HAVE_STAT) && defined(ROAR_HAVE_H_SYS_STAT)
206  if ( user_sock[0] == '/' ) {
207   if ( stat(user_sock, &sockstat) == 0 ) {
208    if ( S_ISCHR(sockstat.st_mode) ) {
209     return open(user_sock, O_RDWR|O_NOCTTY, 0666);
210    }
211   }
212  }
213#endif
214  fh = roar_socket_connect(user_sock, ROAR_DEFAULT_PORT);
215 }
216
217 if ( fh == -1 )
218  return -1;
219
220 if ( roar_vio_open_fh_socket(con->viocon, fh) == -1 ) {
221  err = roar_error;
222#ifdef ROAR_TARGET_WIN32
223  closesocket(fh);
224#else
225  close(fh);
226#endif
227  roar_error = err;
228  return -1;
229 } else {
230  con->flags |= ROAR_CON_FLAGS_VIO;
231 }
232
233 roar_err_set(ROAR_ERROR_NONE);
234 return 0;
235}
236
237static int roar_connect_raw (struct roar_connection * con, const char * server, int flags, uint_least32_t timeout) {
[4780]238#ifdef ROAR_HAVE_LIBSLP
[4653]239 struct roar_libroar_config * config = roar_libroar_get_config();
[4780]240#endif
[5114]241 char user_sock[128];
[0]242 char * roar_server;
[701]243 int i = 0;
[1475]244#if !defined(ROAR_TARGET_WIN32) && !defined(ROAR_TARGET_MICROCONTROLLER)
[1026]245 struct passwd * pwd;
[1393]246#endif
[828]247#ifdef ROAR_HAVE_LIBDNET
248 struct stat decnet_stat;
249#endif
[3372]250#ifdef ROAR_HAVE_LIBX11
251 struct roar_x11_connection * x11con;
252#endif
[4945]253#ifdef ROAR_HAVE_LIBSLP
[4806]254 struct roar_server * list;
255 int workarounds_store;
[4945]256#endif
[0]257
[4873]258 roar_err_set(ROAR_ERROR_UNKNOWN);
[807]259
[4806]260 if ( timeout != 0 ) {
[4873]261  roar_err_set(ROAR_ERROR_INVAL);
[4806]262  return -1;
263 }
264
265 if ( flags & ROAR_ENUM_FLAG_HARDNONBLOCK )
266  flags |= ROAR_ENUM_FLAG_NONBLOCK;
267
268
[5368]269 if ( server == NULL || *server == 0 )
[2567]270  server = roar_libroar_get_server();
271
[5368]272 if ( server == NULL || *server == 0 )
273  server = getenv("ROAR_SERVER");
[0]274
[3372]275#ifdef ROAR_HAVE_LIBX11
[5368]276 if ( server == NULL || *server == 0 ) {
[3372]277  if ( (x11con = roar_x11_connect(NULL)) != NULL ) {
278   server = roar_x11_get_prop(x11con, "ROAR_SERVER");
279   roar_x11_disconnect(x11con);
280  }
281 }
282#endif
283
[1436]284#if !defined(ROAR_TARGET_WIN32) && !defined(ROAR_TARGET_MICROCONTROLLER)
[5368]285 if ( (server == NULL || *server == 0) && (i = readlink("/etc/roarserver", user_sock, sizeof(user_sock)-1)) != -1 ) {
[448]286   user_sock[i] = 0;
287   server = user_sock;
288 }
[1093]289#endif
[448]290
[3077]291 if ( server != NULL && !strcasecmp(server, "+slp") ) {
[3076]292  server = roar_slp_find_roard(0);
293  if ( server == NULL ) {
294   return -1;
295  }
296 }
297
[61]298 if ( server == NULL || *server == 0 ) {
[0]299  /* connect via defaults */
300
[1764]301#ifdef ROAR_HAVE_UNIX
[1436]302#ifndef ROAR_TARGET_MICROCONTROLLER
[1026]303  roar_server = getenv("HOME");
[1435]304#else
305  roar_server = NULL;
306#endif
[1026]307
308  if ( roar_server == NULL ) {
[1436]309#if !defined(ROAR_TARGET_WIN32) && !defined(ROAR_TARGET_MICROCONTROLLER)
[1026]310   if ( (pwd = getpwuid(getuid())) == NULL ) {
311    roar_server = "/NX-HOME-DIR";
312   } else {
313    roar_server = pwd->pw_dir;
314   }
[1078]315#else
316   roar_server = "/WIN32-SUCKS";
317#endif
[1026]318  }
319
[5114]320  snprintf(user_sock, sizeof(user_sock)-1, "%s/%s", roar_server, ROAR_DEFAULT_SOCK_USER);
321  user_sock[sizeof(user_sock)-1] = 0;
[0]322
[5368]323  if ( _connect_server(con, user_sock, ROAR_SOCKET_TYPE_UNIX, flags, timeout) == 0 )
324   return 0;
[0]325
[5368]326  if ( _connect_server(con, ROAR_DEFAULT_SOCK_GLOBAL, ROAR_SOCKET_TYPE_UNIX, flags, timeout) == 0 )
327   return 0;
[1764]328#endif
[237]329
[5368]330  if ( _connect_server(con, ROAR_DEFAULT_HOSTPORT, ROAR_SOCKET_TYPE_TCP, flags, timeout) == 0 )
331   return 0;
[0]332
[828]333#ifdef ROAR_HAVE_LIBDNET
334  if ( stat(ROAR_PROC_NET_DECNET, &decnet_stat) == 0 ) {
335   if ( roar_socket_get_local_nodename() ) {
336    snprintf(user_sock, 79, "%s::%s", roar_socket_get_local_nodename(), ROAR_DEFAULT_OBJECT);
[5368]337    if ( _connect_server(con, user_sock, ROAR_SOCKET_TYPE_DECNET, flags, timeout) == 0 )
338     return 0;
[828]339   }
[522]340  }
[828]341#endif
[2007]342
[5368]343  if ( _connect_server(con, "+abstract", -1, flags, timeout) == 0 )
344   return 0;
[4109]345
[2007]346#ifdef ROAR_HAVE_LIBSLP
[4806]347 if ( !(config->workaround.workarounds & ROAR_LIBROAR_CONFIG_WAS_NO_SLP) &&
348      !(flags & ROAR_ENUM_FLAG_NONBLOCK)
349    ) {
350  if ( (server = roar_slp_find_roard(0)) != NULL ) {
[5368]351   if ( _connect_server(con, server, -1, 0, 0) == 0 )
352    return 0;
[2014]353
[4806]354   /* in case we can not connect to the server given this may be a cache problem,
355      we do a new lookup with the cache disabled in this case                     */
[5237]356   ROAR_WARN("roar_connect_raw(*): Can not connect to SLP located server, disabling cache");
[4806]357   if ( (server = roar_slp_find_roard(1)) != NULL )
[5368]358    if ( _connect_server(con, server, -1, 0, 0) == 0 )
359     return 0;
[4806]360  }
361 }
362
363 workarounds_store = config->workaround.workarounds;
364 config->workaround.workarounds |= ROAR_LIBROAR_CONFIG_WAS_NO_SLP;
365 list = roar_enum_servers(flags, -1, -1);
366 config->workaround.workarounds = workarounds_store;
367 if ( list != NULL ) {
368  for (i = 0; list[i].server != NULL; i++) {
[5368]369   if ( _connect_server(con, list[i].server, -1, 0, 0) == 0 ) {
[4806]370    roar_enum_servers_free(list);
[5368]371    return 0;
[4806]372   }
373  }
374  roar_enum_servers_free(list);
[4653]375 }
[2007]376#endif
[522]377
[2014]378 return -1;
379
[0]380 } else {
381  /* connect via (char*)server */
[5369]382  if ( _connect_server(con, server, -1, flags, timeout) != 0 )
383   return -1;
384  return 0;
[0]385 }
386
[5368]387 roar_err_set(ROAR_ERROR_NODEV);
388 ROAR_DBG("roar_connect_raw(*) = -1 // error=NODEV");
389 return -1;
[0]390}
391
[5237]392int roar_connect     (struct roar_connection * con, const char * server, int flags, uint_least32_t timeout) {
[1315]393 if ( con == NULL ) {
[4873]394  roar_err_set(ROAR_ERROR_FAULT);
[1315]395  return -1;
396 }
397
[5368]398 if ( roar_connect_none(con) == -1 )
[0]399  return -1;
400
[5368]401 roar_err_set(ROAR_ERROR_UNKNOWN);
402 if ( roar_connect_raw(con, server, flags, timeout) == -1 )
[5296]403  return -1;
404
405 if ( server != NULL ) {
406  con->server_name = roar_mm_strdup(server);
407 }
408
409 return 0;
[1315]410}
411
[5365]412int roar_connect_none (struct roar_connection * con) {
413 if ( con == NULL ) {
[4873]414  roar_err_set(ROAR_ERROR_INVAL);
[1315]415  return -1;
416 }
417
418 memset(con, 0, sizeof(struct roar_connection));
[5296]419 con->refc        = 1;
[5232]420 con->flags       = ROAR_CON_FLAGS_NONE;
421 con->version     = 0;
422 con->cb_userdata = NULL;
423 con->cb          = NULL;
[5296]424 con->server_stds = NULL;
425 con->server_name = NULL;
[5232]426
427 roar_err_init(&(con->errorframe));
[1315]428
[5296]429 con->viocon = &(con->viocon_store);
430
[5365]431// con->flags |= ROAR_CON_FLAGS_VIO;
432
433 roar_err_set(ROAR_ERROR_NONE);
434 return 0;
435}
436
437int roar_connect_vio (struct roar_connection * con, struct roar_vio_calls * vio) {
438 if ( con == NULL || vio == NULL ) {
439  roar_err_set(ROAR_ERROR_INVAL);
440  return -1;
441 }
442
443 if ( roar_connect_none(con) == -1 )
444  return -1;
445
446 con->viocon = vio;
447 con->flags |= ROAR_CON_FLAGS_VIO;
448
449 return -1;
450}
451
452int roar_connect_fh (struct roar_connection * con, int fh) {
453
454 if ( con == NULL || fh == -1 ) {
455  roar_err_set(ROAR_ERROR_INVAL);
456  return -1;
457 }
458
459 if ( roar_connect_none(con) == -1 )
460  return -1;
461
462 // specal hack to set an ilegal value used internaly in libroar:
463 if ( fh == -2 )
464  fh = -1;
465
[5296]466 if ( roar_vio_open_fh_socket(con->viocon, fh) != -1 ) {
[3869]467  con->flags |= ROAR_CON_FLAGS_VIO;
[5231]468 }
[1315]469
[4873]470 roar_err_set(ROAR_ERROR_NONE);
[0]471 return 0;
472}
473
[1660]474int roar_get_connection_fh (struct roar_connection * con) {
[5231]475 int fh;
476
477 ROAR_DBG("roar_get_connection_fh(con=%p) = ?", con);
478
[3869]479 roar_debug_warn_sysio("roar_get_connection_fh", "roar_get_connection_vio2", NULL);
[2809]480
[2043]481 if ( con == NULL )
482  return -1;
483
[5231]484 ROAR_DBG("roar_get_connection_fh(con=%p) = ?", con);
485
[5296]486 if ( roar_vio_ctl(con->viocon, ROAR_VIO_CTL_GET_FH, &fh) == -1 )
[3869]487  return -1;
488
[5231]489 ROAR_DBG("roar_get_connection_fh(con=%p) = %i", con, fh);
[3869]490
[5231]491 return fh;
[2043]492}
493
[3869]494struct roar_vio_calls * roar_get_connection_vio2 (struct roar_connection * con) {
495 if ( con == NULL )
496  return NULL;
497
498 if ( con->flags & ROAR_CON_FLAGS_VIO )
[5296]499  return con->viocon;
[3869]500
501// TODO: try to open the VIO.
502
503 return NULL;
504}
505
[5296]506const char * roar_get_connection_server(struct roar_connection * con) {
507 if ( con == NULL ) {
508  roar_err_set(ROAR_ERROR_FAULT);
509  return NULL;
510 }
511
512 return con->server_name;
513}
514
515int roar_connectionref(struct roar_connection * con) {
516 if ( con == NULL ) {
517  roar_err_set(ROAR_ERROR_FAULT);
518  return -1;
519 }
520
521 con->refc++;
522
523 return 0;
524}
525
526int roar_connectionunref(struct roar_connection * con) {
[3869]527 struct roar_vio_calls * vio;
[0]528 struct roar_message m;
529
[5296]530 if ( con == NULL ) {
531  roar_err_set(ROAR_ERROR_FAULT);
532  return -1;
533 }
534
535 con->refc--;
536
537 if ( con->refc )
538  return 0;
539
[3875]540 memset(&m, 0, sizeof(m));
541
[5144]542 m.datalen =  0;
543 m.stream  = -1;
544 m.pos     =  0;
[0]545 m.cmd     = ROAR_CMD_QUIT;
546
547 roar_req(con, &m, NULL);
548
[3869]549 if ( (vio = roar_get_connection_vio2(con)) != NULL ) {
550  roar_vio_close(vio);
[2809]551 }
[0]552
[5296]553 if ( con->server_stds != NULL ) {
554  roar_stds_free(con->server_stds);
555  con->server_stds = NULL;
556 }
[0]557
[5296]558 if ( con->server_name != NULL ) {
559  roar_mm_free(con->server_name);
560  con->server_name = NULL;
561 }
562
563 if ( con->flags & ROAR_CON_FLAGS_FREESELF ) {
564  roar_mm_free(con);
565 } else {
566  roar_connect_fh(con, -2);
567 }
[807]568
[0]569 return 0;
570}
571
[3913]572int roar_set_connection_callback(struct roar_connection * con,
573                                 void (*cb)(struct roar_connection * con,
574                                            struct roar_message    * mes,
[5231]575                                            void                   * data,
[3913]576                                            void                   * userdata),
577                                 void * userdata) {
578 if ( con == NULL )
579  return -1;
580
581 con->cb       = cb;
[5231]582 con->cb_userdata = userdata;
[3913]583
584 return 0;
585}
586
[3914]587int roar_sync         (struct roar_connection * con) {
588 // wait for any non-client reqs
589 return roar_wait_msg(con, 0x0000, 0x8000);
590}
591
592int roar_wait_msg     (struct roar_connection * con, int16_t seq, int16_t seqmask) {
[5148]593 roar_err_set(ROAR_ERROR_NOSYS);
[3914]594 return -1;
595}
[3913]596
[3836]597int roar_noop         (struct roar_connection * con) {
598 struct roar_message mes;
599
600 if ( con == NULL ) {
[5148]601  roar_err_set(ROAR_ERROR_FAULT);
[3836]602  return -1;
603 }
604
605 memset(&mes, 0, sizeof(mes));
606
607 mes.cmd = ROAR_CMD_NOOP;
[5144]608 mes.stream = -1;
[3836]609
[5146]610 return roar_req3(con, &mes, NULL);
[3836]611}
612
[5114]613int roar_identify   (struct roar_connection * con, const char * name) {
[0]614 struct roar_message mes;
[5132]615 uint32_t pid;
[0]616 int max_len;
617
[4873]618 roar_err_set(ROAR_ERROR_UNKNOWN);
[807]619
[0]620 ROAR_DBG("roar_identify(*): try to identify myself...");
621
[3875]622 memset(&mes, 0, sizeof(mes));
623
[0]624 mes.cmd    = ROAR_CMD_IDENTIFY;
[5144]625 mes.stream = -1;
626 mes.pos    =  0;
[0]627
628 ROAR_DBG("roar_identify(*): name=%p", name);
629
630 if ( name == NULL )
631  name = "libroar client";
632
633 ROAR_DBG("roar_identify(*): name=%p", name);
634
[5114]635 max_len = roar_mm_strlen(name);
[0]636 ROAR_DBG("roar_identify(*): strlen(name) = %i", max_len);
637
638 if ( max_len > (LIBROAR_BUFFER_MSGDATA - 5) )
639  max_len = LIBROAR_BUFFER_MSGDATA - 5;
640
641 mes.datalen = 5 + max_len;
642 mes.data[0] = 1;
643
644 pid = getpid();
[5132]645 mes.data[1] = (pid & 0xFF000000UL) >> 24;
646 mes.data[2] = (pid & 0x00FF0000UL) >> 16;
647 mes.data[3] = (pid & 0x0000FF00UL) >>  8;
648 mes.data[4] = (pid & 0x000000FFUL) >>  0;
649 ROAR_DBG("roar_identify(*): pid = %i", (int)pid);
[0]650
651 strncpy(mes.data+5, name, max_len);
652
[5146]653 return roar_req3(con, &mes, NULL);
[0]654}
655
656//ll
Note: See TracBrowser for help on using the repository browser.