mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
Merge branch 'main' into pr35072
This commit is contained in:
10
.github/actions/spell-check/expect.txt
vendored
10
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
|
||||
"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
"PowerToys.WorkspacesLauncher.exe",
|
||||
"PowerToys.WorkspacesWindowArranger.exe",
|
||||
"PowerToys.WorkspacesEditor.exe",
|
||||
"PowerToys.WorkspacesEditor.dll",
|
||||
"PowerToys.WorkspacesLauncherUI.exe",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')))
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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'))
|
||||
@@ -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\**
|
||||
@@ -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
|
||||
@@ -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
46
.pipelines/v2/ci.yml
Normal 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
106
.pipelines/v2/release.yml
Normal 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)
|
||||
538
.pipelines/v2/templates/job-build-project.yml
Normal file
538
.pipelines/v2/templates/job-build-project.yml
Normal 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()
|
||||
30
.pipelines/v2/templates/job-ci-precheck.yml
Normal file
30
.pipelines/v2/templates/job-ci-precheck.yml
Normal 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
|
||||
@@ -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
|
||||
76
.pipelines/v2/templates/job-test-project.yml
Normal file
76
.pipelines/v2/templates/job-test-project.yml
Normal 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\**
|
||||
59
.pipelines/v2/templates/pipeline-ci-build.yml
Normal file
59
.pipelines/v2/templates/pipeline-ci-build.yml
Normal 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
|
||||
184
.pipelines/v2/templates/steps-build-installer.yml
Normal file
184
.pipelines/v2/templates/steps-build-installer.yml
Normal 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
|
||||
27
.pipelines/v2/templates/steps-ensure-dotnet-version.yml
Normal file
27
.pipelines/v2/templates/steps-ensure-dotnet-version.yml
Normal 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}}"
|
||||
5
.pipelines/v2/templates/steps-ensure-nuget-version.yml
Normal file
5
.pipelines/v2/templates/steps-ensure-nuget-version.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
steps:
|
||||
- task: NuGetToolInstaller@1
|
||||
displayName: Use NuGet 6.6.1
|
||||
inputs:
|
||||
versionSpec: 6.6.1
|
||||
22
.pipelines/v2/templates/steps-esrp-signing.yml
Normal file
22
.pipelines/v2/templates/steps-esrp-signing.yml
Normal 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 }}
|
||||
@@ -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
|
||||
20
.pipelines/v2/templates/steps-restore-nuget.yml
Normal file
20
.pipelines/v2/templates/steps-restore-nuget.yml
Normal 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'
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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).
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
50
src/common/ManagedCommon/LanguageHelper.cs
Normal file
50
src/common/ManagedCommon/LanguageHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
22
src/common/utils/language_helper.h
Normal file
22
src/common/utils/language_helper.h
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
123
src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp
Normal file
123
src/modules/Workspaces/WorkspacesLauncher/Launcher.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/modules/Workspaces/WorkspacesLauncher/Launcher.h
Normal file
31
src/modules/Workspaces/WorkspacesLauncher/Launcher.h
Normal 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);
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
42
src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp
Normal file
42
src/modules/Workspaces/WorkspacesLib/IPCHelper.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
29
src/modules/Workspaces/WorkspacesLib/IPCHelper.h
Normal file
29
src/modules/Workspaces/WorkspacesLib/IPCHelper.h
Normal 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;
|
||||
};
|
||||
106
src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp
Normal file
106
src/modules/Workspaces/WorkspacesLib/JsonUtils.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
19
src/modules/Workspaces/WorkspacesLib/JsonUtils.h
Normal file
19
src/modules/Workspaces/WorkspacesLib/JsonUtils.h
Normal 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);
|
||||
}
|
||||
10
src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h
Normal file
10
src/modules/Workspaces/WorkspacesLib/LaunchingStateEnum.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// sync with WorkspacesLauncherUI : Data : LaunchingState.cs
|
||||
enum class LaunchingState
|
||||
{
|
||||
Waiting = 0,
|
||||
Launched,
|
||||
LaunchedAndMoved,
|
||||
Failed
|
||||
};
|
||||
65
src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp
Normal file
65
src/modules/Workspaces/WorkspacesLib/LaunchingStatus.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
24
src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h
Normal file
24
src/modules/Workspaces/WorkspacesLib/LaunchingStatus.h
Normal 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;
|
||||
};
|
||||
53
src/modules/Workspaces/WorkspacesLib/Result.h
Normal file
53
src/modules/Workspaces/WorkspacesLib/Result.h
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
54
src/modules/Workspaces/WorkspacesLib/utils.h
Normal file
54
src/modules/Workspaces/WorkspacesLib/utils.h
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
120
src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx
Normal file
120
src/modules/Workspaces/WorkspacesWindowArranger/Resource.resx
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user