diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 370847ac38..131d5fb850 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -30,6 +30,7 @@ AFX AGGREGATABLE AHybrid AKV +akv ALarger ALLAPPS ALLINPUT @@ -728,6 +729,7 @@ ISettings IShell isocpp iss +issecret ISSEPARATOR ITask ith @@ -946,6 +948,7 @@ mrw msc mscorlib msdata +MSDL msedge MSGFLT msiexec @@ -1138,6 +1141,7 @@ pch pchast PCIDLIST PCWSTR +pdbs pdisp pdo pdto @@ -1203,6 +1207,7 @@ ppv prc Prefixer Preinstalled +prependpath prevhost previewer PREVIEWHANDLERFRAMEINFO @@ -1552,7 +1557,7 @@ Stubless STYLECHANGED STYLECHANGING subkeys -SUBLANG +sublang subquery Superbar sut @@ -1565,6 +1570,7 @@ SWC SWFO SWP SWRESTORE +symbolrequestprod SYMCACHE SYMED SYMOPT @@ -1640,6 +1646,7 @@ TOUCHEVENTF TOUCHINPUT touchpad tracelogging +trafficmanager traies transicc TRAYMOUSEMESSAGE @@ -1861,6 +1868,7 @@ workarounds WORKSPACESEDITOR WORKSPACESLAUNCHER WORKSPACESSNAPSHOTTOOL +WORKSPACESWINDOWARRANGER wox wparam wpf diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index ed5bd325a7..57a22ffd57 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -194,6 +194,7 @@ "PowerToys.WorkspacesSnapshotTool.exe", "PowerToys.WorkspacesLauncher.exe", + "PowerToys.WorkspacesWindowArranger.exe", "PowerToys.WorkspacesEditor.exe", "PowerToys.WorkspacesEditor.dll", "PowerToys.WorkspacesLauncherUI.exe", diff --git a/.pipelines/ci/caching.yml b/.pipelines/ci/caching.yml deleted file mode 100644 index b802c9efae..0000000000 --- a/.pipelines/ci/caching.yml +++ /dev/null @@ -1,41 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/main/service-schema.json -trigger: - batch: true - branches: - include: - - main - - stable - paths: - exclude: - - doc/* - - temp/* - - tools/* - - '**.md' - -pr: - branches: - include: - - main - - stable - paths: - exclude: - - '**.md' - - doc - -# 0.0.yyMM.dd## -# 0.0.1904.0900 -name: 0.0.$(Date:yyMM).$(Date:dd)$(Rev:rr) - -variables: - EnablePipelineCache: true - -jobs: - - template: ./templates/build-powertoys-precheck.yml - - template: ./templates/build-powertoys-ci.yml - parameters: - platform: x64 - enableCaching: true - - template: ./templates/build-powertoys-ci.yml - parameters: - platform: arm64 - enableCaching: true \ No newline at end of file diff --git a/.pipelines/ci/ci.yml b/.pipelines/ci/ci.yml deleted file mode 100644 index e746e2afd2..0000000000 --- a/.pipelines/ci/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/main/service-schema.json -trigger: - batch: true - branches: - include: - - main - - stable - paths: - exclude: - - doc/* - - temp/* - - tools/* - - '**.md' - -pr: - branches: - include: - - main - - stable - paths: - exclude: - - '**.md' - - doc - -# 0.0.yyMM.dd## -# 0.0.1904.0900 -name: 0.0.$(Date:yyMM).$(Date:dd)$(Rev:rr) - -variables: - EnablePipelineCache: true - -jobs: - - template: ./templates/build-powertoys-precheck.yml - - template: ./templates/build-powertoys-ci.yml - parameters: - platform: x64 - ${{ if eq(variables['System.PullRequest.IsFork'], 'False') }}: - enableCaching: true - - template: ./templates/build-powertoys-ci.yml - parameters: - platform: arm64 - ${{ if eq(variables['System.PullRequest.IsFork'], 'False') }}: - enableCaching: true -# - template: ./templates/run-ui-tests-ci.yml -# parameters: -# platform: x64 diff --git a/.pipelines/ci/templates/build-powertoys-ci.yml b/.pipelines/ci/templates/build-powertoys-ci.yml deleted file mode 100644 index 177313e117..0000000000 --- a/.pipelines/ci/templates/build-powertoys-ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -parameters: - - name: configuration - type: string - default: 'Release' - - name: platform - type: string - default: 'x64' - - name: additionalBuildArguments - type: string - default: '-p:RestorePackagesConfig=true -m' - - name: enableCaching - type: boolean - default: false - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} - dependsOn: Precheck - condition: and(succeeded(),ne(dependencies.Precheck.outputs['verifyBuildRequest.skipBuild'], 'Yes')) - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - NUGET_RESTORE_MSBUILD_ARGS: /p:Platform=${{ parameters.platform }} # Required for nuget to work due to self contained - NODE_OPTIONS: --max_old_space_size=16384 - pool: - demands: ImageOverride -equals SHINE-VS17-Latest - ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: - name: SHINE-INT-L - ${{ else }}: - name: SHINE-OSS-L - timeoutInMinutes: 120 - strategy: - maxParallel: 10 - steps: - - template: build-powertoys-steps.yml - parameters: - additionalBuildArguments: ${{ parameters.additionalBuildArguments }} - enableCaching: ${{ parameters.enableCaching }} - - # It appears that the Component Governance build task that gets automatically injected stopped working - # when we renamed our main branch. - - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 - displayName: 'Component Detection' - condition: and(succeededOrFailed(), not(eq(variables['Build.Reason'], 'PullRequest'))) \ No newline at end of file diff --git a/.pipelines/ci/templates/build-powertoys-installer.yml b/.pipelines/ci/templates/build-powertoys-installer.yml deleted file mode 100644 index 1756171cf2..0000000000 --- a/.pipelines/ci/templates/build-powertoys-installer.yml +++ /dev/null @@ -1,20 +0,0 @@ -parameters: - configuration: 'Release' - platform: '' - additionalBuildArguments: '/p:RestorePackagesConfig=true -m' - -jobs: -- job: Build${{ parameters.platform }}${{ parameters.configuration }} - displayName: Build ${{ parameters.platform }} ${{ parameters.configuration }} - variables: - BuildConfiguration: ${{ parameters.configuration }} - BuildPlatform: ${{ parameters.platform }} - pool: - name: SHINE-INT-L - timeoutInMinutes: 120 - strategy: - maxParallel: 10 - steps: - - template: build-powertoys-steps.yml - parameters: - additionalBuildArguments: ${{ parameters.additionalBuildArguments }} diff --git a/.pipelines/ci/templates/build-powertoys-precheck.yml b/.pipelines/ci/templates/build-powertoys-precheck.yml deleted file mode 100644 index 7601289652..0000000000 --- a/.pipelines/ci/templates/build-powertoys-precheck.yml +++ /dev/null @@ -1,38 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/master/service-schema.json -jobs: -- job: Precheck - pool: - demands: ImageOverride -equals SHINE-VS17-Latest - ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: - name: SHINE-INT-L - ${{ else }}: - name: SHINE-OSS-L - steps: - - checkout: none - - - task: PowerShell@2 - displayName: Verify Build Request - inputs: - targetType: 'inline' - script: | - try { - # Try based on pull request first - $pullRequestNumber = "$(system.pullRequest.pullRequestNumber)"; - $gitHubPullRequest = Invoke-RestMethod -Method Get "https://api.github.com/repos/microsoft/PowerToys/pulls/$pullRequestNumber/files" - # If there are no files updated in the commit that are .md, set skipBuild variable - if(([array]($gitHubPullRequest.filename) -notmatch ".md|.txt").Length -eq 0) { - Write-Host '##vso[task.setvariable variable=skipBuild;isOutput=true]Yes' - Write-Host 'Skipping Build' - } - } - catch { - # Fall back to the latest commit otherwise. - $commit = "$(build.sourceVersion)"; - $gitHubCommit = Invoke-RestMethod -Method Get "https://api.github.com/repos/microsoft/PowerToys/commits/$commit" - if(([array]($githubCommit.files.filename) -notmatch ".md|.txt").Length -eq 0) { - Write-Host '##vso[task.setvariable variable=skipBuild;isOutput=true]Yes' - Write-Host 'Skipping Build' - } - } - pwsh: true - name: verifyBuildRequest diff --git a/.pipelines/ci/templates/build-powertoys-steps.yml b/.pipelines/ci/templates/build-powertoys-steps.yml deleted file mode 100644 index 2aa1390c4d..0000000000 --- a/.pipelines/ci/templates/build-powertoys-steps.yml +++ /dev/null @@ -1,297 +0,0 @@ -parameters: - - name: additionalBuildArguments - type: string - default: '' - - name: enableCaching - type: boolean - default: false - -steps: -- checkout: self - fetchDepth: 1 - submodules: true - clean: true - -- task: UseDotNet@2 - displayName: 'Use .NET 6 SDK' - inputs: - packageType: sdk - version: '6.x' - -- task: PowerShell@2 - displayName: Verify XAML formatting - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\applyXamlStyling.ps1' - arguments: -Passive - pwsh: true - -- task: PowerShell@2 - displayName: Verify Nuget package versions for PowerToys.sln - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyNugetPackages.ps1' - arguments: -solution '$(build.sourcesdirectory)\PowerToys.sln' - pwsh: true - -- task: PowerShell@2 - displayName: Verify Arm64 configuration for PowerToys.sln - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyArm64Configuration.ps1' - arguments: -solution '$(build.sourcesdirectory)\PowerToys.sln' - pwsh: true - -- task: PowerShell@2 - displayName: Verify Arm64 configuration for BugReportTool.sln - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyArm64Configuration.ps1' - arguments: -solution '$(build.sourcesdirectory)\tools\BugReportTool\BugReportTool.sln' - pwsh: true - -- task: PowerShell@2 - displayName: Verify Arm64 configuration for WebcamReportTool.sln - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyArm64Configuration.ps1' - arguments: -solution '$(build.sourcesdirectory)\tools\WebcamReportTool\WebcamReportTool.sln' - pwsh: true - -- task: PowerShell@2 - displayName: Verify Arm64 configuration for StylesReportTool.sln - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyArm64Configuration.ps1' - arguments: -solution '$(build.sourcesdirectory)\tools\StylesReportTool\StylesReportTool.sln' - pwsh: true - -- task: PowerShell@2 - displayName: Verify Arm64 configuration for PowerToysSetup.sln - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyArm64Configuration.ps1' - arguments: -solution '$(build.sourcesdirectory)\installer\PowerToysSetup.sln' - pwsh: true - -- task: PowerShell@2 - displayName: Verify and set latest VCToolsVersion usage - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyAndSetLatestVCToolsVersion.ps1' - pwsh: true - -- task: UseDotNet@2 - displayName: 'Use .NET 8 SDK' - inputs: - packageType: sdk - version: '8.x' - includePreviewVersions: true - -- task: VisualStudioTestPlatformInstaller@1 - displayName: Ensure VSTest Platform - -- task: Cache@2 - displayName: 'Cache nuget packages (PackageReference)' - inputs: - key: '"PackageReference" | "$(Agent.OS)" | Directory.Packages.props' - restoreKeys: | - "PackageReference" | "$(Agent.OS)" - "PackageReference" - path: $(NUGET_PACKAGES) - -- task: Cache@2 - displayName: 'Cache nuget packages (packages.config)' - inputs: - key: '"packages.config" | "$(Agent.OS)" | **/packages.config' - restoreKeys: | - "packages.config" | "$(Agent.OS)" - "packages.config" - path: packages - -- ${{ if eq(parameters.enableCaching, true) }}: - - task: NuGetToolInstaller@1 - displayName: Install NuGet - - - script: nuget restore packages.config -SolutionDirectory . - displayName: 'nuget restore packages.config' - -- task: VSBuild@1 - displayName: 'Build and Test PowerToys.sln' - inputs: - solution: '**\PowerToys.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - ${{ if eq(parameters.enableCaching, true) }}: - msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} -t:Build;Test -graph -reportfileaccesses -p:MSBuildCacheEnabled=true -p:MSBuildCacheLogDirectory=$(Build.ArtifactStagingDirectory)\logs\MSBuildCache -bl:$(Build.ArtifactStagingDirectory)\logs\PowerToys.binlog -ds:false - ${{ else }}: - msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} -t:Build;Test -graph -bl:$(Build.ArtifactStagingDirectory)\logs\PowerToys.binlog -ds:false - msbuildArchitecture: x64 - maximumCpuCount: true - ${{ if eq(parameters.enableCaching, true) }}: - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - -- task: VSBuild@1 - displayName: 'Build BugReportTool.sln' - inputs: - solution: '**\BugReportTool.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} -graph -bl:$(Build.ArtifactStagingDirectory)\logs\BugReportTool.binlog -ds:false - msbuildArchitecture: x64 - maximumCpuCount: true - -- task: VSBuild@1 - displayName: 'Build WebcamReportTool.sln' - inputs: - solution: '**\WebcamReportTool.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} -graph -bl:$(Build.ArtifactStagingDirectory)\logs\WebcamReportTool.binlog -ds:false - msbuildArchitecture: x64 - maximumCpuCount: true - -- task: VSBuild@1 - displayName: 'Build StylesReportTool.sln' - inputs: - solution: '**\StylesReportTool.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} -graph -bl:$(Build.ArtifactStagingDirectory)\logs\StylesReportTool.binlog -ds:false - msbuildArchitecture: x64 - maximumCpuCount: true - -- task: PowerShell@2 - displayName: Download and install WiX 3.14 development build - inputs: - targetType: filePath - filePath: '$(build.sourcesdirectory)\.pipelines\installWiX.ps1' - -- task: VSBuild@1 - displayName: 'Build PowerToys per-machine MSI' - inputs: - solution: '**\installer\PowerToysSetup.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: /t:PowerToysInstaller -restore ${{ parameters.additionalBuildArguments }} -bl:$(Build.ArtifactStagingDirectory)\logs\PowerToysSetup-PowerToysInstaller.binlog -ds:false - msbuildArchitecture: x64 - maximumCpuCount: true - -- task: VSBuild@1 - displayName: 'Build PowerToys per-machine Bootstrapper' - inputs: - solution: '**\installer\PowerToysSetup.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: /t:PowerToysBootstrapper ${{ parameters.additionalBuildArguments }} -bl:$(Build.ArtifactStagingDirectory)\logs\PowerToysSetup-PowerToysBootstrapper.binlog -ds:false - clean: false - msbuildArchitecture: x64 - maximumCpuCount: true - -- task: PowerShell@2 - displayName: Clean installer dir before building per-user installer - inputs: - targetType: inline - script: git clean -xfd -e *exe -- .\installer\ - pwsh: true - -- task: VSBuild@1 - displayName: 'Build PowerToys per-user MSI' - inputs: - solution: '**\installer\PowerToysSetup.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: /t:PowerToysInstaller -restore ${{ parameters.additionalBuildArguments }} /p:PerUser=true -bl:$(Build.ArtifactStagingDirectory)\logs\PowerToysSetup-PowerToysInstaller-PerUser.binlog -ds:false - msbuildArchitecture: x64 - maximumCpuCount: true - -- task: VSBuild@1 - displayName: 'Build PowerToys per-user Bootstrapper' - inputs: - solution: '**\installer\PowerToysSetup.sln' - vsVersion: 17.0 - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - msbuildArgs: /t:PowerToysBootstrapper ${{ parameters.additionalBuildArguments }} /p:PerUser=true -bl:$(Build.ArtifactStagingDirectory)\logs\PowerToysSetup-PowerToysBootstrapper-PerUser.binlog -ds:false - clean: false - msbuildArchitecture: x64 - maximumCpuCount: true - -# Check if deps.json files don't reference different dll versions. -- task: PowerShell@2 - displayName: Audit deps.json files for all applications - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyDepsJsonLibraryVersions.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)' - pwsh: true - -# Check if asset files on the main application paths are playing nice and avoiding basic conflicts. -- task: PowerShell@2 - displayName: Audit base applications path asset conflicts - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyPossibleAssetConflicts.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)' - pwsh: true - -- task: PowerShell@2 - displayName: Audit WinAppSDK applications path asset conflicts - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyPossibleAssetConflicts.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)\WinUI3Apps' - pwsh: true - -# Publish test results which ran in MSBuild -- task: PublishTestResults@2 - displayName: 'Publish Test Results' - inputs: - testResultsFormat: VSTest - testResultsFiles: '**/*.trx' - condition: ne(variables['BuildPlatform'],'arm64') - -# Native dlls -- task: VSTest@2 - condition: ne(variables['BuildPlatform'],'arm64') # No arm64 agents to run the tests. - displayName: 'Native Tests' - inputs: - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - testSelector: 'testAssemblies' - testAssemblyVer2: | - **\KeyboardManagerEngineTest.dll - **\KeyboardManagerEditorTest.dll - **\UnitTests-CommonLib.dll - **\PowerRenameUnitTests.dll - **\UnitTests-FancyZones.dll - !**\obj\** - -- task: PowerShell@2 - displayName: Trigger dotnet welcome message so that it does not cause errors on other scripts - inputs: - targetType: 'inline' - script: | - dotnet list $(build.sourcesdirectory)\src\common\Common.UI\Common.UI.csproj package - -- task: PowerShell@2 - displayName: Verify Notice.md and Nuget packages match - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyNoticeMdAgainstNugetPackages.ps1' - arguments: -path '$(build.sourcesdirectory)\' - pwsh: true - -- publish: $(Build.ArtifactStagingDirectory)\logs - displayName: Publish Logs - artifact: '$(System.JobDisplayName) logs' - condition: always() - -- task: CopyFiles@2 - displayName: Copy Build Files - condition: and(succeeded(), ne(variables['BuildPlatform'],'arm64')) - inputs: - sourceFolder: '$(Build.SourcesDirectory)' - contents: '$(BuildPlatform)/$(BuildConfiguration)/**/*' - targetFolder: '$(Build.ArtifactStagingDirectory)\$(BuildPlatform)\$(BuildConfiguration)' - -- publish: $(Build.ArtifactStagingDirectory)\$(BuildPlatform)\$(BuildConfiguration) - displayName: Publish Build Artifacts - artifact: build-$(BuildPlatform)-$(BuildConfiguration) - condition: and(succeeded(), ne(variables['BuildPlatform'],'arm64')) diff --git a/.pipelines/ci/templates/run-ui-tests-ci.yml b/.pipelines/ci/templates/run-ui-tests-ci.yml deleted file mode 100644 index e08c08ff7a..0000000000 --- a/.pipelines/ci/templates/run-ui-tests-ci.yml +++ /dev/null @@ -1,70 +0,0 @@ -parameters: - configuration: 'Release' - platform: '' - -jobs: -- job: UITest - displayName: UI Test ${{ parameters.platform }} ${{ parameters.configuration }} - dependsOn: Build${{ parameters.platform }}${{ parameters.configuration }} - variables: - SrcPath: $(Build.Repository.LocalPath) - pool: - ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: - name: SHINE-INT-Testing-x64 - ${{ else }}: - name: SHINE-OSS-Testing-x64 - steps: - - checkout: self - fetchDepth: 1 - submodules: false - clean: true - fetchTags: false - - - download: current - displayName: Download artifacts - artifact: build-${{ parameters.platform }}-${{ parameters.configuration }} - - - task: UseDotNet@2 - displayName: 'Use .NET 6 SDK' - inputs: - packageType: sdk - version: '6.x' - - - task: UseDotNet@2 - displayName: 'Use .NET 8 SDK' - inputs: - packageType: sdk - version: '8.x' - includePreviewVersions: true - - - task: VisualStudioTestPlatformInstaller@1 - displayName: Ensure VSTest Platform - - - task: PowerShell@2 - displayName: Download and install WinAppDriver - inputs: - targetType: filePath - filePath: '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1' - - - task: ScreenResolutionUtility@1 - inputs: - displaySettings: 'optimal' - - - task: VSTest@2 - displayName: 'UI Tests' - condition: and(succeeded(), ne(variables['BuildPlatform'],'arm64')) # No arm64 agents to run the tests. - inputs: - platform: '$(BuildPlatform)' - configuration: '$(BuildConfiguration)' - testSelector: 'testAssemblies' - searchFolder: '$(Pipeline.Workspace)\build-${{ parameters.platform }}-${{ parameters.configuration }}' - vstestLocationMethod: 'location' # otherwise fails to find vstest.console.exe - #vstestLocation: '$(Agent.ToolsDirectory)\VsTest\**\${{ parameters.platform }}\tools\net462\Common7\IDE\Extensions\TestPlatform' - vstestLocation: '$(Agent.ToolsDirectory)\VsTest\17.10.0\x64\tools\net462\Common7\IDE\Extensions\TestPlatform' - uiTests: true - rerunFailedTests: true - testAssemblyVer2: | - **\UITests-FancyZones.dll - **\UITests-FancyZonesEditor.dll - !**\obj\** - !**\ref\** diff --git a/.pipelines/installer-steps.yml b/.pipelines/installer-steps.yml deleted file mode 100644 index 3fe30dfc62..0000000000 --- a/.pipelines/installer-steps.yml +++ /dev/null @@ -1,152 +0,0 @@ -parameters: - - name: versionNumber - type: string - default: "0.0.1" - - name: perUserArg - type: string - default: "false" - - name: buildSubDir - type: string - default: "MachineSetup" - - name: installerPrefix - type: string - default: "PowerToysSetup" - - name: signingParameters - type: object - default: {} - -steps: - - task: VSBuild@1 - displayName: Build PowerToysSetupCustomActions DLL # This dll needs to be build and signed before building the MSI. - inputs: - solution: "**/installer/PowerToysSetup.sln" - vsVersion: 17.0 - msbuildArgs: -restore /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog /t:PowerToysSetupCustomActions /p:RunBuildEvents=true /p:PerUser=${{parameters.perUserArg}} - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: true - maximumCpuCount: true - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign PowerToysSetupCustomActions DLL - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: 'installer/PowerToysSetupCustomActions/$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - - ## INSTALLER START - #### MSI BUILDING AND SIGNING - - task: VSBuild@1 - displayName: Build MSI - inputs: - solution: "**/installer/PowerToysSetup.sln" - vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /p:BuildProjectReferences=false /target:PowerToysInstaller /bl:$(Build.SourcesDirectory)\msbuild.binlog /p:RunBuildEvents=false /p:PerUser=${{parameters.perUserArg}} - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: false # don't undo our hard work above by deleting the CustomActions dll - maximumCpuCount: true - - - task: CmdLine@2 - displayName: "Extracting MSI to verify contents" - inputs: - script: | - "C:\Program Files (x86)\WiX Toolset v3.14\bin\dark.exe" -x $(build.sourcesdirectory)\extractedMsi installer\PowerToysSetup\$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}\${{parameters.installerPrefix}}-${{ parameters.versionNumber }}-$(BuildPlatform).msi - dir $(build.sourcesdirectory)\extractedMsi - - # Check if deps.json files don't reference different dll versions. - - task: PowerShell@2 - displayName: Audit deps.json in MSI extracted files - inputs: - filePath: '.pipelines/verifyDepsJsonLibraryVersions.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\extractedMsi\File' - pwsh: true - - # Did we sign all files - - task: PowerShell@1 - displayName: Verifying entire build is signed and version set - inputs: - scriptName: .pipelines/versionAndSignCheck.ps1 - arguments: -targetDir '$(build.sourcesdirectory)\extractedMsi\File' - - - task: PowerShell@1 - displayName: Verifying MSI Custom Actions DLL is signed - inputs: - scriptName: .pipelines/versionAndSignCheck.ps1 - arguments: -targetDir '$(build.sourcesdirectory)\extractedMsi\Binary' - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign MSI - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: 'installer/PowerToysSetup/$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - #### END MSI - #### BOOTSTRAP BUILDING AND SIGNING - - - task: VSBuild@1 - displayName: Build Bootstrapper - inputs: - solution: "**/installer/PowerToysSetup.sln" - vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog /t:PowerToysBootstrapper /p:PerUser=${{parameters.perUserArg}} - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: false # don't undo our hard work above by deleting the MSI - maximumCpuCount: true - - - task: CmdLine@2 - displayName: "Insignia: Extract Engine from Bundle" - inputs: - script: '"C:\Program Files (x86)\WiX Toolset v3.14\bin\insignia.exe" -ib installer\PowerToysSetup\$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}\${{parameters.installerPrefix}}-${{ parameters.versionNumber }}-$(BuildPlatform).exe -o installer\engine.exe' - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: "ESRP CodeSigning (Engine)" - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: "installer" - Pattern: engine.exe - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "http://www.microsoft.com", - "FileDigest": "/fd \"SHA256\"", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - }, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - - - task: CmdLine@2 - displayName: "Insignia: Merge Engine into Bundle" - inputs: - script: '"C:\Program Files (x86)\WiX Toolset v3.14\bin\insignia.exe" -ab installer\engine.exe installer\PowerToysSetup\$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}\${{parameters.installerPrefix}}-${{ parameters.versionNumber }}-$(BuildPlatform).exe -o installer\PowerToysSetup\$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}\${{parameters.installerPrefix}}-${{ parameters.versionNumber }}-$(BuildPlatform).exe' - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign Bootstrapper - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: 'installer/PowerToysSetup/$(BuildPlatform)\$(BuildConfiguration)\${{parameters.buildSubDir}}' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - #### END BOOTSTRAP - ## END INSTALLER diff --git a/.pipelines/release.yml b/.pipelines/release.yml deleted file mode 100644 index 22a3563c8d..0000000000 --- a/.pipelines/release.yml +++ /dev/null @@ -1,551 +0,0 @@ -# This build should never run as CI or against a pull request. -name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) -trigger: none -pr: none - -resources: - repositories: - - repository: 1ESPipelineTemplates - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - -parameters: - - name: buildConfigurations - type: object - default: - - Release - - name: buildPlatforms - type: object - default: - - x64 - - arm64 - - name: versionNumber - type: string - default: '0.0.1' - - name: signingParameters - type: object - default: - ConnectedServiceName: $(SigningServiceName) - AppRegistrationClientId: $(SigningAppId) - AppRegistrationTenantId: $(SigningTenantId) - AuthAKVName: $(SigningAKVName) - AuthCertName: $(SigningAuthCertName) - AuthSignCertName: $(SigningSignCertName) - -extends: - template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates - parameters: - customBuildTags: - - 1ES.PT.ViaStartRight - pool: - name: SHINE-INT-S - image: SHINE-VS17-Latest - os: windows - sdl: - tsa: - enabled: true - configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json' - - stages: - - stage: build - displayName: Build (Complete) - pool: - name: SHINE-INT-L - image: SHINE-VS17-Latest - os: windows - jobs: - - job: Build - strategy: - matrix: - ${{ each config in parameters.buildConfigurations }}: - ${{ each platform in parameters.buildPlatforms }}: - ${{ config }}_${{ platform }}: - BuildConfiguration: ${{ config }} - BuildPlatform: ${{ platform }} - templateContext: - outputs: - - output: pipelineArtifact - artifactName: setup-$(BuildPlatform) - targetPath: $(Build.ArtifactStagingDirectory) - displayName: Build - timeoutInMinutes: 240 # Some of the 1ES Pipeline stuff and Loc take a very long time - cancelTimeoutInMinutes: 1 - variables: - NUGET_RESTORE_MSBUILD_ARGS: /p:Platform=$(BuildPlatform) # Required for nuget to work due to self contained - NODE_OPTIONS: --max_old_space_size=16384 - IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations - SkipCppCodeAnalysis: 1 # Skip the code analysis to speed up release CI. It runs on PR CI, anyway - # IsExperimentationLive: 1 # The build and installer use this to turn on experimentation - steps: - - checkout: self - clean: true - submodules: true - persistCredentials: True - - # Sets versions for all PowerToy created DLLs - - task: PowerShell@1 - displayName: Set Versions.Prop - inputs: - scriptName: .pipelines/versionSetting.ps1 - arguments: -versionNumber '${{ parameters.versionNumber }}' -DevEnvironment '' - - # ESRP needs 'Microsoft.NETCore.App', version '6.0.0' (x64) - - task: UseDotNet@2 - displayName: 'Use .NET 6 SDK' - inputs: - packageType: sdk - version: '6.x' - - - task: UseDotNet@2 - displayName: 'Use .NET 8 SDK' - inputs: - packageType: sdk - version: '8.x' - - - task: PowerShell@2 - displayName: Verify and set latest VCToolsVersion usage - inputs: - filePath: '$(build.sourcesdirectory)\.pipelines\verifyAndSetLatestVCToolsVersion.ps1' - pwsh: true - - - task: NuGetAuthenticate@1 - - - task: NuGetToolInstaller@1 - displayName: Use NuGet Installer latest - - # this will restore the following nugets: - # - main solution - # - Bug report tool - # - Webcam report tool - # - Installer - # - Bootstrapper Installer - - task: PowerShell@2 - displayName: Download and install WiX 3.14 development build - inputs: - targetType: filePath - filePath: '$(build.sourcesdirectory)\.pipelines\installWiX.ps1' - - - task: MicrosoftTDBuild.tdbuild-task.tdbuild-task.TouchdownBuildTask@3 - displayName: 'Download Localization Files -- PowerToys 37400' - inputs: - teamId: 37400 - TDBuildServiceConnection: $(TouchdownServiceConnection) - authType: SubjectNameIssuer - resourceFilePath: | - **\Resources.resx - **\Resource.resx - **\Resources.resw - appendRelativeDir: true - localizationTarget: false - # pseudoSetting: Included - - - task: PowerShell@2 - displayName: Move Loc files into correct locations - inputs: - targetType: inline - script: >- - $VerbosePreference = "Continue" - - ./tools/build/move-and-rename-resx.ps1 - - ./tools/build/move-uwp-resw.ps1 - pwsh: true - - - task: CmdLine@2 - displayName: Moving telem files - inputs: - script: | - call nuget.exe restore -configFile .pipelines/release-nuget.config -PackagesDirectory . .pipelines/packages.config || exit /b 1 - move /Y "Microsoft.PowerToys.Telemetry.2.0.0\build\include\TraceLoggingDefines.h" "src\common\Telemetry\TraceLoggingDefines.h" || exit /b 1 - move /Y "Microsoft.PowerToys.Telemetry.2.0.0\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs" || exit /b 1 - - ## ALL BUT INSTALLER BUILDING - - task: VSBuild@1 - displayName: Build PowerToys main project - inputs: - solution: '**\PowerToys.sln' - vsVersion: 17.0 - msbuildArgs: -restore -graph /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: true - maximumCpuCount: true - - ### BEGIN SECTION - build and sign nuget packages for abstracted UI utils - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign Utilities libraries - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: 'src/modules' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_abstracted_utils_dll.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - - - task: VSBuild@1 - displayName: Create Hosts File Editor package - inputs: - solution: '**\HostsUILib.csproj' - vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true -t:pack /bl:$(Build.SourcesDirectory)\msbuild.binlog - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Create Environment Variables Editor package - inputs: - solution: '**\EnvironmentVariablesUILib.csproj' - vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true -t:pack /bl:$(Build.SourcesDirectory)\msbuild.binlog - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Create Registry Preview package - inputs: - solution: '**\RegistryPreviewUILib.csproj' - vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true -t:pack /bl:$(Build.SourcesDirectory)\msbuild.binlog - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: CopyFiles@2 - displayName: Copying nuget packages file over - inputs: - contents: "**/bin/Release/PowerToys*.nupkg" - flattenFolders: True - targetFolder: $(Build.ArtifactStagingDirectory)/nupkg - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Submit *.nupkg to ESRP for code signing - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: $(Build.ArtifactStagingDirectory)/nupkg - Pattern: '*.nupkg' - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - ### END SECTION - build and sign nuget packages for abstracted UI utils - - - task: VSBuild@1 - displayName: Build BugReportTool - inputs: - solution: '**/tools/BugReportTool/BugReportTool.sln' - vsVersion: 17.0 - msbuildArgs: -restore -graph /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: true - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Build WebcamReportTool - inputs: - solution: '**/tools/WebcamReportTool/WebcamReportTool.sln' - vsVersion: 17.0 - msbuildArgs: -restore -graph /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: true - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Build StylesReportTool - inputs: - solution: '**/tools/StylesReportTool/StylesReportTool.sln' - vsVersion: 17.0 - msbuildArgs: -restore -graph /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: true - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish Settings for Packaging - inputs: - solution: 'src/settings-ui/Settings.UI/PowerToys.Settings.csproj' - vsVersion: 17.0 - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish Launcher for Packaging - inputs: - solution: 'src/modules/launcher/PowerLauncher/PowerLauncher.csproj' - vsVersion: 17.0 - # The arguments should be the same as the ones for Settings; make sure they are. - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish Monaco Preview Handler for Packaging - inputs: - solution: 'src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandler.csproj' - vsVersion: 17.0 - # The arguments should be the same as the ones for Settings; make sure they are. - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish Markdown Preview Handler for Packaging - inputs: - solution: 'src/modules/previewpane/MarkdownPreviewHandler/MarkdownPreviewHandler.csproj' - vsVersion: 17.0 - # The arguments should be the same as the ones for Settings; make sure they are. - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish Svg Preview Handler for Packaging - inputs: - solution: 'src/modules/previewpane/SvgPreviewHandler/SvgPreviewHandler.csproj' - vsVersion: 17.0 - # The arguments should be the same as the ones for Settings; make sure they are. - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish Svg Thumbnail Provider for Packaging - inputs: - solution: 'src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.csproj' - vsVersion: 17.0 - # The arguments should be the same as the ones for Settings; make sure they are. - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - - task: VSBuild@1 - displayName: Publish File Locksmith UI for Packaging - inputs: - solution: 'src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj' - vsVersion: 17.0 - # The arguments should be the same as the ones for Settings; make sure they are. - msbuildArgs: >- - /target:Publish - /graph - /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never - /p:VCRTForwarders-IncludeDebugCRT=false - /p:PowerToysRoot=$(Build.SourcesDirectory) - /p:PublishProfile=InstallationPublishProfile.pubxml - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - maximumCpuCount: true - - # Check if deps.json files don't reference different dll versions. - - task: PowerShell@2 - displayName: Audit deps.json files for all applications - inputs: - filePath: '.pipelines/verifyDepsJsonLibraryVersions.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)' - pwsh: true - - # Check if asset files on the main application paths are playing nice and avoiding basic conflicts. - - task: PowerShell@2 - displayName: Audit base applications path asset conflicts - inputs: - filePath: '.pipelines/verifyPossibleAssetConflicts.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)' - pwsh: true - - - task: PowerShell@2 - displayName: Audit WinAppSDK applications path asset conflicts - inputs: - filePath: '.pipelines/verifyPossibleAssetConflicts.ps1' - arguments: -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)\WinUI3Apps' - pwsh: true - - #### MAIN SIGNING AREA - # reference https://dev.azure.com/microsoft/Dart/_git/AppDriver?path=/ESRPSigning.json&version=GBarm64-netcore&_a=contents for winappdriver - # https://dev.azure.com/microsoft/Dart/_git/AppDriver?path=/CIPolicy.xml&version=GBarm64-netcore&_a=contents - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign Core PT - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: '$(BuildPlatform)/$(BuildConfiguration)' # Video conf uses x86 and x64. - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_core.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign DSC Powershell files - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: 'src/dsc/Microsoft.PowerToys.Configure' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_DSC.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - - - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@5 - displayName: Sign x86 directshow VCM - inputs: - ${{ insert }}: ${{ parameters.signingParameters }} - FolderPath: 'x86/$(BuildConfiguration)' # Video conf uses x86 and x64. - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_vcm.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - #### END SIGNING - ## END MAIN - - - pwsh: |- - Move-Item msbuild.binlog "$(Build.ArtifactStagingDirectory)/" - displayName: Stage binlog into artifact directory - condition: always() - - - task: ComponentGovernanceComponentDetection@0 - displayName: Component Detection - - - task: CopyFiles@2 - displayName: Copying files for symbols - inputs: - contents: >- - **/*.pdb - flattenFolders: True - targetFolder: $(Build.ArtifactStagingDirectory)/Symbols-$(BuildPlatform)/ - - - task: PowerShell@2 - displayName: 'Remove unneeded files from ArtifactStagingDirectory' - inputs: - targetType: 'inline' - script: | - cd $(Build.ArtifactStagingDirectory)/Symbols-$(BuildPlatform)/ - Remove-Item vc143.pdb - Remove-Item *test* - - - task: PublishSymbols@2 - displayName: Publish symbols path - continueOnError: True - inputs: - SearchPattern: | - $(Build.ArtifactStagingDirectory)/Symbols-$(BuildPlatform)/**/*.* - IndexSources: false - SymbolServerType: TeamServices - - - template: .pipelines/installer-steps.yml@self - parameters: - signingParameters: ${{ parameters.signingParameters }} - versionNumber: ${{ parameters.versionNumber }} - perUserArg: "false" - buildSubDir: "MachineSetup" - installerPrefix: "PowerToysSetup" - - - task: PowerShell@2 - displayName: Clean installer dir before building per-user installer - inputs: - targetType: inline - script: git clean -xfd -e *exe -- .\installer\ - pwsh: true - - - template: .pipelines/installer-steps.yml@self - parameters: - signingParameters: ${{ parameters.signingParameters }} - versionNumber: ${{ parameters.versionNumber }} - perUserArg: "true" - buildSubDir: "UserSetup" - installerPrefix: "PowerToysUserSetup" - - - task: CopyFiles@2 - displayName: Copying setup file over - inputs: - contents: "**/PowerToys*Setup-*.exe" - flattenFolders: True - targetFolder: $(Build.ArtifactStagingDirectory) - - - task: PowerShell@2 - displayName: 'Calculating SHA256 hash' - inputs: - targetType: 'inline' - script: | - $p = "$(System.ArtifactsDirectory)\"; - $staging = "$(Build.ArtifactStagingDirectory)\" - $userHash = ((get-item $p\PowerToysUserSetup*.exe | Get-FileHash).Hash); - $machineHash = ((get-item $p\PowerToysSetup*.exe | Get-FileHash).Hash); - $userPlat = "hash_user_$(BuildPlatform).txt"; - $machinePlat = "hash_machine_$(BuildPlatform).txt"; - $combinedUserPath = $staging + $userPlat; - $combinedMachinePath = $staging + $machinePlat; - - echo $p - - echo $userPlat - echo $userHash - echo $combinedUserPath - - echo $machinePlat - echo $machineHash - echo $combinedMachinePath - - $userHash | out-file -filepath $combinedUserPath - $machineHash | out-file -filepath $combinedMachinePath - pwsh: true - - # Publishing the GPO files - - pwsh: |- - New-Item "$(Build.ArtifactStagingDirectory)/gpo" -Type Directory - Copy-Item src\gpo\assets\* "$(Build.ArtifactStagingDirectory)/gpo" -Recurse - displayName: Stage the GPO files - -... diff --git a/.pipelines/v2/ci.yml b/.pipelines/v2/ci.yml new file mode 100644 index 0000000000..268bea7d11 --- /dev/null +++ b/.pipelines/v2/ci.yml @@ -0,0 +1,46 @@ +trigger: + batch: true + branches: + include: + - main + - stable + paths: + exclude: + - doc/* + - temp/* + - tools/* + - '**.md' + +pr: + branches: + include: + - main + - stable + paths: + exclude: + - '**.md' + - doc + +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + +parameters: + - name: buildPlatforms + type: object + default: + - x64 + - arm64 + - name: enableMsBuildCaching + type: boolean + displayName: "Enable MSBuild Caching" + default: true + - name: runTests + type: boolean + displayName: "Run Tests" + default: true + +extends: + template: templates/pipeline-ci-build.yml + parameters: + buildPlatforms: ${{ parameters.buildPlatforms }} + enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }} + runTests: ${{ parameters.runTests }} diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml new file mode 100644 index 0000000000..b4a6a80833 --- /dev/null +++ b/.pipelines/v2/release.yml @@ -0,0 +1,106 @@ +trigger: none +pr: none + +resources: + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +# Expose all of these parameters for user configuration. +parameters: + - name: publishSymbolsToPublic + displayName: "Publish Symbols to **PUBLIC** (use only for Final Builds)" + type: boolean + default: false + + - name: versionNumber + displayName: "Version Number" + type: string + default: '0.0.1' + + - name: buildConfigurations + displayName: "Build Configurations" + type: object + default: + - Release + + - name: buildPlatforms + displayName: "Build Platforms" + type: object + default: + - x64 + - arm64 + +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + customBuildTags: + - 1ES.PT.ViaStartRight + pool: + name: SHINE-INT-S + image: SHINE-VS17-Latest + os: windows + sdl: + tsa: + enabled: true + configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json' + + stages: + - stage: Build + displayName: Build + dependsOn: [] + jobs: + - template: .pipelines/v2/templates/job-build-project.yml@self + parameters: + pool: + name: SHINE-INT-L + image: SHINE-VS17-Latest + os: windows + variables: + IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations + SkipCppCodeAnalysis: 1 # Skip the code analysis to speed up release CI. It runs on PR CI, anyway + # IsExperimentationLive: 1 # The build and installer use this to turn on experimentation + buildPlatforms: ${{ parameters.buildPlatforms }} + buildConfigurations: ${{ parameters.buildConfigurations }} + versionNumber: ${{ parameters.versionNumber }} + publishArtifacts: false # 1ES PT handles publication for us. + codeSign: true + runTests: false + signingIdentity: + serviceName: $(SigningServiceName) + appId: $(SigningAppId) + tenantId: $(SigningTenantId) + akvName: $(SigningAKVName) + authCertName: $(SigningAuthCertName) + signCertName: $(SigningSignCertName) + # Have msbuild use the release nuget config profile + additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" + beforeBuildSteps: + # Sets versions for all PowerToy created DLLs + - pwsh: |- + .pipelines/versionSetting.ps1 -versionNumber '${{ parameters.versionNumber }}' -DevEnvironment '' + displayName: Prepare versioning + + # Prepare the localizations and telemetry config before the release build + - template: .pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml@self + + - script: | + call nuget.exe restore -configFile .pipelines/release-nuget.config -PackagesDirectory . .pipelines/packages.config || exit /b 1 + move /Y "Microsoft.PowerToys.Telemetry.2.0.0\build\include\TraceLoggingDefines.h" "src\common\Telemetry\TraceLoggingDefines.h" || exit /b 1 + move /Y "Microsoft.PowerToys.Telemetry.2.0.0\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs" || exit /b 1 + displayName: Emplace telemetry files + + - stage: Publish + displayName: Publish + dependsOn: [Build] + jobs: + - template: .pipelines/v2/templates/job-publish-symbols-using-symbolrequestprod-api.yml@self + parameters: + versionNumber: ${{ parameters.versionNumber }} + includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }} + subscription: $(SymbolPublishingServiceConnection) + symbolProject: $(SymbolPublishingProject) diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml new file mode 100644 index 0000000000..88417ca983 --- /dev/null +++ b/.pipelines/v2/templates/job-build-project.yml @@ -0,0 +1,538 @@ +parameters: + - name: additionalBuildOptions + type: string + default: '' + - name: buildConfigurations + type: object + default: + - Release + - name: buildPlatforms + type: object + default: + - x64 + - arm64 + - name: codeSign + type: boolean + default: false + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: 'Build' + - name: condition + type: string + default: '' + - name: dependsOn + type: object + default: [] + - name: pool + type: object + default: [] + - name: beforeBuildSteps + type: stepList + default: [] + - name: variables + type: object + default: {} + - name: publishArtifacts + type: boolean + default: true + - name: signingIdentity + type: object + default: {} + - name: enablePackageCaching + type: boolean + default: false + - name: enableMsBuildCaching + type: boolean + default: false + - name: runTests + type: boolean + default: true + + - name: versionNumber + type: string + default: '0.0.1' + - name: csProjectsToPublish + type: object + default: + - 'src/settings-ui/Settings.UI/PowerToys.Settings.csproj' + - 'src/modules/launcher/PowerLauncher/PowerLauncher.csproj' + - 'src/modules/previewpane/MonacoPreviewHandler/MonacoPreviewHandler.csproj' + - 'src/modules/previewpane/MarkdownPreviewHandler/MarkdownPreviewHandler.csproj' + - 'src/modules/previewpane/SvgPreviewHandler/SvgPreviewHandler.csproj' + - 'src/modules/previewpane/SvgThumbnailProvider/SvgThumbnailProvider.csproj' + - 'src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithUI.csproj' + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + dependsOn: ${{ parameters.dependsOn }} + condition: ${{ parameters.condition }} + strategy: + matrix: + ${{ each config in parameters.buildConfigurations }}: + ${{ each platform in parameters.buildPlatforms }}: + ${{ config }}_${{ platform }}: + BuildConfiguration: ${{ config }} + BuildPlatform: ${{ platform }} + ${{ if eq(platform, 'x86') }}: + OutputBuildPlatform: Win32 + ${{ elseif eq(platform, 'Any CPU') }}: + OutputBuildPlatform: AnyCPU + ${{ else }}: + OutputBuildPlatform: ${{ platform }} + variables: + # Azure DevOps abhors a vacuum + # If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names* + # later on. We'll just... set them to a single space and if we need to, check IsNullOrWhiteSpace. + # Yup. + MSBuildCacheParameters: ' ' + JobOutputDirectory: $(Build.ArtifactStagingDirectory) + LogOutputDirectory: $(Build.ArtifactStagingDirectory)\logs + JobOutputArtifactName: build-$(BuildPlatform)-$(BuildConfiguration)${{ parameters.artifactStem }} + NUGET_RESTORE_MSBUILD_ARGS: /p:Platform=$(BuildPlatform) # Required for nuget to work due to self contained + NODE_OPTIONS: --max_old_space_size=16384 + ${{ if eq(parameters.runTests, true) }}: + MSBuildMainBuildTargets: Build;Test + ${{ else }}: + MSBuildMainBuildTargets: Build + ${{ insert }}: ${{ parameters.variables }} + displayName: Build + timeoutInMinutes: 240 + cancelTimeoutInMinutes: 1 + templateContext: # Required when this template is hosted in 1ES PT + outputs: + - output: pipelineArtifact + artifactName: $(JobOutputArtifactName) + targetPath: $(Build.ArtifactStagingDirectory) + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: True + fetchTags: false + fetchDepth: 1 + + - ${{ if eq(parameters.enableMsBuildCaching, true) }}: + - pwsh: |- + $MSBuildCacheParameters = "" + $MSBuildCacheParameters += " -graph" + $MSBuildCacheParameters += " -reportfileaccesses" + $MSBuildCacheParameters += " -p:MSBuildCacheEnabled=true" + $MSBuildCacheParameters += " -p:MSBuildCacheLogDirectory=$(LogOutputDirectory)\MSBuildCacheLogs" + Write-Host "MSBuildCacheParameters: $MSBuildCacheParameters" + Write-Host "##vso[task.setvariable variable=MSBuildCacheParameters]$MSBuildCacheParameters" + displayName: Prepare MSBuildCache variables + + - ${{ if eq(parameters.codeSign, true) }}: + # Only required if we're using ESRP + - template: steps-ensure-dotnet-version.yml + parameters: + sdk: true + version: '6.0' + + - template: steps-ensure-dotnet-version.yml + parameters: + sdk: true + version: '8.0' + + - ${{ if eq(parameters.runTests, true) }}: + - task: VisualStudioTestPlatformInstaller@1 + displayName: Ensure VSTest Platform + + - pwsh: |- + & '.pipelines/applyXamlStyling.ps1' -Passive + & '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\PowerToys.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\BugReportTool\BugReportTool.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\WebcamReportTool\WebcamReportTool.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\StylesReportTool\StylesReportTool.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\installer\PowerToysSetup.sln' + displayName: Verify formatting, nuget, and ARM64 configurations + + - ${{ if eq(parameters.enablePackageCaching, true) }}: + - task: Cache@2 + displayName: 'Cache nuget packages (PackageReference)' + inputs: + key: '"PackageReference" | "$(Agent.OS)" | Directory.Packages.props' + restoreKeys: | + "PackageReference" | "$(Agent.OS)" + "PackageReference" + path: $(NUGET_PACKAGES) + + - task: Cache@2 + displayName: 'Cache nuget packages (packages.config)' + inputs: + key: '"packages.config" | "$(Agent.OS)" | **/packages.config' + restoreKeys: | + "packages.config" | "$(Agent.OS)" + "packages.config" + path: packages + + - template: .\steps-restore-nuget.yml + + - pwsh: |- + & "$(build.sourcesdirectory)\.pipelines\verifyAndSetLatestVCToolsVersion.ps1" + displayName: Work around DD-1541167 (VCToolsVersion) + + - pwsh: |- + & "$(build.sourcesdirectory)\.pipelines\installWiX.ps1" + displayName: Download and install WiX 3.14 development build + + - ${{ parameters.beforeBuildSteps }} + + - task: VSBuild@1 + ${{ if eq(parameters.runTests, true) }}: + displayName: Build and Test PowerToys main project + ${{ else }}: + displayName: Build PowerToys main project + inputs: + solution: 'PowerToys.sln' + vsVersion: 17.0 + msbuildArgs: >- + -restore -graph + /p:RestorePackagesConfig=true + /p:CIBuild=true + /bl:$(LogOutputDirectory)\build-0-main.binlog + ${{ parameters.additionalBuildOptions }} + $(MSBuildCacheParameters) + /t:$(MSBuildMainBuildTargets) + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-esrp-signing.yml + parameters: + displayName: Sign Utilities + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'src/modules' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_abstracted_utils_dll.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + - task: VSBuild@1 + displayName: Create Hosts File Editor package + inputs: + solution: '**\HostsUILib.csproj' + vsVersion: 17.0 + msbuildArgs: /p:CIBuild=true -t:pack /bl:$(LogOutputDirectory)\build-hosts.binlog + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - task: VSBuild@1 + displayName: Create Environment Variables Editor package + inputs: + solution: '**\EnvironmentVariablesUILib.csproj' + vsVersion: 17.0 + msbuildArgs: /p:CIBuild=true -t:pack /bl:$(LogOutputDirectory)\build-env-var-editor.binlog + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - task: VSBuild@1 + displayName: Create Registry Preview package + inputs: + solution: '**\RegistryPreviewUILib.csproj' + vsVersion: 17.0 + msbuildArgs: /p:CIBuild=true -t:pack /bl:$(LogOutputDirectory)\build-registry-preview.binlog + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - task: CopyFiles@2 + displayName: Stage NuGet packages + inputs: + contents: "**/bin/Release/PowerToys*.nupkg" + flattenFolders: True + targetFolder: $(JobOutputDirectory)/nupkg + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-esrp-signing.yml + parameters: + displayName: Sign NuGet packages + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: $(JobOutputDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + + - task: VSBuild@1 + displayName: Build BugReportTool + inputs: + solution: '**/tools/BugReportTool/BugReportTool.sln' + vsVersion: 17.0 + msbuildArgs: >- + -restore -graph + /p:RestorePackagesConfig=true + /p:CIBuild=true + /bl:$(LogOutputDirectory)\build-bug-report.binlog + ${{ parameters.additionalBuildOptions }} + $(MSBuildCacheParameters) + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - task: VSBuild@1 + displayName: Build WebcamReportTool + inputs: + solution: '**/tools/WebcamReportTool/WebcamReportTool.sln' + vsVersion: 17.0 + msbuildArgs: >- + -restore -graph + /p:RestorePackagesConfig=true + /p:CIBuild=true + /bl:$(LogOutputDirectory)\build-webcam-report.binlog + ${{ parameters.additionalBuildOptions }} + $(MSBuildCacheParameters) + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - task: VSBuild@1 + displayName: Build StylesReportTool + inputs: + solution: '**/tools/StylesReportTool/StylesReportTool.sln' + vsVersion: 17.0 + msbuildArgs: >- + -restore -graph + /p:RestorePackagesConfig=true + /p:CIBuild=true + /bl:$(LogOutputDirectory)\build-styles-report.binlog + ${{ parameters.additionalBuildOptions }} + $(MSBuildCacheParameters) + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + ${{ if eq(parameters.enableMsBuildCaching, true) }}: + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + + - ${{ each project in parameters.csProjectsToPublish }}: + - task: VSBuild@1 + displayName: Publish ${{ project }} for Packaging + inputs: + solution: ${{ project }} + vsVersion: 17.0 + msbuildArgs: >- + /target:Publish + /graph + /p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never + /p:VCRTForwarders-IncludeDebugCRT=false + /p:PowerToysRoot=$(Build.SourcesDirectory) + /p:PublishProfile=InstallationPublishProfile.pubxml + /bl:$(LogOutputDirectory)\publish-${{ join('_',split(project, '/')) }}.binlog + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + + # Check if deps.json files don't reference different dll versions. + - pwsh: |- + & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)' + displayName: Audit deps.json files for all applications + + # Check if asset files on the main application paths are playing nice and avoiding basic conflicts. + - pwsh: |- + & '.pipelines/verifyPossibleAssetConflicts.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)' + displayName: Audit base applications path asset conflicts + + - pwsh: |- + & '.pipelines/verifyPossibleAssetConflicts.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)\WinUI3Apps' + displayName: Audit WinAppSDK applications path asset conflicts + + - pwsh: |- + & '.pipelines/verifyNoticeMdAgainstNugetPackages.ps1' -path '$(build.sourcesdirectory)\' + displayName: Verify NOTICE.md and NuGet packages match + + - ${{ if eq(parameters.runTests, true) }}: + # Publish test results which ran in MSBuild + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + inputs: + testResultsFormat: VSTest + testResultsFiles: '**/*.trx' + condition: ne(variables['BuildPlatform'],'arm64') + + # Native dlls + - task: VSTest@2 + condition: ne(variables['BuildPlatform'],'arm64') # No arm64 agents to run the tests. + displayName: 'Native Tests' + inputs: + platform: '$(BuildPlatform)' + configuration: '$(BuildConfiguration)' + testSelector: 'testAssemblies' + testAssemblyVer2: | + **\KeyboardManagerEngineTest.dll + **\KeyboardManagerEditorTest.dll + **\UnitTests-CommonLib.dll + **\PowerRenameUnitTests.dll + **\UnitTests-FancyZones.dll + !**\obj\** + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-esrp-signing.yml + parameters: + displayName: Sign Core PowerToys + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: '$(BuildPlatform)/$(BuildConfiguration)' # Video conf uses x86 and x64. + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_core.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + - template: steps-esrp-signing.yml + parameters: + displayName: Sign DSC files + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'src/dsc/Microsoft.PowerToys.Configure' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_DSC.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + - template: steps-esrp-signing.yml + parameters: + displayName: Sign x86 DirectShow VCM + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'x86/$(BuildConfiguration)' # Video conf uses x86 and x64. + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_vcm.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + - template: steps-build-installer.yml + parameters: + codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} + versionNumber: ${{ parameters.versionNumber }} + additionalBuildOptions: ${{ parameters.additionalBuildOptions }} + + - template: steps-build-installer.yml + parameters: + codeSign: ${{ parameters.codeSign }} + signingIdentity: ${{ parameters.signingIdentity }} + versionNumber: ${{ parameters.versionNumber }} + additionalBuildOptions: ${{ parameters.additionalBuildOptions }} + buildUserInstaller: true # NOTE: This is the distinction between the above and below rules + + # This saves ~1GiB per architecture. We won't need these later. + # Removes: + # - All .pdbs from any static libs .libs (which were only used during linking) + - pwsh: |- + $binDir = '$(Build.SourcesDirectory)' + $ImportLibs = Get-ChildItem $binDir -Recurse -File -Filter '*.exp' | ForEach-Object { $_.FullName -Replace "exp$","lib" } + $StaticLibs = Get-ChildItem $binDir -Recurse -File -Filter '*.lib' | Where-Object FullName -NotIn $ImportLibs + + $Items = @() + $Items += Get-Item ($StaticLibs.FullName -Replace "lib$","pdb") -ErrorAction:Ignore + + $Items | Remove-Item -Recurse -Force -Verbose -ErrorAction:Ignore + displayName: Clean up static libs PDBs + errorActionPreference: silentlyContinue # It's OK if this silently fails + + - task: CopyFiles@2 + displayName: Stage Installers + inputs: + contents: "**/PowerToys*Setup-*.exe" + flattenFolders: True + targetFolder: $(JobOutputDirectory) + + - task: CopyFiles@2 + displayName: Stage Symbols + inputs: + contents: |- + **\*.pdb + !**\vc143.pdb + !**\*test*.pdb + flattenFolders: True + targetFolder: $(JobOutputDirectory)/symbols-$(BuildPlatform)/ + + - pwsh: |- + $p = "$(JobOutputDirectory)\" + $userHash = ((Get-Item $p\PowerToysUserSetup*.exe | Get-FileHash).Hash); + $machineHash = ((Get-Item $p\PowerToysSetup*.exe | Get-FileHash).Hash); + $userPlat = "hash_user_$(BuildPlatform).txt"; + $machinePlat = "hash_machine_$(BuildPlatform).txt"; + $combinedUserPath = $p + $userPlat; + $combinedMachinePath = $p + $machinePlat; + + echo $p + + echo $userPlat + echo $userHash + echo $combinedUserPath + + echo $machinePlat + echo $machineHash + echo $combinedMachinePath + + $userHash | out-file -filepath $combinedUserPath + $machineHash | out-file -filepath $combinedMachinePath + displayName: Calculate file hashes + + # Publishing the GPO files + - pwsh: |- + New-Item "$(JobOutputDirectory)/gpo" -Type Directory + Copy-Item src\gpo\assets\* "$(JobOutputDirectory)/gpo" -Recurse + displayName: Stage GPO files + + # Running the tests may result in future jobs consuming artifacts out of this build + - ${{ if eq(parameters.runTests, true) }}: + - task: CopyFiles@2 + displayName: Stage entire build output + inputs: + sourceFolder: '$(Build.SourcesDirectory)' + contents: '$(BuildPlatform)/$(BuildConfiguration)/**/*' + targetFolder: '$(JobOutputDirectory)\$(BuildPlatform)\$(BuildConfiguration)' + + - ${{ if eq(parameters.publishArtifacts, true) }}: + - publish: $(JobOutputDirectory) + artifact: $(JobOutputArtifactName) + displayName: Publish all outputs + condition: always() diff --git a/.pipelines/v2/templates/job-ci-precheck.yml b/.pipelines/v2/templates/job-ci-precheck.yml new file mode 100644 index 0000000000..6d3616ff69 --- /dev/null +++ b/.pipelines/v2/templates/job-ci-precheck.yml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/master/service-schema.json +jobs: +- job: Precheck + pool: + vmImage: windows-2022 + steps: + - checkout: none + + - pwsh: |- + try { + # Try based on pull request first + $pullRequestNumber = "$(system.pullRequest.pullRequestNumber)"; + $gitHubPullRequest = Invoke-RestMethod -Method Get "https://api.github.com/repos/microsoft/PowerToys/pulls/$pullRequestNumber/files" + # If there are no files updated in the commit that are .md, set skipBuild variable + if(([array]($gitHubPullRequest.filename) -notmatch ".md|.txt").Length -eq 0) { + Write-Host '##vso[task.setvariable variable=skipBuild;isOutput=true]Yes' + Write-Host 'Skipping Build' + } + } + catch { + # Fall back to the latest commit otherwise. + $commit = "$(build.sourceVersion)"; + $gitHubCommit = Invoke-RestMethod -Method Get "https://api.github.com/repos/microsoft/PowerToys/commits/$commit" + if(([array]($githubCommit.files.filename) -notmatch ".md|.txt").Length -eq 0) { + Write-Host '##vso[task.setvariable variable=skipBuild;isOutput=true]Yes' + Write-Host 'Skipping Build' + } + } + displayName: Verify whether we need to build at all + name: verifyBuildRequest diff --git a/.pipelines/v2/templates/job-publish-symbols-using-symbolrequestprod-api.yml b/.pipelines/v2/templates/job-publish-symbols-using-symbolrequestprod-api.yml new file mode 100644 index 0000000000..967b7ba4eb --- /dev/null +++ b/.pipelines/v2/templates/job-publish-symbols-using-symbolrequestprod-api.yml @@ -0,0 +1,116 @@ +parameters: + - name: includePublicSymbolServer + type: boolean + default: false + - name: pool + type: object + default: [] + - name: dependsOn + type: object + default: null + - name: versionNumber + type: string + default: '0.0.1' + - name: artifactStem + type: string + default: '' + - name: jobName + type: string + default: PublishSymbols + - name: symbolExpiryTime + type: string + default: 36530 # This is the default from PublishSymbols@2 + - name: variables + type: object + default: {} + - name: subscription + type: string + - name: symbolProject + type: string + +jobs: +- job: ${{ parameters.jobName }} + ${{ if ne(length(parameters.pool), 0) }}: + pool: ${{ parameters.pool }} + ${{ if eq(parameters.includePublicSymbolServer, true) }}: + displayName: Publish Symbols to Internal and MSDL + ${{ else }}: + displayName: Publish Symbols Internally + dependsOn: ${{ parameters.dependsOn }} + variables: + ${{ insert }}: ${{ parameters.variables }} + SymbolsArtifactName: "PowerToys_${{parameters.versionNumber}}_$(Build.BuildNumber)" + steps: + - checkout: self + clean: true + fetchDepth: 1 + fetchTags: false # Tags still result in depth > 1 fetch; we don't need them here + submodules: true + persistCredentials: True + + - task: DownloadPipelineArtifact@2 + displayName: Download all PDBs from all prior build phases + inputs: + itemPattern: '**/*.pdb' + targetPath: '$(Build.SourcesDirectory)/symbolStaging' + + - powershell: |- + Get-PackageProvider -Name NuGet -ForceBootstrap + Install-Module -Verbose -AllowClobber -Force Az.Accounts, Az.Storage, Az.Network, Az.Resources, Az.Compute + displayName: Install Azure Module Dependencies + + # Transit the Azure token from the Service Connection into a secret variable for the rest of the pipeline to use. + - task: AzurePowerShell@5 + displayName: Generate an Azure Token + inputs: + azureSubscription: ${{ parameters.subscription }} + azurePowerShellVersion: LatestVersion + pwsh: true + ScriptType: InlineScript + Inline: |- + $AzToken = (Get-AzAccessToken -ResourceUrl api://30471ccf-0966-45b9-a979-065dbedb24c1).Token + Write-Host "##vso[task.setvariable variable=SymbolAccessToken;issecret=true]$AzToken" + + + - task: PublishSymbols@2 + displayName: Publish Symbols (to current Azure DevOps tenant) + continueOnError: True + inputs: + SymbolsFolder: '$(Build.SourcesDirectory)/symbolStaging' + SearchPattern: '**/*.pdb' + IndexSources: false + DetailedLog: true + SymbolsMaximumWaitTime: 30 + SymbolServerType: 'TeamServices' + SymbolsProduct: 'PowerToys Converged Symbols' + SymbolsVersion: '${{ parameters.versionNumber }}' + SymbolsArtifactName: $(SymbolsArtifactName) + SymbolExpirationInDays: ${{ parameters.symbolExpiryTime }} + env: + LIB: $(Build.SourcesDirectory) + + - pwsh: |- + # Prepare the defaults for IRM + $PSDefaultParameterValues['Invoke-RestMethod:Headers'] = @{ Authorization = "Bearer $(SymbolAccessToken)" } + $PSDefaultParameterValues['Invoke-RestMethod:ContentType'] = "application/json" + $PSDefaultParameterValues['Invoke-RestMethod:Method'] = "POST" + + $BaseUri = "https://symbolrequestprod.trafficmanager.net/projects/${{ parameters.symbolProject }}/requests" + + # Prepare the request + $expiration = (Get-Date).Add([TimeSpan]::FromDays(${{ parameters.symbolExpiryTime }})) + $createRequestBody = @{ + requestName = "$(SymbolsArtifactName)"; + expirationTime = $expiration.ToString(); + } + Write-Host "##[debug]Starting request $($createRequestBody.requestName) with expiration date of $($createRequestBody.expirationTime)" + Invoke-RestMethod -Uri "$BaseUri" -Body ($createRequestBody | ConvertTo-Json -Compress) -Verbose + + # Request symbol publication + $publishRequestBody = @{ + publishToInternalServer = $true; + publishToPublicServer = $${{ parameters.includePublicSymbolServer }}; + } + Write-Host "##[debug]Submitting request $($createRequestBody.requestName) ($($publishRequestBody | ConvertTo-Json -Compress))" + Invoke-RestMethod -Uri "$BaseUri/$($createRequestBody.requestName)" -Body ($publishRequestBody | ConvertTo-Json -Compress) -Verbose + displayName: Publish Symbols using internal REST API diff --git a/.pipelines/v2/templates/job-test-project.yml b/.pipelines/v2/templates/job-test-project.yml new file mode 100644 index 0000000000..f5167ededd --- /dev/null +++ b/.pipelines/v2/templates/job-test-project.yml @@ -0,0 +1,76 @@ +parameters: + - name: configuration + type: string + default: "Release" + - name: platform + type: string + default: "" + - name: inputArtifactStem + type: string + default: "" + +jobs: +- job: Test${{ parameters.platform }}${{ parameters.configuration }} + displayName: Test ${{ parameters.platform }} ${{ parameters.configuration }} + variables: + BuildPlatform: ${{ parameters.platform }} + BuildConfiguration: ${{ parameters.configuration }} + SrcPath: $(Build.Repository.LocalPath) + pool: + ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: + ${{ if ne(parameters.platform, 'ARM64') }}: + name: SHINE-INT-Testing-x64 + ${{ else }}: + name: SHINE-INT-Testing-arm64 + ${{ else }}: + ${{ if ne(parameters.platform, 'ARM64') }}: + name: SHINE-OSS-Testing-x64 + ${{ else }}: + name: SHINE-OSS-Testing-arm64 + steps: + - checkout: self + submodules: false + clean: true + fetchDepth: 1 + fetchTags: false + + - download: current + displayName: Download artifacts + artifact: build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }} + patterns: |- + ** + !**\*.pdb + !**\*.lib + + - template: steps-ensure-dotnet-version.yml + parameters: + sdk: true + version: '8.0' + + - task: VisualStudioTestPlatformInstaller@1 + displayName: Ensure VSTest Platform + + - pwsh: |- + & '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1' + displayName: Download and install WinAppDriver + + - ${{ if ne(parameters.platform, 'arm64') }}: + - task: ScreenResolutionUtility@1 + inputs: + displaySettings: 'optimal' + + - task: VSTest@3 + displayName: Run UI Tests + inputs: + platform: '$(BuildPlatform)' + configuration: '$(BuildConfiguration)' + testSelector: 'testAssemblies' + searchFolder: '$(Pipeline.Workspace)\build-${{ parameters.platform }}-${{ parameters.configuration }}${{ parameters.inputArtifactStem }}' + vsTestVersion: 'toolsInstaller' + uiTests: true + rerunFailedTests: true + testAssemblyVer2: | + **\UITests-FancyZones.dll + **\UITests-FancyZonesEditor.dll + !**\obj\** + !**\ref\** diff --git a/.pipelines/v2/templates/pipeline-ci-build.yml b/.pipelines/v2/templates/pipeline-ci-build.yml new file mode 100644 index 0000000000..73a3ff766d --- /dev/null +++ b/.pipelines/v2/templates/pipeline-ci-build.yml @@ -0,0 +1,59 @@ +variables: + - name: runCodesignValidationInjectionBG + value: false + - name: EnablePipelineCache + value: true + +parameters: + - name: buildPlatforms + type: object + default: + - x64 + - arm64 + - name: enableMsBuildCaching + type: boolean + default: false + - name: runTests + type: boolean + default: true + +stages: + # Allow manual builds to skip pre-check + - ${{ if ne(variables['Build.Reason'], 'Manual') }}: + - stage: Precheck + jobs: + - template: job-ci-precheck.yml + + - ${{ each platform in parameters.buildPlatforms }}: + - stage: Build_${{ platform }} + displayName: Build ${{ platform }} + ${{ if ne(variables['Build.Reason'], 'Manual') }}: + dependsOn: [Precheck] + ${{ else }}: + dependsOn: [] + jobs: + - template: job-build-project.yml + parameters: + condition: and(succeeded(), or(eq(variables['Build.Reason'], 'Manual'), ne(stageDependencies.Precheck.Precheck.outputs['verifyBuildRequest.skipBuild'], 'Yes'))) + pool: + ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: + name: SHINE-INT-L + ${{ else }}: + name: SHINE-OSS-L + buildPlatforms: + - ${{ platform }} + buildConfigurations: [Release] + enablePackageCaching: true + enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }} + runTests: ${{ parameters.runTests }} + + - ${{ if eq(parameters.runTests, true) }}: + - stage: Test_${{ platform }} + displayName: Test ${{ platform }} + dependsOn: + - Build_${{platform}} + jobs: + - template: job-test-project.yml + parameters: + platform: ${{ platform }} + configuration: Release diff --git a/.pipelines/v2/templates/steps-build-installer.yml b/.pipelines/v2/templates/steps-build-installer.yml new file mode 100644 index 0000000000..a4eb61481c --- /dev/null +++ b/.pipelines/v2/templates/steps-build-installer.yml @@ -0,0 +1,184 @@ +parameters: + - name: versionNumber + type: string + default: "0.0.1" + - name: buildUserInstaller + type: boolean + default: false + - name: codeSign + type: boolean + default: false + - name: signingIdentity + type: object + default: {} + - name: additionalBuildOptions + type: string + default: '' + +steps: + - pwsh: |- + & git clean -xfd -e *exe -- .\installer\ + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Clean installer to reduce cross-contamination + + - pwsh: |- + $IsPerUser = $${{ parameters.buildUserInstaller }} + $InstallerBuildSlug = "MachineSetup" + $InstallerBasename = "PowerToysSetup" + If($IsPerUser) { + $InstallerBuildSlug = "UserSetup" + $InstallerBasename = "PowerToysUserSetup" + } + $InstallerBasename += "-${{ parameters.versionNumber }}-$(BuildPlatform)" + Write-Host "##vso[task.setvariable variable=InstallerBuildSlug]$InstallerBuildSlug" + Write-Host "##vso[task.setvariable variable=InstallerRelativePath]$(BuildPlatform)\$(BuildConfiguration)\$InstallerBuildSlug" + Write-Host "##vso[task.setvariable variable=InstallerBasename]$InstallerBasename" + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Prepare Installer variables + + # This dll needs to be built and signed before building the MSI. + - task: VSBuild@1 + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build PowerToysSetupCustomActions + inputs: + solution: "**/installer/PowerToysSetup.sln" + vsVersion: 17.0 + msbuildArgs: >- + /t:PowerToysSetupCustomActions + /p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true + -restore -graph + /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-actions.binlog + ${{ parameters.additionalBuildOptions }} + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: true + msbuildArchitecture: x64 + maximumCpuCount: true + + - ${{ if eq(parameters.codeSign, true) }}: + - template: steps-esrp-signing.yml + parameters: + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign PowerToysSetupCustomActions + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'installer/PowerToysSetupCustomActions/$(InstallerRelativePath)' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + ## INSTALLER START + #### MSI BUILDING AND SIGNING + - task: VSBuild@1 + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build MSI + inputs: + solution: "**/installer/PowerToysSetup.sln" + vsVersion: 17.0 + msbuildArgs: >- + -restore + /t:PowerToysInstaller + /p:RunBuildEvents=false;PerUser=${{parameters.buildUserInstaller}};BuildProjectReferences=false;CIBuild=true + /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-msi.binlog + ${{ parameters.additionalBuildOptions }} + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: false # don't undo our hard work above by deleting the CustomActions dll + msbuildArchitecture: x64 + maximumCpuCount: true + + - script: |- + "C:\Program Files (x86)\WiX Toolset v3.14\bin\dark.exe" -x $(build.sourcesdirectory)\extractedMsi installer\PowerToysSetup\$(InstallerRelativePath)\$(InstallerBasename).msi + dir $(build.sourcesdirectory)\extractedMsi + displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Extract and verify MSI" + + # Check if deps.json files don't reference different dll versions. + - pwsh: |- + & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMsi\File' + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Audit deps.json in MSI extracted files + + - ${{ if eq(parameters.codeSign, true) }}: + - pwsh: |- + & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\File' + & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\Binary' + git clean -xfd ./extractedMsi + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Verify all binaries are signed and versioned + + - template: steps-esrp-signing.yml + parameters: + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign MSI + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'installer/PowerToysSetup/$(InstallerRelativePath)' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + + #### END MSI + #### BOOTSTRAP BUILDING AND SIGNING + + - task: VSBuild@1 + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build Bootstrapper + inputs: + solution: "**/installer/PowerToysSetup.sln" + vsVersion: 17.0 + msbuildArgs: >- + /t:PowerToysBootstrapper + /p:PerUser=${{parameters.buildUserInstaller}};CIBuild=true + /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-bootstrapper.binlog + -restore -graph + ${{ parameters.additionalBuildOptions }} + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: false # don't undo our hard work above by deleting the MSI + msbuildArchitecture: x64 + maximumCpuCount: true + + # The entirety of bundle unpacking/re-packing is unnecessary if we are not code signing it. + - ${{ if eq(parameters.codeSign, true) }}: + - script: |- + "C:\Program Files (x86)\WiX Toolset v3.14\bin\insignia.exe" -ib installer\PowerToysSetup\$(InstallerRelativePath)\$(InstallerBasename).exe -o installer\engine.exe + displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Insignia: Extract Engine from Bundle" + + - template: steps-esrp-signing.yml + parameters: + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign WiX Engine + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: "installer" + Pattern: engine.exe + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] + + - script: |- + "C:\Program Files (x86)\WiX Toolset v3.14\bin\insignia.exe" -ab installer\engine.exe installer\PowerToysSetup\$(InstallerRelativePath)\$(InstallerBasename).exe -o installer\PowerToysSetup\$(InstallerRelativePath)\$(InstallerBasename).exe + displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Insignia: Merge Engine into Bundle" + + - template: steps-esrp-signing.yml + parameters: + displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign Final Bootstrapper + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: 'installer/PowerToysSetup/$(InstallerRelativePath)' + signType: batchSigning + batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' + ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + #### END BOOTSTRAP + ## END INSTALLER diff --git a/.pipelines/v2/templates/steps-ensure-dotnet-version.yml b/.pipelines/v2/templates/steps-ensure-dotnet-version.yml new file mode 100644 index 0000000000..ee688d18f6 --- /dev/null +++ b/.pipelines/v2/templates/steps-ensure-dotnet-version.yml @@ -0,0 +1,27 @@ +parameters: +- name: version + type: string + default: "8.0" +- name: sdk + type: boolean + default: false + +# You might be wondering, "Why didn't they use UseDotNet?" +# Azure Pipelines is practically unmaintained, that's why. +# +# "[BUG]: UseDotNet task installs x86 build on Windows arm64" +# https://github.com/microsoft/azure-pipelines-tasks/issues/20300 +# +# Herein we replicate 90% of the meaningful logic in that task. +steps: +- pwsh: |- + curl.exe -J -L -O "https://dot.net/v1/dotnet-install.ps1" + $NEW_DOTNET_ROOT = "$(Agent.ToolsDirectory)\dotnet" + & ./dotnet-install.ps1 -Channel "${{parameters.version}}" -InstallDir $NEW_DOTNET_ROOT + Write-Host "##vso[task.setvariable variable=DOTNET_ROOT]${NEW_DOTNET_ROOT}" + Write-Host "##vso[task.prependpath]${NEW_DOTNET_ROOT}" + Remove-Item dotnet-install.ps1 -ErrorAction:Ignore + ${{ if eq(parameters.sdk, true) }}: + displayName: "Install .NET ${{parameters.version}} SDK" + ${{ else }}: + displayName: "Install .NET ${{parameters.version}}" diff --git a/.pipelines/v2/templates/steps-ensure-nuget-version.yml b/.pipelines/v2/templates/steps-ensure-nuget-version.yml new file mode 100644 index 0000000000..8d206bcaec --- /dev/null +++ b/.pipelines/v2/templates/steps-ensure-nuget-version.yml @@ -0,0 +1,5 @@ +steps: +- task: NuGetToolInstaller@1 + displayName: Use NuGet 6.6.1 + inputs: + versionSpec: 6.6.1 diff --git a/.pipelines/v2/templates/steps-esrp-signing.yml b/.pipelines/v2/templates/steps-esrp-signing.yml new file mode 100644 index 0000000000..2d47a4df58 --- /dev/null +++ b/.pipelines/v2/templates/steps-esrp-signing.yml @@ -0,0 +1,22 @@ +parameters: + - name: displayName + type: string + default: ESRP Code Signing + - name: inputs + type: object + default: {} + - name: signingIdentity + type: object + default: {} + +steps: + - task: EsrpCodeSigning@5 + displayName: 🔏 ${{ parameters.displayName }} + inputs: + ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} + AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} + AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} + AuthAKVName: ${{ parameters.signingIdentity.akvName }} + AuthCertName: ${{ parameters.signingIdentity.authCertName }} + AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} + ${{ insert }}: ${{ parameters.inputs }} diff --git a/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml b/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml new file mode 100644 index 0000000000..30cf2b6f67 --- /dev/null +++ b/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml @@ -0,0 +1,26 @@ +parameters: + - name: includePseudoLoc + type: boolean + default: false + +steps: + - task: TouchdownBuildTask@3 + displayName: 'Download Localization Files -- PowerToys 37400' + inputs: + teamId: 37400 + TDBuildServiceConnection: $(TouchdownServiceConnection) + authType: SubjectNameIssuer + resourceFilePath: | + **\Resources.resx + **\Resource.resx + **\Resources.resw + appendRelativeDir: true + localizationTarget: false + ${{ if eq(parameters.includePseudoLoc, true) }}: + pseudoSetting: Included + + - pwsh: |- + $VerbosePreference = "Continue" + ./tools/build/move-and-rename-resx.ps1 + ./tools/build/move-uwp-resw.ps1 + displayName: Move Loc files into final locations diff --git a/.pipelines/v2/templates/steps-restore-nuget.yml b/.pipelines/v2/templates/steps-restore-nuget.yml new file mode 100644 index 0000000000..f7928ccdcb --- /dev/null +++ b/.pipelines/v2/templates/steps-restore-nuget.yml @@ -0,0 +1,20 @@ +steps: +- template: steps-ensure-nuget-version.yml + +- task: NuGetAuthenticate@1 + +- script: |- + echo ##vso[task.setvariable variable=NUGET_RESTORE_MSBUILD_ARGS]/p:Platform=$(BuildPlatform) + displayName: Ensure NuGet restores for $(BuildPlatform) + condition: and(succeeded(), ne(variables['BuildPlatform'], 'Any CPU')) + +# In the Microsoft Azure DevOps tenant, NuGetCommand is ambiguous. +# This should be `task: NuGetCommand@2` +- task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 + displayName: Restore NuGet packages + inputs: + command: restore + feedsToUse: config + configPath: NuGet.config + restoreSolution: packages.config + restoreDirectory: '$(Build.SourcesDirectory)\packages' diff --git a/.pipelines/versionSetting.ps1 b/.pipelines/versionSetting.ps1 index 59cb8f398f..fa37bfbcd7 100644 --- a/.pipelines/versionSetting.ps1 +++ b/.pipelines/versionSetting.ps1 @@ -58,3 +58,12 @@ $imageResizerContextMenuAppManifestReadFileLocation = $imageResizerContextMenuAp $imageResizerContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0' Write-Host "ImageResizerContextMenu version" $imageResizerContextMenuAppManifest.Package.Identity.Version $imageResizerContextMenuAppManifest.Save($imageResizerContextMenuAppManifestWriteFileLocation); + +# Set FileLocksmithContextMenu package version in AppManifest.xml +$fileLocksmithContextMenuAppManifestWriteFileLocation = $PSScriptRoot + '/../src/modules/FileLocksmith/FileLocksmithContextMenu/AppxManifest.xml'; +$fileLocksmithContextMenuAppManifestReadFileLocation = $fileLocksmithContextMenuAppManifestWriteFileLocation; + +[XML]$fileLocksmithContextMenuAppManifest = Get-Content $fileLocksmithContextMenuAppManifestReadFileLocation +$fileLocksmithContextMenuAppManifest.Package.Identity.Version = $versionNumber + '.0' +Write-Host "FileLocksmithContextMenu version" $fileLocksmithContextMenuAppManifest.Package.Identity.Version +$fileLocksmithContextMenuAppManifest.Save($fileLocksmithContextMenuAppManifestWriteFileLocation); diff --git a/PowerToys.sln b/PowerToys.sln index 6f214eb5c0..e91c2e3907 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -273,6 +273,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643 src\common\utils\HDropIterator.h = src\common\utils\HDropIterator.h src\common\utils\HttpClient.h = src\common\utils\HttpClient.h src\common\utils\json.h = src\common\utils\json.h + src\common\utils\language_helper.h = src\common\utils\language_helper.h src\common\utils\logger_helper.h = src\common\utils\logger_helper.h src\common\utils\modulesRegistry.h = src\common\utils\modulesRegistry.h src\common\utils\MsiUtils.h = src\common\utils\MsiUtils.h @@ -619,6 +620,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesEditor", "src\mod EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesWindowArranger", "src\modules\Workspaces\WorkspacesWindowArranger\WorkspacesWindowArranger.vcxproj", "{37D07516-4185-43A4-924F-3C7A5D95ECF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2719,6 +2722,18 @@ Global {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64 {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.ActiveCfg = Release|x64 {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x86.Build.0 = Release|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.Build.0 = Debug|ARM64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.ActiveCfg = Debug|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.Build.0 = Debug|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x86.ActiveCfg = Debug|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x86.Build.0 = Debug|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.ActiveCfg = Release|ARM64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.Build.0 = Release|ARM64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.ActiveCfg = Release|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.Build.0 = Release|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x86.ActiveCfg = Release|x64 + {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2946,6 +2961,7 @@ Global {3D63307B-9D27-44FD-B033-B26F39245B85} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {367D7543-7DBA-4381-99F1-BF6142A996C4} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} + {37D07516-4185-43A4-924F-3C7A5D95ECF6} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/doc/devdocs/modules/launcher/plugins/community.unitconverter.md b/doc/devdocs/modules/launcher/plugins/community.unitconverter.md index aff29968b0..33d0fb58ba 100644 --- a/doc/devdocs/modules/launcher/plugins/community.unitconverter.md +++ b/doc/devdocs/modules/launcher/plugins/community.unitconverter.md @@ -19,13 +19,13 @@ This plugin uses a package called [UnitsNet](https://github.com/angularsen/Units - [Temperature](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/TemperatureUnit.g.cs) - [Volume](https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GeneratedCode/Units/VolumeUnit.g.cs) - These are the ones that are currently enabled (though UnitsNet supports many more). They are defined in [`Main.cs`](/src/modules/launcher/Plugins/Community.PowerToys.Run.UnitConverter/Main.cs). + These are the ones that are currently enabled (though UnitsNet supports many more). They are defined in [`UnitHandler.cs`](/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs). -### [`InputInterpreter`](/src/modules/launcher/Plugins/Community.PowerToys.Run.UnitConverter/InputInterpreter.cs) +### [`InputInterpreter`](/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs) - Class which manipulates user input such that it may be interpreted correctly and thus converted. - Uses a regex amongst other things to do this. -### [`UnitHandler`](/src/modules/launcher/Plugins/Community.PowerToys.Run.UnitConverter/UnitHandler.cs) +### [`UnitHandler`](/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs) - Class that does the actual conversion. - Supports abbreviations in user input (single, double, or none). \ No newline at end of file diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 216a05aaab..8845208200 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } processes.resize(bytes / sizeof(processes[0])); - std::array processesToTerminate = { + std::array processesToTerminate = { L"PowerToys.PowerLauncher.exe", L"PowerToys.Settings.exe", L"PowerToys.AdvancedPaste.exe", @@ -1259,6 +1259,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) L"PowerToys.WorkspacesLauncher.exe", L"PowerToys.WorkspacesLauncherUI.exe", L"PowerToys.WorkspacesEditor.exe", + L"PowerToys.WorkspacesWindowArranger.exe", L"PowerToys.exe", }; diff --git a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj index b58e19460d..af6a0f184b 100644 --- a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj +++ b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj @@ -36,6 +36,9 @@ $(Platform)\$(Configuration)\UserSetup\ $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\ $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\ + + false + true true @@ -73,8 +76,8 @@ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\VideoConference.wxs"" ""$(ProjectDir)..\PowerToysSetup\VideoConference.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\WinAppSDK.wxs"" ""$(ProjectDir)..\PowerToysSetup\WinAppSDK.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\WinUI3Applications.wxs"" ""$(ProjectDir)..\PowerToysSetup\WinUI3Applications.wxs.bk"""" - if not "$(PerUser)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetup\generateAllFileComponents.ps1 -platform $(Platform) - if "$(PerUser)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetup\generateAllFileComponents.ps1 -platform $(Platform) -installscopeperuser $(PerUser) + if not "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetup\generateAllFileComponents.ps1 -platform $(Platform) + if "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetup\generateAllFileComponents.ps1 -platform $(Platform) -installscopeperuser $(NormalizedPerUserValue) Backing up original files and populating .NET and WPF Runtime dependencies diff --git a/src/common/ManagedCommon/LanguageHelper.cs b/src/common/ManagedCommon/LanguageHelper.cs new file mode 100644 index 0000000000..90791a03bc --- /dev/null +++ b/src/common/ManagedCommon/LanguageHelper.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.IO.Abstractions; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ManagedCommon +{ + public static class LanguageHelper + { + public const string SettingsFilePath = "\\Microsoft\\PowerToys\\"; + public const string SettingsFile = "language.json"; + + internal sealed class OutGoingLanguageSettings + { + [JsonPropertyName("language")] + public string LanguageTag { get; set; } + } + + public static string LoadLanguage() + { + FileSystem fileSystem = new FileSystem(); + var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var file = localAppDataDir + SettingsFilePath + SettingsFile; + + if (fileSystem.File.Exists(file)) + { + try + { + Stream inputStream = fileSystem.File.Open(file, FileMode.Open); + StreamReader reader = new StreamReader(inputStream); + string data = reader.ReadToEnd(); + inputStream.Close(); + reader.Dispose(); + + return JsonSerializer.Deserialize(data).LanguageTag; + } + catch (Exception) + { + } + } + + return string.Empty; + } + } +} diff --git a/src/common/interop/PowerToys.Interop.vcxproj b/src/common/interop/PowerToys.Interop.vcxproj index 8180339f37..88115f9eab 100644 --- a/src/common/interop/PowerToys.Interop.vcxproj +++ b/src/common/interop/PowerToys.Interop.vcxproj @@ -168,6 +168,11 @@ + + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + + diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index 814547ef0b..e20bc999d9 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -72,6 +72,8 @@ struct LogSettings inline const static std::string newLoggerName = "NewPlus"; inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher"; inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt"; + inline const static std::string workspacesWindowArrangerLoggerName = "workspaces-window-arranger"; + inline const static std::wstring workspacesWindowArrangerLogPath = L"workspaces-window-arranger-log.txt"; inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool"; inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.txt"; inline const static int retention = 30; diff --git a/src/common/utils/language_helper.h b/src/common/utils/language_helper.h new file mode 100644 index 0000000000..85448efef3 --- /dev/null +++ b/src/common/utils/language_helper.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +namespace LanguageHelpers +{ + inline std::wstring load_language() + { + std::filesystem::path languageJsonFilePath(PTSettingsHelper::get_root_save_folder_location() + L"\\language.json"); + + auto langJson = json::from_file(languageJsonFilePath.c_str()); + if (!langJson.has_value()) + { + return {}; + } + + std::wstring language = langJson->GetNamedString(L"language", L"").c_str(); + return language; + } +} diff --git a/src/common/utils/resources.h b/src/common/utils/resources.h index 00caff6f70..5cd0b36204 100644 --- a/src/common/utils/resources.h +++ b/src/common/utils/resources.h @@ -4,33 +4,204 @@ #include #include -// Get a string from the resource file -inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback) +#include + + +inline std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance) { - wchar_t* text_ptr; - auto length = LoadStringW(instance, resource_id, reinterpret_cast(&text_ptr), 0); - if (length == 0) + // Try to load en-us string as the first fallback. + WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); + + ATL::CStringW english_string; + try { - // Try to load en-us string as the first fallback. - WORD english_language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US); - ATL::CStringW english_string; + if (!english_string.LoadStringW(instance, resource_id, english_language)) + { + return {}; + } + } + catch (...) + { + return {}; + } + + return std::wstring(english_string); +} + +inline std::wstring get_resource_string_language_override(UINT resource_id, HINSTANCE instance) +{ + static std::wstring language = LanguageHelpers::load_language(); + unsigned lang = LANG_ENGLISH; + unsigned sublang = SUBLANG_ENGLISH_US; + + if (!language.empty()) + { + // Language list taken from Resources.wxs + if (language == L"ar-SA") + { + lang = LANG_ARABIC; + sublang = SUBLANG_ARABIC_SAUDI_ARABIA; + } + else if (language == L"cs-CZ") + { + lang = LANG_CZECH; + sublang = SUBLANG_CZECH_CZECH_REPUBLIC; + } + else if (language == L"de-DE") + { + lang = LANG_GERMAN; + sublang = SUBLANG_GERMAN; + } + else if (language == L"en-US") + { + lang = LANG_ENGLISH; + sublang = SUBLANG_ENGLISH_US; + } + else if (language == L"es-ES") + { + lang = LANG_SPANISH; + sublang = SUBLANG_SPANISH; + } + else if (language == L"fa-IR") + { + lang = LANG_PERSIAN; + sublang = SUBLANG_PERSIAN_IRAN; + } + else if (language == L"fr-FR") + { + lang = LANG_FRENCH; + sublang = SUBLANG_FRENCH; + } + else if (language == L"he-IL") + { + lang = LANG_HEBREW; + sublang = SUBLANG_HEBREW_ISRAEL; + } + else if (language == L"hu-HU") + { + lang = LANG_HUNGARIAN; + sublang = SUBLANG_HUNGARIAN_HUNGARY; + } + else if (language == L"it-IT") + { + lang = LANG_ITALIAN; + sublang = SUBLANG_ITALIAN; + } + else if (language == L"ja-JP") + { + lang = LANG_JAPANESE; + sublang = SUBLANG_JAPANESE_JAPAN; + } + else if (language == L"ko-KR") + { + lang = LANG_KOREAN; + sublang = SUBLANG_KOREAN; + } + else if (language == L"nl-NL") + { + lang = LANG_DUTCH; + sublang = SUBLANG_DUTCH; + } + else if (language == L"pl-PL") + { + lang = LANG_POLISH; + sublang = SUBLANG_POLISH_POLAND; + } + else if (language == L"pt-BR") + { + lang = LANG_PORTUGUESE; + sublang = SUBLANG_PORTUGUESE_BRAZILIAN; + } + else if (language == L"pt-PT") + { + lang = LANG_PORTUGUESE; + sublang = SUBLANG_PORTUGUESE; + } + else if (language == L"ru-RU") + { + lang = LANG_RUSSIAN; + sublang = SUBLANG_RUSSIAN_RUSSIA; + } + else if (language == L"sv-SE") + { + lang = LANG_SWEDISH; + sublang = SUBLANG_SWEDISH; + } + else if (language == L"tr-TR") + { + lang = LANG_TURKISH; + sublang = SUBLANG_TURKISH_TURKEY; + } + else if (language == L"uk-UA") + { + lang = LANG_UKRAINIAN; + sublang = SUBLANG_UKRAINIAN_UKRAINE; + } + else if (language == L"zh-CN") + { + lang = LANG_CHINESE_SIMPLIFIED; + sublang = SUBLANG_CHINESE_SIMPLIFIED; + } + else if (language == L"zh-TW") + { + lang = LANG_CHINESE_TRADITIONAL; + sublang = SUBLANG_CHINESE_TRADITIONAL; + } + + WORD languageID = MAKELANGID(lang, sublang); + ATL::CStringW result; try { - if (!english_string.LoadStringW(instance, resource_id, english_language)) + if (!result.LoadStringW(instance, resource_id, languageID)) { - return fallback; + return {}; } } catch (...) { - return fallback; + return {}; } - return std::wstring(english_string); + if (!result.IsEmpty()) + { + return std::wstring(result); + } + } + + return {}; +} + +// Get a string from the resource file +inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback) +{ + // Try to load en-us string as the first fallback. + std::wstring english_string = get_english_fallback_string(resource_id, instance); + + std::wstring language_override_resource = get_resource_string_language_override(resource_id, instance); + + if (!language_override_resource.empty()) + { + return language_override_resource; } else { - return { text_ptr, static_cast(length) }; + wchar_t* text_ptr; + auto length = LoadStringW(instance, resource_id, reinterpret_cast(&text_ptr), 0); + if (length == 0) + { + if (!english_string.empty()) + { + return std::wstring(english_string); + } + else + { + return fallback; + } + } + else + { + return { text_ptr, static_cast(length) }; + } } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs index 3e3fc9c4b5..3f990ef6fa 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs @@ -50,6 +50,12 @@ namespace AdvancedPaste /// public App() { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + this.InitializeComponent(); Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) => diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs index 53545bc72f..314e503ed8 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs @@ -6,6 +6,7 @@ using System; using AdvancedPaste.Helpers; using AdvancedPaste.Settings; +using AdvancedPaste.ViewModels; using ManagedCommon; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; @@ -26,18 +27,26 @@ namespace AdvancedPaste this.InitializeComponent(); _userSettings = App.GetService(); + var optionsViewModel = App.GetService(); var baseHeight = MinHeight; void UpdateHeight() { - var trimmedCustomActionCount = Math.Min(_userSettings.CustomActions.Count, 5); + var trimmedCustomActionCount = optionsViewModel.IsPasteWithAIEnabled ? Math.Min(_userSettings.CustomActions.Count, 5) : 0; Height = MinHeight = baseHeight + (trimmedCustomActionCount * 40); } UpdateHeight(); _userSettings.CustomActions.CollectionChanged += (_, _) => UpdateHeight(); + optionsViewModel.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(optionsViewModel.IsPasteWithAIEnabled)) + { + UpdateHeight(); + } + }; AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico"); this.ExtendsContentIntoTitleBar = true; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index def4d89d11..4d59e327af 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -51,6 +51,7 @@ namespace AdvancedPaste.ViewModels [ObservableProperty] [NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))] [NotifyPropertyChangedFor(nameof(GeneralErrorText))] + [NotifyPropertyChangedFor(nameof(IsPasteWithAIEnabled))] [NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))] private bool _isAllowedByGPO; @@ -67,7 +68,9 @@ namespace AdvancedPaste.ViewModels public ObservableCollection CustomActionPasteFormats { get; } = []; - public bool IsCustomAIEnabled => IsAllowedByGPO && IsClipboardDataText && aiHelper.IsAIEnabled; + public bool IsPasteWithAIEnabled => IsAllowedByGPO && aiHelper.IsAIEnabled; + + public bool IsCustomAIEnabled => IsPasteWithAIEnabled && IsClipboardDataText; public event EventHandler CustomActionActivated; @@ -94,6 +97,7 @@ namespace AdvancedPaste.ViewModels ClipboardHistoryEnabled = IsClipboardHistoryEnabled(); ReadClipboard(); + UpdateOpenAIKey(); _clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) }; _clipboardTimer.Tick += ClipboardTimer_Tick; _clipboardTimer.Start(); @@ -102,7 +106,7 @@ namespace AdvancedPaste.ViewModels _userSettings.CustomActions.CollectionChanged += (_, _) => EnqueueRefreshPasteFormats(); PropertyChanged += (_, e) => { - if (e.PropertyName == nameof(Query)) + if (e.PropertyName == nameof(Query) || e.PropertyName == nameof(IsPasteWithAIEnabled)) { EnqueueRefreshPasteFormats(); } @@ -158,11 +162,14 @@ namespace AdvancedPaste.ViewModels } CustomActionPasteFormats.Clear(); - foreach (var customAction in _userSettings.CustomActions) + if (IsPasteWithAIEnabled) { - if (Filter(customAction.Name) || Filter(customAction.Prompt)) + foreach (var customAction in _userSettings.CustomActions) { - CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText())); + if (Filter(customAction.Name) || Filter(customAction.Prompt)) + { + CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText())); + } } } } @@ -182,34 +189,19 @@ namespace AdvancedPaste.ViewModels public void OnShow() { ReadClipboard(); - UpdateAllowedByGPO(); - if (IsAllowedByGPO) + if (UpdateOpenAIKey()) { - var openAIKey = AICompletionsHelper.LoadOpenAIKey(); - var currentKey = aiHelper.GetKey(); - bool keyChanged = openAIKey != currentKey; + app.GetMainWindow()?.StartLoading(); - if (keyChanged) + _dispatcherQueue.TryEnqueue(() => { - app.GetMainWindow().StartLoading(); - - Task.Run(() => - { - aiHelper.SetOpenAIKey(openAIKey); - }).ContinueWith( - (t) => - { - _dispatcherQueue.TryEnqueue(() => - { - app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled); - OnPropertyChanged(nameof(InputTxtBoxPlaceholderText)); - OnPropertyChanged(nameof(GeneralErrorText)); - OnPropertyChanged(nameof(IsCustomAIEnabled)); - }); - }, - TaskScheduler.Default); - } + app.GetMainWindow()?.FinishLoading(aiHelper.IsAIEnabled); + OnPropertyChanged(nameof(InputTxtBoxPlaceholderText)); + OnPropertyChanged(nameof(GeneralErrorText)); + OnPropertyChanged(nameof(IsPasteWithAIEnabled)); + OnPropertyChanged(nameof(IsCustomAIEnabled)); + }); } ClipboardHistoryEnabled = IsClipboardHistoryEnabled(); @@ -462,7 +454,7 @@ namespace AdvancedPaste.ViewModels { Logger.LogTrace(); - if (string.IsNullOrWhiteSpace(inputInstructions)) + if (string.IsNullOrWhiteSpace(inputInstructions) || !IsCustomAIEnabled) { return string.Empty; } @@ -573,5 +565,20 @@ namespace AdvancedPaste.ViewModels { IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled; } + + private bool UpdateOpenAIKey() + { + UpdateAllowedByGPO(); + + if (IsAllowedByGPO) + { + var oldKey = aiHelper.GetKey(); + var newKey = AICompletionsHelper.LoadOpenAIKey(); + aiHelper.SetOpenAIKey(newKey); + return newKey != oldKey; + } + + return false; + } } } diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index 261fa43932..649342bc2f 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -13,7 +13,9 @@ #include #include #include +#include +#include #include #include #include @@ -54,6 +56,9 @@ namespace const wchar_t JSON_KEY_PASTE_AS_JSON_HOTKEY[] = L"paste-as-json-hotkey"; const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview"; const wchar_t JSON_KEY_VALUE[] = L"value"; + + const wchar_t OPENAI_VAULT_RESOURCE[] = L"https://platform.openai.com/api-keys"; + const wchar_t OPENAI_VAULT_USERNAME[] = L"PowerToys_AdvancedPaste_OpenAIKey"; } class AdvancedPaste : public PowertoyModuleIface @@ -133,6 +138,34 @@ private: return jsonObject; } + static bool open_ai_key_exists() + { + try + { + winrt::Windows::Security::Credentials::PasswordVault().Retrieve(OPENAI_VAULT_RESOURCE, OPENAI_VAULT_USERNAME); + return true; + } + catch (const winrt::hresult_error& ex) + { + // Looks like the only way to access the PasswordVault is through the an API that throws an exception in case the resource doesn't exist. + // If the debugger breaks here, just continue. + // If you want to disable breaking here in a more permanent way, just add a condition in Visual Studio's Exception Settings to not break on win::hresult_error, but that might make you not hit other exceptions you might want to catch. + if (ex.code() == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) + { + return false; // Credential doesn't exist. + } + Logger::error("Unexpected error while retrieving OpenAI key from vault: {}", winrt::to_string(ex.message())); + return false; + } + } + + bool is_open_ai_enabled() + { + return gpo_policy_enabled_configuration() != powertoys_gpo::gpo_rule_configured_disabled && + powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue() != powertoys_gpo::gpo_rule_configured_disabled && + open_ai_key_exists(); + } + bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey) { const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut"; @@ -216,15 +249,17 @@ private: if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS)) { const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE); - - for (const auto& customAction : customActions) + if (customActions.Size() > 0 && is_open_ai_enabled()) { - const auto object = customAction.GetObjectW(); - - if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) + for (const auto& customAction : customActions) { - m_custom_action_hotkeys.push_back(parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))); - m_custom_action_ids.push_back(static_cast(object.GetNamedNumber(JSON_KEY_ID))); + const auto object = customAction.GetObjectW(); + + if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) + { + m_custom_action_hotkeys.push_back(parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))); + m_custom_action_ids.push_back(static_cast(object.GetNamedNumber(JSON_KEY_ID))); + } } } } diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs index 61c8e33f17..bc6f5aa1da 100644 --- a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs @@ -44,6 +44,12 @@ namespace EnvironmentVariables /// public App() { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + this.InitializeComponent(); Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) => diff --git a/src/modules/FileLocksmith/FileLocksmithContextMenu/pch.h b/src/modules/FileLocksmith/FileLocksmithContextMenu/pch.h index 885d5d62e4..46064ef044 100644 --- a/src/modules/FileLocksmith/FileLocksmithContextMenu/pch.h +++ b/src/modules/FileLocksmith/FileLocksmithContextMenu/pch.h @@ -7,6 +7,8 @@ #ifndef PCH_H #define PCH_H +#include + // add headers that you want to pre-compile here #include "framework.h" diff --git a/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/App.xaml.cs b/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/App.xaml.cs index 8e5ff669de..e6186c46c8 100644 --- a/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/App.xaml.cs +++ b/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/App.xaml.cs @@ -23,6 +23,12 @@ namespace FileLocksmithUI /// public App() { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + Logger.InitializeLogger("\\File Locksmith\\FileLocksmithUI\\Logs"); this.InitializeComponent(); diff --git a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs index 27ab33fc87..cd4d8b177f 100644 --- a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs +++ b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs @@ -38,6 +38,12 @@ namespace Hosts /// public App() { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + InitializeComponent(); Host.HostInstance = Microsoft.Extensions.Hosting.Host. diff --git a/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/App.xaml.cs b/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/App.xaml.cs index 8e08ac0afd..728d541207 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/App.xaml.cs +++ b/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/App.xaml.cs @@ -24,6 +24,12 @@ namespace MeasureToolUI { Logger.InitializeLogger("\\Measure Tool\\MeasureToolUI\\Logs"); + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + this.InitializeComponent(); } diff --git a/src/modules/PowerOCR/PowerOCR/App.xaml.cs b/src/modules/PowerOCR/PowerOCR/App.xaml.cs index 045a961e26..eab9acd8cd 100644 --- a/src/modules/PowerOCR/PowerOCR/App.xaml.cs +++ b/src/modules/PowerOCR/PowerOCR/App.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.Threading; using System.Windows; @@ -28,6 +29,19 @@ public partial class App : Application, IDisposable { Logger.InitializeLogger("\\TextExtractor\\Logs"); + try + { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage); + } + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + NativeThreadCTS = new CancellationTokenSource(); } diff --git a/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs b/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs index 2edfd5c4b8..df86a35ed9 100644 --- a/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs +++ b/src/modules/Workspaces/WorkspacesEditor/App.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.Threading; using System.Windows; @@ -40,6 +41,20 @@ namespace WorkspacesEditor Logger.InitializeLogger("\\Workspaces\\Logs"); AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + var languageTag = LanguageHelper.LoadLanguage(); + + if (!string.IsNullOrEmpty(languageTag)) + { + try + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(languageTag); + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + } + const string appName = "Local\\PowerToys_Workspaces_Editor_InstanceMutex"; bool createdNew; _instanceMutex = new Mutex(true, appName, out createdNew); diff --git a/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs b/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs index c2fbd2ae5b..9f7be13742 100644 --- a/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs +++ b/src/modules/Workspaces/WorkspacesEditor/Models/Application.cs @@ -358,6 +358,37 @@ namespace WorkspacesEditor.Models if (_monitorSetup == null) { _monitorSetup = Parent.Monitors.Where(x => x.MonitorNumber == MonitorNumber).FirstOrDefault(); + if (_monitorSetup == null) + { + // monitors changed: try to determine monitor id based on middle point + int middleX = Position.X + (Position.Width / 2); + int middleY = Position.Y + (Position.Height / 2); + var monitorCandidate = Parent.Monitors.Where(x => + (x.MonitorDpiUnawareBounds.Left < middleX) && + (x.MonitorDpiUnawareBounds.Right > middleX) && + (x.MonitorDpiUnawareBounds.Top < middleY) && + (x.MonitorDpiUnawareBounds.Bottom > middleY)).FirstOrDefault(); + if (monitorCandidate != null) + { + _monitorSetup = monitorCandidate; + MonitorNumber = monitorCandidate.MonitorNumber; + } + else + { + // monitors and even the app's area unknown, set the main monitor (which is closer to (0,0)) as the app's monitor + monitorCandidate = Parent.Monitors.OrderBy(x => Math.Abs(x.MonitorDpiUnawareBounds.Left) + Math.Abs(x.MonitorDpiUnawareBounds.Top)).FirstOrDefault(); + if (monitorCandidate != null) + { + _monitorSetup = monitorCandidate; + MonitorNumber = monitorCandidate.MonitorNumber; + } + else + { + // no monitors defined at all. + Logger.LogError($"Wrong workspace setup. No monitors defined for the workspace: {Parent.Name}."); + } + } + } } return _monitorSetup; diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp index 73e89a40d8..f9aed31fe6 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp @@ -1,150 +1,27 @@ #include "pch.h" #include "AppLauncher.h" +#include + +#include + #include #include -#include -#include - -#include - -#include -#include -#include +#include #include -#include -#include - -#include -#include #include -#include using namespace winrt; using namespace Windows::Foundation; using namespace Windows::Management::Deployment; -namespace FancyZones +namespace AppLauncher { - inline bool allMonitorsHaveSameDpiScaling() + void UpdatePackagedApps(std::vector& apps, const Utils::Apps::AppList& installedApps) { - auto monitors = MonitorEnumerator::Enumerate(); - if (monitors.size() < 2) - { - return true; - } - - UINT firstMonitorDpiX; - UINT firstMonitorDpiY; - - if (S_OK != GetDpiForMonitor(monitors[0].first, MDT_EFFECTIVE_DPI, &firstMonitorDpiX, &firstMonitorDpiY)) - { - return false; - } - - for (int i = 1; i < monitors.size(); i++) - { - UINT iteratedMonitorDpiX; - UINT iteratedMonitorDpiY; - - if (S_OK != GetDpiForMonitor(monitors[i].first, MDT_EFFECTIVE_DPI, &iteratedMonitorDpiX, &iteratedMonitorDpiY) || - iteratedMonitorDpiX != firstMonitorDpiX) - { - return false; - } - } - - return true; - } - - inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect) - { - MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) }; - GetMonitorInfoW(monitor, &monitorInfo); - - auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left; - auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top; - - DPIAware::Convert(monitor, rect); - - auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset); - - // Now, this rect should be used to determine the monitor and thus taskbar size. This fixes - // scenarios where the zone lies approximately between two monitors, and the taskbar is on the left. - monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY); - GetMonitorInfoW(monitor, &monitorInfo); - - xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left; - yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top; - - rect.left -= xOffset; - rect.right -= xOffset; - rect.top -= yOffset; - rect.bottom -= yOffset; - } - - inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept - { - WINDOWPLACEMENT placement{}; - ::GetWindowPlacement(window, &placement); - - if (isMinimized) - { - placement.showCmd = SW_MINIMIZE; - } - else - { - if ((placement.showCmd != SW_SHOWMINIMIZED) && - (placement.showCmd != SW_MINIMIZE)) - { - if (placement.showCmd == SW_SHOWMAXIMIZED) - placement.flags &= ~WPF_RESTORETOMAXIMIZED; - - placement.showCmd = SW_RESTORE; - } - - ScreenToWorkAreaCoords(window, monitor, rect); - placement.rcNormalPosition = rect; - } - - placement.flags |= WPF_ASYNCWINDOWPLACEMENT; - - auto result = ::SetWindowPlacement(window, &placement); - if (!result) - { - Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError())); - return false; - } - - // make sure window is moved to the correct monitor before maximize. - if (isMaximized) - { - placement.showCmd = SW_SHOWMAXIMIZED; - } - - // Do it again, allowing Windows to resize the window and set correct scaling - // This fixes Issue #365 - result = ::SetWindowPlacement(window, &placement); - if (!result) - { - Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError())); - return false; - } - - return true; - } -} - -namespace -{ - LaunchingApps Prepare(std::vector& apps, const Utils::Apps::AppList& installedApps) - { - LaunchingApps launchedApps{}; - launchedApps.reserve(apps.size()); - for (auto& app : apps) { // Packaged apps have version in the path, it will be outdated after update. @@ -160,322 +37,173 @@ namespace Logger::trace(L"Updated package full name for {}: {}", app.name, app.packageFullName); } } - - launchedApps.push_back({ app, nullptr, L"waiting" }); } - - return launchedApps; } - bool AllWindowsFound(const LaunchingApps& launchedApps) + Result LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated) { - return std::find_if(launchedApps.begin(), launchedApps.end(), [&](const LaunchingApp& val) { - return val.window == nullptr; - }) == launchedApps.end(); - }; + std::wstring dir = std::filesystem::path(appPath).parent_path(); - bool AddOpenedWindows(LaunchingApps& launchedApps, const std::vector& windows, const Utils::Apps::AppList& installedApps) - { - bool statusChanged = false; - for (HWND window : windows) + SHELLEXECUTEINFO sei = { 0 }; + sei.cbSize = sizeof(SHELLEXECUTEINFO); + sei.hwnd = nullptr; + sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE; + sei.lpVerb = elevated ? L"runas" : L"open"; + sei.lpFile = appPath.c_str(); + sei.lpParameters = commandLineArgs.c_str(); + sei.lpDirectory = dir.c_str(); + sei.nShow = SW_SHOWNORMAL; + + if (!ShellExecuteEx(&sei)) { - auto installedAppData = Utils::Apps::GetApp(window, installedApps); - if (!installedAppData.has_value()) - { - continue; - } + std::wstring error = get_last_error_or_default(GetLastError()); + Logger::error(L"Failed to launch process. {}", error); + return Error(error); + } - auto insertionIter = launchedApps.end(); - for (auto iter = launchedApps.begin(); iter != launchedApps.end(); ++iter) + return Ok(sei); + } + + bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors) + { + try + { + PackageManager packageManager; + for (const auto& package : packageManager.FindPackagesForUser({})) { - if (iter->window == nullptr && installedAppData.value().name == iter->application.name) + if (package.Id().FullName() == packageFullName) { - insertionIter = iter; + auto getAppListEntriesOperation = package.GetAppListEntriesAsync(); + auto appEntries = getAppListEntriesOperation.get(); + + if (appEntries.Size() > 0) + { + IAsyncOperation launchOperation = appEntries.GetAt(0).LaunchAsync(); + bool launchResult = launchOperation.get(); + return launchResult; + } + else + { + Logger::error(L"No app entries found for the package."); + launchErrors.push_back({ packageFullName, L"No app entries found for the package." }); + } } - - // keep the window at the same position if it's already opened - WINDOWPLACEMENT placement{}; - ::GetWindowPlacement(window, &placement); - HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY); - UINT dpi = DPIAware::DEFAULT_DPI; - DPIAware::GetScreenDPIForMonitor(monitor, dpi); - - float x = static_cast(placement.rcNormalPosition.left); - float y = static_cast(placement.rcNormalPosition.top); - float width = static_cast(placement.rcNormalPosition.right - placement.rcNormalPosition.left); - float height = static_cast(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top); - - DPIAware::InverseConvert(monitor, x, y); - DPIAware::InverseConvert(monitor, width, height); - - WorkspacesData::WorkspacesProject::Application::Position windowPosition{ - .x = static_cast(std::round(x)), - .y = static_cast(std::round(y)), - .width = static_cast(std::round(width)), - .height = static_cast(std::round(height)), - }; - if (iter->application.position == windowPosition) - { - Logger::debug(L"{} window already found at {} {}.", iter->application.name, iter->application.position.x, iter->application.position.y); - insertionIter = iter; - break; - } - } - - if (insertionIter != launchedApps.end()) - { - insertionIter->window = window; - insertionIter->state = L"launched"; - statusChanged = true; - } - - if (AllWindowsFound(launchedApps)) - { - break; } } - return statusChanged; - } -} + catch (const hresult_error& ex) + { + Logger::error(L"Packaged app launching error: {}", ex.message()); + launchErrors.push_back({ packageFullName, ex.message().c_str() }); + } -bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, ErrorList& launchErrors) -{ - std::wstring dir = std::filesystem::path(appPath).parent_path(); - - SHELLEXECUTEINFO sei = { 0 }; - sei.cbSize = sizeof(SHELLEXECUTEINFO); - sei.hwnd = nullptr; - sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE; - sei.lpVerb = elevated ? L"runas" : L"open"; - sei.lpFile = appPath.c_str(); - sei.lpParameters = commandLineArgs.c_str(); - sei.lpDirectory = dir.c_str(); - sei.nShow = SW_SHOWNORMAL; - - if (!ShellExecuteEx(&sei)) - { - auto error = GetLastError(); - Logger::error(L"Failed to launch process. {}", get_last_error_or_default(error)); - launchErrors.push_back({ std::filesystem::path(appPath).filename(), get_last_error_or_default(error) }); return false; } - return true; -} - -bool LaunchPackagedApp(const std::wstring& packageFullName, ErrorList& launchErrors) -{ - try + bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors) { - PackageManager packageManager; - for (const auto& package : packageManager.FindPackagesForUser({})) - { - if (package.Id().FullName() == packageFullName) - { - auto getAppListEntriesOperation = package.GetAppListEntriesAsync(); - auto appEntries = getAppListEntriesOperation.get(); + bool launched{ false }; - if (appEntries.Size() > 0) + // packaged apps: check protocol in registry + // usage example: Settings with cmd args + if (!app.packageFullName.empty()) + { + auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName); + if (!names.empty()) + { + Logger::trace(L"Launching packaged by protocol with command line args {}", app.name); + + std::wstring uriProtocolName = names[0]; + std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs); + + auto res = LaunchApp(command, L"", app.isElevated); + if (res.isOk()) { - IAsyncOperation launchOperation = appEntries.GetAt(0).LaunchAsync(); - bool launchResult = launchOperation.get(); - return launchResult; + launched = true; } else { - Logger::error(L"No app entries found for the package."); - launchErrors.push_back({ packageFullName, L"No app entries found for the package." }); + launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() }); } } - } - } - catch (const hresult_error& ex) - { - Logger::error(L"Packaged app launching error: {}", ex.message()); - launchErrors.push_back({ packageFullName, ex.message().c_str() }); - } - - return false; -} - -bool Launch(const WorkspacesData::WorkspacesProject::Application& app, ErrorList& launchErrors) -{ - bool launched{ false }; - - // packaged apps: check protocol in registry - // usage example: Settings with cmd args - if (!app.packageFullName.empty()) - { - auto names = RegistryUtils::GetUriProtocolNames(app.packageFullName); - if (!names.empty()) - { - Logger::trace(L"Launching packaged by protocol with command line args {}", app.name); - - std::wstring uriProtocolName = names[0]; - std::wstring command = std::wstring(uriProtocolName + (app.commandLineArgs.starts_with(L":") ? L"" : L":") + app.commandLineArgs); - - launched = LaunchApp(command, L"", app.isElevated, launchErrors); - } - else - { - Logger::info(L"Uri protocol names not found for {}", app.packageFullName); - } - } - - // packaged apps: try launching first by AppUserModel.ID - // usage example: elevated Terminal - if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty()) - { - Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId); - launched = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated, launchErrors); - } - - // packaged apps: try launching by package full name - // doesn't work for elevated apps or apps with command line args - if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated) - { - Logger::trace(L"Launching packaged app {}", app.name); - launched = LaunchPackagedApp(app.packageFullName, launchErrors); - } - - if (!launched) - { - Logger::trace(L"Launching {} at {}", app.name, app.path); - - DWORD dwAttrib = GetFileAttributesW(app.path.c_str()); - if (dwAttrib == INVALID_FILE_ATTRIBUTES) - { - Logger::error(L"File not found at {}", app.path); - launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" }); - return false; - } - - launched = LaunchApp(app.path, app.commandLineArgs, app.isElevated, launchErrors); - } - - Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path); - return launched; -} - -bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector& monitors, ErrorList& launchErrors) -{ - bool launchedSuccessfully{ true }; - - LauncherUIHelper uiHelper; - uiHelper.LaunchUI(); - - // Get the set of windows before launching the app - std::vector windowsBefore = WindowEnumerator::Enumerate(WindowFilter::Filter); - auto installedApps = Utils::Apps::GetAppsList(); - auto launchedApps = Prepare(project.apps, installedApps); - - uiHelper.UpdateLaunchStatus(launchedApps); - - // Launch apps - for (auto& app : launchedApps) - { - if (!app.window) - { - if (!Launch(app.application, launchErrors)) + else { - Logger::error(L"Failed to launch {}", app.application.name); - app.state = L"failed"; - uiHelper.UpdateLaunchStatus(launchedApps); - launchedSuccessfully = false; + Logger::info(L"Uri protocol names not found for {}", app.packageFullName); } } + + // packaged apps: try launching first by AppUserModel.ID + // usage example: elevated Terminal + if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty()) + { + Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId); + auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated); + if (res.isOk()) + { + launched = true; + } + else + { + launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() }); + } + } + + // packaged apps: try launching by package full name + // doesn't work for elevated apps or apps with command line args + if (!launched && !app.packageFullName.empty() && app.commandLineArgs.empty() && !app.isElevated) + { + Logger::trace(L"Launching packaged app {}", app.name); + launched = LaunchPackagedApp(app.packageFullName, launchErrors); + } + + if (!launched) + { + Logger::trace(L"Launching {} at {}", app.name, app.path); + + DWORD dwAttrib = GetFileAttributesW(app.path.c_str()); + if (dwAttrib == INVALID_FILE_ATTRIBUTES) + { + Logger::error(L"File not found at {}", app.path); + launchErrors.push_back({ std::filesystem::path(app.path).filename(), L"File not found" }); + return false; + } + + auto res = LaunchApp(app.path, app.commandLineArgs, app.isElevated); + if (res.isOk()) + { + launched = true; + } + else + { + launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() }); + } + } + + Logger::trace(L"{} {} at {}", app.name, (launched ? L"launched" : L"not launched"), app.path); + return launched; } - // Get newly opened windows after launching apps, keep retrying for 5 seconds - Logger::trace(L"Find new windows"); - for (int attempt = 0; attempt < 50 && !AllWindowsFound(launchedApps); attempt++) + bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors) { - std::vector windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter); - std::vector windowsDiff{}; - std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(windowsBefore.begin(), windowsBefore.end(), window) == windowsBefore.end(); }); - if (AddOpenedWindows(launchedApps, windowsDiff, installedApps)) + bool launchedSuccessfully{ true }; + + auto installedApps = Utils::Apps::GetAppsList(); + UpdatePackagedApps(project.apps, installedApps); + + // Launch apps + for (auto& app : project.apps) { - uiHelper.UpdateLaunchStatus(launchedApps); + if (!Launch(app, launchErrors)) + { + Logger::error(L"Failed to launch {}", app.name); + launchingStatus.Update(app, LaunchingState::Failed); + launchedSuccessfully = false; + } + else + { + launchingStatus.Update(app, LaunchingState::Launched); + } } - // check if all windows were found - if (AllWindowsFound(launchedApps)) - { - Logger::trace(L"All windows found."); - break; - } - else - { - Logger::trace(L"Not all windows found, retry."); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } + return launchedSuccessfully; } - - // Check single-instance app windows - Logger::trace(L"Find single-instance app windows"); - if (!AllWindowsFound(launchedApps)) - { - if (AddOpenedWindows(launchedApps, WindowEnumerator::Enumerate(WindowFilter::Filter), installedApps)) - { - uiHelper.UpdateLaunchStatus(launchedApps); - } - } - - // Place windows - for (const auto& [app, window, status] : launchedApps) - { - if (window == nullptr) - { - Logger::warn(L"{} window not found.", app.name); - launchedSuccessfully = false; - continue; - } - - auto snapMonitorIter = std::find_if(project.monitors.begin(), project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; }); - if (snapMonitorIter == project.monitors.end()) - { - Logger::error(L"No monitor saved for launching the app"); - continue; - } - - bool launchMinimized = app.isMinimized; - bool launchMaximized = app.isMaximized; - - HMONITOR currentMonitor{}; - UINT currentDpi = DPIAware::DEFAULT_DPI; - auto currentMonitorIter = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; }); - if (currentMonitorIter != monitors.end()) - { - currentMonitor = currentMonitorIter->monitor; - currentDpi = currentMonitorIter->dpi; - } - else - { - currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY); - DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi); - launchMinimized = true; - launchMaximized = false; - - } - - RECT rect = app.position.toRect(); - float mult = static_cast(snapMonitorIter->dpi) / currentDpi; - rect.left = static_cast(std::round(rect.left * mult)); - rect.right = static_cast(std::round(rect.right * mult)); - rect.top = static_cast(std::round(rect.top * mult)); - rect.bottom = static_cast(std::round(rect.bottom * mult)); - - if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect)) - { - WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window); - Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); - } - else - { - Logger::error(L"Failed placing {}", app.name); - launchedSuccessfully = false; - } - } - - return launchedSuccessfully; -} +} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h index 03f6a6d06d..0827afc4c9 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h +++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.h @@ -1,7 +1,16 @@ #pragma once +#include + +#include +#include #include -using ErrorList = std::vector>; +namespace AppLauncher +{ + using ErrorList = std::vector>; -bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector& monitors, ErrorList& launchErrors); \ No newline at end of file + Result LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated); + + bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors); +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp b/src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp new file mode 100644 index 0000000000..b660d0d939 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp @@ -0,0 +1,123 @@ +#include "pch.h" +#include "Launcher.h" + +#include + +#include + +#include + +#include + +Launcher::Launcher(const WorkspacesData::WorkspacesProject& project, + std::vector& workspaces, + InvokePoint invokePoint) : + m_project(project), + m_workspaces(workspaces), + m_invokePoint(invokePoint), + m_start(std::chrono::high_resolution_clock::now()), + m_uiHelper(std::make_unique()), + m_windowArrangerHelper(std::make_unique(std::bind(&Launcher::handleWindowArrangerMessage, this, std::placeholders::_1))), + m_launchingStatus(m_project, std::bind(&LauncherUIHelper::UpdateLaunchStatus, m_uiHelper.get(), std::placeholders::_1)) +{ + m_uiHelper->LaunchUI(); + m_uiHelper->UpdateLaunchStatus(m_launchingStatus.Get()); + + bool launchElevated = std::find_if(m_project.apps.begin(), m_project.apps.end(), [](const WorkspacesData::WorkspacesProject::Application& app) { return app.isElevated; }) != m_project.apps.end(); + m_windowArrangerHelper->Launch(m_project.id, launchElevated, [&]() -> bool + { + if (m_launchingStatus.AllLaunchedAndMoved()) + { + return false; + } + + if (m_launchingStatus.AllLaunched()) + { + static auto arrangerTimeDelay = std::chrono::high_resolution_clock::now(); + auto currentTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration timeDiff = currentTime - arrangerTimeDelay; + if (timeDiff.count() >= 5) + { + return false; + } + } + + return true; + }); +} + +Launcher::~Launcher() +{ + Logger::trace(L"Finalizing launch"); + + // update last-launched time + if (m_invokePoint != InvokePoint::LaunchAndEdit) + { + time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + m_project.lastLaunchedTime = launchedTime; + for (int i = 0; i < m_workspaces.size(); i++) + { + if (m_workspaces[i].id == m_project.id) + { + m_workspaces[i] = m_project; + break; + } + } + json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(m_workspaces)); + } + + // telemetry + auto end = std::chrono::high_resolution_clock::now(); + std::chrono::duration duration = end - m_start; + Logger::trace(L"Launching time: {} s", duration.count()); + + auto monitors = MonitorUtils::IdentifyMonitors(); + bool differentSetup = monitors.size() != m_project.monitors.size(); + if (!differentSetup) + { + for (const auto& monitor : m_project.monitors) + { + auto setup = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.dpi == monitor.dpi && val.monitorRectDpiAware == monitor.monitorRectDpiAware; }); + if (setup == monitors.end()) + { + differentSetup = true; + break; + } + } + } + + Trace::Workspaces::Launch(m_launchedSuccessfully, m_project, m_invokePoint, duration.count(), differentSetup, m_launchErrors); +} + +void Launcher::Launch() +{ + Logger::info(L"Launch Workspace {} : {}", m_project.name, m_project.id); + m_launchedSuccessfully = AppLauncher::Launch(m_project, m_launchingStatus, m_launchErrors); +} + +void Launcher::handleWindowArrangerMessage(const std::wstring& msg) +{ + if (msg == L"ready") + { + Launch(); + } + else + { + try + { + auto data = WorkspacesData::AppLaunchInfoJSON::FromJson(json::JsonValue::Parse(msg).GetObjectW()); + if (data.has_value()) + { + m_launchingStatus.Update(data.value().application, data.value().state); + } + else + { + Logger::error(L"Failed to parse message from WorkspacesWindowArranger"); + } + } + catch (const winrt::hresult_error&) + { + Logger::error(L"Failed to parse message from WorkspacesWindowArranger"); + } + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/Launcher.h b/src/modules/Workspaces/WorkspacesLauncher/Launcher.h new file mode 100644 index 0000000000..36f17329d2 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/Launcher.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +class Launcher +{ +public: + Launcher(const WorkspacesData::WorkspacesProject& project, std::vector& workspaces, InvokePoint invokePoint); + ~Launcher(); + + void Launch(); + +private: + WorkspacesData::WorkspacesProject m_project; + std::vector& m_workspaces; + const InvokePoint m_invokePoint; + const std::chrono::steady_clock::time_point m_start; + std::unique_ptr m_uiHelper; + std::unique_ptr m_windowArrangerHelper; + LaunchingStatus m_launchingStatus; + bool m_launchedSuccessfully{}; + std::vector> m_launchErrors{}; + + void handleWindowArrangerMessage(const std::wstring& msg); +}; diff --git a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp index e1d06ac0f9..b35c6d3657 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.cpp @@ -7,12 +7,22 @@ #include #include +#include + +LauncherUIHelper::LauncherUIHelper() : + m_processId{}, + m_ipcHelper(IPCHelperStrings::LauncherUIPipeName, IPCHelperStrings::UIPipeName, nullptr) +{ +} + LauncherUIHelper::~LauncherUIHelper() { OnThreadExecutor().submit(OnThreadExecutor::task_t{ [&] { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, uiProcessId); + Logger::info(L"Stopping WorkspacesLauncherUI with pid {}", m_processId); + + HANDLE uiProcess = OpenProcess(PROCESS_ALL_ACCESS, false, m_processId); if (uiProcess) { bool res = TerminateProcess(uiProcess, 0); @@ -25,54 +35,39 @@ LauncherUIHelper::~LauncherUIHelper() { Logger::error(L"Unable to find UI process: {}", get_last_error_or_default(GetLastError())); } - - std::filesystem::remove(WorkspacesData::LaunchWorkspacesFile()); } }).wait(); } void LauncherUIHelper::LaunchUI() { Logger::trace(L"Starting WorkspacesLauncherUI"); - - STARTUPINFO info = { sizeof(info) }; - PROCESS_INFORMATION pi = { 0 }; + TCHAR buffer[MAX_PATH] = { 0 }; GetModuleFileName(NULL, buffer, MAX_PATH); std::wstring path = std::filesystem::path(buffer).parent_path(); - path.append(L"\\PowerToys.WorkspacesLauncherUI.exe"); - auto succeeded = CreateProcessW(path.c_str(), nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &info, &pi); - if (succeeded) + + auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesLauncherUI.exe", L"", false); + if (res.isOk()) { - if (pi.hProcess) - { - uiProcessId = pi.dwProcessId; - CloseHandle(pi.hProcess); - } - if (pi.hThread) - { - CloseHandle(pi.hThread); - } + auto value = res.value(); + m_processId = GetProcessId(value.hProcess); + CloseHandle(value.hProcess); + Logger::info(L"WorkspacesLauncherUI started with pid {}", m_processId); } else { - Logger::error(L"CreateProcessW() failed. {}", get_last_error_or_default(GetLastError())); + Logger::error(L"Failed to launch PowerToys.WorkspacesLauncherUI: {}", res.error()); } } -void LauncherUIHelper::UpdateLaunchStatus(LaunchingApps launchedApps) +void LauncherUIHelper::UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const { - WorkspacesData::AppLaunchData appData = WorkspacesData::AppLaunchData(); - appData.appLaunchInfoList.reserve(launchedApps.size()); + WorkspacesData::AppLaunchData appData; appData.launcherProcessID = GetCurrentProcessId(); - for (auto& app : launchedApps) + for (auto& [app, data] : launchedApps) { - WorkspacesData::AppLaunchInfo appLaunchInfo = WorkspacesData::AppLaunchInfo(); - appLaunchInfo.name = app.application.name; - appLaunchInfo.path = app.application.path; - appLaunchInfo.state = app.state; - - appData.appLaunchInfoList.push_back(appLaunchInfo); + appData.appsStateList.insert({ app, { app, nullptr, data.state } }); } - json::to_file(WorkspacesData::LaunchWorkspacesFile(), WorkspacesData::AppLaunchDataJSON::ToJson(appData)); + m_ipcHelper.send(WorkspacesData::AppLaunchDataJSON::ToJson(appData).ToString().c_str()); } diff --git a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h index 53fe78f931..20704f13a2 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h +++ b/src/modules/Workspaces/WorkspacesLauncher/LauncherUIHelper.h @@ -1,16 +1,18 @@ #pragma once -#include +#include +#include class LauncherUIHelper { public: - LauncherUIHelper() = default; + LauncherUIHelper(); ~LauncherUIHelper(); void LaunchUI(); - void UpdateLaunchStatus(LaunchingApps launchedApps); + void UpdateLaunchStatus(WorkspacesData::LaunchingAppStateMap launchedApps) const; private: - DWORD uiProcessId; + DWORD m_processId; + IPCHelper m_ipcHelper; }; diff --git a/src/modules/Workspaces/WorkspacesLauncher/LaunchingApp.h b/src/modules/Workspaces/WorkspacesLauncher/LaunchingApp.h deleted file mode 100644 index c2daae31f2..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncher/LaunchingApp.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include -#include - -struct LaunchingApp -{ - WorkspacesData::WorkspacesProject::Application application; - HWND window; - std::wstring state; -}; - -using LaunchingApps = std::vector; \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.cpp b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.cpp new file mode 100644 index 0000000000..600038ea61 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.cpp @@ -0,0 +1,71 @@ +#include "pch.h" +#include "WindowArrangerHelper.h" + +#include + +#include +#include + +#include + +#include + +WindowArrangerHelper::WindowArrangerHelper(std::function ipcCallback) : + m_processId{}, + m_ipcHelper(IPCHelperStrings::LauncherArrangerPipeName, IPCHelperStrings::WindowArrangerPipeName, ipcCallback) +{ +} + +WindowArrangerHelper::~WindowArrangerHelper() +{ + Logger::info(L"Stopping WorkspacesWindowArranger with pid {}", m_processId); + + HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, m_processId); + if (process) + { + bool res = TerminateProcess(process, 0); + if (!res) + { + Logger::error(L"Unable to terminate PowerToys.WorkspacesWindowArranger process: {}", get_last_error_or_default(GetLastError())); + } + } + else + { + Logger::error(L"Unable to find PowerToys.WorkspacesWindowArranger process: {}", get_last_error_or_default(GetLastError())); + } +} + +void WindowArrangerHelper::Launch(const std::wstring& projectId, bool elevated, std::function keepWaitingCallback) +{ + Logger::trace(L"Starting WorkspacesWindowArranger"); + + TCHAR buffer[MAX_PATH] = { 0 }; + GetModuleFileName(NULL, buffer, MAX_PATH); + std::wstring path = std::filesystem::path(buffer).parent_path(); + + auto res = AppLauncher::LaunchApp(path + L"\\PowerToys.WorkspacesWindowArranger.exe", projectId, elevated); + if (res.isOk()) + { + auto value = res.value(); + m_processId = GetProcessId(value.hProcess); + Logger::info(L"WorkspacesWindowArranger started with pid {}", m_processId); + std::atomic_bool timeoutExpired = false; + m_threadExecutor.submit(OnThreadExecutor::task_t{ + [&] { + HANDLE process = value.hProcess; + while (keepWaitingCallback()) + { + WaitForSingleObject(process, 100); + } + + Logger::trace(L"Finished waiting WorkspacesWindowArranger"); + CloseHandle(process); + }}).wait(); + + timeoutExpired = true; + } + else + { + Logger::error(L"Failed to launch PowerToys.WorkspacesWindowArranger: {}", res.error()); + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.h b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.h new file mode 100644 index 0000000000..22d7e3ea1c --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncher/WindowArrangerHelper.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include + +class WindowArrangerHelper +{ +public: + WindowArrangerHelper(std::function ipcCallback); + ~WindowArrangerHelper(); + + void Launch(const std::wstring& projectId, bool elevated, std::function keepWaitingCallback); + +private: + DWORD m_processId; + IPCHelper m_ipcHelper; + OnThreadExecutor m_threadExecutor; +}; diff --git a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj index 7d69c20533..8ff482677c 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj +++ b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj @@ -127,21 +127,23 @@ + Create + + - - + diff --git a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters index 326bec179f..191fdb87af 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesLauncher/WorkspacesLauncher.vcxproj.filters @@ -27,13 +27,13 @@ Header Files - - Header Files - Header Files - + + Header Files + + Header Files @@ -53,6 +53,12 @@ Source Files + + Source Files + + + Source Files + diff --git a/src/modules/Workspaces/WorkspacesLauncher/main.cpp b/src/modules/Workspaces/WorkspacesLauncher/main.cpp index 98d6319231..38e88acd48 100644 --- a/src/modules/Workspaces/WorkspacesLauncher/main.cpp +++ b/src/modules/Workspaces/WorkspacesLauncher/main.cpp @@ -1,15 +1,4 @@ -#include "pch.h" - -#include -#include - -#include -#include - -#include - -#include -#include +#include "pch.h" #include #include @@ -18,6 +7,13 @@ #include #include +#include +#include + +#include + +#include + const std::wstring moduleName = L"Workspaces\\WorkspacesLauncher"; const std::wstring internalPath = L""; @@ -32,6 +28,15 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm return 0; } + std::wstring cmdLineStr{ GetCommandLineW() }; + auto cmdArgs = split(cmdLineStr, L" "); + if (cmdArgs.workspaceId.empty()) + { + Logger::warn("Incorrect command line arguments: no workspace id"); + MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); + return 1; + } + if (is_process_elevated()) { Logger::warn("Workspaces Launcher is elevated, restart"); @@ -45,7 +50,9 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm std::string cmdLineStr(cmdline); std::wstring cmdLineWStr(cmdLineStr.begin(), cmdLineStr.end()); - run_non_elevated(exe_path.get(), cmdLineWStr, nullptr, modulePath.c_str()); + std::wstring cmd = cmdArgs.workspaceId + L" " + std::to_wstring(cmdArgs.invokePoint); + + RunNonElevatedEx(exe_path.get(), cmd, modulePath); return 1; } @@ -58,116 +65,59 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - std::string cmdLineStr(cmdline); - auto cmdArgs = split(cmdLineStr, " "); - if (cmdArgs.size() < 1) - { - Logger::warn("Incorrect command line arguments"); - MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; - } - - std::wstring id(cmdArgs[0].begin(), cmdArgs[0].end()); - if (id.empty()) - { - Logger::warn("Incorrect command line arguments: no workspace id"); - MessageBox(NULL, GET_RESOURCE_STRING(IDS_INCORRECT_ARGS).c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; - } - - InvokePoint invokePoint = InvokePoint::EditorButton; - if (cmdArgs.size() > 1) - { - try - { - invokePoint = static_cast(std::stoi(cmdArgs[1])); - } - catch (std::exception) - { - } - } - - Logger::trace(L"Invoke point: {}", invokePoint); + Logger::trace(L"Invoke point: {}", cmdArgs.invokePoint); // read workspaces std::vector workspaces; WorkspacesData::WorkspacesProject projectToLaunch{}; - if (invokePoint == InvokePoint::LaunchAndEdit) + if (cmdArgs.invokePoint == InvokePoint::LaunchAndEdit) { // check the temp file in case the project is just created and not saved to the workspaces.json yet - if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile())) + auto file = WorkspacesData::TempWorkspacesFile(); + auto res = JsonUtils::ReadSingleWorkspace(file); + if (res.isOk() && projectToLaunch.id == cmdArgs.workspaceId) { - try + projectToLaunch = res.getValue(); + } + else if (res.isError()) + { + std::wstring formattedMessage{}; + switch (res.error()) { - auto savedWorkspacesJson = json::from_file(WorkspacesData::TempWorkspacesFile()); - if (savedWorkspacesJson.has_value()) - { - auto savedWorkspaces = WorkspacesData::WorkspacesProjectJSON::FromJson(savedWorkspacesJson.value()); - if (savedWorkspaces.has_value()) - { - if (savedWorkspaces.value().id == id) - { - projectToLaunch = savedWorkspaces.value(); - } - } - else - { - Logger::critical("Incorrect Workspaces file"); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile()); - MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; - } - } - else - { - Logger::critical("Incorrect Workspaces file"); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::TempWorkspacesFile()); - MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; - } - } - catch (std::exception ex) - { - Logger::critical("Exception on reading Workspaces file: {}", ex.what()); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::TempWorkspacesFile()); - MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; + case JsonUtils::WorkspacesFileError::FileReadingError: + formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file); + break; + case JsonUtils::WorkspacesFileError::IncorrectFileError: + formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file); + break; } + + MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); + return 1; } } if (projectToLaunch.id.empty()) { - try + auto file = WorkspacesData::WorkspacesFile(); + auto res = JsonUtils::ReadWorkspaces(file); + if (res.isOk()) { - auto savedWorkspacesJson = json::from_file(WorkspacesData::WorkspacesFile()); - if (savedWorkspacesJson.has_value()) - { - auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value()); - if (savedWorkspaces.has_value()) - { - workspaces = savedWorkspaces.value(); - } - else - { - Logger::critical("Incorrect Workspaces file"); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile()); - MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; - } - } - else - { - Logger::critical("Incorrect Workspaces file"); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), WorkspacesData::WorkspacesFile()); - MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); - return 1; - } + workspaces = res.getValue(); } - catch (std::exception ex) + else { - Logger::critical("Exception on reading Workspaces file: {}", ex.what()); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), WorkspacesData::WorkspacesFile()); + std::wstring formattedMessage{}; + switch (res.error()) + { + case JsonUtils::WorkspacesFileError::FileReadingError: + formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_FILE_READING_ERROR), file); + break; + case JsonUtils::WorkspacesFileError::IncorrectFileError: + formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_INCORRECT_FILE_ERROR), file); + break; + } + MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); return 1; } @@ -175,14 +125,14 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm if (workspaces.empty()) { Logger::warn("Workspaces file is empty"); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), WorkspacesData::WorkspacesFile()); + std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_EMPTY_FILE), file); MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); return 1; } for (const auto& proj : workspaces) { - if (proj.id == id) + if (proj.id == cmdArgs.workspaceId) { projectToLaunch = proj; break; @@ -192,56 +142,15 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm if (projectToLaunch.id.empty()) { - Logger::critical(L"Workspace {} not found", id); - std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_PROJECT_NOT_FOUND), id); + Logger::critical(L"Workspace {} not found", cmdArgs.workspaceId); + std::wstring formattedMessage = fmt::format(GET_RESOURCE_STRING(IDS_PROJECT_NOT_FOUND), cmdArgs.workspaceId); MessageBox(NULL, formattedMessage.c_str(), GET_RESOURCE_STRING(IDS_WORKSPACES).c_str(), MB_ICONERROR | MB_OK); return 1; } - // launch apps - Logger::info(L"Launch Workspace {} : {}", projectToLaunch.name, projectToLaunch.id); - auto monitors = MonitorUtils::IdentifyMonitors(); - std::vector> launchErrors{}; - auto start = std::chrono::high_resolution_clock::now(); - bool launchedSuccessfully = Launch(projectToLaunch, monitors, launchErrors); - - // update last-launched time - if (invokePoint != InvokePoint::LaunchAndEdit) - { - time_t launchedTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - projectToLaunch.lastLaunchedTime = launchedTime; - for (int i = 0; i < workspaces.size(); i++) - { - if (workspaces[i].id == projectToLaunch.id) - { - workspaces[i] = projectToLaunch; - break; - } - } - json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(workspaces)); - } - - // telemetry - auto end = std::chrono::high_resolution_clock::now(); - std::chrono::duration duration = end - start; - Logger::trace(L"Launching time: {} s", duration.count()); - - bool differentSetup = monitors.size() != projectToLaunch.monitors.size(); - if (!differentSetup) - { - for (const auto& monitor : projectToLaunch.monitors) - { - auto setup = std::find_if(monitors.begin(), monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.dpi == monitor.dpi && val.monitorRectDpiAware == monitor.monitorRectDpiAware; }); - if (setup == monitors.end()) - { - differentSetup = true; - break; - } - } - } - - Trace::Workspaces::Launch(launchedSuccessfully, projectToLaunch, invokePoint, duration.count(), differentSetup, launchErrors); + Launcher launcher(projectToLaunch, workspaces, cmdArgs.invokePoint); + Logger::trace("Finished"); CoUninitialize(); return 0; } diff --git a/src/modules/Workspaces/WorkspacesLauncher/utils.h b/src/modules/Workspaces/WorkspacesLauncher/utils.h deleted file mode 100644 index 571db60d4d..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncher/utils.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include - -std::vector split(std::string s, const std::string& delimiter) -{ - std::vector tokens; - size_t pos = 0; - std::string token; - while ((pos = s.find(delimiter)) != std::string::npos) - { - token = s.substr(0, pos); - tokens.push_back(token); - s.erase(0, pos + delimiter.length()); - } - tokens.push_back(s); - - return tokens; -} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs b/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs index 0709a59c09..a065918523 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/App.xaml.cs @@ -3,13 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.Threading; using System.Windows; -using System.Windows.Forms.Design.Behavior; - using Common.UI; using ManagedCommon; -using WorkspacesLauncherUI.Utils; +using PowerToys.Interop; using WorkspacesLauncherUI.ViewModels; namespace WorkspacesLauncherUI @@ -21,6 +20,9 @@ namespace WorkspacesLauncherUI { private static Mutex _instanceMutex; + // Create an instance of the IPC wrapper. + private static TwoWayPipeMessageIPCManaged ipcmanager; + private StatusWindow _mainWindow; private MainViewModel _mainViewModel; @@ -29,21 +31,37 @@ namespace WorkspacesLauncherUI private bool _isDisposed; + public static Action IPCMessageReceivedCallback { get; set; } + public App() { } private void OnStartup(object sender, StartupEventArgs e) { - Logger.InitializeLogger("\\Workspaces\\Logs"); + Logger.InitializeLogger("\\Workspaces\\WorkspacesLauncherUI"); AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; - const string appName = "Local\\PowerToys_Workspaces_Launcher_InstanceMutex"; + var languageTag = LanguageHelper.LoadLanguage(); + + if (!string.IsNullOrEmpty(languageTag)) + { + try + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(languageTag); + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + } + + const string appName = "Local\\PowerToys_Workspaces_LauncherUI_InstanceMutex"; bool createdNew; _instanceMutex = new Mutex(true, appName, out createdNew); if (!createdNew) { - Logger.LogWarning("Another instance of Workspaces Launcher is already running. Exiting this instance."); + Logger.LogWarning("Another instance of Workspaces Launcher UI is already running. Exiting this instance."); _instanceMutex = null; Shutdown(0); return; @@ -56,6 +74,15 @@ namespace WorkspacesLauncherUI return; } + ipcmanager = new TwoWayPipeMessageIPCManaged("\\\\.\\pipe\\powertoys_workspaces_ui_", "\\\\.\\pipe\\powertoys_workspaces_launcher_ui_", (string message) => + { + if (IPCMessageReceivedCallback != null && message.Length > 0) + { + IPCMessageReceivedCallback(message); + } + }); + ipcmanager.Start(); + ThemeManager = new ThemeManager(this); if (_mainViewModel == null) @@ -97,6 +124,10 @@ namespace WorkspacesLauncherUI if (disposing) { ThemeManager?.Dispose(); + + ipcmanager?.End(); + ipcmanager?.Dispose(); + _instanceMutex?.Dispose(); } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs index 6415e18713..dc19b86647 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchData.cs @@ -1,33 +1,16 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; -using System.Threading.Tasks; - using Workspaces.Data; -using WorkspacesLauncherUI.Utils; - using static WorkspacesLauncherUI.Data.AppLaunchData; -using static WorkspacesLauncherUI.Data.AppLaunchInfoData; using static WorkspacesLauncherUI.Data.AppLaunchInfosData; namespace WorkspacesLauncherUI.Data { - internal sealed class AppLaunchData : WorkspacesEditorData + public class AppLaunchData : WorkspacesUIData { - public static string File - { - get - { - return FolderUtils.DataFolder() + "\\launch-workspaces.json"; - } - } - public struct AppLaunchDataWrapper { [JsonPropertyName("apps")] diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs index 2d339f0603..aa64510ba9 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfoData.cs @@ -1,32 +1,23 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json.Serialization; -using System.Threading.Tasks; - using Workspaces.Data; using static WorkspacesLauncherUI.Data.AppLaunchInfoData; namespace WorkspacesLauncherUI.Data { - public class AppLaunchInfoData : WorkspacesEditorData + public class AppLaunchInfoData : WorkspacesUIData { public struct AppLaunchInfoWrapper { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("path")] - public string Path { get; set; } + [JsonPropertyName("application")] + public ApplicationWrapper Application { get; set; } [JsonPropertyName("state")] - public string State { get; set; } + public LaunchingState State { get; set; } } } } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs index ecb37fd48c..cb00cb4478 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/AppLaunchInfosData.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -13,7 +12,7 @@ using static WorkspacesLauncherUI.Data.AppLaunchInfosData; namespace WorkspacesLauncherUI.Data { - public class AppLaunchInfosData : WorkspacesEditorData + public class AppLaunchInfosData : WorkspacesUIData { public struct AppLaunchInfoListWrapper { diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/ApplicationWrapper.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/ApplicationWrapper.cs new file mode 100644 index 0000000000..51bb1a24c6 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/ApplicationWrapper.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace WorkspacesLauncherUI.Data +{ + public struct ApplicationWrapper + { + public string Application { get; set; } + + public string ApplicationPath { get; set; } + + public string Title { get; set; } + + public string PackageFullName { get; set; } + + public string AppUserModelId { get; set; } + + public string CommandLineArguments { get; set; } + + public bool IsElevated { get; set; } + + public bool CanLaunchElevated { get; set; } + + public bool Minimized { get; set; } + + public bool Maximized { get; set; } + + public PositionWrapper Position { get; set; } + + public int Monitor { get; set; } + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/LaunchingState.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/LaunchingState.cs new file mode 100644 index 0000000000..9ad8c958d2 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/LaunchingState.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace WorkspacesLauncherUI.Data +{ + // sync with WorkspacesLib : LaunchingStateEnum.h + public enum LaunchingState + { + Waiting = 0, + Launched, + LaunchedAndMoved, + Failed, + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/PositionWrapper.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/PositionWrapper.cs new file mode 100644 index 0000000000..6b39c7d918 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/PositionWrapper.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace WorkspacesLauncherUI.Data +{ + public struct PositionWrapper + { + public int X { get; set; } + + public int Y { get; set; } + + public int Width { get; set; } + + public int Height { get; set; } + + public static bool operator ==(PositionWrapper left, PositionWrapper right) + { + return left.X == right.X && left.Y == right.Y && left.Width == right.Width && left.Height == right.Height; + } + + public static bool operator !=(PositionWrapper left, PositionWrapper right) + { + return left.X != right.X || left.Y != right.Y || left.Width != right.Width || left.Height != right.Height; + } + + public override bool Equals(object obj) + { + if (obj == null || GetType() != obj.GetType()) + { + return false; + } + + PositionWrapper pos = (PositionWrapper)obj; + return X == pos.X && Y == pos.Y && Width == pos.Width && Height == pos.Height; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesEditorData`1.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs similarity index 82% rename from src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesEditorData`1.cs rename to src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs index c2599b92d5..5e9b88a728 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesEditorData`1.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Data/WorkspacesUIData`1.cs @@ -8,7 +8,7 @@ using WorkspacesLauncherUI.Utils; namespace Workspaces.Data { - public class WorkspacesEditorData + public class WorkspacesUIData { protected JsonSerializerOptions JsonOptions { @@ -22,10 +22,8 @@ namespace Workspaces.Data } } - public T Read(string file) + public T Deserialize(string data) { - IOUtils ioUtils = new IOUtils(); - string data = ioUtils.ReadFile(file); return JsonSerializer.Deserialize(data, JsonOptions); } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs b/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs index 9f7ea0beda..c7e87e39fc 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/HeadingTextBlock.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Windows; using System.Windows.Automation.Peers; using System.Windows.Controls; diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs index 82fc47860b..bd0f50b467 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/Models/AppLaunching.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -13,9 +13,9 @@ using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Windows.Media; using System.Windows.Media.Imaging; - using ManagedCommon; using Windows.Management.Deployment; +using WorkspacesLauncherUI.Data; namespace WorkspacesLauncherUI.Models { @@ -28,9 +28,9 @@ namespace WorkspacesLauncherUI.Models PropertyChanged?.Invoke(this, e); } - public string AppPath { get; set; } + public ApplicationWrapper Application { get; set; } - public bool Loading => LaunchState == "waiting"; + public bool Loading => LaunchState == LaunchingState.Waiting || LaunchState == LaunchingState.Launched; private Icon _icon; @@ -51,12 +51,12 @@ namespace WorkspacesLauncherUI.Models } else { - _icon = Icon.ExtractAssociatedIcon(AppPath); + _icon = Icon.ExtractAssociatedIcon(Application.ApplicationPath); } } catch (Exception) { - Logger.LogWarning($"Icon not found on app path: {AppPath}. Using default icon"); + Logger.LogWarning($"Icon not found on app path: {Application.ApplicationPath}. Using default icon"); IsNotFound = true; _icon = new Icon(@"images\DefaultIcon.ico"); } @@ -66,16 +66,22 @@ namespace WorkspacesLauncherUI.Models } } - public string Name { get; set; } + public string Name + { + get + { + return Application.Application; + } + } - public string LaunchState { get; set; } + public LaunchingState LaunchState { get; set; } public string StateGlyph { get => LaunchState switch { - "launched" => "\U0000F78C", - "failed" => "\U0000EF2C", + LaunchingState.LaunchedAndMoved => "\U0000F78C", + LaunchingState.Failed => "\U0000EF2C", _ => "\U0000EF2C", }; } @@ -84,8 +90,8 @@ namespace WorkspacesLauncherUI.Models { get => LaunchState switch { - "launched" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)), - "failed" => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)), + LaunchingState.LaunchedAndMoved => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 0, 128, 0)), + LaunchingState.Failed => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)), _ => new SolidColorBrush(System.Windows.Media.Color.FromArgb(255, 254, 0, 0)), }; } @@ -139,13 +145,13 @@ namespace WorkspacesLauncherUI.Models { if (_isPackagedApp == null) { - if (!AppPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase)) + if (!Application.ApplicationPath.StartsWith("C:\\Program Files\\WindowsApps\\", StringComparison.InvariantCultureIgnoreCase)) { _isPackagedApp = false; } else { - string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty); + string appPath = Application.ApplicationPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty); Regex packagedAppPathRegex = new Regex(@"(?[^_]*)_\d+.\d+.\d+.\d+_x64__(?[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); Match match = packagedAppPathRegex.Match(appPath); _isPackagedApp = match.Success; @@ -200,7 +206,7 @@ namespace WorkspacesLauncherUI.Models } catch (Exception e) { - Logger.LogError($"Exception while drawing icon for app with path: {AppPath}. Exception message: {e.Message}"); + Logger.LogError($"Exception while drawing icon for app with path: {Application.ApplicationPath}. Exception message: {e.Message}"); } } diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/FolderUtils.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Utils/FolderUtils.cs deleted file mode 100644 index 3501c2045c..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/FolderUtils.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; - -namespace WorkspacesLauncherUI.Utils -{ - public class FolderUtils - { - public static string Desktop() - { - return Environment.GetFolderPath(Environment.SpecialFolder.Desktop); - } - - public static string Temp() - { - return Path.GetTempPath(); - } - - // Note: the same path should be used in SnapshotTool and Launcher - public static string DataFolder() - { - return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces"; - } - } -} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/IOUtils.cs b/src/modules/Workspaces/WorkspacesLauncherUI/Utils/IOUtils.cs deleted file mode 100644 index 39b6ea47bb..0000000000 --- a/src/modules/Workspaces/WorkspacesLauncherUI/Utils/IOUtils.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using System.IO.Abstractions; -using System.Threading.Tasks; - -namespace WorkspacesLauncherUI.Utils -{ - public class IOUtils - { - private readonly IFileSystem _fileSystem = new FileSystem(); - - public IOUtils() - { - } - - public void WriteFile(string fileName, string data) - { - _fileSystem.File.WriteAllText(fileName, data); - } - - public string ReadFile(string fileName) - { - if (_fileSystem.File.Exists(fileName)) - { - var attempts = 0; - while (attempts < 10) - { - try - { - using (Stream inputStream = _fileSystem.File.Open(fileName, FileMode.Open)) - using (StreamReader reader = new StreamReader(inputStream)) - { - string data = reader.ReadToEnd(); - inputStream.Close(); - return data; - } - } - catch (Exception) - { - Task.Delay(10).Wait(); - } - - attempts++; - } - } - - return string.Empty; - } - } -} diff --git a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs index b7d63dafe8..ed6cdd8f31 100644 --- a/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs +++ b/src/modules/Workspaces/WorkspacesLauncherUI/ViewModels/MainViewModel.cs @@ -7,9 +7,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; -using System.IO; -using System.IO.Abstractions; - using ManagedCommon; using WorkspacesLauncherUI.Data; using WorkspacesLauncherUI.Models; @@ -20,8 +17,6 @@ namespace WorkspacesLauncherUI.ViewModels { public ObservableCollection AppsListed { get; set; } = new ObservableCollection(); - private IFileSystemWatcher _watcher; - private System.Timers.Timer selfDestroyTimer; private StatusWindow _snapshotWindow; private int launcherProcessID; private bool _exiting; @@ -36,60 +31,43 @@ namespace WorkspacesLauncherUI.ViewModels public MainViewModel() { _exiting = false; - LoadAppLaunchInfos(); - string fileName = Path.GetFileName(AppLaunchData.File); - _watcher = Microsoft.PowerToys.Settings.UI.Library.Utilities.Helper.GetFileWatcher("Workspaces", fileName, () => AppLaunchInfoStateChanged()); + + // receive IPC Message + App.IPCMessageReceivedCallback = (string msg) => + { + try + { + AppLaunchData parser = new AppLaunchData(); + AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Deserialize(msg); + HandleAppLaunchingState(appLaunchData); + } + catch (Exception ex) + { + Logger.LogError(ex.Message); + } + }; } - private void AppLaunchInfoStateChanged() - { - LoadAppLaunchInfos(); - } - - private void LoadAppLaunchInfos() + private void HandleAppLaunchingState(AppLaunchData.AppLaunchDataWrapper appLaunchData) { if (_exiting) { return; } - AppLaunchData parser = new AppLaunchData(); - if (!File.Exists(AppLaunchData.File)) - { - Logger.LogWarning($"AppLaunchInfosData storage file not found: {AppLaunchData.File}"); - return; - } - - AppLaunchData.AppLaunchDataWrapper appLaunchData = parser.Read(AppLaunchData.File); - launcherProcessID = appLaunchData.LauncherProcessID; - List appLaunchingList = new List(); - bool allLaunched = true; foreach (var app in appLaunchData.AppLaunchInfos.AppLaunchInfoList) { appLaunchingList.Add(new AppLaunching() { - Name = app.Name, - AppPath = app.Path, + Application = app.Application, LaunchState = app.State, }); - if (app.State != "launched" && app.State != "failed") - { - allLaunched = false; - } } AppsListed = new ObservableCollection(appLaunchingList); OnPropertyChanged(new PropertyChangedEventArgs(nameof(AppsListed))); - - if (allLaunched) - { - selfDestroyTimer = new System.Timers.Timer(); - selfDestroyTimer.Interval = 1000; - selfDestroyTimer.Elapsed += SelfDestroy; - selfDestroyTimer.Start(); - } } private void SelfDestroy(object source, System.Timers.ElapsedEventArgs e) @@ -113,7 +91,6 @@ namespace WorkspacesLauncherUI.ViewModels internal void CancelLaunch() { _exiting = true; - _watcher.Dispose(); Process proc = Process.GetProcessById(launcherProcessID); proc.Kill(); } diff --git a/src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp b/src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp new file mode 100644 index 0000000000..6c888f434d --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp @@ -0,0 +1,42 @@ +#include "pch.h" +#include "IPCHelper.h" + +#include + +IPCHelper::IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function messageCallback) : + callback(messageCallback) +{ + HANDLE hToken = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) + { + Logger::error("Failed to get process token"); + return; + } + + std::unique_lock lock{ ipcMutex }; + ipc = make_unique(currentPipeName, receiverPipeName, std::bind(&IPCHelper::receive, this, std::placeholders::_1)); + ipc->start(hToken); +} + +IPCHelper::~IPCHelper() +{ + std::unique_lock lock{ ipcMutex }; + if (ipc) + { + ipc->end(); + ipc = nullptr; + } +} + +void IPCHelper::send(const std::wstring& message) const +{ + ipc->send(message); +} + +void IPCHelper::receive(const std::wstring& msg) +{ + if (callback) + { + callback(msg); + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/IPCHelper.h b/src/modules/Workspaces/WorkspacesLib/IPCHelper.h new file mode 100644 index 0000000000..4ccf9cb5e3 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/IPCHelper.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace IPCHelperStrings +{ + static std::wstring LauncherUIPipeName(L"\\\\.\\pipe\\powertoys_workspaces_launcher_ui_"); + static std::wstring UIPipeName(L"\\\\.\\pipe\\powertoys_workspaces_ui_"); + + static std::wstring LauncherArrangerPipeName(L"\\\\.\\pipe\\powertoys_workspaces_launcher_arranger_"); + static std::wstring WindowArrangerPipeName(L"\\\\.\\pipe\\powertoys_workspaces_window_arranger_"); +} + +class IPCHelper +{ +public: + IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function messageCallback); + ~IPCHelper(); + + void send(const std::wstring& message) const; + +private: + void receive(const std::wstring& msg); + + std::unique_ptr ipc; + std::mutex ipcMutex; + std::function callback; +}; diff --git a/src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp b/src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp new file mode 100644 index 0000000000..039000213f --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp @@ -0,0 +1,106 @@ +#include "pch.h" +#include "JsonUtils.h" + +#include + +#include + +namespace JsonUtils +{ + Result ReadSingleWorkspace(const std::wstring& fileName) + { + if (std::filesystem::exists(fileName)) + { + try + { + auto tempWorkspacesJson = json::from_file(fileName); + if (tempWorkspacesJson.has_value()) + { + auto tempWorkspace = WorkspacesData::WorkspacesProjectJSON::FromJson(tempWorkspacesJson.value()); + if (tempWorkspace.has_value()) + { + return Ok(tempWorkspace.value()); + } + else + { + Logger::critical("Incorrect Workspaces file"); + return Error(WorkspacesFileError::IncorrectFileError); + } + } + else + { + Logger::critical("Incorrect Workspaces file"); + return Error(WorkspacesFileError::IncorrectFileError); + } + } + catch (std::exception ex) + { + Logger::critical("Exception on reading Workspaces file: {}", ex.what()); + return Error(WorkspacesFileError::FileReadingError); + } + } + + return Ok(WorkspacesData::WorkspacesProject{}); + } + + Result, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName) + { + try + { + auto savedWorkspacesJson = json::from_file(fileName); + if (savedWorkspacesJson.has_value()) + { + auto savedWorkspaces = WorkspacesData::WorkspacesListJSON::FromJson(savedWorkspacesJson.value()); + if (savedWorkspaces.has_value()) + { + return Ok(savedWorkspaces.value()); + } + else + { + Logger::critical("Incorrect Workspaces file"); + return Error(WorkspacesFileError::IncorrectFileError); + } + } + else + { + Logger::critical("Incorrect Workspaces file"); + return Error(WorkspacesFileError::IncorrectFileError); + } + } + catch (std::exception ex) + { + Logger::critical("Exception on reading Workspaces file: {}", ex.what()); + return Error(WorkspacesFileError::FileReadingError); + } + } + + bool Write(const std::wstring& fileName, const std::vector& projects) + { + try + { + json::to_file(fileName, WorkspacesData::WorkspacesListJSON::ToJson(projects)); + } + catch (std::exception ex) + { + Logger::error("Error writing workspaces file. {}", ex.what()); + return false; + } + + return true; + } + + bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project) + { + try + { + json::to_file(fileName, WorkspacesData::WorkspacesProjectJSON::ToJson(project)); + } + catch (std::exception ex) + { + Logger::error("Error writing workspaces file. {}", ex.what()); + return false; + } + + return true; + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/JsonUtils.h b/src/modules/Workspaces/WorkspacesLib/JsonUtils.h new file mode 100644 index 0000000000..012b9bf702 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/JsonUtils.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace JsonUtils +{ + enum class WorkspacesFileError + { + FileReadingError, + IncorrectFileError, + }; + + Result ReadSingleWorkspace(const std::wstring& fileName); + Result, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName); + + bool Write(const std::wstring& fileName, const std::vector& projects); + bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project); +} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h b/src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h new file mode 100644 index 0000000000..2fbaf2fe97 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h @@ -0,0 +1,10 @@ +#pragma once + +// sync with WorkspacesLauncherUI : Data : LaunchingState.cs +enum class LaunchingState +{ + Waiting = 0, + Launched, + LaunchedAndMoved, + Failed +}; \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp new file mode 100644 index 0000000000..46c4bb6e3f --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp @@ -0,0 +1,65 @@ +#include "pch.h" +#include "LaunchingStatus.h" + +#include + +LaunchingStatus::LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function updateCallback) : + m_updateCallback(updateCallback) +{ + std::unique_lock lock(m_mutex); + for (const auto& app : project.apps) + { + m_appsState.insert({ app, { app, nullptr, LaunchingState::Waiting } }); + } +} + +const WorkspacesData::LaunchingAppStateMap& LaunchingStatus::Get() noexcept +{ + std::shared_lock lock(m_mutex); + return m_appsState; +} + +bool LaunchingStatus::AllLaunchedAndMoved() noexcept +{ + std::shared_lock lock(m_mutex); + for (const auto& [app, data] : m_appsState) + { + if (data.state != LaunchingState::Failed && data.state != LaunchingState::LaunchedAndMoved) + { + return false; + } + } + + return true; +} + +bool LaunchingStatus::AllLaunched() noexcept +{ + std::shared_lock lock(m_mutex); + for (const auto& [app, data] : m_appsState) + { + if (data.state == LaunchingState::Waiting) + { + return false; + } + } + + return true; +} + +void LaunchingStatus::Update(const WorkspacesData::WorkspacesProject::Application& app, LaunchingState state) +{ + std::unique_lock lock(m_mutex); + if (!m_appsState.contains(app)) + { + Logger::error(L"Error updating state: app {} is not tracked in the project", app.name); + return; + } + + m_appsState[app].state = state; + + if (m_updateCallback) + { + m_updateCallback(m_appsState); + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h new file mode 100644 index 0000000000..eec0b1b0f6 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +#include + +class LaunchingStatus +{ +public: + LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function updateCallback); + ~LaunchingStatus() = default; + + bool AllLaunchedAndMoved() noexcept; + bool AllLaunched() noexcept; + const WorkspacesData::LaunchingAppStateMap& Get() noexcept; + + void Update(const WorkspacesData::WorkspacesProject::Application& app, LaunchingState state); + +private: + WorkspacesData::LaunchingAppStateMap m_appsState; + std::function m_updateCallback; + std::shared_mutex m_mutex; +}; diff --git a/src/modules/Workspaces/WorkspacesLib/Result.h b/src/modules/Workspaces/WorkspacesLib/Result.h new file mode 100644 index 0000000000..56a5101b42 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/Result.h @@ -0,0 +1,53 @@ +#pragma once + +#include + +template +class Ok +{ +public: + explicit constexpr Ok(T value) : + value(std::move(value)) {} + + constexpr T&& get() { return std::move(value); } + + T value; +}; + +template +class Error +{ +public: + explicit constexpr Error(T value) : + value(std::move(value)) {} + + constexpr T&& get() { return std::move(value); } + + T value; +}; + +template +class Result +{ +public: + using VariantT = std::variant, Error>; + + constexpr Result(Ok value) : + variant(std::move(value)) + {} + + constexpr Result(Error value) : + variant(std::move(value)) + {} + + constexpr bool isOk() const { return std::holds_alternative>(variant); } + constexpr bool isError() const { return std::holds_alternative>(variant); } + + constexpr OkT value() const { return std::get>(variant).value; } + constexpr ErrT error() const { return std::get>(variant).value; } + + constexpr OkT&& getValue() { return std::get>(variant).get(); } + constexpr ErrT&& getError() { return std::get>(variant).get(); } + + VariantT variant; +}; diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp index b9748aa993..d71619f90b 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.cpp @@ -21,12 +21,6 @@ namespace WorkspacesData std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey); return settingsFolderPath + L"\\temp-workspaces.json"; } - - std::wstring LaunchWorkspacesFile() - { - std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey); - return settingsFolderPath + L"\\launch-workspaces.json"; - } RECT WorkspacesProject::Application::Position::toRect() const noexcept { @@ -420,19 +414,40 @@ namespace WorkspacesData { namespace NonLocalizable { - const static wchar_t* NameID = L"name"; - const static wchar_t* PathID = L"path"; + const static wchar_t* ApplicationID = L"application"; const static wchar_t* StateID = L"state"; } - json::JsonObject ToJson(const AppLaunchInfo& data) + json::JsonObject ToJson(const LaunchingAppState& data) { json::JsonObject json{}; - json.SetNamedValue(NonLocalizable::NameID, json::value(data.name)); - json.SetNamedValue(NonLocalizable::PathID, json::value(data.path)); - json.SetNamedValue(NonLocalizable::StateID, json::value(data.state)); + json.SetNamedValue(NonLocalizable::ApplicationID, WorkspacesProjectJSON::ApplicationJSON::ToJson(data.application)); + json.SetNamedValue(NonLocalizable::StateID, json::value(static_cast(data.state))); return json; } + + std::optional FromJson(const json::JsonObject& json) + { + LaunchingAppState result{}; + + try + { + auto app = WorkspacesProjectJSON::ApplicationJSON::FromJson(json.GetNamedObject(NonLocalizable::ApplicationID)); + if (!app.has_value()) + { + return std::nullopt; + } + + result.application = app.value(); + result.state = static_cast(json.GetNamedNumber(NonLocalizable::StateID)); + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + + return result; + } } namespace AppLaunchInfoListJSON @@ -442,18 +457,46 @@ namespace WorkspacesData const static wchar_t* AppLaunchInfoID = L"appLaunchInfos"; } - json::JsonObject ToJson(const std::vector& data) + json::JsonObject ToJson(const LaunchingAppStateMap& data) { json::JsonObject json{}; json::JsonArray appLaunchInfoArray{}; for (const auto& appLaunchInfo : data) { - appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo)); + appLaunchInfoArray.Append(AppLaunchInfoJSON::ToJson(appLaunchInfo.second)); } json.SetNamedValue(NonLocalizable::AppLaunchInfoID, appLaunchInfoArray); return json; } + + std::optional FromJson(const json::JsonObject& json) + { + LaunchingAppStateMap result{}; + + try + { + auto array = json.GetNamedArray(NonLocalizable::AppLaunchInfoID); + for (uint32_t i = 0; i < array.Size(); ++i) + { + auto obj = AppLaunchInfoJSON::FromJson(array.GetObjectAt(i)); + if (obj.has_value()) + { + result.insert({ obj.value().application, obj.value() }); + } + else + { + return std::nullopt; + } + } + } + catch (const winrt::hresult_error&) + { + return std::nullopt; + } + + return result; + } } namespace AppLaunchDataJSON @@ -467,7 +510,7 @@ namespace WorkspacesData json::JsonObject ToJson(const AppLaunchData& data) { json::JsonObject json{}; - json.SetNamedValue(NonLocalizable::AppsID, AppLaunchInfoListJSON::ToJson(data.appLaunchInfoList)); + json.SetNamedValue(NonLocalizable::AppsID, AppLaunchInfoListJSON::ToJson(data.appsStateList)); json.SetNamedValue(NonLocalizable::ProcessID, json::value(data.launcherProcessID)); return json; } diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h index 908552530f..40252850a3 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesData.h @@ -2,11 +2,12 @@ #include +#include + namespace WorkspacesData { std::wstring WorkspacesFile(); std::wstring TempWorkspacesFile(); - std::wstring LaunchWorkspacesFile(); struct WorkspacesProject { @@ -21,10 +22,7 @@ namespace WorkspacesData RECT toRect() const noexcept; - inline bool operator==(const Position& other) const noexcept - { - return x == other.x && y == other.y && width == other.width && height == other.height; - } + auto operator<=>(const Position&) const = default; }; std::wstring name; @@ -39,6 +37,8 @@ namespace WorkspacesData bool isMaximized{}; Position position{}; unsigned int monitor{}; + + auto operator<=>(const Application&) const = default; }; struct Monitor @@ -80,34 +80,22 @@ namespace WorkspacesData std::vector projects; }; - struct AppLaunchInfo + struct LaunchingAppState { - std::wstring name; - std::wstring path; - std::wstring state; + WorkspacesData::WorkspacesProject::Application application; + HWND window{}; + LaunchingState state { LaunchingState::Waiting }; }; - namespace AppLaunchInfoJSON - { - json::JsonObject ToJson(const AppLaunchInfo& data); - } - - namespace AppLaunchInfoListJSON - { - json::JsonObject ToJson(const std::vector& data); - } + using LaunchingAppStateMap = std::map; + using LaunchingAppStateList = std::vector>; struct AppLaunchData { - std::vector appLaunchInfoList; + LaunchingAppStateMap appsStateList; int launcherProcessID = 0; }; - namespace AppLaunchDataJSON - { - json::JsonObject ToJson(const AppLaunchData& data); - } - namespace WorkspacesProjectJSON { namespace ApplicationJSON @@ -143,4 +131,22 @@ namespace WorkspacesData json::JsonObject ToJson(const std::vector& data); std::optional> FromJson(const json::JsonObject& json); } + + namespace AppLaunchInfoJSON + { + json::JsonObject ToJson(const LaunchingAppState& data); + std::optional FromJson(const json::JsonObject& json); + } + + namespace AppLaunchInfoListJSON + { + json::JsonObject ToJson(const LaunchingAppStateMap& data); + std::optional FromJson(const json::JsonObject& json); + } + + namespace AppLaunchDataJSON + { + json::JsonObject ToJson(const AppLaunchData& data); + } + }; diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj index 4ef6d283ec..f3aa1424b0 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj @@ -33,19 +33,32 @@ + + + + + + + + + Create + + + {f055103b-f80b-4d0c-bf48-057c55620033} + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters index f30fd53623..c910f65015 100644 --- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters @@ -23,6 +23,24 @@ Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + @@ -37,6 +55,18 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + diff --git a/src/modules/Workspaces/WorkspacesLib/two_way_pipe_message_ipc.cpp b/src/modules/Workspaces/WorkspacesLib/two_way_pipe_message_ipc.cpp new file mode 100644 index 0000000000..40b2f1dbe5 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/two_way_pipe_message_ipc.cpp @@ -0,0 +1,469 @@ +#include "pch.h" + +#include + +#include + +constexpr DWORD BUFSIZE = 1024; + +TwoWayPipeMessageIPC::TwoWayPipeMessageIPC( + std::wstring _input_pipe_name, + std::wstring _output_pipe_name, + callback_function p_func) : + impl(new TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl( + _input_pipe_name, + _output_pipe_name, + p_func)) +{ +} + +TwoWayPipeMessageIPC::~TwoWayPipeMessageIPC() +{ + delete impl; +} + +void TwoWayPipeMessageIPC::send(std::wstring msg) +{ + impl->send(msg); +} + +void TwoWayPipeMessageIPC::start(HANDLE _restricted_pipe_token) +{ + impl->start(_restricted_pipe_token); +} + +void TwoWayPipeMessageIPC::end() +{ + impl->end(); +} + +TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::TwoWayPipeMessageIPCImpl( + std::wstring _input_pipe_name, + std::wstring _output_pipe_name, + callback_function p_func) +{ + input_pipe_name = _input_pipe_name; + output_pipe_name = _output_pipe_name; + dispatch_inc_message_function = p_func; +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::send(std::wstring msg) +{ + output_queue.queue_message(msg); +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::start(HANDLE _restricted_pipe_token) +{ + output_queue_thread = std::thread(&TwoWayPipeMessageIPCImpl::consume_output_queue_thread, this); + input_queue_thread = std::thread(&TwoWayPipeMessageIPCImpl::consume_input_queue_thread, this); + input_pipe_thread = std::thread(&TwoWayPipeMessageIPCImpl::start_named_pipe_server, this, _restricted_pipe_token); +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::end() +{ + closed = true; + input_queue.interrupt(); + input_queue_thread.join(); + output_queue.interrupt(); + output_queue_thread.join(); + pipe_connect_handle_mutex.lock(); + if (current_connect_pipe_handle != NULL) + { + //Cancels the Pipe currently waiting for a connection. + CancelIoEx(current_connect_pipe_handle, NULL); + } + pipe_connect_handle_mutex.unlock(); + input_pipe_thread.join(); +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::send_pipe_message(std::wstring message) +{ + // Adapted from https://learn.microsoft.com/windows/win32/ipc/named-pipe-client + HANDLE output_pipe_handle; + const wchar_t* message_send = message.c_str(); + BOOL fSuccess = FALSE; + DWORD cbToWrite, cbWritten, dwMode; + const wchar_t* lpszPipename = output_pipe_name.c_str(); + + // Try to open a named pipe; wait for it, if necessary. + + while (1) + { + output_pipe_handle = CreateFile( + lpszPipename, // pipe name + GENERIC_READ | // read and write access + GENERIC_WRITE, + 0, // no sharing + NULL, // default security attributes + OPEN_EXISTING, // opens existing pipe + 0, // default attributes + NULL); // no template file + + // Break if the pipe handle is valid. + + if (output_pipe_handle != INVALID_HANDLE_VALUE) + break; + + // Exit if an error other than ERROR_PIPE_BUSY occurs. + DWORD curr_error = 0; + if ((curr_error = GetLastError()) != ERROR_PIPE_BUSY) + { + return; + } + + // All pipe instances are busy, so wait for 20 seconds. + + if (!WaitNamedPipe(lpszPipename, 20000)) + { + return; + } + } + dwMode = PIPE_READMODE_MESSAGE; + fSuccess = SetNamedPipeHandleState( + output_pipe_handle, // pipe handle + &dwMode, // new pipe mode + NULL, // don't set maximum bytes + NULL); // don't set maximum time + if (!fSuccess) + { + return; + } + + // Send a message to the pipe server. + + cbToWrite = (lstrlen(message_send)) * sizeof(WCHAR); // no need to send final '\0'. Pipe is in message mode. + + fSuccess = WriteFile( + output_pipe_handle, // pipe handle + message_send, // message + cbToWrite, // message length + &cbWritten, // bytes written + NULL); // not overlapped + if (!fSuccess) + { + return; + } + CloseHandle(output_pipe_handle); + return; +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::consume_output_queue_thread() +{ + while (!closed) + { + std::wstring message = output_queue.pop_message(); + if (message.length() == 0) + { + break; + } + send_pipe_message(message); + } +} + +BOOL TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::GetLogonSID(HANDLE hToken, PSID* ppsid) +{ + // From https://learn.microsoft.com/previous-versions/aa446670(v=vs.85) + BOOL bSuccess = FALSE; + DWORD dwIndex; + DWORD dwLength = 0; + PTOKEN_GROUPS ptg = NULL; + + // Verify the parameter passed in is not NULL. + if (NULL == ppsid) + goto Cleanup; + + // Get required buffer size and allocate the TOKEN_GROUPS buffer. + + if (!GetTokenInformation( + hToken, // handle to the access token + TokenGroups, // get information about the token's groups + static_cast(ptg), // pointer to TOKEN_GROUPS buffer + 0, // size of buffer + &dwLength // receives required buffer size + )) + { + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) + goto Cleanup; + + ptg = static_cast(HeapAlloc(GetProcessHeap(), + HEAP_ZERO_MEMORY, + dwLength)); + + if (ptg == NULL) + goto Cleanup; + } + + // Get the token group information from the access token. + + if (!GetTokenInformation( + hToken, // handle to the access token + TokenGroups, // get information about the token's groups + static_cast(ptg), // pointer to TOKEN_GROUPS buffer + dwLength, // size of buffer + &dwLength // receives required buffer size + )) + { + goto Cleanup; + } + + // Loop through the groups to find the logon SID. + + for (dwIndex = 0; dwIndex < ptg->GroupCount; dwIndex++) + if ((ptg->Groups[dwIndex].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID) + { + // Found the logon SID; make a copy of it. + + dwLength = GetLengthSid(ptg->Groups[dwIndex].Sid); + *ppsid = static_cast(HeapAlloc(GetProcessHeap(), + HEAP_ZERO_MEMORY, + dwLength)); + if (*ppsid == NULL) + goto Cleanup; + if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid)) + { + HeapFree(GetProcessHeap(), 0, static_cast(*ppsid)); + goto Cleanup; + } + break; + } + + bSuccess = TRUE; + +Cleanup: + + // Free the buffer for the token groups. + + if (ptg != NULL) + HeapFree(GetProcessHeap(), 0, static_cast(ptg)); + + return bSuccess; +} + +VOID TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::FreeLogonSID(PSID* ppsid) +{ + // From https://learn.microsoft.com/previous-versions/aa446670(v=vs.85) + HeapFree(GetProcessHeap(), 0, static_cast(*ppsid)); +} + +int TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::change_pipe_security_allow_restricted_token(HANDLE handle, HANDLE token) +{ + PACL old_dacl, new_dacl; + PSECURITY_DESCRIPTOR sd; + EXPLICIT_ACCESS ea; + PSID user_restricted; + int error; + + if (!GetLogonSID(token, &user_restricted)) + { + error = 5; // No access error. + goto Ldone; + } + + if (GetSecurityInfo(handle, + SE_KERNEL_OBJECT, + DACL_SECURITY_INFORMATION, + NULL, + NULL, + &old_dacl, + NULL, + &sd)) + { + error = GetLastError(); + goto Lclean_sid; + } + + memset(&ea, 0, sizeof(EXPLICIT_ACCESS)); + ea.grfAccessPermissions |= GENERIC_READ | FILE_WRITE_ATTRIBUTES; + ea.grfAccessPermissions |= GENERIC_WRITE | FILE_READ_ATTRIBUTES; + ea.grfAccessPermissions |= SYNCHRONIZE; + ea.grfAccessMode = SET_ACCESS; + ea.grfInheritance = NO_INHERITANCE; + ea.Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea.Trustee.TrusteeType = TRUSTEE_IS_USER; + ea.Trustee.ptstrName = static_cast(user_restricted); + + if (SetEntriesInAcl(1, &ea, old_dacl, &new_dacl)) + { + error = GetLastError(); + goto Lclean_sd; + } + + if (SetSecurityInfo(handle, + SE_KERNEL_OBJECT, + DACL_SECURITY_INFORMATION, + NULL, + NULL, + new_dacl, + NULL)) + { + error = GetLastError(); + goto Lclean_dacl; + } + + error = 0; + +Lclean_dacl: + LocalFree(static_cast(new_dacl)); +Lclean_sd: + LocalFree(static_cast(sd)); +Lclean_sid: + FreeLogonSID(&user_restricted); +Ldone: + return error; +} + +HANDLE TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::create_medium_integrity_token() +{ + HANDLE restricted_token_handle; + SAFER_LEVEL_HANDLE level_handle = NULL; + DWORD sid_size = SECURITY_MAX_SID_SIZE; + BYTE medium_sid[SECURITY_MAX_SID_SIZE]; + if (!SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &level_handle, NULL)) + { + return NULL; + } + if (!SaferComputeTokenFromLevel(level_handle, NULL, &restricted_token_handle, 0, NULL)) + { + SaferCloseLevel(level_handle); + return NULL; + } + SaferCloseLevel(level_handle); + + if (!CreateWellKnownSid(WinMediumLabelSid, nullptr, medium_sid, &sid_size)) + { + CloseHandle(restricted_token_handle); + return NULL; + } + + TOKEN_MANDATORY_LABEL integrity_level = { 0 }; + integrity_level.Label.Attributes = SE_GROUP_INTEGRITY; + integrity_level.Label.Sid = reinterpret_cast(medium_sid); + + if (!SetTokenInformation(restricted_token_handle, TokenIntegrityLevel, &integrity_level, sizeof(integrity_level))) + { + CloseHandle(restricted_token_handle); + return NULL; + } + + return restricted_token_handle; +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::handle_pipe_connection(HANDLE input_pipe_handle) +{ + if (!input_pipe_handle) + { + return; + } + constexpr DWORD readBlockBytes = BUFSIZE; + std::wstring message; + size_t iBlock = 0; + message.reserve(BUFSIZE); + bool ok; + do + { + constexpr size_t charsPerBlock = readBlockBytes / sizeof(message[0]); + message.resize(message.size() + charsPerBlock); + DWORD bytesRead = 0; + ok = ReadFile( + input_pipe_handle, + // read the message directly into the string block by block simultaneously resizing it + message.data() + iBlock * charsPerBlock, + readBlockBytes, + &bytesRead, + nullptr); + + if (!ok && GetLastError() != ERROR_MORE_DATA) + { + break; + } + iBlock++; + } while (!ok); + // trim the message's buffer + const auto nullCharPos = message.find_last_not_of(L'\0'); + if (nullCharPos != std::wstring::npos) + { + message.resize(nullCharPos + 1); + } + + input_queue.queue_message(std::move(message)); + + // Flush the pipe to allow the client to read the pipe's contents + // before disconnecting. Then disconnect the pipe, and close the + // handle to this pipe instance. + + FlushFileBuffers(input_pipe_handle); + DisconnectNamedPipe(input_pipe_handle); + CloseHandle(input_pipe_handle); +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::start_named_pipe_server(HANDLE token) +{ + // Adapted from https://learn.microsoft.com/windows/win32/ipc/multithreaded-pipe-server + const wchar_t* pipe_name = input_pipe_name.c_str(); + BOOL connected = FALSE; + HANDLE connect_pipe_handle = INVALID_HANDLE_VALUE; + while (!closed) + { + { + std::unique_lock lock(pipe_connect_handle_mutex); + connect_pipe_handle = CreateNamedPipe( + pipe_name, + PIPE_ACCESS_DUPLEX | + WRITE_DAC, + PIPE_TYPE_MESSAGE | + PIPE_READMODE_MESSAGE | + PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + BUFSIZE, + BUFSIZE, + 0, + NULL); + + if (connect_pipe_handle == INVALID_HANDLE_VALUE) + { + return; + } + + if (token != NULL) + { + change_pipe_security_allow_restricted_token(connect_pipe_handle, token); + } + current_connect_pipe_handle = connect_pipe_handle; + } + connected = ConnectNamedPipe(connect_pipe_handle, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); + { + std::unique_lock lock(pipe_connect_handle_mutex); + current_connect_pipe_handle = NULL; + } + if (connected) + { + std::thread(&TwoWayPipeMessageIPCImpl::handle_pipe_connection, this, connect_pipe_handle).detach(); + } + else + { + // Client could not connect. + CloseHandle(connect_pipe_handle); + } + } +} + +void TwoWayPipeMessageIPC::TwoWayPipeMessageIPCImpl::consume_input_queue_thread() +{ + while (!closed) + { + outgoing_message = L""; + std::wstring message = input_queue.pop_message(); + if (message.length() == 0) + { + break; + } + + // Check if callback method exists first before trying to call it. + // otherwise just store the response message in a variable. + if (dispatch_inc_message_function != nullptr) + { + dispatch_inc_message_function(message); + } + outgoing_message = message; + } +} diff --git a/src/modules/Workspaces/WorkspacesLib/utils.h b/src/modules/Workspaces/WorkspacesLib/utils.h new file mode 100644 index 0000000000..5aebd197b3 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesLib/utils.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +#include +#include + +struct CommandLineArgs +{ + std::wstring workspaceId; + InvokePoint invokePoint; +}; + +CommandLineArgs split(std::wstring s, const std::wstring& delimiter) +{ + CommandLineArgs cmdArgs{}; + + size_t pos = 0; + std::wstring token; + std::vector tokens; + while ((pos = s.find(delimiter)) != std::wstring::npos) + { + token = s.substr(0, pos); + tokens.push_back(token); + s.erase(0, pos + delimiter.length()); + } + tokens.push_back(s); + + for (const auto& token : tokens) + { + if (!cmdArgs.workspaceId.empty()) + { + try + { + auto invokePoint = static_cast(std::stoi(token)); + cmdArgs.invokePoint = invokePoint; + } + catch (std::exception) + { + } + } + else + { + auto guid = GuidFromString(token); + if (guid.has_value()) + { + cmdArgs.workspaceId = token; + } + } + } + + return cmdArgs; +} diff --git a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp index c0d2089b3e..21ccd19261 100644 --- a/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp +++ b/src/modules/Workspaces/WorkspacesModuleInterface/dllmain.cpp @@ -20,6 +20,7 @@ // Non-localizable const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe"; +const std::wstring workspacesWindowArrangerPath = L"PowerToys.WorkspacesWindowArranger.exe"; const std::wstring workspacesSnapshotToolPath = L"PowerToys.WorkspacesSnapshotTool.exe"; const std::wstring workspacesEditorPath = L"PowerToys.WorkspacesEditor.exe"; diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/JsonUtils.h b/src/modules/Workspaces/WorkspacesSnapshotTool/JsonUtils.h deleted file mode 100644 index be895e5950..0000000000 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/JsonUtils.h +++ /dev/null @@ -1,57 +0,0 @@ -#pragma once - -#include - -#include - -#include - -namespace WorkspacesJsonUtils -{ - inline std::vector Read(const std::wstring& fileName) - { - std::vector projects{}; - try - { - auto savedProjectsJson = json::from_file(fileName); - if (savedProjectsJson.has_value()) - { - auto savedProjects = WorkspacesData::WorkspacesListJSON::FromJson(savedProjectsJson.value()); - if (savedProjects.has_value()) - { - projects = savedProjects.value(); - } - } - } - catch (std::exception ex) - { - Logger::error("Error reading workspaces file. {}", ex.what()); - } - - return projects; - } - - inline void Write(const std::wstring& fileName, const std::vector& projects) - { - try - { - json::to_file(fileName, WorkspacesData::WorkspacesListJSON::ToJson(projects)); - } - catch (std::exception ex) - { - Logger::error("Error writing workspaces file. {}", ex.what()); - } - } - - inline void Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project) - { - try - { - json::to_file(fileName, WorkspacesData::WorkspacesProjectJSON::ToJson(project)); - } - catch (std::exception ex) - { - Logger::error("Error writing workspaces file. {}", ex.what()); - } - } -} \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj index ff4491e636..b817abd043 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj @@ -133,7 +133,6 @@ - diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters index 5f837c5409..2402f15f61 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj.filters @@ -18,9 +18,6 @@ Header Files - - Header Files - Header Files diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp b/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp index e0803fde7f..b736ce238e 100644 --- a/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp +++ b/src/modules/Workspaces/WorkspacesSnapshotTool/main.cpp @@ -5,21 +5,21 @@ #include #include +#include #include -#include #include #include #include #include -const std::wstring moduleName = L"Workspaces\\ProjectsSnapshotTool"; +const std::wstring moduleName = L"Workspaces\\WorkspacesSnapshotTool"; const std::wstring internalPath = L""; int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cmdShow) { - LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesLauncherLoggerName); + LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesSnapshotToolLoggerName); InitUnhandledExceptionHandler(); if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) @@ -46,14 +46,6 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm return -1; } - std::wstring fileName = WorkspacesData::WorkspacesFile(); - std::string cmdLineStr(cmdLine); - if (!cmdLineStr.empty()) - { - std::wstring fileNameParam(cmdLineStr.begin(), cmdLineStr.end()); - fileName = fileNameParam; - } - // create new project time_t creationTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); WorkspacesData::WorkspacesProject project{ .id = CreateGuidString(), .creationTime = creationTime }; @@ -75,7 +67,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdLine, int cm return monitorNumber; }); - WorkspacesJsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project); + JsonUtils::Write(WorkspacesData::TempWorkspacesFile(), project); Logger::trace(L"WorkspacesProject {}:{} created", project.name, project.id); CoUninitialize(); diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/PropertySheet.props b/src/modules/Workspaces/WorkspacesWindowArranger/PropertySheet.props new file mode 100644 index 0000000000..b0c622690f --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx b/src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp new file mode 100644 index 0000000000..ba56dc1843 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp @@ -0,0 +1,257 @@ +#include "pch.h" +#include "WindowArranger.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace FancyZones +{ + inline void ScreenToWorkAreaCoords(HWND window, HMONITOR monitor, RECT& rect) + { + MONITORINFOEXW monitorInfo{ sizeof(MONITORINFOEXW) }; + GetMonitorInfoW(monitor, &monitorInfo); + + auto xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left; + auto yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top; + + DPIAware::Convert(monitor, rect); + + auto referenceRect = RECT(rect.left - xOffset, rect.top - yOffset, rect.right - xOffset, rect.bottom - yOffset); + + // Now, this rect should be used to determine the monitor and thus taskbar size. This fixes + // scenarios where the zone lies approximately between two monitors, and the taskbar is on the left. + monitor = MonitorFromRect(&referenceRect, MONITOR_DEFAULTTOPRIMARY); + GetMonitorInfoW(monitor, &monitorInfo); + + xOffset = monitorInfo.rcWork.left - monitorInfo.rcMonitor.left; + yOffset = monitorInfo.rcWork.top - monitorInfo.rcMonitor.top; + + rect.left -= xOffset; + rect.right -= xOffset; + rect.top -= yOffset; + rect.bottom -= yOffset; + } + + inline bool SizeWindowToRect(HWND window, HMONITOR monitor, bool isMinimized, bool isMaximized, RECT rect) noexcept + { + WINDOWPLACEMENT placement{}; + ::GetWindowPlacement(window, &placement); + + if (isMinimized) + { + placement.showCmd = SW_MINIMIZE; + } + else + { + if ((placement.showCmd != SW_SHOWMINIMIZED) && + (placement.showCmd != SW_MINIMIZE)) + { + if (placement.showCmd == SW_SHOWMAXIMIZED) + placement.flags &= ~WPF_RESTORETOMAXIMIZED; + + placement.showCmd = SW_RESTORE; + } + + ScreenToWorkAreaCoords(window, monitor, rect); + placement.rcNormalPosition = rect; + } + + placement.flags |= WPF_ASYNCWINDOWPLACEMENT; + + auto result = ::SetWindowPlacement(window, &placement); + if (!result) + { + Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError())); + return false; + } + + // make sure window is moved to the correct monitor before maximize. + if (isMaximized) + { + placement.showCmd = SW_SHOWMAXIMIZED; + } + + // Do it again, allowing Windows to resize the window and set correct scaling + // This fixes Issue #365 + result = ::SetWindowPlacement(window, &placement); + if (!result) + { + Logger::error(L"SetWindowPlacement failed, {}", get_last_error_or_default(GetLastError())); + return false; + } + + return true; + } +} + + +WindowArranger::WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper) : + m_project(project), + m_windowsBefore(WindowEnumerator::Enumerate(WindowFilter::Filter)), + m_monitors(MonitorUtils::IdentifyMonitors()), + m_installedApps(Utils::Apps::GetAppsList()), + //m_windowCreationHandler(std::bind(&WindowArranger::onWindowCreated, this, std::placeholders::_1)), + m_ipcHelper(ipcHelper) +{ + for (auto& app : project.apps) + { + m_launchingApps.insert({ app, { app, nullptr } }); + } + + m_ipcHelper.send(L"ready"); + + for (int attempt = 0; attempt < 50 && !allWindowsFound(); attempt++) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + std::vector windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter); + std::vector windowsDiff{}; + std::copy_if(windowsAfter.begin(), windowsAfter.end(), std::back_inserter(windowsDiff), [&](HWND window) { return std::find(m_windowsBefore.begin(), m_windowsBefore.end(), window) == m_windowsBefore.end(); }); + + for (HWND window : windowsDiff) + { + processWindow(window); + } + } + + bool allFound = allWindowsFound(); + Logger::info(L"Finished moving new windows, all windows found: {}", allFound); + + if (!allFound) + { + std::vector allWindows = WindowEnumerator::Enumerate(WindowFilter::Filter); + for (HWND window : allWindows) + { + processWindow(window); + } + } +} + +//void WindowArranger::onWindowCreated(HWND window) +//{ +// if (!WindowFilter::Filter(window)) +// { +// return; +// } +// +// processWindow(window); +//} + +void WindowArranger::processWindow(HWND window) +{ + // check if this window is already handled + auto windowIter = std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const auto& val) { return val.second.window == window; }); + if (windowIter != m_launchingApps.end()) + { + return; + } + + RECT rect = WindowUtils::GetWindowRect(window); + if (rect.right - rect.left <= 0 || rect.bottom - rect.top <= 0) + { + return; + } + + std::wstring title = WindowUtils::GetWindowTitle(window); + if (title.empty()) + { + return; + } + + std::wstring processPath = get_process_path(window); + if (processPath.empty()) + { + return; + } + + auto data = Utils::Apps::GetApp(processPath, m_installedApps); + if (!data.has_value() || data->name.empty()) + { + return; + } + + auto iter = std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const auto& val) + { return val.second.state == LaunchingState::Waiting && val.first.name == data.value().name; }); + if (iter == m_launchingApps.end()) + { + Logger::info(L"A window of {} is not in the project", processPath); + return; + } + + Logger::debug(L"Move {}", title); + iter->second.window = window; + if (moveWindow(window, iter->first)) + { + iter->second.state = LaunchingState::LaunchedAndMoved; + } + else + { + iter->second.state = LaunchingState::Failed; + } + + m_ipcHelper.send(WorkspacesData::AppLaunchInfoJSON::ToJson({iter->first, nullptr, iter->second.state}).ToString().c_str()); +} + +bool WindowArranger::moveWindow(HWND window, const WorkspacesData::WorkspacesProject::Application& app) +{ + auto snapMonitorIter = std::find_if(m_project.monitors.begin(), m_project.monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; }); + if (snapMonitorIter == m_project.monitors.end()) + { + Logger::error(L"No monitor saved for launching the app"); + return false; + } + + bool launchMinimized = app.isMinimized; + bool launchMaximized = app.isMaximized; + + HMONITOR currentMonitor{}; + UINT currentDpi = DPIAware::DEFAULT_DPI; + auto currentMonitorIter = std::find_if(m_monitors.begin(), m_monitors.end(), [&](const WorkspacesData::WorkspacesProject::Monitor& val) { return val.number == app.monitor; }); + if (currentMonitorIter != m_monitors.end()) + { + currentMonitor = currentMonitorIter->monitor; + currentDpi = currentMonitorIter->dpi; + } + else + { + currentMonitor = MonitorFromPoint(POINT{ 0, 0 }, MONITOR_DEFAULTTOPRIMARY); + DPIAware::GetScreenDPIForMonitor(currentMonitor, currentDpi); + launchMinimized = true; + launchMaximized = false; + } + + RECT rect = app.position.toRect(); + float mult = static_cast(snapMonitorIter->dpi) / currentDpi; + rect.left = static_cast(std::round(rect.left * mult)); + rect.right = static_cast(std::round(rect.right * mult)); + rect.top = static_cast(std::round(rect.top * mult)); + rect.bottom = static_cast(std::round(rect.bottom * mult)); + + if (FancyZones::SizeWindowToRect(window, currentMonitor, launchMinimized, launchMaximized, rect)) + { + WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window); + Logger::trace(L"Placed {} to ({},{}) [{}x{}]", app.name, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + return true; + } + else + { + Logger::error(L"Failed placing {}", app.name); + return false; + } +} + +bool WindowArranger::allWindowsFound() const +{ + return std::find_if(m_launchingApps.begin(), m_launchingApps.end(), [&](const std::pair& val) { + return val.second.window == nullptr; + }) == m_launchingApps.end(); +} diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.h b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.h new file mode 100644 index 0000000000..e18b52829b --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include +#include +#include +#include + +class WindowArranger +{ +public: + WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper); + ~WindowArranger() = default; + +private: + const WorkspacesData::WorkspacesProject m_project; + const std::vector m_windowsBefore; + const std::vector m_monitors; + const Utils::Apps::AppList m_installedApps; + //const WindowCreationHandler m_windowCreationHandler; + const IPCHelper& m_ipcHelper; + WorkspacesData::LaunchingAppStateMap m_launchingApps{}; + + //void onWindowCreated(HWND window); + void processWindow(HWND window); + bool moveWindow(HWND window, const WorkspacesData::WorkspacesProject::Application& app); + + bool allWindowsFound() const; +}; diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.cpp new file mode 100644 index 0000000000..afcdd1c7bf --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.cpp @@ -0,0 +1,60 @@ +#include "pch.h" +#include "WindowCreationHandler.h" + +WindowCreationHandler::WindowCreationHandler(std::function windowCreatedCallback) : + m_windowCreatedCallback(windowCreatedCallback) +{ + s_instance = this; + InitHooks(); +} + +WindowCreationHandler::~WindowCreationHandler() +{ + m_staticWinEventHooks.erase(std::remove_if(begin(m_staticWinEventHooks), + end(m_staticWinEventHooks), + [](const HWINEVENTHOOK hook) { + return UnhookWinEvent(hook); + }), + end(m_staticWinEventHooks)); +} + +void WindowCreationHandler::InitHooks() +{ + std::array events_to_subscribe = { + EVENT_OBJECT_UNCLOAKED, + EVENT_OBJECT_SHOW, + EVENT_OBJECT_CREATE + }; + for (const auto event : events_to_subscribe) + { + auto hook = SetWinEventHook(event, event, nullptr, WinHookProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); + if (hook) + { + m_staticWinEventHooks.emplace_back(hook); + } + else + { + Logger::error(L"Failed to initialize win event hooks"); + } + } +} + +void WindowCreationHandler::HandleWinHookEvent(DWORD event, HWND window) noexcept +{ + switch (event) + { + //case EVENT_OBJECT_UNCLOAKED: + //case EVENT_OBJECT_SHOW: + case EVENT_OBJECT_CREATE: + { + if (m_windowCreatedCallback) + { + m_windowCreatedCallback(window); + } + } + break; + + default: + break; + } +} diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.h b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.h new file mode 100644 index 0000000000..6e82a964a0 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowCreationHandler.h @@ -0,0 +1,30 @@ +#pragma once + +class WindowCreationHandler +{ +public: + WindowCreationHandler(std::function windowCreatedCallback); + ~WindowCreationHandler(); + +private: + static inline WindowCreationHandler* s_instance = nullptr; + std::vector m_staticWinEventHooks; + std::function m_windowCreatedCallback; + + void InitHooks(); + void HandleWinHookEvent(DWORD event, HWND window) noexcept; + + static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook, + DWORD event, + HWND window, + LONG object, + LONG child, + DWORD eventThread, + DWORD eventTime) + { + if (s_instance) + { + s_instance->HandleWinHookEvent(event, window); + } + } +}; diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj new file mode 100644 index 0000000000..2451be2470 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj @@ -0,0 +1,179 @@ + + + + + + + + + + + + Level3 + false + true + stdcpplatest + /await %(AdditionalOptions) + _UNICODE;UNICODE;%(PreprocessorDefinitions) + + + Windows + + + true + + + + + + _DEBUG;%(PreprocessorDefinitions) + Disabled + true + MultiThreadedDebug + + + true + + + + + NDEBUG;%(PreprocessorDefinitions) + MaxSpeed + false + MultiThreaded + true + true + + + true + true + true + + + + + 16.0 + Win32Proj + {37D07516-4185-43A4-924F-3C7A5D95ECF6} + WorkspacesWindowArranger + + + + Application + v143 + Unicode + Spectre + + + + true + true + + + false + true + false + + + + + + + + + + + + PowerToys.$(MSBuildProjectName) + ..\..\..\..\$(Platform)\$(Configuration)\ + + + true + + + false + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + ./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories) + + + Windows + true + shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + ./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories) + + + Windows + true + true + true + shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies) + + + + + + Create + + + + + + + + + + + + + + + + {caba8dfb-823b-4bf2-93ac-3f31984150d9} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {b31fcc55-b5a4-4ea7-b414-2dceae6af332} + + + + + + + + + Designer + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj.filters b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj.filters new file mode 100644 index 0000000000..78a21dc4d1 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj.filters @@ -0,0 +1,64 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + + Resource Files + + + + + Resource Files + + + + + Resource Files + + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc new file mode 100644 index 0000000000..acbc7659d5 Binary files /dev/null and b/src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArrangerResource.base.rc differ diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/main.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/main.cpp new file mode 100644 index 0000000000..1c6a72dae5 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/main.cpp @@ -0,0 +1,105 @@ +#include "pch.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include + +const std::wstring moduleName = L"Workspaces\\WorkspacesWindowArranger"; +const std::wstring internalPath = L""; + +int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cmdShow) +{ + LoggerHelpers::init_logger(moduleName, internalPath, LogSettings::workspacesWindowArrangerLoggerName); + InitUnhandledExceptionHandler(); + + if (powertoys_gpo::getConfiguredWorkspacesEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) + { + Logger::warn(L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); + return 0; + } + + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + std::wstring commandLine{ GetCommandLineW() }; + if (commandLine.empty()) + { + Logger::warn("Empty command line arguments"); + return 1; + } + + auto args = split(commandLine, L" "); + if (args.workspaceId.empty()) + { + Logger::warn("Incorrect command line arguments: no workspace id"); + return 1; + } + + // read workspaces + std::vector workspaces; + WorkspacesData::WorkspacesProject projectToLaunch{}; + + // check the temp file in case the project is just created and not saved to the workspaces.json yet + if (std::filesystem::exists(WorkspacesData::TempWorkspacesFile())) + { + auto file = WorkspacesData::TempWorkspacesFile(); + auto res = JsonUtils::ReadSingleWorkspace(file); + if (res.isOk() && res.value().id == args.workspaceId) + { + projectToLaunch = res.getValue(); + } + else if (res.isError()) + { + Logger::error(L"Error reading temp file"); + return 1; + } + } + + if (projectToLaunch.id.empty()) + { + auto file = WorkspacesData::WorkspacesFile(); + auto res = JsonUtils::ReadWorkspaces(file); + if (res.isOk()) + { + workspaces = res.getValue(); + } + else + { + return 1; + } + + for (const auto& proj : workspaces) + { + if (proj.id == args.workspaceId) + { + projectToLaunch = proj; + break; + } + } + } + + if (projectToLaunch.id.empty()) + { + Logger::critical(L"Workspace {} not found", args.workspaceId); + return 1; + } + + // IPC + IPCHelper ipc(IPCHelperStrings::WindowArrangerPipeName, IPCHelperStrings::LauncherArrangerPipeName, nullptr); + + // arrange windows + Logger::info(L"Arrange windows from Workspace {} : {}", projectToLaunch.name, projectToLaunch.id); + WindowArranger windowArranger(projectToLaunch, ipc); + //run_message_loop(); + + Logger::debug(L"Arranger finished"); + + CoUninitialize(); + return 0; +} diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/packages.config b/src/modules/Workspaces/WorkspacesWindowArranger/packages.config new file mode 100644 index 0000000000..ff4b059648 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/pch.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/pch.h b/src/modules/Workspaces/WorkspacesWindowArranger/pch.h new file mode 100644 index 0000000000..99c9d8a242 --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/pch.h @@ -0,0 +1,6 @@ +#pragma once +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/resource.base.h b/src/modules/Workspaces/WorkspacesWindowArranger/resource.base.h new file mode 100644 index 0000000000..8a3207d1ad --- /dev/null +++ b/src/modules/Workspaces/WorkspacesWindowArranger/resource.base.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by WorkspacesWindowArrangerResource.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys Workspaces Window Arranger" +#define INTERNAL_NAME "PowerToys.WorkspacesWindowArranger" +#define ORIGINAL_FILENAME "PowerToys.WorkspacesWindowArranger.exe" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/Workspaces/workspaces-common/WindowUtils.h b/src/modules/Workspaces/workspaces-common/WindowUtils.h index aff0c957a3..8424591dfa 100644 --- a/src/modules/Workspaces/workspaces-common/WindowUtils.h +++ b/src/modules/Workspaces/workspaces-common/WindowUtils.h @@ -21,6 +21,7 @@ namespace WindowUtils const wchar_t WorkspacesSnapshotTool[] = L"POWERTOYS.WORKSPACESSNAPSHOTTOOL"; const wchar_t WorkspacesEditor[] = L"POWERTOYS.WORKSPACESEDITOR"; const wchar_t WorkspacesLauncher[] = L"POWERTOYS.WORKSPACESLAUNCHER"; + const wchar_t WorkspacesWindowArranger[] = L"POWERTOYS.WORKSPACESWINDOWARRANGER"; } inline bool IsRoot(HWND window) noexcept @@ -79,7 +80,8 @@ namespace WindowUtils NonLocalizable::SearchUI, NonLocalizable::HelpWindow, NonLocalizable::WorkspacesEditor, - NonLocalizable::WorkspacesLauncher, + NonLocalizable::WorkspacesLauncher, + NonLocalizable::WorkspacesWindowArranger, NonLocalizable::WorkspacesSnapshotTool, }; return (check_excluded_app(window, processPathUpper, defaultExcludedApps)); diff --git a/src/modules/awake/Awake/Program.cs b/src/modules/awake/Awake/Program.cs index 7dbdd753e8..9ecaccd3a7 100644 --- a/src/modules/awake/Awake/Program.cs +++ b/src/modules/awake/Awake/Program.cs @@ -56,6 +56,19 @@ namespace Awake Logger.InitializeLogger(Path.Combine("\\", Core.Constants.AppName, "Logs")); + try + { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage); + } + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + AppDomain.CurrentDomain.UnhandledException += AwakeUnhandledExceptionCatcher; if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredAwakeEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) diff --git a/src/modules/colorPicker/ColorPickerUI/App.xaml.cs b/src/modules/colorPicker/ColorPickerUI/App.xaml.cs index b90e231892..98646583be 100644 --- a/src/modules/colorPicker/ColorPickerUI/App.xaml.cs +++ b/src/modules/colorPicker/ColorPickerUI/App.xaml.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel.Composition; +using System.Globalization; using System.Threading; using System.Windows; @@ -29,6 +30,19 @@ namespace ColorPickerUI protected override void OnStartup(StartupEventArgs e) { + try + { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage); + } + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + NativeThreadCTS = new CancellationTokenSource(); ExitToken = NativeThreadCTS.Token; diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp index b1505c2699..d0839f2844 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZones.cpp @@ -398,7 +398,7 @@ void FancyZones::WindowCreated(HWND window) noexcept return; } - if (!FancyZonesWindowProcessing::IsProcessable(window)) + if (!FancyZonesWindowProcessing::IsProcessableAutomatically(window)) { return; } @@ -1084,7 +1084,7 @@ bool FancyZones::ShouldProcessSnapHotkey(DWORD vkCode) noexcept } auto window = GetForegroundWindow(); - if (!FancyZonesWindowProcessing::IsProcessable(window)) + if (!FancyZonesWindowProcessing::IsProcessableManually(window)) { return false; } diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp index a71c65e6c8..45c4ed2a34 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.cpp @@ -65,16 +65,28 @@ FancyZonesWindowProcessing::ProcessabilityType FancyZonesWindowProcessing::Defin return ProcessabilityType::NotCurrentVirtualDesktop; } - // Ignore windows launched by Workspaces - if (FancyZonesWindowProperties::IsLaunchedByWorkspaces(window)) - { - return ProcessabilityType::LaunchedByWorkspaces; - } - return ProcessabilityType::Processable; } -bool FancyZonesWindowProcessing::IsProcessable(HWND window) noexcept +bool FancyZonesWindowProcessing::IsProcessableAutomatically(HWND window) noexcept { - return DefineWindowType(window) == ProcessabilityType::Processable; + auto type = DefineWindowType(window); + if (type != ProcessabilityType::Processable) + { + return false; + } + + // Ignore windows launched by Workspaces + if (FancyZonesWindowProperties::IsLaunchedByWorkspaces(window)) + { + return false; + } + + return true; +} + +bool FancyZonesWindowProcessing::IsProcessableManually(HWND window) noexcept +{ + auto type = DefineWindowType(window); + return type == ProcessabilityType::Processable; } diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h index 19de91b0ba..fabc7f2821 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesWindowProcessing.h @@ -13,10 +13,10 @@ namespace FancyZonesWindowProcessing NonProcessablePopupWindow, ChildWindow, Excluded, - NotCurrentVirtualDesktop, - LaunchedByWorkspaces + NotCurrentVirtualDesktop }; ProcessabilityType DefineWindowType(HWND window) noexcept; - bool IsProcessable(HWND window) noexcept; + bool IsProcessableAutomatically(HWND window) noexcept; + bool IsProcessableManually(HWND window) noexcept; } \ No newline at end of file diff --git a/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp b/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp index 0a7251c78c..e127371347 100644 --- a/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp +++ b/src/modules/fancyzones/FancyZonesLib/WindowMouseSnap.cpp @@ -29,7 +29,7 @@ WindowMouseSnap::~WindowMouseSnap() std::unique_ptr WindowMouseSnap::Create(HWND window, const std::unordered_map>& activeWorkAreas) { - if (FancyZonesWindowUtils::IsCursorTypeIndicatingSizeEvent() || !FancyZonesWindowProcessing::IsProcessable(window)) + if (FancyZonesWindowUtils::IsCursorTypeIndicatingSizeEvent() || !FancyZonesWindowProcessing::IsProcessableManually(window)) { return nullptr; } diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/WindowProcessingTests.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/WindowProcessingTests.Spec.cpp index 58c50f1026..7ce509b806 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/WindowProcessingTests.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/WindowProcessingTests.Spec.cpp @@ -4,6 +4,8 @@ #include #include +#include + #include "Util.h" #include @@ -45,7 +47,8 @@ namespace FancyZonesUnitTests Assert::IsTrue(IsIconic(window)); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Minimized, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ToolWindow) @@ -53,7 +56,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", WS_EX_TOOLWINDOW); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::ToolWindow, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (InvisibleWindow) @@ -63,7 +67,8 @@ namespace FancyZonesUnitTests std::this_thread::sleep_for(std::chrono::milliseconds(100)); // let ShowWindow finish Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::NotVisible, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD(NonRootWindow) @@ -75,7 +80,8 @@ namespace FancyZonesUnitTests Assert::IsFalse(FancyZonesWindowUtils::IsRoot(window)); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::NonRootWindow, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (Popup_App) @@ -83,7 +89,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_TILEDWINDOW | WS_POPUP); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsTrue(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (Popup_Menu) @@ -91,7 +98,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_POPUP | WS_TILED | WS_CLIPCHILDREN | WS_CLIPSIBLINGS); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::NonProcessablePopupWindow, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (Popup_MenuEdge) @@ -99,7 +107,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_POPUP | WS_TILED | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_THICKFRAME | WS_SIZEBOX); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::NonProcessablePopupWindow, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (Popup_Calculator) @@ -107,7 +116,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_BORDER | WS_CLIPSIBLINGS | WS_DLGFRAME | WS_GROUP | WS_POPUP | WS_POPUPWINDOW | WS_SIZEBOX | WS_TABSTOP | WS_TILEDWINDOW); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsTrue(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (Popup_CalculatorTopmost) @@ -115,7 +125,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_BORDER | WS_CAPTION | WS_CLIPSIBLINGS | WS_DLGFRAME | WS_OVERLAPPED | WS_POPUP | WS_POPUPWINDOW | WS_SIZEBOX | WS_SYSMENU | WS_THICKFRAME); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsTrue(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD(Popup_FacebookMessenger) @@ -123,7 +134,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_GROUP | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_POPUP | WS_TABSTOP | WS_THICKFRAME); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsTrue(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ChildWindow_OptionDisabled) @@ -142,7 +154,8 @@ namespace FancyZonesUnitTests Assert::IsTrue(FancyZonesWindowUtils::HasVisibleOwner(window), L"Child window doesn't have visible owner"); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::ChildWindow, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ChildWindow_OptionEnabled) @@ -159,7 +172,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, 0, parentWindow); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsTrue(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ExcludedApp_ByDefault) @@ -168,7 +182,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"SysListView32"); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Excluded, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ExcludedApp_ByDefault_SplashScreen) @@ -176,7 +191,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"MsoSplash"); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Excluded, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ExcludedApp_ByUser) @@ -188,7 +204,18 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"Test_Excluded"); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Excluded, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsFalse(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableManually(window)); + } + + TEST_METHOD (LaunchedByWorkspaces) + { + HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_TILEDWINDOW); + WorkspacesWindowProperties::StampWorkspacesLaunchedProperty(window); + + Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); + Assert::IsFalse(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } TEST_METHOD (ProcessableWindow) @@ -196,7 +223,8 @@ namespace FancyZonesUnitTests HWND window = Mocks::WindowCreate(hInst, L"", L"", 0, WS_TILEDWINDOW); Assert::AreEqual(FancyZonesWindowProcessing::ProcessabilityType::Processable, FancyZonesWindowProcessing::DefineWindowType(window)); - Assert::IsTrue(FancyZonesWindowProcessing::IsProcessable(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableAutomatically(window)); + Assert::IsTrue(FancyZonesWindowProcessing::IsProcessableManually(window)); } }; } \ No newline at end of file diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs index 2edb096018..9de3baeeba 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; +using System.Globalization; using System.Threading; using System.Windows; using System.Windows.Input; @@ -55,6 +56,20 @@ namespace FancyZonesEditor public App() { + var languageTag = LanguageHelper.LoadLanguage(); + + if (!string.IsNullOrEmpty(languageTag)) + { + try + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(languageTag); + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + } + Logger.InitializeLogger("\\FancyZones\\Editor\\Logs"); // DebugModeCheck(); diff --git a/src/modules/imageresizer/ui/App.xaml.cs b/src/modules/imageresizer/ui/App.xaml.cs index db5a43e905..33e2240614 100644 --- a/src/modules/imageresizer/ui/App.xaml.cs +++ b/src/modules/imageresizer/ui/App.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/ using System; +using System.Globalization; using System.Text; using System.Windows; @@ -19,6 +20,19 @@ namespace ImageResizer { static App() { + try + { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage); + } + } + catch (CultureNotFoundException) + { + // error + } + Console.InputEncoding = Encoding.Unicode; } diff --git a/src/modules/imageresizer/ui/ImageResizerUI.csproj b/src/modules/imageresizer/ui/ImageResizerUI.csproj index b1f25927e8..3a7701607e 100644 --- a/src/modules/imageresizer/ui/ImageResizerUI.csproj +++ b/src/modules/imageresizer/ui/ImageResizerUI.csproj @@ -46,6 +46,7 @@ + diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/Dialog.cpp b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/Dialog.cpp index 3ded9a2a7e..812c0f05ed 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/Dialog.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/Dialog.cpp @@ -1,9 +1,7 @@ #include "pch.h" #include "Dialog.h" -using namespace winrt::Windows::Foundation; - -IAsyncOperation Dialog::PartialRemappingConfirmationDialog(XamlRoot root, std::wstring dialogTitle) +winrt::Windows::Foundation::IAsyncOperation Dialog::PartialRemappingConfirmationDialog(XamlRoot root, std::wstring dialogTitle) { ContentDialog confirmationDialog; confirmationDialog.XamlRoot(root); diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp index 5779f8739e..6bf0994e3f 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp @@ -24,8 +24,6 @@ #include "EditorConstants.h" #include -using namespace winrt::Windows::Foundation; - static UINT g_currentDPI = DPIAware::DEFAULT_DPI; LRESULT CALLBACK EditKeyboardWindowProc(HWND, UINT, WPARAM, LPARAM); @@ -57,7 +55,7 @@ static void handleTheme() } } -static IAsyncOperation OrphanKeysConfirmationDialog( +static winrt::Windows::Foundation::IAsyncOperation OrphanKeysConfirmationDialog( KBMEditor::KeyboardManagerState& state, const std::vector& keys, XamlRoot root) @@ -90,7 +88,7 @@ static IAsyncOperation OrphanKeysConfirmationDialog( co_return res == ContentDialogResult::Primary; } -static IAsyncAction OnClickAccept(KBMEditor::KeyboardManagerState& keyboardManagerState, XamlRoot root, std::function ApplyRemappings) +static winrt::Windows::Foundation::IAsyncAction OnClickAccept(KBMEditor::KeyboardManagerState& keyboardManagerState, XamlRoot root, std::function ApplyRemappings) { ShortcutErrorType isSuccess = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(SingleKeyRemapControl::singleKeyRemapBuffer); diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp index 9656598b4e..c85c36cde2 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp @@ -18,8 +18,6 @@ #include "EditorConstants.h" #include -using namespace winrt::Windows::Foundation; - static UINT g_currentDPI = DPIAware::DEFAULT_DPI; LRESULT CALLBACK EditShortcutsWindowProc(HWND, UINT, WPARAM, LPARAM); @@ -51,7 +49,7 @@ static void handleTheme() } } -static IAsyncAction OnClickAccept( +static winrt::Windows::Foundation::IAsyncAction OnClickAccept( KBMEditor::KeyboardManagerState& keyboardManagerState, XamlRoot root, std::function ApplyRemappings) diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs index ee8c2ebc97..d6932f0f0c 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/InputInterpreterTests.cs @@ -68,9 +68,9 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter.UnitTest [DataTestMethod] [DataRow(new string[] { "5", "CeLsIuS", "in", "faHrenheiT" }, new string[] { "5", "DegreeCelsius", "in", "DegreeFahrenheit" })] - [DataRow(new string[] { "5", "f", "in", "celsius" }, new string[] { "5", "°f", "in", "DegreeCelsius" })] - [DataRow(new string[] { "5", "c", "in", "f" }, new string[] { "5", "°c", "in", "°f" })] - [DataRow(new string[] { "5", "f", "in", "c" }, new string[] { "5", "°f", "in", "°c" })] + [DataRow(new string[] { "5", "f", "in", "celsius" }, new string[] { "5", "°F", "in", "DegreeCelsius" })] + [DataRow(new string[] { "5", "c", "in", "f" }, new string[] { "5", "°C", "in", "°F" })] + [DataRow(new string[] { "5", "f", "in", "c" }, new string[] { "5", "°F", "in", "°C" })] #pragma warning restore CA1861 // Avoid constant arrays as arguments public void PrefixesDegrees(string[] input, string[] expectedResult) { diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs index 0a34b2942f..1af8b11ea3 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/InputInterpreter.cs @@ -92,7 +92,7 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter if (!isFeet || !isInches) { - // atleast one could not be parsed correctly + // at least one could not be parsed correctly break; } @@ -115,7 +115,7 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter } /// - /// Adds degree prefixes to degree units for shorthand notation. E.g. '10 c in fahrenheit' becomes '10 °c in DegreeFahrenheit'. + /// Adds degree prefixes to degree units for shorthand notation. E.g. '10 c in fahrenheit' becomes '10 °C in DegreeFahrenheit'. /// public static void DegreePrefixer(ref string[] split) { @@ -130,11 +130,11 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter break; case "c": - split[1] = "°c"; + split[1] = "°C"; break; case "f": - split[1] = "°f"; + split[1] = "°F"; break; default: @@ -152,11 +152,11 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter break; case "c": - split[3] = "°c"; + split[3] = "°C"; break; case "f": - split[3] = "°f"; + split[3] = "°F"; break; default: @@ -164,22 +164,6 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter } } - /// - /// The plural form "feet" is not recognized by UniteNets. Replace it with "ft". - /// - public static void FeetToFt(ref string[] split) - { - if (string.Equals(split[1], "feet", StringComparison.OrdinalIgnoreCase)) - { - split[1] = "ft"; - } - - if (string.Equals(split[3], "feet", StringComparison.OrdinalIgnoreCase)) - { - split[3] = "ft"; - } - } - /// /// Converts spelling "kph" to "km/h" /// @@ -292,7 +276,6 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter InputInterpreter.DegreePrefixer(ref split); InputInterpreter.MetreToMeter(ref split); - InputInterpreter.FeetToFt(ref split); InputInterpreter.KPHHandler(ref split); InputInterpreter.GallonHandler(ref split, CultureInfo.CurrentCulture); InputInterpreter.OunceHandler(ref split, CultureInfo.CurrentCulture); diff --git a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs index 6e9e3b48a2..d8c0dce3c6 100644 --- a/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs +++ b/src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter/UnitHandler.cs @@ -11,7 +11,7 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter { public static class UnitHandler { - private static readonly int _roundingFractionalDigits = 4; + private static readonly int _roundingSignificantDigits = 4; private static readonly QuantityInfo[] _included = new QuantityInfo[] { @@ -72,7 +72,7 @@ namespace Community.PowerToys.Run.Plugin.UnitConverter var power = Math.Floor(Math.Log10(Math.Abs(value))); var exponent = Math.Pow(10, power); - var rounded = Math.Round(value / exponent, _roundingFractionalDigits) * exponent; + var rounded = Math.Round(value / exponent, _roundingSignificantDigits) * exponent; return rounded; } diff --git a/src/modules/launcher/PowerLauncher/App.xaml.cs b/src/modules/launcher/PowerLauncher/App.xaml.cs index 30613d2cf5..86255f3ad7 100644 --- a/src/modules/launcher/PowerLauncher/App.xaml.cs +++ b/src/modules/launcher/PowerLauncher/App.xaml.cs @@ -17,6 +17,7 @@ using PowerLauncher.Helper; using PowerLauncher.Plugin; using PowerLauncher.ViewModel; using PowerToys.Interop; +using Windows.Globalization; using Wox; using Wox.Infrastructure; using Wox.Infrastructure.Image; @@ -54,6 +55,19 @@ namespace PowerLauncher { NativeThreadCTS = new CancellationTokenSource(); + try + { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage); + } + } + catch (CultureNotFoundException ex) + { + Logger.LogError("CultureNotFoundException: " + ex.Message); + } + Log.Info($"Starting PowerToys Run with PID={Environment.ProcessId}", typeof(App)); if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredPowerLauncherEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled) { diff --git a/src/modules/launcher/Wox.Plugin/PluginLoadContext.cs b/src/modules/launcher/Wox.Plugin/PluginLoadContext.cs index 03268dd49d..9f4bd29632 100644 --- a/src/modules/launcher/Wox.Plugin/PluginLoadContext.cs +++ b/src/modules/launcher/Wox.Plugin/PluginLoadContext.cs @@ -3,9 +3,11 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.Loader; +using ManagedCommon; namespace Wox.Plugin { diff --git a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs index 9a9dd783aa..6df56d6f46 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs @@ -40,6 +40,12 @@ namespace Peek.UI /// public App() { + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + InitializeComponent(); Logger.InitializeLogger("\\Peek\\Logs"); diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp index 415acad0d4..71c510e357 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -33,6 +34,12 @@ const std::wstring moduleName = L"PowerRename"; /// App::App() { + std::wstring appLanguage = LanguageHelpers::load_language(); + if (!appLanguage.empty()) + { + Microsoft::Windows::Globalization::ApplicationLanguages::PrimaryLanguageOverride(appLanguage); + } + InitializeComponent(); #if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION diff --git a/src/modules/powerrename/PowerRenameUILib/pch.h b/src/modules/powerrename/PowerRenameUILib/pch.h index 66720e53c3..f3b4f298f3 100644 --- a/src/modules/powerrename/PowerRenameUILib/pch.h +++ b/src/modules/powerrename/PowerRenameUILib/pch.h @@ -37,4 +37,5 @@ #include #include #include +#include #include diff --git a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj index a37734ffde..61e0e50c46 100644 --- a/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj +++ b/src/modules/previewpane/UnitTests-SvgThumbnailProvider/UnitTests-SvgThumbnailProvider.csproj @@ -27,6 +27,7 @@ + diff --git a/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml.cs b/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml.cs index 01007f5610..a6a3898f0f 100644 --- a/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml.cs +++ b/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/App.xaml.cs @@ -5,6 +5,7 @@ using System; using System.Web; +using ManagedCommon; using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; using Windows.ApplicationModel.Activation; @@ -25,6 +26,13 @@ namespace RegistryPreview /// public App() { + string appLanguage = LanguageHelper.LoadLanguage(); + + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + this.InitializeComponent(); } diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index c8829773f7..6430824bb1 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -233,6 +233,12 @@ void dispatch_received_json(const std::wstring& json_to_parse) SendMessageW(pt_main_window, WM_CLOSE, 0, 0); } } + else if (name == L"language") + { + constexpr const wchar_t* language_filename = L"\\language.json"; + const std::wstring save_file_location = PTSettingsHelper::get_root_save_folder_location() + language_filename; + json::to_file(save_file_location, j); + } } return; } diff --git a/src/settings-ui/Settings.UI.Library/LanguageModel.cs b/src/settings-ui/Settings.UI.Library/LanguageModel.cs new file mode 100644 index 0000000000..1e9ef82ec6 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/LanguageModel.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.IO.Abstractions; +using System.Text.Json; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class LanguageModel + { + public const string SettingsFilePath = "\\Microsoft\\PowerToys\\"; + public const string SettingsFile = "language.json"; + + public string Tag { get; set; } + + public string ResourceID { get; set; } + + public string Language { get; set; } + + public static string LoadSetting() + { + FileSystem fileSystem = new FileSystem(); + var localAppDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var file = localAppDataDir + SettingsFilePath + SettingsFile; + + if (fileSystem.File.Exists(file)) + { + try + { + Stream inputStream = fileSystem.File.Open(file, FileMode.Open); + StreamReader reader = new StreamReader(inputStream); + string data = reader.ReadToEnd(); + inputStream.Close(); + reader.Dispose(); + + return JsonSerializer.Deserialize(data).LanguageTag; + } + catch (Exception) + { + } + } + + return string.Empty; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs b/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs new file mode 100644 index 0000000000..61a16eb778 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class OutGoingLanguageSettings + { + [JsonPropertyName("language")] + public string LanguageTag { get; set; } + + public OutGoingLanguageSettings() + { + } + + public OutGoingLanguageSettings(string language) + { + LanguageTag = language; + } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 8d38ec94c3..a820502dcd 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -78,6 +78,12 @@ namespace Microsoft.PowerToys.Settings.UI { Logger.InitializeLogger(@"\Settings\Logs"); + string appLanguage = LanguageHelper.LoadLanguage(); + if (!string.IsNullOrEmpty(appLanguage)) + { + Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage; + } + InitializeComponent(); UnhandledException += App_UnhandledException; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml index ed7a634388..1598beecc8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml @@ -102,7 +102,10 @@ - +