diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b7f6a82349130010063d195f1b632547c8adf022 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Auto-generated VScode +.vs \ No newline at end of file diff --git a/starter-kit/LICENSE b/starter-kit/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ba3c9d72735ee7be588d66eb2b748b2f546b3913 --- /dev/null +++ b/starter-kit/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Flutter Python Starter Kit +Copyright (c) 2023 Maxim Saplin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/starter-kit/bundle-python.sh b/starter-kit/bundle-python.sh new file mode 100644 index 0000000000000000000000000000000000000000..df34ab74578e10b220c78987f7f57abb4c1250d9 --- /dev/null +++ b/starter-kit/bundle-python.sh @@ -0,0 +1,95 @@ +set -e # halt on any error +#set -x + +flutterDir="" +pythonDir="" +exeName="server_py_flutter" +nuitka=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --proto) + proto="$2" + shift 2 + ;; + --flutterDir) + flutterDir="$2" + shift 2 + ;; + --pythonDir) + pythonDir="$2" + shift 2 + ;; + --exeName) + exeName="$2" + shift 2 + ;; + --nuitka) # Set `nuitka` to `true` if there's a flag `--nuitka` + nuitka=true + shift + ;; + *) + shift + ;; + esac +done + +flutterDir=$(realpath "$flutterDir" | sed 's/\/$//') +pythonDir=$(realpath "$pythonDir" | sed 's/\/$//') +workingDir=$(pwd) + +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + PYTHON=python +else + PYTHON=python3 +fi + +# Check the OS +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + exeNameFull="${exeName}_win" +elif [[ "$OSTYPE" == "darwin"* ]]; then + exeNameFull="${exeName}_osx" +else + exeNameFull="${exeName}_lnx" +fi + +$PYTHON -m pip install -r $pythonDir/requirements.txt + +cd $pythonDir +if [[ $nuitka == true ]]; then + export PYTHONPATH="./grpc_generated" + $PYTHON -m nuitka server.py --standalone --onefile --output-dir=./dist --output-filename="$exeNameFull" +else + $PYTHON -m PyInstaller --onefile --noconfirm --clean --log-level=WARN --name="$exeNameFull" --paths="./grpc_generated" server.py +fi +cd $workingDir + +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + exeNameFull="$exeNameFull.exe" +fi + +mkdir -p $flutterDir/assets/ +cp $pythonDir/dist/$exeNameFull $flutterDir/assets/ + +# Check if assets already exists in pubspec.yaml +if ! grep -q "assets:" "$flutterDir"/pubspec.yaml; then + echo " assets:" >> "$flutterDir"/pubspec.yaml + echo " - assets/" >> "$flutterDir"/pubspec.yaml +fi + +# Get current date time and create a string of the following format '2023_09_02_10_24_56' +currentDateTime=$(date +"%Y_%m_%d_%H_%M_%S") + +# Update version in py_file_info.dart + +numLines=$(wc -l < "$flutterDir"/lib/grpc_generated/py_file_info.dart) +lastLine=$((numLines - 2)) +head -n $lastLine "$flutterDir"/lib/grpc_generated/py_file_info.dart > "$flutterDir"/lib/grpc_generated/py_file_info_temp.dart +mv "$flutterDir"/lib/grpc_generated/py_file_info_temp.dart "$flutterDir"/lib/grpc_generated/py_file_info.dart + +echo "const exeFileName = '$exeName';" >> "$flutterDir"/lib/grpc_generated/py_file_info.dart +echo "const currentFileVersionFromAssets = '$currentDateTime';" >> "$flutterDir"/lib/grpc_generated/py_file_info.dart + +GREEN='\033[0;32m' +NC='\033[0m' +echo -e "\n${GREEN}Python built and put to $flutterDir/assets/$exeNameFull${NC}" \ No newline at end of file diff --git a/starter-kit/prepare-sources.sh b/starter-kit/prepare-sources.sh new file mode 100644 index 0000000000000000000000000000000000000000..f966b902b49acf21c6418448c310e2e1cc62f25f --- /dev/null +++ b/starter-kit/prepare-sources.sh @@ -0,0 +1,219 @@ +#!/bin/bash +set -e # halt on any error +#set -x + +proto="" +flutterDir="" +pythonDir="" +exeName="server_py_flutter" + +# Loop through command line arguments + +while [[ $# -gt 0 ]]; do + case "$1" in + --proto) + proto="$2" + shift 2 + ;; + --flutterDir) + flutterDir="$2" + shift 2 + ;; + --pythonDir) + pythonDir="$2" + shift 2 + ;; + --exeName) + exeName="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +if [[ -z "$proto" ]]; then + echo "Error: Missing required parameter '--proto'" + exit 1 +fi + +if [[ ! -f "$proto" ]]; then + echo "Error: Protofile '$proto' does not exist" + exit 1 +fi + +if [[ -z "$flutterDir" ]]; then + echo "Error: Missing required parameter '--flutterDir'" + exit 1 +fi + +if [[ -z "$pythonDir" ]]; then + echo "Error: Missing required parameter '--pythonDir'" + exit 1 +fi + +mkdir -p $flutterDir +mkdir -p $pythonDir + +# Convert flutterDir and pythonDir to absolute paths +flutterDir=$(realpath "$flutterDir" | sed 's/\/$//') +pythonDir=$(realpath "$pythonDir" | sed 's/\/$//') +scriptDir=$(dirname "$(realpath "$0")") +workingDir=$(pwd) +protoDir=$(dirname "$proto" | sed 's/\/$//') +grpcGeneratedDir=$flutterDir/lib/grpc_generated +protoFile=$(basename "$proto") + +serviceName=$(basename "$proto" .proto) + +# Set the Python interpreter name +if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + PYTHON=python + grpcGeneratedDir=$(cygpath -w "$grpcGeneratedDir") +else + PYTHON=python3 +fi + + +flagFile="$protoDir/.starterDependenciesInstalled" + +if [ ! -f "$flagFile" ]; then + echo "Initializing dependencies" + echo "$OSTYPE" + # Prepare Dart/Flutter + # Update the installation command for different operating systems + + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + brew install protobuf + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux + sudo apt install protobuf-compiler + sudo apt install python3-pip + elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then + # Windows + choco install protoc + else + echo "Error: Unsupported operating system" + exit 1 + fi + + + # Prepare Dart/Flutter + flutter pub global activate protoc_plugin + + # Prepare Python dependencies + # pip3 install -r requirements.txt + $PYTHON -m pip install grpcio + $PYTHON -m pip install grpcio-tools + $PYTHON -m pip install tinyaes + $PYTHON -m pip install pyinstaller + $PYTHON -m pip install nuitka + touch "$flagFile" +fi + +export PATH="$PATH":"$HOME/.pub-cache/bin" # make Dart's protoc_plugin available + +# Print the file name with extension +echo "$fileNameWithExtension" + +# Generate Dart code +mkdir -p $grpcGeneratedDir +cd $protoDir # changing dir to avoid created nexted folders in --dart_out beacause of implicitly following grpc namespaces +protoc --dart_out=grpc:"$grpcGeneratedDir" $protoFile +echo "$(pwd)" +cd $workingDir + +cd $flutterDir +flutter pub add grpc || echo "Can't add `grpc` to Flutter project, continuing..." +flutter pub add protobuf || echo "Can't add `protobuf` to Flutter project, continuing..." +flutter pub add path_provider || echo "Can't add 'path_provider' to Flutter project, continuing..." +flutter pub add path || echo "Can't add 'path' to Flutter project, continuing..." + +# macOS, update entitlements files and disable sandbox +entitlements_file_1="macos/Runner/DebugProfile.entitlements" +entitlements_file_2="macos/Runner/Release.entitlements" + +if [ -f "$entitlements_file_1" ]; then + # H;1h;$!d;x; - this part enables whole file processing (rather than line-by-line) + entitlements_content=$(echo "$entitlements_content" | sed 'H;1h;$!d;x; s/<key>com\.apple\.security\.app-sandbox<\/key>[[:space:]]*<true\/>/<key>com.apple.security.app-sandbox<\/key>\n\t<false\/>/' "$entitlements_file_1") + echo "$entitlements_content" > "$entitlements_file_1" +fi + +if [ -f "$entitlements_file_2" ]; then + entitlements_content=$(echo "$entitlements_content" | sed 'H;1h;$!d;x; s/<key>com\.apple\.security\.app-sandbox<\/key>[[:space:]]*<true\/>/<key>com.apple.security.app-sandbox<\/key>\n\t<false\/>/' "$entitlements_file_2") + echo "$entitlements_content" > "$entitlements_file_2" +fi + +# Dart clients +if [[ ! -f "$grpcGeneratedDir/client.dart" ]]; then + cp "$scriptDir/templates/client.dart" "$grpcGeneratedDir/client.dart" +fi +if [[ ! -f "$grpcGeneratedDir/client_native.dart" ]]; then + cp "$scriptDir/templates/client_native.dart" "$grpcGeneratedDir/client_native.dart" +fi +if [[ ! -f "$grpcGeneratedDir/client_web.dart" ]]; then + cp "$scriptDir/templates/client_web.dart" "$grpcGeneratedDir/client_web.dart" +fi + +if [ ! -f "$grpcGeneratedDir/init_py.dart" ]; then + cp "$scriptDir/templates/init_py.dart" "$grpcGeneratedDir/init_py.dart" +fi +if [ ! -f "$grpcGeneratedDir/init_py_native.dart" ]; then + cp "$scriptDir/templates/init_py_native.dart" "$grpcGeneratedDir/init_py_native.dart" +fi +if [ ! -f "$grpcGeneratedDir/init_py_web.dart" ]; then + cp "$scriptDir/templates/init_py_web.dart" "$grpcGeneratedDir/init_py_web.dart" +fi +if [ ! -f "$grpcGeneratedDir/health.pb.dart" ]; then + cp "$scriptDir/templates/health.pb.dart" "$grpcGeneratedDir/health.pb.dart" +fi +if [ ! -f "$grpcGeneratedDir/health.pbenum.dart" ]; then + cp "$scriptDir/templates/health.pbenum.dart" "$grpcGeneratedDir/health.pbenum.dart" +fi +if [ ! -f "$grpcGeneratedDir/health.pbgrpc.dart" ]; then + cp "$scriptDir/templates/health.pbgrpc.dart" "$grpcGeneratedDir/health.pbgrpc.dart" +fi +if [ ! -f "$grpcGeneratedDir/health.pbjson.dart" ]; then + cp "$scriptDir/templates/health.pbjson.dart" "$grpcGeneratedDir/health.pbjson.dart" +fi + + + +echo "// !Will be rewriten upon \`prepare sources\` or \`build\` actions by Flutter-Python starter kit" > $grpcGeneratedDir/py_file_info.dart +echo "const versionFileName = 'server_py_version.txt';" >> $grpcGeneratedDir/py_file_info.dart +echo "const exeFileName = '$exeName';" >> $grpcGeneratedDir/py_file_info.dart +echo "const currentFileVersionFromAssets = '';" >> $grpcGeneratedDir/py_file_info.dart + +cd $workingDir + +# Generate Python code +mkdir -p $pythonDir +mkdir -p $pythonDir/grpc_generated +cd $protoDir # changing dir to avoid created nested folders in --dart_out beacause of implicitly following grpc namespaces +$PYTHON -m grpc_tools.protoc -I. --python_out=$pythonDir/grpc_generated --grpc_python_out=$pythonDir/grpc_generated $protoFile +cd $workingDir +cp $scriptDir/templates/__init__.py $pythonDir/grpc_generated + +# Pyhton boilderplate code for running self-hosted gRPC server +serverpy=$(cat << EOF +$(<$scriptDir/templates/server.py) +EOF +) + +if [ ! -f "$pythonDir/server.py" ]; then +echo "${serverpy//\$\{serviceName\}/$serviceName}" > "$pythonDir/server.py" +fi + +if ! grep -q "^grpcio" "$pythonDir/requirements.txt"; then + echo -e "\ngrpcio" >> "$pythonDir/requirements.txt" +fi + +if ! grep -q "^grpcio-health-checking" "$pythonDir/requirements.txt"; then + echo -e "\ngrpcio-health-checking" >> "$pythonDir/requirements.txt" +fi + +GREEN='\033[0;32m' +NC='\033[0m' +echo -e "\n${GREEN}Dart/Flutter and Python bindings have been generated for '$proto' definition${NC}" \ No newline at end of file diff --git a/starter-kit/templates/LICENSE b/starter-kit/templates/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ba3c9d72735ee7be588d66eb2b748b2f546b3913 --- /dev/null +++ b/starter-kit/templates/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Flutter Python Starter Kit +Copyright (c) 2023 Maxim Saplin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/starter-kit/templates/__init__.py b/starter-kit/templates/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d65abb68f622e9ffc641267f175c3e82aed2fb42 --- /dev/null +++ b/starter-kit/templates/__init__.py @@ -0,0 +1,3 @@ +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) \ No newline at end of file diff --git a/starter-kit/templates/client.dart b/starter-kit/templates/client.dart new file mode 100644 index 0000000000000000000000000000000000000000..c5679668a567e1c02df2c392be04e76a4e70474d --- /dev/null +++ b/starter-kit/templates/client.dart @@ -0,0 +1,36 @@ +import 'package:grpc/grpc_connection_interface.dart'; +// Conditional imports to enable single gRPC client creation for native and Web platfrom +import 'client_native.dart' + if (dart.library.io) 'client_native.dart' + if (dart.library.html) 'client_web.dart'; + +Map<String, ClientChannelBase> _clientChannelMap = {}; + +int? _port; + +int get defaultPort => _port ?? 50055; + +/// Override the default port where the client will attempt to connect +set defaultPort(int? value) { + _port = value; +} + +String _defaultHost = 'localhost'; + +String get defaultHost => _defaultHost; + +/// Set the default host address +set defaultHost(String value) { + _defaultHost = value; +} + +/// Lazily creates client channel, for each host/port pair there's one channel created and stored internally. +/// You can use this channel to instatiate specigic client, i.e. `MyCleint(getClientChannel())` +/// Parameters: +/// - `host`: The host address. Default value is [defaultHost]. +/// - `port`: The port number. If not provided, the [defaultPort] value will be used. +ClientChannelBase getClientChannel({String? host, int? port}) { + _clientChannelMap['$host:$port'] ??= + getGrpcClientChannel(host ?? defaultHost, port ?? defaultPort, false); + return _clientChannelMap['$host:$port']!; +} diff --git a/starter-kit/templates/client_native.dart b/starter-kit/templates/client_native.dart new file mode 100644 index 0000000000000000000000000000000000000000..1c89c0ccd5cf1990345670d35ba9418f5b912e48 --- /dev/null +++ b/starter-kit/templates/client_native.dart @@ -0,0 +1,13 @@ +import 'package:grpc/grpc.dart'; +import 'package:grpc/grpc_connection_interface.dart'; + +ClientChannelBase getGrpcClientChannel(String host, int port, bool useHttps) { + return ClientChannel( + host, + port: port, + options: ChannelOptions( + credentials: useHttps + ? const ChannelCredentials.secure() + : const ChannelCredentials.insecure()), + ); +} diff --git a/starter-kit/templates/client_web.dart b/starter-kit/templates/client_web.dart new file mode 100644 index 0000000000000000000000000000000000000000..b8e37630385ef19f940b808f24dbd2f7f26b38d0 --- /dev/null +++ b/starter-kit/templates/client_web.dart @@ -0,0 +1,7 @@ +import 'package:grpc/grpc_connection_interface.dart'; +import 'package:grpc/grpc_web.dart'; + +ClientChannelBase getGrpcClientChannel(String host, int port, bool useHttp) { + return GrpcWebClientChannel.xhr( + Uri.parse('http${useHttp ? 's' : ''}://$host:$port')); +} diff --git a/starter-kit/templates/health.pb.dart b/starter-kit/templates/health.pb.dart new file mode 100644 index 0000000000000000000000000000000000000000..16ee88eb374f87b6a4a65085ac8b27b90ad818a0 --- /dev/null +++ b/starter-kit/templates/health.pb.dart @@ -0,0 +1,122 @@ +// +// Generated code. Do not modify. +// source: health.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +import 'health.pbenum.dart'; + +export 'health.pbenum.dart'; + +class HealthCheckRequest extends $pb.GeneratedMessage { + factory HealthCheckRequest({ + $core.String? service, + }) { + final $result = create(); + if (service != null) { + $result.service = service; + } + return $result; + } + HealthCheckRequest._() : super(); + factory HealthCheckRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory HealthCheckRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'HealthCheckRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'grpc.health.v1'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'service') + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + HealthCheckRequest clone() => HealthCheckRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + HealthCheckRequest copyWith(void Function(HealthCheckRequest) updates) => super.copyWith((message) => updates(message as HealthCheckRequest)) as HealthCheckRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HealthCheckRequest create() => HealthCheckRequest._(); + HealthCheckRequest createEmptyInstance() => create(); + static $pb.PbList<HealthCheckRequest> createRepeated() => $pb.PbList<HealthCheckRequest>(); + @$core.pragma('dart2js:noInline') + static HealthCheckRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<HealthCheckRequest>(create); + static HealthCheckRequest? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get service => $_getSZ(0); + @$pb.TagNumber(1) + set service($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasService() => $_has(0); + @$pb.TagNumber(1) + void clearService() => clearField(1); +} + +class HealthCheckResponse extends $pb.GeneratedMessage { + factory HealthCheckResponse({ + HealthCheckResponse_ServingStatus? status, + }) { + final $result = create(); + if (status != null) { + $result.status = status; + } + return $result; + } + HealthCheckResponse._() : super(); + factory HealthCheckResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory HealthCheckResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'HealthCheckResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'grpc.health.v1'), createEmptyInstance: create) + ..e<HealthCheckResponse_ServingStatus>(1, _omitFieldNames ? '' : 'status', $pb.PbFieldType.OE, defaultOrMaker: HealthCheckResponse_ServingStatus.UNKNOWN, valueOf: HealthCheckResponse_ServingStatus.valueOf, enumValues: HealthCheckResponse_ServingStatus.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + HealthCheckResponse clone() => HealthCheckResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + HealthCheckResponse copyWith(void Function(HealthCheckResponse) updates) => super.copyWith((message) => updates(message as HealthCheckResponse)) as HealthCheckResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static HealthCheckResponse create() => HealthCheckResponse._(); + HealthCheckResponse createEmptyInstance() => create(); + static $pb.PbList<HealthCheckResponse> createRepeated() => $pb.PbList<HealthCheckResponse>(); + @$core.pragma('dart2js:noInline') + static HealthCheckResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<HealthCheckResponse>(create); + static HealthCheckResponse? _defaultInstance; + + @$pb.TagNumber(1) + HealthCheckResponse_ServingStatus get status => $_getN(0); + @$pb.TagNumber(1) + set status(HealthCheckResponse_ServingStatus v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasStatus() => $_has(0); + @$pb.TagNumber(1) + void clearStatus() => clearField(1); +} + + +const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); +const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/starter-kit/templates/health.pbenum.dart b/starter-kit/templates/health.pbenum.dart new file mode 100644 index 0000000000000000000000000000000000000000..58fc4f8b48fbca0d055879a21497505a5b7e2533 --- /dev/null +++ b/starter-kit/templates/health.pbenum.dart @@ -0,0 +1,36 @@ +// +// Generated code. Do not modify. +// source: health.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class HealthCheckResponse_ServingStatus extends $pb.ProtobufEnum { + static const HealthCheckResponse_ServingStatus UNKNOWN = HealthCheckResponse_ServingStatus._(0, _omitEnumNames ? '' : 'UNKNOWN'); + static const HealthCheckResponse_ServingStatus SERVING = HealthCheckResponse_ServingStatus._(1, _omitEnumNames ? '' : 'SERVING'); + static const HealthCheckResponse_ServingStatus NOT_SERVING = HealthCheckResponse_ServingStatus._(2, _omitEnumNames ? '' : 'NOT_SERVING'); + static const HealthCheckResponse_ServingStatus SERVICE_UNKNOWN = HealthCheckResponse_ServingStatus._(3, _omitEnumNames ? '' : 'SERVICE_UNKNOWN'); + + static const $core.List<HealthCheckResponse_ServingStatus> values = <HealthCheckResponse_ServingStatus> [ + UNKNOWN, + SERVING, + NOT_SERVING, + SERVICE_UNKNOWN, + ]; + + static final $core.Map<$core.int, HealthCheckResponse_ServingStatus> _byValue = $pb.ProtobufEnum.initByValue(values); + static HealthCheckResponse_ServingStatus? valueOf($core.int value) => _byValue[value]; + + const HealthCheckResponse_ServingStatus._($core.int v, $core.String n) : super(v, n); +} + + +const _omitEnumNames = $core.bool.fromEnvironment('protobuf.omit_enum_names'); diff --git a/starter-kit/templates/health.pbgrpc.dart b/starter-kit/templates/health.pbgrpc.dart new file mode 100644 index 0000000000000000000000000000000000000000..bb346137fee1e942d94ffddf7eee9d384b0eca9e --- /dev/null +++ b/starter-kit/templates/health.pbgrpc.dart @@ -0,0 +1,79 @@ +// +// Generated code. Do not modify. +// source: health.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:async' as $async; +import 'dart:core' as $core; + +import 'package:grpc/service_api.dart' as $grpc; +import 'package:protobuf/protobuf.dart' as $pb; + +import 'health.pb.dart' as $0; + +export 'health.pb.dart'; + +@$pb.GrpcServiceName('grpc.health.v1.Health') +class HealthClient extends $grpc.Client { + static final _$check = $grpc.ClientMethod<$0.HealthCheckRequest, $0.HealthCheckResponse>( + '/grpc.health.v1.Health/Check', + ($0.HealthCheckRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.HealthCheckResponse.fromBuffer(value)); + static final _$watch = $grpc.ClientMethod<$0.HealthCheckRequest, $0.HealthCheckResponse>( + '/grpc.health.v1.Health/Watch', + ($0.HealthCheckRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.HealthCheckResponse.fromBuffer(value)); + + HealthClient($grpc.ClientChannel channel, + {$grpc.CallOptions? options, + $core.Iterable<$grpc.ClientInterceptor>? interceptors}) + : super(channel, options: options, + interceptors: interceptors); + + $grpc.ResponseFuture<$0.HealthCheckResponse> check($0.HealthCheckRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$check, request, options: options); + } + + $grpc.ResponseStream<$0.HealthCheckResponse> watch($0.HealthCheckRequest request, {$grpc.CallOptions? options}) { + return $createStreamingCall(_$watch, $async.Stream.fromIterable([request]), options: options); + } +} + +@$pb.GrpcServiceName('grpc.health.v1.Health') +abstract class HealthServiceBase extends $grpc.Service { + $core.String get $name => 'grpc.health.v1.Health'; + + HealthServiceBase() { + $addMethod($grpc.ServiceMethod<$0.HealthCheckRequest, $0.HealthCheckResponse>( + 'Check', + check_Pre, + false, + false, + ($core.List<$core.int> value) => $0.HealthCheckRequest.fromBuffer(value), + ($0.HealthCheckResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.HealthCheckRequest, $0.HealthCheckResponse>( + 'Watch', + watch_Pre, + false, + true, + ($core.List<$core.int> value) => $0.HealthCheckRequest.fromBuffer(value), + ($0.HealthCheckResponse value) => value.writeToBuffer())); + } + + $async.Future<$0.HealthCheckResponse> check_Pre($grpc.ServiceCall call, $async.Future<$0.HealthCheckRequest> request) async { + return check(call, await request); + } + + $async.Stream<$0.HealthCheckResponse> watch_Pre($grpc.ServiceCall call, $async.Future<$0.HealthCheckRequest> request) async* { + yield* watch(call, await request); + } + + $async.Future<$0.HealthCheckResponse> check($grpc.ServiceCall call, $0.HealthCheckRequest request); + $async.Stream<$0.HealthCheckResponse> watch($grpc.ServiceCall call, $0.HealthCheckRequest request); +} diff --git a/starter-kit/templates/health.pbjson.dart b/starter-kit/templates/health.pbjson.dart new file mode 100644 index 0000000000000000000000000000000000000000..bf29fbb60cf7217785871b3c2328cbbdd856c73d --- /dev/null +++ b/starter-kit/templates/health.pbjson.dart @@ -0,0 +1,54 @@ +// +// Generated code. Do not modify. +// source: health.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:convert' as $convert; +import 'dart:core' as $core; +import 'dart:typed_data' as $typed_data; + +@$core.Deprecated('Use healthCheckRequestDescriptor instead') +const HealthCheckRequest$json = { + '1': 'HealthCheckRequest', + '2': [ + {'1': 'service', '3': 1, '4': 1, '5': 9, '10': 'service'}, + ], +}; + +/// Descriptor for `HealthCheckRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List healthCheckRequestDescriptor = $convert.base64Decode( + 'ChJIZWFsdGhDaGVja1JlcXVlc3QSGAoHc2VydmljZRgBIAEoCVIHc2VydmljZQ=='); + +@$core.Deprecated('Use healthCheckResponseDescriptor instead') +const HealthCheckResponse$json = { + '1': 'HealthCheckResponse', + '2': [ + {'1': 'status', '3': 1, '4': 1, '5': 14, '6': '.grpc.health.v1.HealthCheckResponse.ServingStatus', '10': 'status'}, + ], + '4': [HealthCheckResponse_ServingStatus$json], +}; + +@$core.Deprecated('Use healthCheckResponseDescriptor instead') +const HealthCheckResponse_ServingStatus$json = { + '1': 'ServingStatus', + '2': [ + {'1': 'UNKNOWN', '2': 0}, + {'1': 'SERVING', '2': 1}, + {'1': 'NOT_SERVING', '2': 2}, + {'1': 'SERVICE_UNKNOWN', '2': 3}, + ], +}; + +/// Descriptor for `HealthCheckResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List healthCheckResponseDescriptor = $convert.base64Decode( + 'ChNIZWFsdGhDaGVja1Jlc3BvbnNlEkkKBnN0YXR1cxgBIAEoDjIxLmdycGMuaGVhbHRoLnYxLk' + 'hlYWx0aENoZWNrUmVzcG9uc2UuU2VydmluZ1N0YXR1c1IGc3RhdHVzIk8KDVNlcnZpbmdTdGF0' + 'dXMSCwoHVU5LTk9XThAAEgsKB1NFUlZJTkcQARIPCgtOT1RfU0VSVklORxACEhMKD1NFUlZJQ0' + 'VfVU5LTk9XThAD'); + diff --git a/starter-kit/templates/init_py.dart b/starter-kit/templates/init_py.dart new file mode 100644 index 0000000000000000000000000000000000000000..80264403e40943a464819574295123f25ec16bb7 --- /dev/null +++ b/starter-kit/templates/init_py.dart @@ -0,0 +1,75 @@ +import 'package:app/grpc_generated/health.pbgrpc.dart'; +import 'package:flutter/foundation.dart'; + +import 'client.dart'; +import 'init_py_native.dart' + if (dart.library.io) 'init_py_native.dart' + if (dart.library.html) 'init_py_web.dart'; + +bool localPyStartSkipped = false; + +/// Initialize Python part, start self-hosted server for desktop, await for local +/// or remote gRPC server to respond. If no response is resived within 15 second +/// exception is thrown. +/// Set [doNotStartPy] to `true` if you would like to use remote server +Future<void> initPy([bool doNoStartPy = false]) async { + _initParamsFromEnvVars(doNoStartPy); + + // Launch self-hosted servr or do nothing + await (localPyStartSkipped ? Future(() => null) : initPyImpl()); + + await _waitForServer(); +} + +Future<void> _waitForServer() async { + var cleint = HealthClient(getClientChannel()); + var request = HealthCheckRequest(); + var started = false; + + for (var i = 0; i < 30; i++) { + try { + var r = await cleint.check(request); + + if (r.status == HealthCheckResponse_ServingStatus.SERVING) { + started = true; + break; + } + } catch (_) { + await Future.delayed(const Duration(milliseconds: 500)); + } + } + + if (!started) { + throw "Can't connect to gRPC server"; + } +} + +void _initParamsFromEnvVars(bool doNoStartPy) { + // Hack to get access to --dart-define values in the web https://stackoverflow.com/questions/65647090/access-dart-define-environment-variables-inside-index-html + if (kIsWeb) { + initPyImpl(); + } + + var flag = const String.fromEnvironment('useRemote', defaultValue: 'false') == + 'true'; + if (doNoStartPy || flag) { + localPyStartSkipped = true; + } + + var hostOverride = const String.fromEnvironment('host', defaultValue: ''); + if (hostOverride.isNotEmpty) { + defaultHost = hostOverride; + } + + var portOverride = + int.tryParse(const String.fromEnvironment('port', defaultValue: '')); + if (portOverride != null) { + defaultPort = portOverride; + } +} + +/// Searches for any processes that match Python server and kills those. +/// Does nothing in the Web environment. +Future<void> shutdownPyIfAny() { + return shutdownPyIfAnyImpl(); +} diff --git a/starter-kit/templates/init_py_native.dart b/starter-kit/templates/init_py_native.dart new file mode 100644 index 0000000000000000000000000000000000000000..09d8c76dcbeb456a474018269a00aad180c654fe --- /dev/null +++ b/starter-kit/templates/init_py_native.dart @@ -0,0 +1,119 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +import 'py_file_info.dart'; +import 'client.dart'; + +Future<void> initPyImpl({String host = "localhost", int? port}) async { + var dir = await getApplicationSupportDirectory(); + var filePath = await _prepareExecutable(dir.path); + + // Ask OS to provide a free port if port is null and host is localhost + if (port == null && host == "localhost") { + var serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + port = serverSocket.port; + serverSocket.close(); + defaultPort = port; + } + + await shutdownPyIfAnyImpl(); + + if (defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.linux) { + await Process.run("chmod", ["u+x", filePath]); + } + var p = await Process.start(filePath, [port.toString()]); + + int? exitCode; + + p.exitCode.then((v) { + exitCode = v; + }); + + // Give a couple of seconds to make sure there're no exceptions upon lanuching Python server + + await Future.delayed(const Duration(seconds: 1)); + if (exitCode != null) { + throw 'Failure while launching server process. It stopped right after starting. Exit code: $exitCode'; + } +} + +Future<String> _prepareExecutable(String directory) async { + var file = File(p.join(directory, _getAssetName())); + var versionFile = File(p.join(directory, versionFileName)); + + if (!file.existsSync()) { + ByteData pyExe = + await PlatformAssetBundle().load('assets/${_getAssetName()}'); + await _writeFile(file, pyExe, versionFile); + } else { + // Check version file and asset sizes, version in the file and the constant + // If they do not match or the version file does not exist, update the executable and version file + var versionMismatch = false; + ByteData pyExe = + await PlatformAssetBundle().load('assets/${_getAssetName()}'); + var loadedBinarySize = pyExe.buffer.lengthInBytes; + var currentBinarySize = await file.length(); + if (loadedBinarySize != currentBinarySize) { + versionMismatch = true; + } + + if (!versionFile.existsSync()) { + versionMismatch = true; + } else { + var fileVersion = await versionFile.readAsString(); + + if (fileVersion != currentFileVersionFromAssets) { + versionMismatch = true; + } + } + + if (versionMismatch) { + await _writeFile(file, pyExe, versionFile); + } + } + + return file.path; +} + +Future<void> _writeFile(File file, ByteData pyExe, File versionFile) async { + if (file.existsSync()) { + file.deleteSync(); + } + await file.create(recursive: true); + await file.writeAsBytes(pyExe.buffer.asUint8List()); + await versionFile.writeAsString(currentFileVersionFromAssets); +} + +/// Searches for any processes that match Python server and kills those +Future<void> shutdownPyIfAnyImpl() async { + var name = _getAssetName(); + + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + case TargetPlatform.macOS: + await Process.run('pkill', [name]); + break; + case TargetPlatform.windows: + await Process.run('taskkill', ['/F', '/IM', name]); + break; + default: + break; + } +} + +String _getAssetName() { + var name = ''; + + if (defaultTargetPlatform == TargetPlatform.windows) { + name += '${exeFileName}_win.exe'; + } else if (defaultTargetPlatform == TargetPlatform.macOS) { + name += '${exeFileName}_osx'; + } else if (defaultTargetPlatform == TargetPlatform.linux) { + name += '${exeFileName}_lnx'; + } + return name; +} diff --git a/starter-kit/templates/init_py_web.dart b/starter-kit/templates/init_py_web.dart new file mode 100644 index 0000000000000000000000000000000000000000..f156f4dd2b4d017fb7f0a15d0d60bec944ddb16b --- /dev/null +++ b/starter-kit/templates/init_py_web.dart @@ -0,0 +1,10 @@ +// ignore_for_file: avoid_web_libraries_in_flutter + +import 'dart:html' as html; + +Future<void> initPyImpl() async { + // Fire special dart event pushing env variables to Dart on app start + html.document.dispatchEvent(html.CustomEvent("dart_loaded")); +} + +Future<void> shutdownPyIfAnyImpl() async {} diff --git a/starter-kit/templates/server.py b/starter-kit/templates/server.py new file mode 100644 index 0000000000000000000000000000000000000000..8066fccb961ed1a025e5c8f7d68fac7b9ee0969e --- /dev/null +++ b/starter-kit/templates/server.py @@ -0,0 +1,29 @@ +import sys +from concurrent import futures +import grpc +from grpc_health.v1 import health_pb2_grpc +from grpc_health.v1 import health +# TODO, import generated gRPC stubs +from grpc_generated import service_pb2_grpc +# TODO, import your service implementation +from number_sorting import NumberSortingService + +def serve(): + DEFAULT_PORT = 50055 + # Get the port number from the command line parameter + port = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PORT + HOST = f'localhost:{port}' + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + + # TODO, add your gRPC service to self-hosted server, e.g. + service_pb2_grpc.add_NumberSortingServiceServicer_to_server(NumberSortingService(), server) + health_pb2_grpc.add_HealthServicer_to_server(health.HealthServicer(), server) + + server.add_insecure_port(HOST) + print(f"gRPC server started and listening on {HOST}") + server.start() + server.wait_for_termination() + +if __name__ == '__main__': + serve()