Merge branch 'main' into pr35072

This commit is contained in:
Jaime Bernardo
2024-09-25 21:25:45 +01:00
142 changed files with 4830 additions and 2332 deletions

View File

@@ -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

View File

@@ -194,6 +194,7 @@
"PowerToys.WorkspacesSnapshotTool.exe",
"PowerToys.WorkspacesLauncher.exe",
"PowerToys.WorkspacesWindowArranger.exe",
"PowerToys.WorkspacesEditor.exe",
"PowerToys.WorkspacesEditor.dll",
"PowerToys.WorkspacesLauncherUI.exe",

View File

@@ -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

View File

@@ -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

View File

@@ -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')))

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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'))

View File

@@ -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\**

View File

@@ -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

View File

@@ -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
...

46
.pipelines/v2/ci.yml Normal file
View File

@@ -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 }}

106
.pipelines/v2/release.yml Normal file
View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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\**

View File

@@ -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

View File

@@ -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

View File

@@ -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}}"

View File

@@ -0,0 +1,5 @@
steps:
- task: NuGetToolInstaller@1
displayName: Use NuGet 6.6.1
inputs:
versionSpec: 6.6.1

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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'

View File

@@ -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);

View File

@@ -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}

View File

@@ -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).

View File

@@ -1223,7 +1223,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
std::array<std::wstring_view, 36> processesToTerminate = {
std::array<std::wstring_view, 37> 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",
};

View File

@@ -36,6 +36,9 @@
<OutDir Condition=" '$(PerUser)' == 'true' ">$(Platform)\$(Configuration)\UserSetup\</OutDir>
<IntDir Condition=" '$(PerUser)' != 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\</IntDir>
<IntDir Condition=" '$(PerUser)' == 'true' ">$(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\</IntDir>
<!-- The CMD script below checks this value, and it is **CASE SENSITIVE** -->
<NormalizedPerUserValue>false</NormalizedPerUserValue>
<NormalizedPerUserValue Condition=" '$(PerUser)' == 'true' ">true</NormalizedPerUserValue>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
@@ -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)
</Command>
<Message>Backing up original files and populating .NET and WPF Runtime dependencies </Message>
</PreBuildEvent>

View File

@@ -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<OutGoingLanguageSettings>(data).LanguageTag;
}
catch (Exception)
{
}
}
return string.Empty;
}
}
}

View File

@@ -168,6 +168,11 @@
<Midl Include="LayoutMapManaged.idl" />
<Midl Include="TwoWayPipeMessageIPCManaged.idl" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\version\version.vcxproj">
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
</ProjectReference>
</ItemGroup>
<ImportGroup Label="ExtensionTargets" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">

View File

@@ -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;

View File

@@ -0,0 +1,22 @@
#pragma once
#include <filesystem>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/json.h>
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;
}
}

View File

@@ -4,33 +4,204 @@
#include <string>
#include <atlstr.h>
// Get a string from the resource file
inline std::wstring get_resource_string(UINT resource_id, HINSTANCE instance, const wchar_t* fallback)
#include <common/utils/language_helper.h>
inline std::wstring get_english_fallback_string(UINT resource_id, HINSTANCE instance)
{
wchar_t* text_ptr;
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&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<std::size_t>(length) };
wchar_t* text_ptr;
auto length = LoadStringW(instance, resource_id, reinterpret_cast<wchar_t*>(&text_ptr), 0);
if (length == 0)
{
if (!english_string.empty())
{
return std::wstring(english_string);
}
else
{
return fallback;
}
}
else
{
return { text_ptr, static_cast<std::size_t>(length) };
}
}
}

View File

@@ -50,6 +50,12 @@ namespace AdvancedPaste
/// </summary>
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) =>

View File

@@ -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<IUserSettings>();
var optionsViewModel = App.GetService<OptionsViewModel>();
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;

View File

