Merge lp:~schuster/mysql-proxy/remove_unix_socket into lp:mysql-proxy

Proposed by Michael Schuster
Status: Merged
Merged at revision: 1081
Proposed branch: lp:~schuster/mysql-proxy/remove_unix_socket
Merge into: lp:mysql-proxy
Diff against target: 231 lines (+130/-5)
4 files modified
src/network-address.c (+28/-1)
src/network-address.h (+1/-1)
src/network-socket.c (+1/-0)
tests/unit/t_network_socket.c (+100/-3)
To merge this branch: bzr merge lp:~schuster/mysql-proxy/remove_unix_socket
Reviewer Review Type Date Requested Status
Jan Kneschke (community) Approve
Review via email: mp+23652@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

Am 19.04.2010 um 08:48 schrieb Michael Schuster:

> Michael Schuster has proposed merging lp:~schuster/mysql-proxy/remove_unix_socket into lp:mysql-proxy.
>
> Requested reviews:
> MySQL Proxy Developers (mysql-proxy-developers)
>
> --
> https://code.launchpad.net/~schuster/mysql-proxy/remove_unix_socket/+merge/23652
> Your team MySQL Proxy Developers is subscribed to branch lp:mysql-proxy.
> === modified file 'src/network-address.c'
> --- src/network-address.c 2010-04-07 12:44:32 +0000
> +++ src/network-address.c 2010-04-19 06:48:19 +0000
> @@ -45,6 +45,8 @@
> #include <string.h>
> #include <errno.h>
> #include <fcntl.h>
> +#include <glib.h>
> +#include <glib/gstdio.h>
>
> #include "network-address.h"
> #include "glib-ext.h"
> @@ -63,10 +65,33 @@
> }
>
> void network_address_free(network_address *addr) {
> +
> if (!addr) return;
>
> +#ifndef WIN32
> + /*
> + * if the name we're freeing starts with a '/', we're
> + * looking at a unix socket which needs to be removed
> + */
> + if (addr->fail_errno == 0 && addr->name != NULL &&
> + addr->name->str != NULL) {
> + gchar *name;
> + int ret;
> +
> + name = addr->name->str;
> + if (name[0] == '/' && g_access(name, 0) == 0) {
> + ret = g_remove(name);

Only try to remove it if we can to hide the error-msg (aka noise) for it ?

> +
> + if (ret == 0)
> + g_debug("%s removing socket %s successful",
> + G_STRLOC, name);
> + else
> + g_debug("%s removing socket %s failed: %s (%d)",
> + G_STRLOC, name, strerror(errno));

... in that case the g_debug() is perhaps better a g_critical() ?

> + }
> + }
> +#endif /* WIN32 */
> +
> g_string_free(addr->name, TRUE);
> -
> g_free(addr);
> }
>
>
> === modified file 'src/network-address.h'
> --- src/network-address.h 2010-04-06 14:26:51 +0000
> +++ src/network-address.h 2010-04-19 06:48:19 +0000
> @@ -63,8 +63,8 @@
> } addr;
>
> GString *name;
> -
> network_socklen_t len;
> + int fail_errno; /* != 0: don't remove ... I guess */
> } network_address;
>
> NETWORK_API network_address *network_address_new(void);
>
> === modified file 'src/network-socket.c'
> --- src/network-socket.c 2010-04-06 14:26:51 +0000
> +++ src/network-socket.c 2010-04-19 06:48:19 +0000
> @@ -397,6 +397,7 @@
> G_STRLOC,
> con->dst->name->str,
> g_strerror(errno), errno);
> + con->dst->fail_errno = errno; /* if a unix socket, this tells us "don't remove" */
> return NETWORK_SOCKET_ERROR;
> }
>
>

Revision history for this message
Michael Schuster (schuster) wrote :

