diff --git a/server/simple/README.md b/server/simple/README.md index 417d134..d7d8942 100644 --- a/server/simple/README.md +++ b/server/simple/README.md @@ -1,3 +1,5 @@ +![Welcome Dart Server](public/welcome_dart_server.png) + A simple Dart HTTP server using [package:shelf](https://pub.dev/packages/shelf). - Listens on "any IP" (0.0.0.0) instead of loop-back (localhost, 127.0.0.1) to @@ -28,13 +30,13 @@ or follow ### With OS-only runtime -Use [`tool/deploy_source.sh`](tool/deploy_source.sh) to build and +Use [`tool/deploy_source.dart`](tool/deploy_source.dart) to build and deploy using the [OS-only runtimes](https://docs.cloud.google.com/docs/buildpacks/osonly) feature. > [!NOTE] -> As of January 2026, this feature is in "Preview" and +> As of May 2026, this feature is in "Preview" and > requires the ["beta" gcloud component][gcloud-beta]. [cloud-beta]: https://docs.cloud.google.com/sdk/docs/components#alpha_and_beta_components diff --git a/server/simple/bin/server.dart b/server/simple/bin/server.dart index c9baafd..4df8d52 100644 --- a/server/simple/bin/server.dart +++ b/server/simple/bin/server.dart @@ -14,7 +14,7 @@ import 'package:shelf_static/shelf_static.dart' as shelf_static; Future main() async { // If the "PORT" environment variable is set, listen to it. Otherwise, 8080. // https://cloud.google.com/run/docs/reference/container-contract#port - final port = int.parse(Platform.environment['PORT'] ?? '8080'); + final port = int.tryParse(Platform.environment['PORT'] ?? '8080') ?? 8080; // See https://pub.dev/documentation/shelf/latest/shelf/Cascade-class.html final cascade = Cascade() @@ -63,8 +63,11 @@ String _jsonEncode(Object? data) => const _jsonHeaders = {'content-type': 'application/json'}; Response _sumHandler(Request request, String a, String b) { - final aNum = int.parse(a); - final bNum = int.parse(b); + final aNum = int.tryParse(a); + final bNum = int.tryParse(b); + if (aNum == null || bNum == null) { + return Response.badRequest(body: 'Invalid integer arguments'); + } return Response.ok( _jsonEncode({'a': aNum, 'b': bNum, 'sum': aNum + bNum}), headers: { diff --git a/server/simple/public/dashland.jpeg b/server/simple/public/dashland.jpeg deleted file mode 100644 index 926a6ab..0000000 Binary files a/server/simple/public/dashland.jpeg and /dev/null differ diff --git a/server/simple/public/index.html b/server/simple/public/index.html index eb48f8c..c3f982c 100644 --- a/server/simple/public/index.html +++ b/server/simple/public/index.html @@ -7,57 +7,36 @@ pkg:shelf example + - - -

pkg:shelf example

-Dashland -

This file is being served by package:shelf_static from the - /public directory.

- -

This app uses package:shelf_router to define several - code-based handlers:

- - -

- The last handler is defined with regular expressions to handle requests to /sum/1/2 - and /sum/35/7, - but not /sum/bob/john or /sum/1/2/3. -

- - + Welcome Dart Server +

This application is built using the following packages:

+
+
package:shelf
+
Provides the core HTTP server middleware and request/response handling.
+
package:shelf_static
+
Serves static files from the local filesystem (including this page and images from the /public + directory).
+
package:shelf_router
+
Defines dynamic route handlers and regular expression URL pattern matching.
+
+ +

The router defines several code-based handlers:

+ + + diff --git a/server/simple/public/style.css b/server/simple/public/style.css new file mode 100644 index 0000000..34ab316 --- /dev/null +++ b/server/simple/public/style.css @@ -0,0 +1,134 @@ +:root { + --bg-color: #f8fafc; + --text-color: #1e293b; + --link-color: #2563eb; + --link-hover: #1d4ed8; + --card-bg: #ffffff; + --border-color: #e2e8f0; + --code-bg: #f1f5f9; + --accent-color: #0284c7; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-color: #0f172a; + --text-color: #f1f5f9; + --link-color: #38bdf8; + --link-hover: #7dd3fc; + --card-bg: #1e293b; + --border-color: #334155; + --code-bg: #0b0f19; + --accent-color: #38bdf8; + } +} + +body { + display: flex; + flex-direction: column; + align-items: center; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + Oxygen, Ubuntu, Cantarell, sans-serif; + background-color: var(--bg-color); + color: var(--text-color); + margin: 0; + padding: 2rem 1rem; + line-height: 1.6; +} + +p { + margin-top: 1rem; + font-size: 1.1rem; + max-width: 650px; + width: 100%; +} + +a { + color: var(--link-color); + text-decoration: none; + font-weight: 500; + transition: color 0.2s ease-in-out; +} + +a:hover { + color: var(--link-hover); + text-decoration: underline; +} + +img { + max-width: 100%; + height: auto; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 8px 10px -6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; +} + +img:hover { + transform: scale(1.02); +} + +code { + background-color: var(--code-bg); + padding: 0.2rem 0.4rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.95em; + color: var(--accent-color); +} + +dl, +ul { + background-color: var(--card-bg); + border: 1px solid var(--border-color); + padding: 1.5rem 2rem; + max-width: 650px; + width: 100%; + box-sizing: border-box; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); + margin: 1rem 0; +} + +dt { + font-weight: 700; + font-size: 1.2rem; + margin-top: 1rem; + color: var(--link-color); +} + +dt:first-child { + margin-top: 0; +} + +dd { + margin-bottom: 1.2rem; + margin-left: 0; + padding-left: 1rem; + border-left: 3px solid var(--link-color); + color: var(--text-color); +} + +ul { + list-style-type: none; + padding-left: 1.5rem; +} + +li { + margin-bottom: 0.5rem; + position: relative; +} + +li::before { + content: "•"; + color: var(--link-color); + font-weight: bold; + display: inline-block; + width: 1em; + margin-left: -1em; +} + +iframe { + border: 1px solid var(--border-color); + background-color: var(--card-bg); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + margin-top: 2rem; + max-width: 100%; + box-sizing: border-box; +} diff --git a/server/simple/public/welcome_dart_server.png b/server/simple/public/welcome_dart_server.png new file mode 100644 index 0000000..d71a42d Binary files /dev/null and b/server/simple/public/welcome_dart_server.png differ diff --git a/server/simple/pubspec.yaml b/server/simple/pubspec.yaml index 04d6a43..1613ab5 100644 --- a/server/simple/pubspec.yaml +++ b/server/simple/pubspec.yaml @@ -10,7 +10,9 @@ dependencies: shelf_static: ^1.1.3 dev_dependencies: + args: ^2.7.0 http: ^1.6.0 lints: ^6.0.0 + path: ^1.9.0 test: ^1.27.0 test_process: ^2.1.1 diff --git a/server/simple/test/test_definitions.dart b/server/simple/test/test_definitions.dart index 23cceb5..e2768a2 100644 --- a/server/simple/test/test_definitions.dart +++ b/server/simple/test/test_definitions.dart @@ -40,4 +40,17 @@ void runTests( expect(response.statusCode, 200); expect(response.body, contains('pkg:shelf example')); }); + + testServer('sum', (host) async { + var response = await get(Uri.parse('$host/sum/1/2')); + expect(response.statusCode, 200); + expect(response.body, contains('"sum": 3')); + + // Test integer overflow / invalid integer handling. + response = await get( + Uri.parse('$host/sum/9999999999999999999999999999999999999999999/2'), + ); + expect(response.statusCode, 400); + expect(response.body, 'Invalid integer arguments'); + }); } diff --git a/server/simple/tool/deploy_source.dart b/server/simple/tool/deploy_source.dart new file mode 100644 index 0000000..640fe89 --- /dev/null +++ b/server/simple/tool/deploy_source.dart @@ -0,0 +1,209 @@ +// Copyright (c) 2026, 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. + +import 'dart:io' hide exit; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as p; + +Future main(List arguments) async { + ArgResults results; + try { + results = _parser.parse(arguments); + } on ArgParserException catch (e) { + stderr.writeln('Error: ${e.message}'); + _printUsage(_parser); + exitCode = 1; + return; + } + + if (results['help'] as bool) { + _printUsage(_parser); + return; + } + + // Ensure script is run from the project root. + var scriptFile = File(Platform.script.toFilePath()); + var expectedDir = scriptFile.parent.parent; + var currentDir = Directory.current; + + if (p.canonicalize(currentDir.path) != p.canonicalize(expectedDir.path)) { + stderr.writeln('Error: This script must be run from: ${expectedDir.path}'); + exitCode = 1; + return; + } + + var project = _resolveOption( + results: results, + optionName: 'project', + envVarName: 'GCP_PROJECT', + warningMessage: + 'GCP_PROJECT environment variable and --project flag not set. Using default project from gcloud configuration.', + ); + + var serviceName = _resolveOption( + results: results, + optionName: 'service-name', + envVarName: 'SERVICE_NAME', + defaultValue: 'dart-sample', + warningMessage: + 'SERVICE_NAME environment variable and --service-name flag not set', + )!; + + var region = _resolveOption( + results: results, + optionName: 'region', + envVarName: 'GCP_REGION', + defaultValue: 'us-central1', + warningMessage: 'GCP_REGION environment variable and --region flag not set', + )!; + + var buildBinDir = Directory(p.join('build', 'bin')); + if (!buildBinDir.existsSync()) { + buildBinDir.createSync(recursive: true); + } + + // Build and prepare Dart source. + var dartExecutable = Platform.resolvedExecutable; + var compileArgs = [ + 'compile', + 'exe', + p.join('bin', 'server.dart'), + '-o', + p.join('build', 'bin', 'server'), + '--target-arch', + 'x64', + '--target-os', + 'linux', + ]; + + await _runProcess(dartExecutable, compileArgs); + + // Copy public directory to build directory. + var publicDir = Directory('public'); + if (publicDir.existsSync()) { + _copyDirectory(publicDir, Directory(p.join('build', 'public'))); + } else { + stderr.writeln('Warning: public directory not found.'); + } + + // Deploy to Google Cloud Run without using build. + var isWindows = Platform.isWindows; + var gcloudExecutable = isWindows ? 'gcloud.cmd' : 'gcloud'; + var gcloudArgs = [ + 'beta', + 'run', + 'deploy', + serviceName, + if (project != null && project.isNotEmpty) '--project=$project', + '--region=$region', + '--allow-unauthenticated', + '--no-build', + '--base-image=osonly24', + '--source', + 'build', + '--command=bin/server', + ]; + + await _runProcess(gcloudExecutable, gcloudArgs); +} + +void _validateArgument(String name, String value) { + if (value.contains('&') || + value.contains('|') || + value.contains(';') || + value.contains('<') || + value.contains('>')) { + throw ArgumentError('Invalid characters in argument $name.'); + } +} + +void _printUsage(ArgParser parser) { + print('Usage: dart tool/deploy_source.dart [options]\n'); + print(parser.usage); +} + +Future _runProcess(String executable, List args) async { + print('Running: $executable ${args.join(' ')}'); + var process = await Process.start( + executable, + args, + mode: ProcessStartMode.inheritStdio, + runInShell: Platform.isWindows, + ); + var exitCode = await process.exitCode; + if (exitCode != 0) { + throw ProcessException( + executable, + args, + 'Command failed with exit code $exitCode', + exitCode, + ); + } +} + +void _copyDirectory(Directory source, Directory destination) { + if (!destination.existsSync()) { + destination.createSync(recursive: true); + } + for (var entity in source.listSync(recursive: false)) { + var newPath = p.join(destination.path, p.basename(entity.path)); + if (entity is Directory) { + _copyDirectory(entity, Directory(newPath)); + } else if (entity is File) { + entity.copySync(newPath); + } + } +} + +String? _resolveOption({ + required ArgResults results, + required String optionName, + required String envVarName, + String? defaultValue, + required String warningMessage, +}) { + var value = + results[optionName] as String? ?? Platform.environment[envVarName]; + if (value == null || value.isEmpty) { + if (defaultValue != null) { + stderr.writeln( + "Warning: $warningMessage, defaulting to '$defaultValue'.", + ); + } else { + stderr.writeln('Warning: $warningMessage'); + } + value = defaultValue; + } + if (value != null) { + _validateArgument(optionName, value); + } + return value; +} + +final _parser = ArgParser() + ..addOption( + 'project', + abbr: 'p', + help: + 'GCP Project ID (defaults to GCP_PROJECT environment variable or gcloud default).', + ) + ..addOption( + 'service-name', + abbr: 's', + help: + 'Cloud Run service name (defaults to SERVICE_NAME environment variable or "dart-sample").', + ) + ..addOption( + 'region', + abbr: 'r', + help: + 'GCP region (defaults to GCP_REGION environment variable or "us-central1").', + ) + ..addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Show this help message.', + ); diff --git a/server/simple/tool/deploy_source.sh b/server/simple/tool/deploy_source.sh deleted file mode 100755 index a896da5..0000000 --- a/server/simple/tool/deploy_source.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# This script deploys the server to Google Cloud Run using source deployment. -# See https://docs.cloud.google.com/docs/buildpacks/osonly -# This feature (as of 2026-01-08) is in "Preview" and requires the "beta" gcloud component. - -set -e - -# Get the directory of this script. -SCRIPT_DIR=$(dirname "$(readlink -f "$0")") -# Get the parent directory of the script's directory. -EXPECTED_DIR=$(dirname "$SCRIPT_DIR") -CURRENT_DIR=$(pwd) - -if [ "$CURRENT_DIR" != "$EXPECTED_DIR" ]; then - echo "Error: This script must be run from: $EXPECTED_DIR" >&2 - exit 1 -fi - -# Configure project from environment variables. -DEFAULT_SERVICE_NAME="dart-sample" -DEFAULT_GCP_REGION="us-central1" - -if [ -z "${GCP_PROJECT}" ]; then - echo "Warning: GCP_PROJECT environment variable not set. Using default project from gcloud configuration." >&2 - PROJECT_ARG="" -else - PROJECT_ARG="--project=${GCP_PROJECT}" -fi - -if [ -z "${SERVICE_NAME}" ]; then - echo "Warning: SERVICE_NAME environment variable not set, defaulting to '${DEFAULT_SERVICE_NAME}'." >&2 - SERVICE_NAME="${DEFAULT_SERVICE_NAME}" -fi - -if [ -z "${GCP_REGION}" ]; then - echo "Warning: GCP_REGION environment variable not set, defaulting to '${DEFAULT_GCP_REGION}'." >&2 - GCP_REGION="${DEFAULT_GCP_REGION}" -fi - -BUILD_DIR="build" -BINARY_LOCATION="bin/server" - -# Build and prepare Dart source. -mkdir -p ${BUILD_DIR}/bin -dart compile exe bin/server.dart -o ${BUILD_DIR}/${BINARY_LOCATION} --target-arch x64 --target-os linux -cp -r public ${BUILD_DIR}/ - -# Deploy to Google Cloud Run without using build. -gcloud beta run deploy ${SERVICE_NAME} \ - ${PROJECT_ARG} \ - --region=${GCP_REGION} \ - --allow-unauthenticated \ - --no-build \ - --base-image=osonly24 \ - --source ${BUILD_DIR} \ - --command=${BINARY_LOCATION}