@@ -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<PasteFormat> CustomActionPasteFormats { get; } = [];
public bool IsCustomAIEnabled => IsAllowedByGPO && IsClipboardDataText && aiHelper.IsAIEnabled;
public bool IsPasteWithAIEnabled => IsAllowedByGPO && aiHelper.IsAIEnabled;
public bool IsCustomAIEnabled => IsPasteWithAIEnabled && IsClipboardDataText;
public event EventHandler<CustomActionActivatedEventArgs> 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;
}
}
}

View File

@@ -13,7 +13,9 @@
#include <common/interop/shared_constants.h>
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <common/utils/gpo.h>
#include <winrt/Windows.Security.Credentials.h>
#include <atlfile.h>
#include <atlstr.h>
#include <vector>
@@ -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<int>(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<int>(object.GetNamedNumber(JSON_KEY_ID)));
}
}
}
}

View File

@@ -44,6 +44,12 @@ namespace EnvironmentVariables
/// </summary>
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) =>

View File

@@ -7,6 +7,8 @@
#ifndef PCH_H
#define PCH_H
#include <atlbase.h>
// add headers that you want to pre-compile here
#include "framework.h"

View File

@@ -23,6 +23,12 @@ namespace FileLocksmithUI
/// </summary>
public App()
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
Logger.InitializeLogger("\\File Locksmith\\FileLocksmithUI\\Logs");
this.InitializeComponent();

View File

@@ -38,6 +38,12 @@ namespace Hosts
/// </summary>
public App()
{
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
InitializeComponent();
Host.HostInstance = Microsoft.Extensions.Hosting.Host.

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -1,150 +1,27 @@
#include "pch.h"
#include "AppLauncher.h"
#include <filesystem>
#include <shellapi.h>
#include <winrt/Windows.Management.Deployment.h>
#include <winrt/Windows.ApplicationModel.Core.h>
#include <shellapi.h>
#include <ShellScalingApi.h>
#include <filesystem>
#include <workspaces-common/MonitorEnumerator.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/AppUtils.h>
#include <common/Display/dpi_aware.h>
#include <common/utils/winapi_error.h>
#include <LaunchingApp.h>
#include <LauncherUIHelper.h>
#include <RegistryUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Management::Deployment;
namespace FancyZones
namespace AppLauncher
{
inline bool allMonitorsHaveSameDpiScaling()
void UpdatePackagedApps(std::vector<WorkspacesData::WorkspacesProject::Application>& 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<WorkspacesData::WorkspacesProject::Application>& 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<SHELLEXECUTEINFO, std::wstring> 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<HWND>& 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<bool> 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<float>(placement.rcNormalPosition.left);
float y = static_cast<float>(placement.rcNormalPosition.top);
float width = static_cast<float>(placement.rcNormalPosition.right - placement.rcNormalPosition.left);
float height = static_cast<float>(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top);
DPIAware::InverseConvert(monitor, x, y);
DPIAware::InverseConvert(monitor, width, height);
WorkspacesData::WorkspacesProject::Application::Position windowPosition{
.x = static_cast<int>(std::round(x)),
.y = static_cast<int>(std::round(y)),
.width = static_cast<int>(std::round(width)),
.height = static_cast<int>(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<bool> 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<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors)
{
bool launchedSuccessfully{ true };
LauncherUIHelper uiHelper;
uiHelper.LaunchUI();
// Get the set of windows before launching the app
std::vector<HWND> 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<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> 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<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(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;
}
}

View File

@@ -1,7 +1,16 @@
#pragma once
#include <shellapi.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/Result.h>
#include <WorkspacesLib/WorkspacesData.h>
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
namespace AppLauncher
{
using ErrorList = std::vector<std::pair<std::wstring, std::wstring>>;
bool Launch(WorkspacesData::WorkspacesProject& project, const std::vector<WorkspacesData::WorkspacesProject::Monitor>& monitors, ErrorList& launchErrors);
Result<SHELLEXECUTEINFO, std::wstring> LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated);
bool Launch(WorkspacesData::WorkspacesProject& project, LaunchingStatus& launchingStatus, ErrorList& launchErrors);
}

View File

@@ -0,0 +1,123 @@
#include "pch.h"
#include "Launcher.h"
#include <common/utils/json.h>
#include <workspaces-common/MonitorUtils.h>
#include <WorkspacesLib/trace.h>
#include <AppLauncher.h>
Launcher::Launcher(const WorkspacesData::WorkspacesProject& project,
std::vector<WorkspacesData::WorkspacesProject>& 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<LauncherUIHelper>()),
m_windowArrangerHelper(std::make_unique<WindowArrangerHelper>(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<double> 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<double> 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");
}
}
}

View File

@@ -0,0 +1,31 @@
#pragma once
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <workspaces-common/InvokePoint.h>
#include <LauncherUIHelper.h>
#include <WindowArrangerHelper.h>
class Launcher
{
public:
Launcher(const WorkspacesData::WorkspacesProject& project, std::vector<WorkspacesData::WorkspacesProject>& workspaces, InvokePoint invokePoint);
~Launcher();
void Launch();
private:
WorkspacesData::WorkspacesProject m_project;
std::vector<WorkspacesData::WorkspacesProject>& m_workspaces;
const InvokePoint m_invokePoint;
const std::chrono::steady_clock::time_point m_start;
std::unique_ptr<LauncherUIHelper> m_uiHelper;
std::unique_ptr<WindowArrangerHelper> m_windowArrangerHelper;
LaunchingStatus m_launchingStatus;
bool m_launchedSuccessfully{};
std::vector<std::pair<std::wstring, std::wstring>> m_launchErrors{};
void handleWindowArrangerMessage(const std::wstring& msg);
};

View File

@@ -7,12 +7,22 @@
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h>
#include <AppLauncher.h>
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());
}

