[io/mac/sigpipe] Ensure SIGPIPE is not triggered for client socket connect code.

Fixes https://github.com/flutter/flutter/issues/84499

TEST=socket_sigpipe_test.dart

Change-Id: I220558e74b41c1969efa422254867c11dd17ee91
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/203660
Commit-Queue: Alexander Aprelev <aam@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
diff --git a/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc b/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
index 87e9d66..8564aa6 100644
--- a/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
+++ b/runtime/bin/ffi_test/ffi_test_functions_vmspecific.cc
@@ -314,6 +314,15 @@
 
 #endif  // defined(TARGET_OS_LINUX)
 
+// Restore default SIGPIPE handler, which is only needed on mac
+// since that is the only platform we explicitly ignore it.
+// See Platform::Initialize() in platform_macos.cc.
+DART_EXPORT void RestoreSIGPIPEHandler() {
+#if defined(HOST_OS_MACOS)
+  signal(SIGPIPE, SIG_DFL);
+#endif
+}
+
 DART_EXPORT void IGH_MsanUnpoison(void* start, intptr_t length) {
   MSAN_UNPOISON(start, length);
 }
@@ -418,7 +427,7 @@
 
 #define FATAL(error) Fatal(__FILE__, __LINE__, error)
 
-void SleepOnAnyOS(intptr_t seconds) {
+DART_EXPORT void SleepOnAnyOS(intptr_t seconds) {
 #if defined(HOST_OS_WINDOWS)
   Sleep(1000 * seconds);
 #else
diff --git a/runtime/bin/socket_macos.cc b/runtime/bin/socket_macos.cc
index 8c6be8b..f9e00c2 100644
--- a/runtime/bin/socket_macos.cc
+++ b/runtime/bin/socket_macos.cc
@@ -44,6 +44,11 @@
     FDUtils::SaveErrorAndClose(fd);
     return -1;
   }
+  // Don't raise SIGPIPE when attempting to write to a connection which has
+  // already closed.
+  int optval = 1;
+  VOID_NO_RETRY_EXPECTED(
+      setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval)));
   return fd;
 }
 
diff --git a/tests/standalone/io/socket_sigpipe_test.dart b/tests/standalone/io/socket_sigpipe_test.dart
new file mode 100644
index 0000000..ec6f5c8
--- /dev/null
+++ b/tests/standalone/io/socket_sigpipe_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// Tests that SIGPIPE won't terminate websocket client dart app.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:ffi';
+import 'dart:io';
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+import 'package:ffi/ffi.dart';
+import 'package:path/path.dart' as p;
+
+import '../../../tests/ffi/dylib_utils.dart';
+
+class Isolate extends Opaque {}
+
+abstract class FfiBindings {
+  static final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
+
+  static final RestoreSIGPIPEHandler =
+      ffiTestFunctions.lookupFunction<Void Function(), void Function()>(
+          "RestoreSIGPIPEHandler");
+  static final SleepOnAnyOS = ffiTestFunctions.lookupFunction<
+      Void Function(IntPtr), void Function(int)>("SleepOnAnyOS");
+}
+
+Future<void> main() async {
+  asyncStart();
+
+  final server = await Process.start(Platform.executable, <String>[
+    p.join(p.dirname(Platform.script.toFilePath()),
+        "socket_sigpipe_test_server.dart")
+  ]);
+  final serverPort = Completer<int>();
+  server.stdout
+      .transform(utf8.decoder)
+      .transform(LineSplitter())
+      .listen((line) {
+    print('server stdout: $line');
+    if (!serverPort.isCompleted) {
+      serverPort.complete(int.parse(line));
+    }
+  });
+  server.stderr
+      .transform(utf8.decoder)
+      .transform(LineSplitter())
+      .listen((data) {
+    print('server stderr: $data');
+  });
+
+  FfiBindings.RestoreSIGPIPEHandler();
+  final ws =
+      await WebSocket.connect('ws://localhost:${await serverPort.future}');
+  ws.listen((var data) {
+    print('Got $data');
+    // Sleep to prevent closed socket events coming through and being handled.
+    // This way websocket stays open and writing into it should trigger SIGPIPE.
+    // Unless of course we requested SIGPIPE not to be generated on broken socket
+    // pipe. This is what this test is testing - that the SIGPIPE is not generated
+    // on broken socket pipe.
+    ws.add('foo');
+    FfiBindings.SleepOnAnyOS(10 /*seconds*/); // give server time to exit
+    ws.add('baz');
+    ws.close();
+  }, onDone: () {
+    asyncEnd();
+  }, onError: (e, st) {
+    Expect.fail('Client websocket failed $e $st');
+  });
+}
diff --git a/tests/standalone/io/socket_sigpipe_test_server.dart b/tests/standalone/io/socket_sigpipe_test_server.dart
new file mode 100644
index 0000000..89265af
--- /dev/null
+++ b/tests/standalone/io/socket_sigpipe_test_server.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// Helper server program for socket_sigpipe_test.dart
+
+import "package:expect/expect.dart";
+import "dart:async";
+import "dart:io";
+
+main() {
+  HttpServer.bind("127.0.0.1", 0).then((server) {
+    print(server.port);
+    server.listen((request) {
+      WebSocketTransformer.upgrade(request).then((websocket) async {
+        websocket.add('bar');
+        await websocket.close();
+        await server.close();
+        print('closed');
+      });
+    });
+  });
+}
diff --git a/tests/standalone/standalone.status b/tests/standalone/standalone.status
index 3c981bc..d299104 100644
--- a/tests/standalone/standalone.status
+++ b/tests/standalone/standalone.status
@@ -47,6 +47,7 @@
 [ $runtime == dart_precompiled ]
 http_launch_test: Skip
 io/addlatexhash_test: Skip