On 04/19/10 09:39, Jan Kneschke wrote:
> Am 19.04.2010 um 08:48 schrieb Michael Schuster:
>
>> Michael Schuster has proposed merging lp:~schuster/mysql-proxy/remove_unix_socket into lp:mysql-proxy.
>>
>> Requested reviews:
>> MySQL Proxy Developers (mysql-proxy-developers)
>>
>> --
>> https://code.launchpad.net/~schuster/mysql-proxy/remove_unix_socket/+merge/23652
>> Your team MySQL Proxy Developers is subscribed to branch lp:mysql-proxy.
>> === modified file 'src/network-address.c'
>> --- src/network-address.c 2010-04-07 12:44:32 +0000
>> +++ src/network-address.c 2010-04-19 06:48:19 +0000
>> @@ -45,6 +45,8 @@
>> #include <string.h>
>> #include <errno.h>
>> #include <fcntl.h>
>> +#include <glib.h>
>> +#include <glib/gstdio.h>
>>
>> #include "network-address.h"
>> #include "glib-ext.h"
>> @@ -63,10 +65,33 @@
>> }
>>
>> void network_address_free(network_address *addr) {
>> +
>> if (!addr) return;
>>
>> +#ifndef WIN32
>> + /*
>> + * if the name we're freeing starts with a '/', we're
>> + * looking at a unix socket which needs to be removed
>> + */
>> + if (addr->fail_errno == 0 && addr->name != NULL &&
>> + addr->name->str != NULL) {
>> + gchar *name;
>> + int ret;
>> +
>> + name = addr->name->str;
>> + if (name[0] == '/' && g_access(name, 0) == 0) {
>> + ret = g_remove(name);
>
> Only try to remove it if we can to hide the error-msg (aka noise) for it ?

sorry, don't understand the question :-(

>> +
>> + if (ret == 0)
>> + g_debug("%s removing socket %s successful",
>> + G_STRLOC, name);
>> + else
>> + g_debug("%s removing socket %s failed: %s (%d)",
>> + G_STRLOC, name, strerror(errno));
>
> ... in that case the g_debug() is perhaps better a g_critical() ?

ok.

Michael

1049. By Michael Schuster

change warning level for failure to remove socket

Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

Am 19.04.2010 um 10:45 schrieb Michael Schuster:

> On 04/19/10 09:39, Jan Kneschke wrote:
>> Am 19.04.2010 um 08:48 schrieb Michael Schuster:
>>
>>> Michael Schuster has proposed merging lp:~schuster/mysql-proxy/remove_unix_socket into lp:mysql-proxy.
>>>
>>> Requested reviews:
>>> MySQL Proxy Developers (mysql-proxy-developers)
>>>
>>> --
>>> https://code.launchpad.net/~schuster/mysql-proxy/remove_unix_socket/+merge/23652
>>> Your team MySQL Proxy Developers is subscribed to branch lp:mysql-proxy.
>>> === modified file 'src/network-address.c'
>>> --- src/network-address.c 2010-04-07 12:44:32 +0000
>>> +++ src/network-address.c 2010-04-19 06:48:19 +0000
>>> @@ -45,6 +45,8 @@
>>> #include <string.h>
>>> #include <errno.h>
>>> #include <fcntl.h>
>>> +#include <glib.h>
>>> +#include <glib/gstdio.h>
>>>
>>> #include "network-address.h"
>>> #include "glib-ext.h"
>>> @@ -63,10 +65,33 @@
>>> }
>>>
>>> void network_address_free(network_address *addr) {
>>> +
>>> if (!addr) return;
>>>
>>> +#ifndef WIN32
>>> + /*
>>> + * if the name we're freeing starts with a '/', we're
>>> + * looking at a unix socket which needs to be removed
>>> + */
>>> + if (addr->fail_errno == 0 && addr->name != NULL &&
>>> + addr->name->str != NULL) {
>>> + gchar *name;
>>> + int ret;
>>> +
>>> + name = addr->name->str;
>>> + if (name[0] == '/' && g_access(name, 0) == 0) {
>>> + ret = g_remove(name);
>>
>> Only try to remove it if we can to hide the error-msg (aka noise) for it ?
>
> sorry, don't understand the question :-(

I tried to figure out the reason for the g_access() and assumed it is to check if the g_remove() could succeed without a permission problem.

Problem with access() is that it creates a race-condition before the the permission check and the g_remove(). Instead just unlink() it and check for the 'errno != EPERM' to see if we can't delete the socket as we drop the privileges.

>>> +
>>> + if (ret == 0)
>>> + g_debug("%s removing socket %s successful",
>>> + G_STRLOC, name);
>>> + else
>>> + g_debug("%s removing socket %s failed: %s (%d)",
>>> + G_STRLOC, name, strerror(errno));
>>
>> ... in that case the g_debug() is perhaps better a g_critical() ?
>
> ok.

The whole patch does unlink() any file that is set as address. In the case of a network_socket_new() + network_address_set_address() + network_socket_connect() + network_socket_free() the socket-file should not be unlinked as it we didn't listen() on it.

Instead of ->fail_errno use a gboolean to track if you want to unlink the socket file on free and only set it to true in the case of a successful network_socket_bind().

> Michael
> --
> https://code.launchpad.net/~schuster/mysql-proxy/remove_unix_socket/+merge/23652
> Your team MySQL Proxy Developers is subscribed to branch lp:mysql-proxy.

Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

A test-case to verify that we don't unlink() socket-files we havn't bound

=== modified file 'tests/unit/t_network_address.c'
--- tests/unit/t_network_address.c revid:michael.schuster@oracle.com-20100423113047-u7cepgr9b32hgxag
+++ tests/unit/t_network_address.c 2010-04-28 07:07:30 +0000
@@ -21,9 +21,14 @@
21#include <stdio.h>21#include <stdio.h>
22#include <stdlib.h>22#include <stdlib.h>
23#include <string.h>23#include <string.h>
24#include <fcntl.h>
2425
25#include <glib.h>26#include <glib.h>
2627
28#ifndef _WIN32
29#include <unistd.h>
30#endif
31
27#include "network-socket.h"32#include "network-socket.h"
2833
29#if GLIB_CHECK_VERSION(2, 16, 0)34#if GLIB_CHECK_VERSION(2, 16, 0)
@@ -80,6 +85,38 @@
80 network_address_free(addr);85 network_address_free(addr);
81}86}
8287
88/**
89 * test if we decode the port number correctly
90 */
91void t_network_address_free_unix_socket() {
92 network_address *addr;
93 int fd;
94
95#define TESTFILE "/tmp/mysql-proxy-t_network_address_free_unix_socket.sock"
96 unlink(TESTFILE); /* remove the file if a previous test-run failed */
97
98 /* create the test-file */
99 fd = open(TESTFILE, O_RDONLY | O_CREAT | O_EXCL, 0600);
100 if (fd != -1) close(fd);
101
102 g_assert_cmpint(fd, >, 0); /* creating the file should have succeeded */
103
104 addr = network_address_new();
105 network_address_set_address(addr, TESTFILE);
106
107 network_address_free(addr); /* the free shouldn't remove the file as we didn't call bind() on it */
108
109 /* file should still be there */
110 fd = open(TESTFILE, O_RDONLY);
111 if (fd != -1) close(fd);
112
113 g_assert_cmpint(fd, >, 0);
114
115 unlink(TESTFILE); /* cleanup */
116#undef TESTFILE
117}
118
119
83120
84int main(int argc, char **argv) {121int main(int argc, char **argv) {
85 g_test_init(&argc, &argv, NULL);122 g_test_init(&argc, &argv, NULL);
@@ -88,6 +125,7 @@
88 g_test_add_func("/core/network_address_new", t_network_address_new);125 g_test_add_func("/core/network_address_new", t_network_address_new);
89 g_test_add_func("/core/network_address_set", t_network_address_set);126 g_test_add_func("/core/network_address_set", t_network_address_set);
90 g_test_add_func("/core/network_address_resolve", t_network_address_resolve);127 g_test_add_func("/core/network_address_resolve", t_network_address_resolve);
128 g_test_add_func("/core/network_address_free_unix_socket", t_network_address_free_unix_socket);
91129
92 return g_test_run();130 return g_test_run();
93}131}
1050. By Michael Schuster

remove unix socket at shutdown: remove race window, make logic for "remove the once we created only" better

1051. By Michael Schuster

add check for local socket removal to "is local" testcase

1052. By Michael Schuster

make sure socket is removed if assertion fails: add sig handler for SIGARBT

Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

* remove the commented lines:

  /* printf("debug: socket name is %s (addr: %p)\n", pp->sockname, pp); */
  /* g_test_add_func("/core/network_socket_is_local_unix", t_network_socket_is_local_unix);*/

* stay with the existing coding style (no line-break after function declarations)
* The signal-handler for SIGABRT is installed for _all_ tests of this test-suite. Instead it should be added in the setup and turned back into SIG_DFL in teardown.
* void t_network_socket_is_local_unix() is about the ..._is_local() function. Comments are missing to describe that it also tests for 'socket is removed after successful bind() only'. It would have been better to have seperate test that only tests that.

Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

g_debug("%s removing socket %s successful", ... is missing a colon after the first %s

Revision history for this message
Jan Kneschke (jan-kneschke) :
review: Needs Fixing
Revision history for this message
Michael Schuster (schuster) wrote :

Jan,

thx for review, comments in-line.

On 04.05.10 13:50, Jan Kneschke wrote:
> * remove the commented lines:
>
> /* printf("debug: socket name is %s (addr: %p)\n", pp->sockname, pp); */
> /* g_test_add_func("/core/network_socket_is_local_unix", t_network_socket_is_local_unix);*/

done

> * stay with the existing coding style (no line-break after function declarations)

ack, done.

> * The signal-handler for SIGABRT is installed for _all_ tests of this test-suite. Instead it should be added in the setup and turned back into SIG_DFL in teardown.

done

> * void t_network_socket_is_local_unix() is about the ..._is_local() function. Comments are missing to describe that it also tests for 'socket is removed after successful bind() only'. It would have been better to have seperate test that only tests that.

ack

> g_debug("%s removing socket %s successful", ... is missing a colon after the first %s

this is now gone.

Michael
--
Michael Schuster http://blogs.sun.com/recursion
Recursion, n.: see 'Recursion'

1053. By Michael Schuster

fix diversion

Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

approved. will merge it to trunk

review: Approve
Revision history for this message
Jan Kneschke (jan-kneschke) wrote :

65 === modified file 'src/network-socket.c'
66 --- src/network-socket.c 2010-05-03 14:26:32 +0000
67 +++ src/network-socket.c 2010-05-10 11:52:26 +0000
...

In static network_socket_retval_t network_socket_write_send(network_socket *con, int send_chunks) is

76 @@ -660,6 +661,7 @@
77 }
78 }
79
80 + con->dst->can_unlink_socket = TRUE;
81 return NETWORK_SOCKET_SUCCESS;
82 }

which would mean:

"unlink socket after successful write" which isn't what we want. Seems to a broken merge. The rest looks good.

review: Needs Fixing
1054. By Michael Schuster

mismerge fixed

Revision history for this message
Jan Kneschke (jan-kneschke) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/network-address.c'
--- src/network-address.c 2010-04-07 12:44:32 +0000
+++ src/network-address.c 2010-05-10 12:15:45 +0000
@@ -45,6 +45,8 @@
45#include <string.h>45#include <string.h>
46#include <errno.h>46#include <errno.h>
47#include <fcntl.h>47#include <fcntl.h>
48#include <glib.h>
49#include <glib/gstdio.h>
4850
49#include "network-address.h"51#include "network-address.h"
50#include "glib-ext.h"52#include "glib-ext.h"
@@ -63,10 +65,35 @@
63}65}
6466
65void network_address_free(network_address *addr) {67void network_address_free(network_address *addr) {
68
66 if (!addr) return;69 if (!addr) return;
6770
71#ifndef WIN32
72 /*
73 * if the name we're freeing starts with a '/', we're
74 * looking at a unix socket which needs to be removed
75 */
76 if (addr->can_unlink_socket == TRUE && addr->name != NULL &&
77 addr->name->str != NULL) {
78 gchar *name;
79 int ret;
80
81 name = addr->name->str;
82 if (name[0] == '/') {
83 ret = g_remove(name);
84 if (ret == 0) {
85 g_debug("%s: removing socket %s successful",
86 G_STRLOC, name);
87 } else {
88 if (errno != EPERM && errno != EACCES)
89 g_critical("%s: removing socket %s failed: %s (%d)",
90 G_STRLOC, name, strerror(errno));
91 }
92 }
93 }
94#endif /* WIN32 */
95
68 g_string_free(addr->name, TRUE);96 g_string_free(addr->name, TRUE);
69
70 g_free(addr);97 g_free(addr);
71}98}
7299
73100
=== modified file 'src/network-address.h'
--- src/network-address.h 2010-04-06 14:26:51 +0000
+++ src/network-address.h 2010-05-10 12:15:45 +0000
@@ -63,8 +63,8 @@
63 } addr;63 } addr;
6464
65 GString *name; 65 GString *name;
66
67 network_socklen_t len;66 network_socklen_t len;
67 gboolean can_unlink_socket; /* set TRUE *only* after successful bind */
68} network_address;68} network_address;
6969
70NETWORK_API network_address *network_address_new(void);70NETWORK_API network_address *network_address_new(void);
7171
=== modified file 'src/network-socket.c'
--- src/network-socket.c 2010-05-03 14:26:32 +0000
+++ src/network-socket.c 2010-05-10 12:15:45 +0000
@@ -431,6 +431,7 @@
431 }431 }
432 }432 }
433433
434 con->dst->can_unlink_socket = TRUE;
434 return NETWORK_SOCKET_SUCCESS;435 return NETWORK_SOCKET_SUCCESS;
435}436}
436437
437438
=== modified file 'tests/unit/t_network_socket.c'
--- tests/unit/t_network_socket.c 2010-04-07 12:44:32 +0000
+++ tests/unit/t_network_socket.c 2010-05-10 12:15:45 +0000
@@ -23,6 +23,12 @@
23#include <string.h>23#include <string.h>
24#include <errno.h>24#include <errno.h>
2525
26#ifndef WIN32
27#include <unistd.h>
28#include <stdlib.h>
29#include <fcntl.h>
30#endif /* WIN32 */
31
26#include <glib.h>32#include <glib.h>
27#include <glib/gstdio.h> /* for g_unlink */33#include <glib/gstdio.h> /* for g_unlink */
2834
@@ -31,6 +37,17 @@
31#if GLIB_CHECK_VERSION(2, 16, 0)37#if GLIB_CHECK_VERSION(2, 16, 0)
32#define C(x) x, sizeof(x) - 138#define C(x) x, sizeof(x) - 1
3339
40#ifndef WIN32
41#define LOCAL_SOCK "/tmp/mysql-proxy-test.socket"
42
43typedef struct {
44 char sockname[sizeof(LOCAL_SOCK) + 10];
45} local_unix_t;
46
47static local_unix_t *pp = NULL;
48static local_unix_t local_test_arg;
49#endif /* WIN32 */
50
34void test_network_socket_new() {51void test_network_socket_new() {
35 network_socket *sock;52 network_socket *sock;
3653
@@ -295,6 +312,8 @@
295 network_socket_free(server);312 network_socket_free(server);
296}313}
297314
315#ifndef WIN32
316
298/**317/**
299 * test if _is_local() works on unix-domain sockets318 * test if _is_local() works on unix-domain sockets
300 *319 *
@@ -314,6 +333,7 @@
314 c_sock = network_socket_new();333 c_sock = network_socket_new();
315 network_address_set_address(c_sock->dst, "/tmp/mysql-proxy-test.socket");334 network_address_set_address(c_sock->dst, "/tmp/mysql-proxy-test.socket");
316335
336
317 /* hack together a network_socket_accept() which we don't have in this tree yet */337 /* hack together a network_socket_accept() which we don't have in this tree yet */
318 g_assert_cmpint(NETWORK_SOCKET_SUCCESS, ==, network_socket_bind(s_sock));338 g_assert_cmpint(NETWORK_SOCKET_SUCCESS, ==, network_socket_bind(s_sock));
319339
@@ -331,6 +351,76 @@
331 g_unlink("/tmp/mysql-proxy-test.socket");351 g_unlink("/tmp/mysql-proxy-test.socket");
332}352}
333353
354void t_network_localsocket_setup(local_unix_t *p) {
355 g_assert(p != NULL);
356 snprintf(p->sockname, sizeof(p->sockname), LOCAL_SOCK ".%d", (int)getpid());
357 pp = p;
358}
359
360void t_network_localsocket_teardown(local_unix_t *p) {
361 g_assert(p != NULL);
362 if (p->sockname[0] != '\0') {
363 (void) g_unlink(p->sockname);
364 p->sockname[0] = '\0';
365 }
366 pp = NULL;
367}
368
369void exitfunc(int sig) {
370 if (pp != NULL && pp->sockname[0] != '\0')
371 (void) g_unlink(pp->sockname);
372
373 abort();
374}
375
376/**
377 * test if local sockets are removed at shutdown
378 * this is an extension of _is_local_unix(), therefore looks much like it;
379 * just in case, we leave all tests from that function in as well.
380 */
381void t_network_socket_rem_local_unix(local_unix_t *p) {
382 network_socket *s_sock; /* the server side socket, listening for requests */
383 network_socket *c_sock; /* the client side socket, that connects */
384 network_socket *a_sock; /* the server side, accepted socket */
385
386 /*
387 * if an assertion fails, we receive the ABORT signal. We need to
388 * close the unix socket in that case too. The regular teardown function
389 * is not called in this case, so we install our own handler.
390 */
391 signal(SIGABRT, exitfunc);
392 g_test_bug("42220");
393 g_log_set_always_fatal(G_LOG_FATAL_MASK); /* gtest modifies the fatal-mask */
394
395 s_sock = network_socket_new();
396 network_address_set_address(s_sock->dst, p->sockname);
397
398 c_sock = network_socket_new();
399 network_address_set_address(c_sock->dst, p->sockname);
400
401 g_assert_cmpint(NETWORK_SOCKET_SUCCESS, ==, network_socket_bind(s_sock));
402
403 g_assert_cmpint(g_access(p->sockname, 0), ==, 0);
404
405 g_assert_cmpint(NETWORK_SOCKET_SUCCESS, ==, network_socket_connect(c_sock));
406
407 a_sock = network_socket_accept(s_sock);
408 g_assert(a_sock);
409
410 g_assert_cmpint(TRUE, ==, network_address_is_local(s_sock->dst, a_sock->dst));
411
412 network_socket_free(a_sock);
413 g_assert_cmpint(g_access(p->sockname, 0), ==, 0);
414 network_socket_free(c_sock);
415 g_assert_cmpint(g_access(p->sockname, 0), ==, 0);
416 network_socket_free(s_sock);
417 g_assert_cmpint(g_access(p->sockname, 0), ==, -1);
418
419 /* re-establish default signal disposition */
420 signal(SIGABRT, SIG_DFL);
421}
422
423#endif /* WIN32 */
334424
335int main(int argc, char **argv) {425int main(int argc, char **argv) {
336 g_test_init(&argc, &argv, NULL);426 g_test_init(&argc, &argv, NULL);
@@ -342,7 +432,14 @@
342 g_test_add_func("/core/network_queue_append", test_network_queue_append);432 g_test_add_func("/core/network_queue_append", test_network_queue_append);
343 g_test_add_func("/core/network_queue_peek_string", test_network_queue_peek_string);433 g_test_add_func("/core/network_queue_peek_string", test_network_queue_peek_string);
344 g_test_add_func("/core/network_queue_pop_string", test_network_queue_pop_string);434 g_test_add_func("/core/network_queue_pop_string", test_network_queue_pop_string);
345 g_test_add_func("/core/network_socket_is_local_unix", t_network_socket_is_local_unix);435#ifndef WIN32
436 g_test_add_func("/core/network_socket_is_local_unix",t_network_socket_is_local_unix);
437
438 g_test_add("/core/network_socket_rem_local_unix", local_unix_t,
439 &local_test_arg,
440 t_network_localsocket_setup, t_network_socket_rem_local_unix,
441 t_network_localsocket_teardown);
442#endif /* WIN32 */
346#if 0443#if 0
347 /**444 /**
348 * disabled for now until we fixed the _to_read() on HP/UX and AIX (and MacOS X)445 * disabled for now until we fixed the _to_read() on HP/UX and AIX (and MacOS X)
@@ -354,8 +451,8 @@
354451
355 return g_test_run();452 return g_test_run();
356}453}
357#else454#else /* GLIB_CHECK_VERSION */
358int main() {455int main() {
359 return 77;456 return 77;
360}457}
361#endif458#endif /* GLIB_CHECK_VERSION */

Subscribers

People subscribed via source and target branches