View File

@@ -1,16 +1,18 @@
#pragma once
#include <LaunchingApp.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <WorkspacesLib/IPCHelper.h>
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;
};

View File

@@ -1,13 +0,0 @@
#pragma once
#include <Windows.h>
#include <WorkspacesLib/WorkspacesData.h>
struct LaunchingApp
{
WorkspacesData::WorkspacesProject::Application application;
HWND window;
std::wstring state;
};
using LaunchingApps = std::vector<LaunchingApp>;

View File

@@ -0,0 +1,71 @@
#include "pch.h"
#include "WindowArrangerHelper.h"
#include <filesystem>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/winapi_error.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <AppLauncher.h>
WindowArrangerHelper::WindowArrangerHelper(std::function<void(const std::wstring&)> 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<bool()> 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());
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <common/utils/OnThreadExecutor.h>
class WindowArrangerHelper
{
public:
WindowArrangerHelper(std::function<void(const std::wstring&)> ipcCallback);
~WindowArrangerHelper();
void Launch(const std::wstring& projectId, bool elevated, std::function<bool()> keepWaitingCallback);
private:
DWORD m_processId;
IPCHelper m_ipcHelper;
OnThreadExecutor m_threadExecutor;
};

View File

@@ -127,21 +127,23 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AppLauncher.cpp" />
<ClCompile Include="Launcher.cpp" />
<ClCompile Include="LauncherUIHelper.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="RegistryUtils.cpp" />
<ClCompile Include="WindowArrangerHelper.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="AppLauncher.h" />
<ClInclude Include="Launcher.h" />
<ClInclude Include="LauncherUIHelper.h" />
<ClInclude Include="LaunchingApp.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="RegistryUtils.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WindowArrangerHelper.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -27,13 +27,13 @@
<ClInclude Include="RegistryUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LauncherUIHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingApp.h">
<ClInclude Include="WindowArrangerHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Launcher.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
@@ -53,6 +53,12 @@
<ClCompile Include="LauncherUIHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowArrangerHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Launcher.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -1,15 +1,4 @@
#include "pch.h"
#include <WorkspacesLib/WorkspacesData.h>
#include <WorkspacesLib/trace.h>
#include <AppLauncher.h>
#include <utils.h>
#include <Generated Files/resource.h>
#include <workspaces-common/InvokePoint.h>
#include <workspaces-common/MonitorUtils.h>
#include "pch.h"
#include <common/utils/elevation.h>
#include <common/utils/gpo.h>
@@ -18,6 +7,13 @@
#include <common/utils/UnhandledExceptionHandler.h>
#include <common/utils/resources.h>
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/utils.h>
#include <Launcher.h>
#include <Generated Files/resource.h>
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<InvokePoint>(std::stoi(cmdArgs[1]));
}
catch (std::exception)
{
}
}
Logger::trace(L"Invoke point: {}", invokePoint);
Logger::trace(L"Invoke point: {}", cmdArgs.invokePoint);
// read workspaces
std::vector<WorkspacesData::WorkspacesProject> 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<std::pair<std::wstring, std::wstring>> 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<double> 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;
}

