1// Copyright Joyent, Inc. and other Node contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a
4// copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to permit
8// persons to whom the Software is furnished to do so, subject to the
9// following conditions:
10//
11// The above copyright notice and this permission notice shall be included
12// in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
20// USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22'use strict';
23const common = require('../common');
24// Skip test in FreeBSD jails.
25if (common.inFreeBSDJail)
26  common.skip('In a FreeBSD jail');
27
28const assert = require('assert');
29const dgram = require('dgram');
30const fork = require('child_process').fork;
31const LOCAL_BROADCAST_HOST = '224.0.0.114';
32const LOCAL_HOST_IFADDR = '0.0.0.0';
33const TIMEOUT = common.platformTimeout(5000);
34const messages = [
35  Buffer.from('First message to send'),
36  Buffer.from('Second message to send'),
37  Buffer.from('Third message to send'),
38  Buffer.from('Fourth message to send'),
39];
40const workers = {};
41const listeners = 3;
42let listening, sendSocket, done, timer, dead;
43
44
45function launchChildProcess() {
46  const worker = fork(__filename, ['child']);
47  workers[worker.pid] = worker;
48
49  worker.messagesReceived = [];
50
51  // Handle the death of workers.
52  worker.on('exit', function(code) {
53    // Don't consider this the true death if the worker has finished
54    // successfully or if the exit code is 0.
55    if (worker.isDone || code === 0) {
56      return;
57    }
58
59    dead += 1;
60    console.error('[PARENT] Worker %d died. %d dead of %d',
61                  worker.pid,
62                  dead,
63                  listeners);
64
65    if (dead === listeners) {
66      console.error('[PARENT] All workers have died.');
67      console.error('[PARENT] Fail');
68      process.exit(1);
69    }
70  });
71
72  worker.on('message', function(msg) {
73    if (msg.listening) {
74      listening += 1;
75
76      if (listening === listeners) {
77        // All child process are listening, so start sending.
78        sendSocket.sendNext();
79      }
80      return;
81    }
82    if (msg.message) {
83      worker.messagesReceived.push(msg.message);
84
85      if (worker.messagesReceived.length === messages.length) {
86        done += 1;
87        worker.isDone = true;
88        console.error('[PARENT] %d received %d messages total.',
89                      worker.pid,
90                      worker.messagesReceived.length);
91      }
92
93      if (done === listeners) {
94        console.error('[PARENT] All workers have received the ' +
95                      'required number of messages. Will now compare.');
96
97        Object.keys(workers).forEach(function(pid) {
98          const worker = workers[pid];
99
100          let count = 0;
101
102          worker.messagesReceived.forEach(function(buf) {
103            for (let i = 0; i < messages.length; ++i) {
104              if (buf.toString() === messages[i].toString()) {
105                count++;
106                break;
107              }
108            }
109          });
110
111          console.error('[PARENT] %d received %d matching messages.',
112                        worker.pid, count);
113
114          assert.strictEqual(count, messages.length);
115        });
116
117        clearTimeout(timer);
118        console.error('[PARENT] Success');
119        killSubprocesses(workers);
120      }
121    }
122  });
123}
124
125function killSubprocesses(subprocesses) {
126  Object.keys(subprocesses).forEach(function(key) {
127    const subprocess = subprocesses[key];
128    subprocess.kill();
129  });
130}
131
132if (process.argv[2] !== 'child') {
133  listening = 0;
134  dead = 0;
135  let i = 0;
136  done = 0;
137
138  // Exit the test if it doesn't succeed within TIMEOUT.
139  timer = setTimeout(function() {
140    console.error('[PARENT] Responses were not received within %d ms.',
141                  TIMEOUT);
142    console.error('[PARENT] Fail');
143
144    killSubprocesses(workers);
145
146    process.exit(1);
147  }, TIMEOUT);
148
149  // Launch child processes.
150  for (let x = 0; x < listeners; x++) {
151    launchChildProcess(x);
152  }
153
154  sendSocket = dgram.createSocket('udp4');
155
156  // The socket is actually created async now.
157  sendSocket.on('listening', function() {
158    sendSocket.setTTL(1);
159    sendSocket.setBroadcast(true);
160    sendSocket.setMulticastTTL(1);
161    sendSocket.setMulticastLoopback(true);
162    sendSocket.setMulticastInterface(LOCAL_HOST_IFADDR);
163  });
164
165  sendSocket.on('close', function() {
166    console.error('[PARENT] sendSocket closed');
167  });
168
169  sendSocket.sendNext = function() {
170    const buf = messages[i++];
171
172    if (!buf) {
173      try { sendSocket.close(); } catch {
174        // Continue regardless of error.
175      }
176      return;
177    }
178
179    sendSocket.send(
180      buf,
181      0,
182      buf.length,
183      common.PORT,
184      LOCAL_BROADCAST_HOST,
185      function(err) {
186        assert.ifError(err);
187        console.error('[PARENT] sent "%s" to %s:%s',
188                      buf.toString(),
189                      LOCAL_BROADCAST_HOST, common.PORT);
190        process.nextTick(sendSocket.sendNext);
191      },
192    );
193  };
194}
195
196if (process.argv[2] === 'child') {
197  const receivedMessages = [];
198  const listenSocket = dgram.createSocket({
199    type: 'udp4',
200    reuseAddr: true,
201  });
202
203  listenSocket.on('listening', function() {
204    listenSocket.addMembership(LOCAL_BROADCAST_HOST, LOCAL_HOST_IFADDR);
205
206    listenSocket.on('message', function(buf, rinfo) {
207      console.error('[CHILD] %s received "%s" from %j', process.pid,
208                    buf.toString(), rinfo);
209
210      receivedMessages.push(buf);
211
212      process.send({ message: buf.toString() });
213
214      if (receivedMessages.length === messages.length) {
215        // .dropMembership() not strictly needed but here as a sanity check.
216        listenSocket.dropMembership(LOCAL_BROADCAST_HOST);
217        process.nextTick(function() {
218          listenSocket.close();
219        });
220      }
221    });
222
223    listenSocket.on('close', function() {
224      // HACK: Wait to exit the process to ensure that the parent
225      // process has had time to receive all messages via process.send()
226      // This may be indicative of some other issue.
227      setTimeout(function() {
228        process.exit();
229      }, common.platformTimeout(1000));
230    });
231    process.send({ listening: true });
232  });
233
234  listenSocket.bind(common.PORT);
235}
236