diff --git a/.devcontainer/geospatial-images-vth-plugin/devcontainer.json b/.devcontainer/geospatial-images-vth-plugin/devcontainer.json new file mode 100644 index 0000000..5951dfa --- /dev/null +++ b/.devcontainer/geospatial-images-vth-plugin/devcontainer.json @@ -0,0 +1,124 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "geospatial-images-vth-plugin", + "image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0", + "runArgs": [ + "--name=geospatial-images-vth-plugin" + ], + "workspaceFolder": "/workspace/geospatial-images-vth-plugin", + "workspaceMount": "source=${localWorkspaceFolder}/datagenerators/geospatial-images/plugin,target=/workspace/geospatial-images-vth-plugin,type=bind,consistency=cached", + "features": { + "ghcr.io/microsoft/azure-orbital-space-sdk/spacefx-dev:0.11.0": { + "app_name": "geospatial-images-vth-plugin", + "app_type": "vth-plugin", + "addl_debug_shim_suffixes": "client", + "pull_containers": "datagenerator-geospatial-images:0.11.0-nightly", + "download_artifacts": "datagenerator-geospatial-images.yaml" + } + }, + "mounts": [ + "source=${localWorkspaceFolder}/datagenerators/geospatial-images/datagenerator,target=/workspace/geospatial-images-vth-datagenerator,type=bind,consistency=cached" + ], + "hostRequirements": { + "cpus": 8, + "memory": "8gb" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "ms-dotnettools.csdevkit", + "DavidAnson.vscode-markdownlint", + "zxh404.vscode-proto3", + "mutantdino.resourcemonitor", + "josefpihrt-vscode.roslynator", + "bierner.markdown-mermaid" + ] + }, + // Grant permissions to the Azure Orbital Space SDK repositories and their packages + "codespaces": { + "repositories": { + "microsoft/azure-orbital-space-sdk": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-core": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-setup": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-coresvc-registry": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-coresvc-fileserver": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-coresvc-switchboard": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-platform-mts": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-platform-deployment": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-vth": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-hostsvc-link": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-hostsvc-logging": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-hostsvc-position": { + "permissions": { + "contents": "read", + "packages": "read" + } + }, + "microsoft/azure-orbital-space-sdk-hostsvc-sensor": { + "permissions": { + "contents": "read", + "packages": "read" + } + } + } + } + }, + "remoteUser": "root" +} \ No newline at end of file diff --git a/.github/workflows/geospatial-images-generator-build.yaml b/.github/workflows/geospatial-images-generator-build.yaml index bea1e99..7d78d18 100644 --- a/.github/workflows/geospatial-images-generator-build.yaml +++ b/.github/workflows/geospatial-images-generator-build.yaml @@ -28,8 +28,8 @@ jobs: YAML_FOLDER_PATH: ./datagenerators/geospatial-images/datagenerator/k3s YAML_FILE_NAME: datagenerator-geospatial-images.yaml secrets: - GIT_HUB_USER_NAME: ${{ secrets.GIT_HUB_USER_NAME }} - GIT_HUB_USER_TOKEN: ${{ secrets.GIT_HUB_USER_TOKEN }} + GIT_HUB_USER_NAME: ${{ secrets.TEMP_GITHUB_USERNAME }} + GIT_HUB_USER_TOKEN: ${{ secrets.TEMP_WORKFLOW_TOKEN }} SETUP_REPO_URL: ${{ secrets.SETUP_REPO_URL }} build-datagenerator-geospatial-images-arm64: @@ -50,6 +50,6 @@ jobs: YAML_FOLDER_PATH: ./datagenerators/geospatial-images/datagenerator/k3s YAML_FILE_NAME: datagenerator-geospatial-images.yaml secrets: - GIT_HUB_USER_NAME: ${{ secrets.GIT_HUB_USER_NAME }} - GIT_HUB_USER_TOKEN: ${{ secrets.GIT_HUB_USER_TOKEN }} + GIT_HUB_USER_NAME: ${{ secrets.TEMP_GITHUB_USERNAME }} + GIT_HUB_USER_TOKEN: ${{ secrets.TEMP_WORKFLOW_TOKEN }} SETUP_REPO_URL: ${{ secrets.SETUP_REPO_URL }} \ No newline at end of file diff --git a/.github/workflows/geospatial-images-plugin-build.yaml b/.github/workflows/geospatial-images-plugin-build.yaml new file mode 100644 index 0000000..33d75f3 --- /dev/null +++ b/.github/workflows/geospatial-images-plugin-build.yaml @@ -0,0 +1,59 @@ +name: geospatial-images-plugin-build + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'datagenerators/geospatial-images/plugin/**' + - '.github/workflows/geospatial-images-plugin-build.yaml' + +jobs: + build-plugin-geospatial-images-amd64: + permissions: + contents: read + packages: write + + uses: microsoft/azure-orbital-space-sdk-github-actions/.github/workflows/plugin-build.yaml@main + with: + APP_PROJECT: ./src/geospatial-images-vth-plugin.csproj + OUTPUT_DIR: /var/spacedev/tmp/geospatial-images-vth-plugin/output + PLUGIN_STAGING_DIRECTORY: /var/spacedev/plugins/vth + PLUGIN_FILE_NAME: geospatial-images-vth-plugin.dll + PLUGIN_CONFIG_FILE_NAME: geospatial-images-vth-plugin.json.spacefx_plugin + ANNOTATION: azure-orbital-space-sdk-data-generators.yaml + WORKFLOW_AGENT: ubuntu-latest + ARCHITECTURE: amd64 + DEV_CONTAINER_JSON: .devcontainer/geospatial-images-vth-plugin/devcontainer.json + PROTO_STAGING_DIRECTORY: /var/spacedev/protos/datagenerator/geospatial-images + PROTO_FOLDER_PATH: ./datagenerators/geospatial-images/plugin/src/Protos + PROTO_FILE_NAME: GeospatialImages.proto + secrets: + GIT_HUB_USER_NAME: ${{ secrets.TEMP_GITHUB_USERNAME }} + GIT_HUB_USER_TOKEN: ${{ secrets.TEMP_WORKFLOW_TOKEN }} + SETUP_REPO_URL: ${{ secrets.SETUP_REPO_URL }} + + build-plugin-geospatial-images-arm64: + permissions: + contents: read + packages: write + + uses: microsoft/azure-orbital-space-sdk-github-actions/.github/workflows/plugin-build.yaml@main + with: + APP_PROJECT: ./src/geospatial-images-vth-plugin.csproj + OUTPUT_DIR: /var/spacedev/tmp/geospatial-images-vth-plugin/output + PLUGIN_STAGING_DIRECTORY: /var/spacedev/plugins/vth + PLUGIN_FILE_NAME: geospatial-images-vth-plugin.dll + PLUGIN_CONFIG_FILE_NAME: geospatial-images-vth-plugin.json.spacefx_plugin + ANNOTATION: azure-orbital-space-sdk-data-generators.yaml + WORKFLOW_AGENT: spacesdk-ubuntu-2204LTS-arm64 + ARCHITECTURE: arm64 + DEV_CONTAINER_JSON: .devcontainer/geospatial-images-vth-plugin/devcontainer.json + PROTO_STAGING_DIRECTORY: /var/spacedev/protos/datagenerator/geospatial-images + PROTO_FOLDER_PATH: ./datagenerators/geospatial-images/plugin/src/Protos + PROTO_FILE_NAME: GeospatialImages.proto + secrets: + GIT_HUB_USER_NAME: ${{ secrets.TEMP_GITHUB_USERNAME }} + GIT_HUB_USER_TOKEN: ${{ secrets.TEMP_WORKFLOW_TOKEN }} + SETUP_REPO_URL: ${{ secrets.SETUP_REPO_URL }} \ No newline at end of file diff --git a/datagenerators/geospatial-images/README.md b/datagenerators/geospatial-images/README.md index e1897da..490af07 100644 --- a/datagenerators/geospatial-images/README.md +++ b/datagenerators/geospatial-images/README.md @@ -1,3 +1,93 @@ # Geospatial Images -TODO \ No newline at end of file +## Using the Geospatial Images Data Generator in your app +Using the Geospatial Images Data Generator requires the Plugin, the Plugin Config, the Data Generator container, and the Data Generator Deployment yaml. The spacefx-dev container feature can be used to automatically download and deploy these with the below configuration: +```json + "features": { + "ghcr.io/microsoft/azure-orbital-space-sdk/spacefx-dev:0.11.0": { + "app_name": "MyAwesomeApp", + "download_artifacts": "GeospatialImages.proto, datagenerator-geospatial-images.yaml, geospatial-images-vth-plugin.dll, geospatial-images-vth-plugin.json.spacefx_plugin", + "pull_containers": "datagenerator-geospatial-images:0.11.0-nightly" + } + }, +``` + +## Geospatial Images Data Generator Source Code +The Geospatial Images Data Generator comprises two components: +* [Geospatial Images Data Generator](https://github.com/microsoft/azure-orbital-space-sdk-data-generators/tree/main/datagenerators/geospatial-images/datagenerator) +* [Geospatial Images VTH Plugin](https://github.com/microsoft/azure-orbital-space-sdk-data-generators/tree/main/datagenerators/geospatial-images/plugin) + +The data generator is a Flask App that contains a repository of geospatial imagery available for querying, while the VTH Plugin is used to interact with the Flask App and download any of the geospatial images stored within the datagenerator, then send them via Link Service to the requesting Payload App. + +## Building the Geospatial Images Data Generator (from source) +>:speech_balloon: The images, plugins, and artifacts are already built and pushed to the github container registry via our CI/CD process. These steps are a reference and **not** needed to run the Geospatial Images Data Generator. If you would like to just run the Geospatial Images Data Generator, please refer to [Geospatial Images Data Generator](https://github.com/microsoft/azure-orbital-space-sdk-data-generators/tree/main/datagenerators/geospatial-images) + +1. Provision /var/spacedev + ```bash + # clone the azure-orbital-space-sdk-setup repo and provision /var/spacedev + git clone https://github.com/microsoft/azure-orbital-space-sdk-setup + cd azure-orbital-space-sdk-setup + bash ./.vscode/copy_to_spacedev.sh + cd - + ``` + +2. Clone this repo + ```bash + # clone this repo + git clone https://github.com/microsoft/azure-orbital-space-sdk-data-generators + + cd azure-orbital-space-sdk-data-generators + ``` + +3. Build and push the Geospatial Images Data Generator + ```bash + # Trigger the build_containerImage.sh from azure-orbital-space-sdk-setup + /var/spacedev/build/build_containerImage.sh \ + --architecture amd64 \ + --app-name datagenerator-geospatial-images \ + --image-tag 0.11.0 \ + --dockerfile Dockerfiles/Dockerfile \ + --repo-dir ${PWD}/datagenerators/geospatial-images/datagenerator \ + --no-push \ + --annotation-config azure-orbital-space-sdk-data-generators.yaml + ``` + >:pencil2: the `--no-push` parameter will prevent build_containerImage.sh from pushing to a container registry. We added it here to prevent accidental pushes when you copy-and-paste the command. You will need to remove the `--no-push` if you want to push the final container image to a container registry + +4. Build the Geospatial Images VTH Plugin + ```bash + # Trigger the build_app.sh from azure-orbital-space-sdk-setup + /var/spacedev/build/dotnet/build_app.sh \ + --architecture amd64 \ + --app-project src/geospatial-images-vth-plugin.csproj \ + --app-version 0.11.0 \ + --output-dir /var/spacedev/tmp/geospatial-images-vth-plugin/output \ + --repo-dir ${PWD} \ + --devcontainer-json .devcontainer/geospatial-images-vth-plugin/devcontainer.json \ + --no-container-build \ + --no-push + ``` + >:pencil2: the `--no-container-build` parameter means that we're just interested in the build artifacts and there's not a container image for this. The `--no-push` is superflous since we aren't generating a container image, but kept it here for reference. + +5. Copy the artifacts to their regular folders so it can be read by the containers + ```bash + # Put the dll, spacefx_config, and yaml in the destination directories + sudo mkdir -p /var/spacedev/plugins/vth + sudo mkdir -p /var/spacedev/yamls/deploy + sudo mkdir -p /var/spacedev/protos/datagenerator/geospatial-images + + sudo cp /var/spacedev/tmp/geospatial-images-vth-plugin/output/amd64/app/geospatial-images-vth-plugin.dll /var/spacedev/plugins/vth/ + sudo cp /var/spacedev/tmp/geospatial-images-vth-plugin/output/amd64/app/geospatial-images-vth-plugin.json.spacefx_plugin /var/spacedev/plugins/vth/ + sudo cp ${PWD}/datagenerators/geospatial-images/datagenerator/k3s/datagenerator-geospatial-images.yaml /var/spacedev/yamls/deploy/ + sudo cp ${PWD}/datagenerators/geospatial-images/plugin/src/Protos/GeospatialImages.proto /var/spacedev/protos/datagenerator/geospatial-images/ + ``` + +6. (Optional) Push the build artifacts to the container registry + >:heavy_exclamation_mark: the next step will push the build artifacts to the first container registry you have write access to, including our automated channel tagging and patching aliases. Do not run this step unless you intend to deploy the artifacts to a container registry for others to consume. + ```bash + /var/spacedev/build/push_build_artifact.sh --artifact /var/spacedev/plugins/vth/geospatial-images-vth-plugin.dll --annotation-config azure-orbital-space-sdk-data-generators.yaml --architecture amd64 --artifact-version 0.11.0 + /var/spacedev/build/push_build_artifact.sh --artifact /var/spacedev/plugins/vth/geospatial-images-vth-plugin.json.spacefx_plugin --annotation-config azure-orbital-space-sdk-data-generators.yaml --architecture amd64 --artifact-version 0.11.0 + /var/spacedev/build/push_build_artifact.sh --artifact /var/spacedev/yamls/deploy/datagenerator-geospatial-images.yaml --annotation-config azure-orbital-space-sdk-data-generators.yaml --architecture amd64 --artifact-version 0.11.0 + /var/spacedev/build/push_build_artifact.sh --artifact /var/spacedev/protos/datagenerator/geospatial-images/GeospatialImages.proto --annotation-config azure-orbital-space-sdk-data-generators.yaml --architecture amd64 --artifact-version 0.11.0 + ``` + + diff --git a/datagenerators/geospatial-images/plugin/.editorconfig b/datagenerators/geospatial-images/plugin/.editorconfig new file mode 100644 index 0000000..be15e47 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/.editorconfig @@ -0,0 +1,140 @@ +# Imported from: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options +# For dotnet and C# settings: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/categories + +############################### +# Core EditorConfig Options # +############################### +root = true +# All files +[*] +indent_style = space +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# file_header_template = Copyright (c) Microsoft. All Rights Reserved. +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_style.pascal_case_style.capitalization.severity = suggestion +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = false +csharp_new_line_before_else = false +csharp_new_line_before_catch = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_members_in_anonymous_types = false +csharp_new_line_between_query_expression_clauses = false +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = true +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = true +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +############################### +# VB Coding Conventions # +############################### +[*.vb] +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/.vscode/launch.json b/datagenerators/geospatial-images/plugin/.vscode/launch.json new file mode 100644 index 0000000..7b67960 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/.vscode/launch.json @@ -0,0 +1,84 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "vth", + "type": "coreclr", + "request": "launch", + "program": "/usr/bin/dotnet", + "preLaunchTask": "deploy-debugshim-host", + "args": [ + "${workspaceFolder}/.git/workspaces/vth/vth.dll" + ], + "env": { + "DOTNET_ENVIRONMENT": "PluginDev" + }, + "cwd": "${workspaceFolder}/src", + "stopAtEntry": false, + "console": "internalConsole", + "pipeTransport": { + "pipeCwd": "${workspaceRoot}/src", + "pipeProgram": "bash", + "pipeArgs": [ + "-c \" kubectl exec --stdin $(kubectl get pods -l app=vth -n payload-app --sort-by=.metadata.creationTimestamp -o jsonpath=\"{.items[-1:].metadata.name}\") -n payload-app -c vth -- " + ], + "quoteArgs": false, + "debuggerPath": "${workspaceFolder}/.git/spacefx-dev/vsdbg/vsdbg" + }, + "postDebugTask": "reset-debugshim-host", + "presentation": { + "hidden": false, + "group": "", + "order": 1 + }, + "requireExactSource": true + }, + { + "name": "DebugPayloadApp", + "type": "coreclr", + "request": "launch", + "program": "/usr/bin/dotnet", + "preLaunchTask": "deploy-debugshim-client", + "args": [ + "${workspaceFolder}/debugPayloadApp/bin/Debug/net6.0/debugPayloadApp.dll" + ], + "env": { + "DOTNET_ENVIRONMENT": "Development" + }, + "cwd": "${workspaceFolder}/debugPayloadApp", + "stopAtEntry": false, + "console": "internalConsole", + "pipeTransport": { + "pipeCwd": "${workspaceRoot}/debugPayloadApp", + "pipeProgram": "bash", + "pipeArgs": [ + "-c \" kubectl exec --stdin $(kubectl get pods -l app=vth-client -n payload-app --sort-by=.metadata.creationTimestamp -o jsonpath=\"{.items[-1:].metadata.name}\") -n payload-app -c vth-client -- " + ], + "quoteArgs": false, + "debuggerPath": "${workspaceFolder}/.git/spacefx-dev/vsdbg/vsdbg" + }, + "postDebugTask": "reset-debugshim-client", + "presentation": { + "hidden": false, + "group": "", + "order": 1 + }, + "requireExactSource": true + } + ], + "compounds": [ + { + "name": "vth & DebugPayloadApp", + "configurations": [ + "vth", + "DebugPayloadApp" + ], + "stopAll": true, + "presentation": { + "hidden": false, + "group": "debug", + "order": 2 + } + } + ] +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/.vscode/settings.json b/datagenerators/geospatial-images/plugin/.vscode/settings.json new file mode 100644 index 0000000..c30e631 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "omnisharp.useEditorFormattingSettings": false, // ignores any Visual Studio Code user or machine preferences for formatting and instead enforces the omnisharp.json and .editorconfig in this workspace + "omnisharp.enableRoslynAnalyzers": true, + "editor.formatOnSave": true, + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/.vscode/tasks.json b/datagenerators/geospatial-images/plugin/.vscode/tasks.json new file mode 100644 index 0000000..3679ebe --- /dev/null +++ b/datagenerators/geospatial-images/plugin/.vscode/tasks.json @@ -0,0 +1,163 @@ +{ + "version": "2.0.0", + "options": { + "env": { + "DEBUG_SHIM_HOST": "vth", + "DEBUG_SHIM_CLIENT": "vth-client", + } + }, + "tasks": [ + { + "label": "deploy-debugshim-host", + "isBackground": false, + "command": "/bin/bash", + "type": "shell", + "dependsOn": [ + "build", + ], + "dependsOrder": "sequence", + "args": [ + "/spacefx-dev/debugShim-deploy.sh", + "--debug_shim", + "${DEBUG_SHIM_HOST}" + ], + "presentation": { + "echo": true, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "reset-debugshim-host", + "isBackground": true, + "command": "/bin/bash", + "type": "shell", + "dependsOrder": "sequence", + "args": [ + "/spacefx-dev/debugShim-reset.sh", + "--debug_shim", + "${DEBUG_SHIM_HOST}", + "--skip-pod-wait" + ], + "presentation": { + "echo": false, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/geospatial-images-vth-plugin.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "deploy-debugshim-client", + "isBackground": false, + "command": "/bin/bash", + "type": "shell", + "dependsOn": [ + "pause-processing", + "build-client" + ], + "dependsOrder": "sequence", + "args": [ + "/spacefx-dev/debugShim-deploy.sh", + "--debug_shim", + "${DEBUG_SHIM_CLIENT}", + "--disable_plugin_configs" + ], + "presentation": { + "echo": true, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "reset-debugshim-client", + "isBackground": true, + "command": "/bin/bash", + "type": "shell", + "dependsOrder": "sequence", + "args": [ + "/spacefx-dev/debugShim-reset.sh", + "--debug_shim", + "${DEBUG_SHIM_CLIENT}", + "--skip-pod-wait" + ], + "presentation": { + "echo": false, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "build-client", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/debugPayloadApp/debugPayloadApp.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "reset-debugshim-test-client", + "isBackground": true, + "command": "/bin/bash", + "type": "shell", + "dependsOrder": "sequence", + "args": [ + "/spacefx-dev/debugShim-reset.sh", + "--debug_shim", + "${DEBUG_SHIM_CLIENT}", + "--skip-pod-wait" + ], + "presentation": { + "echo": false, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "pause-processing", + "isBackground": false, + "command": "sleep", + "type": "process", + "args": [ + "3s" + ], + "presentation": { + "echo": false, + "reveal": "never", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } + } + ] +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/README.md b/datagenerators/geospatial-images/plugin/README.md new file mode 100644 index 0000000..fa02517 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/README.md @@ -0,0 +1 @@ +README doc for Geospatial Images plugin \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/Program.cs b/datagenerators/geospatial-images/plugin/debugPayloadApp/Program.cs new file mode 100755 index 0000000..05fe317 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/Program.cs @@ -0,0 +1,53 @@ +using Microsoft.Azure.SpaceFx.MessageFormats.Common; +using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Sensor; + +namespace DebugClient; + +public class Program { + public static void Main(string[] args) { + var builder = WebApplication.CreateBuilder(args); + + builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true); + + builder.WebHost.ConfigureKestrel(options => options.ListenAnyIP(50051, o => o.Protocols = HttpProtocols.Http2)) + .ConfigureServices((services) => { + services.AddAzureOrbitalFramework(); + services.AddHostedService(); + + // Register with the messages we're expecting to receive from the service + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + services.AddSingleton, MessageHandler>(); + + }).ConfigureLogging((logging) => { + // Enable the Azure Orbital Space SDK Logging to route messages to the hostsvc-logging + logging.AddProvider(new Microsoft.Extensions.Logging.SpaceFX.Logger.HostSvcLoggerProvider()); + logging.AddConsole(); + }); + + var app = builder.Build(); + + // Configure a global exception handler to log the exception and stop the application on any uncaught exceptions + app.Use(async (context, next) => { + var logger = app.Services.GetRequiredService>(); + var appLifetime = app.Services.GetRequiredService(); + try { + await next(); + } catch (Exception ex) { + logger.LogError(ex, "Unhandled exception occurred. Stopping the application."); + appLifetime.StopApplication(); + throw; // Re-throwing the exception is optional and depends on how you want to handle the error response. + } + }); + + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapGrpcService()); + app.Run(); + } +} diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/Properties/launchSettings.json b/datagenerators/geospatial-images/plugin/debugPayloadApp/Properties/launchSettings.json new file mode 100755 index 0000000..d588f81 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "debugClient": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/Services/MessageHandler.cs b/datagenerators/geospatial-images/plugin/debugPayloadApp/Services/MessageHandler.cs new file mode 100644 index 0000000..845ede8 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/Services/MessageHandler.cs @@ -0,0 +1,25 @@ +namespace DebugClient; + +public class MessageHandler : Core.IMessageHandler where T : notnull { + private readonly ILogger> _logger; + private readonly IServiceProvider _serviceProvider; + public static event EventHandler? MessageReceivedEvent; + public MessageHandler(ILogger> logger, IServiceProvider serviceProvider) { + _logger = logger; + _serviceProvider = serviceProvider; + } + + public void MessageReceived(T message, Microsoft.Azure.SpaceFx.MessageFormats.Common.DirectToApp fullMessage) { + using (var scope = _serviceProvider.CreateScope()) { + + _logger.LogInformation($"Receieved message type '{typeof(T).Name}' from '{fullMessage.SourceAppId}'"); + + if (MessageReceivedEvent != null) { + foreach (Delegate handler in MessageReceivedEvent.GetInvocationList()) { + Task.Factory.StartNew( + () => handler.DynamicInvoke(fullMessage.SourceAppId, message)); + } + } + } + } +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/Services/MessageSender.cs b/datagenerators/geospatial-images/plugin/debugPayloadApp/Services/MessageSender.cs new file mode 100644 index 0000000..df5d4ce --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/Services/MessageSender.cs @@ -0,0 +1,430 @@ +using Microsoft.Azure.SpaceFx.MessageFormats.Common; +using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Sensor; + +namespace DebugClient; + +public class MessageSender : BackgroundService { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly Core.Client _client; + private readonly string _appId; + private readonly string _hostSvcAppId; + private readonly List _appsOnline = new(); + private readonly TimeSpan MAX_TIMESPAN_TO_WAIT_FOR_MSG = TimeSpan.FromSeconds(10); + private readonly string _testFile = "/workspace/geospatial-images-vth-plugin/sampleData/astronaut.jpg"; + + public MessageSender(ILogger logger, IServiceProvider serviceProvider) { + _logger = logger; + _serviceProvider = serviceProvider; + _client = _serviceProvider.GetService() ?? throw new NullReferenceException($"{nameof(Core.Client)} is null"); + _appId = _client.GetAppID().Result; + _hostSvcAppId = _appId.Replace("-client", ""); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + + using (var scope = _serviceProvider.CreateScope()) { + _logger.LogInformation("MessageSender running at: {time}", DateTimeOffset.Now); + + Boolean SVC_ONLINE = _client.ServicesOnline().Any(pulse => pulse.AppId.Equals(_hostSvcAppId, StringComparison.CurrentCultureIgnoreCase)); + + _logger.LogInformation($"Waiting for service '{_hostSvcAppId}' to come online..."); + + while (!SVC_ONLINE && DateTime.Now < maxTimeToWait) { + await Task.Delay(1000); + SVC_ONLINE = _client.ServicesOnline().Any(pulse => pulse.AppId.Equals(_hostSvcAppId, StringComparison.CurrentCultureIgnoreCase)); + ListHeardServices(); + } + + if (!SVC_ONLINE) { + throw new Exception($"Service '{_hostSvcAppId}' did not come online in time."); + } + + // // Hostsvc-Position endpoints + // await UpdatePosition(); + // await GetCurrentPosition(); + + // // Hostsvc-Link endpoints + // await SendFileRootDirectory(); + + // // Hostsvc-Logging endpoints + // await SendTelemetryMetric(); + // await SendLogMessage(); + + // Hostsvc-Sensor endpoints + RegisterForSensorData(); + await SendPluginHealthCheck(); + await SendSensorsAvailableRequest(); + await SendTaskingPreCheckRequest(); + await SendTaskingRequest(); + + _logger.LogInformation("DebugPayloadApp completed at: {time}", DateTimeOffset.Now); + } + } + + private void ListHeardServices() { + _client.ServicesOnline().ForEach((pulse) => { + if (_appsOnline.Contains(pulse.AppId)) return; + _appsOnline.Add(pulse.AppId); + _logger.LogInformation($"App:...{pulse.AppId}..."); + }); + } + + private async Task SendPluginHealthCheck() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + PluginHealthCheckMultiResponse? response = null; + PluginHealthCheckRequest request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + } + }; + + _logger.LogInformation($"Sending Plugin Healthcheck request (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void PluginResponseEventHandler(object? _, PluginHealthCheckMultiResponse _response) { + response = _response; + MessageHandler.MessageReceivedEvent -= PluginResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += PluginResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + if (response.ResponseHeader.Status != Microsoft.Azure.SpaceFx.MessageFormats.Common.StatusCodes.Successful) { + throw new Exception($"Plugin Health Check failed with status '{response.ResponseHeader.Status}' and message '{response.ResponseHeader.Message}'"); + } + + + _logger.LogInformation($"Plugin Healthcheck request received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + } + + private async Task UpdatePosition() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + PositionUpdateResponse? response = null; + PositionUpdateRequest request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + }, + Position = new Position() { + PositionTime = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow), + Point = new Position.Types.Point() { + X = 1, + Y = 2, + Z = 3, + }, + Attitude = new Position.Types.Attitude() { + X = 1, + Y = 2, + Z = 3, + K = 4 + } + } + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void ResponseEventHandler(object? _, PositionUpdateResponse _response) { + response = _response; + MessageHandler.MessageReceivedEvent -= ResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += ResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + _logger.LogInformation($"'{request.GetType().Name}' request received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + } + + private async Task GetCurrentPosition() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + PositionResponse? response = null; + PositionRequest request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + } + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void ResponseEventHandler(object? _, PositionResponse _response) { + response = _response; + MessageHandler.MessageReceivedEvent -= ResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += ResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + _logger.LogInformation($"'{request.GetType().Name}' request received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + } + + private async Task SendFileRootDirectory() { + var (inbox, outbox, root) = _client.GetXFerDirectories().Result; + + File.Copy(_testFile, string.Format($"{outbox}/{Path.GetFileName(_testFile)}"), overwrite: true); + + LinkRequest request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + }, + FileName = Path.GetFileName(_testFile), + DestinationAppId = "contoso-app-id" + }; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + } + + private async Task SendTelemetryMetric() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + TelemetryMetricResponse? response = null; + + TelemetryMetric request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + }, + MetricName = "Testing", + MetricValue = 37 + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request to '{_hostSvcAppId}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void TelemetryMetricResponseEventHandler(object? _, TelemetryMetricResponse _response) { + if (_response.ResponseHeader.CorrelationId != request.RequestHeader.CorrelationId) return; + response = _response; + MessageHandler.MessageReceivedEvent -= TelemetryMetricResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += TelemetryMetricResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response message type (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + _logger.LogInformation($"'{request.GetType().Name}'received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + } + + private async Task SendLogMessage() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + LogMessageResponse? response = null; + + LogMessage request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + }, + LogLevel = LogMessage.Types.LOG_LEVEL.Info, + Message = "Log Message from DebugPayloadApp", + Priority = Priority.Medium, + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request to '{_hostSvcAppId}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void LogMessageResponseEventHandler(object? _, LogMessageResponse _response) { + if (_response.ResponseHeader.CorrelationId != request.RequestHeader.CorrelationId) return; + response = _response; + MessageHandler.MessageReceivedEvent -= LogMessageResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += LogMessageResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response message type (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + _logger.LogInformation($"'{request.GetType().Name}'received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + } + + private void RegisterForSensorData() { + + _logger.LogInformation($"Registering a function to process Sensor Data"); + + // Register a callback event to catch the Sensor Data + void SensorDataEventHandler(object? _, SensorData _response) { + _logger.LogInformation($"Received Sensor Data: '{_response.SensorID}'"); + _logger.LogInformation($"SensorData Status: {_response.ResponseHeader.Status}. TrackingID: {_response.ResponseHeader.TrackingId}"); + + if (!_response.SensorID.Equals("GeospatialImages", StringComparison.InvariantCultureIgnoreCase)) return; // This is not the sensor you're looking for + + Microsoft.Azure.SpaceFx.GeospatialImages.EarthImageResponse? earthImageResponse = _response.Data.Unpack(); + _logger.LogInformation($"...GeospatialImages.EarthImageResponse unpacked. datagenerator-geospatial-images output:'{earthImageResponse.Filename}'."); + } + + MessageHandler.MessageReceivedEvent += SensorDataEventHandler; + + _logger.LogInformation($"Successfully registered a function to process any Sensor Data"); + + } + + private async Task SendSensorsAvailableRequest() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + SensorsAvailableResponse? response = null; + SensorsAvailableRequest request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + } + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void ResponseEventHandler(object? _, SensorsAvailableResponse _response) { + if (_response.ResponseHeader.CorrelationId != request.RequestHeader.CorrelationId) return; + response = _response; + MessageHandler.MessageReceivedEvent -= ResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += ResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + + _logger.LogInformation($"'{request.GetType().Name}' request received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + _logger.LogInformation($"SensorsAvailableResponse Heard from {response.ResponseHeader.AppId}"); + _logger.LogInformation($"{response.GetType().Name} Sensors ({response.Sensors.Count}): "); + response.Sensors.ToList().ForEach((sensor) => Console.WriteLine($"...Sensor '{sensor.SensorID}'...")); + } + + private async Task SendTaskingPreCheckRequest() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + TaskingPreCheckResponse? response = null; + TaskingPreCheckRequest request = new() { + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = Guid.NewGuid().ToString() + } + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void ResponseEventHandler(object? _, TaskingPreCheckResponse _response) { + if (_response.ResponseHeader.CorrelationId != request.RequestHeader.CorrelationId) return; + response = _response; + MessageHandler.MessageReceivedEvent -= ResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += ResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + _logger.LogInformation($"'{request.GetType().Name}' request received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + } + + private async Task SendTaskingRequest() { + DateTime maxTimeToWait = DateTime.Now.Add(TimeSpan.FromSeconds(10)); + TaskingResponse? response = null; + + var trackingId = Guid.NewGuid().ToString(); + + Microsoft.Azure.SpaceFx.GeospatialImages.EarthLineOfSight lineOfSight = new Microsoft.Azure.SpaceFx.GeospatialImages.EarthLineOfSight { + Latitude = (float) 47.6062, + Longitude = (float) -122.3321 + }; + + Microsoft.Azure.SpaceFx.GeospatialImages.EarthImageRequest imageRequest = new Microsoft.Azure.SpaceFx.GeospatialImages.EarthImageRequest { + LineOfSight = lineOfSight, + ImageType = Microsoft.Azure.SpaceFx.GeospatialImages.ImageType.Geotiff + }; + + TaskingRequest request = new() { + RequestHeader = new RequestHeader() { + TrackingId = trackingId, + CorrelationId = trackingId + }, + SensorID = Microsoft.Azure.SpaceFx.VTH.Plugins.GeospatialImagesPlugin.SENSOR_ID, + RequestData = Google.Protobuf.WellKnownTypes.Any.Pack(imageRequest) + }; + + _logger.LogInformation($"Sending '{request.GetType().Name}' request (TrackingId: '{request.RequestHeader.TrackingId}')"); + + // Register a callback event to catch the response + void ResponseEventHandler(object? _, TaskingResponse _response) { + if (_response.ResponseHeader.CorrelationId != request.RequestHeader.CorrelationId) return; + response = _response; + MessageHandler.MessageReceivedEvent -= ResponseEventHandler; + } + + MessageHandler.MessageReceivedEvent += ResponseEventHandler; + + await _client.DirectToApp(appId: _hostSvcAppId, message: request); + + _logger.LogInformation($"Waiting for response (TrackingId: '{request.RequestHeader.TrackingId}')"); + + while (response == null && DateTime.Now <= maxTimeToWait) { + Thread.Sleep(100); + } + + if (response == null) throw new TimeoutException($"Failed to hear {nameof(response)} after {MAX_TIMESPAN_TO_WAIT_FOR_MSG}. Please check that {_hostSvcAppId} is deployed"); + + _logger.LogInformation($"'{request.GetType().Name}' request received. Status: '{response.ResponseHeader.Status}' (TrackingId: '{request.RequestHeader.TrackingId}')"); + + } +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/Usings.cs b/datagenerators/geospatial-images/plugin/debugPayloadApp/Usings.cs new file mode 100755 index 0000000..846996d --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/Usings.cs @@ -0,0 +1,9 @@ +global using Microsoft.Extensions.Logging; +global using Microsoft.Azure.SpaceFx; +global using Microsoft.AspNetCore.Server.Kestrel.Core; +global using Microsoft.Azure.SpaceFx.MessageFormats.Common; +global using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Link; +global using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Sensor; +global using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Position; +global using Google.Protobuf; +global using Google.Protobuf.WellKnownTypes; \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/appsettings.json b/datagenerators/geospatial-images/plugin/debugPayloadApp/appsettings.json new file mode 100755 index 0000000..596c99e --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Error", + "Microsoft.AspNetCore.Hosting.Diagnostics": "Error", + "Microsoft.AspNetCore.Routing.EndpointMiddleware": "Error", + "Microsoft.Azure.SpaceFx.Core.Utils.SideCar": "Error", + "Microsoft.Azure.SpaceFx.Core.Services.HeartbeatService": "Error" + } + } +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/debugPayloadApp/debugPayloadApp.csproj b/datagenerators/geospatial-images/plugin/debugPayloadApp/debugPayloadApp.csproj new file mode 100755 index 0000000..7e4e1cf --- /dev/null +++ b/datagenerators/geospatial-images/plugin/debugPayloadApp/debugPayloadApp.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + enable + enable + win-x64;linux-x64;linux-arm64 + + + + + + + + + + diff --git a/datagenerators/geospatial-images/plugin/geospatial-images-vth-plugin.sln b/datagenerators/geospatial-images/plugin/geospatial-images-vth-plugin.sln new file mode 100644 index 0000000..c832256 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/geospatial-images-vth-plugin.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "geospatial-images-vth-plugin", "src\geospatial-images-vth-plugin.csproj", "{DDA13A82-3756-4A22-A6B3-4B4EE3062059}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "debugPayloadApp", "debugPayloadApp\debugPayloadApp.csproj", "{455C1F9D-E4B8-4606-8410-F603FB5CA2E2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DDA13A82-3756-4A22-A6B3-4B4EE3062059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DDA13A82-3756-4A22-A6B3-4B4EE3062059}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DDA13A82-3756-4A22-A6B3-4B4EE3062059}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DDA13A82-3756-4A22-A6B3-4B4EE3062059}.Release|Any CPU.Build.0 = Release|Any CPU + {455C1F9D-E4B8-4606-8410-F603FB5CA2E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {455C1F9D-E4B8-4606-8410-F603FB5CA2E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {455C1F9D-E4B8-4606-8410-F603FB5CA2E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {455C1F9D-E4B8-4606-8410-F603FB5CA2E2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4E761151-B04C-4C7E-8567-19139E58D7C1} + EndGlobalSection +EndGlobal diff --git a/datagenerators/geospatial-images/plugin/omnisharp.json b/datagenerators/geospatial-images/plugin/omnisharp.json new file mode 100644 index 0000000..e1e5784 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/omnisharp.json @@ -0,0 +1,13 @@ +{ + "FormattingOptions": { + "EnableEditorConfigSupport": true + }, + "RoslynExtensionsOptions": { + "documentAnalysisTimeoutMs": 30000, + "enableDecompilationSupport": true, + "enableImportCompletion": true, + "enableAnalyzersSupport": true, + "diagnosticWorkersThreadCount": 8 + }, + "files.trimTrailingWhitespace": true +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/sampleData/astronaut.jpg b/datagenerators/geospatial-images/plugin/sampleData/astronaut.jpg new file mode 100644 index 0000000..5466051 Binary files /dev/null and b/datagenerators/geospatial-images/plugin/sampleData/astronaut.jpg differ diff --git a/datagenerators/geospatial-images/plugin/src/Protos/GeospatialImages.proto b/datagenerators/geospatial-images/plugin/src/Protos/GeospatialImages.proto new file mode 100644 index 0000000..f1c398c --- /dev/null +++ b/datagenerators/geospatial-images/plugin/src/Protos/GeospatialImages.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +option csharp_namespace = "Microsoft.Azure.SpaceFx.GeospatialImages"; +package Microsoft.Azure.SpaceFx.GeospatialImages; + +enum ServiceEndpoints { + health = 0; + imageRequest = 1; +} + +enum ImageType { + GEOTIFF = 0; + IMAGE = 1; +} + +message EarthLineOfSight { + float Latitude = 1; + float Longitude = 2; +} + +message EarthImageRequest { + EarthLineOfSight lineOfSight = 1; + ImageType imageType= 2; +} + +message EarthImageResponse { + EarthLineOfSight lineOfSight = 1; + google.protobuf.Timestamp timestamp = 2; + string filename = 3; +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/src/Usings.cs b/datagenerators/geospatial-images/plugin/src/Usings.cs new file mode 100644 index 0000000..11d3bc7 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/src/Usings.cs @@ -0,0 +1,14 @@ +global using Microsoft.Azure.SpaceFx; +global using Microsoft.Azure.SpaceFx.MessageFormats.Common; +global using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Link; +global using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Sensor; +global using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Position; +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; +global using System.Collections.Concurrent; +global using Google.Protobuf.WellKnownTypes; +global using System.Threading.Tasks; +global using System.Text; +global using Microsoft.Extensions.DependencyInjection; +global using System.Collections.Concurrent; +global using System.Net.Http.Json; diff --git a/datagenerators/geospatial-images/plugin/src/geospatial-images-vth-plugin.csproj b/datagenerators/geospatial-images/plugin/src/geospatial-images-vth-plugin.csproj new file mode 100644 index 0000000..9e44787 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/src/geospatial-images-vth-plugin.csproj @@ -0,0 +1,34 @@ + + + net6.0 + enable + enable + true + linux-x64;linux-arm64 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + False + None + False + False + + + + + + + + diff --git a/datagenerators/geospatial-images/plugin/src/geospatial-images-vth-plugin.json.spacefx_plugin b/datagenerators/geospatial-images/plugin/src/geospatial-images-vth-plugin.json.spacefx_plugin new file mode 100644 index 0000000..5e43c1d --- /dev/null +++ b/datagenerators/geospatial-images/plugin/src/geospatial-images-vth-plugin.json.spacefx_plugin @@ -0,0 +1,10 @@ +{ + "pluginFile": "geospatial-images-vth-plugin.dll", + "pluginName": "geospatial-images-vth-plugin", + "targetService": "vth", + "plugin_permissions": "ALL", + "core_permissions": "ALL", + "build_artifacts": [], + "configuration": { + } +} \ No newline at end of file diff --git a/datagenerators/geospatial-images/plugin/src/plugin.cs b/datagenerators/geospatial-images/plugin/src/plugin.cs new file mode 100644 index 0000000..4351c41 --- /dev/null +++ b/datagenerators/geospatial-images/plugin/src/plugin.cs @@ -0,0 +1,313 @@ +using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Link; +using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Position; +using Microsoft.Azure.SpaceFx.MessageFormats.HostServices.Sensor; + +namespace Microsoft.Azure.SpaceFx.VTH.Plugins; +public class GeospatialImagesPlugin : Microsoft.Azure.SpaceFx.VTH.Plugins.PluginBase { + + // HelloWorld sensor is a simple request/reply sensor to validate the direct path scenario works + public const string SENSOR_ID = "GeospatialImages"; + private readonly string OUTPUT_DIR = ""; + private readonly string IMAGES_DIR = ""; + private readonly HttpClient HTTP_CLIENT; + private readonly ConcurrentQueue IMAGE_QUEUE = new(); + private readonly ConcurrentDictionary LinkRequestIDs = new(); + + public GeospatialImagesPlugin() { + LoggerFactory loggerFactory = new(); + this.Logger = loggerFactory.CreateLogger(); + ServiceProvider? serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider() ?? throw new Exception("Unable to initialize the HTTP Client Factory"); + HTTP_CLIENT = serviceProvider.GetService().CreateClient(); + OUTPUT_DIR = Core.GetXFerDirectories().Result.outbox_directory; + } + + public override void ConfigureLogging(ILoggerFactory loggerFactory) => this.Logger = loggerFactory.CreateLogger(); + + public override ILogger Logger { get; set; } + + public override Task BackgroundTask() => Task.Run(async () => { + Logger.LogInformation("{pluginName}: {methodRequest} Background Task started.", + nameof(GeospatialImagesPlugin), nameof(BackgroundTask)); + + DateTime maxTimeToWaitForLinkResponse; + + while (true) { + if (IMAGE_QUEUE.TryDequeue(out var taskingRequest)) { + try { + Logger.LogDebug("{pluginName}: {methodRequest} Processing {messageType} (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(BackgroundTask), nameof(TaskingRequest), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + SensorData sensorData = await processImageRequest(taskingRequest); + + if (sensorData.ResponseHeader.Status == StatusCodes.Successful) { + + Logger.LogDebug("{pluginName}: {methodRequest} Waiting for successful Link Response for all files (maximum 30 seconds) (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(BackgroundTask), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + bool hasPendingLinkRequests = true; + maxTimeToWaitForLinkResponse = DateTime.Now.Add(TimeSpan.FromSeconds(30)); + + while (hasPendingLinkRequests && DateTime.Now <= maxTimeToWaitForLinkResponse) { + hasPendingLinkRequests = LinkRequestIDs.Any(i => i.Value.RequestHeader.CorrelationId == sensorData.ResponseHeader.CorrelationId); + System.Threading.Thread.Sleep(100); ; + } + + if (hasPendingLinkRequests) { + sensorData.ResponseHeader.Status = StatusCodes.Timeout; + sensorData.ResponseHeader.Message = $"Timeout while transmitting files to '{taskingRequest.RequestHeader.AppId}'. Check LinkService is deployed and operational, then retry your query."; + } + } + + Logger.LogInformation("{pluginName}: {methodRequest} Sending results to {appId} (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(BackgroundTask), taskingRequest.RequestHeader.AppId, taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + Core.DirectToApp(appId: taskingRequest.RequestHeader.AppId, message: sensorData); + } catch (Exception ex) { + Logger.LogError("{pluginName}: {methodRequest} Error processing tasking request. Error message: {errorMsg} (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(BackgroundTask), ex.ToString(), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + } + } else { + System.Threading.Thread.Sleep(1000); + } + } + }); + + public override Task LinkResponse(LinkResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: received and processed a LinkResponse Event", nameof(GeospatialImagesPlugin)); + if (input_response == null) return input_response; + + LinkRequestIDs.TryRemove(input_response.ResponseHeader.CorrelationId, out _); + + return (input_response ?? null); + }); + + public override Task<(PositionUpdateRequest?, PositionUpdateResponse?)> PositionUpdateRequest(PositionUpdateRequest? input_request, PositionUpdateResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a PositionUpdateRequest Event", nameof(GeospatialImagesPlugin)); + return (input_request, input_response); + }); + + public override Task PositionUpdateResponse(PositionUpdateResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a PositionUpdateResponse Event", nameof(GeospatialImagesPlugin)); + return (input_response ?? null); + }); + + public override Task PluginHealthCheckResponse() => Task.Run(() => { + return new MessageFormats.Common.PluginHealthCheckResponse { + ResponseHeader = new MessageFormats.Common.ResponseHeader { + CorrelationId = Guid.NewGuid().ToString(), + TrackingId = Guid.NewGuid().ToString(), + Status = MessageFormats.Common.StatusCodes.Healthy, + Message = "geospatial-images-vth-plugin is operational" + }, + }; + }); + + public override Task SensorData(SensorData? input_request) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a SensorData Event", nameof(GeospatialImagesPlugin)); + return (input_request ?? null); + }); + + public override Task<(SensorsAvailableRequest?, SensorsAvailableResponse?)> SensorsAvailableRequest(SensorsAvailableRequest? input_request, SensorsAvailableResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a SensorsAvailableResponse Event", nameof(GeospatialImagesPlugin)); + + if (input_request == null || input_response == null) return (input_request, input_response); + + input_response.ResponseHeader.Status = StatusCodes.Successful; + input_response.Sensors.Add(new SensorsAvailableResponse.Types.SensorAvailable() { SensorID = SENSOR_ID }); + + + return (input_request, input_response); + }); + + public override Task SensorsAvailableResponse(SensorsAvailableResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a SensorsAvailableResponse Event", nameof(GeospatialImagesPlugin)); + return (input_response ?? null); + }); + + public override Task<(TaskingPreCheckRequest?, TaskingPreCheckResponse?)> TaskingPreCheckRequest(TaskingPreCheckRequest? input_request, TaskingPreCheckResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a TaskingPreCheckRequest Event", nameof(GeospatialImagesPlugin)); + if (input_request == null || input_response == null) return (input_request, input_response); + + // Flip it to success + input_response.ResponseHeader.Status = StatusCodes.Successful; + return (input_request, input_response); + }); + + public override Task TaskingPreCheckResponse(TaskingPreCheckResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a TaskingPreCheckResponse Event", nameof(GeospatialImagesPlugin)); + return (input_response ?? null); + }); + + public override Task<(TaskingRequest?, TaskingResponse?)> TaskingRequest(TaskingRequest? input_request, TaskingResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: {methodRequest} received tasking request. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(TaskingRequest), input_request.RequestHeader.TrackingId, input_request.RequestHeader.CorrelationId); + if (input_request == null) return (input_request, input_response); + if (!input_request.SensorID.Equals(SENSOR_ID, StringComparison.InvariantCultureIgnoreCase)) return (input_request, input_response); // This is not the plugin you're looking for + + + + Logger.LogTrace("{pluginName}: {methodRequest} Adding request to queue. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(TaskingRequest), input_request.RequestHeader.TrackingId, input_request.RequestHeader.CorrelationId); + + IMAGE_QUEUE.Enqueue(input_request); + + + // Flip it to success + input_response.ResponseHeader.Status = StatusCodes.Successful; + input_response.SensorID = input_request.SensorID; + + + Logger.LogDebug("{pluginName}: {methodRequest} Setting {image_response_type} status to {status}. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(TaskingRequest), input_response.GetType().Name, input_response.ResponseHeader.Status, input_request.RequestHeader.TrackingId, input_request.RequestHeader.CorrelationId); + + Logger.LogDebug("{pluginName}: {methodRequest} Returning {image_response_type} to VTH. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(TaskingRequest), input_response.GetType().Name, input_request.RequestHeader.TrackingId, input_request.RequestHeader.CorrelationId); + + return (input_request, input_response); + }); + + public override Task TaskingResponse(TaskingResponse? input_response) => Task.Run(() => { + Logger.LogInformation("{pluginName}: Plugin received and processed a SensorsAvailableRequest Event", nameof(GeospatialImagesPlugin)); + return (input_response ?? null); + }); + + // Helper Functions: + private async Task processImageRequest(TaskingRequest taskingRequest) { + GeospatialImages.EarthImageRequest imageRequest; + GeospatialImages.EarthImageResponse imageResponse; + string fileName; + + SensorData sensorData = new() { + ResponseHeader = new ResponseHeader() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = taskingRequest.RequestHeader.CorrelationId + }, + DestinationAppId = taskingRequest.RequestHeader.AppId, + TaskingTrackingId = taskingRequest.RequestHeader.TrackingId, + SensorID = SENSOR_ID + }; + + // Unpack the request + try { + Logger.LogTrace("{pluginName}: {methodRequest} Extracting {embeddedMessageType} from {messageType}. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), nameof(GeospatialImages.EarthImageRequest), nameof(TaskingRequest), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + imageRequest = taskingRequest.RequestData.Unpack(); + + Logger.LogDebug("{pluginName}: {methodRequest} Successfully extracted {embeddedMessageType} from {messageType}. Request Object: {requestObject} (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), nameof(GeospatialImages.EarthImageRequest), nameof(TaskingRequest), Google.Protobuf.JsonFormatter.Default.Format(imageRequest), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + } catch (Exception ex) { + Logger.LogError("{pluginName}: {methodRequest} Failed to extract {embeddedMessageType} from {messageType}. Error: {errMsg} (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), nameof(GeospatialImages.EarthImageRequest), nameof(TaskingRequest), ex.Message, taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + sensorData.ResponseHeader.Status = StatusCodes.GeneralFailure; + sensorData.ResponseHeader.Message = string.Format($"{nameof(GeospatialImagesPlugin)}: {nameof(TaskingRequest)} Failed to extract {nameof(GeospatialImages.EarthImageRequest)} from {nameof(TaskingRequest)}. Error: {ex.Message}"); + return sensorData; + } + + // Query the Geotiff Processor Tool + try { + Logger.LogTrace("{pluginName}: {methodRequest} Querying tool-image-provider computer. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + string imageType = imageRequest.ImageType.ToString().ToLower(); + + Logger.LogDebug("{pluginName}: {methodRequest} requesting {imageType}. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), imageType, taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + + // var datagenerator_url = string.Format("http://datagenerator-geospatial-images.svc.cluster.local:8080/get_geotiff?lat={0}&lon={1}", imageRequest.LineOfSight.Latitude.ToString(), imageRequest.LineOfSight.Longitude.ToString()); + var datagenerator_url = string.Format("http://datagenerator-geospatial-images.platformsvc.svc.cluster.local:8080/get_{0}?lat={1}&lon={2}", imageType, imageRequest.LineOfSight.Latitude.ToString(), imageRequest.LineOfSight.Longitude.ToString()); + + + Logger.LogDebug("{pluginName}: {methodRequest} Query: {datagenerator_url} . (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), datagenerator_url, taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + fileName = await downloadFile(url: datagenerator_url, filePath: Path.Combine(OUTPUT_DIR, taskingRequest.RequestHeader.TrackingId)); + + + } catch (Exception ex) { + Logger.LogError("{pluginName}: {methodRequest} Failed to query datagenerator-geospatial-images. Error: {errMsg} (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), ex.Message, taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + sensorData.ResponseHeader.Status = MessageFormats.Common.StatusCodes.GeneralFailure; + sensorData.ResponseHeader.Message = string.Format($"{nameof(GeospatialImagesPlugin)}: {nameof(TaskingRequest)} Failed to extract {nameof(GeospatialImages.EarthImageRequest)} from {nameof(TaskingRequest)}. Error: {ex.Message}"); + return sensorData; + } + + // Build the sensor data message to return to the calling method + imageResponse = new() { + LineOfSight = imageRequest.LineOfSight, + Filename = fileName + }; + + DateTime maxTimeToWaitForFile = DateTime.Now.Add(TimeSpan.FromSeconds(30)); + + Logger.LogDebug("{pluginName}: {methodRequest} waiting for {filePath}. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), Path.Combine(OUTPUT_DIR, imageResponse.Filename), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + while (!File.Exists(Path.Combine(OUTPUT_DIR, imageResponse.Filename)) && DateTime.Now <= maxTimeToWaitForFile) { + await Task.Delay(100); + } + + + if (File.Exists(Path.Combine(OUTPUT_DIR, imageResponse.Filename))) { + Logger.LogInformation("{pluginName}: {methodRequest} Found file at '{filePath}'. Sending link request to '{destinationAppId}'. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), Path.Combine(OUTPUT_DIR, imageResponse.Filename), taskingRequest.RequestHeader.AppId, taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + LinkRequest linkRequest = new() { + DestinationAppId = taskingRequest.RequestHeader.AppId, + ExpirationTime = Timestamp.FromDateTime(DateTime.UtcNow.AddHours(1)), + FileName = fileName, + LeaveSourceFile = false, + LinkType = LinkRequest.Types.LinkType.App2App, + Priority = Priority.Medium, + RequestHeader = new() { + TrackingId = Guid.NewGuid().ToString(), + CorrelationId = sensorData.ResponseHeader.CorrelationId + } + }; + + if (taskingRequest.RequestHeader.Metadata.FirstOrDefault((_item) => _item.Key == "SOURCE_PAYLOAD_APP_ID").Value != null) { + string sourcePayloadAppID = taskingRequest.RequestHeader.Metadata.FirstOrDefault((_item) => _item.Key == "SOURCE_PAYLOAD_APP_ID").Value; + linkRequest.RequestHeader.Metadata.Add("SOURCE_PAYLOAD_APP_ID", sourcePayloadAppID); + } + + LinkRequestIDs.TryAdd(sensorData.ResponseHeader.CorrelationId, linkRequest); + await Core.DirectToApp(appId: $"hostsvc-{nameof(HostServices.Link)}", message: linkRequest); + + } else { + sensorData.ResponseHeader.Status = StatusCodes.Timeout; + sensorData.ResponseHeader.Message = $"Timeout while waiting for file to land in {OUTPUT_DIR}. Check the tool-geotiff-processor pod logs for errors and troubleshoot"; + } + + Logger.LogInformation("{pluginName}: {methodRequest} Returning generated sensor data. (TrackingId: {trackingId}, CorrelationId: {correlationId})", + nameof(GeospatialImagesPlugin), nameof(processImageRequest), taskingRequest.RequestHeader.TrackingId, taskingRequest.RequestHeader.CorrelationId); + + sensorData.ResponseHeader.Status = StatusCodes.Successful; + sensorData.Data = Any.Pack(imageResponse); + + return sensorData; + } + + private async Task downloadFile(string url, string filePath) { + using (HttpResponseMessage response = await HTTP_CLIENT.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) { + response.EnsureSuccessStatusCode(); + string contentType = response.Content.Headers.ContentType.MediaType.Split('/')[1]; + string fullFilePath = filePath + "." + contentType; + + + using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync()) { + using (Stream streamToWriteTo = File.Open(fullFilePath, FileMode.Create)) { + await streamToReadFrom.CopyToAsync(streamToWriteTo); + streamToWriteTo.Close(); + } + streamToReadFrom.Close(); + } + response.Dispose(); + + return Path.GetFileName(fullFilePath); + } + } +}