View File

@@ -1,20 +0,0 @@
#pragma once
#include <vector>
#include <string>
std::vector<std::string> split(std::string s, const std::string& delimiter)
{
std::vector<std::string> 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;
}

View File

@@ -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<string> 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();
}

View File

@@ -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<AppLaunchDataWrapper>
public class AppLaunchData : WorkspacesUIData<AppLaunchDataWrapper>
{
public static string File
{
get
{
return FolderUtils.DataFolder() + "\\launch-workspaces.json";
}
}
public struct AppLaunchDataWrapper
{
[JsonPropertyName("apps")]

View File

@@ -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<AppLaunchInfoWrapper>
public class AppLaunchInfoData : WorkspacesUIData<AppLaunchInfoWrapper>
{
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; }
}
}
}

View File

@@ -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<AppLaunchInfoListWrapper>
public class AppLaunchInfosData : WorkspacesUIData<AppLaunchInfoListWrapper>
{
public struct AppLaunchInfoListWrapper
{

View File

@@ -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; }
}
}

View File

@@ -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,
}
}

View File

@@ -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();
}
}
}

View File

@@ -8,7 +8,7 @@ using WorkspacesLauncherUI.Utils;
namespace Workspaces.Data
{
public class WorkspacesEditorData<T>
public class WorkspacesUIData<T>
{
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<T>(data, JsonOptions);
}

View File

@@ -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;

View File

@@ -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(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", 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}");
}
}

View File

@@ -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";
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<AppLaunching> AppsListed { get; set; } = new ObservableCollection<AppLaunching>();
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<AppLaunching> appLaunchingList = new List<AppLaunching>();
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<AppLaunching>(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();
}

View File

@@ -0,0 +1,42 @@
#include "pch.h"
#include "IPCHelper.h"
#include <common/logger/logger.h>
IPCHelper::IPCHelper(const std::wstring& currentPipeName, const std::wstring receiverPipeName, std::function<void(const std::wstring&)> 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<TwoWayPipeMessageIPC>(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);
}
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <mutex>
#include <common/interop/two_way_pipe_message_ipc.h>
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<void(const std::wstring&)> messageCallback);
~IPCHelper();
void send(const std::wstring& message) const;
private:
void receive(const std::wstring& msg);
std::unique_ptr<TwoWayPipeMessageIPC> ipc;
std::mutex ipcMutex;
std::function<void(const std::wstring&)> callback;
};

View File

@@ -0,0 +1,106 @@
#include "pch.h"
#include "JsonUtils.h"
#include <filesystem>
#include <common/logger/logger.h>
namespace JsonUtils
{
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> 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<std::vector<WorkspacesData::WorkspacesProject>, 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<WorkspacesData::WorkspacesProject>& 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;
}
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <WorkspacesLib/Result.h>
#include <WorkspacesLib/WorkspacesData.h>
namespace JsonUtils
{
enum class WorkspacesFileError
{
FileReadingError,
IncorrectFileError,
};
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName);
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName);
bool Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects);
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project);
}

