mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-07 11:00:32 +02:00
Compare commits
2 Commits
user/muyua
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b568e93dc | ||
|
|
4b1e0f6220 |
@@ -74,6 +74,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.UnitTests", "sr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ModuleTemplateCompileTest", "tools\project_template\ModuleTemplate\ModuleTemplateCompileTest.vcxproj", "{64A80062-4D8B-4229-8A38-DFA1D7497749}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ModuleTemplateCompileTest", "tools\project_template\ModuleTemplate\ModuleTemplateCompileTest.vcxproj", "{64A80062-4D8B-4229-8A38-DFA1D7497749}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ptcli", "tools\ptcli\ptcli.csproj", "{2589570C-B068-41CA-A554-BDCAE6FC4CAC}"
|
||||||
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager", "src\modules\keyboardmanager\dll\KeyboardManager.vcxproj", "{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager", "src\modules\keyboardmanager\dll\KeyboardManager.vcxproj", "{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imageresizer", "imageresizer", "{6C7F47CC-2151-44A3-A546-41C70025132C}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imageresizer", "imageresizer", "{6C7F47CC-2151-44A3-A546-41C70025132C}"
|
||||||
@@ -926,6 +928,14 @@ Global
|
|||||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|ARM64.Build.0 = Release|ARM64
|
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.ActiveCfg = Release|x64
|
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.ActiveCfg = Release|x64
|
||||||
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.Build.0 = Release|x64
|
{64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.Build.0 = Release|x64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{2589570C-B068-41CA-A554-BDCAE6FC4CAC}.Release|x64.Build.0 = Release|x64
|
||||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.Build.0 = Debug|ARM64
|
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.ActiveCfg = Debug|x64
|
{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
|||||||
59
pt-cli-plan.md
Normal file
59
pt-cli-plan.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# PowerToys CLI Implementation Plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
- Deliver the `ptcli` command-line experience described in `pt-cli.md`, with `Runner` acting as the single broker for module commands.
|
||||||
|
- Provide a maintainable architecture where modules self-describe commands, and CLI clients consume a uniform JSON/NamedPipe protocol.
|
||||||
|
|
||||||
|
## Workstreams
|
||||||
|
|
||||||
|
### 1. Broker Foundation (Runner)
|
||||||
|
- **Command Registry**: Implement `IModuleCommandProvider` registration on module load and persist `CommandDescriptor` metadata (schema, elevation flag, long-running hints, docs).
|
||||||
|
- **IPC Host**: Stand up the `\\.\pipe\PowerToys.Runner.CLI` NamedPipe server; define request/response DTOs with versioning (`v` field, correlation IDs).
|
||||||
|
- **Dispatch Pipeline**: Validate module/action, apply schema validation, enforce elevation policy, and invoke `ExecuteAsync`.
|
||||||
|
- **Response Envelope**: Normalize `status` (`ok|error|accepted`), payload, and error block (`code/message/details`). Emit diagnostic logging (caller, command, latency, result).
|
||||||
|
|
||||||
|
### 2. CLI Thin Client (`ptcli`)
|
||||||
|
- **Argument Parsing**: Support `ptcli -m <module> <action> [--arg value]`, plus `--list-modules`, `--list-commands`.
|
||||||
|
- **Transport**: Serialize requests to JSON, connect to the pipe with timeout handling, and deserialize responses.
|
||||||
|
- **Output UX**: Map standard errors to friendly text, show structured results, and support optional `--json` passthrough.
|
||||||
|
- **Async Jobs**: Handle `status=accepted` by printing job IDs, exposing `ptcli job status <id>` and `ptcli job cancel <id>` commands (polling via Runner endpoints).
|
||||||
|
|
||||||
|
### 3. Module Onboarding
|
||||||
|
- **Awake**: Implement `IModuleCommandProvider` returning `set/start/stop/list` commands. Adapt current APIs or legacy triggers inside `ExecuteAsync`.
|
||||||
|
- **Workspaces**: Provide `list/apply/delete` commands; wrap existing workspace manager calls. Ensure long-running operations flag `LongRunning=true`.
|
||||||
|
- **Legacy Adapters**: For modules still using raw events/pipes, add Runner-side shims that translate command invocations while longer-term refactors are scheduled.
|
||||||
|
|
||||||
|
### 4. Capability Discovery & Help
|
||||||
|
- **Describe APIs**: Expose Runner endpoints for `modules`, `commands`, parameter schemas, and elevation requirements.
|
||||||
|
- **CLI Help**: Use discovery data to render `ptcli help`, module-specific usage, and argument hints without duplicating metadata.
|
||||||
|
|
||||||
|
### 5. Reliability, Security, Observability
|
||||||
|
- **Security**: Configure pipe DACL to restrict access to the interactive user; enforce argument length/type limits.
|
||||||
|
- **Concurrency**: Process each request on a dedicated task; delegate concurrency limits to modules. Provide cancellation tokens from Runner.
|
||||||
|
- **Tracing**: Emit structured logs/ETW for requests, errors, and long-running progress notifications.
|
||||||
|
- **Error Catalog**: Implement standardized error codes (`E_MODULE_NOT_FOUND`, `E_ARGS_INVALID`, `E_NEEDS_ELEVATION`, `E_TIMEOUT`, etc.) and map module exceptions accordingly.
|
||||||
|
|
||||||
|
### 6. Elevation & Policies
|
||||||
|
- **Elevation Flow**: Detect when commands require elevation; if Runner is not elevated, return `E_NEEDS_ELEVATION` with actionable hints. Integrate with existing elevated Runner helper when available.
|
||||||
|
- **Policy Hooks**: Add optional checks for policy/experiment gates before command execution.
|
||||||
|
|
||||||
|
### 7. Progress & Notifications
|
||||||
|
- **Progress Channel**: Support incremental JSON progress messages over the same pipe or via job polling endpoints.
|
||||||
|
- **Timeouts/Retry**: Implement configurable `timeoutMs` handling and `E_BUSY_RETRY` responses for transient module lock scenarios.
|
||||||
|
|
||||||
|
### 8. Incremental Rollout Strategy
|
||||||
|
- **Phase 1**: Ship Runner pipe host + CLI client with two flagship commands (Awake.Set, Workspaces.List); document manual enablement.
|
||||||
|
- **Phase 2**: Migrate additional modules through adapters; add help/describe surfaces and job management.
|
||||||
|
- **Phase 3**: Enforce schema validation, finalize error catalog, and wire observability dashboards.
|
||||||
|
- **Phase 4**: Deprecate direct module NamedPipe/event entry points once CLI parity is achieved.
|
||||||
|
|
||||||
|
### 9. Documentation & Maintenance
|
||||||
|
- **User Docs**: Populate `pt-cli.md` with usage examples, elevation guidance, and troubleshooting mapped to error codes.
|
||||||
|
- **Developer Guide**: Add module author instructions for implementing `IModuleCommandProvider`, including schema examples and best practices.
|
||||||
|
- **Release Checklist**: Track new commands per release, update discovery metadata, and ensure CLI integration tests cover regression cases.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- What tooling will maintain JSON schemas (hand-authored vs. source-generated)?
|
||||||
|
- Should progress streaming use duplex pipe messages or a per-job polling API?
|
||||||
|
- How will elevated Runner lifecycle be managed (reuse existing helper vs. new broker)?
|
||||||
|
- Which modules are in-scope for the first public preview, and what is the rollout schedule?
|
||||||
358
pt-cli.md
Normal file
358
pt-cli.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
选 Runner 作为唯一的 Server/Broker,ptcli 只是瘦客户端。
|
||||||
|
|
||||||
|
|
||||||
|
模块通过各自的 ModuleInterface 向 Runner 注册“可被调用的命令/参数模式”。
|
||||||
|
|
||||||
|
|
||||||
|
ptcli → Runner 用 统一的 IPC(建议 NamedPipe + JSON-RPC/自定义轻量 JSON 协议)。
|
||||||
|
|
||||||
|
|
||||||
|
Runner 再把请求转发到对应模块(可以是直接调用模块公开的接口,或转译为该模块现有的触发机制,如 Event/NamedPipe)。
|
||||||
|
|
||||||
|
|
||||||
|
对“历史遗留的 event handle/pipe 触发点”,短期由 Runner 做兼容层;长期逐步统一为“命令接口”。
|
||||||
|
|
||||||
|
|
||||||
|
这样你能得到:能力发现、参数校验、权限/提权、错误码一致、可观察性一致、向后兼容。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
组件与职责
|
||||||
|
|
||||||
|
|
||||||
|
ptcli(瘦客户端)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
解析命令行:ptcli -m awake set --duration 1h / ptcli -m workspace list
|
||||||
|
|
||||||
|
|
||||||
|
将其映射为通用消息(JSON)发给 Runner。
|
||||||
|
|
||||||
|
|
||||||
|
处理同步/异步返回、展示统一错误码与人类可读信息。
|
||||||
|
|
||||||
|
|
||||||
|
最多内置“列出模块/命令的帮助”这类“离线功能”,但真正的能力发现来自 Runner。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Runner(统一 Server/Broker)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
启动时建立 NamedPipe 服务端:\\.\pipe\PowerToys.Runner.CLI(示例)。
|
||||||
|
|
||||||
|
|
||||||
|
维护 Command Registry:每个模块在加载/初始化时注册自己的命令(名称、参数 schema、是否需要提权、是否长任务、超时时间建议、描述文案等)。
|
||||||
|
|
||||||
|
|
||||||
|
收到请求后:
|
||||||
|
|
||||||
|
|
||||||
|
校验模块是否存在、命令是否存在、参数是否通过 schema 验证。
|
||||||
|
|
||||||
|
|
||||||
|
如需提权且当前 Runner 权限不足:按策略返回“需要提权”的标准错误,或通过你们现有的提权助手启动“Elevated Runner”做代办。
|
||||||
|
|
||||||
|
|
||||||
|
转发给目标模块(优先调用模块公开的“命令接口方法”;若模块尚未改造,由 Runner 适配为该模块现有触发(Event/NamedPipe))。
|
||||||
|
|
||||||
|
|
||||||
|
汇总返回值,统一封装标准响应(状态、数据、错误码、诊断信息)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Module(实现者)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
实现 IModuleCommandProvider(示例命名):
|
||||||
|
|
||||||
|
|
||||||
|
IEnumerable<CommandDescriptor> DescribeCommands() 暴露命令元数据;
|
||||||
|
|
||||||
|
|
||||||
|
Task<CommandResult> ExecuteAsync(CommandInvocation ctx) 执行命令;
|
||||||
|
|
||||||
|
|
||||||
|
可标注“需要前台 UI”、“需要管理员”、“可能长时间运行(支持取消/进度)”等。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
现有“事件/NamedPipe 触发路径”的模块:短期由 Runner 适配;长期建议模块直接实现上面的 ExecuteAsync,统一语义与可观测性。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
协议与数据结构(建议)
|
||||||
|
请求(ptcli→Runner)
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"correlationId": "uuid",
|
||||||
|
"command": {
|
||||||
|
"module": "awake",
|
||||||
|
"action": "set", // 例如 set/start/stop/list 等
|
||||||
|
"args": { "duration": "1h" } // 按模块定义的 schema
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"timeoutMs": 20000,
|
||||||
|
"wantProgress": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
响应(Runner→ptcli)
|
||||||
|
{
|
||||||
|
"v": 1,
|
||||||
|
"correlationId": "uuid",
|
||||||
|
"status": "ok", // ok | error | accepted (异步)
|
||||||
|
"result": { /* 模块返回的结构化数据 */ },
|
||||||
|
"error": { // 仅当 status=error
|
||||||
|
"code": "E_NEEDS_ELEVATION", // 标准化错误码
|
||||||
|
"message": "Awake requires elevation",
|
||||||
|
"details": { "hint": "rerun with --elevated" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
进度/异步(可选)
|
||||||
|
|
||||||
|
|
||||||
|
长任务时,status="accepted" 并返回 jobId;ptcli 可 ptcli job status <jobId> 轮询,或 Runner 通过同管道 增量推送 progress(JSON lines)。
|
||||||
|
|
||||||
|
|
||||||
|
取消:ptcli 发送 { action: "cancel", jobId: "..." },Runner 调用模块 CancellationToken。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
命令发现与帮助
|
||||||
|
|
||||||
|
|
||||||
|
ptcli -m list:列出模块(Runner 直接返回 registry)。
|
||||||
|
|
||||||
|
|
||||||
|
ptcli -m awake -h:DescribeCommands() 中的 Awake 条目返回所有 action、参数与示例。
|
||||||
|
|
||||||
|
|
||||||
|
参数 schema:用简化 JSON Schema(或手写约束)即可,让 ptcli 能本地提示,也让 Runner 能服务器端校验。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
示例映射
|
||||||
|
Awake
|
||||||
|
|
||||||
|
|
||||||
|
ptcli -m awake set --duration 1h
|
||||||
|
→ Awake.Set(duration=1h)
|
||||||
|
→ Runner 调用 AwakeModule.ExecuteAsync("set", args)
|
||||||
|
→ 结果:{ "effectiveUntil": "2025-10-30T18:00:00+08:00" }
|
||||||
|
|
||||||
|
|
||||||
|
ptcli -m awake stop
|
||||||
|
→ Awake.Stop()(幂等)
|
||||||
|
|
||||||
|
|
||||||
|
Workspaces
|
||||||
|
|
||||||
|
|
||||||
|
ptcli -m workspace list
|
||||||
|
→ Workspaces.List() 返回 { "items": [{ "id": "...", "name": "...", "monitors": 2, "windows": 14 }] }
|
||||||
|
|
||||||
|
|
||||||
|
ptcli -m workspace apply --id 123 --strict
|
||||||
|
→ Workspaces.Apply(id=123, strict=true) 支持进度与失败报告(缺失进程、权限不足等)。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Runner vs 直接敲模块事件/NamedPipe
|
||||||
|
直接敲模块(如你举的 EventWaitHandle)优点
|
||||||
|
|
||||||
|
|
||||||
|
省一跳,模块自己掌控。
|
||||||
|
|
||||||
|
|
||||||
|
对少数“拍一下就够”的快捷触发点,写起来快。
|
||||||
|
|
||||||
|
|
||||||
|
缺点(关键)
|
||||||
|
|
||||||
|
|
||||||
|
入口分散:每个模块各有各的触发名、参数约定、错误语义。
|
||||||
|
|
||||||
|
|
||||||
|
能力发现困难:ptcli 无法统一列出“模块能干啥、参数是什么”。
|
||||||
|
|
||||||
|
|
||||||
|
权限与多实例问题:有的模块需要管理员/前台,有的在用户会话,有的在服务;直接对模块打洞容易踩坑。
|
||||||
|
|
||||||
|
|
||||||
|
审计/可观察性差:难以统一日志/遥测/超时/取消。
|
||||||
|
|
||||||
|
|
||||||
|
演进成本高:接口一旦铺散,很难回收。
|
||||||
|
|
||||||
|
|
||||||
|
走 Runner Proxy(推荐)
|
||||||
|
|
||||||
|
|
||||||
|
统一注册:模块只跟 Runner 说“我能做哪些命令、参数是什么”。
|
||||||
|
|
||||||
|
|
||||||
|
统一协议:ptcli 只会说一种“通用 JSON 命令”。
|
||||||
|
|
||||||
|
|
||||||
|
统一安全/提权/会话:Runner 最懂自己所在的权限/桌面会话,可决定是否需要跳 Elevation/切用户会话。
|
||||||
|
|
||||||
|
|
||||||
|
兼容旧触发:Runner 内部去“Set 事件/写管道”,外部对 ptcli 完全透明。
|
||||||
|
|
||||||
|
|
||||||
|
可测试/可监控:所有调用都经由同一 Broker,便于打点、限流、诊断。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
结论:把直接事件/管道触发视为“模块侧 private API”,只由 Runner 调用。ptcli 与普通用户两边都只看得到 Runner 的“公共命令接口”。
|
||||||
|
|
||||||
|
|
||||||
|
Runner 怎么“轻量 Server”
|
||||||
|
|
||||||
|
|
||||||
|
进程:沿用现有 Runner,不另起新守护;新增一个 CommandRouter 子系统即可。
|
||||||
|
|
||||||
|
|
||||||
|
IPC:NamedPipeServerStream + StreamJsonRpc(或你们已有的 JSON 框架);单管道多请求(长度前缀 + correlationId)。
|
||||||
|
|
||||||
|
|
||||||
|
并发:每请求一个 Task,模块执行受自身并发控制。
|
||||||
|
|
||||||
|
|
||||||
|
安全:给管道设定 DACL,仅允许同一交互式用户(或受信 SID)连接;参数白名单与长度限制防注入。
|
||||||
|
|
||||||
|
|
||||||
|
错误码:统一枚举(像 HTTP 状态一样):
|
||||||
|
|
||||||
|
|
||||||
|
E_MODULE_NOT_FOUND / E_COMMAND_NOT_FOUND / E_ARGS_INVALID
|
||||||
|
|
||||||
|
|
||||||
|
E_NEEDS_ELEVATION / E_ACCESS_DENIED
|
||||||
|
|
||||||
|
|
||||||
|
E_BUSY_RETRY / E_TIMEOUT / E_INTERNAL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
最小可行落地(增量实施顺序)
|
||||||
|
|
||||||
|
|
||||||
|
在 Runner 加一个 Pipe + CommandRouter,硬编码两个演示命令:
|
||||||
|
|
||||||
|
|
||||||
|
Awake.Set(duration)(直接调用 Awake 的现有 API)
|
||||||
|
|
||||||
|
|
||||||
|
Workspaces.List()(调用 Workspace 管理器)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
写 ptcli:只做 JSON 打包、发管道、打印结果。
|
||||||
|
|
||||||
|
|
||||||
|
给两个模块各加 IModuleCommandProvider,从 Runner 注册。
|
||||||
|
|
||||||
|
|
||||||
|
把 1~2 个“历史事件触发点”接入 Router(Runner 内部去 Set Event),对外暴露为 Module.Action。
|
||||||
|
|
||||||
|
|
||||||
|
扩展:help/describe、Job/进度、取消、提权路径、返回码规范化。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
简短示例(C#,仅示意;注释英文)
|
||||||
|
Runner – 接口定义
|
||||||
|
public record CommandDescriptor(
|
||||||
|
string Module, string Action, string Description,
|
||||||
|
IReadOnlyDictionary<string, ParamSpec> Params,
|
||||||
|
bool RequiresElevation = false, bool LongRunning = false);
|
||||||
|
|
||||||
|
public interface IModuleCommandProvider
|
||||||
|
{
|
||||||
|
IEnumerable<CommandDescriptor> DescribeCommands();
|
||||||
|
Task<CommandResult> ExecuteAsync(CommandInvocation ctx, CancellationToken ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record CommandInvocation(string Action, IReadOnlyDictionary<string, object?> Args);
|
||||||
|
public record CommandResult(bool Ok, object? Data = null, string? ErrorCode = null, string? ErrorMessage = null);
|
||||||
|
|
||||||
|
Runner – 注册与路由(伪码)
|
||||||
|
// On module load:
|
||||||
|
registry.Register(provider.DescribeCommands(), provider);
|
||||||
|
|
||||||
|
// On request:
|
||||||
|
var cmd = request.Command; // module, action, args
|
||||||
|
var provider = registry.Resolve(cmd.Module, cmd.Action);
|
||||||
|
ValidateArgs(cmd.Args, provider.Schema);
|
||||||
|
if (provider.RequiresElevation && !IsElevated())
|
||||||
|
return Error("E_NEEDS_ELEVATION", "Elevation required.");
|
||||||
|
return await provider.ExecuteAsync(new CommandInvocation(cmd.Action, cmd.Args), ct);
|
||||||
|
|
||||||
|
ptcli – 调用(伪码)
|
||||||
|
// Build request from CLI args
|
||||||
|
var req = new { v = 1, correlationId = Guid.NewGuid(), command = new { module, action, args } };
|
||||||
|
using var client = new NamedPipeClientStream(".", "PowerToys.Runner.CLI", PipeDirection.InOut);
|
||||||
|
await client.ConnectAsync(timeout);
|
||||||
|
await WriteJsonAsync(client, req);
|
||||||
|
var resp = await ReadJsonAsync(client);
|
||||||
|
Render(resp);
|
||||||
|
return resp.status == "ok" ? 0 : 1;
|
||||||
|
|
||||||
|
|
||||||
|
兼容性与演进策略
|
||||||
|
|
||||||
|
|
||||||
|
不强行改造所有模块:Runner 先用“适配器”把旧事件/管道触发包起来,CLI 立即可用;后续模块逐步实现 IModuleCommandProvider。
|
||||||
|
|
||||||
|
|
||||||
|
版本化:请求/响应带 v,便于以后扩展字段。
|
||||||
|
|
||||||
|
|
||||||
|
可观测性:Runner 统一日志:调用者、命令、耗时、结果码;必要时加 ETW/Trace。
|
||||||
|
|
||||||
|
|
||||||
|
权限:Runner 决策提权路径;ptcli 接到 E_NEEDS_ELEVATION 时,可提示 --elevated 或弹系统 UAC(按你们策略)。
|
||||||
|
|
||||||
|
|
||||||
|
并行/队列:对长任务(如 workspace apply)可按模块内队列执行,避免竞态;CLI 侧提供 job status/cancel。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
你关心的具体问题直答
|
||||||
|
|
||||||
|
|
||||||
|
“由 moduleinterface 做统一入口管理是个好主意吗?”
|
||||||
|
好,但入口管理应驻留在 Runner。模块通过 ModuleInterface 报告命令与执行入口,Runner 统一对外暴露。
|
||||||
|
|
||||||
|
|
||||||
|
“ptcli 直接找模块执行,还是让 Runner 派发?”
|
||||||
|
让 Runner 派发。这样获得统一发现、权限、错误、日志与演进空间。
|
||||||
|
|
||||||
|
|
||||||
|
“现在很多模块用 named pipe/event 触发,这算绕过 Runner 吗?”
|
||||||
|
目前你看到的例子确实是“模块直连触发”。建议以后仅 Runner 使用这些内部触发(做兼容/过渡),对外统一成命令。
|
||||||
|
|
||||||
|
|
||||||
|
“Runner 起一个 light-weight server 吗?”
|
||||||
|
对。就在 Runner 里加一个轻量 NamedPipe JSON-RPC 服务,不必单独再起一个服务进程。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
要的话,我可以按上面的协议给你一版 ptcli(C#)与 Runner 侧 CommandRouter 的“最小可用代码骨架”,再加上 Awake/Workspaces 的两个真实命令适配示例。
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <common/utils/logger_helper.h>
|
#include <common/utils/logger_helper.h>
|
||||||
#include <common/utils/resources.h>
|
#include <common/utils/resources.h>
|
||||||
#include <common/utils/winapi_error.h>
|
#include <common/utils/winapi_error.h>
|
||||||
|
#include <common/utils/json.h>
|
||||||
|
|
||||||
#include <WorkspacesLib/trace.h>
|
#include <WorkspacesLib/trace.h>
|
||||||
#include <WorkspacesLib/WorkspacesData.h>
|
#include <WorkspacesLib/WorkspacesData.h>
|
||||||
@@ -17,6 +18,9 @@
|
|||||||
|
|
||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
#include <common/utils/EventWaiter.h>
|
#include <common/utils/EventWaiter.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cwctype>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
// Non-localizable
|
// Non-localizable
|
||||||
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
|
const std::wstring workspacesLauncherPath = L"PowerToys.WorkspacesLauncher.exe";
|
||||||
@@ -69,6 +73,8 @@ public:
|
|||||||
return app_key.c_str();
|
return app_key.c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pt::cli::IModuleCommandProvider* command_provider() override;
|
||||||
|
|
||||||
virtual std::optional<HotkeyEx> GetHotkeyEx() override
|
virtual std::optional<HotkeyEx> GetHotkeyEx() override
|
||||||
{
|
{
|
||||||
return m_hotkey;
|
return m_hotkey;
|
||||||
@@ -359,9 +365,85 @@ private:
|
|||||||
.modifiersMask = MOD_CONTROL | MOD_WIN,
|
.modifiersMask = MOD_CONTROL | MOD_WIN,
|
||||||
.vkCode = 0xC0, // VK_OEM_3 key; usually `~
|
.vkCode = 0xC0, // VK_OEM_3 key; usually `~
|
||||||
};
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<pt::cli::IModuleCommandProvider> m_cliProvider;
|
||||||
|
|
||||||
|
pt::cli::CommandResult HandleList(const json::JsonObject& args) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class WorkspacesCommandProvider final : public pt::cli::IModuleCommandProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit WorkspacesCommandProvider(const WorkspacesModuleInterface& owner) :
|
||||||
|
m_owner(owner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ModuleKey() const override
|
||||||
|
{
|
||||||
|
return L"workspaces";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<pt::cli::CommandDescriptor> DescribeCommands() const override
|
||||||
|
{
|
||||||
|
pt::cli::CommandDescriptor listDescriptor;
|
||||||
|
listDescriptor.action = L"list";
|
||||||
|
listDescriptor.description = L"List configured workspaces.";
|
||||||
|
return { std::move(listDescriptor) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult Execute(const pt::cli::CommandInvocation& invocation) override
|
||||||
|
{
|
||||||
|
std::wstring action = invocation.action;
|
||||||
|
std::transform(action.begin(), action.end(), action.begin(), [](wchar_t ch) {
|
||||||
|
return static_cast<wchar_t>(std::towlower(ch));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (action == L"list")
|
||||||
|
{
|
||||||
|
return m_owner.HandleList(invocation.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported Workspaces command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const WorkspacesModuleInterface& m_owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
pt::cli::IModuleCommandProvider* WorkspacesModuleInterface::command_provider()
|
||||||
|
{
|
||||||
|
if (!m_cliProvider)
|
||||||
|
{
|
||||||
|
m_cliProvider = std::make_unique<WorkspacesCommandProvider>(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_cliProvider.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult WorkspacesModuleInterface::HandleList(const json::JsonObject& args) const
|
||||||
|
{
|
||||||
|
UNREFERENCED_PARAMETER(args);
|
||||||
|
|
||||||
|
json::JsonObject payload = json::JsonObject();
|
||||||
|
auto workspacesPath = WorkspacesData::WorkspacesFile();
|
||||||
|
payload.SetNamedValue(L"path", json::value(workspacesPath));
|
||||||
|
|
||||||
|
auto stored = json::from_file(workspacesPath);
|
||||||
|
if (stored.has_value())
|
||||||
|
{
|
||||||
|
payload.SetNamedValue(L"data", json::value(*stored));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
payload.SetNamedValue(L"data", json::value(json::JsonObject()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt::cli::CommandResult::Success(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||||
{
|
{
|
||||||
return new WorkspacesModuleInterface();
|
return new WorkspacesModuleInterface();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,13 @@
|
|||||||
#include <common/utils/resources.h>
|
#include <common/utils/resources.h>
|
||||||
#include <common/utils/os-detect.h>
|
#include <common/utils/os-detect.h>
|
||||||
#include <common/utils/winapi_error.h>
|
#include <common/utils/winapi_error.h>
|
||||||
|
#include <common/utils/json.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <cwctype>
|
||||||
|
#include <optional>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
@@ -37,6 +43,105 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp
|
|||||||
const static wchar_t* MODULE_NAME = L"Awake";
|
const static wchar_t* MODULE_NAME = L"Awake";
|
||||||
const static wchar_t* MODULE_DESC = L"A module that keeps your computer awake on-demand.";
|
const static wchar_t* MODULE_DESC = L"A module that keeps your computer awake on-demand.";
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::wstring to_lower_copy(std::wstring value)
|
||||||
|
{
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(), [](wchar_t ch) {
|
||||||
|
return static_cast<wchar_t>(std::towlower(ch));
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring mode_to_string(int mode)
|
||||||
|
{
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
return L"passive";
|
||||||
|
case 1:
|
||||||
|
return L"indefinite";
|
||||||
|
case 2:
|
||||||
|
return L"timed";
|
||||||
|
case 3:
|
||||||
|
return L"expirable";
|
||||||
|
default:
|
||||||
|
return L"unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint32_t> parse_duration_string(const std::wstring& raw)
|
||||||
|
{
|
||||||
|
if (raw.empty())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring value = raw;
|
||||||
|
double multiplier = 1.0;
|
||||||
|
|
||||||
|
wchar_t suffix = value.back();
|
||||||
|
if (!iswdigit(suffix))
|
||||||
|
{
|
||||||
|
value.pop_back();
|
||||||
|
if (suffix == L'h' || suffix == L'H')
|
||||||
|
{
|
||||||
|
multiplier = 60.0;
|
||||||
|
}
|
||||||
|
else if (suffix == L'm' || suffix == L'M')
|
||||||
|
{
|
||||||
|
multiplier = 1.0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
double numeric = std::stod(value);
|
||||||
|
if (numeric < 0)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
double totalMinutes = numeric * multiplier;
|
||||||
|
if (totalMinutes < 0 || totalMinutes > static_cast<double>(std::numeric_limits<uint32_t>::max()))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<uint32_t>(totalMinutes);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<uint32_t> extract_duration_minutes(const json::JsonObject& args)
|
||||||
|
{
|
||||||
|
if (args.HasKey(L"durationMinutes"))
|
||||||
|
{
|
||||||
|
auto value = args.GetNamedNumber(L"durationMinutes");
|
||||||
|
if (value < 0)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return static_cast<uint32_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.HasKey(L"duration"))
|
||||||
|
{
|
||||||
|
auto asString = args.GetNamedString(L"duration");
|
||||||
|
return parse_duration_string(asString.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Awake : public PowertoyModuleIface
|
class Awake : public PowertoyModuleIface
|
||||||
{
|
{
|
||||||
std::wstring app_name;
|
std::wstring app_name;
|
||||||
@@ -45,6 +150,7 @@ class Awake : public PowertoyModuleIface
|
|||||||
private:
|
private:
|
||||||
bool m_enabled = false;
|
bool m_enabled = false;
|
||||||
PROCESS_INFORMATION p_info = {};
|
PROCESS_INFORMATION p_info = {};
|
||||||
|
std::unique_ptr<pt::cli::IModuleCommandProvider> m_cliProvider;
|
||||||
|
|
||||||
bool is_process_running()
|
bool is_process_running()
|
||||||
{
|
{
|
||||||
@@ -176,9 +282,167 @@ public:
|
|||||||
{
|
{
|
||||||
return m_enabled;
|
return m_enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pt::cli::IModuleCommandProvider* command_provider() override;
|
||||||
|
|
||||||
|
pt::cli::CommandResult HandleStatus() const;
|
||||||
|
pt::cli::CommandResult HandleSet(const json::JsonObject& args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class AwakeCommandProvider final : public pt::cli::IModuleCommandProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit AwakeCommandProvider(Awake& owner) :
|
||||||
|
m_owner(owner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring ModuleKey() const override
|
||||||
|
{
|
||||||
|
return L"awake";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<pt::cli::CommandDescriptor> DescribeCommands() const override
|
||||||
|
{
|
||||||
|
std::vector<pt::cli::CommandParameter> setParameters{
|
||||||
|
{ L"mode", false, L"Awake mode: passive | indefinite | timed." },
|
||||||
|
{ L"durationMinutes", false, L"Total duration in minutes for timed mode." },
|
||||||
|
{ L"duration", false, L"Duration with unit (e.g. 30m, 2h) for timed mode." },
|
||||||
|
{ L"displayOn", false, L"Whether to keep the display active (true/false)." },
|
||||||
|
};
|
||||||
|
|
||||||
|
pt::cli::CommandDescriptor setDescriptor;
|
||||||
|
setDescriptor.action = L"set";
|
||||||
|
setDescriptor.description = L"Configure the Awake module.";
|
||||||
|
setDescriptor.parameters = std::move(setParameters);
|
||||||
|
|
||||||
|
pt::cli::CommandDescriptor statusDescriptor;
|
||||||
|
statusDescriptor.action = L"status";
|
||||||
|
statusDescriptor.description = L"Inspect the current Awake mode.";
|
||||||
|
|
||||||
|
return { std::move(setDescriptor), std::move(statusDescriptor) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult Execute(const pt::cli::CommandInvocation& invocation) override
|
||||||
|
{
|
||||||
|
auto action = to_lower_copy(invocation.action);
|
||||||
|
if (action == L"set")
|
||||||
|
{
|
||||||
|
return m_owner.HandleSet(invocation.args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == L"status")
|
||||||
|
{
|
||||||
|
return m_owner.HandleStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported Awake action.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Awake& m_owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
pt::cli::IModuleCommandProvider* Awake::command_provider()
|
||||||
|
{
|
||||||
|
if (!m_cliProvider)
|
||||||
|
{
|
||||||
|
m_cliProvider = std::make_unique<AwakeCommandProvider>(*this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_cliProvider.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult Awake::HandleStatus() const
|
||||||
|
{
|
||||||
|
auto settings = PTSettingsHelper::load_module_settings(app_key);
|
||||||
|
json::JsonObject payload = json::JsonObject();
|
||||||
|
|
||||||
|
if (!settings.HasKey(L"properties"))
|
||||||
|
{
|
||||||
|
payload.SetNamedValue(L"mode", json::value(L"unknown"));
|
||||||
|
payload.SetNamedValue(L"keepDisplayOn", json::value(false));
|
||||||
|
return pt::cli::CommandResult::Success(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto properties = settings.GetNamedObject(L"properties");
|
||||||
|
|
||||||
|
const auto modeValue = static_cast<int>(properties.GetNamedNumber(L"mode", 0));
|
||||||
|
payload.SetNamedValue(L"mode", json::value(mode_to_string(modeValue)));
|
||||||
|
payload.SetNamedValue(L"modeValue", json::value(modeValue));
|
||||||
|
payload.SetNamedValue(L"keepDisplayOn", json::value(properties.GetNamedBoolean(L"keepDisplayOn", false)));
|
||||||
|
payload.SetNamedValue(L"intervalHours", json::value(static_cast<uint32_t>(properties.GetNamedNumber(L"intervalHours", 0))));
|
||||||
|
payload.SetNamedValue(L"intervalMinutes", json::value(static_cast<uint32_t>(properties.GetNamedNumber(L"intervalMinutes", 0))));
|
||||||
|
|
||||||
|
if (properties.HasKey(L"expirationDateTime"))
|
||||||
|
{
|
||||||
|
payload.SetNamedValue(L"expirationDateTime", json::value(properties.GetNamedString(L"expirationDateTime")));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt::cli::CommandResult::Success(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult Awake::HandleSet(const json::JsonObject& args)
|
||||||
|
{
|
||||||
|
std::wstring requestedMode = L"indefinite";
|
||||||
|
if (args.HasKey(L"mode"))
|
||||||
|
{
|
||||||
|
requestedMode = to_lower_copy(std::wstring(args.GetNamedString(L"mode").c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto settings = PTSettingsHelper::load_module_settings(app_key);
|
||||||
|
json::JsonObject properties = settings.HasKey(L"properties") ? settings.GetNamedObject(L"properties") : json::JsonObject();
|
||||||
|
|
||||||
|
const bool keepDisplayOn = args.GetNamedBoolean(L"displayOn", properties.GetNamedBoolean(L"keepDisplayOn", false));
|
||||||
|
|
||||||
|
int modeValue = 1; // default to indefinite
|
||||||
|
if (requestedMode == L"passive")
|
||||||
|
{
|
||||||
|
modeValue = 0;
|
||||||
|
}
|
||||||
|
else if (requestedMode == L"indefinite" || requestedMode.empty())
|
||||||
|
{
|
||||||
|
modeValue = 1;
|
||||||
|
}
|
||||||
|
else if (requestedMode == L"timed")
|
||||||
|
{
|
||||||
|
modeValue = 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"Unsupported mode. Use passive, indefinite, or timed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.SetNamedValue(L"keepDisplayOn", json::value(keepDisplayOn));
|
||||||
|
properties.SetNamedValue(L"mode", json::value(modeValue));
|
||||||
|
|
||||||
|
if (modeValue == 2)
|
||||||
|
{
|
||||||
|
auto durationMinutes = extract_duration_minutes(args);
|
||||||
|
if (!durationMinutes.has_value() || durationMinutes.value() == 0)
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"Timed mode requires a non-zero duration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t totalMinutes = durationMinutes.value();
|
||||||
|
const uint32_t hours = totalMinutes / 60;
|
||||||
|
const uint32_t minutes = totalMinutes % 60;
|
||||||
|
properties.SetNamedValue(L"intervalHours", json::value(hours));
|
||||||
|
properties.SetNamedValue(L"intervalMinutes", json::value(minutes));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
properties.SetNamedValue(L"intervalHours", json::value(0));
|
||||||
|
properties.SetNamedValue(L"intervalMinutes", json::value(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.SetNamedValue(L"properties", json::value(properties));
|
||||||
|
PTSettingsHelper::save_module_settings(app_key, settings);
|
||||||
|
|
||||||
|
return HandleStatus();
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||||
{
|
{
|
||||||
return new Awake();
|
return new Awake();
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/modules/interface/powertoy_cli.h
Normal file
68
src/modules/interface/powertoy_cli.h
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <utility>
|
||||||
|
#include <common/utils/json.h>
|
||||||
|
|
||||||
|
namespace pt::cli
|
||||||
|
{
|
||||||
|
struct CommandParameter
|
||||||
|
{
|
||||||
|
std::wstring name;
|
||||||
|
bool required = false;
|
||||||
|
std::wstring description;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CommandDescriptor
|
||||||
|
{
|
||||||
|
std::wstring action;
|
||||||
|
std::wstring description;
|
||||||
|
std::vector<CommandParameter> parameters;
|
||||||
|
bool requiresElevation = false;
|
||||||
|
bool longRunning = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CommandInvocation
|
||||||
|
{
|
||||||
|
std::wstring action;
|
||||||
|
json::JsonObject args;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CommandResult
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
json::JsonObject data;
|
||||||
|
std::wstring errorCode;
|
||||||
|
std::wstring errorMessage;
|
||||||
|
|
||||||
|
static CommandResult Success(json::JsonObject data = {});
|
||||||
|
static CommandResult Error(std::wstring code, std::wstring message);
|
||||||
|
};
|
||||||
|
|
||||||
|
inline CommandResult CommandResult::Success(json::JsonObject data)
|
||||||
|
{
|
||||||
|
CommandResult result;
|
||||||
|
result.ok = true;
|
||||||
|
result.data = std::move(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline CommandResult CommandResult::Error(std::wstring code, std::wstring message)
|
||||||
|
{
|
||||||
|
CommandResult result;
|
||||||
|
result.ok = false;
|
||||||
|
result.errorCode = std::move(code);
|
||||||
|
result.errorMessage = std::move(message);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class IModuleCommandProvider
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IModuleCommandProvider() = default;
|
||||||
|
virtual std::wstring ModuleKey() const = 0;
|
||||||
|
virtual std::vector<CommandDescriptor> DescribeCommands() const = 0;
|
||||||
|
virtual CommandResult Execute(const CommandInvocation& invocation) = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <compare>
|
#include <compare>
|
||||||
#include <common/utils/gpo.h>
|
#include <common/utils/gpo.h>
|
||||||
|
#include "powertoy_cli.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
DLL Interface for PowerToys. The powertoy_create() (see below) must return
|
DLL Interface for PowerToys. The powertoy_create() (see below) must return
|
||||||
@@ -140,20 +141,25 @@ public:
|
|||||||
* milliseconds_win_key_must_be_pressed returns the number of milliseconds the win key should be pressed before triggering the module.
|
* milliseconds_win_key_must_be_pressed returns the number of milliseconds the win key should be pressed before triggering the module.
|
||||||
* Don't use these for new modules.
|
* Don't use these for new modules.
|
||||||
*/
|
*/
|
||||||
virtual bool keep_track_of_pressed_win_key() { return false; }
|
virtual bool keep_track_of_pressed_win_key() { return false; }
|
||||||
virtual UINT milliseconds_win_key_must_be_pressed() { return 0; }
|
virtual UINT milliseconds_win_key_must_be_pressed() { return 0; }
|
||||||
|
|
||||||
virtual void send_settings_telemetry()
|
virtual void send_settings_telemetry()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool is_enabled_by_default() const { return true; }
|
virtual bool is_enabled_by_default() const { return true; }
|
||||||
|
|
||||||
/* Provides the GPO configuration value for the module. This should be overridden by the module interface to get the proper gpo policy setting. */
|
/* Provides the GPO configuration value for the module. This should be overridden by the module interface to get the proper gpo policy setting. */
|
||||||
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration()
|
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration()
|
||||||
{
|
{
|
||||||
return powertoys_gpo::gpo_rule_configured_not_configured;
|
return powertoys_gpo::gpo_rule_configured_not_configured;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual pt::cli::IModuleCommandProvider* command_provider()
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Some actions like AdvancedPaste generate new inputs, which we don't want to catch again.
|
// Some actions like AdvancedPaste generate new inputs, which we don't want to catch again.
|
||||||
// The flag was purposefully chose to not collide with other keyboard manager flags.
|
// The flag was purposefully chose to not collide with other keyboard manager flags.
|
||||||
|
|||||||
342
src/runner/cli_server.cpp
Normal file
342
src/runner/cli_server.cpp
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
#include "cli_server.h"
|
||||||
|
|
||||||
|
#include "command_registry.h"
|
||||||
|
|
||||||
|
#include <common/logger/logger.h>
|
||||||
|
#include <common/utils/json.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr wchar_t PIPE_NAME[] = LR"(\\.\pipe\PowerToys.Runner.CLI)";
|
||||||
|
constexpr DWORD PIPE_BUFFER_SIZE = 64 * 1024;
|
||||||
|
|
||||||
|
std::once_flag startFlag;
|
||||||
|
std::atomic_bool running = false;
|
||||||
|
|
||||||
|
std::wstring utf8_to_wstring(const std::string& input)
|
||||||
|
{
|
||||||
|
if (input.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int wideSize = MultiByteToWideChar(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), nullptr, 0);
|
||||||
|
if (wideSize <= 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring result(static_cast<size_t>(wideSize), L'\0');
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), result.data(), wideSize);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string wstring_to_utf8(const std::wstring& input)
|
||||||
|
{
|
||||||
|
if (input.empty())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int narrowSize = WideCharToMultiByte(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), nullptr, 0, nullptr, nullptr);
|
||||||
|
if (narrowSize <= 0)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result(static_cast<size_t>(narrowSize), '\0');
|
||||||
|
WideCharToMultiByte(CP_UTF8, 0, input.data(), static_cast<int>(input.size()), result.data(), narrowSize, nullptr, nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_message(HANDLE pipe, std::string& out)
|
||||||
|
{
|
||||||
|
char buffer[4096];
|
||||||
|
DWORD bytesRead = 0;
|
||||||
|
bool continueReading = true;
|
||||||
|
|
||||||
|
while (continueReading)
|
||||||
|
{
|
||||||
|
BOOL success = ReadFile(pipe, buffer, sizeof(buffer), &bytesRead, nullptr);
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == ERROR_MORE_DATA)
|
||||||
|
{
|
||||||
|
out.append(buffer, buffer + bytesRead);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error != ERROR_BROKEN_PIPE && error != ERROR_PIPE_NOT_CONNECTED)
|
||||||
|
{
|
||||||
|
Logger::warn(L"CLI pipe read failed with error {}", error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesRead > 0)
|
||||||
|
{
|
||||||
|
out.append(buffer, buffer + bytesRead);
|
||||||
|
}
|
||||||
|
continueReading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
json::JsonArray parameters_to_json(const std::vector<pt::cli::CommandParameter>& parameters)
|
||||||
|
{
|
||||||
|
json::JsonArray array;
|
||||||
|
for (const auto& parameter : parameters)
|
||||||
|
{
|
||||||
|
json::JsonObject node;
|
||||||
|
node.SetNamedValue(L"name", json::value(parameter.name));
|
||||||
|
node.SetNamedValue(L"required", json::value(parameter.required));
|
||||||
|
node.SetNamedValue(L"description", json::value(parameter.description));
|
||||||
|
array.Append(json::value(std::move(node)));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult handle_system_command(const std::wstring& action, const json::JsonObject& args)
|
||||||
|
{
|
||||||
|
if (action == L"list-modules")
|
||||||
|
{
|
||||||
|
auto snapshot = CommandRegistry::instance().snapshot();
|
||||||
|
json::JsonArray modules;
|
||||||
|
for (auto& moduleInfo : snapshot)
|
||||||
|
{
|
||||||
|
json::JsonObject moduleJson;
|
||||||
|
moduleJson.SetNamedValue(L"module", json::value(moduleInfo.moduleKey));
|
||||||
|
|
||||||
|
json::JsonArray commands;
|
||||||
|
for (const auto& descriptor : moduleInfo.commands)
|
||||||
|
{
|
||||||
|
json::JsonObject cmdJson;
|
||||||
|
cmdJson.SetNamedValue(L"action", json::value(descriptor.action));
|
||||||
|
cmdJson.SetNamedValue(L"description", json::value(descriptor.description));
|
||||||
|
cmdJson.SetNamedValue(L"requiresElevation", json::value(descriptor.requiresElevation));
|
||||||
|
cmdJson.SetNamedValue(L"longRunning", json::value(descriptor.longRunning));
|
||||||
|
cmdJson.SetNamedValue(L"parameters", json::value(parameters_to_json(descriptor.parameters)));
|
||||||
|
commands.Append(json::value(std::move(cmdJson)));
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleJson.SetNamedValue(L"commands", json::value(std::move(commands)));
|
||||||
|
modules.Append(json::value(std::move(moduleJson)));
|
||||||
|
}
|
||||||
|
|
||||||
|
json::JsonObject payload;
|
||||||
|
payload.SetNamedValue(L"modules", json::value(std::move(modules)));
|
||||||
|
return pt::cli::CommandResult::Success(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == L"list-commands")
|
||||||
|
{
|
||||||
|
if (!args.HasKey(L"module"))
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_ARGS_INVALID", L"'module' argument is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto moduleName = std::wstring(args.GetNamedString(L"module").c_str());
|
||||||
|
auto reflection = CommandRegistry::instance().snapshot(moduleName);
|
||||||
|
if (!reflection.has_value())
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_MODULE_NOT_FOUND", L"Module not registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
json::JsonArray commands;
|
||||||
|
for (const auto& descriptor : reflection->commands)
|
||||||
|
{
|
||||||
|
json::JsonObject cmdJson;
|
||||||
|
cmdJson.SetNamedValue(L"action", json::value(descriptor.action));
|
||||||
|
cmdJson.SetNamedValue(L"description", json::value(descriptor.description));
|
||||||
|
cmdJson.SetNamedValue(L"requiresElevation", json::value(descriptor.requiresElevation));
|
||||||
|
cmdJson.SetNamedValue(L"longRunning", json::value(descriptor.longRunning));
|
||||||
|
cmdJson.SetNamedValue(L"parameters", json::value(parameters_to_json(descriptor.parameters)));
|
||||||
|
commands.Append(json::value(std::move(cmdJson)));
|
||||||
|
}
|
||||||
|
|
||||||
|
json::JsonObject payload;
|
||||||
|
payload.SetNamedValue(L"module", json::value(reflection->moduleKey));
|
||||||
|
payload.SetNamedValue(L"commands", json::value(std::move(commands)));
|
||||||
|
return pt::cli::CommandResult::Success(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == L"ping")
|
||||||
|
{
|
||||||
|
json::JsonObject payload;
|
||||||
|
payload.SetNamedValue(L"status", json::value(L"ok"));
|
||||||
|
return pt::cli::CommandResult::Success(std::move(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Unsupported system command.");
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult dispatch_command(const std::wstring& module, const std::wstring& action, const json::JsonObject& args)
|
||||||
|
{
|
||||||
|
if (module == L"$system")
|
||||||
|
{
|
||||||
|
return handle_system_command(action, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandInvocation invocation{ action, args };
|
||||||
|
return CommandRegistry::instance().execute(module, invocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
json::JsonObject build_error_payload(const std::wstring& code, const std::wstring& message)
|
||||||
|
{
|
||||||
|
json::JsonObject error;
|
||||||
|
error.SetNamedValue(L"code", json::value(code));
|
||||||
|
error.SetNamedValue(L"message", json::value(message));
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_response(HANDLE pipe, const json::JsonObject& response)
|
||||||
|
{
|
||||||
|
auto serialized = response.Stringify();
|
||||||
|
auto utf8 = wstring_to_utf8(serialized.c_str());
|
||||||
|
DWORD bytesWritten = 0;
|
||||||
|
WriteFile(pipe, utf8.data(), static_cast<DWORD>(utf8.size()), &bytesWritten, nullptr);
|
||||||
|
FlushFileBuffers(pipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_session(HANDLE pipe)
|
||||||
|
{
|
||||||
|
std::string rawRequest;
|
||||||
|
if (!read_message(pipe, rawRequest))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
json::JsonObject response;
|
||||||
|
response.SetNamedValue(L"v", json::value(1));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto requestText = utf8_to_wstring(rawRequest);
|
||||||
|
if (requestText.empty())
|
||||||
|
{
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INVALID", L"Empty request.")));
|
||||||
|
write_response(pipe, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto jsonValue = winrt::Windows::Data::Json::JsonValue::Parse(requestText);
|
||||||
|
auto root = jsonValue.GetObjectW();
|
||||||
|
|
||||||
|
auto correlationId = root.GetNamedString(L"correlationId", L"");
|
||||||
|
response.SetNamedValue(L"correlationId", json::value(correlationId));
|
||||||
|
|
||||||
|
if (!root.HasKey(L"command"))
|
||||||
|
{
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INVALID", L"Missing command payload.")));
|
||||||
|
write_response(pipe, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto command = root.GetNamedObject(L"command");
|
||||||
|
const std::wstring module = std::wstring(command.GetNamedString(L"module", L"").c_str());
|
||||||
|
const std::wstring action = std::wstring(command.GetNamedString(L"action", L"").c_str());
|
||||||
|
|
||||||
|
json::JsonObject args = json::JsonObject();
|
||||||
|
if (command.HasKey(L"args"))
|
||||||
|
{
|
||||||
|
args = command.GetNamedObject(L"args");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.empty() || action.empty())
|
||||||
|
{
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_ARGS_INVALID", L"'module' and 'action' must be provided.")));
|
||||||
|
write_response(pipe, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = dispatch_command(module, action, args);
|
||||||
|
if (result.ok)
|
||||||
|
{
|
||||||
|
response.SetNamedValue(L"status", json::value(L"ok"));
|
||||||
|
response.SetNamedValue(L"result", json::value(result.data));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
auto errorCode = result.errorCode.empty() ? L"E_INTERNAL" : result.errorCode;
|
||||||
|
auto errorMsg = result.errorMessage.empty() ? L"Command failed." : result.errorMessage;
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(errorCode, errorMsg)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const winrt::hresult_error&)
|
||||||
|
{
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INVALID_JSON", L"Request payload was not valid JSON.")));
|
||||||
|
}
|
||||||
|
catch (const std::exception& ex)
|
||||||
|
{
|
||||||
|
Logger::error(L"CLI request processing threw: {}", winrt::to_hstring(ex.what()));
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INTERNAL", L"Internal processing failure.")));
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
Logger::error(L"CLI request processing failed with unknown exception.");
|
||||||
|
response.SetNamedValue(L"status", json::value(L"error"));
|
||||||
|
response.SetNamedValue(L"error", json::value(build_error_payload(L"E_INTERNAL", L"Unknown processing failure.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
write_response(pipe, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void server_loop()
|
||||||
|
{
|
||||||
|
while (running.load())
|
||||||
|
{
|
||||||
|
HANDLE pipe = CreateNamedPipeW(
|
||||||
|
PIPE_NAME,
|
||||||
|
PIPE_ACCESS_DUPLEX,
|
||||||
|
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
|
||||||
|
PIPE_UNLIMITED_INSTANCES,
|
||||||
|
PIPE_BUFFER_SIZE,
|
||||||
|
PIPE_BUFFER_SIZE,
|
||||||
|
0,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
if (pipe == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
Logger::error(L"Failed to create CLI named pipe (error {}).", error);
|
||||||
|
Sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL connected = ConnectNamedPipe(pipe, nullptr)
|
||||||
|
? TRUE
|
||||||
|
: (GetLastError() == ERROR_PIPE_CONNECTED);
|
||||||
|
|
||||||
|
if (connected)
|
||||||
|
{
|
||||||
|
handle_session(pipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushFileBuffers(pipe);
|
||||||
|
DisconnectNamedPipe(pipe);
|
||||||
|
CloseHandle(pipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_cli_server()
|
||||||
|
{
|
||||||
|
std::call_once(startFlag, [] {
|
||||||
|
running = true;
|
||||||
|
std::thread(server_loop).detach();
|
||||||
|
});
|
||||||
|
}
|
||||||
3
src/runner/cli_server.h
Normal file
3
src/runner/cli_server.h
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void start_cli_server();
|
||||||
144
src/runner/command_registry.cpp
Normal file
144
src/runner/command_registry.cpp
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
#include "pch.h"
|
||||||
|
#include "command_registry.h"
|
||||||
|
|
||||||
|
#include <common/logger/logger.h>
|
||||||
|
#include <common/utils/elevation.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cwctype>
|
||||||
|
|
||||||
|
CommandRegistry& CommandRegistry::instance()
|
||||||
|
{
|
||||||
|
static CommandRegistry registry;
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandRegistry::register_module(PowertoyModuleIface* module)
|
||||||
|
{
|
||||||
|
if (!module)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto provider = module->command_provider();
|
||||||
|
if (!provider)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring moduleKey = provider->ModuleKey();
|
||||||
|
if (moduleKey.empty())
|
||||||
|
{
|
||||||
|
moduleKey = module->get_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto descriptors = provider->DescribeCommands();
|
||||||
|
if (descriptors.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Entry entry{};
|
||||||
|
entry.provider = provider;
|
||||||
|
entry.moduleKey = moduleKey;
|
||||||
|
|
||||||
|
for (const auto& descriptor : descriptors)
|
||||||
|
{
|
||||||
|
auto normalizedAction = normalize_key(descriptor.action);
|
||||||
|
entry.descriptorsByAction.emplace(normalizedAction, descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_lock guard{ mutex_ };
|
||||||
|
entries_[normalize_key(moduleKey)] = std::move(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
pt::cli::CommandResult CommandRegistry::execute(const std::wstring& moduleKey, const pt::cli::CommandInvocation& invocation)
|
||||||
|
{
|
||||||
|
std::shared_lock guard{ mutex_ };
|
||||||
|
|
||||||
|
auto moduleIt = entries_.find(normalize_key(moduleKey));
|
||||||
|
if (moduleIt == entries_.end())
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_MODULE_NOT_FOUND", L"Module not registered for CLI use.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& entry = moduleIt->second;
|
||||||
|
|
||||||
|
auto descriptorIt = entry.descriptorsByAction.find(normalize_key(invocation.action));
|
||||||
|
if (descriptorIt == entry.descriptorsByAction.end())
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_COMMAND_NOT_FOUND", L"Command not available for this module.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& descriptor = descriptorIt->second;
|
||||||
|
if (descriptor.requiresElevation && !is_process_elevated())
|
||||||
|
{
|
||||||
|
return pt::cli::CommandResult::Error(L"E_NEEDS_ELEVATION", L"This command requires elevation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto provider = entry.provider;
|
||||||
|
guard.unlock();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return provider->Execute(invocation);
|
||||||
|
}
|
||||||
|
catch (const std::exception& ex)
|
||||||
|
{
|
||||||
|
Logger::error(L"CLI command execution failed: {}", winrt::to_hstring(ex.what()));
|
||||||
|
return pt::cli::CommandResult::Error(L"E_INTERNAL", L"Command execution failed due to an internal error.");
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
Logger::error(L"CLI command execution failed due to an unknown exception.");
|
||||||
|
return pt::cli::CommandResult::Error(L"E_INTERNAL", L"Command execution failed due to an unknown error.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CommandModuleReflection> CommandRegistry::snapshot() const
|
||||||
|
{
|
||||||
|
std::shared_lock guard{ mutex_ };
|
||||||
|
std::vector<CommandModuleReflection> result;
|
||||||
|
result.reserve(entries_.size());
|
||||||
|
|
||||||
|
for (const auto& [normalizedKey, entry] : entries_)
|
||||||
|
{
|
||||||
|
CommandModuleReflection reflection;
|
||||||
|
reflection.moduleKey = entry.moduleKey;
|
||||||
|
for (const auto& [actionKey, descriptor] : entry.descriptorsByAction)
|
||||||
|
{
|
||||||
|
reflection.commands.push_back(descriptor);
|
||||||
|
}
|
||||||
|
result.push_back(std::move(reflection));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<CommandModuleReflection> CommandRegistry::snapshot(const std::wstring& moduleKey) const
|
||||||
|
{
|
||||||
|
std::shared_lock guard{ mutex_ };
|
||||||
|
auto it = entries_.find(normalize_key(moduleKey));
|
||||||
|
if (it == entries_.end())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandModuleReflection reflection;
|
||||||
|
reflection.moduleKey = it->second.moduleKey;
|
||||||
|
for (const auto& [actionKey, descriptor] : it->second.descriptorsByAction)
|
||||||
|
{
|
||||||
|
reflection.commands.push_back(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflection;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring CommandRegistry::normalize_key(const std::wstring& value)
|
||||||
|
{
|
||||||
|
std::wstring normalized = value;
|
||||||
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](wchar_t ch) {
|
||||||
|
return static_cast<wchar_t>(std::towlower(ch));
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
42
src/runner/command_registry.h
Normal file
42
src/runner/command_registry.h
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <modules/interface/powertoy_module_interface.h>
|
||||||
|
#include <modules/interface/powertoy_cli.h>
|
||||||
|
#include <common/utils/json.h>
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <vector>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
struct CommandModuleReflection
|
||||||
|
{
|
||||||
|
std::wstring moduleKey;
|
||||||
|
std::vector<pt::cli::CommandDescriptor> commands;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CommandRegistry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static CommandRegistry& instance();
|
||||||
|
|
||||||
|
void register_module(PowertoyModuleIface* module);
|
||||||
|
|
||||||
|
pt::cli::CommandResult execute(const std::wstring& moduleKey, const pt::cli::CommandInvocation& invocation);
|
||||||
|
|
||||||
|
std::vector<CommandModuleReflection> snapshot() const;
|
||||||
|
std::optional<CommandModuleReflection> snapshot(const std::wstring& moduleKey) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry
|
||||||
|
{
|
||||||
|
pt::cli::IModuleCommandProvider* provider = nullptr;
|
||||||
|
std::wstring moduleKey;
|
||||||
|
std::unordered_map<std::wstring, pt::cli::CommandDescriptor> descriptorsByAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::wstring normalize_key(const std::wstring& value);
|
||||||
|
|
||||||
|
mutable std::shared_mutex mutex_;
|
||||||
|
std::unordered_map<std::wstring, Entry> entries_;
|
||||||
|
};
|
||||||
@@ -28,7 +28,9 @@
|
|||||||
#include <common/utils/clean_video_conference.h>
|
#include <common/utils/clean_video_conference.h>
|
||||||
|
|
||||||
#include "UpdateUtils.h"
|
#include "UpdateUtils.h"
|
||||||
#include "ActionRunnerUtils.h"
|
#include "ActionRunnerUtils.h"
|
||||||
|
#include "command_registry.h"
|
||||||
|
#include "cli_server.h"
|
||||||
|
|
||||||
#include <winrt/Windows.System.h>
|
#include <winrt/Windows.System.h>
|
||||||
|
|
||||||
@@ -180,17 +182,22 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
|||||||
L"PowerToys.LightSwitchModuleInterface.dll",
|
L"PowerToys.LightSwitchModuleInterface.dll",
|
||||||
};
|
};
|
||||||
|
|
||||||
for (auto moduleSubdir : knownModules)
|
for (auto moduleSubdir : knownModules)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
auto pt_module = load_powertoy(moduleSubdir);
|
auto ptModule = load_powertoy(moduleSubdir);
|
||||||
modules().emplace(pt_module->get_key(), std::move(pt_module));
|
std::wstring moduleKey{ ptModule->get_key() };
|
||||||
}
|
auto [it, inserted] = modules().emplace(moduleKey, std::move(ptModule));
|
||||||
catch (...)
|
if (inserted)
|
||||||
{
|
{
|
||||||
std::wstring errorMessage = POWER_TOYS_MODULE_LOAD_FAIL;
|
CommandRegistry::instance().register_module(it->second.operator->());
|
||||||
errorMessage += moduleSubdir;
|
}
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
std::wstring errorMessage = POWER_TOYS_MODULE_LOAD_FAIL;
|
||||||
|
errorMessage += moduleSubdir;
|
||||||
|
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
// In debug mode, simply log the warning and continue execution.
|
// In debug mode, simply log the warning and continue execution.
|
||||||
@@ -205,11 +212,12 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
|||||||
MB_OK | MB_ICONERROR);
|
MB_OK | MB_ICONERROR);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Start initial powertoys
|
// Start initial powertoys
|
||||||
start_enabled_powertoys();
|
start_enabled_powertoys();
|
||||||
std::wstring product_version = get_product_version();
|
start_cli_server();
|
||||||
Trace::EventLaunch(product_version, isProcessElevated);
|
std::wstring product_version = get_product_version();
|
||||||
|
Trace::EventLaunch(product_version, isProcessElevated);
|
||||||
PTSettingsHelper::save_last_version_run(product_version);
|
PTSettingsHelper::save_last_version_run(product_version);
|
||||||
|
|
||||||
if (openSettings)
|
if (openSettings)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
<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 runner.base.rc runner.rc" />
|
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h runner.base.rc runner.rc" />
|
||||||
@@ -65,6 +65,8 @@
|
|||||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="powertoy_module.cpp" />
|
<ClCompile Include="powertoy_module.cpp" />
|
||||||
|
<ClCompile Include="command_registry.cpp" />
|
||||||
|
<ClCompile Include="cli_server.cpp" />
|
||||||
<ClCompile Include="main.cpp" />
|
<ClCompile Include="main.cpp" />
|
||||||
<ClCompile Include="restart_elevated.cpp" />
|
<ClCompile Include="restart_elevated.cpp" />
|
||||||
<ClCompile Include="centralized_kb_hook.cpp" />
|
<ClCompile Include="centralized_kb_hook.cpp" />
|
||||||
@@ -86,6 +88,8 @@
|
|||||||
<ClInclude Include="centralized_kb_hook.h" />
|
<ClInclude Include="centralized_kb_hook.h" />
|
||||||
<ClInclude Include="settings_telemetry.h" />
|
<ClInclude Include="settings_telemetry.h" />
|
||||||
<ClInclude Include="UpdateUtils.h" />
|
<ClInclude Include="UpdateUtils.h" />
|
||||||
|
<ClInclude Include="command_registry.h" />
|
||||||
|
<ClInclude Include="cli_server.h" />
|
||||||
<ClInclude Include="powertoy_module.h" />
|
<ClInclude Include="powertoy_module.h" />
|
||||||
<ClInclude Include="resource.h" />
|
<ClInclude Include="resource.h" />
|
||||||
<ClInclude Include="restart_elevated.h" />
|
<ClInclude Include="restart_elevated.h" />
|
||||||
@@ -174,4 +178,4 @@
|
|||||||
<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'))" />
|
<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'))" />
|
||||||
<Error Condition="!Exists('..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
<Error Condition="!Exists('..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ClCompile Include="command_registry.cpp">
|
||||||
|
<Filter>Utils</Filter>
|
||||||
|
</ClCompile>
|
||||||
|
<ClCompile Include="cli_server.cpp">
|
||||||
|
<Filter>Utils</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="main.cpp" />
|
<ClCompile Include="main.cpp" />
|
||||||
<ClCompile Include="pch.cpp" />
|
<ClCompile Include="pch.cpp" />
|
||||||
<ClCompile Include="unhandled_exception_handler.cpp">
|
<ClCompile Include="unhandled_exception_handler.cpp">
|
||||||
@@ -57,6 +63,12 @@
|
|||||||
<ClInclude Include="tray_icon.h">
|
<ClInclude Include="tray_icon.h">
|
||||||
<Filter>Utils</Filter>
|
<Filter>Utils</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
|
<ClInclude Include="command_registry.h">
|
||||||
|
<Filter>Utils</Filter>
|
||||||
|
</ClInclude>
|
||||||
|
<ClInclude Include="cli_server.h">
|
||||||
|
<Filter>Utils</Filter>
|
||||||
|
</ClInclude>
|
||||||
<ClInclude Include="powertoy_module.h">
|
<ClInclude Include="powertoy_module.h">
|
||||||
<Filter>Utils</Filter>
|
<Filter>Utils</Filter>
|
||||||
</ClInclude>
|
</ClInclude>
|
||||||
@@ -132,4 +144,4 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
<Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
340
tools/ptcli/Program.cs
Normal file
340
tools/ptcli/Program.cs
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
// 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.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace PowerToys.Cli
|
||||||
|
{
|
||||||
|
internal sealed class Program
|
||||||
|
{
|
||||||
|
private const string PipeName = "PowerToys.Runner.CLI";
|
||||||
|
private static readonly JsonSerializerOptions JsonOutputOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonRequestOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
PropertyNamingPolicy = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
{
|
||||||
|
PrintHelp();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool listModules = false;
|
||||||
|
bool listCommands = false;
|
||||||
|
string? listCommandsModule = null;
|
||||||
|
string? module = null;
|
||||||
|
string? action = null;
|
||||||
|
bool rawJson = false;
|
||||||
|
int timeoutMs = 20000;
|
||||||
|
|
||||||
|
var payload = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
var argument = args[i];
|
||||||
|
switch (argument)
|
||||||
|
{
|
||||||
|
case "--help":
|
||||||
|
case "-h":
|
||||||
|
PrintHelp();
|
||||||
|
return 0;
|
||||||
|
case "--list-modules":
|
||||||
|
listModules = true;
|
||||||
|
break;
|
||||||
|
case "--list-commands":
|
||||||
|
listCommands = true;
|
||||||
|
if (i + 1 < args.Length && !args[i + 1].StartsWith('-'))
|
||||||
|
{
|
||||||
|
listCommandsModule = args[++i];
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "--module":
|
||||||
|
case "-m":
|
||||||
|
module = RequireValue(args, ref i, argument);
|
||||||
|
break;
|
||||||
|
case "--action":
|
||||||
|
case "-a":
|
||||||
|
action = RequireValue(args, ref i, argument);
|
||||||
|
break;
|
||||||
|
case "--json":
|
||||||
|
rawJson = true;
|
||||||
|
break;
|
||||||
|
case "--timeout":
|
||||||
|
var timeoutValue = RequireValue(args, ref i, argument);
|
||||||
|
if (!int.TryParse(timeoutValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out timeoutMs) || timeoutMs <= 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("--timeout expects a positive integer value (milliseconds).");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (argument.StartsWith("--", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var key = argument.Substring(2);
|
||||||
|
var value = RequireValue(args, ref i, argument);
|
||||||
|
payload[key] = ParseValue(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Unrecognized argument '{argument}'.");
|
||||||
|
PrintHelp();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (listModules)
|
||||||
|
{
|
||||||
|
return await ExecuteCommandAsync(string.Empty, "list-modules", new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase), rawJson, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listCommands)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(listCommandsModule))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("--list-commands requires a module name (e.g. ptcli --list-commands awake).");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var argsPayload = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["module"] = listCommandsModule!,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await ExecuteCommandAsync(string.Empty, "list-commands", argsPayload, rawJson, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(module) || string.IsNullOrWhiteSpace(action))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Both --module and --action must be specified.");
|
||||||
|
PrintHelp();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ExecuteCommandAsync(module!, action!, payload, rawJson, timeoutMs);
|
||||||
|
}
|
||||||
|
catch (TimeoutException)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Timed out while communicating with the PowerToys runner.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Pipe communication failed: {ex.Message}");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RequireValue(string[] args, ref int index, string option)
|
||||||
|
{
|
||||||
|
if (index + 1 >= args.Length)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Option {option} requires a value.");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args[++index];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ParseValue(string value)
|
||||||
|
{
|
||||||
|
if (bool.TryParse(value, out var boolValue))
|
||||||
|
{
|
||||||
|
return boolValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
|
||||||
|
{
|
||||||
|
return intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
return doubleValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private static async Task<int> ExecuteCommandAsync(string module, string action, Dictionary<string, object> args, bool rawJson, int timeoutMs)
|
||||||
|
{
|
||||||
|
var request = new
|
||||||
|
{
|
||||||
|
v = 1,
|
||||||
|
correlationId = Guid.NewGuid().ToString(),
|
||||||
|
command = new
|
||||||
|
{
|
||||||
|
module,
|
||||||
|
action,
|
||||||
|
args,
|
||||||
|
},
|
||||||
|
options = new
|
||||||
|
{
|
||||||
|
timeoutMs,
|
||||||
|
wantProgress = false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
string payload = JsonSerializer.Serialize(request, JsonRequestOptions);
|
||||||
|
|
||||||
|
using var client = new NamedPipeClientStream(
|
||||||
|
".",
|
||||||
|
PipeName,
|
||||||
|
PipeDirection.InOut,
|
||||||
|
PipeOptions.Asynchronous);
|
||||||
|
|
||||||
|
await client.ConnectAsync(timeoutMs).ConfigureAwait(false);
|
||||||
|
client.ReadMode = PipeTransmissionMode.Message;
|
||||||
|
|
||||||
|
using (var writer = new StreamWriter(client, Encoding.UTF8, leaveOpen: true))
|
||||||
|
{
|
||||||
|
await writer.WriteAsync(payload).ConfigureAwait(false);
|
||||||
|
await writer.FlushAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.WaitForPipeDrain();
|
||||||
|
|
||||||
|
var responseMessage = await ReadMessageAsync(client).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrEmpty(responseMessage))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Received empty response from the PowerToys runner.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = JsonSerializer.Deserialize<JsonElement>(responseMessage);
|
||||||
|
var status = document.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : "error";
|
||||||
|
|
||||||
|
if (rawJson)
|
||||||
|
{
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(document, JsonOutputOptions));
|
||||||
|
return string.Equals(status, "ok", StringComparison.OrdinalIgnoreCase) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(status, "ok", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (document.TryGetProperty("result", out var resultElement))
|
||||||
|
{
|
||||||
|
RenderResult(resultElement);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("Command completed successfully.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.TryGetProperty("error", out var errorElement))
|
||||||
|
{
|
||||||
|
var code = errorElement.TryGetProperty("code", out var codeElement) ? codeElement.GetString() : "E_UNKNOWN";
|
||||||
|
var message = errorElement.TryGetProperty("message", out var messageElement) ? messageElement.GetString() : "Command failed.";
|
||||||
|
Console.Error.WriteLine($"{code}: {message}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Command failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> ReadMessageAsync(NamedPipeClientStream client)
|
||||||
|
{
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
using var reader = new StreamReader(client, Encoding.UTF8, false, bufferSize: 1024, leaveOpen: true);
|
||||||
|
char[] buffer = new char[1024];
|
||||||
|
int read;
|
||||||
|
while ((read = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0)
|
||||||
|
{
|
||||||
|
builder.Append(buffer, 0, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RenderResult(JsonElement element)
|
||||||
|
{
|
||||||
|
if (element.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
if (element.TryGetProperty("modules", out var modulesElement) && modulesElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var module in modulesElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = module.TryGetProperty("module", out var moduleName) ? moduleName.GetString() : "<module>";
|
||||||
|
Console.WriteLine(name);
|
||||||
|
|
||||||
|
if (module.TryGetProperty("commands", out var commandsElement) && commandsElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var command in commandsElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
var action = command.TryGetProperty("action", out var actionElement) ? actionElement.GetString() : "<action>";
|
||||||
|
var description = command.TryGetProperty("description", out var descriptionElement) ? descriptionElement.GetString() : string.Empty;
|
||||||
|
Console.WriteLine($" - {action}: {description}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.TryGetProperty("commands", out var commands) && commands.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
var moduleName = element.TryGetProperty("module", out var moduleElement) ? moduleElement.GetString() : "<module>";
|
||||||
|
Console.WriteLine(moduleName);
|
||||||
|
foreach (var command in commands.EnumerateArray())
|
||||||
|
{
|
||||||
|
var action = command.TryGetProperty("action", out var actionElement) ? actionElement.GetString() : "<action>";
|
||||||
|
var description = command.TryGetProperty("description", out var descriptionElement) ? descriptionElement.GetString() : string.Empty;
|
||||||
|
Console.WriteLine($" - {action}: {description}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(element, JsonOutputOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PrintHelp()
|
||||||
|
{
|
||||||
|
Console.WriteLine("PowerToys CLI");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Usage:");
|
||||||
|
Console.WriteLine(" ptcli --list-modules");
|
||||||
|
Console.WriteLine(" ptcli --list-commands <module>");
|
||||||
|
Console.WriteLine(" ptcli -m <module> -a <action> [--key value] [--json]");
|
||||||
|
Console.WriteLine();
|
||||||
|
Console.WriteLine("Examples:");
|
||||||
|
Console.WriteLine(" ptcli --list-modules");
|
||||||
|
Console.WriteLine(" ptcli --list-commands awake");
|
||||||
|
Console.WriteLine(" ptcli -m awake -a status");
|
||||||
|
Console.WriteLine(" ptcli -m awake -a set --mode timed --duration 30m --displayOn true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
tools/ptcli/ptcli.csproj
Normal file
24
tools/ptcli/ptcli.csproj
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<!-- Look at Directory.Build.props in root for common settings as well -->
|
||||||
|
<Import Project="..\..\src\Common.SelfContained.props" />
|
||||||
|
<Import Project="..\..\src\Common.Dotnet.CsWinRT.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ProjectGuid>{2589570C-B068-41CA-A554-BDCAE6FC4CAC}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AssemblyName>PowerToys.Cli</AssemblyName>
|
||||||
|
<RootNamespace>PowerToys.Cli</RootNamespace>
|
||||||
|
<OutputPath>..\..\$(Platform)\$(Configuration)\ptcli\</OutputPath>
|
||||||
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Platform)'=='x64'">
|
||||||
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Platform)'=='ARM64'">
|
||||||
|
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user