|
1 // Exercise Unix domain sockets. |
|
2 |
|
3 const CC = Components.Constructor; |
|
4 |
|
5 const UnixServerSocket = CC("@mozilla.org/network/server-socket;1", |
|
6 "nsIServerSocket", |
|
7 "initWithFilename"); |
|
8 |
|
9 const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", |
|
10 "nsIScriptableInputStream", |
|
11 "init"); |
|
12 |
|
13 const IOService = Cc["@mozilla.org/network/io-service;1"] |
|
14 .getService(Ci.nsIIOService); |
|
15 const socketTransportService = Cc["@mozilla.org/network/socket-transport-service;1"] |
|
16 .getService(Ci.nsISocketTransportService); |
|
17 |
|
18 const threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); |
|
19 |
|
20 const allPermissions = parseInt("777", 8); |
|
21 |
|
22 function run_test() |
|
23 { |
|
24 // If we're on Windows, simply check for graceful failure. |
|
25 if ("@mozilla.org/windows-registry-key;1" in Cc) { |
|
26 test_not_supported(); |
|
27 return; |
|
28 } |
|
29 |
|
30 add_test(test_echo); |
|
31 add_test(test_name_too_long); |
|
32 add_test(test_no_directory); |
|
33 add_test(test_no_such_socket); |
|
34 add_test(test_address_in_use); |
|
35 add_test(test_file_in_way); |
|
36 add_test(test_create_permission); |
|
37 add_test(test_connect_permission); |
|
38 add_test(test_long_socket_name); |
|
39 add_test(test_keep_when_offline); |
|
40 |
|
41 run_next_test(); |
|
42 } |
|
43 |
|
44 // Check that creating a Unix domain socket fails gracefully on Windows. |
|
45 function test_not_supported() |
|
46 { |
|
47 let socketName = do_get_tempdir(); |
|
48 socketName.append('socket'); |
|
49 do_print("creating socket: " + socketName.path); |
|
50 |
|
51 do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), |
|
52 "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED"); |
|
53 |
|
54 do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName), |
|
55 "NS_ERROR_SOCKET_ADDRESS_NOT_SUPPORTED"); |
|
56 } |
|
57 |
|
58 // Actually exchange data with Unix domain sockets. |
|
59 function test_echo() |
|
60 { |
|
61 let log = ''; |
|
62 |
|
63 let socketName = do_get_tempdir(); |
|
64 socketName.append('socket'); |
|
65 |
|
66 // Create a server socket, listening for connections. |
|
67 do_print("creating socket: " + socketName.path); |
|
68 let server = new UnixServerSocket(socketName, allPermissions, -1); |
|
69 server.asyncListen({ |
|
70 onSocketAccepted: function(aServ, aTransport) { |
|
71 do_print("called test_echo's onSocketAccepted"); |
|
72 log += 'a'; |
|
73 |
|
74 do_check_eq(aServ, server); |
|
75 |
|
76 let connection = aTransport; |
|
77 |
|
78 // Check the server socket's self address. |
|
79 let connectionSelfAddr = connection.getScriptableSelfAddr(); |
|
80 do_check_eq(connectionSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); |
|
81 do_check_eq(connectionSelfAddr.address, socketName.path); |
|
82 |
|
83 // The client socket is anonymous, so the server transport should |
|
84 // have an empty peer address. |
|
85 do_check_eq(connection.host, ''); |
|
86 do_check_eq(connection.port, 0); |
|
87 let connectionPeerAddr = connection.getScriptablePeerAddr(); |
|
88 do_check_eq(connectionPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); |
|
89 do_check_eq(connectionPeerAddr.address, ''); |
|
90 |
|
91 let serverAsyncInput = connection.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
92 let serverOutput = connection.openOutputStream(0, 0, 0); |
|
93 |
|
94 serverAsyncInput.asyncWait(function (aStream) { |
|
95 do_print("called test_echo's server's onInputStreamReady"); |
|
96 let serverScriptableInput = new ScriptableInputStream(aStream); |
|
97 |
|
98 // Receive data from the client, and send back a response. |
|
99 do_check_eq(serverScriptableInput.readBytes(17), "Mervyn Murgatroyd"); |
|
100 do_print("server has read message from client"); |
|
101 serverOutput.write("Ruthven Murgatroyd", 18); |
|
102 do_print("server has written to client"); |
|
103 }, 0, 0, threadManager.currentThread); |
|
104 }, |
|
105 |
|
106 onStopListening: function(aServ, aStatus) { |
|
107 do_print("called test_echo's onStopListening"); |
|
108 log += 's'; |
|
109 |
|
110 do_check_eq(aServ, server); |
|
111 do_check_eq(log, 'acs'); |
|
112 |
|
113 run_next_test(); |
|
114 } |
|
115 }); |
|
116 |
|
117 // Create a client socket, and connect to the server. |
|
118 let client = socketTransportService.createUnixDomainTransport(socketName); |
|
119 do_check_eq(client.host, socketName.path); |
|
120 do_check_eq(client.port, 0); |
|
121 |
|
122 let clientAsyncInput = client.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
123 let clientInput = new ScriptableInputStream(clientAsyncInput); |
|
124 let clientOutput = client.openOutputStream(0, 0, 0); |
|
125 |
|
126 clientOutput.write("Mervyn Murgatroyd", 17); |
|
127 do_print("client has written to server"); |
|
128 |
|
129 clientAsyncInput.asyncWait(function (aStream) { |
|
130 do_print("called test_echo's client's onInputStreamReady"); |
|
131 log += 'c'; |
|
132 |
|
133 do_check_eq(aStream, clientAsyncInput); |
|
134 |
|
135 // Now that the connection has been established, we can check the |
|
136 // transport's self and peer addresses. |
|
137 let clientSelfAddr = client.getScriptableSelfAddr(); |
|
138 do_check_eq(clientSelfAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); |
|
139 do_check_eq(clientSelfAddr.address, ''); |
|
140 |
|
141 do_check_eq(client.host, socketName.path); // re-check, but hey |
|
142 let clientPeerAddr = client.getScriptablePeerAddr(); |
|
143 do_check_eq(clientPeerAddr.family, Ci.nsINetAddr.FAMILY_LOCAL); |
|
144 do_check_eq(clientPeerAddr.address, socketName.path); |
|
145 |
|
146 do_check_eq(clientInput.readBytes(18), "Ruthven Murgatroyd"); |
|
147 do_print("client has read message from server"); |
|
148 |
|
149 server.close(); |
|
150 }, 0, 0, threadManager.currentThread); |
|
151 } |
|
152 |
|
153 // Create client and server sockets using a path that's too long. |
|
154 function test_name_too_long() |
|
155 { |
|
156 let socketName = do_get_tempdir(); |
|
157 // The length limits on all the systems NSPR supports are a bit past 100. |
|
158 socketName.append(new Array(1000).join('x')); |
|
159 |
|
160 // The length must be checked before we ever make any system calls --- we |
|
161 // have to create the sockaddr first --- so it's unambiguous which error |
|
162 // we should get here. |
|
163 |
|
164 do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1), |
|
165 "NS_ERROR_FILE_NAME_TOO_LONG"); |
|
166 |
|
167 // Unlike most other client socket errors, this one gets reported |
|
168 // immediately, as we can't even initialize the sockaddr with the given |
|
169 // name. |
|
170 do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName), |
|
171 "NS_ERROR_FILE_NAME_TOO_LONG"); |
|
172 |
|
173 run_next_test(); |
|
174 } |
|
175 |
|
176 // Try creating a socket in a directory that doesn't exist. |
|
177 function test_no_directory() |
|
178 { |
|
179 let socketName = do_get_tempdir(); |
|
180 socketName.append('directory-that-does-not-exist'); |
|
181 socketName.append('socket'); |
|
182 |
|
183 do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1), |
|
184 "NS_ERROR_FILE_NOT_FOUND"); |
|
185 |
|
186 run_next_test(); |
|
187 } |
|
188 |
|
189 // Try connecting to a server socket that isn't there. |
|
190 function test_no_such_socket() |
|
191 { |
|
192 let socketName = do_get_tempdir(); |
|
193 socketName.append('nonexistent-socket'); |
|
194 |
|
195 let client = socketTransportService.createUnixDomainTransport(socketName); |
|
196 let clientAsyncInput = client.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
197 clientAsyncInput.asyncWait(function (aStream) { |
|
198 do_print("called test_no_such_socket's onInputStreamReady"); |
|
199 |
|
200 do_check_eq(aStream, clientAsyncInput); |
|
201 |
|
202 // nsISocketTransport puts off actually creating sockets as long as |
|
203 // possible, so the error in connecting doesn't actually show up until |
|
204 // this point. |
|
205 do_check_throws_nsIException(() => clientAsyncInput.available(), |
|
206 "NS_ERROR_FILE_NOT_FOUND"); |
|
207 |
|
208 clientAsyncInput.close(); |
|
209 client.close(Cr.NS_OK); |
|
210 |
|
211 run_next_test(); |
|
212 }, 0, 0, threadManager.currentThread); |
|
213 } |
|
214 |
|
215 // Creating a socket with a name that another socket is already using is an |
|
216 // error. |
|
217 function test_address_in_use() |
|
218 { |
|
219 let socketName = do_get_tempdir(); |
|
220 socketName.append('socket-in-use'); |
|
221 |
|
222 // Create one server socket. |
|
223 let server = new UnixServerSocket(socketName, allPermissions, -1); |
|
224 |
|
225 // Now try to create another with the same name. |
|
226 do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), |
|
227 "NS_ERROR_SOCKET_ADDRESS_IN_USE"); |
|
228 |
|
229 run_next_test(); |
|
230 } |
|
231 |
|
232 // Creating a socket with a name that is already a file is an error. |
|
233 function test_file_in_way() |
|
234 { |
|
235 let socketName = do_get_tempdir(); |
|
236 socketName.append('file_in_way'); |
|
237 |
|
238 // Create a file with the given name. |
|
239 socketName.create(Ci.nsIFile.NORMAL_FILE_TYPE, allPermissions); |
|
240 |
|
241 // Try to create a socket with the same name. |
|
242 do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), |
|
243 "NS_ERROR_SOCKET_ADDRESS_IN_USE"); |
|
244 |
|
245 // Try to create a socket under a name that uses that as a parent directory. |
|
246 socketName.append('socket'); |
|
247 do_check_throws_nsIException(() => new UnixServerSocket(socketName, 0, -1), |
|
248 "NS_ERROR_FILE_NOT_DIRECTORY"); |
|
249 |
|
250 run_next_test(); |
|
251 } |
|
252 |
|
253 // It is not permitted to create a socket in a directory which we are not |
|
254 // permitted to execute, or create files in. |
|
255 function test_create_permission() |
|
256 { |
|
257 let dirName = do_get_tempdir(); |
|
258 dirName.append('unfriendly'); |
|
259 |
|
260 let socketName = dirName.clone(); |
|
261 socketName.append('socket'); |
|
262 |
|
263 // The test harness has difficulty cleaning things up if we don't make |
|
264 // everything writable before we're done. |
|
265 try { |
|
266 // Create a directory which we are not permitted to search. |
|
267 dirName.create(Ci.nsIFile.DIRECTORY_TYPE, 0); |
|
268 |
|
269 // Try to create a socket in that directory. Because Linux returns EACCES |
|
270 // when a 'connect' fails because of a local firewall rule, |
|
271 // nsIServerSocket returns NS_ERROR_CONNECTION_REFUSED in this case. |
|
272 do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), |
|
273 "NS_ERROR_CONNECTION_REFUSED"); |
|
274 |
|
275 // Grant read and execute permission, but not write permission on the directory. |
|
276 dirName.permissions = parseInt("0555", 8); |
|
277 |
|
278 // This should also fail; we need write permission. |
|
279 do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), |
|
280 "NS_ERROR_CONNECTION_REFUSED"); |
|
281 |
|
282 } finally { |
|
283 // Make the directory writable, so the test harness can clean it up. |
|
284 dirName.permissions = allPermissions; |
|
285 } |
|
286 |
|
287 // This should succeed, since we now have all the permissions on the |
|
288 // directory we could want. |
|
289 do_check_instanceof(new UnixServerSocket(socketName, allPermissions, -1), |
|
290 Ci.nsIServerSocket); |
|
291 |
|
292 run_next_test(); |
|
293 } |
|
294 |
|
295 // To connect to a Unix domain socket, we need search permission on the |
|
296 // directories containing it, and some kind of permission or other on the |
|
297 // socket itself. |
|
298 function test_connect_permission() |
|
299 { |
|
300 // This test involves a lot of callbacks, but they're written out so that |
|
301 // the actual control flow proceeds from top to bottom. |
|
302 let log = ''; |
|
303 |
|
304 // Create a directory which we are permitted to search - at first. |
|
305 let dirName = do_get_tempdir(); |
|
306 dirName.append('inhospitable'); |
|
307 dirName.create(Ci.nsIFile.DIRECTORY_TYPE, allPermissions); |
|
308 |
|
309 let socketName = dirName.clone(); |
|
310 socketName.append('socket'); |
|
311 |
|
312 // Create a server socket in that directory, listening for connections, |
|
313 // and accessible. |
|
314 let server = new UnixServerSocket(socketName, allPermissions, -1); |
|
315 server.asyncListen({ onSocketAccepted: socketAccepted, onStopListening: stopListening }); |
|
316 |
|
317 // Make the directory unsearchable. |
|
318 dirName.permissions = 0; |
|
319 |
|
320 let client3; |
|
321 |
|
322 let client1 = socketTransportService.createUnixDomainTransport(socketName); |
|
323 let client1AsyncInput = client1.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
324 client1AsyncInput.asyncWait(function (aStream) { |
|
325 do_print("called test_connect_permission's client1's onInputStreamReady"); |
|
326 log += '1'; |
|
327 |
|
328 // nsISocketTransport puts off actually creating sockets as long as |
|
329 // possible, so the error doesn't actually show up until this point. |
|
330 do_check_throws_nsIException(() => client1AsyncInput.available(), |
|
331 "NS_ERROR_CONNECTION_REFUSED"); |
|
332 |
|
333 client1AsyncInput.close(); |
|
334 client1.close(Cr.NS_OK); |
|
335 |
|
336 // Make the directory searchable, but make the socket inaccessible. |
|
337 dirName.permissions = allPermissions; |
|
338 socketName.permissions = 0; |
|
339 |
|
340 let client2 = socketTransportService.createUnixDomainTransport(socketName); |
|
341 let client2AsyncInput = client2.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
342 client2AsyncInput.asyncWait(function (aStream) { |
|
343 do_print("called test_connect_permission's client2's onInputStreamReady"); |
|
344 log += '2'; |
|
345 |
|
346 do_check_throws_nsIException(() => client2AsyncInput.available(), |
|
347 "NS_ERROR_CONNECTION_REFUSED"); |
|
348 |
|
349 client2AsyncInput.close(); |
|
350 client2.close(Cr.NS_OK); |
|
351 |
|
352 // Now make everything accessible, and try one last time. |
|
353 socketName.permissions = allPermissions; |
|
354 |
|
355 client3 = socketTransportService.createUnixDomainTransport(socketName); |
|
356 |
|
357 let client3Output = client3.openOutputStream(0, 0, 0); |
|
358 client3Output.write("Hanratty", 8); |
|
359 |
|
360 let client3AsyncInput = client3.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
361 client3AsyncInput.asyncWait(client3InputStreamReady, 0, 0, threadManager.currentThread); |
|
362 }, 0, 0, threadManager.currentThread); |
|
363 }, 0, 0, threadManager.currentThread); |
|
364 |
|
365 function socketAccepted(aServ, aTransport) { |
|
366 do_print("called test_connect_permission's onSocketAccepted"); |
|
367 log += 'a'; |
|
368 |
|
369 let serverInput = aTransport.openInputStream(0, 0, 0).QueryInterface(Ci.nsIAsyncInputStream); |
|
370 let serverOutput = aTransport.openOutputStream(0, 0, 0); |
|
371 |
|
372 serverInput.asyncWait(function (aStream) { |
|
373 do_print("called test_connect_permission's socketAccepted's onInputStreamReady"); |
|
374 log += 'i'; |
|
375 |
|
376 // Receive data from the client, and send back a response. |
|
377 let serverScriptableInput = new ScriptableInputStream(serverInput); |
|
378 do_check_eq(serverScriptableInput.readBytes(8), "Hanratty"); |
|
379 serverOutput.write("Ferlingatti", 11); |
|
380 }, 0, 0, threadManager.currentThread); |
|
381 } |
|
382 |
|
383 function client3InputStreamReady(aStream) { |
|
384 do_print("called client3's onInputStreamReady"); |
|
385 log += '3'; |
|
386 |
|
387 let client3Input = new ScriptableInputStream(aStream); |
|
388 |
|
389 do_check_eq(client3Input.readBytes(11), "Ferlingatti"); |
|
390 |
|
391 client3.close(Cr.NS_OK); |
|
392 server.close(); |
|
393 } |
|
394 |
|
395 function stopListening(aServ, aStatus) { |
|
396 do_print("called test_connect_permission's server's stopListening"); |
|
397 log += 's'; |
|
398 |
|
399 do_check_eq(log, '12ai3s'); |
|
400 |
|
401 run_next_test(); |
|
402 } |
|
403 } |
|
404 |
|
405 // Creating a socket with a long filename doesn't crash. |
|
406 function test_long_socket_name() |
|
407 { |
|
408 let socketName = do_get_tempdir(); |
|
409 socketName.append(new Array(10000).join('long')); |
|
410 |
|
411 // Try to create a server socket with the long name. |
|
412 do_check_throws_nsIException(() => new UnixServerSocket(socketName, allPermissions, -1), |
|
413 "NS_ERROR_FILE_NAME_TOO_LONG"); |
|
414 |
|
415 // Try to connect to a socket with the long name. |
|
416 do_check_throws_nsIException(() => socketTransportService.createUnixDomainTransport(socketName), |
|
417 "NS_ERROR_FILE_NAME_TOO_LONG"); |
|
418 |
|
419 run_next_test(); |
|
420 } |
|
421 |
|
422 // Going offline should not shut down Unix domain sockets. |
|
423 function test_keep_when_offline() |
|
424 { |
|
425 let log = ''; |
|
426 |
|
427 let socketName = do_get_tempdir(); |
|
428 socketName.append('keep-when-offline'); |
|
429 |
|
430 // Create a listening socket. |
|
431 let listener = new UnixServerSocket(socketName, allPermissions, -1); |
|
432 listener.asyncListen({ onSocketAccepted: onAccepted, onStopListening: onStopListening }); |
|
433 |
|
434 // Connect a client socket to the listening socket. |
|
435 let client = socketTransportService.createUnixDomainTransport(socketName); |
|
436 let clientOutput = client.openOutputStream(0, 0, 0); |
|
437 let clientInput = client.openInputStream(0, 0, 0); |
|
438 clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread); |
|
439 let clientScriptableInput = new ScriptableInputStream(clientInput); |
|
440 |
|
441 let server, serverInput, serverScriptableInput, serverOutput; |
|
442 |
|
443 // How many times has the server invited the client to go first? |
|
444 let count = 0; |
|
445 |
|
446 // The server accepted connection callback. |
|
447 function onAccepted(aListener, aServer) { |
|
448 do_print("test_keep_when_offline: onAccepted called"); |
|
449 log += 'a'; |
|
450 do_check_eq(aListener, listener); |
|
451 server = aServer; |
|
452 |
|
453 // Prepare to receive messages from the client. |
|
454 serverInput = server.openInputStream(0, 0, 0); |
|
455 serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread); |
|
456 serverScriptableInput = new ScriptableInputStream(serverInput); |
|
457 |
|
458 // Start a conversation with the client. |
|
459 serverOutput = server.openOutputStream(0, 0, 0); |
|
460 serverOutput.write("After you, Alphonse!", 20); |
|
461 count++; |
|
462 } |
|
463 |
|
464 // The client has seen its end of the socket close. |
|
465 function clientReady(aStream) { |
|
466 log += 'c'; |
|
467 do_print("test_keep_when_offline: clientReady called: " + log); |
|
468 do_check_eq(aStream, clientInput); |
|
469 |
|
470 // If the connection has been closed, end the conversation and stop listening. |
|
471 let available; |
|
472 try { |
|
473 available = clientInput.available(); |
|
474 } catch (ex) { |
|
475 do_check_instanceof(ex, Ci.nsIException); |
|
476 do_check_eq(ex.result, Cr.NS_BASE_STREAM_CLOSED); |
|
477 |
|
478 do_print("client received end-of-stream; closing client output stream"); |
|
479 log += ')'; |
|
480 |
|
481 client.close(Cr.NS_OK); |
|
482 |
|
483 // Now both output streams have been closed, and both input streams |
|
484 // have received the close notification. Stop listening for |
|
485 // connections. |
|
486 listener.close(); |
|
487 } |
|
488 |
|
489 if (available) { |
|
490 // Check the message from the server. |
|
491 do_check_eq(clientScriptableInput.readBytes(20), "After you, Alphonse!"); |
|
492 |
|
493 // Write our response to the server. |
|
494 clientOutput.write("No, after you, Gaston!", 22); |
|
495 |
|
496 // Ask to be called again, when more input arrives. |
|
497 clientInput.asyncWait(clientReady, 0, 0, threadManager.currentThread); |
|
498 } |
|
499 } |
|
500 |
|
501 function serverReady(aStream) { |
|
502 log += 's'; |
|
503 do_print("test_keep_when_offline: serverReady called: " + log); |
|
504 do_check_eq(aStream, serverInput); |
|
505 |
|
506 // Check the message from the client. |
|
507 do_check_eq(serverScriptableInput.readBytes(22), "No, after you, Gaston!"); |
|
508 |
|
509 // This should not shut things down: Unix domain sockets should |
|
510 // remain open in offline mode. |
|
511 if (count == 5) { |
|
512 IOService.offline = true; |
|
513 log += 'o'; |
|
514 } |
|
515 |
|
516 if (count < 10) { |
|
517 // Insist. |
|
518 serverOutput.write("After you, Alphonse!", 20); |
|
519 count++; |
|
520 |
|
521 // As long as the input stream is open, always ask to be called again |
|
522 // when more input arrives. |
|
523 serverInput.asyncWait(serverReady, 0, 0, threadManager.currentThread); |
|
524 } else if (count == 10) { |
|
525 // After sending ten times and receiving ten replies, we're not |
|
526 // going to send any more. Close the server's output stream; the |
|
527 // client's input stream should see this. |
|
528 do_print("closing server transport"); |
|
529 server.close(Cr.NS_OK); |
|
530 log += '('; |
|
531 } |
|
532 } |
|
533 |
|
534 // We have stopped listening. |
|
535 function onStopListening(aServ, aStatus) { |
|
536 do_print("test_keep_when_offline: onStopListening called"); |
|
537 log += 'L'; |
|
538 do_check_eq(log, 'acscscscscsocscscscscs(c)L'); |
|
539 |
|
540 do_check_eq(aServ, listener); |
|
541 do_check_eq(aStatus, Cr.NS_BINDING_ABORTED); |
|
542 |
|
543 run_next_test(); |
|
544 } |
|
545 } |