View File

@@ -0,0 +1,10 @@
#pragma once
// sync with WorkspacesLauncherUI : Data : LaunchingState.cs
enum class LaunchingState
{
Waiting = 0,
Launched,
LaunchedAndMoved,
Failed
};

View File

@@ -0,0 +1,65 @@
#include "pch.h"
#include "LaunchingStatus.h"
#include <common/logger/logger.h>
LaunchingStatus::LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function<void(const WorkspacesData::LaunchingAppStateMap&)> 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);
}
}

View File

@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <shared_mutex>
#include <WorkspacesLib/WorkspacesData.h>
class LaunchingStatus
{
public:
LaunchingStatus(const WorkspacesData::WorkspacesProject& project, std::function<void(const WorkspacesData::LaunchingAppStateMap&)> 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<void(const WorkspacesData::LaunchingAppStateMap&)> m_updateCallback;
std::shared_mutex m_mutex;
};

View File

@@ -0,0 +1,53 @@
#pragma once
#include <variant>
template<typename T>
class Ok
{
public:
explicit constexpr Ok(T value) :
value(std::move(value)) {}
constexpr T&& get() { return std::move(value); }
T value;
};
template<typename T>
class Error
{
public:
explicit constexpr Error(T value) :
value(std::move(value)) {}
constexpr T&& get() { return std::move(value); }
T value;
};
template<typename OkT, typename ErrT>
class Result
{
public:
using VariantT = std::variant<Ok<OkT>, Error<ErrT>>;
constexpr Result(Ok<OkT> value) :
variant(std::move(value))
{}
constexpr Result(Error<ErrT> value) :
variant(std::move(value))
{}
constexpr bool isOk() const { return std::holds_alternative<Ok<OkT>>(variant); }
constexpr bool isError() const { return std::holds_alternative<Error<ErrT>>(variant); }
constexpr OkT value() const { return std::get<Ok<OkT>>(variant).value; }
constexpr ErrT error() const { return std::get<Error<ErrT>>(variant).value; }
constexpr OkT&& getValue() { return std::get<Ok<OkT>>(variant).get(); }
constexpr ErrT&& getError() { return std::get<Error<ErrT>>(variant).get(); }
VariantT variant;
};

View File

@@ -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<int>(data.state)));
return json;
}
std::optional<LaunchingAppState> 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<LaunchingState>(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<AppLaunchInfo>& 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<LaunchingAppStateMap> 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;
}

View File

@@ -2,11 +2,12 @@
#include <common/utils/json.h>
#include <WorkspacesLib/LaunchingStateEnum.h>
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<WorkspacesProject> 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<AppLaunchInfo>& data);
}
using LaunchingAppStateMap = std::map<WorkspacesData::WorkspacesProject::Application, LaunchingAppState>;
using LaunchingAppStateList = std::vector<std::pair<WorkspacesData::WorkspacesProject::Application, LaunchingState>>;
struct AppLaunchData
{
std::vector<AppLaunchInfo> 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<WorkspacesProject>& data);
std::optional<std::vector<WorkspacesProject>> FromJson(const json::JsonObject& json);
}
namespace AppLaunchInfoJSON
{
json::JsonObject ToJson(const LaunchingAppState& data);
std::optional<LaunchingAppState> FromJson(const json::JsonObject& json);
}
namespace AppLaunchInfoListJSON
{
json::JsonObject ToJson(const LaunchingAppStateMap& data);
std::optional<LaunchingAppStateMap> FromJson(const json::JsonObject& json);
}
namespace AppLaunchDataJSON
{
json::JsonObject ToJson(const AppLaunchData& data);
}
};

View File