+io/socket_sigpipe_test: SkipByDesign # Spawns server process using Platform.executable
 io/wait_for_event_isolate_test: SkipByDesign # Uses mirrors.
 io/wait_for_event_microtask_test: SkipByDesign # Uses mirrors.
 io/wait_for_event_nested_microtask_test: SkipByDesign # Uses mirrors.
diff --git a/tests/standalone_2/io/socket_sigpipe_test.dart b/tests/standalone_2/io/socket_sigpipe_test.dart
new file mode 100644
index 0000000..ec6f5c8
--- /dev/null
+++ b/tests/standalone_2/io/socket_sigpipe_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// Tests that SIGPIPE won't terminate websocket client dart app.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:ffi';
+import 'dart:io';
+
+import "package:async_helper/async_helper.dart";
+import "package:expect/expect.dart";
+import 'package:ffi/ffi.dart';
+import 'package:path/path.dart' as p;
+
+import '../../../tests/ffi/dylib_utils.dart';
+
+class Isolate extends Opaque {}
+
+abstract class FfiBindings {
+  static final ffiTestFunctions = dlopenPlatformSpecific("ffi_test_functions");
+
+  static final RestoreSIGPIPEHandler =
+      ffiTestFunctions.lookupFunction<Void Function(), void Function()>(
+          "RestoreSIGPIPEHandler");
+  static final SleepOnAnyOS = ffiTestFunctions.lookupFunction<
+      Void Function(IntPtr), void Function(int)>("SleepOnAnyOS");
+}
+
+Future<void> main() async {
+  asyncStart();
+
+  final server = await Process.start(Platform.executable, <String>[
+    p.join(p.dirname(Platform.script.toFilePath()),
+        "socket_sigpipe_test_server.dart")
+  ]);
+  final serverPort = Completer<int>();
+  server.stdout
+      .transform(utf8.decoder)
+      .transform(LineSplitter())
+      .listen((line) {
+    print('server stdout: $line');
+    if (!serverPort.isCompleted) {
+      serverPort.complete(int.parse(line));
+    }
+  });
+  server.stderr
+      .transform(utf8.decoder)
+      .transform(LineSplitter())
+      .listen((data) {
+    print('server stderr: $data');
+  });
+
+  FfiBindings.RestoreSIGPIPEHandler();
+  final ws =
+      await WebSocket.connect('ws://localhost:${await serverPort.future}');
+  ws.listen((var data) {
+    print('Got $data');
+    // Sleep to prevent closed socket events coming through and being handled.
+    // This way websocket stays open and writing into it should trigger SIGPIPE.
+    // Unless of course we requested SIGPIPE not to be generated on broken socket
+    // pipe. This is what this test is testing - that the SIGPIPE is not generated
+    // on broken socket pipe.
+    ws.add('foo');
+    FfiBindings.SleepOnAnyOS(10 /*seconds*/); // give server time to exit
+    ws.add('baz');
+    ws.close();
+  }, onDone: () {
+    asyncEnd();
+  }, onError: (e, st) {
+    Expect.fail('Client websocket failed $e $st');
+  });
+}
diff --git a/tests/standalone_2/io/socket_sigpipe_test_server.dart b/tests/standalone_2/io/socket_sigpipe_test_server.dart
new file mode 100644
index 0000000..89265af
--- /dev/null
+++ b/tests/standalone_2/io/socket_sigpipe_test_server.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+//
+// Helper server program for socket_sigpipe_test.dart
+
+import "package:expect/expect.dart";
+import "dart:async";
+import "dart:io";
+
+main() {
+  HttpServer.bind("127.0.0.1", 0).then((server) {
+    print(server.port);
+    server.listen((request) {
+      WebSocketTransformer.upgrade(request).then((websocket) async {
+        websocket.add('bar');
+        await websocket.close();
+        await server.close();
+        print('closed');
+      });
+    });
+  });
+}
diff --git a/tests/standalone_2/standalone_2.status b/tests/standalone_2/standalone_2.status
index df81919..702d8e9 100644
--- a/tests/standalone_2/standalone_2.status
+++ b/tests/standalone_2/standalone_2.status
@@ -47,6 +47,7 @@
 [ $runtime == dart_precompiled ]
 http_launch_test: Skip
 io/addlatexhash_test: Skip
+io/socket_sigpipe_test: SkipByDesign # Spawns server process using Platform.executable
 io/wait_for_event_isolate_test: SkipByDesign # Uses mirrors.
 io/wait_for_event_microtask_test: SkipByDesign # Uses mirrors.
 io/wait_for_event_nested_microtask_test: SkipByDesign # Uses mirrors.