From f66443929c0e0c51e64dc419e990f9856a362eda Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sun, 23 Oct 2022 15:44:22 +0100 Subject: [PATCH 01/34] CI - Added back builds --- .github/workflows/master.yml | 121 +++++++++++++++++++++++++++++++++++ .github/workflows/nuget.yml | 2 +- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/master.yml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 000000000..d084692a6 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,121 @@ +name: Master - Build & Upload to Ftp + +on: + workflow_dispatch: + push: + +jobs: + version: + runs-on: ubuntu-latest + outputs: + version-number: ${{ steps.get-version.outputs.version-number }} + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get Version String + id: get-version + shell: pwsh + run: | + $MidnightUtc = [DateTime]::UtcNow.Date + $BranchName = "${{ github.ref_name }}".replace('/','-').replace('.','-') + $ApiVersion = (Select-Xml -Path 'src/Artemis.Core/Artemis.Core.csproj' -XPath '//PluginApiVersion').Node.InnerText + $NumberOfCommitsToday = (git log --after=$($MidnightUtc.ToString("o")) --oneline | Measure-Object -Line).Lines + $VersionNumber = "$ApiVersion.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday" + # If we're not in master, add the branch name to the version so it counts as prerelease + if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" } + "version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT + + build: + needs: version + strategy: + matrix: + include: + - os: windows-latest + rid: win-x64 + csproj: Windows + + - os: ubuntu-latest + rid: linux-x64 + csproj: Linux + + - os: macos-latest + rid: osx-x64 + csproj: MacOS + name: ${{ matrix.csproj }} Build + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Artemis + uses: actions/checkout@v3 + with: + path: Artemis + - name: Checkout Plugins + uses: actions/checkout@v3 + with: + repository: Artemis-RGB/Artemis.Plugins + path: Artemis.Plugins + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: '6.0.x' + - name: Publish Artemis + run: dotnet publish --configuration Release -p:Version=${{ needs.version.outputs.version-number }} --runtime ${{ matrix.rid }} --output build/${{ matrix.rid }} --self-contained Artemis/src/Artemis.UI.${{ matrix.csproj }}/Artemis.UI.${{ matrix.csproj }}.csproj + - name: Publish Plugins + run: | + New-Item -ItemType "Directory" -Path build/${{ matrix.rid }}/Plugins/ + Get-ChildItem -File -Recurse -Filter *.csproj Artemis.Plugins/src/ | + Foreach-Object -Parallel { + dotnet publish --configuration Release --runtime ${{ matrix.rid }} --output build-plugins/$($_.BaseName) --no-self-contained $($_.FullName); + Compress-Archive -Path "build-plugins/$($_.BaseName)" -DestinationPath "build/${{ matrix.rid }}/Plugins/$($_.BaseName).zip"; + } + shell: pwsh + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: artemis-build-${{ matrix.rid }} + path: build/${{ matrix.rid }} + + ftp: + name: Ftp Upload + runs-on: ubuntu-latest + needs: [version, build] + steps: + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + - name: Zip and Hash artifacts + run: | + New-Item -ItemType "Directory" -Path ftp/ + Get-ChildItem -Directory artifacts | + Foreach-Object -Parallel { + Compress-Archive -Path "artifacts/$($_.Name)" -DestinationPath "ftp/$($_.Name).zip" -Verbose + (Get-FileHash -Algorithm MD5 "ftp/$($_.Name).zip").Hash | Out-File -FilePath "ftp/$($_.Name).md5" -Verbose + } + shell: pwsh + - name: Upload builds to FTP + uses: dennisameling/ftp-upload-action@v1 + with: + server: artemis-rgb.com + username: ${{ secrets.FTP_USER }} + password: ${{ secrets.FTP_PASSWORD }} + secure: false + local_dir: ftp/ + server_dir: /builds.artemis-rgb.com/binaries/gh-actions/${{ needs.version.outputs.version-number }}/ + # - name: Prepare Build Info + # run: | + # New-Item -ItemType "Directory" -Path info/ + # $buildId = ${{ needs.version.outputs.version-number }}.Split(".", 2)[1] + # $sha = ${{ github.sha }} + # Set-Content -Path info/latest.json -Value "{ `"BuildId`": $buildId, `"CommitSha`": `"$sha`" }" + # shell: pwsh + # - name: Upload Build Info to FTP + # uses: dennisameling/ftp-upload-action@v1 + # with: + # server: artemis-rgb.com + # username: ${{ secrets.FTP_USER }} + # password: ${{ secrets.FTP_PASSWORD }} + # secure: false + # local_dir: info/ + # server_dir: /builds.artemis-rgb.com/binaries/ \ No newline at end of file diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index b6fe9fb50..fe329b9d9 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -27,7 +27,7 @@ jobs: $VersionNumber = "$ApiVersion.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday" # If we're not in master, add the branch name to the version so it counts as prerelease if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" } - Write-Output "::set-output name=version-number::$VersionNumber" + "version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT nuget: name: Publish Nuget Packages From a3fde60b3bcfe09ff6c6472e88ed27a587e73b86 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Sun, 23 Oct 2022 15:47:07 +0100 Subject: [PATCH 02/34] CI - Changed artifact name --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index d084692a6..f6c55d811 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -73,7 +73,7 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v3 with: - name: artemis-build-${{ matrix.rid }} + name: artemis-${{ matrix.rid }}-${{ needs.version.outputs.version-number }} path: build/${{ matrix.rid }} ftp: From fb4c8ac839e5dd37ed0f8124d9393c1983f22ffe Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Tue, 10 Jan 2023 21:42:10 +0000 Subject: [PATCH 03/34] CI - Added support for new update backend --- .github/workflows/master.yml | 65 +++++++++++++++--------------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index f6c55d811..ad9362d6c 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -76,46 +76,33 @@ jobs: name: artemis-${{ matrix.rid }}-${{ needs.version.outputs.version-number }} path: build/${{ matrix.rid }} - ftp: - name: Ftp Upload + notify: + name: Notify Backend of build runs-on: ubuntu-latest - needs: [version, build] + needs: build steps: - - name: Download Artifacts - uses: actions/download-artifact@v3 - with: - path: artifacts - - name: Zip and Hash artifacts + - name: post thing + if: needs.build.result == 'success' run: | - New-Item -ItemType "Directory" -Path ftp/ - Get-ChildItem -Directory artifacts | - Foreach-Object -Parallel { - Compress-Archive -Path "artifacts/$($_.Name)" -DestinationPath "ftp/$($_.Name).zip" -Verbose - (Get-FileHash -Algorithm MD5 "ftp/$($_.Name).zip").Hash | Out-File -FilePath "ftp/$($_.Name).md5" -Verbose + $tokenUri = "https://identity.artemis-rgb.com/connect/token" + + $headers = @{ + "Content-Type" = "application/x-www-form-urlencoded" } - shell: pwsh - - name: Upload builds to FTP - uses: dennisameling/ftp-upload-action@v1 - with: - server: artemis-rgb.com - username: ${{ secrets.FTP_USER }} - password: ${{ secrets.FTP_PASSWORD }} - secure: false - local_dir: ftp/ - server_dir: /builds.artemis-rgb.com/binaries/gh-actions/${{ needs.version.outputs.version-number }}/ - # - name: Prepare Build Info - # run: | - # New-Item -ItemType "Directory" -Path info/ - # $buildId = ${{ needs.version.outputs.version-number }}.Split(".", 2)[1] - # $sha = ${{ github.sha }} - # Set-Content -Path info/latest.json -Value "{ `"BuildId`": $buildId, `"CommitSha`": `"$sha`" }" - # shell: pwsh - # - name: Upload Build Info to FTP - # uses: dennisameling/ftp-upload-action@v1 - # with: - # server: artemis-rgb.com - # username: ${{ secrets.FTP_USER }} - # password: ${{ secrets.FTP_PASSWORD }} - # secure: false - # local_dir: info/ - # server_dir: /builds.artemis-rgb.com/binaries/ \ No newline at end of file + + $body = @{ + "grant_type" = "client_credentials" + "client_id" = "github.task-runners" + "client_secret" = "${{ secrets.UPDATE_SECRET }}" + "scope" = "artemis-updating.releases:retrieve" + } + + $response = Invoke-RestMethod -Method Post -Uri $tokenUri -Body $body -Headers $headers + $accessToken = $response.access_token + + $apiUri = "https://updating.artemis-rgb.com/api/github/retrieve-run/${{ github.run_id }}" + $authHeader = @{ + "Authorization" = "Bearer $accessToken" + } + + $updateResponse = Invoke-RestMethod -Method Post -Uri $apiUri -Headers $authHeader From 6b6ea05ff6802ff9fde0563c478d8d76226e3446 Mon Sep 17 00:00:00 2001 From: Diogo Trindade Date: Tue, 10 Jan 2023 21:56:20 +0000 Subject: [PATCH 04/34] CI - run powershell in powershell... --- .github/workflows/master.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index ad9362d6c..87736b431 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -83,6 +83,7 @@ jobs: steps: - name: post thing if: needs.build.result == 'success' + shell: pwsh run: | $tokenUri = "https://identity.artemis-rgb.com/connect/token" From ac960b8f319639889c996fbfe00f2cf4620f8d58 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 15 Jan 2023 12:57:38 +0100 Subject: [PATCH 05/34] Core - Nullability fixes (and I need a build) --- .../Shared/DataModelVisualizationViewModel.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index 029a6dc18..249b422c4 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -163,21 +163,16 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa /// /// Whether the type may be a loose match, meaning it can be cast or converted /// The types to filter - public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes) + public void ApplyTypeFilter(bool looseMatch, params Type?[]? filteredTypes) { if (filteredTypes != null) - { - if (filteredTypes.All(t => t == null)) - filteredTypes = null; - else - filteredTypes = filteredTypes.Where(t => t != null).ToArray(); - } + filteredTypes = filteredTypes.All(t => t == null) ? null : filteredTypes.Where(t => t != null).ToArray(); // If the VM has children, its own type is not relevant if (Children.Any()) { foreach (DataModelVisualizationViewModel child in Children) - child?.ApplyTypeFilter(looseMatch, filteredTypes); + child.ApplyTypeFilter(looseMatch, filteredTypes); IsMatchingFilteredTypes = true; return; @@ -287,7 +282,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken)) { string childPath = AppendToPath(propertyInfo.Name); - if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath))) + if (Children.Any(c => c.Path != null && c.Path.Equals(childPath))) continue; if (propertyInfo.GetCustomAttribute() != null) continue; From 3222eae8767a4ecf9bb023cee07c4d65944bcf68 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 4 Feb 2023 23:29:26 +0100 Subject: [PATCH 06/34] Updating - Initial GraphQL client commit --- src/Artemis.UI/Artemis.UI.csproj | 1 + src/Artemis.UI/ArtemisBootstrapper.cs | 2 - .../.config/dotnet-tools.json | 12 + src/Artemis.WebClient.Updating/.graphqlconfig | 15 + .../.graphqlrc.json | 22 ++ .../Artemis.WebClient.Updating.csproj | 16 ++ .../DryIoc/ContainerExtensions.cs | 26 ++ .../Queries/GetReleaseById.graphql | 16 ++ .../schema.extensions.graphql | 13 + src/Artemis.WebClient.Updating/schema.graphql | 262 ++++++++++++++++++ src/Artemis.sln | 6 + 11 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 src/Artemis.WebClient.Updating/.config/dotnet-tools.json create mode 100644 src/Artemis.WebClient.Updating/.graphqlconfig create mode 100644 src/Artemis.WebClient.Updating/.graphqlrc.json create mode 100644 src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj create mode 100644 src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs create mode 100644 src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql create mode 100644 src/Artemis.WebClient.Updating/schema.extensions.graphql create mode 100644 src/Artemis.WebClient.Updating/schema.graphql diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index bf4ce5d0c..987233c0e 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Artemis.UI/ArtemisBootstrapper.cs b/src/Artemis.UI/ArtemisBootstrapper.cs index 1056070e1..c1dd988a9 100644 --- a/src/Artemis.UI/ArtemisBootstrapper.cs +++ b/src/Artemis.UI/ArtemisBootstrapper.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Reactive; -using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.DryIoc; using Artemis.UI.DryIoc; @@ -12,7 +11,6 @@ using Artemis.UI.Shared.DataModelPicker; using Artemis.UI.Shared.DryIoc; using Artemis.UI.Shared.Services; using Artemis.VisualScripting.DryIoc; -using Artemis.WebClient.Updating; using Artemis.WebClient.Updating.DryIoc; using Avalonia; using Avalonia.Controls; diff --git a/src/Artemis.WebClient.Updating/.config/dotnet-tools.json b/src/Artemis.WebClient.Updating/.config/dotnet-tools.json new file mode 100644 index 000000000..7d8626c03 --- /dev/null +++ b/src/Artemis.WebClient.Updating/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "strawberryshake.tools": { + "version": "13.0.0-rc.4", + "commands": [ + "dotnet-graphql" + ] + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/.graphqlconfig b/src/Artemis.WebClient.Updating/.graphqlconfig new file mode 100644 index 000000000..727ec86a0 --- /dev/null +++ b/src/Artemis.WebClient.Updating/.graphqlconfig @@ -0,0 +1,15 @@ +{ + "name": "Untitled GraphQL Schema", + "schemaPath": "schema.graphql", + "extensions": { + "endpoints": { + "Default GraphQL Endpoint": { + "url": "https://updating.artemis-rgb.com/graphql", + "headers": { + "user-agent": "JS GraphQL" + }, + "introspect": true + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/.graphqlrc.json b/src/Artemis.WebClient.Updating/.graphqlrc.json new file mode 100644 index 000000000..d500e1cee --- /dev/null +++ b/src/Artemis.WebClient.Updating/.graphqlrc.json @@ -0,0 +1,22 @@ +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "UpdatingClient", + "namespace": "Artemis.WebClient.Updating", + "url": "https://updating.artemis-rgb.com/graphql/", + "emitGeneratedCode": false, + "records": { + "inputs": false, + "entities": false + }, + "transportProfiles": [ + { + "default": "Http", + "subscription": "WebSocket" + } + ] + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj b/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj new file mode 100644 index 000000000..7f2bb002f --- /dev/null +++ b/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs b/src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs new file mode 100644 index 000000000..e52e454d3 --- /dev/null +++ b/src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs @@ -0,0 +1,26 @@ +using DryIoc; +using DryIoc.Microsoft.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Artemis.WebClient.Updating.DryIoc; + +/// +/// Provides an extension method to register services onto a DryIoc . +/// +public static class ContainerExtensions +{ + /// + /// Registers the updating client into the container. + /// + /// The builder building the current container + public static void RegisterUpdatingClient(this IContainer container) + { + ServiceCollection serviceCollection = new(); + serviceCollection + .AddHttpClient() + .AddUpdatingClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://updating.artemis-rgb.com/graphql")); + + container.WithDependencyInjectionAdapter(serviceCollection); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql new file mode 100644 index 000000000..fa17ed4f0 --- /dev/null +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -0,0 +1,16 @@ +query GetReleaseById { + release(id: "63b71dd69a5bb32a0a81a410") { + branch + commit + version + artifacts { + platform + artifactId + fileInfo { + md5Hash + downloadSize + downloads + } + } + } +} diff --git a/src/Artemis.WebClient.Updating/schema.extensions.graphql b/src/Artemis.WebClient.Updating/schema.extensions.graphql new file mode 100644 index 000000000..0b5fbd98b --- /dev/null +++ b/src/Artemis.WebClient.Updating/schema.extensions.graphql @@ -0,0 +1,13 @@ +scalar _KeyFieldSet + +directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT + +directive @serializationType(name: String!) on SCALAR + +directive @runtimeType(name: String!) on SCALAR + +directive @enumValue(value: String!) on ENUM_VALUE + +directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE + +extend schema @key(fields: "id") \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/schema.graphql b/src/Artemis.WebClient.Updating/schema.graphql new file mode 100644 index 000000000..8a4840e8e --- /dev/null +++ b/src/Artemis.WebClient.Updating/schema.graphql @@ -0,0 +1,262 @@ +# This file was generated based on ".graphqlconfig". Do not edit manually. + +schema { + query: Query + mutation: Mutation +} + +"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." +directive @defer( + "Deferred when true." + if: Boolean, + "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." + label: String +) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." +directive @stream( + "Streamed when true." + if: Boolean, + "The initial elements that shall be send down to the consumer." + initialCount: Int! = 0, + "If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." + label: String +) on FIELD + +directive @authorize( + "Defines when when the resolver shall be executed.By default the resolver is executed after the policy has determined that the current user is allowed to access the field." + apply: ApplyPolicy! = BEFORE_RESOLVER, + "The name of the authorization policy that determines access to the annotated resource." + policy: String, + "Roles that are allowed to access the annotated resource." + roles: [String!] +) on SCHEMA | OBJECT | FIELD_DEFINITION + +type ArtemisChannel { + branch: String! + releases: Int! +} + +type Artifact { + artifactId: Long! + deltaFileInfo: ArtifactFileInfo! + fileInfo: ArtifactFileInfo! + fileName(deltaFile: Boolean!): String! + platform: Platform! +} + +type ArtifactFileInfo { + downloadSize: Long! + downloads: Long! + md5Hash: String +} + +type Mutation { + updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload! +} + +type Query { + channelByBranch(branch: String!): ArtemisChannel + channels: [ArtemisChannel!]! + nextRelease(branch: String!, platform: Platform!, version: String!): Release + release(id: String!): Release + releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]! + releases(order: [ReleaseSortInput!], where: ReleaseFilterInput): [Release!]! +} + +type Release { + artifacts: [Artifact!]! + branch: String! + changelog: String! + commit: String! + createdAt: DateTime! + id: String! + isDraft: Boolean! + previousRelease: String + version: String! + workflowRunId: Long! +} + +type ReleaseStatistic { + count: Int! + lastReportedUsage: DateTime! + linuxCount: Int! + oSXCount: Int! + releaseId: String! + windowsCount: Int! +} + +type UpdateReleaseChangelogPayload { + release: Release +} + +enum ApplyPolicy { + AFTER_RESOLVER + BEFORE_RESOLVER +} + +enum Platform { + LINUX + OSX + WINDOWS +} + +enum SortEnumType { + ASC + DESC +} + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime + +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long + +input ArtifactFileInfoFilterInput { + and: [ArtifactFileInfoFilterInput!] + downloadSize: ComparableInt64OperationFilterInput + downloads: ComparableInt64OperationFilterInput + md5Hash: StringOperationFilterInput + or: [ArtifactFileInfoFilterInput!] +} + +input ArtifactFilterInput { + and: [ArtifactFilterInput!] + artifactId: ComparableInt64OperationFilterInput + deltaFileInfo: ArtifactFileInfoFilterInput + fileInfo: ArtifactFileInfoFilterInput + or: [ArtifactFilterInput!] + platform: PlatformOperationFilterInput +} + +input BooleanOperationFilterInput { + eq: Boolean + neq: Boolean +} + +input ComparableDateTimeOffsetOperationFilterInput { + eq: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + neq: DateTime + ngt: DateTime + ngte: DateTime + nin: [DateTime!] + nlt: DateTime + nlte: DateTime +} + +input ComparableInt32OperationFilterInput { + eq: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + neq: Int + ngt: Int + ngte: Int + nin: [Int!] + nlt: Int + nlte: Int +} + +input ComparableInt64OperationFilterInput { + eq: Long + gt: Long + gte: Long + in: [Long!] + lt: Long + lte: Long + neq: Long + ngt: Long + ngte: Long + nin: [Long!] + nlt: Long + nlte: Long +} + +input ListFilterInputTypeOfArtifactFilterInput { + all: ArtifactFilterInput + any: Boolean + none: ArtifactFilterInput + some: ArtifactFilterInput +} + +input PlatformOperationFilterInput { + eq: Platform + in: [Platform!] + neq: Platform + nin: [Platform!] +} + +input ReleaseFilterInput { + and: [ReleaseFilterInput!] + artifacts: ListFilterInputTypeOfArtifactFilterInput + branch: StringOperationFilterInput + changelog: StringOperationFilterInput + commit: StringOperationFilterInput + createdAt: ComparableDateTimeOffsetOperationFilterInput + id: StringOperationFilterInput + isDraft: BooleanOperationFilterInput + or: [ReleaseFilterInput!] + previousRelease: StringOperationFilterInput + version: StringOperationFilterInput + workflowRunId: ComparableInt64OperationFilterInput +} + +input ReleaseSortInput { + branch: SortEnumType + changelog: SortEnumType + commit: SortEnumType + createdAt: SortEnumType + id: SortEnumType + isDraft: SortEnumType + previousRelease: SortEnumType + version: SortEnumType + workflowRunId: SortEnumType +} + +input ReleaseStatisticFilterInput { + and: [ReleaseStatisticFilterInput!] + count: ComparableInt32OperationFilterInput + lastReportedUsage: ComparableDateTimeOffsetOperationFilterInput + linuxCount: ComparableInt32OperationFilterInput + oSXCount: ComparableInt32OperationFilterInput + or: [ReleaseStatisticFilterInput!] + releaseId: StringOperationFilterInput + windowsCount: ComparableInt32OperationFilterInput +} + +input ReleaseStatisticSortInput { + count: SortEnumType + lastReportedUsage: SortEnumType + linuxCount: SortEnumType + oSXCount: SortEnumType + releaseId: SortEnumType + windowsCount: SortEnumType +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + contains: String + endsWith: String + eq: String + in: [String] + ncontains: String + nendsWith: String + neq: String + nin: [String] + nstartsWith: String + or: [StringOperationFilterInput!] + startsWith: String +} + +input UpdateReleaseChangelogInput { + changelog: String! + id: String! + isDraft: Boolean! +} diff --git a/src/Artemis.sln b/src/Artemis.sln index dec26a200..2e53b5d9a 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.MacOS", "Artemis EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{412B921A-26F5-4AE6-8B32-0C19BE54F421}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.WebClient.Updating", "Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj", "{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -57,6 +59,10 @@ Global {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|x64.Build.0 = Debug|x64 {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.ActiveCfg = Release|x64 {412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.Build.0 = Release|x64 + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.ActiveCfg = Debug|Any CPU + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.Build.0 = Debug|Any CPU + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.ActiveCfg = Release|Any CPU + {7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a5d224959366f88503dae2118e55f9ae16f0135b Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 5 Feb 2023 21:48:56 +0100 Subject: [PATCH 07/34] Updating - Initial client code --- .../Providers/IUpdateProvider.cs | 31 --- .../Providers/UpdateProvider.cs | 213 ------------------ src/Artemis.UI/Artemis.UI.csproj | 1 + .../Extensions/HttpClientExtensions.cs | 56 +++++ .../Updating/UpdateInstallationViewModel.cs | 14 ++ src/Artemis.UI/Services/UpdateService.cs | 113 ---------- .../Updating/IUpdateNotificationProvider.cs | 8 + .../IUpdateService.cs | 2 +- .../Services/Updating/ReleaseInstaller.cs | 142 ++++++++++++ .../SimpleUpdateNotificationProvider.cs | 6 + .../Services/Updating/UpdateService.cs | 147 ++++++++++++ .../Queries/GetReleaseById.graphql | 24 +- 12 files changed, 394 insertions(+), 363 deletions(-) delete mode 100644 src/Artemis.UI.Shared/Providers/IUpdateProvider.cs delete mode 100644 src/Artemis.UI.Windows/Providers/UpdateProvider.cs create mode 100644 src/Artemis.UI/Extensions/HttpClientExtensions.cs create mode 100644 src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs delete mode 100644 src/Artemis.UI/Services/UpdateService.cs create mode 100644 src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs rename src/Artemis.UI/Services/{Interfaces => Updating}/IUpdateService.cs (93%) create mode 100644 src/Artemis.UI/Services/Updating/ReleaseInstaller.cs create mode 100644 src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs create mode 100644 src/Artemis.UI/Services/Updating/UpdateService.cs diff --git a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs b/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs deleted file mode 100644 index 0c33b135f..000000000 --- a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; - -namespace Artemis.UI.Shared.Providers; - -/// -/// Represents a provider for custom cursors. -/// -public interface IUpdateProvider -{ - /// - /// Asynchronously checks whether an update is available. - /// - /// The channel to use when checking updates (i.e. master or development) - /// A task returning if an update is available; otherwise . - Task CheckForUpdate(string channel); - - /// - /// Applies any available updates. - /// - /// The channel to use when checking updates (i.e. master or development) - /// Whether or not to update silently. - Task ApplyUpdate(string channel, bool silent); - - /// - /// Offer to install the update to the user. - /// - /// The channel to use when checking updates (i.e. master or development) - /// A boolean indicating whether the main window is open. - /// A task returning if the user chose to update; otherwise . - Task OfferUpdate(string channel, bool windowOpen); -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs b/src/Artemis.UI.Windows/Providers/UpdateProvider.cs deleted file mode 100644 index d3d20081e..000000000 --- a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Exceptions; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.MainWindow; -using Artemis.UI.Windows.Models; -using Artemis.UI.Windows.Screens.Update; -using Avalonia.Threading; -using Flurl; -using Flurl.Http; -using Microsoft.Toolkit.Uwp.Notifications; -using Serilog; -using File = System.IO.File; - -namespace Artemis.UI.Windows.Providers; - -public class UpdateProvider : IUpdateProvider, IDisposable -{ - private const string API_URL = "https://dev.azure.com/artemis-rgb/Artemis/_apis/"; - private const string INSTALLER_URL = "https://builds.artemis-rgb.com/binaries/Artemis.Installer.exe"; - - private readonly ILogger _logger; - private readonly IMainWindowService _mainWindowService; - private readonly IWindowService _windowService; - - public UpdateProvider(ILogger logger, IWindowService windowService, IMainWindowService mainWindowService) - { - _logger = logger; - _windowService = windowService; - _mainWindowService = mainWindowService; - - ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; - } - - public async Task GetBuildInfo(int buildDefinition, string? buildNumber = null) - { - Url request = API_URL.AppendPathSegments("build", "builds") - .SetQueryParam("definitions", buildDefinition) - .SetQueryParam("resultFilter", "succeeded") - .SetQueryParam("branchName", "refs/heads/master") - .SetQueryParam("$top", 1) - .SetQueryParam("api-version", "6.1-preview.6"); - - if (buildNumber != null) - request = request.SetQueryParam("buildNumber", buildNumber); - - try - { - DevOpsBuilds result = await request.GetJsonAsync(); - try - { - return result.Builds.FirstOrDefault(); - } - catch (Exception e) - { - _logger.Warning(e, "GetBuildInfo: Failed to retrieve build info JSON"); - throw; - } - } - catch (FlurlHttpException e) - { - _logger.Warning("GetBuildInfo: Getting build info, request returned {StatusCode}", e.StatusCode); - throw; - } - } - - public async Task GetBuildDifferences(DevOpsBuild a, DevOpsBuild b) - { - return await "https://api.github.com" - .AppendPathSegments("repos", "Artemis-RGB", "Artemis", "compare") - .AppendPathSegment(a.SourceVersion + "..." + b.SourceVersion) - .WithHeader("User-Agent", "Artemis 2") - .WithHeader("Accept", "application/vnd.github.v3+json") - .GetJsonAsync(); - } - - private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e) - { - ToastArguments args = ToastArguments.Parse(e.Argument); - string channel = args.Get("channel"); - string action = "view-changes"; - if (args.Contains("action")) - action = args.Get("action"); - - if (action == "install") - await RunInstaller(channel, true); - else if (action == "view-changes") - await Dispatcher.UIThread.InvokeAsync(async () => - { - _mainWindowService.OpenMainWindow(); - await OfferUpdate(channel, true); - }); - } - - private async Task RunInstaller(string channel, bool silent) - { - _logger.Information("ApplyUpdate: Applying update"); - - // Ensure the installer is up-to-date, get installer build info - DevOpsBuild? buildInfo = await GetBuildInfo(6); - string installerPath = Path.Combine(Constants.DataFolder, "installer", "Artemis.Installer.exe"); - - // Always update installer if it is missing ^^ - if (!File.Exists(installerPath)) - { - await UpdateInstaller(); - } - // Compare the creation date of the installer with the build date and update if needed - else - { - if (buildInfo != null && File.GetLastWriteTime(installerPath) < buildInfo.FinishTime) - await UpdateInstaller(); - } - - _logger.Information("ApplyUpdate: Running installer at {InstallerPath}", installerPath); - - try - { - Process.Start(new ProcessStartInfo(installerPath, "-autoupdate") - { - UseShellExecute = true, - Verb = "runas" - }); - } - catch (Win32Exception e) - { - if (e.NativeErrorCode == 0x4c7) - _logger.Warning("ApplyUpdate: Operation was cancelled, user likely clicked No in UAC dialog"); - else - throw; - } - } - - private async Task UpdateInstaller() - { - string installerDirectory = Path.Combine(Constants.DataFolder, "installer"); - string installerPath = Path.Combine(installerDirectory, "Artemis.Installer.exe"); - - _logger.Information("UpdateInstaller: Downloading installer from {DownloadUrl}", INSTALLER_URL); - using HttpClient client = new(); - HttpResponseMessage httpResponseMessage = await client.GetAsync(INSTALLER_URL); - if (!httpResponseMessage.IsSuccessStatusCode) - throw new ArtemisUIException($"Failed to download installer, status code {httpResponseMessage.StatusCode}"); - - _logger.Information("UpdateInstaller: Writing installer file to {InstallerPath}", installerPath); - if (File.Exists(installerPath)) - File.Delete(installerPath); - - Core.Utilities.CreateAccessibleDirectory(installerDirectory); - await using FileStream fs = new(installerPath, FileMode.Create, FileAccess.Write, FileShare.None); - await httpResponseMessage.Content.CopyToAsync(fs); - } - - private void ShowDesktopNotification(string channel) - { - new ToastContentBuilder() - .AddArgument("channel", channel) - .AddText("An update is available") - .AddButton(new ToastButton().SetContent("Install").AddArgument("action", "install").SetBackgroundActivation()) - .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes")) - .Show(); - } - - /// - public void Dispose() - { - ToastNotificationManagerCompat.OnActivated -= ToastNotificationManagerCompatOnOnActivated; - ToastNotificationManagerCompat.Uninstall(); - } - - /// - public async Task CheckForUpdate(string channel) - { - DevOpsBuild? buildInfo = await GetBuildInfo(1); - if (buildInfo == null) - return false; - - double buildNumber = double.Parse(buildInfo.BuildNumber, CultureInfo.InvariantCulture); - string buildNumberDisplay = buildNumber.ToString(CultureInfo.InvariantCulture); - _logger.Information("Latest build is {BuildNumber}, we're running {LocalBuildNumber}", buildNumberDisplay, Constants.BuildInfo.BuildNumberDisplay); - - return buildNumber > Constants.BuildInfo.BuildNumber; - } - - /// - public async Task ApplyUpdate(string channel, bool silent) - { - await RunInstaller(channel, silent); - } - - /// - public async Task OfferUpdate(string channel, bool windowOpen) - { - if (windowOpen) - { - bool update = await _windowService.ShowDialogAsync(channel); - if (update) - await RunInstaller(channel, false); - } - else - { - ShowDesktopNotification(channel); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 987233c0e..101b1ac8a 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Artemis.UI/Extensions/HttpClientExtensions.cs b/src/Artemis.UI/Extensions/HttpClientExtensions.cs new file mode 100644 index 000000000..50af33443 --- /dev/null +++ b/src/Artemis.UI/Extensions/HttpClientExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.UI.Extensions +{ + public static class HttpClientProgressExtensions + { + public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress? progress, CancellationToken cancellationToken) + { + using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + response.EnsureSuccessStatusCode(); + + long? contentLength = response.Content.Headers.ContentLength; + await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken); + // no progress... no contentLength... very sad + if (progress is null || !contentLength.HasValue) + { + await download.CopyToAsync(destination, cancellationToken); + return; + } + + // Such progress and contentLength much reporting Wow! + Progress progressWrapper = new(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value))); + await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken); + + float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f; + } + + static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress progress, CancellationToken cancellationToken) + { + if (bufferSize < 0) + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + if (source is null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new InvalidOperationException($"'{nameof(source)}' is not readable."); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new InvalidOperationException($"'{nameof(destination)}' is not writable."); + + byte[] buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report(totalBytesRead); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs new file mode 100644 index 000000000..4b1950846 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs @@ -0,0 +1,14 @@ +using Artemis.Core; +using Artemis.UI.Shared; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class UpdateInstallationViewModel : DialogViewModelBase +{ + private readonly string _nextReleaseId; + + public UpdateInstallationViewModel(string nextReleaseId) + { + _nextReleaseId = nextReleaseId; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs deleted file mode 100644 index ab8e0e2ff..000000000 --- a/src/Artemis.UI/Services/UpdateService.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Timers; -using Artemis.Core; -using Artemis.Core.Services; -using Artemis.UI.Services.Interfaces; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services.MainWindow; -using Avalonia.Threading; -using DryIoc; -using Serilog; - -namespace Artemis.UI.Services; - -public class UpdateService : IUpdateService -{ - private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour - - private readonly PluginSetting _autoUpdate; - private readonly PluginSetting _checkForUpdates; - private readonly ILogger _logger; - private readonly IMainWindowService _mainWindowService; - private readonly IUpdateProvider? _updateProvider; - - public UpdateService(ILogger logger, IContainer container, ISettingsService settingsService, IMainWindowService mainWindowService) - { - _logger = logger; - _mainWindowService = mainWindowService; - - if (!Constants.BuildInfo.IsLocalBuild) - _updateProvider = container.Resolve(IfUnresolved.ReturnDefault); - - _checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true); - _autoUpdate = settingsService.GetSetting("UI.AutoUpdate", false); - _checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged; - _mainWindowService.MainWindowOpened += WindowServiceOnMainWindowOpened; - - Timer timer = new(UPDATE_CHECK_INTERVAL); - timer.Elapsed += TimerOnElapsed; - timer.Start(); - } - - private async void TimerOnElapsed(object? sender, ElapsedEventArgs e) - { - await AutoUpdate(); - } - - private async void CheckForUpdatesOnSettingChanged(object? sender, EventArgs e) - { - // Run an auto-update as soon as the setting gets changed to enabled - if (_checkForUpdates.Value) - await AutoUpdate(); - } - - private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) - { - await AutoUpdate(); - } - - private async Task AutoUpdate() - { - if (_updateProvider == null || !_checkForUpdates.Value || SuspendAutoUpdate) - return; - - try - { - bool updateAvailable = await _updateProvider.CheckForUpdate("master"); - if (!updateAvailable) - return; - - // Only offer it once per session - SuspendAutoUpdate = true; - - // If the window is open show the changelog, don't auto-update while the user is busy - if (_mainWindowService.IsMainWindowOpen) - { - await Dispatcher.UIThread.InvokeAsync(async () => - { - // Call OpenMainWindow anyway to focus the main window - _mainWindowService.OpenMainWindow(); - await _updateProvider.OfferUpdate("master", true); - }); - return; - } - - // If the window is closed but auto-update is enabled, update silently - if (_autoUpdate.Value) - await _updateProvider.ApplyUpdate("master", true); - // If auto-update is disabled the update provider can show a notification and handle the rest - else - await _updateProvider.OfferUpdate("master", false); - } - catch (Exception e) - { - _logger.Warning(e, "Auto update failed"); - } - } - - public bool SuspendAutoUpdate { get; set; } - public bool UpdatingSupported => _updateProvider != null; - - public async Task ManualUpdate() - { - if (_updateProvider == null || !_mainWindowService.IsMainWindowOpen) - return; - - bool updateAvailable = await _updateProvider.CheckForUpdate("master"); - if (!updateAvailable) - return; - - await _updateProvider.OfferUpdate("master", true); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs new file mode 100644 index 000000000..fcf306bb4 --- /dev/null +++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs @@ -0,0 +1,8 @@ +using System.Threading.Tasks; + +namespace Artemis.UI.Services.Updating; + +public interface IUpdateNotificationProvider +{ + Task ShowNotification(string releaseId); +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Interfaces/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs similarity index 93% rename from src/Artemis.UI/Services/Interfaces/IUpdateService.cs rename to src/Artemis.UI/Services/Updating/IUpdateService.cs index cc6236853..157d1ef07 100644 --- a/src/Artemis.UI/Services/Interfaces/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -12,7 +12,7 @@ public interface IUpdateService : IArtemisUIService /// /// Gets or sets a boolean indicating whether auto-updating is suspended. /// - bool SuspendAutoUpdate { get; set; } + bool SuspendAutoCheck { get; set; } /// /// Manually checks for updates and offers to install it if found. diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs new file mode 100644 index 000000000..f868298b7 --- /dev/null +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -0,0 +1,142 @@ +using System; +using System.Drawing.Drawing2D; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Reflection.Metadata; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.WebClient.Updating; +using NoStringEvaluating.Functions.Math; +using Octodiff.Core; +using Octodiff.Diagnostics; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Services.Updating; + +/// +/// Represents the installation process of a release +/// +public class ReleaseInstaller +{ + private readonly string _releaseId; + private readonly ILogger _logger; + private readonly IUpdatingClient _updatingClient; + private readonly HttpClient _httpClient; + private readonly Platform _updatePlatform; + private readonly string _dataFolder; + + public IProgress OverallProgress { get; } = new Progress(); + public IProgress StepProgress { get; } = new Progress(); + + public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) + { + _releaseId = releaseId; + _logger = logger; + _updatingClient = updatingClient; + _httpClient = httpClient; + _dataFolder = Path.Combine(Constants.DataFolder, "updating"); + + if (OperatingSystem.IsWindows()) + _updatePlatform = Platform.Windows; + if (OperatingSystem.IsLinux()) + _updatePlatform = Platform.Linux; + else if (OperatingSystem.IsMacOS()) + _updatePlatform = Platform.Osx; + else + throw new PlatformNotSupportedException("Cannot auto update on the current platform"); + + if (!Directory.Exists(_dataFolder)) + Directory.CreateDirectory(_dataFolder); + } + + public async Task InstallAsync(CancellationToken cancellationToken) + { + OverallProgress.Report(0); + + _logger.Information("Retrieving details for release {ReleaseId}", _releaseId); + IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken); + result.EnsureNoErrors(); + + IGetReleaseById_Release? release = result.Data?.Release; + if (release == null) + throw new Exception($"Could not find release with ID {_releaseId}"); + + IGetReleaseById_Release_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform); + if (artifact == null) + throw new Exception("Found the release but it has no artifact for the current platform"); + + OverallProgress.Report(0.1f); + + // Determine whether the last update matches our local version, then we can download the delta + if (release.PreviousRelease != null && File.Exists(Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip")) && artifact.DeltaFileInfo.DownloadSize != 0) + await DownloadDelta(artifact, Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip"), cancellationToken); + else + await Download(artifact, cancellationToken); + } + + private async Task DownloadDelta(IGetReleaseById_Release_Artifacts artifact, string previousRelease, CancellationToken cancellationToken) + { + await using MemoryStream stream = new(); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken); + + OverallProgress.Report(0.33f); + + await PatchDelta(stream, previousRelease, cancellationToken); + } + + private async Task PatchDelta(MemoryStream deltaStream, string previousRelease, CancellationToken cancellationToken) + { + await using FileStream baseStream = File.OpenRead(previousRelease); + await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + + deltaStream.Seek(0, SeekOrigin.Begin); + DeltaApplier deltaApplier = new(); + deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream); + cancellationToken.ThrowIfCancellationRequested(); + + OverallProgress.Report(0.66f); + await Extract(newFileStream, cancellationToken); + } + + private async Task Download(IGetReleaseById_Release_Artifacts artifact, CancellationToken cancellationToken) + { + await using MemoryStream stream = new(); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken); + + OverallProgress.Report(0.5f); + } + + private async Task Extract(FileStream archiveStream, CancellationToken cancellationToken) + { + // Ensure the directory is empty + string extractDirectory = Path.Combine(_dataFolder, "pending"); + if (Directory.Exists(extractDirectory)) + Directory.Delete(extractDirectory, true); + Directory.CreateDirectory(extractDirectory); + + archiveStream.Seek(0, SeekOrigin.Begin); + using ZipArchive archive = new(archiveStream); + archive.ExtractToDirectory(extractDirectory); + OverallProgress.Report(1); + } +} + +internal class DeltaApplierProgressReporter : IProgressReporter +{ + private readonly IProgress _stepProgress; + + public DeltaApplierProgressReporter(IProgress stepProgress) + { + _stepProgress = stepProgress; + } + + public void ReportProgress(string operation, long currentPosition, long total) + { + _stepProgress.Report((float) currentPosition / total); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs new file mode 100644 index 000000000..94bcaf3be --- /dev/null +++ b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs @@ -0,0 +1,6 @@ +namespace Artemis.UI.Services.Updating; + +public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider +{ + +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs new file mode 100644 index 000000000..1077d549c --- /dev/null +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -0,0 +1,147 @@ +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.Settings.Updating; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared.Providers; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.MainWindow; +using Artemis.WebClient.Updating; +using Avalonia.Threading; +using DryIoc; +using Serilog; +using StrawberryShake; +using Timer = System.Timers.Timer; + +namespace Artemis.UI.Services; + +public class UpdateService : IUpdateService +{ + private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour + + private readonly ILogger _logger; + private readonly IMainWindowService _mainWindowService; + private readonly IWindowService _windowService; + private readonly IUpdatingClient _updatingClient; + private readonly Lazy _updateNotificationProvider; + private readonly Func _getReleaseInstaller; + private readonly Platform _updatePlatform; + private readonly PluginSetting _channel; + private readonly PluginSetting _autoCheck; + private readonly PluginSetting _autoInstall; + + private bool _suspendAutoCheck; + + public UpdateService(ILogger logger, + ISettingsService settingsService, + IMainWindowService mainWindowService, + IWindowService windowService, + IUpdatingClient updatingClient, + Lazy updateNotificationProvider, + Func getReleaseInstaller) + { + _logger = logger; + _mainWindowService = mainWindowService; + _windowService = windowService; + _updatingClient = updatingClient; + _updateNotificationProvider = updateNotificationProvider; + _getReleaseInstaller = getReleaseInstaller; + + if (OperatingSystem.IsWindows()) + _updatePlatform = Platform.Windows; + if (OperatingSystem.IsLinux()) + _updatePlatform = Platform.Linux; + else if (OperatingSystem.IsMacOS()) + _updatePlatform = Platform.Osx; + else + throw new PlatformNotSupportedException("Cannot auto update on the current platform"); + + _channel = settingsService.GetSetting("UI.Updating.Channel", "master"); + _autoCheck = settingsService.GetSetting("UI.Updating.AutoCheck", true); + _autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", false); + _autoCheck.SettingChanged += HandleAutoUpdateEvent; + _mainWindowService.MainWindowOpened += HandleAutoUpdateEvent; + Timer timer = new(UPDATE_CHECK_INTERVAL); + timer.Elapsed += HandleAutoUpdateEvent; + timer.Start(); + } + + public async Task CheckForUpdate() + { + string? currentVersion = AssemblyProductVersion; + if (currentVersion == null) + return false; + + IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform); + result.EnsureNoErrors(); + + // No update was found + if (result.Data?.NextRelease == null) + return false; + + // Only offer it once per session + _suspendAutoCheck = true; + + // If the window is open show the changelog, don't auto-update while the user is busy + if (_mainWindowService.IsMainWindowOpen) + await ShowUpdateDialog(result.Data.NextRelease.Id); + else if (!_autoInstall.Value) + await ShowUpdateNotification(result.Data.NextRelease.Id); + else + await AutoInstallUpdate(result.Data.NextRelease.Id); + + return true; + } + + private async Task ShowUpdateDialog(string nextReleaseId) + { + await Dispatcher.UIThread.InvokeAsync(async () => + { + // Main window is probably already open but this will bring it into focus + _mainWindowService.OpenMainWindow(); + await _windowService.ShowDialogAsync(nextReleaseId); + }); + } + + private async Task ShowUpdateNotification(string nextReleaseId) + { + await _updateNotificationProvider.Value.ShowNotification(nextReleaseId); + } + + private async Task AutoInstallUpdate(string nextReleaseId) + { + ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId); + await installer.InstallAsync(CancellationToken.None); + } + + private async void HandleAutoUpdateEvent(object? sender, EventArgs e) + { + if (!_autoCheck.Value || _suspendAutoCheck) + return; + + try + { + await CheckForUpdate(); + } + catch (Exception ex) + { + _logger.Warning(ex, "Auto update failed"); + } + } + + private static string? AssemblyProductVersion + { + get + { + object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion; + } + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql index fa17ed4f0..cd7e8b2a8 100644 --- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -1,16 +1,30 @@ -query GetReleaseById { - release(id: "63b71dd69a5bb32a0a81a410") { +query GetReleaseById($id: String!) { + release(id: $id) { branch commit version + previousRelease artifacts { platform artifactId fileInfo { - md5Hash - downloadSize - downloads + ...fileInfo + } + deltaFileInfo { + ...fileInfo } } } } + +fragment fileInfo on ArtifactFileInfo { + md5Hash + downloadSize +} + +query GetNextRelease($currentVersion: String!, $branch: String!, $platform: Platform!) { + nextRelease(version: $currentVersion, branch: $branch, platform: $platform) { + id + version + } +} \ No newline at end of file From 0cd65a2ebff79e239cbb4dc4143f6f906cda6b04 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 12 Feb 2023 16:48:16 +0100 Subject: [PATCH 08/34] Implemented Artemis.Web.Update client --- src/Artemis.Core/Events/UpdateEventArgs.cs | 20 ++ .../Services/PluginManagementService.cs | 16 +- src/Artemis.Core/Utilities/Utilities.cs | 21 +- src/Artemis.Storage/StorageManager.cs | 2 +- .../ApplicationStateManager.cs | 32 ++- .../Artemis.UI.Windows.csproj | 18 +- .../DryIoc/ContainerExtensions.cs | 1 - .../Screens/Update/UpdateDialogView.axaml | 70 ------ .../Screens/Update/UpdateDialogView.axaml.cs | 21 -- .../Screens/Update/UpdateDialogViewModel.cs | 120 ---------- src/Artemis.UI.Windows/Scripts/update.ps1 | 42 ++++ src/Artemis.UI/Artemis.UI.csproj | 1 + .../BrushPropertyInputView.axaml | 3 +- .../BrushPropertyInputViewModel.cs | 2 +- src/Artemis.UI/DryIoc/ContainerExtensions.cs | 2 + .../CompositeDisposableExtensions.cs | 14 ++ .../Extensions/ZipArchiveExtensions.cs | 74 ++++++ src/Artemis.UI/Screens/Root/RootViewModel.cs | 1 + .../Settings/Tabs/GeneralTabView.axaml | 2 +- .../Settings/Tabs/GeneralTabViewModel.cs | 20 +- .../Updating/ReleaseAvailableView.axaml | 216 ++++++++++++++++++ .../Updating/ReleaseAvailableView.axaml.cs | 29 +++ .../Updating/ReleaseAvailableViewModel.cs | 76 ++++++ .../Updating/ReleaseInstallerView.axaml | 40 ++++ .../Updating/ReleaseInstallerView.axaml.cs | 28 +++ .../Updating/ReleaseInstallerViewModel.cs | 81 +++++++ .../Updating/UpdateInstallationViewModel.cs | 14 -- .../StartupWizard/StartupWizardViewModel.cs | 10 +- .../StartupWizard/Steps/SettingsStep.axaml | 2 +- .../Services/Updating/IUpdateService.cs | 21 +- .../Services/Updating/ReleaseInstaller.cs | 65 +++--- .../SimpleUpdateNotificationProvider.cs | 8 +- .../Services/Updating/UpdateService.cs | 116 +++++----- src/Artemis.UI/Styles/Artemis.axaml | 1 + src/Artemis.UI/Styles/Markdown.axaml | 191 ++++++++++++++++ .../Queries/GetReleaseById.graphql | 1 + 36 files changed, 1022 insertions(+), 359 deletions(-) create mode 100644 src/Artemis.Core/Events/UpdateEventArgs.cs delete mode 100644 src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml delete mode 100644 src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs delete mode 100644 src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs create mode 100644 src/Artemis.UI.Windows/Scripts/update.ps1 create mode 100644 src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs create mode 100644 src/Artemis.UI/Extensions/ZipArchiveExtensions.cs create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs create mode 100644 src/Artemis.UI/Styles/Markdown.axaml diff --git a/src/Artemis.Core/Events/UpdateEventArgs.cs b/src/Artemis.Core/Events/UpdateEventArgs.cs new file mode 100644 index 000000000..d513bae5e --- /dev/null +++ b/src/Artemis.Core/Events/UpdateEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core; + +/// +/// Provides data about application update events +/// +public class UpdateEventArgs : EventArgs +{ + internal UpdateEventArgs(bool silent) + { + Silent = silent; + } + + /// + /// Gets a boolean indicating whether to silently update or not. + /// + public bool Silent { get; } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 6de473234..e313ca59d 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -84,7 +84,20 @@ internal class PluginManagementService : IPluginManagementService foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip")) { - // Find the metadata file in the zip + try + { + ExtractBuiltInPlugin(zipFile, pluginDirectory); + } + catch (Exception e) + { + _logger.Error(e, "Failed to copy built-in plugin from {ZipFile}", zipFile.FullName); + } + } + } + + private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory) + { + // Find the metadata file in the zip using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); ZipArchiveEntry? metaDataFileEntry = archive.GetEntry("plugin.json"); if (metaDataFileEntry == null) @@ -135,7 +148,6 @@ internal class PluginManagementService : IPluginManagementService } } } - } } #endregion diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index bbb38c4be..e5590b568 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -50,6 +50,15 @@ public static class Utilities OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); } + /// + /// Applies a pending update + /// + /// A boolean indicating whether to silently update or not. + public static void ApplyUpdate(bool silent) + { + OnUpdateRequested(new UpdateEventArgs(silent)); + } + /// /// Opens the provided URL in the default web browser /// @@ -96,11 +105,16 @@ public static class Utilities /// Occurs when the core has requested an application shutdown /// public static event EventHandler? ShutdownRequested; - + /// /// Occurs when the core has requested an application restart /// public static event EventHandler? RestartRequested; + + /// + /// Occurs when the core has requested a pending application update to be applied + /// + public static event EventHandler? UpdateRequested; /// /// Opens the provided folder in the user's file explorer @@ -136,6 +150,11 @@ public static class Utilities { ShutdownRequested?.Invoke(null, EventArgs.Empty); } + + private static void OnUpdateRequested(UpdateEventArgs e) + { + UpdateRequested?.Invoke(null, e); + } #region Scaling diff --git a/src/Artemis.Storage/StorageManager.cs b/src/Artemis.Storage/StorageManager.cs index 87bc97b76..78fcc3643 100644 --- a/src/Artemis.Storage/StorageManager.cs +++ b/src/Artemis.Storage/StorageManager.cs @@ -30,7 +30,7 @@ public static class StorageManager { FileSystemInfo newest = files.OrderByDescending(fi => fi.CreationTime).First(); FileSystemInfo oldest = files.OrderBy(fi => fi.CreationTime).First(); - if (DateTime.Now - newest.CreationTime < TimeSpan.FromMinutes(10)) + if (DateTime.Now - newest.CreationTime < TimeSpan.FromHours(12)) return; oldest.Delete(); diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index a14b50248..2eb3ce47f 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -17,7 +17,7 @@ namespace Artemis.UI.Windows; public class ApplicationStateManager { private const int SM_SHUTTINGDOWN = 0x2000; - + public ApplicationStateManager(IContainer container, string[] startupArguments) { StartupArguments = startupArguments; @@ -25,6 +25,7 @@ public class ApplicationStateManager Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + Core.Utilities.UpdateRequested += UtilitiesOnUpdateRequested; // On Windows shutdown dispose the IOC container just so device providers get a chance to clean up if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) @@ -91,6 +92,33 @@ public class ApplicationStateManager Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); } + private void UtilitiesOnUpdateRequested(object? sender, UpdateEventArgs e) + { + List argsList = new(StartupArguments); + if (e.Silent) + argsList.Add("--autorun"); + + // Retain startup arguments after update by providing them to the script + string script = $"\"{Path.Combine(Constants.DataFolder, "updating", "pending", "scripts", "update.ps1")}\""; + string source = $"-sourceDirectory \"{Path.Combine(Constants.DataFolder, "updating", "pending")}\""; + string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; + string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; + + // Run the PowerShell script included in the new version, that way any changes made to the script are used + ProcessStartInfo info = new() + { + Arguments = $"-File {script} {source} {destination} {args}", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + + // Lets try a graceful shutdown, PowerShell will kill if needed + if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) + Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); + } + private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) { // Use PowerShell to kill the process after 8 sec just in case @@ -115,7 +143,7 @@ public class ApplicationStateManager }; Process.Start(info); } - + [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 74ef13f12..96d6d57fb 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -12,20 +12,10 @@ - - - - - - - - - - - - - - + + + PreserveNewest + application.ico diff --git a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs index 000931d92..4b3bf7fda 100644 --- a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs @@ -20,7 +20,6 @@ public static class UIContainerExtensions { container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); - container.Register(Reuse.Singleton); container.Register(); container.Register(serviceKey: WindowsInputProvider.Id); } diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml deleted file mode 100644 index 481709577..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - A new Artemis update is available! 🥳 - - - - Retrieving changes... - - - - - - - - - - - - - - - - - - - Changelog (auto-generated) - - - - - - - - - - - - - - - - - - We couldn't retrieve any changes - View online - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs deleted file mode 100644 index 9c33b35b8..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Artemis.UI.Shared; -using Avalonia; -using Avalonia.Markup.Xaml; - -namespace Artemis.UI.Windows.Screens.Update; - -public class UpdateDialogView : ReactiveCoreWindow -{ - public UpdateDialogView() - { - InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs deleted file mode 100644 index a19aecc6a..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Disposables; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Artemis.UI.Windows.Models; -using Artemis.UI.Windows.Providers; -using Avalonia.Threading; -using DynamicData; -using ReactiveUI; - -namespace Artemis.UI.Windows.Screens.Update; - -public class UpdateDialogViewModel : DialogViewModelBase -{ - // Based on https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#skipping-ci-for-individual-commits - private readonly string[] _excludedCommitMessages = - { - "[skip ci]", - "[ci skip]", - "skip-checks: true", - "skip-checks:true", - "[skip azurepipelines]", - "[azurepipelines skip]", - "[skip azpipelines]", - "[azpipelines skip]", - "[skip azp]", - "[azp skip]", - "***NO_CI***" - }; - - private readonly INotificationService _notificationService; - private readonly UpdateProvider _updateProvider; - private bool _hasChanges; - private string? _latestBuild; - - private bool _retrievingChanges; - - public UpdateDialogViewModel(string channel, IUpdateProvider updateProvider, INotificationService notificationService) - { - _updateProvider = (UpdateProvider) updateProvider; - _notificationService = notificationService; - - Channel = channel; - CurrentBuild = Constants.BuildInfo.BuildNumberDisplay; - - this.WhenActivated((CompositeDisposable _) => Dispatcher.UIThread.InvokeAsync(GetBuildChanges)); - Install = ReactiveCommand.Create(() => Close(true)); - AskLater = ReactiveCommand.Create(() => Close(false)); - } - - public ReactiveCommand Install { get; } - public ReactiveCommand AskLater { get; } - - public string Channel { get; } - public string CurrentBuild { get; } - - public ObservableCollection Changes { get; } = new(); - - public bool RetrievingChanges - { - get => _retrievingChanges; - set => RaiseAndSetIfChanged(ref _retrievingChanges, value); - } - - public bool HasChanges - { - get => _hasChanges; - set => RaiseAndSetIfChanged(ref _hasChanges, value); - } - - public string? LatestBuild - { - get => _latestBuild; - set => RaiseAndSetIfChanged(ref _latestBuild, value); - } - - private async Task GetBuildChanges() - { - try - { - RetrievingChanges = true; - Task currentTask = _updateProvider.GetBuildInfo(1, CurrentBuild); - Task latestTask = _updateProvider.GetBuildInfo(1); - - DevOpsBuild? current = await currentTask; - DevOpsBuild? latest = await latestTask; - - LatestBuild = latest?.BuildNumber; - if (current != null && latest != null) - { - GitHubDifference difference = await _updateProvider.GetBuildDifferences(current, latest); - - // Only take commits with one parents (no merges) - Changes.Clear(); - Changes.AddRange(difference.Commits.Where(c => c.Parents.Count == 1) - .SelectMany(c => c.Commit.Message.Split("\n")) - .Select(m => m.Trim()) - .Where(m => !string.IsNullOrWhiteSpace(m) && !_excludedCommitMessages.Contains(m)) - .OrderBy(m => m) - ); - HasChanges = Changes.Any(); - } - } - catch (Exception e) - { - _notificationService.CreateNotification().WithTitle("Failed to retrieve build changes").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show(); - } - finally - { - RetrievingChanges = false; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 new file mode 100644 index 000000000..4247178dc --- /dev/null +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -0,0 +1,42 @@ +param ( + [Parameter(Mandatory=$true)][string]$sourceDirectory, + [Parameter(Mandatory=$true)][string]$destinationDirectory, + [Parameter(Mandatory=$false)][string]$artemisArgs +) + +# Wait up to 10 seconds for the process to shut down +for ($i=1; $i -le 10; $i++) { + $process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue + if (!$process) { + break + } + Write-Host "Waiting for Artemis to shut down ($i / 10)" + Start-Sleep -Seconds 1 +} + +# If the process is still running, kill it +$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue +if ($process) { + Stop-Process -Id $process.Id -Force + Start-Sleep -Seconds 1 +} + +# Check if the destination directory exists +if (!(Test-Path $destinationDirectory)) { + Write-Error "The destination directory does not exist" +} + +# If the destination directory exists, clear it +Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force + +# Move the contents of the source directory to the destination directory +Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory + +Start-Sleep -Seconds 1 + +# When finished, run the updated version +if ($artemisArgs) { + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs +} else { + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory +} \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 101b1ac8a..086380658 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index 8407e5714..183116c26 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -27,7 +27,8 @@ Width="200" VerticalAlignment="Center" Items="{CompiledBinding Descriptors}" - SelectedItem="{CompiledBinding SelectedDescriptor}"> + SelectedItem="{CompiledBinding SelectedDescriptor}" + PlaceholderText="Please select a brush"> diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index 26c548dca..8f9ae6277 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -59,7 +59,7 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel protected override void ApplyInputValue() { - if (LayerProperty.ProfileElement is not Layer layer || layer.LayerBrush == null || SelectedDescriptor == null) + if (LayerProperty.ProfileElement is not Layer layer || SelectedDescriptor == null) return; _profileEditorService.ExecuteCommand(new ChangeLayerBrush(layer, SelectedDescriptor)); diff --git a/src/Artemis.UI/DryIoc/ContainerExtensions.cs b/src/Artemis.UI/DryIoc/ContainerExtensions.cs index f4c484bec..58970b2fd 100644 --- a/src/Artemis.UI/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI/DryIoc/ContainerExtensions.cs @@ -4,6 +4,7 @@ using Artemis.UI.DryIoc.InstanceProviders; using Artemis.UI.Screens; using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.ProfileEditor; @@ -36,6 +37,7 @@ public static class UIContainerExtensions container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); + container.Register(); container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), Reuse.Singleton); } diff --git a/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs b/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs new file mode 100644 index 000000000..743035d3e --- /dev/null +++ b/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs @@ -0,0 +1,14 @@ +using System.Reactive.Disposables; +using System.Threading; + +namespace Artemis.UI.Extensions; + +public static class CompositeDisposableExtensions +{ + public static CancellationToken AsCancellationToken(this CompositeDisposable disposable) + { + CancellationTokenSource tokenSource = new(); + Disposable.Create(tokenSource, s => s.Cancel()).DisposeWith(disposable); + return tokenSource.Token; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs new file mode 100644 index 000000000..0d1fc507d --- /dev/null +++ b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Threading; + +namespace Artemis.UI.Extensions; + +// Taken from System.IO.Compression with progress reporting slapped on top +public static class ZipArchiveExtensions +{ + /// + /// Extracts all the files in the zip archive to a directory on the file system. + /// + /// The zip archive to extract files from. + /// The path to the directory to place the extracted files in. You can specify either a relative or an absolute path. A relative path is interpreted as relative to the current working directory. + /// A boolean indicating whether to override existing files + /// The progress to report to. + /// A cancellation token + public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress progress, CancellationToken cancellationToken) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (destinationDirectoryName == null) + throw new ArgumentNullException(nameof(destinationDirectoryName)); + + for (int index = 0; index < source.Entries.Count; index++) + { + ZipArchiveEntry entry = source.Entries[index]; + entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles); + progress.Report((index + 1f) / source.Entries.Count * 100f); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName, bool overwrite) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (destinationDirectoryName == null) + throw new ArgumentNullException(nameof(destinationDirectoryName)); + + // Note that this will give us a good DirectoryInfo even if destinationDirectoryName exists: + DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName); + string destinationDirectoryFullPath = di.FullName; + if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar)) + destinationDirectoryFullPath += Path.DirectorySeparatorChar; + + string fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, source.FullName)); + + if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, StringComparison)) + throw new IOException($"The file '{fileDestinationPath}' already exists."); + + if (Path.GetFileName(fileDestinationPath).Length == 0) + { + // If it is a directory: + + if (source.Length != 0) + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + + Directory.CreateDirectory(fileDestinationPath); + } + else + { + // If it is a file: + // Create containing directory: + Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + source.ExtractToFile(fileDestinationPath, overwrite: overwrite); + } + } + private static StringComparison StringComparison => IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + private static bool IsCaseSensitive => !(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()); +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index b2ba9e8cd..fbd81e943 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -7,6 +7,7 @@ using Artemis.UI.DryIoc.Factories; using Artemis.UI.Models; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index d8cd0ff65..953f0ac1c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -137,7 +137,7 @@ - + Updating diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 06d212fa1..9d3aa0d1b 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -12,6 +12,7 @@ using Artemis.Core.Providers; using Artemis.Core.Services; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; @@ -30,6 +31,7 @@ public class GeneralTabViewModel : ActivatableViewModelBase private readonly PluginSetting _defaultLayerBrushDescriptor; private readonly ISettingsService _settingsService; private readonly IUpdateService _updateService; + private readonly INotificationService _notificationService; private readonly IWindowService _windowService; private bool _startupWizardOpen; @@ -38,13 +40,15 @@ public class GeneralTabViewModel : ActivatableViewModelBase IPluginManagementService pluginManagementService, IDebugService debugService, IWindowService windowService, - IUpdateService updateService) + IUpdateService updateService, + INotificationService notificationService) { DisplayName = "General"; _settingsService = settingsService; _debugService = debugService; _windowService = windowService; _updateService = updateService; + _notificationService = notificationService; _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); List layerBrushProviders = pluginManagementService.GetFeaturesOfType(); @@ -88,7 +92,6 @@ public class GeneralTabViewModel : ActivatableViewModelBase public ReactiveCommand ShowDataFolder { get; } public bool IsAutoRunSupported => _autoRunProvider != null; - public bool IsUpdatingSupported => _updateService.UpdatingSupported; public ObservableCollection LayerBrushDescriptors { get; } public ObservableCollection GraphicsContexts { get; } @@ -142,8 +145,8 @@ public class GeneralTabViewModel : ActivatableViewModelBase public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); - public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); public PluginSetting ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information); public PluginSetting CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); @@ -159,7 +162,14 @@ public class GeneralTabViewModel : ActivatableViewModelBase private async Task ExecuteCheckForUpdate(CancellationToken cancellationToken) { - await _updateService.ManualUpdate(); + // If an update was available a popup was shown, no need to continue + if (await _updateService.CheckForUpdate()) + return; + + _notificationService.CreateNotification() + .WithTitle("No update available") + .WithMessage("You are running the latest version in your current channel") + .Show(); } private async Task ExecuteShowSetupWizard() diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml new file mode 100644 index 000000000..acf70d5b4 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml @@ -0,0 +1,216 @@ + + + + + A new Artemis update is available! 🥳 + + + + Retrieving release... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs new file mode 100644 index 000000000..ed0373da4 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs @@ -0,0 +1,29 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Settings.Updating; + +public partial class ReleaseAvailableView : ReactiveCoreWindow +{ + public ReleaseAvailableView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs new file mode 100644 index 000000000..118177c7f --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs @@ -0,0 +1,76 @@ +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Updating; +using ReactiveUI; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class ReleaseAvailableViewModel : ActivatableViewModelBase +{ + private readonly string _nextReleaseId; + private readonly ILogger _logger; + private readonly IUpdateService _updateService; + private readonly IUpdatingClient _updatingClient; + private readonly INotificationService _notificationService; + private IGetReleaseById_Release? _release; + + public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService) + { + _nextReleaseId = nextReleaseId; + _logger = logger; + _updateService = updateService; + _updatingClient = updatingClient; + _notificationService = notificationService; + + CurrentVersion = _updateService.CurrentVersion ?? "Development build"; + Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null)); + + this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken())); + } + + private void ExecuteInstall() + { + _updateService.InstallRelease(_nextReleaseId); + } + + private async Task RetrieveRelease(CancellationToken cancellationToken) + { + IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken); + // Borrow GraphQLClientException for messaging, how lazy of me.. + if (result.Errors.Count > 0) + { + GraphQLClientException exception = new(result.Errors); + _logger.Error(exception, "Failed to retrieve release details"); + _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show(); + return; + } + + if (result.Data?.Release == null) + { + _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show(); + return; + } + + Release = result.Data.Release; + } + + public string CurrentVersion { get; } + + public IGetReleaseById_Release? Release + { + get => _release; + set => RaiseAndSetIfChanged(ref _release, value); + } + + public ReactiveCommand Install { get; } + public ReactiveCommand AskLater { get; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml new file mode 100644 index 000000000..46699f5ae --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml @@ -0,0 +1,40 @@ + + + + Downloading & installing update... + + + + This should not take long, when finished Artemis must restart. + + + + + + + + Done, click restart to apply the update 🫡 + + + Restart when finished + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs new file mode 100644 index 000000000..ec2a689e6 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs @@ -0,0 +1,28 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Settings.Updating; + +public partial class ReleaseInstallerView : ReactiveCoreWindow +{ + public ReleaseInstallerView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Cancel_OnClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs new file mode 100644 index 000000000..79a80aa06 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using ReactiveUI; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class ReleaseInstallerViewModel : ActivatableViewModelBase +{ + private readonly ReleaseInstaller _releaseInstaller; + private readonly IWindowService _windowService; + private ObservableAsPropertyHelper? _overallProgress; + private ObservableAsPropertyHelper? _stepProgress; + private bool _ready; + private bool _restartWhenFinished; + + public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService) + { + _releaseInstaller = releaseInstaller; + _windowService = windowService; + + Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false)); + this.WhenActivated(d => + { + _overallProgress = Observable.FromEventPattern(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x) + .Select(e => e.EventArgs) + .ToProperty(this, vm => vm.OverallProgress) + .DisposeWith(d); + _stepProgress = Observable.FromEventPattern(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x) + .Select(e => e.EventArgs) + .ToProperty(this, vm => vm.StepProgress) + .DisposeWith(d); + + Task.Run(() => InstallUpdate(d.AsCancellationToken())); + }); + } + + public ReactiveCommand Restart { get; } + + public float OverallProgress => _overallProgress?.Value ?? 0; + public float StepProgress => _stepProgress?.Value ?? 0; + + public bool Ready + { + get => _ready; + set => RaiseAndSetIfChanged(ref _ready, value); + } + + public bool RestartWhenFinished + { + get => _restartWhenFinished; + set => RaiseAndSetIfChanged(ref _restartWhenFinished, value); + } + + private async Task InstallUpdate(CancellationToken cancellationToken) + { + try + { + await _releaseInstaller.InstallAsync(cancellationToken); + Ready = true; + if (RestartWhenFinished) + Utilities.ApplyUpdate(false); + } + catch (TaskCanceledException) + { + // ignored + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Something went wrong while installing the update", e); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs deleted file mode 100644 index 4b1950846..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Artemis.Core; -using Artemis.UI.Shared; - -namespace Artemis.UI.Screens.Settings.Updating; - -public class UpdateInstallationViewModel : DialogViewModelBase -{ - private readonly string _nextReleaseId; - - public UpdateInstallationViewModel(string nextReleaseId) - { - _nextReleaseId = nextReleaseId; - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index e2a3e516a..dd72303e1 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -11,6 +11,7 @@ using Artemis.Core.Services; using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Plugins; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; @@ -81,13 +82,12 @@ public class StartupWizardViewModel : DialogViewModelBase public ObservableCollection DeviceProviders { get; } public bool IsAutoRunSupported => _autoRunProvider != null; - public bool IsUpdatingSupported => _updateService.UpdatingSupported; public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); - public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); public int CurrentStep { @@ -119,7 +119,7 @@ public class StartupWizardViewModel : DialogViewModelBase CurrentStep--; // Skip the settings step if none of it's contents are supported - if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported) + if (CurrentStep == 4 && !IsAutoRunSupported) CurrentStep--; SetupButtons(); @@ -131,7 +131,7 @@ public class StartupWizardViewModel : DialogViewModelBase CurrentStep++; // Skip the settings step if none of it's contents are supported - if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported) + if (CurrentStep == 4 && !IsAutoRunSupported) CurrentStep++; SetupButtons(); diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml index 294f59e91..bf68bf234 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml @@ -68,7 +68,7 @@ - + Updating diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs index 157d1ef07..5212e4eec 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -1,22 +1,11 @@ using System.Threading.Tasks; +using Artemis.UI.Services.Interfaces; -namespace Artemis.UI.Services.Interfaces; +namespace Artemis.UI.Services.Updating; public interface IUpdateService : IArtemisUIService { - /// - /// Gets a boolean indicating whether updating is supported. - /// - bool UpdatingSupported { get; } - - /// - /// Gets or sets a boolean indicating whether auto-updating is suspended. - /// - bool SuspendAutoCheck { get; set; } - - /// - /// Manually checks for updates and offers to install it if found. - /// - /// Whether an update was found, regardless of whether the user chose to install it. - Task ManualUpdate(); + Task CheckForUpdate(); + Task InstallRelease(string releaseId); + string? CurrentVersion { get; } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs index f868298b7..55a3e6a26 100644 --- a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -1,16 +1,13 @@ using System; -using System.Drawing.Drawing2D; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; -using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Extensions; using Artemis.WebClient.Updating; -using NoStringEvaluating.Functions.Math; using Octodiff.Core; using Octodiff.Diagnostics; using Serilog; @@ -19,19 +16,16 @@ using StrawberryShake; namespace Artemis.UI.Services.Updating; /// -/// Represents the installation process of a release +/// Represents the installation process of a release /// public class ReleaseInstaller { - private readonly string _releaseId; - private readonly ILogger _logger; - private readonly IUpdatingClient _updatingClient; - private readonly HttpClient _httpClient; - private readonly Platform _updatePlatform; private readonly string _dataFolder; - - public IProgress OverallProgress { get; } = new Progress(); - public IProgress StepProgress { get; } = new Progress(); + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly string _releaseId; + private readonly Platform _updatePlatform; + private readonly IUpdatingClient _updatingClient; public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) { @@ -43,7 +37,7 @@ public class ReleaseInstaller if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; - if (OperatingSystem.IsLinux()) + else if (OperatingSystem.IsLinux()) _updatePlatform = Platform.Linux; else if (OperatingSystem.IsMacOS()) _updatePlatform = Platform.Osx; @@ -54,9 +48,13 @@ public class ReleaseInstaller Directory.CreateDirectory(_dataFolder); } + + public Progress OverallProgress { get; } = new(); + public Progress StepProgress { get; } = new(); + public async Task InstallAsync(CancellationToken cancellationToken) { - OverallProgress.Report(0); + ((IProgress) OverallProgress).Report(0); _logger.Information("Retrieving details for release {ReleaseId}", _releaseId); IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken); @@ -70,7 +68,7 @@ public class ReleaseInstaller if (artifact == null) throw new Exception("Found the release but it has no artifact for the current platform"); - OverallProgress.Report(0.1f); + ((IProgress) OverallProgress).Report(10); // Determine whether the last update matches our local version, then we can download the delta if (release.PreviousRelease != null && File.Exists(Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip")) && artifact.DeltaFileInfo.DownloadSize != 0) @@ -84,22 +82,26 @@ public class ReleaseInstaller await using MemoryStream stream = new(); await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken); - OverallProgress.Report(0.33f); + ((IProgress) OverallProgress).Report(33); await PatchDelta(stream, previousRelease, cancellationToken); } - private async Task PatchDelta(MemoryStream deltaStream, string previousRelease, CancellationToken cancellationToken) + private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken) { await using FileStream baseStream = File.OpenRead(previousRelease); await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); deltaStream.Seek(0, SeekOrigin.Begin); - DeltaApplier deltaApplier = new(); - deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream); + + await Task.Run(() => + { + DeltaApplier deltaApplier = new(); + deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream); + }); cancellationToken.ThrowIfCancellationRequested(); - - OverallProgress.Report(0.66f); + + ((IProgress) OverallProgress).Report(66); await Extract(newFileStream, cancellationToken); } @@ -108,21 +110,28 @@ public class ReleaseInstaller await using MemoryStream stream = new(); await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken); - OverallProgress.Report(0.5f); + ((IProgress) OverallProgress).Report(50); + await Extract(stream, cancellationToken); } - private async Task Extract(FileStream archiveStream, CancellationToken cancellationToken) + private async Task Extract(Stream archiveStream, CancellationToken cancellationToken) { // Ensure the directory is empty string extractDirectory = Path.Combine(_dataFolder, "pending"); if (Directory.Exists(extractDirectory)) Directory.Delete(extractDirectory, true); Directory.CreateDirectory(extractDirectory); + + + + await Task.Run(() => + { + archiveStream.Seek(0, SeekOrigin.Begin); + using ZipArchive archive = new(archiveStream); + archive.ExtractToDirectory(extractDirectory, false, StepProgress, cancellationToken); + }); - archiveStream.Seek(0, SeekOrigin.Begin); - using ZipArchive archive = new(archiveStream); - archive.ExtractToDirectory(extractDirectory); - OverallProgress.Report(1); + ((IProgress) OverallProgress).Report(100); } } @@ -137,6 +146,6 @@ internal class DeltaApplierProgressReporter : IProgressReporter public void ReportProgress(string operation, long currentPosition, long total) { - _stepProgress.Report((float) currentPosition / total); + _stepProgress.Report(currentPosition / total * 100); } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs index 94bcaf3be..86f8f7a91 100644 --- a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs @@ -1,6 +1,12 @@ +using System.Threading.Tasks; + namespace Artemis.UI.Services.Updating; public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider { - + /// + public async Task ShowNotification(string releaseId) + { + throw new System.NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 1077d549c..83f65739e 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -1,21 +1,15 @@ using System; -using System.Linq; -using System.Reactive.Disposables; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Exceptions; using Artemis.UI.Screens.Settings.Updating; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Updating; -using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; using Avalonia.Threading; -using DryIoc; using Serilog; using StrawberryShake; using Timer = System.Timers.Timer; @@ -25,17 +19,17 @@ namespace Artemis.UI.Services; public class UpdateService : IUpdateService { private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour + private readonly PluginSetting _autoCheck; + private readonly PluginSetting _autoInstall; + private readonly PluginSetting _channel; + private readonly Func _getReleaseInstaller; private readonly ILogger _logger; private readonly IMainWindowService _mainWindowService; - private readonly IWindowService _windowService; - private readonly IUpdatingClient _updatingClient; private readonly Lazy _updateNotificationProvider; - private readonly Func _getReleaseInstaller; private readonly Platform _updatePlatform; - private readonly PluginSetting _channel; - private readonly PluginSetting _autoCheck; - private readonly PluginSetting _autoInstall; + private readonly IUpdatingClient _updatingClient; + private readonly IWindowService _windowService; private bool _suspendAutoCheck; @@ -56,7 +50,7 @@ public class UpdateService : IUpdateService if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; - if (OperatingSystem.IsLinux()) + else if (OperatingSystem.IsLinux()) _updatePlatform = Platform.Linux; else if (OperatingSystem.IsMacOS()) _updatePlatform = Platform.Osx; @@ -72,14 +66,61 @@ public class UpdateService : IUpdateService timer.Elapsed += HandleAutoUpdateEvent; timer.Start(); } + + public string? CurrentVersion + { + get + { + object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion; + } + } + + private async Task ShowUpdateDialog(string nextReleaseId) + { + await Dispatcher.UIThread.InvokeAsync(async () => + { + // Main window is probably already open but this will bring it into focus + _mainWindowService.OpenMainWindow(); + await _windowService.ShowDialogAsync(nextReleaseId); + }); + } + + private async Task ShowUpdateNotification(string nextReleaseId) + { + await _updateNotificationProvider.Value.ShowNotification(nextReleaseId); + } + + private async Task AutoInstallUpdate(string nextReleaseId) + { + ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId); + await installer.InstallAsync(CancellationToken.None); + Utilities.ApplyUpdate(true); + } + + private async void HandleAutoUpdateEvent(object? sender, EventArgs e) + { + if (!_autoCheck.Value || _suspendAutoCheck) + return; + + try + { + await CheckForUpdate(); + } + catch (Exception ex) + { + _logger.Warning(ex, "Auto update failed"); + } + } public async Task CheckForUpdate() { - string? currentVersion = AssemblyProductVersion; + string? currentVersion = CurrentVersion; if (currentVersion == null) return false; - IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform); + // IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform); + IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, "feature/gh-actions", _updatePlatform); result.EnsureNoErrors(); // No update was found @@ -99,49 +140,16 @@ public class UpdateService : IUpdateService return true; } - - private async Task ShowUpdateDialog(string nextReleaseId) + + /// + public async Task InstallRelease(string releaseId) { - await Dispatcher.UIThread.InvokeAsync(async () => + ReleaseInstaller installer = _getReleaseInstaller(releaseId); + await Dispatcher.UIThread.InvokeAsync(() => { // Main window is probably already open but this will bring it into focus _mainWindowService.OpenMainWindow(); - await _windowService.ShowDialogAsync(nextReleaseId); + _windowService.ShowWindow(installer); }); } - - private async Task ShowUpdateNotification(string nextReleaseId) - { - await _updateNotificationProvider.Value.ShowNotification(nextReleaseId); - } - - private async Task AutoInstallUpdate(string nextReleaseId) - { - ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId); - await installer.InstallAsync(CancellationToken.None); - } - - private async void HandleAutoUpdateEvent(object? sender, EventArgs e) - { - if (!_autoCheck.Value || _suspendAutoCheck) - return; - - try - { - await CheckForUpdate(); - } - catch (Exception ex) - { - _logger.Warning(ex, "Auto update failed"); - } - } - - private static string? AssemblyProductVersion - { - get - { - object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); - return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion; - } - } } \ No newline at end of file diff --git a/src/Artemis.UI/Styles/Artemis.axaml b/src/Artemis.UI/Styles/Artemis.axaml index b10f8bdb6..d92851a90 100644 --- a/src/Artemis.UI/Styles/Artemis.axaml +++ b/src/Artemis.UI/Styles/Artemis.axaml @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/src/Artemis.UI/Styles/Markdown.axaml b/src/Artemis.UI/Styles/Markdown.axaml new file mode 100644 index 000000000..34acbe75f --- /dev/null +++ b/src/Artemis.UI/Styles/Markdown.axaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ## Core + * Cleaned up ProfileService render condition + * Core - Added fading in and out of profiles + * Core - Apply opacity layer only when fading + * Core - Fixed when condition stops being true mid-fade + * Core - Removed FadingStatus enum + + # General + - Meta - Fixed warnings + - Meta - Update RGB.NET + + # Plugins + - Plugins - Ignore version when loading shared assemblies + + # UI + - Sidebar - Improved category reordering code + + + + + + \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql index cd7e8b2a8..56af7ecb5 100644 --- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -4,6 +4,7 @@ query GetReleaseById($id: String!) { commit version previousRelease + changelog artifacts { platform artifactId From acd005e4a2ecc3bf4a524748401cfba9437c5884 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 24 Feb 2023 22:54:17 +0100 Subject: [PATCH 09/34] Implemented most of the updating mechanism --- .../Interfaces/IQueuedActionRepository.cs | 2 + .../Repositories/QueuedActionRepository.cs | 12 ++ .../Converters/BytesToStringConverter.cs | 34 +++ src/Artemis.UI.Shared/Styles/Artemis.axaml | 1 + src/Artemis.UI.Shared/Styles/Skeleton.axaml | 174 +++++++++++++++ .../ApplicationStateManager.cs | 2 - src/Artemis.UI.Windows/Scripts/update.ps1 | 9 + src/Artemis.UI/Artemis.UI.csproj | 11 + src/Artemis.UI/DryIoc/ContainerExtensions.cs | 3 +- src/Artemis.UI/DryIoc/Factories/IVMFactory.cs | 23 +- .../Screens/Settings/SettingsViewModel.cs | 2 + .../Settings/Tabs/ReleasesTabView.axaml | 26 +++ .../Settings/Tabs/ReleasesTabView.axaml.cs | 17 ++ .../Settings/Tabs/ReleasesTabViewModel.cs | 106 +++++++++ .../Updating/ReleaseAvailableView.axaml.cs | 29 --- .../Updating/ReleaseAvailableViewModel.cs | 76 ------- .../Updating/ReleaseInstallerView.axaml | 40 ---- .../Updating/ReleaseInstallerView.axaml.cs | 28 --- .../Updating/ReleaseInstallerViewModel.cs | 81 ------- ...eAvailableView.axaml => ReleaseView.axaml} | 204 ++++++++++++++---- .../Settings/Updating/ReleaseView.axaml.cs | 23 ++ .../Settings/Updating/ReleaseViewModel.cs | 202 +++++++++++++++++ .../Services/Updating/IUpdateService.cs | 9 +- .../Services/Updating/ReleaseInstaller.cs | 124 +++++++---- .../Services/Updating/UpdateService.cs | 87 +++++--- .../Queries/GetReleaseById.graphql | 33 ++- src/Artemis.WebClient.Updating/schema.graphql | 80 ++++++- 27 files changed, 1055 insertions(+), 383 deletions(-) create mode 100644 src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs create mode 100644 src/Artemis.UI.Shared/Styles/Skeleton.axaml create mode 100644 src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml create mode 100644 src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml delete mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs delete mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs rename src/Artemis.UI/Screens/Settings/Updating/{ReleaseAvailableView.axaml => ReleaseView.axaml} (50%) create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs diff --git a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs index dfcbfbfe7..cb5852eaa 100644 --- a/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs +++ b/src/Artemis.Storage/Repositories/Interfaces/IQueuedActionRepository.cs @@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository void Remove(QueuedActionEntity queuedActionEntity); List GetAll(); List GetByType(string type); + bool IsTypeQueued(string type); + void ClearByType(string type); } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs index faa6a304f..f5c83cd0e 100644 --- a/src/Artemis.Storage/Repositories/QueuedActionRepository.cs +++ b/src/Artemis.Storage/Repositories/QueuedActionRepository.cs @@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository return _repository.Query().Where(q => q.Type == type).ToList(); } + /// + public bool IsTypeQueued(string type) + { + return _repository.Query().Where(q => q.Type == type).Count() > 0; + } + + /// + public void ClearByType(string type) + { + _repository.DeleteMany(q => q.Type == type); + } + #endregion } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs b/src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs new file mode 100644 index 000000000..f6f7da7e6 --- /dev/null +++ b/src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; +using Humanizer; +using Humanizer.Bytes; + +namespace Artemis.UI.Shared.Converters; + +/// +/// Converts bytes to a string +/// +public class BytesToStringConverter : IValueConverter +{ + /// + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int intBytes) + return intBytes.Bytes().Humanize(); + if (value is long longBytes) + return longBytes.Bytes().Humanize(); + if (value is double doubleBytes) + return doubleBytes.Bytes().Humanize(); + + return value; + } + + /// + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is string formatted && ByteSize.TryParse(formatted, out ByteSize result)) + return result.Bytes; + return value; + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Artemis.UI.Shared/Styles/Artemis.axaml index 97dafc8a1..7b838cdbf 100644 --- a/src/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Artemis.UI.Shared/Styles/Artemis.axaml @@ -21,6 +21,7 @@ + diff --git a/src/Artemis.UI.Shared/Styles/Skeleton.axaml b/src/Artemis.UI.Shared/Styles/Skeleton.axaml new file mode 100644 index 000000000..2596c3fe6 --- /dev/null +++ b/src/Artemis.UI.Shared/Styles/Skeleton.axaml @@ -0,0 +1,174 @@ + + + + + + + + + + This is heading 1 + This is heading 2 + This is heading 3 + This is heading 4 + This is heading 5 + This is heading 6 + This is regular text + This is regular text + This is regular text + + + + + + + + + + + + + + + + + + + + + This is heading 1 + This is heading 2 + This is heading 3 + This is heading 4 + This is heading 5 + This is heading 6 + This is regular text + + + + + + + + + + + + + + + + + + + + + This is heading 1 + This is heading 2 + This is heading 3 + This is heading 4 + This is heading 5 + This is heading 6 + This is regular text + + + + + + + 8 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 2eb3ce47f..1a7d8147e 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -108,8 +108,6 @@ public class ApplicationStateManager ProcessStartInfo info = new() { Arguments = $"-File {script} {source} {destination} {args}", - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, FileName = "PowerShell.exe" }; Process.Start(info); diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 index 4247178dc..7603b013e 100644 --- a/src/Artemis.UI.Windows/Scripts/update.ps1 +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -4,6 +4,10 @@ param ( [Parameter(Mandatory=$false)][string]$artemisArgs ) +Write-Host "Artemis update script v1" +Write-Host "Please do not close this window, this should not take long" +Write-Host "" + # Wait up to 10 seconds for the process to shut down for ($i=1; $i -le 10; $i++) { $process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue @@ -26,12 +30,17 @@ if (!(Test-Path $destinationDirectory)) { Write-Error "The destination directory does not exist" } + # If the destination directory exists, clear it +Write-Host "Cleaning up old version where needed" Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force # Move the contents of the source directory to the destination directory +Write-Host "Installing new files" Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory + +Write-Host "Finished! Restarting Artemis" Start-Sleep -Seconds 1 # When finished, run the updated version diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 086380658..502846ca4 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -43,4 +43,15 @@ + + + + UpdatingTabView.axaml + Code + + + UpdatingTabView.axaml + Code + + \ No newline at end of file diff --git a/src/Artemis.UI/DryIoc/ContainerExtensions.cs b/src/Artemis.UI/DryIoc/ContainerExtensions.cs index 58970b2fd..da1dcd815 100644 --- a/src/Artemis.UI/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI/DryIoc/ContainerExtensions.cs @@ -31,14 +31,13 @@ public static class UIContainerExtensions container.Register(Reuse.Singleton); container.RegisterMany(thisAssembly, type => type.IsAssignableTo()); - container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), ifAlreadyRegistered: IfAlreadyRegistered.Replace); container.RegisterMany(thisAssembly, type => type.IsAssignableTo() && type.IsInterface); container.RegisterMany(thisAssembly, type => type.IsAssignableTo() && type != typeof(PropertyVmFactory)); container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); container.Register(); - + container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), Reuse.Singleton); } } \ No newline at end of file diff --git a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs index ed71b2c6e..8b3c5fa42 100644 --- a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs +++ b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using System.Reactive; using Artemis.Core; using Artemis.Core.LayerBrushes; @@ -17,6 +18,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; using Artemis.UI.Screens.Scripting; using Artemis.UI.Screens.Settings; +using Artemis.UI.Screens.Settings.Updating; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.VisualScripting; @@ -474,4 +476,23 @@ public class ScriptVmFactory : IScriptVmFactory { return _container.Resolve(new object[] { profile, scriptConfiguration }); } +} + +public interface IReleaseVmFactory : IVmFactory +{ + ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt); +} +public class ReleaseVmFactory : IReleaseVmFactory +{ + private readonly IContainer _container; + + public ReleaseVmFactory(IContainer container) + { + _container = container; + } + + public ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt) + { + return _container.Resolve(new object[] { releaseId, version, createdAt }); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs index 0a372c29b..773656c1f 100644 --- a/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/SettingsViewModel.cs @@ -10,6 +10,7 @@ public class SettingsViewModel : MainScreenViewModel GeneralTabViewModel generalTabViewModel, PluginsTabViewModel pluginsTabViewModel, DevicesTabViewModel devicesTabViewModel, + ReleasesTabViewModel releasesTabViewModel, AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings") { SettingTabs = new ObservableCollection @@ -17,6 +18,7 @@ public class SettingsViewModel : MainScreenViewModel generalTabViewModel, pluginsTabViewModel, devicesTabViewModel, + releasesTabViewModel, aboutTabViewModel }; } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml new file mode 100644 index 000000000..6528d8b04 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs new file mode 100644 index 000000000..3421db5a7 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Settings; + +public class ReleasesTabView : ReactiveUserControl +{ + public ReleasesTabView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs new file mode 100644 index 000000000..61da057c8 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.DryIoc.Factories; +using Artemis.UI.Extensions; +using Artemis.UI.Screens.Settings.Updating; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.WebClient.Updating; +using Avalonia.Threading; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Screens.Settings; + +public class ReleasesTabViewModel : ActivatableViewModelBase +{ + private readonly ILogger _logger; + private readonly IUpdatingClient _updatingClient; + private readonly INotificationService _notificationService; + private readonly SourceList _releases; + private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo; + private bool _loading; + private ReleaseViewModel? _selectedReleaseViewModel; + + public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService) + { + _logger = logger; + _updatingClient = updatingClient; + _notificationService = notificationService; + + _releases = new SourceList(); + _releases.Connect() + .Sort(SortExpressionComparer.Descending(p => p.CreatedAt)) + .Transform(r => releaseVmFactory.ReleaseListViewModel(r.Id, r.Version, r.CreatedAt)) + .ObserveOn(AvaloniaScheduler.Instance) + .Bind(out ReadOnlyObservableCollection releaseViewModels) + .Subscribe(); + + DisplayName = "Releases"; + ReleaseViewModels = releaseViewModels; + this.WhenActivated(async d => + { + await updateService.CacheLatestRelease(); + await GetMoreReleases(d.AsCancellationToken()); + SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(); + }); + } + + public ReadOnlyObservableCollection ReleaseViewModels { get; } + + public ReleaseViewModel? SelectedReleaseViewModel + { + get => _selectedReleaseViewModel; + set => RaiseAndSetIfChanged(ref _selectedReleaseViewModel, value); + } + + public bool Loading + { + get => _loading; + private set => RaiseAndSetIfChanged(ref _loading, value); + } + + public async Task GetMoreReleases(CancellationToken cancellationToken) + { + if (_lastPageInfo != null && !_lastPageInfo.HasNextPage) + return; + + try + { + Loading = true; + + IOperationResult result = await _updatingClient.GetReleases.ExecuteAsync("feature/gh-actions", Platform.Windows, 20, _lastPageInfo?.EndCursor, cancellationToken); + if (result.Data?.PublishedReleases?.Nodes == null) + return; + + _lastPageInfo = result.Data.PublishedReleases.PageInfo; + _releases.AddRange(result.Data.PublishedReleases.Nodes); + } + catch (TaskCanceledException) + { + // ignored + } + catch (Exception e) + { + _logger.Warning(e, "Failed to retrieve releases"); + _notificationService.CreateNotification() + .WithTitle("Failed to retrieve releases") + .WithMessage(e.Message) + .WithSeverity(NotificationSeverity.Warning) + .Show(); + } + finally + { + Loading = false; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs deleted file mode 100644 index ed0373da4..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Artemis.UI.Shared; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; -using Avalonia.ReactiveUI; - -namespace Artemis.UI.Screens.Settings.Updating; - -public partial class ReleaseAvailableView : ReactiveCoreWindow -{ - public ReleaseAvailableView() - { - InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private void Button_OnClick(object? sender, RoutedEventArgs e) - { - Close(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs deleted file mode 100644 index 118177c7f..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Reactive; -using System.Reactive.Linq; -using System.Threading; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Extensions; -using Artemis.UI.Services.Updating; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; -using Artemis.WebClient.Updating; -using ReactiveUI; -using Serilog; -using StrawberryShake; - -namespace Artemis.UI.Screens.Settings.Updating; - -public class ReleaseAvailableViewModel : ActivatableViewModelBase -{ - private readonly string _nextReleaseId; - private readonly ILogger _logger; - private readonly IUpdateService _updateService; - private readonly IUpdatingClient _updatingClient; - private readonly INotificationService _notificationService; - private IGetReleaseById_Release? _release; - - public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService) - { - _nextReleaseId = nextReleaseId; - _logger = logger; - _updateService = updateService; - _updatingClient = updatingClient; - _notificationService = notificationService; - - CurrentVersion = _updateService.CurrentVersion ?? "Development build"; - Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null)); - - this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken())); - } - - private void ExecuteInstall() - { - _updateService.InstallRelease(_nextReleaseId); - } - - private async Task RetrieveRelease(CancellationToken cancellationToken) - { - IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken); - // Borrow GraphQLClientException for messaging, how lazy of me.. - if (result.Errors.Count > 0) - { - GraphQLClientException exception = new(result.Errors); - _logger.Error(exception, "Failed to retrieve release details"); - _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show(); - return; - } - - if (result.Data?.Release == null) - { - _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show(); - return; - } - - Release = result.Data.Release; - } - - public string CurrentVersion { get; } - - public IGetReleaseById_Release? Release - { - get => _release; - set => RaiseAndSetIfChanged(ref _release, value); - } - - public ReactiveCommand Install { get; } - public ReactiveCommand AskLater { get; } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml deleted file mode 100644 index 46699f5ae..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml +++ /dev/null @@ -1,40 +0,0 @@ - - - - Downloading & installing update... - - - - This should not take long, when finished Artemis must restart. - - - - - - - - Done, click restart to apply the update 🫡 - - - Restart when finished - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs deleted file mode 100644 index ec2a689e6..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Artemis.UI.Shared; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Markup.Xaml; - -namespace Artemis.UI.Screens.Settings.Updating; - -public partial class ReleaseInstallerView : ReactiveCoreWindow -{ - public ReleaseInstallerView() - { - InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - - private void Cancel_OnClick(object? sender, RoutedEventArgs e) - { - Close(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs deleted file mode 100644 index 79a80aa06..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Extensions; -using Artemis.UI.Services.Updating; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Services; -using ReactiveUI; - -namespace Artemis.UI.Screens.Settings.Updating; - -public class ReleaseInstallerViewModel : ActivatableViewModelBase -{ - private readonly ReleaseInstaller _releaseInstaller; - private readonly IWindowService _windowService; - private ObservableAsPropertyHelper? _overallProgress; - private ObservableAsPropertyHelper? _stepProgress; - private bool _ready; - private bool _restartWhenFinished; - - public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService) - { - _releaseInstaller = releaseInstaller; - _windowService = windowService; - - Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false)); - this.WhenActivated(d => - { - _overallProgress = Observable.FromEventPattern(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x) - .Select(e => e.EventArgs) - .ToProperty(this, vm => vm.OverallProgress) - .DisposeWith(d); - _stepProgress = Observable.FromEventPattern(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x) - .Select(e => e.EventArgs) - .ToProperty(this, vm => vm.StepProgress) - .DisposeWith(d); - - Task.Run(() => InstallUpdate(d.AsCancellationToken())); - }); - } - - public ReactiveCommand Restart { get; } - - public float OverallProgress => _overallProgress?.Value ?? 0; - public float StepProgress => _stepProgress?.Value ?? 0; - - public bool Ready - { - get => _ready; - set => RaiseAndSetIfChanged(ref _ready, value); - } - - public bool RestartWhenFinished - { - get => _restartWhenFinished; - set => RaiseAndSetIfChanged(ref _restartWhenFinished, value); - } - - private async Task InstallUpdate(CancellationToken cancellationToken) - { - try - { - await _releaseInstaller.InstallAsync(cancellationToken); - Ready = true; - if (RestartWhenFinished) - Utilities.ApplyUpdate(false); - } - catch (TaskCanceledException) - { - // ignored - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Something went wrong while installing the update", e); - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml similarity index 50% rename from src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml rename to src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml index acf70d5b4..91e1a0a4c 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml @@ -1,49 +1,163 @@ - - + + + + + + + + + + + + + + - - A new Artemis update is available! 🥳 - + + + + + Release info - - Retrieving release... - - + + + + + + + + - - - - - - - - - - + + + - - - + + + + Ready, restart to install + + + + + - + + + + + + Release date + + + + + + Source + + + + + + File size + + + + + + + + + + Release notes + + + + Loading releases... @@ -16,9 +24,10 @@ - + + Learn more about channels on the wiki @@ -31,20 +40,15 @@ - - + + - + + - + diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml index 91e1a0a4c..b1f10bdab 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings" xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating" xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia" xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia" @@ -55,7 +54,7 @@ - + diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs index 7d4d34f65..bf959adbb 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs @@ -25,11 +25,10 @@ public class ReleaseViewModel : ActivatableViewModelBase private readonly IUpdateService _updateService; private readonly Platform _updatePlatform; private readonly IUpdatingClient _updatingClient; - private readonly IWindowService _windowService; private CancellationTokenSource? _installerCts; - private string _changelog = string.Empty; - private string _commit = string.Empty; - private string _shortCommit = string.Empty; + private string? _changelog; + private string? _commit; + private string? _shortCommit; private long _fileSize; private bool _installationAvailable; private bool _installationFinished; @@ -43,14 +42,12 @@ public class ReleaseViewModel : ActivatableViewModelBase ILogger logger, IUpdatingClient updatingClient, INotificationService notificationService, - IUpdateService updateService, - IWindowService windowService) + IUpdateService updateService) { _logger = logger; _updatingClient = updatingClient; _notificationService = notificationService; _updateService = updateService; - _windowService = windowService; if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; @@ -97,19 +94,19 @@ public class ReleaseViewModel : ActivatableViewModelBase public DateTimeOffset CreatedAt { get; } public ReleaseInstaller ReleaseInstaller { get; } - public string Changelog + public string? Changelog { get => _changelog; set => RaiseAndSetIfChanged(ref _changelog, value); } - public string Commit + public string? Commit { get => _commit; set => RaiseAndSetIfChanged(ref _commit, value); } - public string ShortCommit + public string? ShortCommit { get => _shortCommit; set => RaiseAndSetIfChanged(ref _shortCommit, value); @@ -146,7 +143,9 @@ public class ReleaseViewModel : ActivatableViewModelBase } public bool IsCurrentVersion => Version == Constants.CurrentVersion; - + public bool IsPreviousVersion => Version == _updateService.PreviousVersion; + public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion; + public void NavigateToSource() { Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Commit}"); @@ -159,14 +158,20 @@ public class ReleaseViewModel : ActivatableViewModelBase { InstallationInProgress = true; await ReleaseInstaller.InstallAsync(_installerCts.Token); - _updateService.QueueUpdate(); + _updateService.QueueUpdate(Version); InstallationFinished = true; } catch (Exception e) { if (_installerCts.IsCancellationRequested) return; - _windowService.ShowExceptionDialog("Failed to install update", e); + + _logger.Warning(e, "Failed to install update through UI"); + _notificationService.CreateNotification() + .WithTitle("Failed to install update") + .WithMessage(e.Message) + .WithSeverity(NotificationSeverity.Warning) + .Show(); } finally { diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs index 519222c24..6b2571600 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -7,11 +7,12 @@ namespace Artemis.UI.Services.Updating; public interface IUpdateService : IArtemisUIService { string Channel { get; } + string? PreviousVersion { get; } IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; } Task CacheLatestRelease(); Task CheckForUpdate(); - void QueueUpdate(); + void QueueUpdate(string version); ReleaseInstaller GetReleaseInstaller(string releaseId); void RestartForUpdate(bool silent); diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index d858c5d79..d2cf9bef9 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -4,8 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.Storage.Entities.General; -using Artemis.Storage.Repositories.Interfaces; +using Artemis.Storage.Repositories; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; using Serilog; @@ -22,10 +21,9 @@ public class UpdateService : IUpdateService private readonly Platform _updatePlatform; private readonly ILogger _logger; - private readonly IMainWindowService _mainWindowService; - private readonly IQueuedActionRepository _queuedActionRepository; - private readonly Lazy _updateNotificationProvider; private readonly IUpdatingClient _updatingClient; + private readonly IReleaseRepository _releaseRepository; + private readonly Lazy _updateNotificationProvider; private readonly Func _getReleaseInstaller; private bool _suspendAutoCheck; @@ -33,15 +31,14 @@ public class UpdateService : IUpdateService public UpdateService(ILogger logger, ISettingsService settingsService, IMainWindowService mainWindowService, - IQueuedActionRepository queuedActionRepository, IUpdatingClient updatingClient, + IReleaseRepository releaseRepository, Lazy updateNotificationProvider, Func getReleaseInstaller) { _logger = logger; - _mainWindowService = mainWindowService; - _queuedActionRepository = queuedActionRepository; _updatingClient = updatingClient; + _releaseRepository = releaseRepository; _updateNotificationProvider = updateNotificationProvider; _getReleaseInstaller = getReleaseInstaller; @@ -59,33 +56,41 @@ public class UpdateService : IUpdateService _updatePlatform = Platform.Osx; else throw new PlatformNotSupportedException("Cannot auto update on the current platform"); - + _autoCheck = settingsService.GetSetting("UI.Updating.AutoCheck", true); _autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", false); _autoCheck.SettingChanged += HandleAutoUpdateEvent; - _mainWindowService.MainWindowOpened += HandleAutoUpdateEvent; + mainWindowService.MainWindowOpened += HandleAutoUpdateEvent; Timer timer = new(UPDATE_CHECK_INTERVAL); timer.Elapsed += HandleAutoUpdateEvent; timer.Start(); - InstallQueuedUpdate(); - _logger.Information("Update service initialized for {Channel} channel", Channel); + ProcessReleaseStatus(); } public string Channel { get; } + public string? PreviousVersion { get; set; } public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } - private void InstallQueuedUpdate() + private void ProcessReleaseStatus() { - if (!_queuedActionRepository.IsTypeQueued("InstallUpdate")) + // If an update is queued, don't bother with anything else + string? queued = _releaseRepository.GetQueuedVersion(); + if (queued != null) + { + // Remove the queued installation, in case something goes wrong then at least we don't end up in a loop + _logger.Information("Installing queued version {Version}", queued); + RestartForUpdate(true); return; + } + + // If a different version was installed, mark it as such + string? installed = _releaseRepository.GetInstalledVersion(); + if (installed != Constants.CurrentVersion) + _releaseRepository.FinishInstallation(Constants.CurrentVersion); - // Remove the queued action, in case something goes wrong then at least we don't end up in a loop - _queuedActionRepository.ClearByType("InstallUpdate"); - - _logger.Information("Installing queued update"); - Utilities.ApplyUpdate(false); + PreviousVersion = _releaseRepository.GetPreviousInstalledVersion(); } private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release) @@ -111,15 +116,22 @@ public class UpdateService : IUpdateService } catch (Exception ex) { - _logger.Warning(ex, "Auto update failed"); + _logger.Warning(ex, "Auto update-check failed"); } } /// public async Task CacheLatestRelease() { - IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform); - CachedLatestRelease = result.Data?.NextPublishedRelease; + try + { + IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform); + CachedLatestRelease = result.Data?.NextPublishedRelease; + } + catch (Exception e) + { + _logger.Warning(e, "Failed to cache latest release"); + } } public async Task CheckForUpdate() @@ -147,18 +159,11 @@ public class UpdateService : IUpdateService } /// - public void QueueUpdate() + public void QueueUpdate(string version) { - if (!_queuedActionRepository.IsTypeQueued("InstallUpdate")) - _queuedActionRepository.Add(new QueuedActionEntity {Type = "InstallUpdate"}); + _releaseRepository.QueueInstallation(version); } - - /// - public void DequeueUpdate() - { - _queuedActionRepository.ClearByType("InstallUpdate"); - } - + /// public ReleaseInstaller GetReleaseInstaller(string releaseId) { @@ -168,7 +173,7 @@ public class UpdateService : IUpdateService /// public void RestartForUpdate(bool silent) { - DequeueUpdate(); + _releaseRepository.DequeueInstallation(); Utilities.ApplyUpdate(silent); } } \ No newline at end of file From ee19776afad8bb6b37d6e569be19f5d94b20cc96 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 4 Mar 2023 19:25:31 +0100 Subject: [PATCH 18/34] Clean up old releases --- .../Entities/General/ReleaseEntity.cs | 4 +- .../Repositories/ReleaseRepository.cs | 38 ++++++------- .../WindowsUpdateNotificationProvider.cs | 2 +- .../Settings/Updating/ReleaseViewModel.cs | 2 +- .../Services/Updating/IUpdateService.cs | 2 +- .../Services/Updating/UpdateService.cs | 56 +++++++++++++------ 6 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs index afae86286..3d10ed71b 100644 --- a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs +++ b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs @@ -7,6 +7,7 @@ public class ReleaseEntity public Guid Id { get; set; } public string Version { get; set; } + public string ReleaseId { get; set; } public ReleaseEntityStatus Status { get; set; } public DateTimeOffset? InstalledAt { get; set; } } @@ -15,5 +16,6 @@ public enum ReleaseEntityStatus { Queued, Installed, - Historical + Historical, + Unknown } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ReleaseRepository.cs b/src/Artemis.Storage/Repositories/ReleaseRepository.cs index d052f550d..0c2d466dd 100644 --- a/src/Artemis.Storage/Repositories/ReleaseRepository.cs +++ b/src/Artemis.Storage/Repositories/ReleaseRepository.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories.Interfaces; using LiteDB; @@ -18,25 +17,25 @@ public class ReleaseRepository : IReleaseRepository _repository.Database.GetCollection().EnsureIndex(s => s.Status); } - public string GetQueuedVersion() + public ReleaseEntity GetQueuedVersion() { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).FirstOrDefault()?.Version; + return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).FirstOrDefault(); } - public string GetInstalledVersion() + public ReleaseEntity GetInstalledVersion() { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Installed).FirstOrDefault()?.Version; + return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Installed).FirstOrDefault(); } - public string GetPreviousInstalledVersion() + public ReleaseEntity GetPreviousInstalledVersion() { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Historical).OrderByDescending(r => r.InstalledAt).FirstOrDefault()?.Version; + return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Historical).OrderByDescending(r => r.InstalledAt).FirstOrDefault(); } - - public void QueueInstallation(string version) + + public void QueueInstallation(string version, string releaseId) { // Mark release as queued and add if missing - ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault() ?? new ReleaseEntity {Version = version}; + ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault() ?? new ReleaseEntity {Version = version, ReleaseId = releaseId}; release.Status = ReleaseEntityStatus.Queued; _repository.Upsert(release); } @@ -50,10 +49,7 @@ public class ReleaseRepository : IReleaseRepository _repository.Upsert(release); // Mark other releases as historical - List oldReleases = _repository.Query().Where(r => r.Version != version && r.Status == ReleaseEntityStatus.Installed).ToList(); - if (!oldReleases.Any()) - return; - + List oldReleases = _repository.Query().Where(r => r.Version != version && r.Status != ReleaseEntityStatus.Historical).ToList(); foreach (ReleaseEntity oldRelease in oldReleases) oldRelease.Status = ReleaseEntityStatus.Historical; _repository.Update(oldReleases); @@ -61,16 +57,20 @@ public class ReleaseRepository : IReleaseRepository public void DequeueInstallation() { - _repository.DeleteMany(r => r.Status == ReleaseEntityStatus.Queued); + // Mark all queued releases as unknown, until FinishInstallation is called we don't know the status + List queuedReleases = _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).ToList(); + foreach (ReleaseEntity queuedRelease in queuedReleases) + queuedRelease.Status = ReleaseEntityStatus.Unknown; + _repository.Update(queuedReleases); } } public interface IReleaseRepository : IRepository { - string GetQueuedVersion(); - string GetInstalledVersion(); - string GetPreviousInstalledVersion(); - void QueueInstallation(string version); + ReleaseEntity GetQueuedVersion(); + ReleaseEntity GetInstalledVersion(); + ReleaseEntity GetPreviousInstalledVersion(); + void QueueInstallation(string version, string releaseId); void FinishInstallation(string version); void DequeueInstallation(); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs index fa9ea76fa..024844bd6 100644 --- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs +++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs @@ -128,7 +128,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider } // Queue an update in case the user interrupts the process after everything has been prepared - _updateService.QueueUpdate(releaseVersion); + _updateService.QueueUpdate(releaseVersion, releaseId); GetBuilderForRelease(releaseId, releaseVersion) .AddAudio(new ToastAudio {Silent = true}) diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs index bf959adbb..263ff055e 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs @@ -158,7 +158,7 @@ public class ReleaseViewModel : ActivatableViewModelBase { InstallationInProgress = true; await ReleaseInstaller.InstallAsync(_installerCts.Token); - _updateService.QueueUpdate(Version); + _updateService.QueueUpdate(Version, ReleaseId); InstallationFinished = true; } catch (Exception e) diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs index 6b2571600..0da40b1a9 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -12,7 +12,7 @@ public interface IUpdateService : IArtemisUIService Task CacheLatestRelease(); Task CheckForUpdate(); - void QueueUpdate(string version); + void QueueUpdate(string version, string releaseId); ReleaseInstaller GetReleaseInstaller(string releaseId); void RestartForUpdate(bool silent); diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index d2cf9bef9..8e46d20b5 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -1,9 +1,11 @@ using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; @@ -18,13 +20,13 @@ public class UpdateService : IUpdateService private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour private readonly PluginSetting _autoCheck; private readonly PluginSetting _autoInstall; - private readonly Platform _updatePlatform; + private readonly Func _getReleaseInstaller; private readonly ILogger _logger; - private readonly IUpdatingClient _updatingClient; private readonly IReleaseRepository _releaseRepository; private readonly Lazy _updateNotificationProvider; - private readonly Func _getReleaseInstaller; + private readonly Platform _updatePlatform; + private readonly IUpdatingClient _updatingClient; private bool _suspendAutoCheck; @@ -69,28 +71,46 @@ public class UpdateService : IUpdateService ProcessReleaseStatus(); } - public string Channel { get; } - public string? PreviousVersion { get; set; } - public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } - private void ProcessReleaseStatus() { // If an update is queued, don't bother with anything else - string? queued = _releaseRepository.GetQueuedVersion(); + ReleaseEntity? queued = _releaseRepository.GetQueuedVersion(); if (queued != null) { // Remove the queued installation, in case something goes wrong then at least we don't end up in a loop - _logger.Information("Installing queued version {Version}", queued); + _logger.Information("Installing queued version {Version}", queued.Version); RestartForUpdate(true); return; } - + // If a different version was installed, mark it as such - string? installed = _releaseRepository.GetInstalledVersion(); - if (installed != Constants.CurrentVersion) + ReleaseEntity? installed = _releaseRepository.GetInstalledVersion(); + if (installed?.Version != Constants.CurrentVersion) _releaseRepository.FinishInstallation(Constants.CurrentVersion); - PreviousVersion = _releaseRepository.GetPreviousInstalledVersion(); + PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version; + + if (!Directory.Exists(Path.Combine(Constants.DataFolder, "updating"))) + return; + + // Clean up the update folder, leaving only the last ZIP + foreach (string file in Directory.GetFiles(Path.Combine(Constants.DataFolder, "updating"))) + { + if (Path.GetExtension(file) != ".zip") + continue; + if (installed != null && Path.GetFileName(file) == $"{installed.ReleaseId}.zip") + continue; + + try + { + _logger.Debug("Cleaning up old update file at {FilePath}", file); + File.Delete(file); + } + catch (Exception e) + { + _logger.Warning(e, "Failed to clean up old update file at {FilePath}", file); + } + } } private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release) @@ -120,6 +140,10 @@ public class UpdateService : IUpdateService } } + public string Channel { get; } + public string? PreviousVersion { get; set; } + public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } + /// public async Task CacheLatestRelease() { @@ -159,11 +183,11 @@ public class UpdateService : IUpdateService } /// - public void QueueUpdate(string version) + public void QueueUpdate(string version, string releaseId) { - _releaseRepository.QueueInstallation(version); + _releaseRepository.QueueInstallation(version, releaseId); } - + /// public ReleaseInstaller GetReleaseInstaller(string releaseId) { From 09a2f769c33aaddd0edc57e93ced79a9be12f550 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 4 Mar 2023 19:34:54 +0100 Subject: [PATCH 19/34] Update homepage update link (I need a build lol) --- src/Artemis.UI/Screens/Home/HomeView.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Artemis.UI/Screens/Home/HomeView.axaml b/src/Artemis.UI/Screens/Home/HomeView.axaml index a20f0df65..691770a13 100644 --- a/src/Artemis.UI/Screens/Home/HomeView.axaml +++ b/src/Artemis.UI/Screens/Home/HomeView.axaml @@ -109,7 +109,7 @@ + NavigateUri="https://wiki.artemis-rgb.com/en/donating"> Donate From 4a8845e57895a75a9f46763f591885173e4d2f8b Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 6 Mar 2023 21:58:44 +0100 Subject: [PATCH 20/34] Simplify release install process Fix install on startup --- src/Artemis.Core/Constants.cs | 4 + src/Artemis.Core/Utilities/Utilities.cs | 1 + .../Entities/General/ReleaseEntity.cs | 10 - .../Repositories/ReleaseRepository.cs | 53 +----- src/Artemis.UI.Windows/App.axaml.cs | 4 +- .../ApplicationStateManager.cs | 4 +- .../WindowsUpdateNotificationProvider.cs | 30 ++- src/Artemis.UI/DryIoc/Factories/IVMFactory.cs | 4 +- src/Artemis.UI/Screens/Root/RootViewModel.cs | 3 + .../Settings/Tabs/ReleasesTabViewModel.cs | 2 +- .../Settings/Updating/ReleaseViewModel.cs | 5 +- .../Updating/IUpdateNotificationProvider.cs | 3 +- .../Services/Updating/IUpdateService.cs | 40 +++- .../InAppUpdateNotificationProvider.cs | 6 +- .../Services/Updating/ReleaseInstaller.cs | 61 +++--- .../Services/Updating/UpdateService.cs | 77 ++++---- .../Queries/GetReleaseById.graphql | 6 +- src/Artemis.WebClient.Updating/schema.graphql | 177 ++++++++---------- 18 files changed, 237 insertions(+), 253 deletions(-) diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 15213165d..252d3b85a 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -48,6 +48,10 @@ public static class Constants /// The full path to the Artemis logs folder /// public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs"); + /// + /// The full path to the Artemis logs folder + /// + public static readonly string UpdatingFolder = Path.Combine(DataFolder, "updating"); /// /// The full path to the Artemis plugins folder diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index e5590b568..db6e373a0 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -21,6 +21,7 @@ public static class Utilities CreateAccessibleDirectory(Constants.DataFolder); CreateAccessibleDirectory(Constants.PluginsFolder); CreateAccessibleDirectory(Constants.LayoutsFolder); + CreateAccessibleDirectory(Constants.UpdatingFolder); } /// diff --git a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs index 3d10ed71b..7c517ff79 100644 --- a/src/Artemis.Storage/Entities/General/ReleaseEntity.cs +++ b/src/Artemis.Storage/Entities/General/ReleaseEntity.cs @@ -7,15 +7,5 @@ public class ReleaseEntity public Guid Id { get; set; } public string Version { get; set; } - public string ReleaseId { get; set; } - public ReleaseEntityStatus Status { get; set; } public DateTimeOffset? InstalledAt { get; set; } -} - -public enum ReleaseEntityStatus -{ - Queued, - Installed, - Historical, - Unknown } \ No newline at end of file diff --git a/src/Artemis.Storage/Repositories/ReleaseRepository.cs b/src/Artemis.Storage/Repositories/ReleaseRepository.cs index 0c2d466dd..3516c6c80 100644 --- a/src/Artemis.Storage/Repositories/ReleaseRepository.cs +++ b/src/Artemis.Storage/Repositories/ReleaseRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories.Interfaces; using LiteDB; @@ -14,63 +13,25 @@ public class ReleaseRepository : IReleaseRepository { _repository = repository; _repository.Database.GetCollection().EnsureIndex(s => s.Version, true); - _repository.Database.GetCollection().EnsureIndex(s => s.Status); } - public ReleaseEntity GetQueuedVersion() + public void SaveVersionInstallDate(string version) { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).FirstOrDefault(); - } + ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault(); + if (release != null) + return; - public ReleaseEntity GetInstalledVersion() - { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Installed).FirstOrDefault(); + _repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow}); } public ReleaseEntity GetPreviousInstalledVersion() { - return _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Historical).OrderByDescending(r => r.InstalledAt).FirstOrDefault(); - } - - public void QueueInstallation(string version, string releaseId) - { - // Mark release as queued and add if missing - ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault() ?? new ReleaseEntity {Version = version, ReleaseId = releaseId}; - release.Status = ReleaseEntityStatus.Queued; - _repository.Upsert(release); - } - - public void FinishInstallation(string version) - { - // Mark release as installed and add if missing - ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault() ?? new ReleaseEntity {Version = version}; - release.Status = ReleaseEntityStatus.Installed; - release.InstalledAt = DateTimeOffset.UtcNow; - _repository.Upsert(release); - - // Mark other releases as historical - List oldReleases = _repository.Query().Where(r => r.Version != version && r.Status != ReleaseEntityStatus.Historical).ToList(); - foreach (ReleaseEntity oldRelease in oldReleases) - oldRelease.Status = ReleaseEntityStatus.Historical; - _repository.Update(oldReleases); - } - - public void DequeueInstallation() - { - // Mark all queued releases as unknown, until FinishInstallation is called we don't know the status - List queuedReleases = _repository.Query().Where(r => r.Status == ReleaseEntityStatus.Queued).ToList(); - foreach (ReleaseEntity queuedRelease in queuedReleases) - queuedRelease.Status = ReleaseEntityStatus.Unknown; - _repository.Update(queuedReleases); + return _repository.Query().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault(); } } public interface IReleaseRepository : IRepository { - ReleaseEntity GetQueuedVersion(); - ReleaseEntity GetInstalledVersion(); + void SaveVersionInstallDate(string version); ReleaseEntity GetPreviousInstalledVersion(); - void QueueInstallation(string version, string releaseId); - void FinishInstallation(string version); - void DequeueInstallation(); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index b2a8db3df..9444327c6 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -43,9 +43,9 @@ public class App : Application { if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown) return; - - ArtemisBootstrapper.Initialize(); + _applicationStateManager = new ApplicationStateManager(_container!, desktop.Args); + ArtemisBootstrapper.Initialize(); RegisterProviders(_container!); } diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 1a7d8147e..05435e5ac 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -99,8 +99,8 @@ public class ApplicationStateManager argsList.Add("--autorun"); // Retain startup arguments after update by providing them to the script - string script = $"\"{Path.Combine(Constants.DataFolder, "updating", "pending", "scripts", "update.ps1")}\""; - string source = $"-sourceDirectory \"{Path.Combine(Constants.DataFolder, "updating", "pending")}\""; + string script = $"\"{Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1")}\""; + string source = $"-sourceDirectory \"{Path.Combine(Constants.UpdatingFolder, "installing")}\""; string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs index 024844bd6..3b434360d 100644 --- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs +++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs @@ -9,6 +9,7 @@ using Artemis.UI.Screens.Settings; using Artemis.UI.Services.Updating; using Artemis.UI.Shared.Services.MainWindow; using Avalonia.Threading; +using DryIoc.ImTools; using Microsoft.Toolkit.Uwp.Notifications; using ReactiveUI; @@ -16,7 +17,7 @@ namespace Artemis.UI.Windows.Providers; public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider { - private readonly Func _getReleaseInstaller; + private readonly Func _getReleaseInstaller; private readonly Func _getSettingsViewModel; private readonly IMainWindowService _mainWindowService; private readonly IUpdateService _updateService; @@ -25,7 +26,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService, IUpdateService updateService, Func getSettingsViewModel, - Func getReleaseInstaller) + Func getReleaseInstaller) { _mainWindowService = mainWindowService; _updateService = updateService; @@ -37,7 +38,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e) { ToastArguments args = ToastArguments.Parse(e.Argument); - string releaseId = args.Get("releaseId"); + Guid releaseId = Guid.Parse(args.Get("releaseId")); string releaseVersion = args.Get("releaseVersion"); string action = "view-changes"; if (args.Contains("action")) @@ -53,7 +54,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider _updateService.RestartForUpdate(false); } - public void ShowNotification(string releaseId, string releaseVersion) + public void ShowNotification(Guid releaseId, string releaseVersion) { GetBuilderForRelease(releaseId, releaseVersion) .AddText("Update available") @@ -62,10 +63,10 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider .SetContent("Install") .AddArgument("action", "install").SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate)) .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes")) - .Show(t => t.Tag = releaseId); + .Show(t => t.Tag = releaseId.ToString()); } - private void ViewRelease(string releaseId) + private void ViewRelease(Guid releaseId) { Dispatcher.UIThread.Post(() => { @@ -87,7 +88,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider }); } - private async Task InstallRelease(string releaseId, string releaseVersion) + private async Task InstallRelease(Guid releaseId, string releaseVersion) { ReleaseInstaller installer = _getReleaseInstaller(releaseId); void InstallerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateInstallProgress(releaseId, installer); @@ -104,7 +105,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider .AddButton(new ToastButton().SetContent("Cancel").AddArgument("action", "cancel")) .Show(t => { - t.Tag = releaseId; + t.Tag = releaseId.ToString(); t.Data = GetDataForInstaller(installer); }); @@ -127,26 +128,23 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider installer.PropertyChanged -= InstallerOnPropertyChanged; } - // Queue an update in case the user interrupts the process after everything has been prepared - _updateService.QueueUpdate(releaseVersion, releaseId); - GetBuilderForRelease(releaseId, releaseVersion) .AddAudio(new ToastAudio {Silent = true}) .AddText("Update ready") .AddText($"Artemis version {releaseVersion} is ready to be applied") .AddButton(new ToastButton().SetContent("Restart Artemis").AddArgument("action", "restart-for-update")) .AddButton(new ToastButton().SetContent("Later").AddArgument("action", "postpone-update")) - .Show(t => t.Tag = releaseId); + .Show(t => t.Tag = releaseId.ToString()); } - private void UpdateInstallProgress(string releaseId, ReleaseInstaller installer) + private void UpdateInstallProgress(Guid releaseId, ReleaseInstaller installer) { - ToastNotificationManagerCompat.CreateToastNotifier().Update(GetDataForInstaller(installer), releaseId); + ToastNotificationManagerCompat.CreateToastNotifier().Update(GetDataForInstaller(installer), releaseId.ToString()); } - private ToastContentBuilder GetBuilderForRelease(string releaseId, string releaseVersion) + private ToastContentBuilder GetBuilderForRelease(Guid releaseId, string releaseVersion) { - return new ToastContentBuilder().AddArgument("releaseId", releaseId).AddArgument("releaseVersion", releaseVersion); + return new ToastContentBuilder().AddArgument("releaseId", releaseId.ToString()).AddArgument("releaseVersion", releaseVersion); } private NotificationData GetDataForInstaller(ReleaseInstaller installer) diff --git a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs index 8b3c5fa42..7d3e344aa 100644 --- a/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs +++ b/src/Artemis.UI/DryIoc/Factories/IVMFactory.cs @@ -480,7 +480,7 @@ public class ScriptVmFactory : IScriptVmFactory public interface IReleaseVmFactory : IVmFactory { - ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt); + ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt); } public class ReleaseVmFactory : IReleaseVmFactory { @@ -491,7 +491,7 @@ public class ReleaseVmFactory : IReleaseVmFactory _container = container; } - public ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt) + public ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt) { return _container.Resolve(new object[] { releaseId, version, createdAt }); } diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 2037d0b1a..373fd22f1 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -64,6 +64,9 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel); Task.Run(() => { + if (_updateService.Initialize()) + return; + coreService.Initialize(); registrationService.RegisterBuiltInDataModelDisplays(); registrationService.RegisterBuiltInDataModelInputs(); diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs index c504c4510..8d4c54f1c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs @@ -61,7 +61,7 @@ public class ReleasesTabViewModel : ActivatableViewModelBase public ReadOnlyObservableCollection ReleaseViewModels { get; } public string Channel { get; } - public string? PreselectId { get; set; } + public Guid? PreselectId { get; set; } public ReleaseViewModel? SelectedReleaseViewModel { diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs index 263ff055e..bce5e0181 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs @@ -36,7 +36,7 @@ public class ReleaseViewModel : ActivatableViewModelBase private bool _loading = true; private bool _retrievedDetails; - public ReleaseViewModel(string releaseId, + public ReleaseViewModel(Guid releaseId, string version, DateTimeOffset createdAt, ILogger logger, @@ -79,7 +79,7 @@ public class ReleaseViewModel : ActivatableViewModelBase }); } - public string ReleaseId { get; } + public Guid ReleaseId { get; } private void ExecuteRestart() { @@ -158,7 +158,6 @@ public class ReleaseViewModel : ActivatableViewModelBase { InstallationInProgress = true; await ReleaseInstaller.InstallAsync(_installerCts.Token); - _updateService.QueueUpdate(Version, ReleaseId); InstallationFinished = true; } catch (Exception e) diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs index 9e1268f29..5f77faf60 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs @@ -1,8 +1,9 @@ +using System; using System.Threading.Tasks; namespace Artemis.UI.Services.Updating; public interface IUpdateNotificationProvider { - void ShowNotification(string releaseId, string releaseVersion); + void ShowNotification(Guid releaseId, string releaseVersion); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs index 0da40b1a9..225ecbd97 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Artemis.UI.Services.Interfaces; using Artemis.WebClient.Updating; @@ -6,14 +7,47 @@ namespace Artemis.UI.Services.Updating; public interface IUpdateService : IArtemisUIService { + /// + /// Gets the current update channel. + /// string Channel { get; } + + /// + /// Gets the version number of the previous release that was installed, if any. + /// string? PreviousVersion { get; } + + /// + /// The latest cached release, can be updated by calling . + /// IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; } + /// + /// Asynchronously caches the latest release. + /// Task CacheLatestRelease(); - Task CheckForUpdate(); - void QueueUpdate(string version, string releaseId); - ReleaseInstaller GetReleaseInstaller(string releaseId); + /// + /// Asynchronously checks whether an update is available on the current . + /// + Task CheckForUpdate(); + + /// + /// Creates a release installed for a release with the provided ID. + /// + /// The ID of the release to create the installer for. + /// The resulting release installer. + ReleaseInstaller GetReleaseInstaller(Guid releaseId); + + /// + /// Restarts the application to install a pending update. + /// + /// A boolean indicating whether to perform a silent install of the update. void RestartForUpdate(bool silent); + + /// + /// Initializes the update service. + /// + /// A boolean indicating whether a restart will occur to install a pending update. + bool Initialize(); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs index 1a4d066b9..46a06a2ea 100644 --- a/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs @@ -23,7 +23,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider _getSettingsViewModel = getSettingsViewModel; } - private void ShowInAppNotification(string releaseId, string releaseVersion) + private void ShowInAppNotification(Guid releaseId, string releaseVersion) { _notification?.Invoke(); _notification = _notificationService.CreateNotification() @@ -35,7 +35,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider .Show(); } - private void ViewRelease(string releaseId) + private void ViewRelease(Guid releaseId) { _notification?.Invoke(); @@ -56,7 +56,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider } /// - public void ShowNotification(string releaseId, string releaseVersion) + public void ShowNotification(Guid releaseId, string releaseVersion) { if (_mainWindowService.IsMainWindowOpen) ShowInAppNotification(releaseId, releaseVersion); diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs index 78535ac31..04ca4f74f 100644 --- a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -22,24 +22,26 @@ namespace Artemis.UI.Services.Updating; /// public class ReleaseInstaller : CorePropertyChanged { - private readonly string _dataFolder; private readonly HttpClient _httpClient; private readonly ILogger _logger; - private readonly string _releaseId; + private readonly Guid _releaseId; private readonly Platform _updatePlatform; private readonly IUpdatingClient _updatingClient; private readonly Progress _progress = new(); + + private IGetReleaseById_PublishedRelease _release = null!; + private IGetReleaseById_PublishedRelease_Artifacts _artifact = null!; + private Progress _stepProgress = new(); private string _status = string.Empty; - private float _progress1; + private float _floatProgress; - public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) + public ReleaseInstaller(Guid releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) { _releaseId = releaseId; _logger = logger; _updatingClient = updatingClient; _httpClient = httpClient; - _dataFolder = Path.Combine(Constants.DataFolder, "updating"); if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; @@ -50,9 +52,6 @@ public class ReleaseInstaller : CorePropertyChanged else throw new PlatformNotSupportedException("Cannot auto update on the current platform"); - if (!Directory.Exists(_dataFolder)) - Directory.CreateDirectory(_dataFolder); - _progress.ProgressChanged += (_, f) => Progress = f; } @@ -64,8 +63,8 @@ public class ReleaseInstaller : CorePropertyChanged public float Progress { - get => _progress1; - set => SetAndNotify(ref _progress1, value); + get => _floatProgress; + set => SetAndNotify(ref _floatProgress, value); } public async Task InstallAsync(CancellationToken cancellationToken) @@ -79,24 +78,24 @@ public class ReleaseInstaller : CorePropertyChanged IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken); result.EnsureNoErrors(); - IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease; - if (release == null) + _release = result.Data?.PublishedRelease!; + if (_release == null) throw new Exception($"Could not find release with ID {_releaseId}"); - IGetReleaseById_PublishedRelease_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform); - if (artifact == null) + _artifact = _release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform)!; + if (_artifact == null) throw new Exception("Found the release but it has no artifact for the current platform"); ((IProgress) _progress).Report(10); // Determine whether the last update matches our local version, then we can download the delta - if (release.PreviousRelease != null && File.Exists(Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip")) && artifact.DeltaFileInfo.DownloadSize != 0) - await DownloadDelta(artifact, Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip"), cancellationToken); + if (_release.PreviousRelease != null && File.Exists(Path.Combine(Constants.UpdatingFolder, $"{_release.PreviousRelease.Version}.zip")) && _artifact.DeltaFileInfo.DownloadSize != 0) + await DownloadDelta(Path.Combine(Constants.UpdatingFolder, $"{_release.PreviousRelease.Version}.zip"), cancellationToken); else - await Download(artifact, cancellationToken); + await Download(cancellationToken); } - private async Task DownloadDelta(IGetReleaseById_PublishedRelease_Artifacts artifact, string previousRelease, CancellationToken cancellationToken) + private async Task DownloadDelta(string previousRelease, CancellationToken cancellationToken) { // 10 - 50% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; @@ -104,20 +103,20 @@ public class ReleaseInstaller : CorePropertyChanged Status = "Downloading..."; await using MemoryStream stream = new(); - await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{_artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken); _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; - await PatchDelta(stream, previousRelease, artifact, cancellationToken); + await PatchDelta(stream, previousRelease, cancellationToken); } - private async Task PatchDelta(Stream deltaStream, string previousRelease, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken) + private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken) { // 50 - 60% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(50f + e * 0.1f); Status = "Patching..."; - await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + await using FileStream newFileStream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); await using (FileStream baseStream = File.OpenRead(previousRelease)) { deltaStream.Seek(0, SeekOrigin.Begin); @@ -132,23 +131,23 @@ public class ReleaseInstaller : CorePropertyChanged _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; - await ValidateArchive(newFileStream, artifact, cancellationToken); + await ValidateArchive(newFileStream, cancellationToken); await Extract(newFileStream, cancellationToken); } - private async Task Download(IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken) + private async Task Download(CancellationToken cancellationToken) { // 10 - 60% _stepProgress.ProgressChanged += StepProgressOnProgressChanged; void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress) _progress).Report(10f + e * 0.5f); Status = "Downloading..."; - await using FileStream stream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); - await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}", stream, _stepProgress, cancellationToken); + await using FileStream stream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{_artifact.ArtifactId}", stream, _stepProgress, cancellationToken); _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; - await ValidateArchive(stream, artifact, cancellationToken); + await ValidateArchive(stream, cancellationToken); await Extract(stream, cancellationToken); } @@ -160,7 +159,7 @@ public class ReleaseInstaller : CorePropertyChanged Status = "Extracting..."; // Ensure the directory is empty - string extractDirectory = Path.Combine(_dataFolder, "pending"); + string extractDirectory = Path.Combine(Constants.UpdatingFolder, "pending"); if (Directory.Exists(extractDirectory)) Directory.Delete(extractDirectory, true); Directory.CreateDirectory(extractDirectory); @@ -176,12 +175,12 @@ public class ReleaseInstaller : CorePropertyChanged _stepProgress.ProgressChanged -= StepProgressOnProgressChanged; } - private async Task ValidateArchive(Stream archiveStream, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken) + private async Task ValidateArchive(Stream archiveStream, CancellationToken cancellationToken) { using MD5 md5 = MD5.Create(); archiveStream.Seek(0, SeekOrigin.Begin); string hash = BitConverter.ToString(await md5.ComputeHashAsync(archiveStream, cancellationToken)).Replace("-", ""); - if (hash != artifact.FileInfo.Md5Hash) - throw new ArtemisUIException($"Update file hash mismatch, expected \"{artifact.FileInfo.Md5Hash}\" but got \"{hash}\""); + if (hash != _artifact.FileInfo.Md5Hash) + throw new ArtemisUIException($"Update file hash mismatch, expected \"{_artifact.FileInfo.Md5Hash}\" but got \"{hash}\""); } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 8e46d20b5..70c53e3f5 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -7,6 +7,7 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.Storage.Entities.General; using Artemis.Storage.Repositories; +using Artemis.UI.Exceptions; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; using Serilog; @@ -20,7 +21,7 @@ public class UpdateService : IUpdateService private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour private readonly PluginSetting _autoCheck; private readonly PluginSetting _autoInstall; - private readonly Func _getReleaseInstaller; + private readonly Func _getReleaseInstaller; private readonly ILogger _logger; private readonly IReleaseRepository _releaseRepository; @@ -36,7 +37,7 @@ public class UpdateService : IUpdateService IUpdatingClient updatingClient, IReleaseRepository releaseRepository, Lazy updateNotificationProvider, - Func getReleaseInstaller) + Func getReleaseInstaller) { _logger = logger; _updatingClient = updatingClient; @@ -66,39 +67,21 @@ public class UpdateService : IUpdateService Timer timer = new(UPDATE_CHECK_INTERVAL); timer.Elapsed += HandleAutoUpdateEvent; timer.Start(); - - _logger.Information("Update service initialized for {Channel} channel", Channel); - ProcessReleaseStatus(); } private void ProcessReleaseStatus() { - // If an update is queued, don't bother with anything else - ReleaseEntity? queued = _releaseRepository.GetQueuedVersion(); - if (queued != null) - { - // Remove the queued installation, in case something goes wrong then at least we don't end up in a loop - _logger.Information("Installing queued version {Version}", queued.Version); - RestartForUpdate(true); - return; - } - - // If a different version was installed, mark it as such - ReleaseEntity? installed = _releaseRepository.GetInstalledVersion(); - if (installed?.Version != Constants.CurrentVersion) - _releaseRepository.FinishInstallation(Constants.CurrentVersion); - + string currentVersion = Constants.CurrentVersion; + _releaseRepository.SaveVersionInstallDate(currentVersion); PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version; - if (!Directory.Exists(Path.Combine(Constants.DataFolder, "updating"))) + if (!Directory.Exists(Constants.UpdatingFolder)) return; // Clean up the update folder, leaving only the last ZIP - foreach (string file in Directory.GetFiles(Path.Combine(Constants.DataFolder, "updating"))) + foreach (string file in Directory.GetFiles(Constants.UpdatingFolder)) { - if (Path.GetExtension(file) != ".zip") - continue; - if (installed != null && Path.GetFileName(file) == $"{installed.ReleaseId}.zip") + if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip") continue; try @@ -140,8 +123,13 @@ public class UpdateService : IUpdateService } } + /// public string Channel { get; } - public string? PreviousVersion { get; set; } + + /// + public string? PreviousVersion { get; private set; } + + /// public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; } /// @@ -158,6 +146,7 @@ public class UpdateService : IUpdateService } } + /// public async Task CheckForUpdate() { IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform); @@ -183,13 +172,7 @@ public class UpdateService : IUpdateService } /// - public void QueueUpdate(string version, string releaseId) - { - _releaseRepository.QueueInstallation(version, releaseId); - } - - /// - public ReleaseInstaller GetReleaseInstaller(string releaseId) + public ReleaseInstaller GetReleaseInstaller(Guid releaseId) { return _getReleaseInstaller(releaseId); } @@ -197,7 +180,33 @@ public class UpdateService : IUpdateService /// public void RestartForUpdate(bool silent) { - _releaseRepository.DequeueInstallation(); + if (!Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending"))) + throw new ArtemisUIException("Cannot install update, none is pending."); + + Directory.Move(Path.Combine(Constants.UpdatingFolder, "pending"), Path.Combine(Constants.UpdatingFolder, "installing")); Utilities.ApplyUpdate(silent); } + + /// + public bool Initialize() + { + // There should never be an installing folder + if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "installing"))) + { + _logger.Warning("Cleaning up leftover installing folder, did an update go wrong?"); + Directory.Delete(Path.Combine(Constants.UpdatingFolder, "installing")); + } + + // If an update is pending, don't bother with anything else + if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending"))) + { + _logger.Information("Installing pending update"); + RestartForUpdate(true); + return true; + } + + ProcessReleaseStatus(); + _logger.Information("Update service initialized for {Channel} channel", Channel); + return false; + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql index dff2f5582..e364038ff 100644 --- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -22,12 +22,14 @@ query GetReleases($branch: String!, $platform: Platform!, $take: Int!, $after: S } -query GetReleaseById($id: String!) { +query GetReleaseById($id: UUID!) { publishedRelease(id: $id) { branch commit version - previousRelease + previousRelease { + version + } changelog artifacts { platform diff --git a/src/Artemis.WebClient.Updating/schema.graphql b/src/Artemis.WebClient.Updating/schema.graphql index 464ffb797..fe855cb6c 100644 --- a/src/Artemis.WebClient.Updating/schema.graphql +++ b/src/Artemis.WebClient.Updating/schema.graphql @@ -5,33 +5,6 @@ schema { mutation: Mutation } -"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`." -directive @defer( - "Deferred when true." - if: Boolean, - "If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." - label: String -) on FRAGMENT_SPREAD | INLINE_FRAGMENT - -"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`." -directive @stream( - "Streamed when true." - if: Boolean, - "The initial elements that shall be send down to the consumer." - initialCount: Int! = 0, - "If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." - label: String -) on FIELD - -directive @authorize( - "Defines when when the resolver shall be executed.By default the resolver is executed after the policy has determined that the current user is allowed to access the field." - apply: ApplyPolicy! = BEFORE_RESOLVER, - "The name of the authorization policy that determines access to the annotated resource." - policy: String, - "Roles that are allowed to access the annotated resource." - roles: [String!] -) on SCHEMA | OBJECT | FIELD_DEFINITION - type ArtemisChannel { branch: String! releases: Int! @@ -41,15 +14,25 @@ type Artifact { artifactId: Long! deltaFileInfo: ArtifactFileInfo! fileInfo: ArtifactFileInfo! + id: UUID! platform: Platform! } type ArtifactFileInfo { downloadSize: Long! downloads: Long! + id: UUID! md5Hash: String } +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + type Mutation { updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload! } @@ -74,6 +57,7 @@ type PublishedReleasesConnection { nodes: [Release!] "Information to aid in pagination." pageInfo: PageInfo! + "Identifies the total count of items in the connection." totalCount: Int! } @@ -90,7 +74,7 @@ type Query { channels: [ArtemisChannel!]! nextPublishedRelease(branch: String!, platform: Platform!, version: String): Release publishedChannels: [String!]! - publishedRelease(id: String!): Release + publishedRelease(id: UUID!): Release publishedReleases( "Returns the elements in the list that come after the specified cursor." after: String, @@ -103,20 +87,9 @@ type Query { order: [ReleaseSortInput!], where: ReleaseFilterInput ): PublishedReleasesConnection - release(id: String!): Release + release(id: UUID!): Release releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]! - releases( - "Returns the elements in the list that come after the specified cursor." - after: String, - "Returns the elements in the list that come before the specified cursor." - before: String, - "Returns the first _n_ elements from the list." - first: Int, - "Returns the last _n_ elements from the list." - last: Int, - order: [ReleaseSortInput!], - where: ReleaseFilterInput - ): ReleasesConnection + releases(order: [ReleaseSortInput!], skip: Int, take: Int, where: ReleaseFilterInput): ReleasesCollectionSegment } type Release { @@ -125,9 +98,9 @@ type Release { changelog: String! commit: String! createdAt: DateTime! - id: String! + id: UUID! isDraft: Boolean! - previousRelease: String + previousRelease: Release version: String! workflowRunId: Long! } @@ -136,30 +109,20 @@ type ReleaseStatistic { count: Int! lastReportedUsage: DateTime! linuxCount: Int! - oSXCount: Int! - releaseId: String! + osxCount: Int! + releaseId: UUID! windowsCount: Int! } -"A connection to a list of items." -type ReleasesConnection { - "A list of edges." - edges: [ReleasesEdge!] - "A flattened list of the nodes." - nodes: [Release!] +"A segment of a collection." +type ReleasesCollectionSegment { + "A flattened list of the items." + items: [Release!] "Information to aid in pagination." - pageInfo: PageInfo! + pageInfo: CollectionSegmentInfo! totalCount: Int! } -"An edge in a connection." -type ReleasesEdge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Release! -} - type UpdateReleaseChangelogPayload { release: Release } @@ -167,6 +130,7 @@ type UpdateReleaseChangelogPayload { enum ApplyPolicy { AFTER_RESOLVER BEFORE_RESOLVER + VALIDATION } enum Platform { @@ -186,19 +150,23 @@ scalar DateTime "The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." scalar Long +scalar UUID + input ArtifactFileInfoFilterInput { and: [ArtifactFileInfoFilterInput!] - downloadSize: ComparableInt64OperationFilterInput - downloads: ComparableInt64OperationFilterInput + downloadSize: LongOperationFilterInput + downloads: LongOperationFilterInput + id: UuidOperationFilterInput md5Hash: StringOperationFilterInput or: [ArtifactFileInfoFilterInput!] } input ArtifactFilterInput { and: [ArtifactFilterInput!] - artifactId: ComparableInt64OperationFilterInput + artifactId: LongOperationFilterInput deltaFileInfo: ArtifactFileInfoFilterInput fileInfo: ArtifactFileInfoFilterInput + id: UuidOperationFilterInput or: [ArtifactFilterInput!] platform: PlatformOperationFilterInput } @@ -208,51 +176,36 @@ input BooleanOperationFilterInput { neq: Boolean } -input ComparableDateTimeOffsetOperationFilterInput { +input DateTimeOperationFilterInput { eq: DateTime gt: DateTime gte: DateTime - in: [DateTime!] + in: [DateTime] lt: DateTime lte: DateTime neq: DateTime ngt: DateTime ngte: DateTime - nin: [DateTime!] + nin: [DateTime] nlt: DateTime nlte: DateTime } -input ComparableInt32OperationFilterInput { +input IntOperationFilterInput { eq: Int gt: Int gte: Int - in: [Int!] + in: [Int] lt: Int lte: Int neq: Int ngt: Int ngte: Int - nin: [Int!] + nin: [Int] nlt: Int nlte: Int } -input ComparableInt64OperationFilterInput { - eq: Long - gt: Long - gte: Long - in: [Long!] - lt: Long - lte: Long - neq: Long - ngt: Long - ngte: Long - nin: [Long!] - nlt: Long - nlte: Long -} - input ListFilterInputTypeOfArtifactFilterInput { all: ArtifactFilterInput any: Boolean @@ -260,6 +213,21 @@ input ListFilterInputTypeOfArtifactFilterInput { some: ArtifactFilterInput } +input LongOperationFilterInput { + eq: Long + gt: Long + gte: Long + in: [Long] + lt: Long + lte: Long + neq: Long + ngt: Long + ngte: Long + nin: [Long] + nlt: Long + nlte: Long +} + input PlatformOperationFilterInput { eq: Platform in: [Platform!] @@ -273,13 +241,13 @@ input ReleaseFilterInput { branch: StringOperationFilterInput changelog: StringOperationFilterInput commit: StringOperationFilterInput - createdAt: ComparableDateTimeOffsetOperationFilterInput - id: StringOperationFilterInput + createdAt: DateTimeOperationFilterInput + id: UuidOperationFilterInput isDraft: BooleanOperationFilterInput or: [ReleaseFilterInput!] - previousRelease: StringOperationFilterInput + previousRelease: ReleaseFilterInput version: StringOperationFilterInput - workflowRunId: ComparableInt64OperationFilterInput + workflowRunId: LongOperationFilterInput } input ReleaseSortInput { @@ -289,27 +257,27 @@ input ReleaseSortInput { createdAt: SortEnumType id: SortEnumType isDraft: SortEnumType - previousRelease: SortEnumType + previousRelease: ReleaseSortInput version: SortEnumType workflowRunId: SortEnumType } input ReleaseStatisticFilterInput { and: [ReleaseStatisticFilterInput!] - count: ComparableInt32OperationFilterInput - lastReportedUsage: ComparableDateTimeOffsetOperationFilterInput - linuxCount: ComparableInt32OperationFilterInput - oSXCount: ComparableInt32OperationFilterInput + count: IntOperationFilterInput + lastReportedUsage: DateTimeOperationFilterInput + linuxCount: IntOperationFilterInput or: [ReleaseStatisticFilterInput!] - releaseId: StringOperationFilterInput - windowsCount: ComparableInt32OperationFilterInput + osxCount: IntOperationFilterInput + releaseId: UuidOperationFilterInput + windowsCount: IntOperationFilterInput } input ReleaseStatisticSortInput { count: SortEnumType lastReportedUsage: SortEnumType linuxCount: SortEnumType - oSXCount: SortEnumType + osxCount: SortEnumType releaseId: SortEnumType windowsCount: SortEnumType } @@ -331,6 +299,21 @@ input StringOperationFilterInput { input UpdateReleaseChangelogInput { changelog: String! - id: String! + id: UUID! isDraft: Boolean! } + +input UuidOperationFilterInput { + eq: UUID + gt: UUID + gte: UUID + in: [UUID] + lt: UUID + lte: UUID + neq: UUID + ngt: UUID + ngte: UUID + nin: [UUID] + nlt: UUID + nlte: UUID +} From 148eb99870cf845024247a27ea2c697c9d3d8bdf Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 6 Mar 2023 22:21:40 +0100 Subject: [PATCH 21/34] Determine update channel during update service initialize --- src/Artemis.UI/Services/Updating/UpdateService.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 70c53e3f5..246277d94 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -45,12 +45,6 @@ public class UpdateService : IUpdateService _updateNotificationProvider = updateNotificationProvider; _getReleaseInstaller = getReleaseInstaller; - string? channelArgument = Constants.StartupArguments.FirstOrDefault(a => a.StartsWith("--channel=")); - if (channelArgument != null) - Channel = channelArgument.Split("=")[1]; - if (string.IsNullOrWhiteSpace(Channel)) - Channel = "master"; - if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; else if (OperatingSystem.IsLinux()) @@ -124,7 +118,7 @@ public class UpdateService : IUpdateService } /// - public string Channel { get; } + public string Channel { get; private set; } = "master"; /// public string? PreviousVersion { get; private set; } @@ -190,6 +184,12 @@ public class UpdateService : IUpdateService /// public bool Initialize() { + string? channelArgument = Constants.StartupArguments.FirstOrDefault(a => a.StartsWith("--channel=")); + if (channelArgument != null) + Channel = channelArgument.Split("=")[1]; + if (string.IsNullOrWhiteSpace(Channel)) + Channel = "master"; + // There should never be an installing folder if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "installing"))) { From 0e32bb6b619df2b61d5575ac18cb50c88e1fcefb Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 9 Mar 2023 21:35:12 +0100 Subject: [PATCH 22/34] Output install log to file --- .../Repositories/ReleaseRepository.cs | 7 +- .../ApplicationStateManager.cs | 24 ++++--- .../WindowsUpdateNotificationProvider.cs | 68 ++++++++++++------- src/Artemis.UI.Windows/Scripts/update.ps1 | 26 ++++--- src/Artemis.UI/DryIoc/ContainerExtensions.cs | 2 +- .../Settings/Tabs/ReleasesTabViewModel.cs | 4 +- ....cs => BasicUpdateNotificationProvider.cs} | 64 +++++++++++------ .../Updating/IUpdateNotificationProvider.cs | 2 +- .../Services/Updating/UpdateService.cs | 15 +++- 9 files changed, 139 insertions(+), 73 deletions(-) rename src/Artemis.UI/Services/Updating/{InAppUpdateNotificationProvider.cs => BasicUpdateNotificationProvider.cs} (56%) diff --git a/src/Artemis.Storage/Repositories/ReleaseRepository.cs b/src/Artemis.Storage/Repositories/ReleaseRepository.cs index 3516c6c80..c5c18ae27 100644 --- a/src/Artemis.Storage/Repositories/ReleaseRepository.cs +++ b/src/Artemis.Storage/Repositories/ReleaseRepository.cs @@ -15,13 +15,14 @@ public class ReleaseRepository : IReleaseRepository _repository.Database.GetCollection().EnsureIndex(s => s.Version, true); } - public void SaveVersionInstallDate(string version) + public bool SaveVersionInstallDate(string version) { ReleaseEntity release = _repository.Query().Where(r => r.Version == version).FirstOrDefault(); if (release != null) - return; + return false; _repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow}); + return true; } public ReleaseEntity GetPreviousInstalledVersion() @@ -32,6 +33,6 @@ public class ReleaseRepository : IReleaseRepository public interface IReleaseRepository : IRepository { - void SaveVersionInstallDate(string version); + bool SaveVersionInstallDate(string version); ReleaseEntity GetPreviousInstalledVersion(); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 05435e5ac..966324b7a 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -99,18 +99,12 @@ public class ApplicationStateManager argsList.Add("--autorun"); // Retain startup arguments after update by providing them to the script - string script = $"\"{Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1")}\""; + string script = Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1"); string source = $"-sourceDirectory \"{Path.Combine(Constants.UpdatingFolder, "installing")}\""; string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; - // Run the PowerShell script included in the new version, that way any changes made to the script are used - ProcessStartInfo info = new() - { - Arguments = $"-File {script} {source} {destination} {args}", - FileName = "PowerShell.exe" - }; - Process.Start(info); + RunScriptWithOutputFile(script, $"{source} {destination} {args}", Path.Combine(Constants.DataFolder, "update-log.txt")); // Lets try a graceful shutdown, PowerShell will kill if needed if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) @@ -142,6 +136,20 @@ public class ApplicationStateManager Process.Start(info); } + private void RunScriptWithOutputFile(string script, string arguments, string outputFile) + { + // Use > for files that are bigger than 200kb to start fresh, otherwise use >> to append + string redirectSymbol = File.Exists(outputFile) && new FileInfo(outputFile).Length > 200000 ? ">" : ">>"; + ProcessStartInfo info = new() + { + Arguments = $"PowerShell -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", + FileName = "PowerShell.exe", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + }; + Process.Start(info); + } + [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs index 3b434360d..7af9d6927 100644 --- a/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs +++ b/src/Artemis.UI.Windows/Providers/WindowsUpdateNotificationProvider.cs @@ -35,30 +35,12 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; } - private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e) - { - ToastArguments args = ToastArguments.Parse(e.Argument); - Guid releaseId = Guid.Parse(args.Get("releaseId")); - string releaseVersion = args.Get("releaseVersion"); - string action = "view-changes"; - if (args.Contains("action")) - action = args.Get("action"); - - if (action == "install") - await InstallRelease(releaseId, releaseVersion); - else if (action == "view-changes") - ViewRelease(releaseId); - else if (action == "cancel") - _cancellationTokenSource?.Cancel(); - else if (action == "restart-for-update") - _updateService.RestartForUpdate(false); - } - + /// public void ShowNotification(Guid releaseId, string releaseVersion) { GetBuilderForRelease(releaseId, releaseVersion) .AddText("Update available") - .AddText($"Artemis version {releaseVersion} has been released") + .AddText($"Artemis {releaseVersion} has been released") .AddButton(new ToastButton() .SetContent("Install") .AddArgument("action", "install").SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate)) @@ -66,14 +48,24 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider .Show(t => t.Tag = releaseId.ToString()); } - private void ViewRelease(Guid releaseId) + /// + public void ShowInstalledNotification(string installedVersion) + { + new ToastContentBuilder().AddArgument("releaseVersion", installedVersion) + .AddText("Update installed") + .AddText($"Artemis {installedVersion} has been installed") + .AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes")) + .Show(); + } + + private void ViewRelease(string releaseVersion) { Dispatcher.UIThread.Post(() => { _mainWindowService.OpenMainWindow(); if (_mainWindowService.HostScreen == null) return; - + // TODO: When proper routing has been implemented, use that here // Create a settings VM to navigate to SettingsViewModel settingsViewModel = _getSettingsViewModel(_mainWindowService.HostScreen); @@ -83,7 +75,7 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider // Navigate to the settings VM _mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel); // Navigate to the release tab - releaseTabViewModel.PreselectId = releaseId; + releaseTabViewModel.PreselectVersion = releaseVersion; settingsViewModel.SelectedTab = releaseTabViewModel; }); } @@ -128,10 +120,18 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider installer.PropertyChanged -= InstallerOnPropertyChanged; } + // If the main window is not open the user isn't busy, restart straight away + if (!_mainWindowService.IsMainWindowOpen) + { + _updateService.RestartForUpdate(true); + return; + } + + // Ask for a restart because the user is actively using Artemis GetBuilderForRelease(releaseId, releaseVersion) .AddAudio(new ToastAudio {Silent = true}) .AddText("Update ready") - .AddText($"Artemis version {releaseVersion} is ready to be applied") + .AddText("Artemis must restart to finish the update") .AddButton(new ToastButton().SetContent("Restart Artemis").AddArgument("action", "restart-for-update")) .AddButton(new ToastButton().SetContent("Later").AddArgument("action", "postpone-update")) .Show(t => t.Tag = releaseId.ToString()); @@ -160,4 +160,24 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider return data; } + + private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e) + { + ToastArguments args = ToastArguments.Parse(e.Argument); + + Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty; + string releaseVersion = args.Get("releaseVersion"); + string action = "view-changes"; + if (args.Contains("action")) + action = args.Get("action"); + + if (action == "install") + await InstallRelease(releaseId, releaseVersion); + else if (action == "view-changes") + ViewRelease(releaseVersion); + else if (action == "cancel") + _cancellationTokenSource?.Cancel(); + else if (action == "restart-for-update") + _updateService.RestartForUpdate(false); + } } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 index 26fbd1165..d78fa8358 100644 --- a/src/Artemis.UI.Windows/Scripts/update.ps1 +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -1,17 +1,16 @@ param ( - [Parameter(Mandatory=$true)][string]$sourceDirectory, - [Parameter(Mandatory=$true)][string]$destinationDirectory, - [Parameter(Mandatory=$false)][string]$artemisArgs + [Parameter(Mandatory = $true)][string]$sourceDirectory, + [Parameter(Mandatory = $true)][string]$destinationDirectory, + [Parameter(Mandatory = $false)][string]$artemisArgs ) Write-Host "Artemis update script v1" -Write-Host "Please do not close this window, this should not take long" -Write-Host "" # Wait up to 10 seconds for the process to shut down -for ($i=1; $i -le 10; $i++) { +for ($i = 1; $i -le 10; $i++) { $process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue - if (!$process) { + if (!$process) + { break } Write-Host "Waiting for Artemis to shut down ($i / 10)" @@ -20,13 +19,15 @@ for ($i=1; $i -le 10; $i++) { # If the process is still running, kill it $process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue -if ($process) { +if ($process) +{ Stop-Process -Id $process.Id -Force Start-Sleep -Seconds 1 } # Check if the destination directory exists -if (!(Test-Path $destinationDirectory)) { +if (!(Test-Path $destinationDirectory)) +{ Write-Error "The destination directory does not exist" } @@ -44,8 +45,11 @@ Write-Host "Finished! Restarting Artemis" Start-Sleep -Seconds 1 # When finished, run the updated version -if ($artemisArgs) { +if ($artemisArgs) +{ Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs -} else { +} +else +{ Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory } \ No newline at end of file diff --git a/src/Artemis.UI/DryIoc/ContainerExtensions.cs b/src/Artemis.UI/DryIoc/ContainerExtensions.cs index cd7b167a5..9a4095b20 100644 --- a/src/Artemis.UI/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI/DryIoc/ContainerExtensions.cs @@ -36,7 +36,7 @@ public static class ContainerExtensions container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); - container.Register(); + container.Register(); container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), Reuse.Singleton); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs index 8d4c54f1c..de7f7cdd5 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs @@ -54,14 +54,14 @@ public class ReleasesTabViewModel : ActivatableViewModelBase { await _updateService.CacheLatestRelease(); await GetMoreReleases(d.AsCancellationToken()); - SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.ReleaseId == PreselectId) ?? ReleaseViewModels.FirstOrDefault(); + SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.Version == PreselectVersion) ?? ReleaseViewModels.FirstOrDefault(); }); } public ReadOnlyObservableCollection ReleaseViewModels { get; } public string Channel { get; } - public Guid? PreselectId { get; set; } + public string? PreselectVersion { get; set; } public ReleaseViewModel? SelectedReleaseViewModel { diff --git a/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs similarity index 56% rename from src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs rename to src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs index 46a06a2ea..6d0335ad7 100644 --- a/src/Artemis.UI/Services/Updating/InAppUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/BasicUpdateNotificationProvider.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Threading.Tasks; using Artemis.UI.Screens.Settings; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; @@ -9,35 +8,67 @@ using ReactiveUI; namespace Artemis.UI.Services.Updating; -public class InAppUpdateNotificationProvider : IUpdateNotificationProvider +public class BasicUpdateNotificationProvider : IUpdateNotificationProvider { private readonly Func _getSettingsViewModel; private readonly IMainWindowService _mainWindowService; private readonly INotificationService _notificationService; - private Action? _notification; + private Action? _available; + private Action? _installed; - public InAppUpdateNotificationProvider(INotificationService notificationService, IMainWindowService mainWindowService, Func getSettingsViewModel) + public BasicUpdateNotificationProvider(INotificationService notificationService, IMainWindowService mainWindowService, Func getSettingsViewModel) { _notificationService = notificationService; _mainWindowService = mainWindowService; _getSettingsViewModel = getSettingsViewModel; } - private void ShowInAppNotification(Guid releaseId, string releaseVersion) + /// + public void ShowNotification(Guid releaseId, string releaseVersion) { - _notification?.Invoke(); - _notification = _notificationService.CreateNotification() + if (_mainWindowService.IsMainWindowOpen) + ShowAvailable(releaseVersion); + else + _mainWindowService.MainWindowOpened += (_, _) => ShowAvailable(releaseVersion); + } + + /// + public void ShowInstalledNotification(string installedVersion) + { + if (_mainWindowService.IsMainWindowOpen) + ShowInstalled(installedVersion); + else + _mainWindowService.MainWindowOpened += (_, _) => ShowInstalled(installedVersion); + } + + private void ShowAvailable(string releaseVersion) + { + _available?.Invoke(); + _available = _notificationService.CreateNotification() .WithTitle("Update available") - .WithMessage($"Artemis version {releaseVersion} has been released") + .WithMessage($"Artemis {releaseVersion} has been released") .WithSeverity(NotificationSeverity.Success) .WithTimeout(TimeSpan.FromSeconds(15)) - .HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(releaseId))) + .HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(releaseVersion))) .Show(); } - private void ViewRelease(Guid releaseId) + private void ShowInstalled(string installedVersion) { - _notification?.Invoke(); + _installed?.Invoke(); + _installed = _notificationService.CreateNotification() + .WithTitle("Update installed") + .WithMessage($"Artemis {installedVersion} has been installed.") + .WithSeverity(NotificationSeverity.Success) + .WithTimeout(TimeSpan.FromSeconds(15)) + .HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(installedVersion))) + .Show(); + } + + private void ViewRelease(string version) + { + _installed?.Invoke(); + _available?.Invoke(); if (_mainWindowService.HostScreen == null) return; @@ -51,16 +82,7 @@ public class InAppUpdateNotificationProvider : IUpdateNotificationProvider // Navigate to the settings VM _mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel); // Navigate to the release tab - releaseTabViewModel.PreselectId = releaseId; + releaseTabViewModel.PreselectVersion = version; settingsViewModel.SelectedTab = releaseTabViewModel; } - - /// - public void ShowNotification(Guid releaseId, string releaseVersion) - { - if (_mainWindowService.IsMainWindowOpen) - ShowInAppNotification(releaseId, releaseVersion); - else - _mainWindowService.MainWindowOpened += (_, _) => ShowInAppNotification(releaseId, releaseVersion); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs index 5f77faf60..0dc3bf356 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateNotificationProvider.cs @@ -1,9 +1,9 @@ using System; -using System.Threading.Tasks; namespace Artemis.UI.Services.Updating; public interface IUpdateNotificationProvider { void ShowNotification(Guid releaseId, string releaseVersion); + void ShowInstalledNotification(string installedVersion); } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 246277d94..e264d9431 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -66,7 +66,7 @@ public class UpdateService : IUpdateService private void ProcessReleaseStatus() { string currentVersion = Constants.CurrentVersion; - _releaseRepository.SaveVersionInstallDate(currentVersion); + bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion); PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version; if (!Directory.Exists(Constants.UpdatingFolder)) @@ -88,6 +88,9 @@ public class UpdateService : IUpdateService _logger.Warning(e, "Failed to clean up old update file at {FilePath}", file); } } + + // if (updated) + _updateNotificationProvider.Value.ShowInstalledNotification(currentVersion); } private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release) @@ -194,7 +197,15 @@ public class UpdateService : IUpdateService if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "installing"))) { _logger.Warning("Cleaning up leftover installing folder, did an update go wrong?"); - Directory.Delete(Path.Combine(Constants.UpdatingFolder, "installing")); + try + { + Directory.Delete(Path.Combine(Constants.UpdatingFolder, "installing"), true); + } + catch (Exception e) + { + _logger.Error(e, "Failed to delete leftover installing folder"); + } + } // If an update is pending, don't bother with anything else From 9e8017e17a460140d39e703f98b88069d94f7e7b Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 Mar 2023 09:45:26 +0100 Subject: [PATCH 23/34] Plugins - Made built-in plugins copy code more resilient to different archive structures --- .../Services/PluginManagementService.cs | 95 +++++++++++-------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index e313ca59d..92cb4d23b 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -47,15 +47,29 @@ internal class PluginManagementService : IPluginManagementService private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) { + ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json"); DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory)); bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); // Remove the old directory if it exists if (Directory.Exists(pluginDirectory.FullName)) pluginDirectory.DeleteRecursively(); - Directory.CreateDirectory(pluginDirectory.FullName); - - zipArchive.ExtractToDirectory(pluginDirectory.FullName, true); + + // Extract everything in the same archive directory to the unique plugin directory + Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); + string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); + foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) + { + if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/")) + { + string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length)); + // Create folders + Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!); + // Extract files + zipArchiveEntry.ExtractToFile(target); + } + } + if (createLockFile) File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close(); } @@ -82,6 +96,7 @@ internal class PluginManagementService : IPluginManagementService return; } + foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip")) { try @@ -98,56 +113,57 @@ internal class PluginManagementService : IPluginManagementService private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory) { // Find the metadata file in the zip - using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); - ZipArchiveEntry? metaDataFileEntry = archive.GetEntry("plugin.json"); - if (metaDataFileEntry == null) - throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName); + using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); - using StreamReader reader = new(metaDataFileEntry.Open()); - PluginInfo builtInPluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; - string preferred = builtInPluginInfo.PreferredPluginDirectory; + ZipArchiveEntry? metaDataFileEntry = archive.Entries.FirstOrDefault(e => e.Name == "plugin.json"); + if (metaDataFileEntry == null) + throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName); - // Find the matching plugin in the plugin folder - DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred); - if (match == null) + using StreamReader reader = new(metaDataFileEntry.Open()); + PluginInfo builtInPluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; + string preferred = builtInPluginInfo.PreferredPluginDirectory; + + // Find the matching plugin in the plugin folder + DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred); + if (match == null) + { + CopyBuiltInPlugin(archive, preferred); + } + else + { + string metadataFile = Path.Combine(match.FullName, "plugin.json"); + if (!File.Exists(metadataFile)) { + _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); CopyBuiltInPlugin(archive, preferred); } else { - string metadataFile = Path.Combine(match.FullName, "plugin.json"); - if (!File.Exists(metadataFile)) + PluginInfo pluginInfo; + try { - _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); + // Compare versions, copy if the same when debugging + pluginInfo = CoreJson.DeserializeObject(File.ReadAllText(metadataFile))!; } - else + catch (Exception e) { - PluginInfo pluginInfo; - try - { - // Compare versions, copy if the same when debugging - pluginInfo = CoreJson.DeserializeObject(File.ReadAllText(metadataFile))!; - } - catch (Exception e) - { - throw new ArtemisPluginException($"Failed read plugin metadata needed to install built-in plugin: {e.Message}", e); - } + throw new ArtemisPluginException($"Failed read plugin metadata needed to install built-in plugin: {e.Message}", e); + } - try + try + { + if (builtInPluginInfo.Version > pluginInfo.Version) { - if (builtInPluginInfo.Version > pluginInfo.Version) - { - _logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); - } - } - catch (Exception e) - { - throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e); + _logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo); + CopyBuiltInPlugin(archive, preferred); } } + catch (Exception e) + { + throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e); + } } + } } #endregion @@ -209,7 +225,7 @@ internal class PluginManagementService : IPluginManagementService // Disposal happens manually before container disposal but the container doesn't know that so a 2nd call will be made if (_disposed) return; - + _disposed = true; UnloadPlugins(); } @@ -568,7 +584,6 @@ internal class PluginManagementService : IPluginManagementService if (metaDataFileEntry == null) throw new ArtemisPluginException("Couldn't find a plugin.json in " + fileName); - using StreamReader reader = new(metaDataFileEntry.Open()); PluginInfo pluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!; if (!pluginInfo.Main.EndsWith(".dll")) From a2e20b38214fe7e3439a3b026ba73ffe5dfd02cf Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 Mar 2023 09:55:38 +0100 Subject: [PATCH 24/34] Clean up web client --- .../Artemis.WebClient.Updating.csproj | 1 - .../Queries/GetNextRelease.graphql | 6 ++++ .../Queries/GetReleaseById.graphql | 34 +------------------ .../Queries/GetReleases.graphql | 22 ++++++++++++ 4 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 src/Artemis.WebClient.Updating/Queries/GetNextRelease.graphql create mode 100644 src/Artemis.WebClient.Updating/Queries/GetReleases.graphql diff --git a/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj b/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj index 7f2bb002f..f087e1de1 100644 --- a/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj +++ b/src/Artemis.WebClient.Updating/Artemis.WebClient.Updating.csproj @@ -12,5 +12,4 @@ - diff --git a/src/Artemis.WebClient.Updating/Queries/GetNextRelease.graphql b/src/Artemis.WebClient.Updating/Queries/GetNextRelease.graphql new file mode 100644 index 000000000..6ec87cc1b --- /dev/null +++ b/src/Artemis.WebClient.Updating/Queries/GetNextRelease.graphql @@ -0,0 +1,6 @@ +query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) { + nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) { + id + version + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql index e364038ff..584445b52 100644 --- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -1,27 +1,3 @@ -query GetReleases($branch: String!, $platform: Platform!, $take: Int!, $after: String) { - publishedReleases( - first: $take - after: $after - where: { - and: [ - { branch: { eq: $branch } } - { artifacts: { some: { platform: { eq: $platform } } } } - ] - } - ) { - pageInfo { - hasNextPage - endCursor - } - nodes { - id - version - createdAt - } - } -} - - query GetReleaseById($id: UUID!) { publishedRelease(id: $id) { branch @@ -47,12 +23,4 @@ query GetReleaseById($id: UUID!) { fragment fileInfo on ArtifactFileInfo { md5Hash downloadSize -} - -query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) { - nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) { - id - version - } -} - +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleases.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleases.graphql new file mode 100644 index 000000000..0297b3f39 --- /dev/null +++ b/src/Artemis.WebClient.Updating/Queries/GetReleases.graphql @@ -0,0 +1,22 @@ +query GetReleases($branch: String!, $platform: Platform!, $take: Int!, $after: String) { + publishedReleases( + first: $take + after: $after + where: { + and: [ + { branch: { eq: $branch } } + { artifacts: { some: { platform: { eq: $platform } } } } + ] + } + ) { + pageInfo { + hasNextPage + endCursor + } + nodes { + id + version + createdAt + } + } +} \ No newline at end of file From d8390d306c5d5a29ad15bf88ddf630204cee7925 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 Mar 2023 15:00:28 +0100 Subject: [PATCH 25/34] Update built-in plugins by build date not by version --- src/Artemis.Core/Constants.cs | 2 +- src/Artemis.Core/Plugins/PluginInfo.cs | 4 ++-- .../Services/PluginManagementService.cs | 24 ++++--------------- .../Services/Updating/UpdateService.cs | 2 +- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 252d3b85a..fd0300579 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -81,7 +81,7 @@ public static class Constants /// public static readonly PluginInfo CorePluginInfo = new() { - Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = new Version(2, 0) + Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = CurrentVersion }; /// diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 43b8319c0..6421749f8 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -25,7 +25,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject private Plugin _plugin = null!; private Uri? _repository; private bool _requiresAdmin; - private Version _version = null!; + private string _version = null!; private Uri? _website; internal PluginInfo() @@ -107,7 +107,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject /// The version of the plugin /// [JsonProperty(Required = Required.Always)] - public Version Version + public string Version { get => _version; internal set => SetAndNotify(ref _version, value); diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 92cb4d23b..06b9b6368 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -54,7 +54,7 @@ internal class PluginManagementService : IPluginManagementService // Remove the old directory if it exists if (Directory.Exists(pluginDirectory.FullName)) pluginDirectory.DeleteRecursively(); - + // Extract everything in the same archive directory to the unique plugin directory Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); @@ -69,7 +69,7 @@ internal class PluginManagementService : IPluginManagementService zipArchiveEntry.ExtractToFile(target); } } - + if (createLockFile) File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close(); } @@ -137,26 +137,12 @@ internal class PluginManagementService : IPluginManagementService _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); CopyBuiltInPlugin(archive, preferred); } - else + else if (metaDataFileEntry.LastWriteTime > File.GetLastWriteTime(metadataFile)) { - PluginInfo pluginInfo; try { - // Compare versions, copy if the same when debugging - pluginInfo = CoreJson.DeserializeObject(File.ReadAllText(metadataFile))!; - } - catch (Exception e) - { - throw new ArtemisPluginException($"Failed read plugin metadata needed to install built-in plugin: {e.Message}", e); - } - - try - { - if (builtInPluginInfo.Version > pluginInfo.Version) - { - _logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); - } + _logger.Debug("Copying updated built-in plugin {builtInPluginInfo}", builtInPluginInfo); + CopyBuiltInPlugin(archive, preferred); } catch (Exception e) { diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index e264d9431..a492ee96d 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -89,7 +89,7 @@ public class UpdateService : IUpdateService } } - // if (updated) + if (updated) _updateNotificationProvider.Value.ShowInstalledNotification(currentVersion); } From 8dee02e6fd2ce29134e8f258b448cb8b11a00186 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 Mar 2023 15:51:33 +0100 Subject: [PATCH 26/34] When updating, bypass execution policy for scripts Fix getting stuck on Initializing Core if updating fails --- src/Artemis.UI.Windows/ApplicationStateManager.cs | 2 +- src/Artemis.UI.Windows/Scripts/update.ps1 | 13 +++++++------ src/Artemis.UI/Services/Updating/UpdateService.cs | 15 +++++++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 966324b7a..e3d9ddba1 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -142,7 +142,7 @@ public class ApplicationStateManager string redirectSymbol = File.Exists(outputFile) && new FileInfo(outputFile).Length > 200000 ? ">" : ">>"; ProcessStartInfo info = new() { - Arguments = $"PowerShell -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", + Arguments = $"PowerShell -ExecutionPolicy Bypass -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", FileName = "PowerShell.exe", WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 index d78fa8358..6d3fa3aca 100644 --- a/src/Artemis.UI.Windows/Scripts/update.ps1 +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -26,20 +26,21 @@ if ($process) } # Check if the destination directory exists -if (!(Test-Path $destinationDirectory)) +if (!(Test-Path "$destinationDirectory")) { Write-Error "The destination directory does not exist" + Exit 1 } # Clear the destination directory but don't remove it, leaving ACL entries in tact Write-Host "Cleaning up old version where needed" -Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force +Get-ChildItem "$destinationDirectory" | Remove-Item -Recurse -Force # Move the contents of the source directory to the destination directory Write-Host "Installing new files" -Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory +Get-ChildItem "$sourceDirectory" | Move-Item -Destination "$destinationDirectory" # Remove the now empty source directory -Remove-Item $sourceDirectory +Remove-Item "$sourceDirectory" Write-Host "Finished! Restarting Artemis" Start-Sleep -Seconds 1 @@ -47,9 +48,9 @@ Start-Sleep -Seconds 1 # When finished, run the updated version if ($artemisArgs) { - Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory "$destinationDirectory" -ArgumentList $artemisArgs } else { - Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory "$destinationDirectory" } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index a492ee96d..b417a49c8 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -88,7 +88,7 @@ public class UpdateService : IUpdateService _logger.Warning(e, "Failed to clean up old update file at {FilePath}", file); } } - + if (updated) _updateNotificationProvider.Value.ShowInstalledNotification(currentVersion); } @@ -205,15 +205,22 @@ public class UpdateService : IUpdateService { _logger.Error(e, "Failed to delete leftover installing folder"); } - } // If an update is pending, don't bother with anything else if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending"))) { _logger.Information("Installing pending update"); - RestartForUpdate(true); - return true; + try + { + RestartForUpdate(true); + return true; + } + catch (Exception e) + { + _logger.Warning(e, "Failed to apply pending update"); + return false; + } } ProcessReleaseStatus(); From 319b84c9493c9790ccbaa928fdf130b478e5796c Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 Mar 2023 21:51:38 +0100 Subject: [PATCH 27/34] Fix updating with directories with whitespaces --- src/Artemis.UI.Windows/ApplicationStateManager.cs | 8 ++++---- src/Artemis.UI.Windows/Scripts/update.ps1 | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index e3d9ddba1..c3b2eb3a6 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -100,9 +100,9 @@ public class ApplicationStateManager // Retain startup arguments after update by providing them to the script string script = Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1"); - string source = $"-sourceDirectory \"{Path.Combine(Constants.UpdatingFolder, "installing")}\""; - string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; - string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; + string source = $"-sourceDirectory \"'{Path.Combine(Constants.UpdatingFolder, "installing")}'\""; + string destination = $"-destinationDirectory \"'{Constants.ApplicationFolder}'\""; + string args = argsList.Any() ? $"-artemisArgs \"'{string.Join(',', argsList)}'\"" : ""; RunScriptWithOutputFile(script, $"{source} {destination} {args}", Path.Combine(Constants.DataFolder, "update-log.txt")); @@ -142,7 +142,7 @@ public class ApplicationStateManager string redirectSymbol = File.Exists(outputFile) && new FileInfo(outputFile).Length > 200000 ? ">" : ">>"; ProcessStartInfo info = new() { - Arguments = $"PowerShell -ExecutionPolicy Bypass -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", + Arguments = $"-ExecutionPolicy Bypass -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", FileName = "PowerShell.exe", WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 index 6d3fa3aca..607032109 100644 --- a/src/Artemis.UI.Windows/Scripts/update.ps1 +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -26,21 +26,21 @@ if ($process) } # Check if the destination directory exists -if (!(Test-Path "$destinationDirectory")) +if (!(Test-Path $destinationDirectory)) { - Write-Error "The destination directory does not exist" + Write-Error "The destination directory at $destinationDirectory does not exist" Exit 1 } # Clear the destination directory but don't remove it, leaving ACL entries in tact Write-Host "Cleaning up old version where needed" -Get-ChildItem "$destinationDirectory" | Remove-Item -Recurse -Force +Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force # Move the contents of the source directory to the destination directory Write-Host "Installing new files" -Get-ChildItem "$sourceDirectory" | Move-Item -Destination "$destinationDirectory" +Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory # Remove the now empty source directory -Remove-Item "$sourceDirectory" +Remove-Item $sourceDirectory Write-Host "Finished! Restarting Artemis" Start-Sleep -Seconds 1 @@ -48,9 +48,9 @@ Start-Sleep -Seconds 1 # When finished, run the updated version if ($artemisArgs) { - Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory "$destinationDirectory" -ArgumentList $artemisArgs + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs } else { - Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory "$destinationDirectory" + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory } \ No newline at end of file From c6b14813b70d49afb58308c40ba55d784bd21ef7 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 Mar 2023 22:19:36 +0100 Subject: [PATCH 28/34] Fixed an oopsie --- .../Shared/DataModelVisualizationViewModel.cs | 2 +- src/Artemis.UI.Windows/ApplicationStateManager.cs | 2 +- src/Artemis.UI.Windows/Properties/launchSettings.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs index 249b422c4..59ceef437 100644 --- a/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs +++ b/src/Artemis.UI.Shared/DataModelVisualization/Shared/DataModelVisualizationViewModel.cs @@ -194,7 +194,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa } if (looseMatch) - IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) || + IsMatchingFilteredTypes = filteredTypes.Any(t => t!.IsCastableFrom(type) || (t == typeof(Enum) && type.IsEnum) || (t == typeof(IEnumerable<>) && type.IsGenericEnumerable()) || (type.IsGenericType && t == type.GetGenericTypeDefinition())); diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index c3b2eb3a6..531eeb870 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -142,7 +142,7 @@ public class ApplicationStateManager string redirectSymbol = File.Exists(outputFile) && new FileInfo(outputFile).Length > 200000 ? ">" : ">>"; ProcessStartInfo info = new() { - Arguments = $"-ExecutionPolicy Bypass -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", + Arguments = $"PowerShell -ExecutionPolicy Bypass -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"", FileName = "PowerShell.exe", WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, diff --git a/src/Artemis.UI.Windows/Properties/launchSettings.json b/src/Artemis.UI.Windows/Properties/launchSettings.json index 62ce0eaf5..a1588c6f9 100644 --- a/src/Artemis.UI.Windows/Properties/launchSettings.json +++ b/src/Artemis.UI.Windows/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Artemis.UI.Windows": { "commandName": "Project", - "commandLineArgs": "--force-elevation --disable-forced-shutdown --pcmr --channel=feature/gh-actions" + "commandLineArgs": "--force-elevation --disable-forced-shutdown --pcmr" } } } \ No newline at end of file From e216737c5037a58cd2f8f2d1590ee0eb54710d7b Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Mar 2023 11:40:30 +0200 Subject: [PATCH 29/34] Fix auto installing updates, set default of auto install to true --- .../Screens/Settings/Tabs/GeneralTabViewModel.cs | 2 +- .../Screens/StartupWizard/StartupWizardViewModel.cs | 2 +- src/Artemis.UI/Services/Updating/UpdateService.cs | 13 ++++++++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index b4db42619..5121af0f5 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -147,7 +147,7 @@ public class GeneralTabViewModel : ActivatableViewModelBase public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true); public PluginSetting ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information); public PluginSetting CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index 4ef72e32f..99d0eb719 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -87,7 +87,7 @@ public class StartupWizardViewModel : DialogViewModelBase public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true); public int CurrentStep { diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index b417a49c8..79bb98ebd 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -24,6 +24,7 @@ public class UpdateService : IUpdateService private readonly Func _getReleaseInstaller; private readonly ILogger _logger; + private readonly IMainWindowService _mainWindowService; private readonly IReleaseRepository _releaseRepository; private readonly Lazy _updateNotificationProvider; private readonly Platform _updatePlatform; @@ -40,6 +41,7 @@ public class UpdateService : IUpdateService Func getReleaseInstaller) { _logger = logger; + _mainWindowService = mainWindowService; _updatingClient = updatingClient; _releaseRepository = releaseRepository; _updateNotificationProvider = updateNotificationProvider; @@ -55,7 +57,7 @@ public class UpdateService : IUpdateService throw new PlatformNotSupportedException("Cannot auto update on the current platform"); _autoCheck = settingsService.GetSetting("UI.Updating.AutoCheck", true); - _autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", false); + _autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", true); _autoCheck.SettingChanged += HandleAutoUpdateEvent; mainWindowService.MainWindowOpened += HandleAutoUpdateEvent; Timer timer = new(UPDATE_CHECK_INTERVAL); @@ -102,7 +104,7 @@ public class UpdateService : IUpdateService { ReleaseInstaller installer = _getReleaseInstaller(release.Id); await installer.InstallAsync(CancellationToken.None); - Utilities.ApplyUpdate(true); + RestartForUpdate(true); } private async void HandleAutoUpdateEvent(object? sender, EventArgs e) @@ -156,11 +158,12 @@ public class UpdateService : IUpdateService if (CachedLatestRelease == null) return false; - // Only offer it once per session - _suspendAutoCheck = true; + // Unless auto install is enabled, only offer it once per session + if (!_autoInstall.Value) + _suspendAutoCheck = true; // If the window is open show the changelog, don't auto-update while the user is busy - if (!_autoInstall.Value) + if (_mainWindowService.IsMainWindowOpen || !_autoInstall.Value) ShowUpdateNotification(CachedLatestRelease); else await AutoInstallUpdate(CachedLatestRelease); From 4c380734315e1214657c8e83a4fba15b0fab2d7f Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Mar 2023 11:52:33 +0200 Subject: [PATCH 30/34] Remove unused models --- src/Artemis.UI.Windows/Models/DevOpsBuilds.cs | 278 ------------------ .../Models/GitHubDifference.cs | 243 --------------- 2 files changed, 521 deletions(-) delete mode 100644 src/Artemis.UI.Windows/Models/DevOpsBuilds.cs delete mode 100644 src/Artemis.UI.Windows/Models/GitHubDifference.cs diff --git a/src/Artemis.UI.Windows/Models/DevOpsBuilds.cs b/src/Artemis.UI.Windows/Models/DevOpsBuilds.cs deleted file mode 100644 index 8bff29ad5..000000000 --- a/src/Artemis.UI.Windows/Models/DevOpsBuilds.cs +++ /dev/null @@ -1,278 +0,0 @@ -#nullable disable - -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Artemis.UI.Windows.Models; - -public class DevOpsBuilds -{ - [JsonProperty("count")] - public long Count { get; set; } - - [JsonProperty("value")] - public List Builds { get; set; } -} - -public class DevOpsBuild -{ - [JsonProperty("_links")] - public BuildLinks Links { get; set; } - - [JsonProperty("properties")] - public Properties Properties { get; set; } - - [JsonProperty("tags")] - public List Tags { get; set; } - - [JsonProperty("validationResults")] - public List ValidationResults { get; set; } - - [JsonProperty("plans")] - public List Plans { get; set; } - - [JsonProperty("triggerInfo")] - public TriggerInfo TriggerInfo { get; set; } - - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("buildNumber")] - public string BuildNumber { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("result")] - public string Result { get; set; } - - [JsonProperty("queueTime")] - public DateTimeOffset QueueTime { get; set; } - - [JsonProperty("startTime")] - public DateTimeOffset StartTime { get; set; } - - [JsonProperty("finishTime")] - public DateTimeOffset FinishTime { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("definition")] - public Definition Definition { get; set; } - - [JsonProperty("buildNumberRevision")] - public long BuildNumberRevision { get; set; } - - [JsonProperty("project")] - public Project Project { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - - [JsonProperty("sourceBranch")] - public string SourceBranch { get; set; } - - [JsonProperty("sourceVersion")] - public string SourceVersion { get; set; } - - [JsonProperty("priority")] - public string Priority { get; set; } - - [JsonProperty("reason")] - public string Reason { get; set; } - - [JsonProperty("requestedFor")] - public LastChangedBy RequestedFor { get; set; } - - [JsonProperty("requestedBy")] - public LastChangedBy RequestedBy { get; set; } - - [JsonProperty("lastChangedDate")] - public DateTimeOffset LastChangedDate { get; set; } - - [JsonProperty("lastChangedBy")] - public LastChangedBy LastChangedBy { get; set; } - - [JsonProperty("orchestrationPlan")] - public Plan OrchestrationPlan { get; set; } - - [JsonProperty("logs")] - public Logs Logs { get; set; } - - [JsonProperty("repository")] - public Repository Repository { get; set; } - - [JsonProperty("keepForever")] - public bool KeepForever { get; set; } - - [JsonProperty("retainedByRelease")] - public bool RetainedByRelease { get; set; } - - [JsonProperty("triggeredByBuild")] - public object TriggeredByBuild { get; set; } -} - -public class Definition -{ - [JsonProperty("drafts")] - public List Drafts { get; set; } - - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("uri")] - public string Uri { get; set; } - - [JsonProperty("path")] - public string Path { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("queueStatus")] - public string QueueStatus { get; set; } - - [JsonProperty("revision")] - public long Revision { get; set; } - - [JsonProperty("project")] - public Project Project { get; set; } -} - -public class Project -{ - [JsonProperty("id")] - public Guid Id { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("state")] - public string State { get; set; } - - [JsonProperty("revision")] - public long Revision { get; set; } - - [JsonProperty("visibility")] - public string Visibility { get; set; } - - [JsonProperty("lastUpdateTime")] - public DateTimeOffset LastUpdateTime { get; set; } -} - -public class LastChangedBy -{ - [JsonProperty("displayName")] - public string DisplayName { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("_links")] - public LastChangedByLinks Links { get; set; } - - [JsonProperty("id")] - public Guid Id { get; set; } - - [JsonProperty("uniqueName")] - public object UniqueName { get; set; } - - [JsonProperty("imageUrl")] - public object ImageUrl { get; set; } - - [JsonProperty("descriptor")] - public string Descriptor { get; set; } -} - -public class LastChangedByLinks -{ - [JsonProperty("avatar")] - public Badge Avatar { get; set; } -} - -public class Badge -{ - [JsonProperty("href")] - public Uri Href { get; set; } -} - -public class BuildLinks -{ - [JsonProperty("self")] - public Badge Self { get; set; } - - [JsonProperty("web")] - public Badge Web { get; set; } - - [JsonProperty("sourceVersionDisplayUri")] - public Badge SourceVersionDisplayUri { get; set; } - - [JsonProperty("timeline")] - public Badge Timeline { get; set; } - - [JsonProperty("badge")] - public Badge Badge { get; set; } -} - -public class Logs -{ - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } -} - -public class Plan -{ - [JsonProperty("planId")] - public Guid PlanId { get; set; } -} - -public class Properties -{ -} - -public class Repository -{ - [JsonProperty("id")] - public string Id { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("clean")] - public object Clean { get; set; } - - [JsonProperty("checkoutSubmodules")] - public bool CheckoutSubmodules { get; set; } -} - -public class TriggerInfo -{ - [JsonProperty("ci.sourceBranch")] - public string CiSourceBranch { get; set; } - - [JsonProperty("ci.sourceSha")] - public string CiSourceSha { get; set; } - - [JsonProperty("ci.message")] - public string CiMessage { get; set; } - - [JsonProperty("ci.triggerRepository")] - public string CiTriggerRepository { get; set; } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Models/GitHubDifference.cs b/src/Artemis.UI.Windows/Models/GitHubDifference.cs deleted file mode 100644 index 8a32d0d77..000000000 --- a/src/Artemis.UI.Windows/Models/GitHubDifference.cs +++ /dev/null @@ -1,243 +0,0 @@ -#nullable disable -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Artemis.UI.Windows.Models; - -public class GitHubDifference -{ - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("html_url")] - public Uri HtmlUrl { get; set; } - - [JsonProperty("permalink_url")] - public Uri PermalinkUrl { get; set; } - - [JsonProperty("diff_url")] - public Uri DiffUrl { get; set; } - - [JsonProperty("patch_url")] - public Uri PatchUrl { get; set; } - - [JsonProperty("base_commit")] - public BaseCommitClass BaseCommit { get; set; } - - [JsonProperty("merge_base_commit")] - public BaseCommitClass MergeBaseCommit { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("ahead_by")] - public long AheadBy { get; set; } - - [JsonProperty("behind_by")] - public long BehindBy { get; set; } - - [JsonProperty("total_commits")] - public long TotalCommits { get; set; } - - [JsonProperty("commits")] - public List Commits { get; set; } - - [JsonProperty("files")] - public List Files { get; set; } -} - -public class BaseCommitClass -{ - [JsonProperty("sha")] - public string Sha { get; set; } - - [JsonProperty("node_id")] - public string NodeId { get; set; } - - [JsonProperty("commit")] - public BaseCommitCommit Commit { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("html_url")] - public Uri HtmlUrl { get; set; } - - [JsonProperty("comments_url")] - public Uri CommentsUrl { get; set; } - - [JsonProperty("author")] - public BaseCommitAuthor Author { get; set; } - - [JsonProperty("committer")] - public BaseCommitAuthor Committer { get; set; } - - [JsonProperty("parents")] - public List Parents { get; set; } -} - -public class BaseCommitAuthor -{ - [JsonProperty("login")] - public string Login { get; set; } - - [JsonProperty("id")] - public long Id { get; set; } - - [JsonProperty("node_id")] - public string NodeId { get; set; } - - [JsonProperty("avatar_url")] - public Uri AvatarUrl { get; set; } - - [JsonProperty("gravatar_id")] - public string GravatarId { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("html_url")] - public Uri HtmlUrl { get; set; } - - [JsonProperty("followers_url")] - public Uri FollowersUrl { get; set; } - - [JsonProperty("following_url")] - public string FollowingUrl { get; set; } - - [JsonProperty("gists_url")] - public string GistsUrl { get; set; } - - [JsonProperty("starred_url")] - public string StarredUrl { get; set; } - - [JsonProperty("subscriptions_url")] - public Uri SubscriptionsUrl { get; set; } - - [JsonProperty("organizations_url")] - public Uri OrganizationsUrl { get; set; } - - [JsonProperty("repos_url")] - public Uri ReposUrl { get; set; } - - [JsonProperty("events_url")] - public string EventsUrl { get; set; } - - [JsonProperty("received_events_url")] - public Uri ReceivedEventsUrl { get; set; } - - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("site_admin")] - public bool SiteAdmin { get; set; } -} - -public class BaseCommitCommit -{ - [JsonProperty("author")] - public PurpleAuthor Author { get; set; } - - [JsonProperty("committer")] - public PurpleAuthor Committer { get; set; } - - [JsonProperty("message")] - public string Message { get; set; } - - [JsonProperty("tree")] - public Tree Tree { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("comment_count")] - public long CommentCount { get; set; } - - [JsonProperty("verification")] - public Verification Verification { get; set; } -} - -public class PurpleAuthor -{ - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("email")] - public string Email { get; set; } - - [JsonProperty("date")] - public DateTimeOffset Date { get; set; } -} - -public class Tree -{ - [JsonProperty("sha")] - public string Sha { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } -} - -public class Verification -{ - [JsonProperty("verified")] - public bool Verified { get; set; } - - [JsonProperty("reason")] - public string Reason { get; set; } - - [JsonProperty("signature")] - public string Signature { get; set; } - - [JsonProperty("payload")] - public string Payload { get; set; } -} - -public class Parent -{ - [JsonProperty("sha")] - public string Sha { get; set; } - - [JsonProperty("url")] - public Uri Url { get; set; } - - [JsonProperty("html_url")] - public Uri HtmlUrl { get; set; } -} - -public class File -{ - [JsonProperty("sha")] - public string Sha { get; set; } - - [JsonProperty("filename")] - public string Filename { get; set; } - - [JsonProperty("status")] - public string Status { get; set; } - - [JsonProperty("additions")] - public long Additions { get; set; } - - [JsonProperty("deletions")] - public long Deletions { get; set; } - - [JsonProperty("changes")] - public long Changes { get; set; } - - [JsonProperty("blob_url")] - public Uri BlobUrl { get; set; } - - [JsonProperty("raw_url")] - public Uri RawUrl { get; set; } - - [JsonProperty("contents_url")] - public Uri ContentsUrl { get; set; } - - [JsonProperty("patch")] - public string Patch { get; set; } - - [JsonProperty("previous_filename", NullValueHandling = NullValueHandling.Ignore)] - public string PreviousFilename { get; set; } -} \ No newline at end of file From 76f7d9c1577bd54b5e6f1749c4d5a79e63bd4112 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Mar 2023 12:17:23 +0200 Subject: [PATCH 31/34] Ensure auto-update checks happen on startup even if the UI never opens --- .../Services/Updating/UpdateService.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 79bb98ebd..fee0b93b0 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -31,6 +31,7 @@ public class UpdateService : IUpdateService private readonly IUpdatingClient _updatingClient; private bool _suspendAutoCheck; + private DateTime _lastAutoUpdateCheck; public UpdateService(ILogger logger, ISettingsService settingsService, @@ -109,6 +110,11 @@ public class UpdateService : IUpdateService private async void HandleAutoUpdateEvent(object? sender, EventArgs e) { + // The event can trigger from multiple sources with a timer acting as a fallback, only actual perform an action once per max 59 minutes + if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59)) + return; + _lastAutoUpdateCheck = DateTime.UtcNow; + if (!_autoCheck.Value || _suspendAutoCheck) return; @@ -148,6 +154,8 @@ public class UpdateService : IUpdateService /// public async Task CheckForUpdate() { + _logger.Information("Performing auto-update check"); + IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform); result.EnsureNoErrors(); @@ -161,12 +169,18 @@ public class UpdateService : IUpdateService // Unless auto install is enabled, only offer it once per session if (!_autoInstall.Value) _suspendAutoCheck = true; - + // If the window is open show the changelog, don't auto-update while the user is busy if (_mainWindowService.IsMainWindowOpen || !_autoInstall.Value) + { + _logger.Information("New update available, offering version {AvailableVersion}", CachedLatestRelease.Version); ShowUpdateNotification(CachedLatestRelease); + } else + { + _logger.Information("New update available, auto-installing version {AvailableVersion}", CachedLatestRelease.Version); await AutoInstallUpdate(CachedLatestRelease); + } return true; } @@ -227,6 +241,10 @@ public class UpdateService : IUpdateService } ProcessReleaseStatus(); + + // Trigger the auto update event so that it doesn't take an hour for the first check to happen + HandleAutoUpdateEvent(this, EventArgs.Empty); + _logger.Information("Update service initialized for {Channel} channel", Channel); return false; } From 834212737f32cc8a4ba6472798c216e7ebf3a6b4 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Mar 2023 12:32:37 +0200 Subject: [PATCH 32/34] Release details - Don't show exception message on cancel --- src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs index bce5e0181..bb640c1d3 100644 --- a/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs @@ -199,6 +199,10 @@ public class ReleaseViewModel : ActivatableViewModelBase _retrievedDetails = true; } + catch (TaskCanceledException) + { + // ignored + } catch (Exception e) { _logger.Warning(e, "Failed to retrieve release details"); From 494c5b405419e316fad1404bb35efe387d74381f Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Mar 2023 13:05:19 +0200 Subject: [PATCH 33/34] Don't show UI when silently updating --- .../ApplicationStateManager.cs | 4 ++-- src/Artemis.UI/Screens/Root/RootViewModel.cs | 20 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index 531eeb870..fd32bf74b 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -95,8 +95,8 @@ public class ApplicationStateManager private void UtilitiesOnUpdateRequested(object? sender, UpdateEventArgs e) { List argsList = new(StartupArguments); - if (e.Silent) - argsList.Add("--autorun"); + if (e.Silent && !argsList.Contains("--minimized")) + argsList.Add("--minimized"); // Retain startup arguments after update by providing them to the script string script = Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1"); diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 373fd22f1..d8abba43a 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -46,7 +46,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi { Router = new RoutingState(); WindowSizeSetting = settingsService.GetSetting("WindowSize"); - + _coreService = coreService; _settingsService = settingsService; _windowService = windowService; @@ -56,17 +56,17 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi _defaultTitleBarViewModel = defaultTitleBarViewModel; _sidebarVmFactory = sidebarVmFactory; _lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!; - + mainWindowService.ConfigureMainWindowProvider(this); mainWindowService.HostScreen = this; - + DisplayAccordingToSettings(); Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel); Task.Run(() => { if (_updateService.Initialize()) return; - + coreService.Initialize(); registrationService.RegisterBuiltInDataModelDisplays(); registrationService.RegisterBuiltInDataModelInputs(); @@ -110,14 +110,10 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi bool showOnAutoRun = _settingsService.GetSetting("UI.ShowOnStartup", true).Value; if ((autoRunning && !showOnAutoRun) || minimized) - { - // TODO: Auto-update - } - else - { - ShowSplashScreen(); - _coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow); - } + return; + + ShowSplashScreen(); + _coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow); } private void ShowSplashScreen() From 173b04101a74a8a2461409e4acf710c3591afd6b Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 26 Mar 2023 14:09:30 +0200 Subject: [PATCH 34/34] Fix args not being passed on correctly --- src/Artemis.UI.Windows/ApplicationStateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index fd32bf74b..2d13fe49e 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -102,7 +102,7 @@ public class ApplicationStateManager string script = Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1"); string source = $"-sourceDirectory \"'{Path.Combine(Constants.UpdatingFolder, "installing")}'\""; string destination = $"-destinationDirectory \"'{Constants.ApplicationFolder}'\""; - string args = argsList.Any() ? $"-artemisArgs \"'{string.Join(',', argsList)}'\"" : ""; + string args = argsList.Any() ? $"-artemisArgs \"'{string.Join(' ', argsList)}'\"" : ""; RunScriptWithOutputFile(script, $"{source} {destination} {args}", Path.Combine(Constants.DataFolder, "update-log.txt"));