@@ -33,19 +33,32 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="AppUtils.h" />
<ClInclude Include="IPCHelper.h" />
<ClInclude Include="JsonUtils.h" />
<ClInclude Include="LaunchingStateEnum.h" />
<ClInclude Include="LaunchingStatus.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Result.h" />
<ClInclude Include="utils.h" />
<ClInclude Include="WorkspacesData.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="AppUtils.cpp" />
<ClCompile Include="IPCHelper.cpp" />
<ClCompile Include="JsonUtils.cpp" />
<ClCompile Include="LaunchingStatus.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
<ClCompile Include="WorkspacesData.cpp" />
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj">
<Project>{f055103b-f80b-4d0c-bf48-057c55620033}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>

View File

@@ -23,6 +23,24 @@
<ClInclude Include="AppUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Result.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="JsonUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="IPCHelper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="utils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingStateEnum.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LaunchingStatus.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
@@ -37,6 +55,18 @@
<ClCompile Include="AppUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="JsonUtils.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="IPCHelper.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="two_way_pipe_message_ipc.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LaunchingStatus.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -0,0 +1,469 @@
#include "pch.h"
#include <common/interop/two_way_pipe_message_ipc_impl.h>
#include <iterator>
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<LPVOID>(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<PTOKEN_GROUPS>(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<LPVOID>(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<PSID>(HeapAlloc(GetProcessHeap(),
HEAP_ZERO_MEMORY,
dwLength));
if (*ppsid == NULL)
goto Cleanup;
if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid))
{
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(*ppsid));
goto Cleanup;
}
break;
}
bSuccess = TRUE;
Cleanup:
// Free the buffer for the token groups.
if (ptg != NULL)
HeapFree(GetProcessHeap(), 0, static_cast<LPVOID>(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<LPVOID>(*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<LPTSTR>(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<HLOCAL>(new_dacl));
Lclean_sd:
LocalFree(static_cast<HLOCAL>(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<PSID>(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;
}
}

View File

@@ -0,0 +1,54 @@
#pragma once
#include <vector>
#include <string>
#include <workspaces-common/GuidUtils.h>
#include <workspaces-common/InvokePoint.h>
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<std::wstring> 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<InvokePoint>(std::stoi(token));
cmdArgs.invokePoint = invokePoint;
}
catch (std::exception)
{
}
}
else
{
auto guid = GuidFromString(token);
if (guid.has_value())
{
cmdArgs.workspaceId = token;
}
}
}
return cmdArgs;
}

View File

@@ -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";

View File

@@ -1,57 +0,0 @@
#pragma once
#include <vector>
#include <WorkspacesLib/WorkspacesData.h>
#include <common/logger/logger.h>
namespace WorkspacesJsonUtils
{
inline std::vector<WorkspacesData::WorkspacesProject> Read(const std::wstring& fileName)
{
std::vector<WorkspacesData::WorkspacesProject> 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<WorkspacesData::WorkspacesProject>& 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());
}
}
}

View File

@@ -133,7 +133,6 @@
<ClCompile Include="SnapshotUtils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="JsonUtils.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.base.h" />
<ClInclude Include="SnapshotUtils.h" />

View File

@@ -18,9 +18,6 @@
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="JsonUtils.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SnapshotUtils.h">
<Filter>Header Files</Filter>
</ClInclude>

View File

