|
2 | 2 | // for details. All rights reserved. Use of this source code is governed by a
|
3 | 3 | // BSD-style license that can be found in the LICENSE file.
|
4 | 4 |
|
| 5 | +import 'dart:async'; |
5 | 6 | import 'dart:io';
|
6 | 7 |
|
7 |
| -String fixPath(String targetPath) => [ |
8 |
| - if (kEntries.isNotEmpty) '/app/', |
9 |
| - targetPath, |
10 |
| - ].join('/'); |
| 8 | +import 'package:http/http.dart' as http; |
| 9 | +import 'package:shelf/shelf.dart'; |
| 10 | +import 'package:shelf/shelf_io.dart'; |
11 | 11 |
|
12 | 12 | final kEntries = Platform.environment.entries
|
13 | 13 | .where((e) => e.key.startsWith('K_'))
|
14 | 14 | .map((e) => '${e.key}\t${e.value}')
|
15 | 15 | .toList();
|
| 16 | + |
| 17 | +/// Serves [handler] on [InternetAddress.anyIPv4] using the port returned by |
| 18 | +/// [listenPort]. |
| 19 | +/// |
| 20 | +/// The returned [Future] will complete using [terminateRequestFuture] after |
| 21 | +/// closing the server. |
| 22 | +Future<void> serveHandler(Handler handler) async { |
| 23 | + final port = listenPort(); |
| 24 | + |
| 25 | + final server = await serve( |
| 26 | + handler, |
| 27 | + InternetAddress.anyIPv4, // Allows external connections |
| 28 | + port, |
| 29 | + ); |
| 30 | + print('Serving at http://${server.address.host}:${server.port}'); |
| 31 | + |
| 32 | + await terminateRequestFuture(); |
| 33 | + |
| 34 | + await server.close(); |
| 35 | +} |
| 36 | + |
| 37 | +/// Returns the port to listen on from environment variable or uses the default |
| 38 | +/// `8080`. |
| 39 | +/// |
| 40 | +/// See https://cloud.google.com/run/docs/reference/container-contract#port |
| 41 | +int listenPort() => int.parse(Platform.environment['PORT'] ?? '8080'); |
| 42 | + |
| 43 | +/// Returns a [Future] that completes when the process receives a |
| 44 | +/// [ProcessSignal] requesting a shutdown. |
| 45 | +/// |
| 46 | +/// [ProcessSignal.sigint] is listened to on all platforms. |
| 47 | +/// |
| 48 | +/// [ProcessSignal.sigterm] is listened to on all platforms except Windows. |
| 49 | +Future<void> terminateRequestFuture() { |
| 50 | + final completer = Completer<bool>.sync(); |
| 51 | + |
| 52 | + // sigIntSub is copied below to avoid a race condition - ignoring this lint |
| 53 | + // ignore: cancel_subscriptions |
| 54 | + StreamSubscription? sigIntSub, sigTermSub; |
| 55 | + |
| 56 | + Future<void> signalHandler(ProcessSignal signal) async { |
| 57 | + print('Received signal $signal - closing'); |
| 58 | + |
| 59 | + final subCopy = sigIntSub; |
| 60 | + if (subCopy != null) { |
| 61 | + sigIntSub = null; |
| 62 | + await subCopy.cancel(); |
| 63 | + sigIntSub = null; |
| 64 | + if (sigTermSub != null) { |
| 65 | + await sigTermSub!.cancel(); |
| 66 | + sigTermSub = null; |
| 67 | + } |
| 68 | + completer.complete(true); |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + sigIntSub = ProcessSignal.sigint.watch().listen(signalHandler); |
| 73 | + |
| 74 | + // SIGTERM is not supported on Windows. Attempting to register a SIGTERM |
| 75 | + // handler raises an exception. |
| 76 | + if (!Platform.isWindows) { |
| 77 | + sigTermSub = ProcessSignal.sigterm.watch().listen(signalHandler); |
| 78 | + } |
| 79 | + |
| 80 | + return completer.future; |
| 81 | +} |
| 82 | + |
| 83 | +/// Returns a [Future] that completes with the |
| 84 | +/// [Project ID](https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects) |
| 85 | +/// for the current Google Cloud Project. |
| 86 | +/// |
| 87 | +/// First, if an environment variable with a name in |
| 88 | +/// [gcpProjectIdEnvironmentVariables] exists, that is returned. |
| 89 | +/// (The list is checked in order.) This is useful for local development. |
| 90 | +/// |
| 91 | +/// If no such environment variable exists, then we assume the code is running |
| 92 | +/// on Google Cloud and |
| 93 | +/// [Project metadata](https://cloud.google.com/compute/docs/metadata/default-metadata-values#project_metadata) |
| 94 | +/// is queried for the Project ID. |
| 95 | +Future<String> currentProjectId() async { |
| 96 | + for (var envKey in gcpProjectIdEnvironmentVariables) { |
| 97 | + final value = Platform.environment[envKey]; |
| 98 | + if (value != null) return value; |
| 99 | + } |
| 100 | + |
| 101 | + const host = 'http://metadata.google.internal/'; |
| 102 | + final url = Uri.parse('$host/computeMetadata/v1/project/project-id'); |
| 103 | + |
| 104 | + try { |
| 105 | + final response = await http.get( |
| 106 | + url, |
| 107 | + headers: {'Metadata-Flavor': 'Google'}, |
| 108 | + ); |
| 109 | + |
| 110 | + if (response.statusCode != 200) { |
| 111 | + throw HttpException( |
| 112 | + '${response.body} (${response.statusCode})', |
| 113 | + uri: url, |
| 114 | + ); |
| 115 | + } |
| 116 | + |
| 117 | + return response.body; |
| 118 | + } on SocketException { |
| 119 | + stderr.writeln( |
| 120 | + ''' |
| 121 | +Could not connect to $host. |
| 122 | +If not running on Google Cloud, one of these environment variables must be set |
| 123 | +to the target Google Project ID: |
| 124 | +${gcpProjectIdEnvironmentVariables.join('\n')} |
| 125 | +''', |
| 126 | + ); |
| 127 | + rethrow; |
| 128 | + } |
| 129 | +} |
| 130 | + |
| 131 | +/// A set of typical environment variables that are likely to represent the |
| 132 | +/// current Google Cloud project ID. |
| 133 | +/// |
| 134 | +/// For context, see: |
| 135 | +/// * https://cloud.google.com/functions/docs/env-var |
| 136 | +/// * https://cloud.google.com/compute/docs/gcloud-compute#default_project |
| 137 | +/// * https://github.com/GoogleContainerTools/gcp-auth-webhook/blob/08136ca171fe5713cc70ef822c911fbd3a1707f5/server.go#L38-L44 |
| 138 | +/// |
| 139 | +/// Note: these are ordered starting from the most current/canonical to least. |
| 140 | +/// (At least as could be determined at the time of writing.) |
| 141 | +const gcpProjectIdEnvironmentVariables = { |
| 142 | + 'GCP_PROJECT', |
| 143 | + 'GCLOUD_PROJECT', |
| 144 | + 'CLOUDSDK_CORE_PROJECT', |
| 145 | + 'GOOGLE_CLOUD_PROJECT', |
| 146 | +}; |
0 commit comments