@@ -5,21 +5,21 @@
#include <workspaces-common/GuidUtils.h>
#include <workspaces-common/MonitorUtils.h>
#include <WorkspacesLib/JsonUtils.h>
#include <WorkspacesLib/WorkspacesData.h>
#include <JsonUtils.h>
#include <SnapshotUtils.h>
#include <common/utils/gpo.h>
#include <common/utils/logger_helper.h>
#include <common/utils/UnhandledExceptionHandler.h>
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();

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,257 @@
#include "pch.h"
#include "WindowArranger.h"
#include <common/logger/logger.h>
#include <common/utils/OnThreadExecutor.h>
#include <common/utils/process_path.h>
#include <common/utils/winapi_error.h>
#include <workspaces-common/MonitorUtils.h>
#include <workspaces-common/WindowEnumerator.h>
#include <workspaces-common/WindowFilter.h>
#include <workspaces-common/WindowUtils.h>
#include <WindowProperties/WorkspacesWindowPropertyUtils.h>
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<HWND> windowsAfter = WindowEnumerator::Enumerate(WindowFilter::Filter);
std::vector<HWND> 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<HWND> 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<float>(snapMonitorIter->dpi) / currentDpi;
rect.left = static_cast<long>(std::round(rect.left * mult));
rect.right = static_cast<long>(std::round(rect.right * mult));
rect.top = static_cast<long>(std::round(rect.top * mult));
rect.bottom = static_cast<long>(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<WorkspacesData::WorkspacesProject::Application, WorkspacesData::LaunchingAppState>& val) {
return val.second.window == nullptr;
}) == m_launchingApps.end();
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <WindowCreationHandler.h>
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/IPCHelper.h>
#include <WorkspacesLib/LaunchingStatus.h>
#include <WorkspacesLib/WorkspacesData.h>
class WindowArranger
{
public:
WindowArranger(WorkspacesData::WorkspacesProject project, const IPCHelper& ipcHelper);
~WindowArranger() = default;
private:
const WorkspacesData::WorkspacesProject m_project;
const std::vector<HWND> m_windowsBefore;
const std::vector<WorkspacesData::WorkspacesProject::Monitor> 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;
};

View File

@@ -0,0 +1,60 @@
#include "pch.h"
#include "WindowCreationHandler.h"
WindowCreationHandler::WindowCreationHandler(std::function<void(HWND)> 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<DWORD, 3> 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;
}
}

View File

@@ -0,0 +1,30 @@
#pragma once
class WindowCreationHandler
{
public:
WindowCreationHandler(std::function<void(HWND)> windowCreatedCallback);
~WindowCreationHandler();
private:
static inline WindowCreationHandler* s_instance = nullptr;
std::vector<HWINEVENTHOOK> m_staticWinEventHooks;
std::function<void(HWND)> 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);
}
}
};

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Project configurations -->
<!-- Props that should be disabled while building on CI server -->
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h WorkspacesWindowArrangerResource.base.rc WorkspacesWindowArrangerResource.rc" />
</Target>
<!-- C++ source compile-specific things for all configurations -->
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
</Link>
<Lib>
<TreatLibWarningAsErrors>true</TreatLibWarningAsErrors>
</Lib>
</ItemDefinitionGroup>
<!-- C++ source compile-specific things for Debug/Release configurations -->
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Optimization>MaxSpeed</Optimization>
<SDLCheck>false</SDLCheck>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{37D07516-4185-43A4-924F-3C7A5D95ECF6}</ProjectGuid>
<RootNamespace>WorkspacesWindowArranger</RootNamespace>
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<SpectreMitigation>Spectre</SpectreMitigation>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<TargetName>PowerToys.$(MSBuildProjectName)</TargetName>
<OutDir>..\..\..\..\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<AdditionalIncludeDirectories>./../;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src\common;$(SolutionDir)src\;./;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>shcore.lib;Shell32.lib;propsys.lib;DbgHelp.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="WindowArranger.cpp" />
<ClCompile Include="WindowCreationHandler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="WindowArranger.h" />
<ClInclude Include="WindowCreationHandler.h" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\WorkspacesLib\WorkspacesLib.vcxproj">
<Project>{b31fcc55-b5a4-4ea7-b414-2dceae6af332}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files/WorkspacesWindowArrangerResource.rc" />
<None Include="WorkspacesWindowArrangerResource.base.rc" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowCreationHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="WindowArranger.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowCreationHandler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="WindowArranger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="WorkspacesWindowArrangerResource.base.rc">
<Filter>Resource Files</Filter>
</None>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files/WorkspacesWindowArrangerResource.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resource.resx">
<Filter>Resource Files</Filter>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More