Compare commits
18 Commits
582c57bc5f
...
v0-m8-done
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae9928782d | ||
|
|
2485ffb14f | ||
|
|
eadcdd7f10 | ||
|
|
36006bd4aa | ||
|
|
230a401386 | ||
|
|
e77f2598b9 | ||
|
|
2b25f805cd | ||
|
|
776ddc1a53 | ||
|
|
c3369b8e48 | ||
|
|
20ee48db32 | ||
|
|
2b4f498259 | ||
|
|
614d31fa71 | ||
|
|
ceed19d517 | ||
|
|
2ea712db36 | ||
|
|
3c158269bf | ||
|
|
e2a8b874d7 | ||
|
|
71a2f2e868 | ||
|
|
6a4bcb75eb |
@@ -14,18 +14,116 @@ find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql)
|
||||
|
||||
qt_standard_project_setup()
|
||||
|
||||
add_subdirectory(third_party/KodoTerm)
|
||||
|
||||
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/FreeRDP/CMakeLists.txt")
|
||||
message(FATAL_ERROR "Vendored FreeRDP source is missing at third_party/FreeRDP")
|
||||
endif()
|
||||
|
||||
# Pin FreeRDP build to the vendored source tree so headers/libs are always from the same revision.
|
||||
set(FREERDP_UNIFIED_BUILD ON CACHE BOOL "" FORCE)
|
||||
set(WITH_MANPAGES OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_SAMPLE OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_TESTING_INTERNAL OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_CLIENT_COMMON ON CACHE BOOL "" FORCE)
|
||||
set(WITH_CLIENT OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_CLIENT_SDL OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_CLIENT_INTERFACE OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_SERVER OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_CHANNELS ON CACHE BOOL "" FORCE)
|
||||
set(WITH_CLIENT_CHANNELS ON CACHE BOOL "" FORCE)
|
||||
set(WITH_FUSE OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_DRDYNVC ON CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_DRDYNVC_CLIENT ON CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_DISP ON CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_DISP_CLIENT ON CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_AINPUT OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_AUDIN OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_CLIPRDR OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_DRIVE OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_ECHO OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_ENCOMSP OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_GEOMETRY OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_LOCATION OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_PARALLEL OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_PRINTER OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RAIL OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPDR OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPEAR OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPECAM OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPEI OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPEMSC OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPGFX OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_RDPSND OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_REMDESK OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_SERIAL OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_SMARTCARD OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_SSHAGENT OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_TELEMETRY OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_URBDRC OFF CACHE BOOL "" FORCE)
|
||||
set(CHANNEL_VIDEO OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_FFMPEG OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_DSP_FFMPEG OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_VIDEO_FFMPEG OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_CAIRO OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_SWSCALE OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_JPEG OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_KRB5 OFF CACHE BOOL "" FORCE)
|
||||
set(WITH_UNICODE_BUILTIN ON CACHE BOOL "" FORCE)
|
||||
set(WITH_WINPR_TOOLS OFF CACHE BOOL "" FORCE)
|
||||
|
||||
add_subdirectory(third_party/FreeRDP EXCLUDE_FROM_ALL)
|
||||
|
||||
add_executable(orbithub
|
||||
src/about_dialog.cpp
|
||||
src/about_dialog.h
|
||||
src/app_icon.cpp
|
||||
src/app_icon.h
|
||||
src/main.cpp
|
||||
src/profile_dialog.cpp
|
||||
src/profile_dialog.h
|
||||
src/profile_repository.cpp
|
||||
src/profile_repository.h
|
||||
src/profiles_tree_widget.cpp
|
||||
src/profiles_tree_widget.h
|
||||
src/profiles_window.cpp
|
||||
src/profiles_window.h
|
||||
src/session_backend.h
|
||||
src/session_backend_factory.cpp
|
||||
src/session_backend_factory.h
|
||||
src/session_tab.cpp
|
||||
src/session_tab.h
|
||||
src/rdp_display_widget.cpp
|
||||
src/rdp_display_widget.h
|
||||
src/terminal_view.cpp
|
||||
src/terminal_view.h
|
||||
src/session_window.cpp
|
||||
src/session_window.h
|
||||
src/rdp_session_backend.cpp
|
||||
src/rdp_session_backend.h
|
||||
src/ssh_session_backend.cpp
|
||||
src/ssh_session_backend.h
|
||||
src/unsupported_session_backend.cpp
|
||||
src/unsupported_session_backend.h
|
||||
)
|
||||
|
||||
target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
|
||||
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
|
||||
if(TARGET freerdp AND TARGET winpr)
|
||||
target_compile_definitions(orbithub PRIVATE ORBITHUB_HAS_FREERDP)
|
||||
target_include_directories(orbithub PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/FreeRDP/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/FreeRDP/winpr/include
|
||||
${CMAKE_CURRENT_BINARY_DIR}/third_party/FreeRDP/include
|
||||
${CMAKE_CURRENT_BINARY_DIR}/third_party/FreeRDP/winpr/include
|
||||
)
|
||||
target_link_libraries(orbithub PRIVATE freerdp winpr)
|
||||
if(TARGET freerdp-client)
|
||||
target_link_libraries(orbithub PRIVATE freerdp-client)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Vendored FreeRDP targets were not produced as expected.")
|
||||
endif()
|
||||
|
||||
install(TARGETS orbithub RUNTIME DESTINATION bin)
|
||||
|
||||
135
README.md
Normal file
135
README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# OrbitHub
|
||||
|
||||
OrbitHub is a cross-platform native desktop app for managing and launching remote sessions from one place.
|
||||
|
||||
It is implemented in C++17 with Qt6 Widgets and built with CMake.
|
||||
|
||||
Supported target platforms:
|
||||
- Windows
|
||||
- Linux
|
||||
- macOS
|
||||
|
||||
## Current Status
|
||||
|
||||
OrbitHub is in active development.
|
||||
|
||||
- Milestones completed: M0-M5 and M8
|
||||
- Current milestone: Milestone 9 (Packaging and Distribution)
|
||||
- Deferred milestone: Milestone 6 (VNC Fully Working)
|
||||
- Latest checkpoint tag: `v0-m8-wip1`
|
||||
- VNC implementation milestone (M6) is currently deferred
|
||||
|
||||
Progress and milestone details:
|
||||
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
||||
|
||||
## Implemented Features
|
||||
|
||||
### Profile Management
|
||||
|
||||
- SQLite-backed profile storage
|
||||
- Create, edit, delete profiles
|
||||
- Protocol-aware profile validation (SSH/RDP/VNC)
|
||||
- Profile search and sorting
|
||||
- Tags support
|
||||
- Folder/subfolder support
|
||||
- `List` and `Folders` profile views
|
||||
- Right-click profile tree actions:
|
||||
- New Folder
|
||||
- New Connection
|
||||
- Drag-and-drop profile moves between folders with persistence
|
||||
|
||||
### Session Experience
|
||||
|
||||
- Multi-tab session window
|
||||
- Auto-connect on tab open
|
||||
- Disconnect on tab close
|
||||
- Session state indicators on tabs
|
||||
- Timestamped event log with filtering and export
|
||||
|
||||
### SSH
|
||||
|
||||
- Embedded interactive terminal (in-app typing)
|
||||
- Theme support (`Dark`, `Light`, `Solarized Dark`)
|
||||
- Password and private-key auth flows
|
||||
- Known-hosts policy support
|
||||
|
||||
### RDP
|
||||
|
||||
- Embedded in-window RDP rendering surface (no external launcher)
|
||||
- Keyboard/mouse input forwarding
|
||||
- Resize handling and resolution renegotiation
|
||||
- Domain-aware authentication support
|
||||
- RDP security/performance profile options
|
||||
|
||||
### App UX
|
||||
|
||||
- App icon and themed About dialog
|
||||
- `File` menu:
|
||||
- New Profile
|
||||
- New Folder
|
||||
- Quit
|
||||
- `Help` menu:
|
||||
- About OrbitHub
|
||||
|
||||
## Build and Run
|
||||
|
||||
Detailed platform instructions:
|
||||
- [docs/BUILDING.md](docs/BUILDING.md)
|
||||
|
||||
Quick start (Linux/macOS with Ninja):
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -G Ninja
|
||||
cmake --build build
|
||||
./build/orbithub
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
Core dependencies:
|
||||
- Qt 6 (Widgets, SQL)
|
||||
- CMake 3.21+
|
||||
- C++17 toolchain
|
||||
|
||||
Protocol/runtime dependencies:
|
||||
- SSH client (`ssh`) available on `PATH` for SSH sessions
|
||||
|
||||
Bundled/vendored third-party components:
|
||||
- KodoTerm
|
||||
- libvterm
|
||||
- FreeRDP/WinPR
|
||||
|
||||
## Licensing
|
||||
|
||||
Project license:
|
||||
- MIT (see [LICENSE](LICENSE))
|
||||
|
||||
License links:
|
||||
- MIT License: <https://opensource.org/licenses/MIT>
|
||||
- GNU LGPLv3: <https://www.gnu.org/licenses/lgpl-3.0.html>
|
||||
- Apache License 2.0: <https://www.apache.org/licenses/LICENSE-2.0>
|
||||
|
||||
Important third-party license notes:
|
||||
- Qt6 is dynamically linked in this project build setup.
|
||||
- Qt6 is used under LGPLv3 terms in this project build setup.
|
||||
- KodoTerm and libvterm are MIT-licensed.
|
||||
- FreeRDP/WinPR is Apache-2.0 licensed.
|
||||
|
||||
Repository license files:
|
||||
- Project: [LICENSE](LICENSE)
|
||||
- KodoTerm: [third_party/KodoTerm/LICENSE](third_party/KodoTerm/LICENSE)
|
||||
- FreeRDP: [third_party/FreeRDP/LICENSE](third_party/FreeRDP/LICENSE)
|
||||
|
||||
See in-app `Help -> About OrbitHub` for license links and third-party inventory.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
- `src/` - application source code
|
||||
- `docs/` - build guide, spec, and progress tracking
|
||||
- `third_party/` - vendored third-party dependencies
|
||||
- `build/` - local build output (generated)
|
||||
|
||||
## Notes
|
||||
|
||||
- Passwords are requested at connect time and are not stored in the profile database.
|
||||
- This repository currently prioritizes integrated SSH and RDP workflows while VNC implementation is pending.
|
||||
@@ -8,7 +8,8 @@ Run all commands from the repository root unless noted.
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
build-essential cmake ninja-build git pkg-config \
|
||||
qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools
|
||||
qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools \
|
||||
openssh-client
|
||||
|
||||
cmake -S . -B build -G Ninja
|
||||
cmake --build build
|
||||
@@ -20,7 +21,7 @@ cmake --build build
|
||||
```bash
|
||||
xcode-select --install
|
||||
brew update
|
||||
brew install cmake ninja pkg-config qt@6
|
||||
brew install cmake ninja pkg-config qt@6 openssh
|
||||
|
||||
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
|
||||
cmake --build build
|
||||
@@ -33,6 +34,7 @@ cmake --build build
|
||||
winget install -e --id Git.Git
|
||||
winget install -e --id Kitware.CMake
|
||||
winget install -e --id Ninja-build.Ninja
|
||||
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
|
||||
winget install -e --id Microsoft.VisualStudio.2022.BuildTools `
|
||||
--override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools"
|
||||
```
|
||||
@@ -53,4 +55,5 @@ cmake --build build
|
||||
## Notes
|
||||
|
||||
- OrbitHub currently requires Qt6 Widgets and CMake 3.21+.
|
||||
- Milestone 3 SSH sessions require an `ssh` client available on `PATH`.
|
||||
- If Qt is installed in a custom location, pass `-DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/<toolchain>` to CMake.
|
||||
|
||||
@@ -64,3 +64,89 @@ OrbitHub uses a two-window model:
|
||||
- Connect loads full profile details into session tab
|
||||
- Session lifecycle states in UI (`Connecting`, `Connected`, `Failed`) with non-blocking updates
|
||||
- Tag: v0-m2-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3
|
||||
|
||||
- Real SSH backend using native `ssh` process (connect, disconnect, reconnect)
|
||||
- Protocol backend abstraction with worker-thread execution
|
||||
- RDP/VNC explicitly marked as not implemented in session UX
|
||||
- Connect-time credential prompts (password/private key path) with no secret storage in DB
|
||||
- Session tab controls: `Connect`, `Disconnect`, `Reconnect`, `Copy Error`
|
||||
- Per-session timestamped event log and user-friendly error mapping
|
||||
- Profile schema extended with `private_key_path` and `known_hosts_policy`
|
||||
- Tag: v0-m3-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4
|
||||
|
||||
- Interactive embedded SSH terminal (`KodoTerm` + `libvterm`)
|
||||
- SSH host-key trust prompt flow for `Ask` policy
|
||||
- Improved SSH auth flow for password / private key
|
||||
- Terminal utilities and UX polish (theme, clear, resize/input behavior)
|
||||
- Session lifecycle UX cleanup (auto-connect, disconnect on close, tab state indicators)
|
||||
- Tag: v0-m4-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 5 - RDP Fully Working
|
||||
|
||||
- Implement complete RDP protocol support (replace not-implemented RDP path)
|
||||
- Deliver a usable in-app RDP session experience consistent with SSH tab UX
|
||||
- Support RDP connect/disconnect/reconnect lifecycle from OrbitHub
|
||||
- Add required RDP-specific connect options in profile/session flows
|
||||
- Normalize event/error reporting with existing SSH behavior
|
||||
- Planned Tag: v0-m5-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 6 - VNC Fully Working
|
||||
|
||||
- Implement complete VNC protocol support (replace not-implemented VNC path)
|
||||
- Deliver a usable in-app VNC session experience consistent with SSH/RDP tab UX
|
||||
- Support VNC connect/disconnect/reconnect lifecycle from OrbitHub
|
||||
- Add required VNC-specific connect options in profile/session flows
|
||||
- Normalize event/error reporting with SSH/RDP behavior
|
||||
- Planned Tag: v0-m6-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 7 - Cross-Platform Protocol Hardening
|
||||
|
||||
- Validate SSH/RDP/VNC behavior on Windows, Linux, and macOS
|
||||
- Resolve platform-specific path/process/auth differences
|
||||
- Improve diagnostics and failure messaging for common protocol issues
|
||||
- Add protocol regression checklist and repeatable verification scripts
|
||||
- Planned Tag: v0-m7-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 8 - Profile and Session UX Completion
|
||||
|
||||
- Complete profile fields/validation for all protocols
|
||||
- Add quality-of-life controls for active sessions (tab context actions, defaults, persistence)
|
||||
- Persist per-user UI/session preferences (theme, panel visibility, terminal defaults)
|
||||
- Improve session history/event visibility and filtering
|
||||
- Planned Tag: v0-m8-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 9 - Packaging and Distribution
|
||||
|
||||
- Produce distributable artifacts for Windows, Linux, and macOS
|
||||
- Document dependency/runtime requirements per platform
|
||||
- Add release build scripts for reproducible packaging
|
||||
- Validate clean install + first-run flow on each platform
|
||||
- Planned Tag: v0-m9-done
|
||||
|
||||
---
|
||||
|
||||
## Milestone 10 - v1.0 Stabilization
|
||||
|
||||
- End-to-end QA pass across core workflows (profile CRUD + SSH/RDP/VNC session lifecycle)
|
||||
- Fix blocker/critical defects from validation
|
||||
- Finalize docs (`BUILDING`, usage notes, known limitations)
|
||||
- Prepare v1.0 release notes and final acceptance checklist
|
||||
- Planned Tag: v1.0.0
|
||||
|
||||
121
docs/PROGRESS.md
121
docs/PROGRESS.md
@@ -43,3 +43,124 @@ Delivered:
|
||||
|
||||
Git:
|
||||
- Tag: `v0-m2-done`
|
||||
|
||||
## Milestone 3 - Real SSH Backend and Session Controls
|
||||
|
||||
Status: Completed
|
||||
|
||||
Delivered:
|
||||
- Backend architecture introduced (`SessionBackend` + protocol-specific implementations)
|
||||
- Worker-thread backend execution for connection lifecycle operations
|
||||
- Real SSH process backend (`ssh`) with connect/disconnect/reconnect
|
||||
- Unsupported protocol backend with explicit not-implemented messaging (RDP/VNC)
|
||||
- Session tab controls: `Connect`, `Disconnect`, `Reconnect`, `Copy Error`
|
||||
- Connect-time credential flow (password prompt / private-key path selection)
|
||||
- Session event log pane with timestamps and user-friendly error mapping
|
||||
- SQLite profile schema migration for `private_key_path` and `known_hosts_policy`
|
||||
|
||||
Git:
|
||||
- Tag: `v0-m3-done`
|
||||
|
||||
## Milestone 4 - Interactive SSH Session UX
|
||||
|
||||
Status: Completed
|
||||
|
||||
Delivered:
|
||||
- Embedded interactive SSH terminal using `KodoTerm` + vendored `libvterm`
|
||||
- Native in-terminal typing for SSH sessions (no separate input box)
|
||||
- ANSI/color rendering with selectable terminal themes (`Dark`, `Light`, `Solarized Dark`)
|
||||
- Cross-platform SSH auth path improvements (`ssh-askpass` handling and host-key policy wiring)
|
||||
- Session UX simplification: auto-connect on tab open, disconnect on tab close
|
||||
- Tab-state indicators via tab color and state suffix (`Connecting`, `Connected`, `Disconnected`, `Failed`)
|
||||
- Right-click tab menu for `Disconnect`, `Reconnect`, `Theme`, and `Clear`
|
||||
- Collapsible events panel retained as primary diagnostics surface; inline detail/status banners removed
|
||||
- Terminal behavior polish: better fixed-width font selection, cursor visibility, backspace handling, and terminal-size negotiation stability
|
||||
|
||||
Git:
|
||||
- Tag: `v0-m4-done`
|
||||
|
||||
## Milestone 5 - RDP Fully Working
|
||||
|
||||
Status: Completed
|
||||
|
||||
Delivered:
|
||||
- Added `RdpSessionBackend` and wired protocol selection so `RDP` no longer routes to unsupported backend
|
||||
- Pivoted RDP design to embedded-only integration (no external RDP process launches)
|
||||
- Implemented embedded FreeRDP client thread with connect/disconnect lifecycle and event-loop handling
|
||||
- Added in-window `RdpDisplayWidget` rendering surface with frame updates from FreeRDP GDI
|
||||
- Wired direct keyboard/mouse input from the embedded RDP surface to the backend
|
||||
- Added RDP connect-time password prompt flow and settings wiring (host/port/user/password, desktop size)
|
||||
- Added explicit profile `Domain` support for RDP auth (with `DOMAIN\username` fallback parsing)
|
||||
- Updated session tab/context-menu behavior so terminal-only actions are hidden on RDP tabs
|
||||
- Implemented dynamic in-session RDP resolution renegotiation from viewport resize events
|
||||
- Enabled minimal FreeRDP client-channel build (`drdynvc` + `disp`) and channel loading for runtime resize support
|
||||
- Added RDP profile-level security mode and performance profile options, wired into FreeRDP connection settings
|
||||
- Hardened RDP lifecycle handling for disconnect/reconnect/abort flows to avoid false failure states on user-initiated stops
|
||||
- Expanded RDP error/disconnect diagnostics with richer FreeRDP code mapping and raw disconnect detail events
|
||||
- Pulled FreeRDP source for integration planning and API review
|
||||
|
||||
Git:
|
||||
- Tag: `v0-m5-done`
|
||||
|
||||
## Milestone 6 - VNC Fully Working
|
||||
|
||||
Status: Deferred (temporarily postponed)
|
||||
|
||||
Planned Scope:
|
||||
- Replace current unsupported VNC path with complete VNC implementation
|
||||
- Deliver usable in-app VNC session behavior aligned to SSH/RDP UX
|
||||
- Implement VNC connect/disconnect/reconnect lifecycle handling
|
||||
- Extend profile/session connect options needed by VNC
|
||||
- Standardize event log and error mapping behavior with SSH/RDP
|
||||
|
||||
## Milestone 7 - Cross-Platform Protocol Hardening
|
||||
|
||||
Status: Planned
|
||||
|
||||
Planned Scope:
|
||||
- Validate SSH/RDP/VNC workflows on Windows, Linux, and macOS
|
||||
- Fix platform-specific runtime/process/auth issues
|
||||
- Add repeatable protocol validation checklist/scripts
|
||||
|
||||
## Milestone 8 - Profile and Session UX Completion
|
||||
|
||||
Status: Completed
|
||||
|
||||
Delivered:
|
||||
- Added profile `tags` field to storage + schema migration and profile editor UX
|
||||
- Added profile `folder_path` field + nested folder/subfolder profile view mode
|
||||
- Added profile tree context actions (`New Folder`, `New Connection`) and drag-to-folder profile moves with persistence
|
||||
- Added `Help -> About OrbitHub` dialog with third-party library inventory and MIT/Apache-2.0 license links
|
||||
- Extended profile search to include tags/folder path and added profile sort controls (`Name`, `Protocol`, `Host`)
|
||||
- Persisted profile list UX preferences (`search text`, `view mode`, protocol/tag filters, `sort order`) across app restarts
|
||||
- Added protocol-aware profile validation/normalization for SSH/RDP/VNC (repository + dialog)
|
||||
- Improved profile form protocol UX hints and SSH private-key path validation
|
||||
- Added session events filtering and tab-context actions (`Show/Hide Events`, `Copy Events`, `Clear Events`)
|
||||
- Added session diagnostics QoL: severity quick-filter (`All/Warnings/Errors`) and `Export Events` action
|
||||
- Persisted session UI defaults (`terminal theme`, `events panel visibility`) for new tabs/windows
|
||||
- Added profile quick filters (`Protocol`, `Tag`) with persistence to speed profile browsing
|
||||
|
||||
Validation:
|
||||
- Local build verification passed (`cmake --build build`)
|
||||
- No automated tests are currently configured in CTest
|
||||
|
||||
Git:
|
||||
- Tag: Pending user approval (`v0-m8-done`)
|
||||
|
||||
## Milestone 9 - Packaging and Distribution
|
||||
|
||||
Status: Planned
|
||||
|
||||
Planned Scope:
|
||||
- Build distributable artifacts for Windows/Linux/macOS
|
||||
- Document runtime dependencies and install prerequisites
|
||||
- Add reproducible release packaging steps/scripts
|
||||
|
||||
## Milestone 10 - v1.0 Stabilization
|
||||
|
||||
Status: Planned
|
||||
|
||||
Planned Scope:
|
||||
- Run final regression and acceptance testing across all protocols
|
||||
- Resolve release-blocking defects
|
||||
- Finalize docs and publish v1.0 release notes/checklist
|
||||
|
||||
92
src/about_dialog.cpp
Normal file
92
src/about_dialog.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "about_dialog.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCoreApplication>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QTextBrowser>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent)
|
||||
{
|
||||
setWindowTitle(QStringLiteral("About OrbitHub"));
|
||||
setWindowIcon(QApplication::windowIcon());
|
||||
resize(760, 560);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(16, 16, 16, 16);
|
||||
layout->setSpacing(12);
|
||||
|
||||
auto* headerRow = new QHBoxLayout();
|
||||
headerRow->setSpacing(12);
|
||||
|
||||
auto* iconLabel = new QLabel(this);
|
||||
iconLabel->setFixedSize(80, 80);
|
||||
iconLabel->setPixmap(QApplication::windowIcon().pixmap(80, 80));
|
||||
iconLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
auto* titleColumn = new QVBoxLayout();
|
||||
titleColumn->setSpacing(4);
|
||||
|
||||
auto* title = new QLabel(QStringLiteral("<h1 style='margin:0'>OrbitHub</h1>"), this);
|
||||
auto* subtitle = new QLabel(
|
||||
QStringLiteral("Unified remote session manager for SSH, RDP, and VNC workflows."),
|
||||
this);
|
||||
subtitle->setWordWrap(true);
|
||||
subtitle->setStyleSheet(QStringLiteral("color: palette(mid);"));
|
||||
|
||||
const QString version = QCoreApplication::applicationVersion().trimmed().isEmpty()
|
||||
? QStringLiteral("Development build")
|
||||
: QCoreApplication::applicationVersion().trimmed();
|
||||
auto* buildLine = new QLabel(
|
||||
QStringLiteral("Version: %1 | Qt runtime linked dynamically").arg(version),
|
||||
this);
|
||||
buildLine->setStyleSheet(QStringLiteral("color: palette(mid);"));
|
||||
|
||||
titleColumn->addWidget(title);
|
||||
titleColumn->addWidget(subtitle);
|
||||
titleColumn->addWidget(buildLine);
|
||||
titleColumn->addStretch();
|
||||
|
||||
headerRow->addWidget(iconLabel, 0, Qt::AlignTop);
|
||||
headerRow->addLayout(titleColumn, 1);
|
||||
|
||||
auto* browser = new QTextBrowser(this);
|
||||
browser->setOpenExternalLinks(true);
|
||||
browser->setStyleSheet(QStringLiteral(
|
||||
"QTextBrowser { border: 1px solid palette(midlight); border-radius: 8px; padding: 8px; }"));
|
||||
browser->setHtml(QStringLiteral(R"(
|
||||
<h3 style="margin-top:0">Third-Party Libraries</h3>
|
||||
<p>OrbitHub uses the following external libraries:</p>
|
||||
<table cellspacing="0" cellpadding="6" border="1" style="border-collapse:collapse; width:100%;">
|
||||
<tr><th align="left">Library</th><th align="left">License</th><th align="left">Upstream</th></tr>
|
||||
<tr><td>Qt 6 (Widgets / SQL)</td><td>LGPLv3 / GPLv3 / Commercial</td><td><a href="https://www.qt.io/licensing">qt.io/licensing</a></td></tr>
|
||||
<tr><td>KodoTerm</td><td>MIT</td><td><a href="https://github.com/diegoiast/KodoTerm">github.com/diegoiast/KodoTerm</a></td></tr>
|
||||
<tr><td>libvterm</td><td>MIT</td><td><a href="https://github.com/neovim/libvterm">github.com/neovim/libvterm</a></td></tr>
|
||||
<tr><td>FreeRDP / WinPR</td><td>Apache License 2.0</td><td><a href="https://github.com/FreeRDP/FreeRDP">github.com/FreeRDP/FreeRDP</a></td></tr>
|
||||
</table>
|
||||
|
||||
<h3>License Links</h3>
|
||||
<ul>
|
||||
<li><a href="https://www.gnu.org/licenses/lgpl-3.0.html">GNU LGPLv3 (gnu.org)</a></li>
|
||||
<li><a href="https://opensource.org/licenses/MIT">MIT License (opensource.org)</a></li>
|
||||
<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0 (apache.org)</a></li>
|
||||
</ul>
|
||||
|
||||
<h3>License Files In This Repository</h3>
|
||||
<ul>
|
||||
<li><code>third_party/KodoTerm/LICENSE</code></li>
|
||||
<li><code>third_party/FreeRDP/LICENSE</code></li>
|
||||
<li><code>LICENSE</code> (project license)</li>
|
||||
</ul>
|
||||
)"));
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
|
||||
layout->addLayout(headerRow);
|
||||
layout->addWidget(browser, 1);
|
||||
layout->addWidget(buttons);
|
||||
}
|
||||
14
src/about_dialog.h
Normal file
14
src/about_dialog.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#ifndef ORBITHUB_ABOUT_DIALOG_H
|
||||
#define ORBITHUB_ABOUT_DIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class AboutDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AboutDialog(QWidget* parent = nullptr);
|
||||
};
|
||||
|
||||
#endif
|
||||
101
src/app_icon.cpp
Normal file
101
src/app_icon.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "app_icon.h"
|
||||
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
#include <QLinearGradient>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPen>
|
||||
#include <QPixmap>
|
||||
|
||||
namespace {
|
||||
QPixmap renderIconPixmap(int size)
|
||||
{
|
||||
QPixmap pixmap(size, size);
|
||||
pixmap.fill(Qt::transparent);
|
||||
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
const qreal s = static_cast<qreal>(size);
|
||||
const QRectF badgeRect(0.06 * s, 0.06 * s, 0.88 * s, 0.88 * s);
|
||||
const qreal badgeRadius = 0.19 * s;
|
||||
|
||||
QLinearGradient badgeGradient(badgeRect.topLeft(), badgeRect.bottomRight());
|
||||
badgeGradient.setColorAt(0.0, QColor(QStringLiteral("#0B1220")));
|
||||
badgeGradient.setColorAt(1.0, QColor(QStringLiteral("#111827")));
|
||||
|
||||
QPainterPath badgePath;
|
||||
badgePath.addRoundedRect(badgeRect, badgeRadius, badgeRadius);
|
||||
painter.fillPath(badgePath, badgeGradient);
|
||||
|
||||
const QPointF center(0.5 * s, 0.5 * s);
|
||||
const QRectF orbitRect(0.14 * s, 0.26 * s, 0.72 * s, 0.44 * s);
|
||||
|
||||
// Draw orbit behind monitor first.
|
||||
QPen orbitBackPen(QColor(QStringLiteral("#38BDF8")));
|
||||
orbitBackPen.setWidthF(0.06 * s);
|
||||
orbitBackPen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(orbitBackPen);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
|
||||
QTransform orbitTransform;
|
||||
orbitTransform.translate(center.x(), center.y());
|
||||
orbitTransform.rotate(-20.0);
|
||||
orbitTransform.translate(-center.x(), -center.y());
|
||||
painter.setTransform(orbitTransform);
|
||||
painter.drawEllipse(orbitRect);
|
||||
painter.resetTransform();
|
||||
|
||||
const QRectF monitorRect(0.2 * s, 0.2 * s, 0.6 * s, 0.44 * s);
|
||||
const QRectF screenRect(0.24 * s, 0.24 * s, 0.52 * s, 0.34 * s);
|
||||
const QRectF standStemRect(0.46 * s, 0.64 * s, 0.08 * s, 0.1 * s);
|
||||
const QRectF standBaseRect(0.34 * s, 0.74 * s, 0.32 * s, 0.08 * s);
|
||||
|
||||
QPainterPath monitorPath;
|
||||
monitorPath.addRoundedRect(monitorRect, 0.07 * s, 0.07 * s);
|
||||
painter.fillPath(monitorPath, QColor(QStringLiteral("#1F2937")));
|
||||
painter.setPen(QPen(QColor(QStringLiteral("#4B5563")), 0.016 * s));
|
||||
painter.drawPath(monitorPath);
|
||||
|
||||
QLinearGradient screenGradient(screenRect.topLeft(), screenRect.bottomRight());
|
||||
screenGradient.setColorAt(0.0, QColor(QStringLiteral("#22D3EE")));
|
||||
screenGradient.setColorAt(0.55, QColor(QStringLiteral("#38BDF8")));
|
||||
screenGradient.setColorAt(1.0, QColor(QStringLiteral("#0EA5E9")));
|
||||
painter.fillRect(screenRect, screenGradient);
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(QStringLiteral("#9CA3AF")));
|
||||
painter.drawRoundedRect(standStemRect, 0.02 * s, 0.02 * s);
|
||||
painter.setBrush(QColor(QStringLiteral("#6B7280")));
|
||||
painter.drawRoundedRect(standBaseRect, 0.03 * s, 0.03 * s);
|
||||
|
||||
// Orbit front segment over monitor for depth.
|
||||
QPen orbitFrontPen(QColor(QStringLiteral("#A3E635")));
|
||||
orbitFrontPen.setWidthF(0.06 * s);
|
||||
orbitFrontPen.setCapStyle(Qt::RoundCap);
|
||||
painter.setPen(orbitFrontPen);
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setTransform(orbitTransform);
|
||||
painter.drawArc(orbitRect, 212 * 16, 126 * 16);
|
||||
painter.drawArc(orbitRect, 6 * 16, 24 * 16);
|
||||
painter.resetTransform();
|
||||
|
||||
// Small indicator star.
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(QStringLiteral("#E5E7EB")));
|
||||
painter.drawEllipse(QPointF(0.77 * s, 0.2 * s), 0.02 * s, 0.02 * s);
|
||||
|
||||
return pixmap;
|
||||
}
|
||||
}
|
||||
|
||||
QIcon createOrbitHubAppIcon()
|
||||
{
|
||||
QIcon icon;
|
||||
const int sizes[] = {16, 24, 32, 48, 64, 128, 256};
|
||||
for (const int size : sizes) {
|
||||
icon.addPixmap(renderIconPixmap(size));
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
8
src/app_icon.h
Normal file
8
src/app_icon.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef ORBITHUB_APP_ICON_H
|
||||
#define ORBITHUB_APP_ICON_H
|
||||
|
||||
#include <QIcon>
|
||||
|
||||
QIcon createOrbitHubAppIcon();
|
||||
|
||||
#endif
|
||||
@@ -1,10 +1,16 @@
|
||||
#include "app_icon.h"
|
||||
#include "profiles_window.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
Q_INIT_RESOURCE(KodoTermThemes);
|
||||
|
||||
QApplication app(argc, argv);
|
||||
app.setOrganizationName(QStringLiteral("FireBugIT"));
|
||||
app.setApplicationName(QStringLiteral("OrbitHub"));
|
||||
app.setWindowIcon(createOrbitHubAppIcon());
|
||||
|
||||
ProfilesWindow window;
|
||||
window.show();
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
@@ -21,6 +25,28 @@ int standardPortForProtocol(const QString& protocol)
|
||||
}
|
||||
return 22; // SSH default
|
||||
}
|
||||
|
||||
QString normalizedProtocol(const QString& protocol)
|
||||
{
|
||||
if (protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("RDP");
|
||||
}
|
||||
if (protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("VNC");
|
||||
}
|
||||
return QStringLiteral("SSH");
|
||||
}
|
||||
|
||||
QString normalizedAuthMode(const QString& protocol, const QString& authMode)
|
||||
{
|
||||
if (protocol != QStringLiteral("SSH")) {
|
||||
return QStringLiteral("Password");
|
||||
}
|
||||
if (authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Private Key");
|
||||
}
|
||||
return QStringLiteral("Password");
|
||||
}
|
||||
}
|
||||
|
||||
ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
@@ -29,10 +55,19 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
m_hostInput(new QLineEdit(this)),
|
||||
m_portInput(new QSpinBox(this)),
|
||||
m_usernameInput(new QLineEdit(this)),
|
||||
m_domainInput(new QLineEdit(this)),
|
||||
m_tagsInput(new QLineEdit(this)),
|
||||
m_protocolInput(new QComboBox(this)),
|
||||
m_authModeInput(new QComboBox(this))
|
||||
m_authModeInput(new QComboBox(this)),
|
||||
m_privateKeyPathInput(new QLineEdit(this)),
|
||||
m_browsePrivateKeyButton(new QPushButton(QStringLiteral("Browse"), this)),
|
||||
m_knownHostsPolicyInput(new QComboBox(this)),
|
||||
m_rdpSecurityModeInput(new QComboBox(this)),
|
||||
m_rdpPerformanceProfileInput(new QComboBox(this)),
|
||||
m_protocolHint(new QLabel(this)),
|
||||
m_folderHint(new QLabel(this))
|
||||
{
|
||||
resize(420, 260);
|
||||
resize(560, 360);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
auto* form = new QFormLayout();
|
||||
@@ -42,30 +77,89 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
m_portInput->setRange(1, 65535);
|
||||
m_portInput->setValue(22);
|
||||
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
|
||||
m_domainInput->setPlaceholderText(QStringLiteral("CONTOSO"));
|
||||
m_tagsInput->setPlaceholderText(QStringLiteral("prod, linux, db"));
|
||||
|
||||
m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")});
|
||||
m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")});
|
||||
m_knownHostsPolicyInput->addItems(
|
||||
{QStringLiteral("Ask"), QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")});
|
||||
m_rdpSecurityModeInput->addItems(
|
||||
{QStringLiteral("Negotiate"), QStringLiteral("NLA"), QStringLiteral("TLS"), QStringLiteral("RDP")});
|
||||
m_rdpPerformanceProfileInput->addItems({QStringLiteral("Balanced"),
|
||||
QStringLiteral("Best Quality"),
|
||||
QStringLiteral("Best Performance"),
|
||||
QStringLiteral("Auto Detect")});
|
||||
|
||||
m_privateKeyPathInput->setPlaceholderText(QStringLiteral("/home/user/.ssh/id_ed25519"));
|
||||
|
||||
auto* privateKeyRow = new QWidget(this);
|
||||
auto* privateKeyLayout = new QHBoxLayout(privateKeyRow);
|
||||
privateKeyLayout->setContentsMargins(0, 0, 0, 0);
|
||||
privateKeyLayout->addWidget(m_privateKeyPathInput, 1);
|
||||
privateKeyLayout->addWidget(m_browsePrivateKeyButton);
|
||||
|
||||
connect(m_browsePrivateKeyButton,
|
||||
&QPushButton::clicked,
|
||||
this,
|
||||
[this]() {
|
||||
const QString selected = QFileDialog::getOpenFileName(this,
|
||||
QStringLiteral("Select Private Key"),
|
||||
QString(),
|
||||
QStringLiteral("All Files (*)"));
|
||||
if (!selected.isEmpty()) {
|
||||
m_privateKeyPathInput->setText(selected);
|
||||
}
|
||||
});
|
||||
|
||||
connect(m_protocolInput,
|
||||
&QComboBox::currentTextChanged,
|
||||
this,
|
||||
[this](const QString& protocol) {
|
||||
m_portInput->setValue(standardPortForProtocol(protocol));
|
||||
if (protocol != QStringLiteral("SSH")) {
|
||||
const QSignalBlocker blocker(m_authModeInput);
|
||||
m_authModeInput->setCurrentText(QStringLiteral("Password"));
|
||||
}
|
||||
refreshAuthFields();
|
||||
});
|
||||
|
||||
connect(m_authModeInput,
|
||||
&QComboBox::currentTextChanged,
|
||||
this,
|
||||
[this](const QString&) { refreshAuthFields(); });
|
||||
|
||||
form->addRow(QStringLiteral("Name"), m_nameInput);
|
||||
form->addRow(QStringLiteral("Host"), m_hostInput);
|
||||
form->addRow(QStringLiteral("Port"), m_portInput);
|
||||
form->addRow(QStringLiteral("Username"), m_usernameInput);
|
||||
form->addRow(QStringLiteral("Domain"), m_domainInput);
|
||||
form->addRow(QStringLiteral("Tags"), m_tagsInput);
|
||||
form->addRow(QStringLiteral("Protocol"), m_protocolInput);
|
||||
form->addRow(QStringLiteral("Auth Mode"), m_authModeInput);
|
||||
form->addRow(QStringLiteral("Private Key"), privateKeyRow);
|
||||
form->addRow(QStringLiteral("Known Hosts"), m_knownHostsPolicyInput);
|
||||
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
|
||||
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
|
||||
|
||||
auto* note = new QLabel(
|
||||
QStringLiteral("Passwords are requested at connect time and are not stored."),
|
||||
this);
|
||||
note->setWordWrap(true);
|
||||
m_protocolHint->setWordWrap(true);
|
||||
m_folderHint->setWordWrap(true);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(m_protocolHint);
|
||||
layout->addWidget(m_folderHint);
|
||||
layout->addWidget(note);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
refreshAuthFields();
|
||||
}
|
||||
|
||||
void ProfileDialog::setDialogTitle(const QString& title)
|
||||
@@ -73,12 +167,22 @@ void ProfileDialog::setDialogTitle(const QString& title)
|
||||
setWindowTitle(title);
|
||||
}
|
||||
|
||||
void ProfileDialog::setDefaultFolderPath(const QString& folderPath)
|
||||
{
|
||||
m_defaultFolderPath = folderPath.trimmed();
|
||||
refreshAuthFields();
|
||||
}
|
||||
|
||||
void ProfileDialog::setProfile(const Profile& profile)
|
||||
{
|
||||
m_nameInput->setText(profile.name);
|
||||
m_hostInput->setText(profile.host);
|
||||
m_portInput->setValue(profile.port > 0 ? profile.port : 22);
|
||||
m_usernameInput->setText(profile.username);
|
||||
m_domainInput->setText(profile.domain);
|
||||
m_defaultFolderPath = profile.folderPath.trimmed();
|
||||
m_tagsInput->setText(profile.tags);
|
||||
m_privateKeyPathInput->setText(profile.privateKeyPath);
|
||||
|
||||
const int protocolIndex = m_protocolInput->findText(profile.protocol);
|
||||
{
|
||||
@@ -89,18 +193,47 @@ void ProfileDialog::setProfile(const Profile& profile)
|
||||
|
||||
const int authModeIndex = m_authModeInput->findText(profile.authMode);
|
||||
m_authModeInput->setCurrentIndex(authModeIndex >= 0 ? authModeIndex : 0);
|
||||
|
||||
const int knownHostsIndex = m_knownHostsPolicyInput->findText(profile.knownHostsPolicy);
|
||||
m_knownHostsPolicyInput->setCurrentIndex(knownHostsIndex >= 0 ? knownHostsIndex : 0);
|
||||
const int securityModeIndex = m_rdpSecurityModeInput->findText(profile.rdpSecurityMode);
|
||||
m_rdpSecurityModeInput->setCurrentIndex(securityModeIndex >= 0 ? securityModeIndex : 0);
|
||||
const int performanceProfileIndex =
|
||||
m_rdpPerformanceProfileInput->findText(profile.rdpPerformanceProfile);
|
||||
m_rdpPerformanceProfileInput->setCurrentIndex(performanceProfileIndex >= 0 ? performanceProfileIndex
|
||||
: 0);
|
||||
|
||||
refreshAuthFields();
|
||||
}
|
||||
|
||||
Profile ProfileDialog::profile() const
|
||||
{
|
||||
Profile profile;
|
||||
const QString protocol = normalizedProtocol(m_protocolInput->currentText());
|
||||
const QString authMode = normalizedAuthMode(protocol, m_authModeInput->currentText());
|
||||
|
||||
profile.id = -1;
|
||||
profile.name = m_nameInput->text().trimmed();
|
||||
profile.host = m_hostInput->text().trimmed();
|
||||
profile.port = m_portInput->value();
|
||||
profile.username = m_usernameInput->text().trimmed();
|
||||
profile.protocol = m_protocolInput->currentText();
|
||||
profile.authMode = m_authModeInput->currentText();
|
||||
profile.domain = protocol == QStringLiteral("RDP") ? m_domainInput->text().trimmed() : QString();
|
||||
profile.folderPath = m_defaultFolderPath.trimmed();
|
||||
profile.tags = m_tagsInput->text().trimmed();
|
||||
profile.protocol = protocol;
|
||||
profile.authMode = authMode;
|
||||
profile.privateKeyPath = (protocol == QStringLiteral("SSH")
|
||||
&& authMode == QStringLiteral("Private Key"))
|
||||
? m_privateKeyPathInput->text().trimmed()
|
||||
: QString();
|
||||
profile.knownHostsPolicy = protocol == QStringLiteral("SSH") ? m_knownHostsPolicyInput->currentText()
|
||||
: QStringLiteral("Ask");
|
||||
profile.rdpSecurityMode = protocol == QStringLiteral("RDP")
|
||||
? m_rdpSecurityModeInput->currentText()
|
||||
: QStringLiteral("Negotiate");
|
||||
profile.rdpPerformanceProfile = protocol == QStringLiteral("RDP")
|
||||
? m_rdpPerformanceProfileInput->currentText()
|
||||
: QStringLiteral("Balanced");
|
||||
return profile;
|
||||
}
|
||||
|
||||
@@ -120,5 +253,73 @@ void ProfileDialog::accept()
|
||||
return;
|
||||
}
|
||||
|
||||
const QString protocol = m_protocolInput->currentText();
|
||||
if ((protocol == QStringLiteral("SSH") || protocol == QStringLiteral("RDP"))
|
||||
&& m_usernameInput->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Validation Error"),
|
||||
QStringLiteral("Username is required for %1 profiles.").arg(protocol));
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocol == QStringLiteral("SSH")
|
||||
&& m_authModeInput->currentText() == QStringLiteral("Private Key")) {
|
||||
const QString privateKeyPath = m_privateKeyPathInput->text().trimmed();
|
||||
if (privateKeyPath.isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Validation Error"),
|
||||
QStringLiteral("Private key path is required for SSH private key authentication."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QFileInfo::exists(privateKeyPath)) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Validation Error"),
|
||||
QStringLiteral("Private key file does not exist: %1").arg(privateKeyPath));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
void ProfileDialog::refreshAuthFields()
|
||||
{
|
||||
const QString protocol = normalizedProtocol(m_protocolInput->currentText());
|
||||
const bool isSsh = protocol == QStringLiteral("SSH");
|
||||
const bool isRdp = protocol == QStringLiteral("RDP");
|
||||
const bool isVnc = protocol == QStringLiteral("VNC");
|
||||
|
||||
const QString normalizedMode = normalizedAuthMode(protocol, m_authModeInput->currentText());
|
||||
if (normalizedMode != m_authModeInput->currentText()) {
|
||||
const QSignalBlocker blocker(m_authModeInput);
|
||||
m_authModeInput->setCurrentText(normalizedMode);
|
||||
}
|
||||
const bool isPrivateKey = normalizedMode == QStringLiteral("Private Key");
|
||||
|
||||
m_authModeInput->setEnabled(isSsh);
|
||||
m_privateKeyPathInput->setEnabled(isSsh && isPrivateKey);
|
||||
m_browsePrivateKeyButton->setEnabled(isSsh && isPrivateKey);
|
||||
m_knownHostsPolicyInput->setEnabled(isSsh);
|
||||
m_domainInput->setEnabled(isRdp);
|
||||
m_rdpSecurityModeInput->setEnabled(isRdp);
|
||||
m_rdpPerformanceProfileInput->setEnabled(isRdp);
|
||||
|
||||
if (isSsh) {
|
||||
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
|
||||
m_protocolHint->setText(
|
||||
QStringLiteral("SSH: username is required. Choose Password or Private Key auth."));
|
||||
} else if (isRdp) {
|
||||
m_usernameInput->setPlaceholderText(QStringLiteral("Administrator"));
|
||||
m_protocolHint->setText(
|
||||
QStringLiteral("RDP: username and password are required. Domain is optional."));
|
||||
} else if (isVnc) {
|
||||
m_usernameInput->setPlaceholderText(QStringLiteral("optional"));
|
||||
m_protocolHint->setText(
|
||||
QStringLiteral("VNC: host and port are required. Username/domain are optional and ignored by most servers."));
|
||||
}
|
||||
|
||||
m_folderHint->setText(m_defaultFolderPath.isEmpty()
|
||||
? QStringLiteral("Target folder: root")
|
||||
: QStringLiteral("Target folder: %1").arg(m_defaultFolderPath));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
#include <QDialog>
|
||||
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QSpinBox;
|
||||
|
||||
class ProfileDialog : public QDialog
|
||||
@@ -17,6 +19,7 @@ public:
|
||||
explicit ProfileDialog(QWidget* parent = nullptr);
|
||||
|
||||
void setDialogTitle(const QString& title);
|
||||
void setDefaultFolderPath(const QString& folderPath);
|
||||
void setProfile(const Profile& profile);
|
||||
Profile profile() const;
|
||||
|
||||
@@ -28,8 +31,20 @@ private:
|
||||
QLineEdit* m_hostInput;
|
||||
QSpinBox* m_portInput;
|
||||
QLineEdit* m_usernameInput;
|
||||
QLineEdit* m_domainInput;
|
||||
QLineEdit* m_tagsInput;
|
||||
QComboBox* m_protocolInput;
|
||||
QComboBox* m_authModeInput;
|
||||
QLineEdit* m_privateKeyPathInput;
|
||||
QPushButton* m_browsePrivateKeyButton;
|
||||
QComboBox* m_knownHostsPolicyInput;
|
||||
QComboBox* m_rdpSecurityModeInput;
|
||||
QComboBox* m_rdpPerformanceProfileInput;
|
||||
QLabel* m_protocolHint;
|
||||
QLabel* m_folderHint;
|
||||
QString m_defaultFolderPath;
|
||||
|
||||
void refreshAuthFields();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QSqlQuery>
|
||||
#include <QStandardPaths>
|
||||
#include <QVariant>
|
||||
#include <QStringList>
|
||||
|
||||
namespace {
|
||||
QString buildDatabasePath()
|
||||
@@ -22,14 +23,160 @@ QString buildDatabasePath()
|
||||
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite"));
|
||||
}
|
||||
|
||||
QString normalizedRdpSecurityMode(const QString& value)
|
||||
{
|
||||
const QString mode = value.trimmed();
|
||||
if (mode.compare(QStringLiteral("NLA"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("NLA");
|
||||
}
|
||||
if (mode.compare(QStringLiteral("TLS"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("TLS");
|
||||
}
|
||||
if (mode.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("RDP");
|
||||
}
|
||||
return QStringLiteral("Negotiate");
|
||||
}
|
||||
|
||||
QString normalizedRdpPerformanceProfile(const QString& value)
|
||||
{
|
||||
const QString profile = value.trimmed();
|
||||
if (profile.compare(QStringLiteral("Best Quality"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Best Quality");
|
||||
}
|
||||
if (profile.compare(QStringLiteral("Best Performance"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Best Performance");
|
||||
}
|
||||
if (profile.compare(QStringLiteral("Auto Detect"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Auto Detect");
|
||||
}
|
||||
return QStringLiteral("Balanced");
|
||||
}
|
||||
|
||||
QString normalizedProtocol(const QString& value)
|
||||
{
|
||||
const QString protocol = value.trimmed();
|
||||
if (protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("RDP");
|
||||
}
|
||||
if (protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("VNC");
|
||||
}
|
||||
return QStringLiteral("SSH");
|
||||
}
|
||||
|
||||
QString normalizedAuthMode(const QString& protocol, const QString& value)
|
||||
{
|
||||
if (protocol != QStringLiteral("SSH")) {
|
||||
return QStringLiteral("Password");
|
||||
}
|
||||
|
||||
const QString authMode = value.trimmed();
|
||||
if (authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Private Key");
|
||||
}
|
||||
return QStringLiteral("Password");
|
||||
}
|
||||
|
||||
QString normalizedKnownHostsPolicy(const QString& value)
|
||||
{
|
||||
const QString policy = value.trimmed();
|
||||
if (policy.compare(QStringLiteral("Strict"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Strict");
|
||||
}
|
||||
if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Accept New");
|
||||
}
|
||||
if (policy.compare(QStringLiteral("Ignore"), Qt::CaseInsensitive) == 0) {
|
||||
return QStringLiteral("Ignore");
|
||||
}
|
||||
return QStringLiteral("Ask");
|
||||
}
|
||||
|
||||
QString normalizedFolderPath(const QString& value)
|
||||
{
|
||||
QString path = value.trimmed();
|
||||
path.replace(QChar::fromLatin1('\\'), QChar::fromLatin1('/'));
|
||||
|
||||
const QStringList rawParts = path.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
|
||||
QStringList normalized;
|
||||
for (const QString& rawPart : rawParts) {
|
||||
const QString part = rawPart.trimmed();
|
||||
if (!part.isEmpty()) {
|
||||
normalized.push_back(part);
|
||||
}
|
||||
}
|
||||
|
||||
return normalized.join(QStringLiteral("/"));
|
||||
}
|
||||
|
||||
QString normalizedTags(const QString& value)
|
||||
{
|
||||
const QStringList rawTokens = value.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
|
||||
QStringList normalized;
|
||||
QSet<QString> dedupe;
|
||||
for (const QString& token : rawTokens) {
|
||||
const QString trimmed = token.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString key = trimmed.toLower();
|
||||
if (dedupe.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
dedupe.insert(key);
|
||||
normalized.push_back(trimmed);
|
||||
}
|
||||
|
||||
return normalized.join(QStringLiteral(", "));
|
||||
}
|
||||
|
||||
QString orderByClause(ProfileSortOrder sortOrder)
|
||||
{
|
||||
switch (sortOrder) {
|
||||
case ProfileSortOrder::ProtocolAsc:
|
||||
return QStringLiteral("ORDER BY lower(protocol) ASC, lower(name) ASC, id ASC");
|
||||
case ProfileSortOrder::HostAsc:
|
||||
return QStringLiteral("ORDER BY lower(host) ASC, lower(name) ASC, id ASC");
|
||||
case ProfileSortOrder::NameAsc:
|
||||
default:
|
||||
return QStringLiteral("ORDER BY lower(name) ASC, id ASC");
|
||||
}
|
||||
}
|
||||
|
||||
QString nonNullTrimmed(const QString& value)
|
||||
{
|
||||
const QString trimmed = value.trimmed();
|
||||
return trimmed.isNull() ? QStringLiteral("") : trimmed;
|
||||
}
|
||||
|
||||
void bindProfileFields(QSqlQuery& query, const Profile& profile)
|
||||
{
|
||||
query.addBindValue(profile.name.trimmed());
|
||||
query.addBindValue(profile.host.trimmed());
|
||||
const QString protocol = normalizedProtocol(profile.protocol);
|
||||
const QString authMode = normalizedAuthMode(protocol, profile.authMode);
|
||||
const bool isSsh = protocol == QStringLiteral("SSH");
|
||||
const bool isRdp = protocol == QStringLiteral("RDP");
|
||||
|
||||
query.addBindValue(nonNullTrimmed(profile.name));
|
||||
query.addBindValue(nonNullTrimmed(profile.host));
|
||||
query.addBindValue(profile.port);
|
||||
query.addBindValue(profile.username.trimmed());
|
||||
query.addBindValue(profile.protocol.trimmed());
|
||||
query.addBindValue(profile.authMode.trimmed());
|
||||
query.addBindValue(nonNullTrimmed(profile.username));
|
||||
query.addBindValue(isRdp ? nonNullTrimmed(profile.domain) : QStringLiteral(""));
|
||||
query.addBindValue(nonNullTrimmed(normalizedFolderPath(profile.folderPath)));
|
||||
query.addBindValue(protocol);
|
||||
query.addBindValue(authMode);
|
||||
query.addBindValue((isSsh && authMode == QStringLiteral("Private Key"))
|
||||
? nonNullTrimmed(profile.privateKeyPath)
|
||||
: QStringLiteral(""));
|
||||
query.addBindValue(isSsh ? normalizedKnownHostsPolicy(profile.knownHostsPolicy)
|
||||
: QStringLiteral("Ask"));
|
||||
query.addBindValue(isRdp ? normalizedRdpSecurityMode(profile.rdpSecurityMode)
|
||||
: QStringLiteral("Negotiate"));
|
||||
query.addBindValue(isRdp ? normalizedRdpPerformanceProfile(profile.rdpPerformanceProfile)
|
||||
: QStringLiteral("Balanced"));
|
||||
query.addBindValue(normalizedTags(profile.tags));
|
||||
}
|
||||
|
||||
Profile profileFromQuery(const QSqlQuery& query)
|
||||
@@ -40,15 +187,68 @@ Profile profileFromQuery(const QSqlQuery& query)
|
||||
profile.host = query.value(2).toString();
|
||||
profile.port = query.value(3).toInt();
|
||||
profile.username = query.value(4).toString();
|
||||
profile.protocol = query.value(5).toString();
|
||||
profile.authMode = query.value(6).toString();
|
||||
profile.domain = query.value(5).toString();
|
||||
profile.folderPath = normalizedFolderPath(query.value(6).toString());
|
||||
profile.protocol = normalizedProtocol(query.value(7).toString());
|
||||
profile.authMode = normalizedAuthMode(profile.protocol, query.value(8).toString());
|
||||
profile.privateKeyPath = profile.authMode == QStringLiteral("Private Key")
|
||||
? query.value(9).toString().trimmed()
|
||||
: QString();
|
||||
profile.knownHostsPolicy = profile.protocol == QStringLiteral("SSH")
|
||||
? normalizedKnownHostsPolicy(query.value(10).toString())
|
||||
: QStringLiteral("Ask");
|
||||
profile.rdpSecurityMode = profile.protocol == QStringLiteral("RDP")
|
||||
? normalizedRdpSecurityMode(query.value(11).toString())
|
||||
: QStringLiteral("Negotiate");
|
||||
profile.rdpPerformanceProfile = profile.protocol == QStringLiteral("RDP")
|
||||
? normalizedRdpPerformanceProfile(query.value(12).toString())
|
||||
: QStringLiteral("Balanced");
|
||||
profile.tags = normalizedTags(query.value(13).toString());
|
||||
return profile;
|
||||
}
|
||||
|
||||
bool isProfileValid(const Profile& profile)
|
||||
bool isProfileValid(const Profile& profile, QString* error)
|
||||
{
|
||||
return !profile.name.trimmed().isEmpty() && !profile.host.trimmed().isEmpty()
|
||||
&& profile.port >= 1 && profile.port <= 65535;
|
||||
if (profile.name.trimmed().isEmpty()) {
|
||||
if (error != nullptr) {
|
||||
*error = QStringLiteral("Profile name is required.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (profile.host.trimmed().isEmpty()) {
|
||||
if (error != nullptr) {
|
||||
*error = QStringLiteral("Host is required.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (profile.port < 1 || profile.port > 65535) {
|
||||
if (error != nullptr) {
|
||||
*error = QStringLiteral("Port must be between 1 and 65535.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString protocol = normalizedProtocol(profile.protocol);
|
||||
if ((protocol == QStringLiteral("SSH") || protocol == QStringLiteral("RDP"))
|
||||
&& profile.username.trimmed().isEmpty()) {
|
||||
if (error != nullptr) {
|
||||
*error = QStringLiteral("Username is required for %1 profiles.").arg(protocol);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString authMode = normalizedAuthMode(protocol, profile.authMode);
|
||||
if (protocol == QStringLiteral("SSH") && authMode == QStringLiteral("Private Key")
|
||||
&& profile.privateKeyPath.trimmed().isEmpty()) {
|
||||
if (error != nullptr) {
|
||||
*error = QStringLiteral("Private key path is required for SSH private key authentication.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +280,60 @@ QString ProfileRepository::lastError() const
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const
|
||||
std::vector<QString> ProfileRepository::listFolders() const
|
||||
{
|
||||
std::vector<QString> result;
|
||||
|
||||
if (!QSqlDatabase::contains(m_connectionName)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
setLastError(QString());
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral("SELECT path FROM profile_folders ORDER BY lower(path) ASC"));
|
||||
if (!query.exec()) {
|
||||
setLastError(query.lastError().text());
|
||||
return result;
|
||||
}
|
||||
|
||||
while (query.next()) {
|
||||
const QString normalized = normalizedFolderPath(query.value(0).toString());
|
||||
if (!normalized.isEmpty()) {
|
||||
result.push_back(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ProfileRepository::createFolder(const QString& folderPath) const
|
||||
{
|
||||
if (!QSqlDatabase::contains(m_connectionName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString normalized = normalizedFolderPath(folderPath);
|
||||
if (normalized.isEmpty()) {
|
||||
setLastError(QStringLiteral("Folder path is required."));
|
||||
return false;
|
||||
}
|
||||
|
||||
setLastError(QString());
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral("INSERT OR IGNORE INTO profile_folders(path) VALUES (?)"));
|
||||
query.addBindValue(normalized);
|
||||
if (!query.exec()) {
|
||||
setLastError(query.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery,
|
||||
ProfileSortOrder sortOrder) const
|
||||
{
|
||||
std::vector<Profile> result;
|
||||
|
||||
@@ -91,20 +344,23 @@ std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery)
|
||||
setLastError(QString());
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
const QString orderBy = orderByClause(sortOrder);
|
||||
if (searchQuery.trimmed().isEmpty()) {
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT id, name, host, port, username, protocol, auth_mode "
|
||||
"FROM profiles "
|
||||
"ORDER BY lower(name) ASC, id ASC"));
|
||||
"SELECT id, name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags "
|
||||
"FROM profiles ")
|
||||
+ orderBy);
|
||||
} else {
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT id, name, host, port, username, protocol, auth_mode "
|
||||
"SELECT id, name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags "
|
||||
"FROM profiles "
|
||||
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
|
||||
"ORDER BY lower(name) ASC, id ASC"));
|
||||
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) OR lower(tags) LIKE lower(?) OR lower(folder_path) LIKE lower(?) ")
|
||||
+ orderBy);
|
||||
const QString search = QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%");
|
||||
query.addBindValue(search);
|
||||
query.addBindValue(search);
|
||||
query.addBindValue(search);
|
||||
query.addBindValue(search);
|
||||
}
|
||||
|
||||
if (!query.exec()) {
|
||||
@@ -129,7 +385,7 @@ std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT id, name, host, port, username, protocol, auth_mode "
|
||||
"SELECT id, name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags "
|
||||
"FROM profiles WHERE id = ?"));
|
||||
query.addBindValue(id);
|
||||
|
||||
@@ -153,15 +409,16 @@ std::optional<Profile> ProfileRepository::createProfile(const Profile& profile)
|
||||
|
||||
setLastError(QString());
|
||||
|
||||
if (!isProfileValid(profile)) {
|
||||
setLastError(QStringLiteral("Name, host, and a valid port are required."));
|
||||
QString validationError;
|
||||
if (!isProfileValid(profile, &validationError)) {
|
||||
setLastError(validationError);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral(
|
||||
"INSERT INTO profiles(name, host, port, username, protocol, auth_mode) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)"));
|
||||
"INSERT INTO profiles(name, host, port, username, domain, folder_path, protocol, auth_mode, private_key_path, known_hosts_policy, rdp_security_mode, rdp_performance_profile, tags) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
|
||||
bindProfileFields(query, profile);
|
||||
|
||||
if (!query.exec()) {
|
||||
@@ -182,15 +439,17 @@ bool ProfileRepository::updateProfile(const Profile& profile) const
|
||||
|
||||
setLastError(QString());
|
||||
|
||||
if (profile.id < 0 || !isProfileValid(profile)) {
|
||||
setLastError(QStringLiteral("Invalid profile data."));
|
||||
QString validationError;
|
||||
if (profile.id < 0 || !isProfileValid(profile, &validationError)) {
|
||||
setLastError(validationError.isEmpty() ? QStringLiteral("Invalid profile data.")
|
||||
: validationError);
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_connectionName));
|
||||
query.prepare(QStringLiteral(
|
||||
"UPDATE profiles "
|
||||
"SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ? "
|
||||
"SET name = ?, host = ?, port = ?, username = ?, domain = ?, folder_path = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ?, rdp_security_mode = ?, rdp_performance_profile = ?, tags = ? "
|
||||
"WHERE id = ?"));
|
||||
bindProfileFields(query, profile);
|
||||
query.addBindValue(profile.id);
|
||||
@@ -241,8 +500,15 @@ bool ProfileRepository::initializeDatabase()
|
||||
"host TEXT NOT NULL DEFAULT '',"
|
||||
"port INTEGER NOT NULL DEFAULT 22,"
|
||||
"username TEXT NOT NULL DEFAULT '',"
|
||||
"domain TEXT NOT NULL DEFAULT '',"
|
||||
"folder_path TEXT NOT NULL DEFAULT '',"
|
||||
"protocol TEXT NOT NULL DEFAULT 'SSH',"
|
||||
"auth_mode TEXT NOT NULL DEFAULT 'Password'"
|
||||
"auth_mode TEXT NOT NULL DEFAULT 'Password',"
|
||||
"private_key_path TEXT NOT NULL DEFAULT '',"
|
||||
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask',"
|
||||
"rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate',"
|
||||
"rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced',"
|
||||
"tags TEXT NOT NULL DEFAULT ''"
|
||||
")"));
|
||||
|
||||
if (!created) {
|
||||
@@ -250,6 +516,15 @@ bool ProfileRepository::initializeDatabase()
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool foldersCreated = query.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS profile_folders ("
|
||||
"path TEXT PRIMARY KEY NOT NULL"
|
||||
")"));
|
||||
if (!foldersCreated) {
|
||||
m_initError = query.lastError().text();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ensureProfileSchema()) {
|
||||
m_initError = m_lastError;
|
||||
return false;
|
||||
@@ -285,8 +560,15 @@ bool ProfileRepository::ensureProfileSchema() const
|
||||
{QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")},
|
||||
{QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("domain"), QStringLiteral("ALTER TABLE profiles ADD COLUMN domain TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("folder_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN folder_path TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")},
|
||||
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")}};
|
||||
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")},
|
||||
{QStringLiteral("private_key_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN private_key_path TEXT NOT NULL DEFAULT ''")},
|
||||
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")},
|
||||
{QStringLiteral("rdp_security_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_security_mode TEXT NOT NULL DEFAULT 'Negotiate'")},
|
||||
{QStringLiteral("rdp_performance_profile"), QStringLiteral("ALTER TABLE profiles ADD COLUMN rdp_performance_profile TEXT NOT NULL DEFAULT 'Balanced'")},
|
||||
{QStringLiteral("tags"), QStringLiteral("ALTER TABLE profiles ADD COLUMN tags TEXT NOT NULL DEFAULT ''")}};
|
||||
|
||||
for (const ColumnDef& column : required) {
|
||||
if (columns.contains(column.name)) {
|
||||
@@ -300,6 +582,15 @@ bool ProfileRepository::ensureProfileSchema() const
|
||||
}
|
||||
}
|
||||
|
||||
QSqlQuery ensureFolders(QSqlDatabase::database(m_connectionName));
|
||||
if (!ensureFolders.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS profile_folders ("
|
||||
"path TEXT PRIMARY KEY NOT NULL"
|
||||
")"))) {
|
||||
setLastError(ensureFolders.lastError().text());
|
||||
return false;
|
||||
}
|
||||
|
||||
setLastError(QString());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,21 @@ struct Profile
|
||||
QString host;
|
||||
int port = 22;
|
||||
QString username;
|
||||
QString domain;
|
||||
QString folderPath;
|
||||
QString protocol = QStringLiteral("SSH");
|
||||
QString authMode = QStringLiteral("Password");
|
||||
QString privateKeyPath;
|
||||
QString knownHostsPolicy = QStringLiteral("Ask");
|
||||
QString rdpSecurityMode = QStringLiteral("Negotiate");
|
||||
QString rdpPerformanceProfile = QStringLiteral("Balanced");
|
||||
QString tags;
|
||||
};
|
||||
|
||||
enum class ProfileSortOrder {
|
||||
NameAsc,
|
||||
ProtocolAsc,
|
||||
HostAsc,
|
||||
};
|
||||
|
||||
class ProfileRepository
|
||||
@@ -27,7 +40,10 @@ public:
|
||||
QString initError() const;
|
||||
QString lastError() const;
|
||||
|
||||
std::vector<Profile> listProfiles(const QString& searchQuery = QString()) const;
|
||||
std::vector<Profile> listProfiles(const QString& searchQuery = QString(),
|
||||
ProfileSortOrder sortOrder = ProfileSortOrder::NameAsc) const;
|
||||
std::vector<QString> listFolders() const;
|
||||
bool createFolder(const QString& folderPath) const;
|
||||
std::optional<Profile> getProfile(qint64 id) const;
|
||||
std::optional<Profile> createProfile(const Profile& profile) const;
|
||||
bool updateProfile(const Profile& profile) const;
|
||||
|
||||
11
src/profiles_tree_widget.cpp
Normal file
11
src/profiles_tree_widget.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "profiles_tree_widget.h"
|
||||
|
||||
#include <QDropEvent>
|
||||
|
||||
ProfilesTreeWidget::ProfilesTreeWidget(QWidget* parent) : QTreeWidget(parent) {}
|
||||
|
||||
void ProfilesTreeWidget::dropEvent(QDropEvent* event)
|
||||
{
|
||||
QTreeWidget::dropEvent(event);
|
||||
emit itemsDropped();
|
||||
}
|
||||
22
src/profiles_tree_widget.h
Normal file
22
src/profiles_tree_widget.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef ORBITHUB_PROFILES_TREE_WIDGET_H
|
||||
#define ORBITHUB_PROFILES_TREE_WIDGET_H
|
||||
|
||||
#include <QTreeWidget>
|
||||
|
||||
class QDropEvent;
|
||||
|
||||
class ProfilesTreeWidget : public QTreeWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ProfilesTreeWidget(QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void itemsDropped();
|
||||
|
||||
protected:
|
||||
void dropEvent(QDropEvent* event) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,42 +1,112 @@
|
||||
#include "profiles_window.h"
|
||||
|
||||
#include "about_dialog.h"
|
||||
#include "profile_dialog.h"
|
||||
#include "profile_repository.h"
|
||||
#include "profiles_tree_widget.h"
|
||||
#include "session_window.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <QAction>
|
||||
#include <QAbstractItemView>
|
||||
#include <QComboBox>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#include <QInputDialog>
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QSet>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
#include <QApplication>
|
||||
#include <QStringList>
|
||||
#include <QStyle>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVariant>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
namespace {
|
||||
QString formatProfileListItem(const Profile& profile)
|
||||
constexpr int kProfileIdRole = Qt::UserRole;
|
||||
constexpr int kFolderPathRole = Qt::UserRole + 1;
|
||||
|
||||
QString normalizeFolderPathForView(const QString& value)
|
||||
{
|
||||
return QStringLiteral("%1 [%2 %3:%4]")
|
||||
.arg(profile.name, profile.protocol, profile.host, QString::number(profile.port));
|
||||
QString path = value.trimmed();
|
||||
path.replace(QChar::fromLatin1('\\'), QChar::fromLatin1('/'));
|
||||
|
||||
const QStringList rawParts = path.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
|
||||
QStringList normalized;
|
||||
for (const QString& rawPart : rawParts) {
|
||||
const QString part = rawPart.trimmed();
|
||||
if (!part.isEmpty()) {
|
||||
normalized.push_back(part);
|
||||
}
|
||||
}
|
||||
|
||||
return normalized.join(QStringLiteral("/"));
|
||||
}
|
||||
|
||||
QString joinFolderPath(const QString& parentFolder, const QString& childName)
|
||||
{
|
||||
const QString parent = normalizeFolderPathForView(parentFolder);
|
||||
const QString child = normalizeFolderPathForView(childName);
|
||||
if (child.isEmpty()) {
|
||||
return parent;
|
||||
}
|
||||
if (parent.isEmpty()) {
|
||||
return child;
|
||||
}
|
||||
return QStringLiteral("%1/%2").arg(parent, child);
|
||||
}
|
||||
|
||||
QStringList splitFolderPath(const QString& folderPath)
|
||||
{
|
||||
const QString normalized = normalizeFolderPathForView(folderPath);
|
||||
if (normalized.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
return normalized.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
bool profileHasTag(const Profile& profile, const QString& requestedTag)
|
||||
{
|
||||
const QString needle = requestedTag.trimmed();
|
||||
if (needle.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QStringList tags = profile.tags.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
|
||||
for (const QString& tag : tags) {
|
||||
if (tag.trimmed().compare(needle, Qt::CaseInsensitive) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ProfilesWindow::ProfilesWindow(QWidget* parent)
|
||||
: QMainWindow(parent),
|
||||
m_searchBox(nullptr),
|
||||
m_profilesList(nullptr),
|
||||
m_viewModeBox(nullptr),
|
||||
m_sortBox(nullptr),
|
||||
m_protocolFilterBox(nullptr),
|
||||
m_tagFilterBox(nullptr),
|
||||
m_profilesTree(nullptr),
|
||||
m_newButton(nullptr),
|
||||
m_editButton(nullptr),
|
||||
m_deleteButton(nullptr),
|
||||
m_repository(std::make_unique<ProfileRepository>())
|
||||
{
|
||||
setWindowTitle(QStringLiteral("OrbitHub Profiles"));
|
||||
resize(640, 620);
|
||||
resize(860, 640);
|
||||
setWindowIcon(QApplication::windowIcon());
|
||||
|
||||
setupUi();
|
||||
|
||||
@@ -49,10 +119,11 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
|
||||
m_editButton->setEnabled(false);
|
||||
m_deleteButton->setEnabled(false);
|
||||
m_searchBox->setEnabled(false);
|
||||
m_profilesList->setEnabled(false);
|
||||
m_profilesTree->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
loadUiPreferences();
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
@@ -65,10 +136,47 @@ void ProfilesWindow::setupUi()
|
||||
|
||||
auto* searchLabel = new QLabel(QStringLiteral("Search"), central);
|
||||
m_searchBox = new QLineEdit(central);
|
||||
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name or host..."));
|
||||
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name, host, folder, or tags..."));
|
||||
|
||||
m_profilesList = new QListWidget(central);
|
||||
m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
auto* viewModeLabel = new QLabel(QStringLiteral("View"), central);
|
||||
m_viewModeBox = new QComboBox(central);
|
||||
m_viewModeBox->addItem(QStringLiteral("List"));
|
||||
m_viewModeBox->addItem(QStringLiteral("Folders"));
|
||||
|
||||
auto* sortLabel = new QLabel(QStringLiteral("Sort"), central);
|
||||
m_sortBox = new QComboBox(central);
|
||||
m_sortBox->addItem(QStringLiteral("Name"), static_cast<int>(ProfileSortOrder::NameAsc));
|
||||
m_sortBox->addItem(QStringLiteral("Protocol"), static_cast<int>(ProfileSortOrder::ProtocolAsc));
|
||||
m_sortBox->addItem(QStringLiteral("Host"), static_cast<int>(ProfileSortOrder::HostAsc));
|
||||
|
||||
auto* protocolFilterLabel = new QLabel(QStringLiteral("Protocol"), central);
|
||||
m_protocolFilterBox = new QComboBox(central);
|
||||
m_protocolFilterBox->addItem(QStringLiteral("All"));
|
||||
m_protocolFilterBox->addItem(QStringLiteral("SSH"));
|
||||
m_protocolFilterBox->addItem(QStringLiteral("RDP"));
|
||||
m_protocolFilterBox->addItem(QStringLiteral("VNC"));
|
||||
|
||||
auto* tagFilterLabel = new QLabel(QStringLiteral("Tag"), central);
|
||||
m_tagFilterBox = new QComboBox(central);
|
||||
m_tagFilterBox->addItem(QStringLiteral("All"));
|
||||
|
||||
m_profilesTree = new ProfilesTreeWidget(central);
|
||||
m_profilesTree->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_profilesTree->setColumnCount(4);
|
||||
m_profilesTree->setHeaderLabels(
|
||||
{QStringLiteral("Name"), QStringLiteral("Protocol"), QStringLiteral("Server"), QStringLiteral("Tags")});
|
||||
m_profilesTree->setRootIsDecorated(true);
|
||||
m_profilesTree->setDragEnabled(true);
|
||||
m_profilesTree->setAcceptDrops(true);
|
||||
m_profilesTree->viewport()->setAcceptDrops(true);
|
||||
m_profilesTree->setDropIndicatorShown(true);
|
||||
m_profilesTree->setDragDropMode(QAbstractItemView::InternalMove);
|
||||
m_profilesTree->setDefaultDropAction(Qt::MoveAction);
|
||||
m_profilesTree->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_profilesTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
m_profilesTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_profilesTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_profilesTree->header()->setSectionResizeMode(3, QHeaderView::Stretch);
|
||||
|
||||
auto* buttonRow = new QHBoxLayout();
|
||||
m_newButton = new QPushButton(QStringLiteral("New"), central);
|
||||
@@ -80,34 +188,117 @@ void ProfilesWindow::setupUi()
|
||||
buttonRow->addWidget(m_deleteButton);
|
||||
buttonRow->addStretch();
|
||||
|
||||
rootLayout->addWidget(searchLabel);
|
||||
rootLayout->addWidget(m_searchBox);
|
||||
rootLayout->addWidget(m_profilesList, 1);
|
||||
auto* filterRow = new QHBoxLayout();
|
||||
filterRow->addWidget(searchLabel);
|
||||
filterRow->addWidget(m_searchBox, 1);
|
||||
filterRow->addWidget(viewModeLabel);
|
||||
filterRow->addWidget(m_viewModeBox);
|
||||
filterRow->addWidget(protocolFilterLabel);
|
||||
filterRow->addWidget(m_protocolFilterBox);
|
||||
filterRow->addWidget(tagFilterLabel);
|
||||
filterRow->addWidget(m_tagFilterBox);
|
||||
filterRow->addWidget(sortLabel);
|
||||
filterRow->addWidget(m_sortBox);
|
||||
|
||||
rootLayout->addLayout(filterRow);
|
||||
rootLayout->addWidget(m_profilesTree, 1);
|
||||
rootLayout->addLayout(buttonRow);
|
||||
|
||||
setCentralWidget(central);
|
||||
|
||||
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
|
||||
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
||||
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
||||
fileMenu->addSeparator();
|
||||
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
|
||||
|
||||
connect(newProfileAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() {
|
||||
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
|
||||
createProfile(folderPath);
|
||||
});
|
||||
connect(newFolderAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() {
|
||||
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
|
||||
createFolderInContext(folderPath);
|
||||
});
|
||||
connect(quitAction, &QAction::triggered, this, [this]() { close(); });
|
||||
|
||||
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
||||
QAction* aboutAction = helpMenu->addAction(QStringLiteral("About OrbitHub"));
|
||||
connect(aboutAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() {
|
||||
AboutDialog dialog(this);
|
||||
dialog.exec();
|
||||
});
|
||||
|
||||
connect(m_searchBox,
|
||||
&QLineEdit::textChanged,
|
||||
this,
|
||||
[this](const QString& text) { loadProfiles(text); });
|
||||
|
||||
connect(m_profilesList,
|
||||
&QListWidget::itemDoubleClicked,
|
||||
[this](const QString&) {
|
||||
saveUiPreferences();
|
||||
loadProfiles();
|
||||
});
|
||||
connect(m_viewModeBox,
|
||||
&QComboBox::currentIndexChanged,
|
||||
this,
|
||||
[this](QListWidgetItem* item) { openSessionForItem(item); });
|
||||
[this](int) {
|
||||
saveUiPreferences();
|
||||
loadProfiles();
|
||||
});
|
||||
connect(m_sortBox,
|
||||
&QComboBox::currentIndexChanged,
|
||||
this,
|
||||
[this](int) {
|
||||
saveUiPreferences();
|
||||
loadProfiles();
|
||||
});
|
||||
connect(m_protocolFilterBox,
|
||||
&QComboBox::currentIndexChanged,
|
||||
this,
|
||||
[this](int) {
|
||||
saveUiPreferences();
|
||||
loadProfiles();
|
||||
});
|
||||
connect(m_tagFilterBox,
|
||||
&QComboBox::currentIndexChanged,
|
||||
this,
|
||||
[this](int) {
|
||||
saveUiPreferences();
|
||||
loadProfiles();
|
||||
});
|
||||
|
||||
connect(m_profilesTree,
|
||||
&QTreeWidget::itemDoubleClicked,
|
||||
this,
|
||||
[this](QTreeWidgetItem* item, int) { openSessionForItem(item); });
|
||||
connect(m_profilesTree,
|
||||
&QWidget::customContextMenuRequested,
|
||||
this,
|
||||
[this](const QPoint& pos) { showTreeContextMenu(pos); });
|
||||
connect(m_profilesTree,
|
||||
&ProfilesTreeWidget::itemsDropped,
|
||||
this,
|
||||
[this]() { persistFolderAssignmentsFromTree(); });
|
||||
|
||||
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
|
||||
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
|
||||
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
|
||||
}
|
||||
|
||||
void ProfilesWindow::loadProfiles(const QString& query)
|
||||
void ProfilesWindow::loadProfiles()
|
||||
{
|
||||
m_profilesList->clear();
|
||||
m_profilesTree->clear();
|
||||
m_profileCache.clear();
|
||||
|
||||
const std::vector<Profile> profiles = m_repository->listProfiles(query);
|
||||
const QString query = m_searchBox == nullptr ? QString() : m_searchBox->text();
|
||||
const std::vector<Profile> profiles = m_repository->listProfiles(query, selectedSortOrder());
|
||||
if (!m_repository->lastError().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Load Profiles"),
|
||||
@@ -116,27 +307,522 @@ void ProfilesWindow::loadProfiles(const QString& query)
|
||||
return;
|
||||
}
|
||||
|
||||
updateTagFilterOptions(profiles);
|
||||
|
||||
const QString protocolFilter = selectedProtocolFilter();
|
||||
const QString tagFilter = selectedTagFilter();
|
||||
const bool folderView = isFolderViewEnabled();
|
||||
|
||||
m_profilesTree->setDragEnabled(folderView);
|
||||
m_profilesTree->setAcceptDrops(folderView);
|
||||
m_profilesTree->viewport()->setAcceptDrops(folderView);
|
||||
m_profilesTree->setDropIndicatorShown(folderView);
|
||||
m_profilesTree->setDragDropMode(folderView ? QAbstractItemView::InternalMove
|
||||
: QAbstractItemView::NoDragDrop);
|
||||
|
||||
std::vector<Profile> filteredProfiles;
|
||||
filteredProfiles.reserve(profiles.size());
|
||||
for (const Profile& profile : profiles) {
|
||||
auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList);
|
||||
item->setData(Qt::UserRole, QVariant::fromValue(profile.id));
|
||||
item->setToolTip(QStringLiteral("%1://%2@%3:%4\nAuth: %5")
|
||||
if (!protocolFilter.isEmpty()
|
||||
&& profile.protocol.compare(protocolFilter, Qt::CaseInsensitive) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (!tagFilter.isEmpty() && !profileHasTag(profile, tagFilter)) {
|
||||
continue;
|
||||
}
|
||||
filteredProfiles.push_back(profile);
|
||||
}
|
||||
|
||||
if (folderView) {
|
||||
std::map<QString, QTreeWidgetItem*> folderNodes;
|
||||
const std::vector<QString> explicitFolders = m_repository->listFolders();
|
||||
for (const QString& folderPath : explicitFolders) {
|
||||
if (query.trimmed().isEmpty()
|
||||
|| folderPath.contains(query.trimmed(), Qt::CaseInsensitive)) {
|
||||
upsertFolderNode(splitFolderPath(folderPath), folderNodes);
|
||||
}
|
||||
}
|
||||
|
||||
for (const Profile& profile : filteredProfiles) {
|
||||
QTreeWidgetItem* parent = nullptr;
|
||||
const QStringList folderParts = splitFolderPath(profile.folderPath);
|
||||
if (!folderParts.isEmpty()) {
|
||||
parent = upsertFolderNode(folderParts, folderNodes);
|
||||
}
|
||||
addProfileNode(parent, profile);
|
||||
}
|
||||
m_profilesTree->expandAll();
|
||||
} else {
|
||||
for (const Profile& profile : filteredProfiles) {
|
||||
addProfileNode(nullptr, profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProfileSortOrder ProfilesWindow::selectedSortOrder() const
|
||||
{
|
||||
if (m_sortBox == nullptr) {
|
||||
return ProfileSortOrder::NameAsc;
|
||||
}
|
||||
|
||||
const QVariant value = m_sortBox->currentData();
|
||||
if (!value.isValid()) {
|
||||
return ProfileSortOrder::NameAsc;
|
||||
}
|
||||
|
||||
const int orderValue = value.toInt();
|
||||
switch (static_cast<ProfileSortOrder>(orderValue)) {
|
||||
case ProfileSortOrder::ProtocolAsc:
|
||||
return ProfileSortOrder::ProtocolAsc;
|
||||
case ProfileSortOrder::HostAsc:
|
||||
return ProfileSortOrder::HostAsc;
|
||||
case ProfileSortOrder::NameAsc:
|
||||
default:
|
||||
return ProfileSortOrder::NameAsc;
|
||||
}
|
||||
}
|
||||
|
||||
bool ProfilesWindow::isFolderViewEnabled() const
|
||||
{
|
||||
if (m_viewModeBox == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return m_viewModeBox->currentText().compare(QStringLiteral("Folders"), Qt::CaseInsensitive)
|
||||
== 0;
|
||||
}
|
||||
|
||||
QString ProfilesWindow::selectedProtocolFilter() const
|
||||
{
|
||||
if (m_protocolFilterBox == nullptr) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QString selected = m_protocolFilterBox->currentText().trimmed();
|
||||
if (selected.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0) {
|
||||
return QString();
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
QString ProfilesWindow::selectedTagFilter() const
|
||||
{
|
||||
if (m_tagFilterBox == nullptr) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QString selected = m_tagFilterBox->currentText().trimmed();
|
||||
if (selected.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0) {
|
||||
return QString();
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
void ProfilesWindow::updateTagFilterOptions(const std::vector<Profile>& profiles)
|
||||
{
|
||||
if (m_tagFilterBox == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString previousSelection = m_tagFilterBox->currentText().trimmed();
|
||||
QString desiredSelection = previousSelection;
|
||||
if (!m_pendingTagFilterPreference.trimmed().isEmpty()) {
|
||||
desiredSelection = m_pendingTagFilterPreference.trimmed();
|
||||
}
|
||||
QSet<QString> seen;
|
||||
QStringList tags;
|
||||
for (const Profile& profile : profiles) {
|
||||
const QStringList splitTags =
|
||||
profile.tags.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
|
||||
for (const QString& rawTag : splitTags) {
|
||||
const QString tag = rawTag.trimmed();
|
||||
if (tag.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const QString dedupeKey = tag.toLower();
|
||||
if (seen.contains(dedupeKey)) {
|
||||
continue;
|
||||
}
|
||||
seen.insert(dedupeKey);
|
||||
tags.push_back(tag);
|
||||
}
|
||||
}
|
||||
tags.sort(Qt::CaseInsensitive);
|
||||
|
||||
const QSignalBlocker blocker(m_tagFilterBox);
|
||||
m_tagFilterBox->clear();
|
||||
m_tagFilterBox->addItem(QStringLiteral("All"));
|
||||
for (const QString& tag : tags) {
|
||||
m_tagFilterBox->addItem(tag);
|
||||
}
|
||||
|
||||
int restoredIndex = m_tagFilterBox->findText(desiredSelection, Qt::MatchFixedString);
|
||||
if (restoredIndex < 0) {
|
||||
restoredIndex = 0;
|
||||
}
|
||||
m_tagFilterBox->setCurrentIndex(restoredIndex);
|
||||
m_pendingTagFilterPreference.clear();
|
||||
}
|
||||
|
||||
QTreeWidgetItem* ProfilesWindow::upsertFolderNode(const QStringList& folderParts,
|
||||
std::map<QString, QTreeWidgetItem*>& folderNodes)
|
||||
{
|
||||
QString currentPath;
|
||||
QTreeWidgetItem* parent = nullptr;
|
||||
|
||||
for (const QString& rawPart : folderParts) {
|
||||
const QString part = rawPart.trimmed();
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
currentPath = currentPath.isEmpty() ? part
|
||||
: QStringLiteral("%1/%2").arg(currentPath, part);
|
||||
const auto existing = folderNodes.find(currentPath);
|
||||
if (existing != folderNodes.end()) {
|
||||
parent = existing->second;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* folderItem = parent == nullptr ? new QTreeWidgetItem(m_profilesTree)
|
||||
: new QTreeWidgetItem(parent);
|
||||
folderItem->setText(0, part);
|
||||
folderItem->setIcon(0, style()->standardIcon(QStyle::SP_DirIcon));
|
||||
folderItem->setData(0, kFolderPathRole, currentPath);
|
||||
folderItem->setFlags((folderItem->flags() | Qt::ItemIsDropEnabled) & ~Qt::ItemIsDragEnabled);
|
||||
folderNodes.insert_or_assign(currentPath, folderItem);
|
||||
parent = folderItem;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
void ProfilesWindow::addProfileNode(QTreeWidgetItem* parent, const Profile& profile)
|
||||
{
|
||||
auto* item = parent == nullptr ? new QTreeWidgetItem(m_profilesTree) : new QTreeWidgetItem(parent);
|
||||
item->setText(0, profile.name);
|
||||
item->setText(1, profile.protocol);
|
||||
item->setText(2, QStringLiteral("%1:%2").arg(profile.host, QString::number(profile.port)));
|
||||
item->setText(3, profile.tags);
|
||||
item->setIcon(0, style()->standardIcon(QStyle::SP_ComputerIcon));
|
||||
item->setData(0, kProfileIdRole, QVariant::fromValue(profile.id));
|
||||
item->setData(0, kFolderPathRole, normalizeFolderPathForView(profile.folderPath));
|
||||
item->setFlags((item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable) & ~Qt::ItemIsDropEnabled);
|
||||
|
||||
const QString identity = [&profile]() {
|
||||
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0
|
||||
&& !profile.domain.trimmed().isEmpty()) {
|
||||
return QStringLiteral("%1\\%2").arg(profile.domain.trimmed(),
|
||||
profile.username.trimmed().isEmpty()
|
||||
? QStringLiteral("<none>")
|
||||
: profile.username.trimmed());
|
||||
}
|
||||
return profile.username.trimmed().isEmpty() ? QStringLiteral("<none>")
|
||||
: profile.username.trimmed();
|
||||
}();
|
||||
|
||||
QString tooltip = QStringLiteral("%1://%2@%3:%4\nAuth: %5")
|
||||
.arg(profile.protocol,
|
||||
profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username,
|
||||
identity,
|
||||
profile.host,
|
||||
QString::number(profile.port),
|
||||
profile.authMode));
|
||||
profile.authMode);
|
||||
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
|
||||
tooltip += QStringLiteral("\nKnown Hosts: %1").arg(profile.knownHostsPolicy);
|
||||
} else if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
tooltip += QStringLiteral("\nRDP Security: %1\nRDP Performance: %2")
|
||||
.arg(profile.rdpSecurityMode, profile.rdpPerformanceProfile);
|
||||
}
|
||||
if (!profile.folderPath.trimmed().isEmpty()) {
|
||||
tooltip += QStringLiteral("\nFolder: %1").arg(profile.folderPath.trimmed());
|
||||
}
|
||||
if (!profile.tags.trimmed().isEmpty()) {
|
||||
tooltip += QStringLiteral("\nTags: %1").arg(profile.tags);
|
||||
}
|
||||
|
||||
item->setToolTip(0, tooltip);
|
||||
item->setToolTip(1, tooltip);
|
||||
item->setToolTip(2, tooltip);
|
||||
item->setToolTip(3, tooltip);
|
||||
|
||||
m_profileCache.insert_or_assign(profile.id, profile);
|
||||
}
|
||||
|
||||
QString ProfilesWindow::folderPathForItem(const QTreeWidgetItem* item) const
|
||||
{
|
||||
if (item == nullptr) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QVariant idValue = item->data(0, kProfileIdRole);
|
||||
if (idValue.isValid()) {
|
||||
const qint64 id = idValue.toLongLong();
|
||||
const auto cacheIt = m_profileCache.find(id);
|
||||
if (cacheIt != m_profileCache.end()) {
|
||||
return normalizeFolderPathForView(cacheIt->second.folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeFolderPathForView(item->data(0, kFolderPathRole).toString());
|
||||
}
|
||||
|
||||
void ProfilesWindow::showTreeContextMenu(const QPoint& pos)
|
||||
{
|
||||
if (m_profilesTree == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTreeWidgetItem* item = m_profilesTree->itemAt(pos);
|
||||
if (item != nullptr) {
|
||||
m_profilesTree->setCurrentItem(item);
|
||||
}
|
||||
|
||||
const QString contextFolder = folderPathForItem(item);
|
||||
const bool isProfileItem = item != nullptr && item->data(0, kProfileIdRole).isValid();
|
||||
|
||||
QMenu menu(this);
|
||||
QAction* newConnectionAction = menu.addAction(QStringLiteral("New Connection"));
|
||||
QAction* newFolderAction = menu.addAction(QStringLiteral("New Folder"));
|
||||
QAction* connectAction = nullptr;
|
||||
QAction* editAction = nullptr;
|
||||
QAction* deleteAction = nullptr;
|
||||
|
||||
if (isProfileItem) {
|
||||
menu.addSeparator();
|
||||
connectAction = menu.addAction(QStringLiteral("Connect"));
|
||||
editAction = menu.addAction(QStringLiteral("Edit"));
|
||||
deleteAction = menu.addAction(QStringLiteral("Delete"));
|
||||
}
|
||||
|
||||
QAction* chosen = menu.exec(m_profilesTree->viewport()->mapToGlobal(pos));
|
||||
if (chosen == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (chosen == newConnectionAction) {
|
||||
createProfile(contextFolder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (chosen == newFolderAction) {
|
||||
createFolderInContext(contextFolder);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isProfileItem && chosen == connectAction) {
|
||||
openSessionForItem(item);
|
||||
return;
|
||||
}
|
||||
if (isProfileItem && chosen == editAction) {
|
||||
editSelectedProfile();
|
||||
return;
|
||||
}
|
||||
if (isProfileItem && chosen == deleteAction) {
|
||||
deleteSelectedProfile();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilesWindow::createFolderInContext(const QString& baseFolderPath)
|
||||
{
|
||||
bool accepted = false;
|
||||
const QString folderName = QInputDialog::getText(
|
||||
this,
|
||||
QStringLiteral("New Folder"),
|
||||
QStringLiteral("Folder name"),
|
||||
QLineEdit::Normal,
|
||||
QString(),
|
||||
&accepted);
|
||||
if (!accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString normalized = joinFolderPath(baseFolderPath, folderName);
|
||||
if (normalized.isEmpty()) {
|
||||
QMessageBox::information(this,
|
||||
QStringLiteral("New Folder"),
|
||||
QStringLiteral("Folder name is required."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_repository->createFolder(normalized)) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("New Folder"),
|
||||
QStringLiteral("Failed to create folder: %1")
|
||||
.arg(m_repository->lastError().isEmpty()
|
||||
? QStringLiteral("unknown error")
|
||||
: m_repository->lastError()));
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::persistFolderAssignmentsFromTree()
|
||||
{
|
||||
if (!isFolderViewEnabled() || m_profilesTree == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_map<qint64, QString> assignments;
|
||||
const int topLevelCount = m_profilesTree->topLevelItemCount();
|
||||
for (int index = 0; index < topLevelCount; ++index) {
|
||||
collectProfileAssignments(m_profilesTree->topLevelItem(index), QString(), assignments);
|
||||
}
|
||||
|
||||
bool hadErrors = false;
|
||||
int updatedCount = 0;
|
||||
|
||||
for (const auto& [id, newFolderPath] : assignments) {
|
||||
auto cacheIt = m_profileCache.find(id);
|
||||
Profile profile;
|
||||
if (cacheIt != m_profileCache.end()) {
|
||||
profile = cacheIt->second;
|
||||
} else {
|
||||
const std::optional<Profile> fetched = m_repository->getProfile(id);
|
||||
if (!fetched.has_value()) {
|
||||
continue;
|
||||
}
|
||||
profile = fetched.value();
|
||||
}
|
||||
|
||||
const QString existing = normalizeFolderPathForView(profile.folderPath);
|
||||
const QString target = normalizeFolderPathForView(newFolderPath);
|
||||
if (existing == target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
profile.folderPath = target;
|
||||
if (!target.isEmpty()) {
|
||||
m_repository->createFolder(target);
|
||||
}
|
||||
if (!m_repository->updateProfile(profile)) {
|
||||
hadErrors = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
m_profileCache.insert_or_assign(profile.id, profile);
|
||||
++updatedCount;
|
||||
}
|
||||
|
||||
if (hadErrors) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Move Profile"),
|
||||
QStringLiteral("One or more profile moves could not be saved."));
|
||||
}
|
||||
|
||||
if (updatedCount > 0 || hadErrors) {
|
||||
loadProfiles();
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilesWindow::collectProfileAssignments(
|
||||
const QTreeWidgetItem* item,
|
||||
const QString& parentFolderPath,
|
||||
std::unordered_map<qint64, QString>& assignments) const
|
||||
{
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariant idValue = item->data(0, kProfileIdRole);
|
||||
if (idValue.isValid()) {
|
||||
assignments.insert_or_assign(idValue.toLongLong(), normalizeFolderPathForView(parentFolderPath));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString folderPath = joinFolderPath(parentFolderPath, item->text(0));
|
||||
const int childCount = item->childCount();
|
||||
for (int index = 0; index < childCount; ++index) {
|
||||
collectProfileAssignments(item->child(index), folderPath, assignments);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilesWindow::loadUiPreferences()
|
||||
{
|
||||
QSettings settings;
|
||||
const QString search = settings.value(QStringLiteral("profiles/searchText")).toString();
|
||||
const QString viewMode =
|
||||
settings.value(QStringLiteral("profiles/viewMode"), QStringLiteral("List")).toString();
|
||||
const int sortValue = settings
|
||||
.value(QStringLiteral("profiles/sortOrder"),
|
||||
static_cast<int>(ProfileSortOrder::NameAsc))
|
||||
.toInt();
|
||||
const QString protocolFilter =
|
||||
settings.value(QStringLiteral("profiles/protocolFilter"), QStringLiteral("All")).toString();
|
||||
const QString tagFilter =
|
||||
settings.value(QStringLiteral("profiles/tagFilter"), QStringLiteral("All")).toString();
|
||||
m_pendingTagFilterPreference = tagFilter.trimmed();
|
||||
|
||||
if (m_searchBox != nullptr) {
|
||||
const QSignalBlocker blocker(m_searchBox);
|
||||
m_searchBox->setText(search);
|
||||
}
|
||||
|
||||
if (m_viewModeBox != nullptr) {
|
||||
int found = m_viewModeBox->findText(viewMode, Qt::MatchFixedString);
|
||||
if (found < 0) {
|
||||
found = 0;
|
||||
}
|
||||
const QSignalBlocker blocker(m_viewModeBox);
|
||||
m_viewModeBox->setCurrentIndex(found);
|
||||
}
|
||||
|
||||
if (m_sortBox != nullptr) {
|
||||
int found = m_sortBox->findData(sortValue);
|
||||
if (found < 0) {
|
||||
found = 0;
|
||||
}
|
||||
const QSignalBlocker blocker(m_sortBox);
|
||||
m_sortBox->setCurrentIndex(found);
|
||||
}
|
||||
|
||||
if (m_protocolFilterBox != nullptr) {
|
||||
int found = m_protocolFilterBox->findText(protocolFilter, Qt::MatchFixedString);
|
||||
if (found < 0) {
|
||||
found = 0;
|
||||
}
|
||||
const QSignalBlocker blocker(m_protocolFilterBox);
|
||||
m_protocolFilterBox->setCurrentIndex(found);
|
||||
}
|
||||
|
||||
if (m_tagFilterBox != nullptr) {
|
||||
int found = m_tagFilterBox->findText(tagFilter, Qt::MatchFixedString);
|
||||
if (found < 0) {
|
||||
found = 0;
|
||||
}
|
||||
const QSignalBlocker blocker(m_tagFilterBox);
|
||||
m_tagFilterBox->setCurrentIndex(found);
|
||||
}
|
||||
}
|
||||
|
||||
void ProfilesWindow::saveUiPreferences() const
|
||||
{
|
||||
QSettings settings;
|
||||
if (m_searchBox != nullptr) {
|
||||
settings.setValue(QStringLiteral("profiles/searchText"), m_searchBox->text());
|
||||
}
|
||||
if (m_viewModeBox != nullptr) {
|
||||
settings.setValue(QStringLiteral("profiles/viewMode"), m_viewModeBox->currentText());
|
||||
}
|
||||
if (m_sortBox != nullptr) {
|
||||
settings.setValue(QStringLiteral("profiles/sortOrder"), m_sortBox->currentData());
|
||||
}
|
||||
if (m_protocolFilterBox != nullptr) {
|
||||
settings.setValue(QStringLiteral("profiles/protocolFilter"), m_protocolFilterBox->currentText());
|
||||
}
|
||||
if (m_tagFilterBox != nullptr) {
|
||||
settings.setValue(QStringLiteral("profiles/tagFilter"), m_tagFilterBox->currentText());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Profile> ProfilesWindow::selectedProfile() const
|
||||
{
|
||||
QListWidgetItem* item = m_profilesList->currentItem();
|
||||
QTreeWidgetItem* item = m_profilesTree->currentItem();
|
||||
if (item == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QVariant value = item->data(Qt::UserRole);
|
||||
const QVariant value = item->data(0, kProfileIdRole);
|
||||
if (!value.isValid()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -150,16 +836,22 @@ std::optional<Profile> ProfilesWindow::selectedProfile() const
|
||||
return m_repository->getProfile(id);
|
||||
}
|
||||
|
||||
void ProfilesWindow::createProfile()
|
||||
void ProfilesWindow::createProfile(const QString& defaultFolderPath)
|
||||
{
|
||||
ProfileDialog dialog(this);
|
||||
dialog.setDialogTitle(QStringLiteral("New Profile"));
|
||||
dialog.setDefaultFolderPath(defaultFolderPath);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_repository->createProfile(dialog.profile()).has_value()) {
|
||||
const Profile newProfile = dialog.profile();
|
||||
if (!newProfile.folderPath.trimmed().isEmpty()) {
|
||||
m_repository->createFolder(newProfile.folderPath);
|
||||
}
|
||||
|
||||
if (!m_repository->createProfile(newProfile).has_value()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Create Profile"),
|
||||
QStringLiteral("Failed to create profile: %1")
|
||||
@@ -169,7 +861,7 @@ void ProfilesWindow::createProfile()
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles(m_searchBox->text());
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::editSelectedProfile()
|
||||
@@ -192,6 +884,9 @@ void ProfilesWindow::editSelectedProfile()
|
||||
|
||||
Profile updated = dialog.profile();
|
||||
updated.id = selected->id;
|
||||
if (!updated.folderPath.trimmed().isEmpty()) {
|
||||
m_repository->createFolder(updated.folderPath);
|
||||
}
|
||||
|
||||
if (!m_repository->updateProfile(updated)) {
|
||||
QMessageBox::warning(this,
|
||||
@@ -203,7 +898,7 @@ void ProfilesWindow::editSelectedProfile()
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles(m_searchBox->text());
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::deleteSelectedProfile()
|
||||
@@ -237,17 +932,18 @@ void ProfilesWindow::deleteSelectedProfile()
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles(m_searchBox->text());
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
|
||||
void ProfilesWindow::openSessionForItem(QTreeWidgetItem* item)
|
||||
{
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariant value = item->data(Qt::UserRole);
|
||||
const QVariant value = item->data(0, kProfileIdRole);
|
||||
if (!value.isValid()) {
|
||||
item->setExpanded(!item->isExpanded());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,22 +959,16 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
|
||||
return;
|
||||
}
|
||||
|
||||
auto* session = new SessionWindow(profile.value());
|
||||
session->setAttribute(Qt::WA_DeleteOnClose);
|
||||
if (m_sessionWindow.isNull()) {
|
||||
m_sessionWindow = new SessionWindow(profile.value());
|
||||
m_sessionWindow->setAttribute(Qt::WA_DeleteOnClose);
|
||||
connect(m_sessionWindow, &QObject::destroyed, this, [this]() { m_sessionWindow = nullptr; });
|
||||
} else {
|
||||
m_sessionWindow->openProfile(profile.value());
|
||||
}
|
||||
|
||||
m_sessionWindows.emplace_back(session);
|
||||
connect(session,
|
||||
&QObject::destroyed,
|
||||
this,
|
||||
[this](QObject* object) {
|
||||
m_sessionWindows.erase(
|
||||
std::remove_if(m_sessionWindows.begin(),
|
||||
m_sessionWindows.end(),
|
||||
[object](const QPointer<SessionWindow>& candidate) {
|
||||
return candidate.isNull() || candidate.data() == object;
|
||||
}),
|
||||
m_sessionWindows.end());
|
||||
});
|
||||
|
||||
session->show();
|
||||
m_sessionWindow->setWindowState(m_sessionWindow->windowState() & ~Qt::WindowMinimized);
|
||||
m_sessionWindow->show();
|
||||
m_sessionWindow->raise();
|
||||
m_sessionWindow->activateWindow();
|
||||
}
|
||||
|
||||
@@ -5,19 +5,24 @@
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <QPointer>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QComboBox;
|
||||
class QPoint;
|
||||
class SessionWindow;
|
||||
class ProfilesTreeWidget;
|
||||
|
||||
class ProfilesWindow : public QMainWindow
|
||||
{
|
||||
@@ -29,21 +34,43 @@ public:
|
||||
|
||||
private:
|
||||
QLineEdit* m_searchBox;
|
||||
QListWidget* m_profilesList;
|
||||
QComboBox* m_viewModeBox;
|
||||
QComboBox* m_sortBox;
|
||||
QComboBox* m_protocolFilterBox;
|
||||
QComboBox* m_tagFilterBox;
|
||||
ProfilesTreeWidget* m_profilesTree;
|
||||
QPushButton* m_newButton;
|
||||
QPushButton* m_editButton;
|
||||
QPushButton* m_deleteButton;
|
||||
std::vector<QPointer<SessionWindow>> m_sessionWindows;
|
||||
QPointer<SessionWindow> m_sessionWindow;
|
||||
std::unique_ptr<ProfileRepository> m_repository;
|
||||
std::unordered_map<qint64, Profile> m_profileCache;
|
||||
QString m_pendingTagFilterPreference;
|
||||
|
||||
void setupUi();
|
||||
void loadProfiles(const QString& query = QString());
|
||||
void loadProfiles();
|
||||
ProfileSortOrder selectedSortOrder() const;
|
||||
bool isFolderViewEnabled() const;
|
||||
QString selectedProtocolFilter() const;
|
||||
QString selectedTagFilter() const;
|
||||
void updateTagFilterOptions(const std::vector<Profile>& profiles);
|
||||
QTreeWidgetItem* upsertFolderNode(const QStringList& folderParts,
|
||||
std::map<QString, QTreeWidgetItem*>& folderNodes);
|
||||
void addProfileNode(QTreeWidgetItem* parent, const Profile& profile);
|
||||
QString folderPathForItem(const QTreeWidgetItem* item) const;
|
||||
void showTreeContextMenu(const QPoint& pos);
|
||||
void createFolderInContext(const QString& baseFolderPath);
|
||||
void persistFolderAssignmentsFromTree();
|
||||
void collectProfileAssignments(const QTreeWidgetItem* item,
|
||||
const QString& parentFolderPath,
|
||||
std::unordered_map<qint64, QString>& assignments) const;
|
||||
void loadUiPreferences();
|
||||
void saveUiPreferences() const;
|
||||
std::optional<Profile> selectedProfile() const;
|
||||
void createProfile();
|
||||
void createProfile(const QString& defaultFolderPath = QString());
|
||||
void editSelectedProfile();
|
||||
void deleteSelectedProfile();
|
||||
void openSessionForItem(QListWidgetItem* item);
|
||||
void openSessionForItem(QTreeWidgetItem* item);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
207
src/rdp_display_widget.cpp
Normal file
207
src/rdp_display_widget.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
#include "rdp_display_widget.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QResizeEvent>
|
||||
#include <QTimer>
|
||||
#include <QWheelEvent>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace {
|
||||
QSize sanitizeSize(const QSize& size)
|
||||
{
|
||||
return QSize(qMax(1, size.width()), qMax(1, size.height()));
|
||||
}
|
||||
}
|
||||
|
||||
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
|
||||
: QWidget(parent), m_remoteSize(1280, 720)
|
||||
{
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setMouseTracking(true);
|
||||
setAutoFillBackground(false);
|
||||
setMinimumSize(320, 200);
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
const QSize size = sanitizeSize(this->size());
|
||||
emit viewportSizeChanged(size.width(), size.height());
|
||||
});
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::setFrame(const QImage& frame)
|
||||
{
|
||||
if (frame.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_frame = frame;
|
||||
m_remoteSize = sanitizeSize(frame.size());
|
||||
update();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::setRemoteDesktopSize(int width, int height)
|
||||
{
|
||||
if (width < 1 || height < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize nextSize(width, height);
|
||||
if (m_remoteSize == nextSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_remoteSize = nextSize;
|
||||
update();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::clearFrame()
|
||||
{
|
||||
m_frame = QImage();
|
||||
update();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
Q_UNUSED(event);
|
||||
|
||||
QPainter painter(this);
|
||||
painter.fillRect(rect(), QColor(QStringLiteral("#101214")));
|
||||
|
||||
const QRectF target = renderRect();
|
||||
if (!m_frame.isNull()) {
|
||||
painter.drawImage(target, m_frame);
|
||||
} else {
|
||||
painter.setPen(QColor(QStringLiteral("#b0bec5")));
|
||||
painter.drawText(rect(),
|
||||
Qt::AlignCenter,
|
||||
QStringLiteral("Waiting for remote desktop frame..."));
|
||||
}
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
const QSize size = sanitizeSize(event->size());
|
||||
emit viewportSizeChanged(size.width(), size.height());
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event == nullptr || event->isAutoRepeat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit keyInput(event->key(),
|
||||
event->nativeScanCode(),
|
||||
event->text(),
|
||||
true,
|
||||
static_cast<int>(event->modifiers()));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::keyReleaseEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event == nullptr || event->isAutoRepeat()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit keyInput(event->key(),
|
||||
event->nativeScanCode(),
|
||||
event->text(),
|
||||
false,
|
||||
static_cast<int>(event->modifiers()));
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFocus(Qt::MouseFocusReason);
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), true);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::mouseReleaseEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
emit mouseButtonInput(mapped.x(), mapped.y(), static_cast<int>(event->button()), false);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::mouseMoveEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
emit mouseMoveInput(mapped.x(), mapped.y());
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void RdpDisplayWidget::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint mapped = mapToRemote(event->position());
|
||||
const QPoint angle = event->angleDelta();
|
||||
emit mouseWheelInput(mapped.x(), mapped.y(), angle.x(), angle.y());
|
||||
event->accept();
|
||||
}
|
||||
|
||||
QRectF RdpDisplayWidget::renderRect() const
|
||||
{
|
||||
const QSize remote = effectiveRemoteSize();
|
||||
const QRectF area = rect();
|
||||
if (area.isEmpty()) {
|
||||
return QRectF();
|
||||
}
|
||||
|
||||
const qreal scale = qMin(area.width() / remote.width(), area.height() / remote.height());
|
||||
const qreal drawWidth = remote.width() * scale;
|
||||
const qreal drawHeight = remote.height() * scale;
|
||||
const qreal x = area.x() + ((area.width() - drawWidth) * 0.5);
|
||||
const qreal y = area.y() + ((area.height() - drawHeight) * 0.5);
|
||||
return QRectF(x, y, drawWidth, drawHeight);
|
||||
}
|
||||
|
||||
QPoint RdpDisplayWidget::mapToRemote(const QPointF& pos) const
|
||||
{
|
||||
const QSize remote = effectiveRemoteSize();
|
||||
const QRectF target = renderRect();
|
||||
if (target.isEmpty()) {
|
||||
return QPoint(0, 0);
|
||||
}
|
||||
|
||||
const qreal clampedX = qBound(target.left(), pos.x(), target.right());
|
||||
const qreal clampedY = qBound(target.top(), pos.y(), target.bottom());
|
||||
|
||||
const qreal normalizedX = (clampedX - target.left()) / qMax(1.0, target.width());
|
||||
const qreal normalizedY = (clampedY - target.top()) / qMax(1.0, target.height());
|
||||
|
||||
const int remoteX = qBound(0, static_cast<int>(normalizedX * remote.width()), remote.width() - 1);
|
||||
const int remoteY = qBound(0, static_cast<int>(normalizedY * remote.height()), remote.height() - 1);
|
||||
return QPoint(remoteX, remoteY);
|
||||
}
|
||||
|
||||
QSize RdpDisplayWidget::effectiveRemoteSize() const
|
||||
{
|
||||
if (m_remoteSize.width() > 0 && m_remoteSize.height() > 0) {
|
||||
return m_remoteSize;
|
||||
}
|
||||
if (!m_frame.isNull()) {
|
||||
return sanitizeSize(m_frame.size());
|
||||
}
|
||||
return QSize(1280, 720);
|
||||
}
|
||||
50
src/rdp_display_widget.h
Normal file
50
src/rdp_display_widget.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef ORBITHUB_RDP_DISPLAY_WIDGET_H
|
||||
#define ORBITHUB_RDP_DISPLAY_WIDGET_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QWidget>
|
||||
|
||||
class QKeyEvent;
|
||||
class QMouseEvent;
|
||||
class QPaintEvent;
|
||||
class QResizeEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
class RdpDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RdpDisplayWidget(QWidget* parent = nullptr);
|
||||
|
||||
void setFrame(const QImage& frame);
|
||||
void setRemoteDesktopSize(int width, int height);
|
||||
void clearFrame();
|
||||
|
||||
signals:
|
||||
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
|
||||
void mouseMoveInput(int x, int y);
|
||||
void mouseButtonInput(int x, int y, int button, bool pressed);
|
||||
void mouseWheelInput(int x, int y, int deltaX, int deltaY);
|
||||
void viewportSizeChanged(int width, int height);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void keyReleaseEvent(QKeyEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
|
||||
private:
|
||||
QImage m_frame;
|
||||
QSize m_remoteSize;
|
||||
|
||||
QRectF renderRect() const;
|
||||
QPoint mapToRemote(const QPointF& pos) const;
|
||||
QSize effectiveRemoteSize() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
1809
src/rdp_session_backend.cpp
Normal file
1809
src/rdp_session_backend.cpp
Normal file
File diff suppressed because it is too large
Load Diff
108
src/rdp_session_backend.h
Normal file
108
src/rdp_session_backend.h
Normal file
@@ -0,0 +1,108 @@
|
||||
#ifndef ORBITHUB_RDP_SESSION_BACKEND_H
|
||||
#define ORBITHUB_RDP_SESSION_BACKEND_H
|
||||
|
||||
#include "session_backend.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
struct rdp_freerdp;
|
||||
|
||||
class RdpSessionBackend : public SessionBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit RdpSessionBackend(const Profile& profile, QObject* parent = nullptr);
|
||||
~RdpSessionBackend() override;
|
||||
|
||||
public slots:
|
||||
void connectSession(const SessionConnectOptions& options) override;
|
||||
void disconnectSession() override;
|
||||
void reconnectSession(const SessionConnectOptions& options) override;
|
||||
void sendInput(const QString& input) override;
|
||||
void confirmHostKey(bool trustHost) override;
|
||||
void updateTerminalSize(int columns, int rows) override;
|
||||
void sendKeyEvent(int key,
|
||||
quint32 nativeScanCode,
|
||||
const QString& text,
|
||||
bool pressed,
|
||||
int modifiers) override;
|
||||
void sendMouseMoveEvent(int x, int y) override;
|
||||
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
|
||||
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
|
||||
|
||||
private:
|
||||
enum class InputEventType {
|
||||
Key,
|
||||
MouseMove,
|
||||
MouseButton,
|
||||
MouseWheel,
|
||||
Resize,
|
||||
};
|
||||
|
||||
struct InputEvent {
|
||||
InputEventType type = InputEventType::MouseMove;
|
||||
int key = 0;
|
||||
quint32 nativeScanCode = 0;
|
||||
QString text;
|
||||
bool pressed = false;
|
||||
int modifiers = 0;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int button = 0;
|
||||
int deltaX = 0;
|
||||
int deltaY = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
SessionState m_state;
|
||||
SessionConnectOptions m_activeOptions;
|
||||
std::atomic_bool m_userInitiatedDisconnect;
|
||||
|
||||
std::atomic_int m_requestedDesktopWidth;
|
||||
std::atomic_int m_requestedDesktopHeight;
|
||||
|
||||
std::thread m_worker;
|
||||
std::atomic_bool m_workerRunning;
|
||||
std::atomic_bool m_stopRequested;
|
||||
|
||||
std::mutex m_instanceMutex;
|
||||
rdp_freerdp* m_instance;
|
||||
|
||||
std::mutex m_inputMutex;
|
||||
std::deque<InputEvent> m_inputEvents;
|
||||
|
||||
std::mutex m_displayControlMutex;
|
||||
void* m_displayControlContext;
|
||||
bool m_displayControlReady;
|
||||
bool m_resizeFailureLogged;
|
||||
int m_lastResizeWidth;
|
||||
int m_lastResizeHeight;
|
||||
|
||||
void setState(SessionState state, const QString& message);
|
||||
bool validateProfile(QString& message) const;
|
||||
void startWorker();
|
||||
void stopWorker(bool userInitiated);
|
||||
void workerMain();
|
||||
void enqueueInputEvent(const InputEvent& event);
|
||||
void processInputEvents(rdp_freerdp* instance);
|
||||
bool sendDisplayResize(rdp_freerdp* instance, int width, int height);
|
||||
public:
|
||||
void onChannelConnectedEvent(const char* name, void* channelInterface);
|
||||
void onChannelDisconnectedEvent(const char* name, void* channelInterface);
|
||||
void onDisplayControlCaps(uint32_t maxNumMonitors,
|
||||
uint32_t maxMonitorAreaFactorA,
|
||||
uint32_t maxMonitorAreaFactorB);
|
||||
private:
|
||||
void emitStateAsync(SessionState state, const QString& message);
|
||||
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);
|
||||
int sanitizeDesktopWidth(int width) const;
|
||||
int sanitizeDesktopHeight(int height) const;
|
||||
};
|
||||
|
||||
#endif
|
||||
97
src/session_backend.h
Normal file
97
src/session_backend.h
Normal file
@@ -0,0 +1,97 @@
|
||||
#ifndef ORBITHUB_SESSION_BACKEND_H
|
||||
#define ORBITHUB_SESSION_BACKEND_H
|
||||
|
||||
#include "profile_repository.h"
|
||||
|
||||
#include <QImage>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
class SessionConnectOptions
|
||||
{
|
||||
public:
|
||||
QString password;
|
||||
QString privateKeyPath;
|
||||
QString knownHostsPolicy;
|
||||
};
|
||||
|
||||
enum class SessionState {
|
||||
Disconnected,
|
||||
Connecting,
|
||||
Connected,
|
||||
Failed,
|
||||
};
|
||||
|
||||
class SessionBackend : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SessionBackend(const Profile& profile, QObject* parent = nullptr)
|
||||
: QObject(parent), m_profile(profile)
|
||||
{
|
||||
}
|
||||
~SessionBackend() override = default;
|
||||
|
||||
const Profile& profile() const
|
||||
{
|
||||
return m_profile;
|
||||
}
|
||||
|
||||
public slots:
|
||||
virtual void connectSession(const SessionConnectOptions& options) = 0;
|
||||
virtual void disconnectSession() = 0;
|
||||
virtual void reconnectSession(const SessionConnectOptions& options) = 0;
|
||||
virtual void sendInput(const QString& input) = 0;
|
||||
virtual void confirmHostKey(bool trustHost) = 0;
|
||||
virtual void updateTerminalSize(int columns, int rows) = 0;
|
||||
virtual void sendKeyEvent(int key,
|
||||
quint32 nativeScanCode,
|
||||
const QString& text,
|
||||
bool pressed,
|
||||
int modifiers)
|
||||
{
|
||||
Q_UNUSED(key);
|
||||
Q_UNUSED(nativeScanCode);
|
||||
Q_UNUSED(text);
|
||||
Q_UNUSED(pressed);
|
||||
Q_UNUSED(modifiers);
|
||||
}
|
||||
virtual void sendMouseMoveEvent(int x, int y)
|
||||
{
|
||||
Q_UNUSED(x);
|
||||
Q_UNUSED(y);
|
||||
}
|
||||
virtual void sendMouseButtonEvent(int x, int y, int button, bool pressed)
|
||||
{
|
||||
Q_UNUSED(x);
|
||||
Q_UNUSED(y);
|
||||
Q_UNUSED(button);
|
||||
Q_UNUSED(pressed);
|
||||
}
|
||||
virtual void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY)
|
||||
{
|
||||
Q_UNUSED(x);
|
||||
Q_UNUSED(y);
|
||||
Q_UNUSED(deltaX);
|
||||
Q_UNUSED(deltaY);
|
||||
}
|
||||
|
||||
signals:
|
||||
void stateChanged(SessionState state, const QString& message);
|
||||
void eventLogged(const QString& message);
|
||||
void connectionError(const QString& displayMessage, const QString& rawMessage);
|
||||
void outputReceived(const QString& text);
|
||||
void hostKeyConfirmationRequested(const QString& prompt);
|
||||
void frameUpdated(const QImage& frame);
|
||||
void remoteDesktopSizeChanged(int width, int height);
|
||||
|
||||
private:
|
||||
Profile m_profile;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(SessionConnectOptions)
|
||||
Q_DECLARE_METATYPE(SessionState)
|
||||
|
||||
#endif
|
||||
18
src/session_backend_factory.cpp
Normal file
18
src/session_backend_factory.cpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#include "session_backend_factory.h"
|
||||
|
||||
#include "rdp_session_backend.h"
|
||||
#include "session_backend.h"
|
||||
#include "ssh_session_backend.h"
|
||||
#include "unsupported_session_backend.h"
|
||||
|
||||
std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
|
||||
{
|
||||
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
|
||||
return std::make_unique<SshSessionBackend>(profile);
|
||||
}
|
||||
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
|
||||
return std::make_unique<RdpSessionBackend>(profile);
|
||||
}
|
||||
|
||||
return std::make_unique<UnsupportedSessionBackend>(profile);
|
||||
}
|
||||
12
src/session_backend_factory.h
Normal file
12
src/session_backend_factory.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifndef ORBITHUB_SESSION_BACKEND_FACTORY_H
|
||||
#define ORBITHUB_SESSION_BACKEND_FACTORY_H
|
||||
|
||||
#include "profile_repository.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
class SessionBackend;
|
||||
|
||||
std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile);
|
||||
|
||||
#endif
|
||||
1023
src/session_tab.cpp
Normal file
1023
src/session_tab.cpp
Normal file
File diff suppressed because it is too large
Load Diff
130
src/session_tab.h
Normal file
130
src/session_tab.h
Normal file
@@ -0,0 +1,130 @@
|
||||
#ifndef ORBITHUB_SESSION_TAB_H
|
||||
#define ORBITHUB_SESSION_TAB_H
|
||||
|
||||
#include "profile_repository.h"
|
||||
#include "session_backend.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStringList>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
class QPlainTextEdit;
|
||||
class QThread;
|
||||
class SessionBackend;
|
||||
class TerminalView;
|
||||
class RdpDisplayWidget;
|
||||
class QToolButton;
|
||||
class QLineEdit;
|
||||
class QComboBox;
|
||||
class KodoTerm;
|
||||
|
||||
struct SessionUiPreferences
|
||||
{
|
||||
QString terminalThemeName = QStringLiteral("Dark");
|
||||
bool eventsPanelExpanded = false;
|
||||
};
|
||||
|
||||
class SessionTab : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SessionTab(const Profile& profile,
|
||||
const SessionUiPreferences& preferences,
|
||||
QWidget* parent = nullptr);
|
||||
~SessionTab() override;
|
||||
|
||||
QString tabTitle() const;
|
||||
void connectSession();
|
||||
void disconnectSession();
|
||||
void reconnectSession();
|
||||
void clearTerminal();
|
||||
void setTerminalThemeName(const QString& themeName);
|
||||
QString terminalThemeName() const;
|
||||
bool supportsThemeSelection() const;
|
||||
bool supportsClearAction() const;
|
||||
bool isEventsPanelExpanded() const;
|
||||
void setEventsPanelExpanded(bool expanded);
|
||||
void clearEvents();
|
||||
void copyEvents() const;
|
||||
void exportEventsToFile();
|
||||
|
||||
signals:
|
||||
void tabTitleChanged(const QString& title);
|
||||
void tabStateChanged(SessionState state);
|
||||
void terminalThemeChanged(const QString& themeName);
|
||||
void eventsPanelVisibilityChanged(bool expanded);
|
||||
void requestConnect(const SessionConnectOptions& options);
|
||||
void requestDisconnect();
|
||||
void requestReconnect(const SessionConnectOptions& options);
|
||||
void requestInput(const QString& input);
|
||||
void requestHostKeyConfirmation(bool trustHost);
|
||||
void requestTerminalSize(int columns, int rows);
|
||||
void requestKeyEvent(int key,
|
||||
quint32 nativeScanCode,
|
||||
const QString& text,
|
||||
bool pressed,
|
||||
int modifiers);
|
||||
void requestMouseMoveEvent(int x, int y);
|
||||
void requestMouseButtonEvent(int x, int y, int button, bool pressed);
|
||||
void requestMouseWheelEvent(int x, int y, int deltaX, int deltaY);
|
||||
|
||||
private slots:
|
||||
void onBackendStateChanged(SessionState state, const QString& message);
|
||||
void onBackendEventLogged(const QString& message);
|
||||
void onBackendConnectionError(const QString& displayMessage, const QString& rawMessage);
|
||||
void onBackendOutputReceived(const QString& text);
|
||||
void onBackendHostKeyConfirmationRequested(const QString& prompt);
|
||||
|
||||
private:
|
||||
Profile m_profile;
|
||||
QThread* m_backendThread;
|
||||
SessionBackend* m_backend;
|
||||
bool m_useKodoTermForSsh;
|
||||
SessionState m_state;
|
||||
QString m_lastError;
|
||||
SessionConnectOptions m_lastConnectOptions;
|
||||
QString m_terminalThemeName;
|
||||
|
||||
KodoTerm* m_sshTerminal;
|
||||
RdpDisplayWidget* m_rdpDisplay;
|
||||
TerminalView* m_terminalOutput;
|
||||
QPlainTextEdit* m_eventLog;
|
||||
QToolButton* m_toggleEventsButton;
|
||||
QLineEdit* m_eventFilterInput;
|
||||
QComboBox* m_eventSeverityFilterInput;
|
||||
QToolButton* m_clearEventsButton;
|
||||
QToolButton* m_exportEventsButton;
|
||||
QWidget* m_eventsPanel;
|
||||
enum class EventSeverity {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
};
|
||||
struct EventEntry {
|
||||
QString line;
|
||||
EventSeverity severity;
|
||||
};
|
||||
std::vector<EventEntry> m_eventEntries;
|
||||
QString m_eventFilter;
|
||||
EventSeverity m_eventSeverityFilter;
|
||||
bool m_eventsPanelExpanded;
|
||||
|
||||
void setupUi();
|
||||
std::optional<SessionConnectOptions> buildConnectOptions();
|
||||
bool validateProfileForConnect();
|
||||
void appendEvent(const QString& message);
|
||||
void setState(SessionState state, const QString& message);
|
||||
QString stateSuffix() const;
|
||||
void refreshActionButtons();
|
||||
void setPanelExpanded(QToolButton* button, QWidget* panel, const QString& name, bool expanded);
|
||||
bool startSshTerminal(const SessionConnectOptions& options);
|
||||
void applyTerminalTheme(const QString& themeName);
|
||||
void refreshEventLogView();
|
||||
static EventSeverity classifyEventSeverity(const QString& message);
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,17 +1,50 @@
|
||||
#include "session_window.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QLabel>
|
||||
#include "about_dialog.h"
|
||||
#include <QApplication>
|
||||
#include "session_tab.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QColor>
|
||||
#include <QMenu>
|
||||
#include <QMenuBar>
|
||||
#include <QPalette>
|
||||
#include <QSettings>
|
||||
#include <QStringList>
|
||||
#include <QTabBar>
|
||||
#include <QTabWidget>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
namespace {
|
||||
QColor tabColorForState(SessionState state, const QPalette& palette)
|
||||
{
|
||||
switch (state) {
|
||||
case SessionState::Disconnected:
|
||||
return palette.color(QPalette::WindowText);
|
||||
case SessionState::Connecting:
|
||||
return QColor(QStringLiteral("#9a6700"));
|
||||
case SessionState::Connected:
|
||||
return QColor(QStringLiteral("#2e7d32"));
|
||||
case SessionState::Failed:
|
||||
return QColor(QStringLiteral("#c62828"));
|
||||
}
|
||||
|
||||
return palette.color(QPalette::WindowText);
|
||||
}
|
||||
|
||||
QStringList terminalThemeNames()
|
||||
{
|
||||
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
||||
}
|
||||
}
|
||||
|
||||
SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
|
||||
: QMainWindow(parent), m_tabs(new QTabWidget(this))
|
||||
{
|
||||
loadUiPreferences();
|
||||
|
||||
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
|
||||
resize(980, 680);
|
||||
resize(1080, 760);
|
||||
setWindowIcon(QApplication::windowIcon());
|
||||
|
||||
m_tabs->setTabsClosable(true);
|
||||
connect(m_tabs,
|
||||
@@ -19,77 +52,177 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
|
||||
this,
|
||||
[this](int index) {
|
||||
QWidget* tab = m_tabs->widget(index);
|
||||
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
|
||||
sessionTab->disconnectSession();
|
||||
}
|
||||
m_tabs->removeTab(index);
|
||||
delete tab;
|
||||
if (m_tabs->count() == 0) {
|
||||
close();
|
||||
}
|
||||
});
|
||||
m_tabs->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_tabs->tabBar(),
|
||||
&QWidget::customContextMenuRequested,
|
||||
this,
|
||||
[this](const QPoint& pos) {
|
||||
const int index = m_tabs->tabBar()->tabAt(pos);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* tab = qobject_cast<SessionTab*>(m_tabs->widget(index));
|
||||
if (tab == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
QMenu menu(this);
|
||||
QAction* disconnectAction = menu.addAction(QStringLiteral("Disconnect"));
|
||||
QAction* reconnectAction = menu.addAction(QStringLiteral("Reconnect"));
|
||||
QAction* toggleEventsAction = menu.addAction(
|
||||
tab->isEventsPanelExpanded() ? QStringLiteral("Hide Events")
|
||||
: QStringLiteral("Show Events"));
|
||||
QAction* copyEventsAction = menu.addAction(QStringLiteral("Copy Events"));
|
||||
QAction* exportEventsAction = menu.addAction(QStringLiteral("Export Events"));
|
||||
QAction* clearEventsAction = menu.addAction(QStringLiteral("Clear Events"));
|
||||
QList<QAction*> themeActions;
|
||||
|
||||
if (tab->supportsThemeSelection()) {
|
||||
menu.addSeparator();
|
||||
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
|
||||
const QString currentTheme = tab->terminalThemeName();
|
||||
for (const QString& themeName : terminalThemeNames()) {
|
||||
QAction* themeAction = themeMenu->addAction(themeName);
|
||||
themeAction->setCheckable(true);
|
||||
themeAction->setChecked(
|
||||
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
|
||||
themeActions.append(themeAction);
|
||||
}
|
||||
}
|
||||
|
||||
QAction* clearAction = nullptr;
|
||||
if (tab->supportsClearAction()) {
|
||||
clearAction = menu.addAction(QStringLiteral("Clear"));
|
||||
}
|
||||
|
||||
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
|
||||
if (chosen == disconnectAction) {
|
||||
tab->disconnectSession();
|
||||
} else if (chosen == reconnectAction) {
|
||||
tab->reconnectSession();
|
||||
} else if (chosen == toggleEventsAction) {
|
||||
tab->setEventsPanelExpanded(!tab->isEventsPanelExpanded());
|
||||
} else if (chosen == copyEventsAction) {
|
||||
tab->copyEvents();
|
||||
} else if (chosen == exportEventsAction) {
|
||||
tab->exportEventsToFile();
|
||||
} else if (chosen == clearEventsAction) {
|
||||
tab->clearEvents();
|
||||
} else if (clearAction != nullptr && chosen == clearAction) {
|
||||
tab->clearTerminal();
|
||||
} else {
|
||||
for (QAction* themeAction : themeActions) {
|
||||
if (chosen == themeAction) {
|
||||
tab->setTerminalThemeName(themeAction->text());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
||||
QAction* aboutAction = helpMenu->addAction(QStringLiteral("About OrbitHub"));
|
||||
connect(aboutAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() {
|
||||
AboutDialog dialog(this);
|
||||
dialog.exec();
|
||||
});
|
||||
|
||||
setCentralWidget(m_tabs);
|
||||
addSessionTab(profile);
|
||||
}
|
||||
|
||||
void SessionWindow::openProfile(const Profile& profile)
|
||||
{
|
||||
addSessionTab(profile);
|
||||
}
|
||||
|
||||
void SessionWindow::addSessionTab(const Profile& profile)
|
||||
{
|
||||
auto* container = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(container);
|
||||
auto* tab = new SessionTab(profile, m_preferences, this);
|
||||
const int index = m_tabs->addTab(tab, tab->tabTitle());
|
||||
m_tabs->setCurrentIndex(index);
|
||||
if (m_tabs->count() > 1) {
|
||||
setWindowTitle(QStringLiteral("OrbitHub Sessions"));
|
||||
}
|
||||
m_tabs->tabBar()->setTabTextColor(
|
||||
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
|
||||
|
||||
auto* profileLabel = new QLabel(QStringLiteral("Profile: %1").arg(profile.name), container);
|
||||
auto* endpointLabel = new QLabel(
|
||||
QStringLiteral("Endpoint: %1://%2@%3:%4")
|
||||
.arg(profile.protocol,
|
||||
profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username,
|
||||
profile.host,
|
||||
QString::number(profile.port)),
|
||||
container);
|
||||
auto* authModeLabel = new QLabel(QStringLiteral("Auth Mode: %1").arg(profile.authMode), container);
|
||||
auto* statusLabel = new QLabel(QStringLiteral("Connection State: Connecting"), container);
|
||||
auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), container);
|
||||
|
||||
QFont profileFont = profileLabel->font();
|
||||
profileFont.setBold(true);
|
||||
profileLabel->setFont(profileFont);
|
||||
|
||||
QFont surfaceFont = surfaceLabel->font();
|
||||
surfaceFont.setPointSize(surfaceFont.pointSize() + 6);
|
||||
surfaceFont.setBold(true);
|
||||
surfaceLabel->setFont(surfaceFont);
|
||||
|
||||
statusLabel->setStyleSheet(
|
||||
QStringLiteral("border: 1px solid #a5a5a5; background-color: #fff3cd; padding: 6px;"));
|
||||
|
||||
surfaceLabel->setAlignment(Qt::AlignCenter);
|
||||
surfaceLabel->setMinimumHeight(220);
|
||||
surfaceLabel->setStyleSheet(
|
||||
QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;"));
|
||||
|
||||
layout->addWidget(profileLabel);
|
||||
layout->addWidget(endpointLabel);
|
||||
layout->addWidget(authModeLabel);
|
||||
layout->addWidget(statusLabel);
|
||||
layout->addWidget(surfaceLabel, 1);
|
||||
|
||||
const int tabIndex = m_tabs->addTab(container, QStringLiteral("%1 (Connecting)").arg(profile.name));
|
||||
|
||||
QTimer::singleShot(900,
|
||||
connect(tab,
|
||||
&SessionTab::tabTitleChanged,
|
||||
this,
|
||||
[this, tabIndex, statusLabel, profile]() {
|
||||
const bool shouldFail = profile.host.contains(QStringLiteral("fail"),
|
||||
Qt::CaseInsensitive);
|
||||
if (shouldFail) {
|
||||
statusLabel->setText(QStringLiteral("Connection State: Failed"));
|
||||
statusLabel->setStyleSheet(QStringLiteral(
|
||||
"border: 1px solid #a94442; background-color: #f2dede; padding: 6px;"));
|
||||
m_tabs->setTabText(tabIndex,
|
||||
QStringLiteral("%1 (Failed)").arg(profile.name));
|
||||
[this, tab](const QString& title) { updateTabTitle(tab, title); });
|
||||
connect(tab,
|
||||
&SessionTab::tabStateChanged,
|
||||
this,
|
||||
[this, tab](SessionState state) {
|
||||
for (int i = 0; i < m_tabs->count(); ++i) {
|
||||
if (m_tabs->widget(i) == tab) {
|
||||
m_tabs->tabBar()->setTabTextColor(
|
||||
i, tabColorForState(state, m_tabs->palette()));
|
||||
return;
|
||||
}
|
||||
|
||||
statusLabel->setText(QStringLiteral("Connection State: Connected"));
|
||||
statusLabel->setStyleSheet(QStringLiteral(
|
||||
"border: 1px solid #3c763d; background-color: #dff0d8; padding: 6px;"));
|
||||
m_tabs->setTabText(tabIndex,
|
||||
QStringLiteral("%1 (Connected)").arg(profile.name));
|
||||
}
|
||||
});
|
||||
connect(tab,
|
||||
&SessionTab::terminalThemeChanged,
|
||||
this,
|
||||
[this](const QString& themeName) {
|
||||
m_preferences.terminalThemeName = themeName.trimmed().isEmpty()
|
||||
? QStringLiteral("Dark")
|
||||
: themeName.trimmed();
|
||||
saveUiPreferences();
|
||||
});
|
||||
connect(tab,
|
||||
&SessionTab::eventsPanelVisibilityChanged,
|
||||
this,
|
||||
[this](bool expanded) {
|
||||
m_preferences.eventsPanelExpanded = expanded;
|
||||
saveUiPreferences();
|
||||
});
|
||||
}
|
||||
|
||||
void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
|
||||
{
|
||||
for (int i = 0; i < m_tabs->count(); ++i) {
|
||||
if (m_tabs->widget(i) == tab) {
|
||||
m_tabs->setTabText(i, title);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SessionWindow::loadUiPreferences()
|
||||
{
|
||||
QSettings settings;
|
||||
m_preferences.terminalThemeName =
|
||||
settings.value(QStringLiteral("session/defaultTerminalTheme"), QStringLiteral("Dark"))
|
||||
.toString()
|
||||
.trimmed();
|
||||
if (m_preferences.terminalThemeName.isEmpty()) {
|
||||
m_preferences.terminalThemeName = QStringLiteral("Dark");
|
||||
}
|
||||
m_preferences.eventsPanelExpanded =
|
||||
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
|
||||
}
|
||||
|
||||
void SessionWindow::saveUiPreferences() const
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("session/defaultTerminalTheme"),
|
||||
m_preferences.terminalThemeName);
|
||||
settings.setValue(QStringLiteral("session/eventsPanelExpanded"),
|
||||
m_preferences.eventsPanelExpanded);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define ORBITHUB_SESSION_WINDOW_H
|
||||
|
||||
#include "profile_repository.h"
|
||||
#include "session_tab.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
@@ -13,11 +14,16 @@ class SessionWindow : public QMainWindow
|
||||
|
||||
public:
|
||||
explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr);
|
||||
void openProfile(const Profile& profile);
|
||||
|
||||
private:
|
||||
QTabWidget* m_tabs;
|
||||
SessionUiPreferences m_preferences;
|
||||
|
||||
void addSessionTab(const Profile& profile);
|
||||
void updateTabTitle(SessionTab* tab, const QString& title);
|
||||
void loadUiPreferences();
|
||||
void saveUiPreferences() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
538
src/ssh_session_backend.cpp
Normal file
538
src/ssh_session_backend.cpp
Normal file
@@ -0,0 +1,538 @@
|
||||
#include "ssh_session_backend.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTextStream>
|
||||
#include <QUuid>
|
||||
|
||||
namespace {
|
||||
QString escapeForShellSingleQuotes(const QString& value)
|
||||
{
|
||||
QString escaped = value;
|
||||
escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'"));
|
||||
return escaped;
|
||||
}
|
||||
}
|
||||
|
||||
SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
|
||||
: SessionBackend(profile, parent),
|
||||
m_process(new QProcess(this)),
|
||||
m_connectedProbeTimer(new QTimer(this)),
|
||||
m_state(SessionState::Disconnected),
|
||||
m_userInitiatedDisconnect(false),
|
||||
m_reconnectPending(false),
|
||||
m_waitingForPasswordPrompt(false),
|
||||
m_waitingForHostKeyConfirmation(false),
|
||||
m_passwordSubmitted(false),
|
||||
m_terminalColumns(0),
|
||||
m_terminalRows(0)
|
||||
{
|
||||
m_connectedProbeTimer->setSingleShot(true);
|
||||
|
||||
connect(m_process, &QProcess::started, this, &SshSessionBackend::onProcessStarted);
|
||||
connect(m_process,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
&SshSessionBackend::onProcessErrorOccurred);
|
||||
connect(m_process,
|
||||
qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
|
||||
this,
|
||||
&SshSessionBackend::onProcessFinished);
|
||||
connect(m_process,
|
||||
&QProcess::readyReadStandardOutput,
|
||||
this,
|
||||
&SshSessionBackend::onReadyReadStandardOutput);
|
||||
connect(m_process,
|
||||
&QProcess::readyReadStandardError,
|
||||
this,
|
||||
&SshSessionBackend::onReadyReadStandardError);
|
||||
connect(m_connectedProbeTimer,
|
||||
&QTimer::timeout,
|
||||
this,
|
||||
&SshSessionBackend::onConnectedProbeTimeout);
|
||||
}
|
||||
|
||||
SshSessionBackend::~SshSessionBackend()
|
||||
{
|
||||
if (m_process->state() != QProcess::NotRunning) {
|
||||
m_process->kill();
|
||||
m_process->waitForFinished(500);
|
||||
}
|
||||
cleanupAskPassScript();
|
||||
}
|
||||
|
||||
void SshSessionBackend::connectSession(const SessionConnectOptions& options)
|
||||
{
|
||||
if (m_state == SessionState::Connected || m_state == SessionState::Connecting) {
|
||||
emit eventLogged(QStringLiteral("Connect skipped: session is already active."));
|
||||
return;
|
||||
}
|
||||
|
||||
m_userInitiatedDisconnect = false;
|
||||
m_reconnectPending = false;
|
||||
m_lastRawError.clear();
|
||||
m_activeOptions = options;
|
||||
m_waitingForPasswordPrompt = false;
|
||||
m_waitingForHostKeyConfirmation = false;
|
||||
m_passwordSubmitted = false;
|
||||
|
||||
if (!startSshProcess(options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(SessionState::Connecting, QStringLiteral("Connecting to SSH endpoint..."));
|
||||
emit eventLogged(QStringLiteral("Launching ssh client."));
|
||||
}
|
||||
|
||||
void SshSessionBackend::disconnectSession()
|
||||
{
|
||||
if (m_process->state() == QProcess::NotRunning) {
|
||||
if (m_state != SessionState::Disconnected) {
|
||||
setState(SessionState::Disconnected, QStringLiteral("Session is disconnected."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_userInitiatedDisconnect = true;
|
||||
emit eventLogged(QStringLiteral("Disconnect requested."));
|
||||
m_connectedProbeTimer->stop();
|
||||
|
||||
m_process->terminate();
|
||||
QTimer::singleShot(1500,
|
||||
this,
|
||||
[this]() {
|
||||
if (m_process->state() != QProcess::NotRunning) {
|
||||
emit eventLogged(QStringLiteral("Force-stopping ssh process."));
|
||||
m_process->kill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SshSessionBackend::reconnectSession(const SessionConnectOptions& options)
|
||||
{
|
||||
emit eventLogged(QStringLiteral("Reconnect requested."));
|
||||
|
||||
if (m_process->state() == QProcess::NotRunning) {
|
||||
connectSession(options);
|
||||
return;
|
||||
}
|
||||
|
||||
m_reconnectPending = true;
|
||||
m_reconnectOptions = options;
|
||||
m_userInitiatedDisconnect = true;
|
||||
m_process->terminate();
|
||||
}
|
||||
|
||||
void SshSessionBackend::sendInput(const QString& input)
|
||||
{
|
||||
if (m_process->state() != QProcess::Running) {
|
||||
emit eventLogged(QStringLiteral("Input ignored: session is not running."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_process->write(input.toUtf8());
|
||||
}
|
||||
|
||||
void SshSessionBackend::confirmHostKey(bool trustHost)
|
||||
{
|
||||
if (m_process->state() != QProcess::Running || !m_waitingForHostKeyConfirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_waitingForHostKeyConfirmation = false;
|
||||
const QString response = trustHost ? QStringLiteral("yes\n") : QStringLiteral("no\n");
|
||||
m_process->write(response.toUtf8());
|
||||
|
||||
emit eventLogged(trustHost
|
||||
? QStringLiteral("Host key accepted by user.")
|
||||
: QStringLiteral("Host key rejected by user."));
|
||||
}
|
||||
|
||||
void SshSessionBackend::updateTerminalSize(int columns, int rows)
|
||||
{
|
||||
m_terminalColumns = columns;
|
||||
m_terminalRows = rows;
|
||||
|
||||
if (m_state == SessionState::Connected) {
|
||||
applyTerminalSizeIfAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
void SshSessionBackend::onProcessStarted()
|
||||
{
|
||||
emit eventLogged(QStringLiteral("ssh process started."));
|
||||
m_connectedProbeTimer->start(1200);
|
||||
}
|
||||
|
||||
void SshSessionBackend::onProcessErrorOccurred(QProcess::ProcessError)
|
||||
{
|
||||
const QString rawError = m_process->errorString();
|
||||
if (!rawError.isEmpty()) {
|
||||
m_lastRawError += rawError + QLatin1Char('\n');
|
||||
}
|
||||
|
||||
if (m_state == SessionState::Connecting) {
|
||||
const QString display = mapSshError(m_lastRawError);
|
||||
setState(SessionState::Failed, display);
|
||||
emit connectionError(display, m_lastRawError.trimmed());
|
||||
}
|
||||
}
|
||||
|
||||
void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus)
|
||||
{
|
||||
m_connectedProbeTimer->stop();
|
||||
cleanupAskPassScript();
|
||||
|
||||
if (m_reconnectPending) {
|
||||
m_reconnectPending = false;
|
||||
SessionConnectOptions options = m_reconnectOptions;
|
||||
setState(SessionState::Disconnected, QStringLiteral("Reconnecting..."));
|
||||
QTimer::singleShot(0, this, [this, options]() { connectSession(options); });
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_userInitiatedDisconnect) {
|
||||
m_userInitiatedDisconnect = false;
|
||||
setState(SessionState::Disconnected, QStringLiteral("Session disconnected."));
|
||||
emit eventLogged(QStringLiteral("ssh process exited after disconnect request."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_state == SessionState::Connecting || exitCode != 0) {
|
||||
QString rawError = m_lastRawError.trimmed();
|
||||
if (rawError.isEmpty()) {
|
||||
rawError = QStringLiteral("ssh exited with code %1").arg(exitCode);
|
||||
}
|
||||
|
||||
const QString display = mapSshError(rawError);
|
||||
setState(SessionState::Failed, display);
|
||||
emit connectionError(display, rawError);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(SessionState::Disconnected, QStringLiteral("SSH session ended."));
|
||||
}
|
||||
|
||||
void SshSessionBackend::onReadyReadStandardOutput()
|
||||
{
|
||||
const QString chunk = QString::fromUtf8(m_process->readAllStandardOutput());
|
||||
if (chunk.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit outputReceived(chunk);
|
||||
|
||||
if (m_state == SessionState::Connecting && !m_waitingForHostKeyConfirmation
|
||||
&& !m_waitingForPasswordPrompt) {
|
||||
setState(SessionState::Connected, QStringLiteral("SSH session established."));
|
||||
}
|
||||
}
|
||||
|
||||
void SshSessionBackend::onReadyReadStandardError()
|
||||
{
|
||||
const QString chunk = QString::fromUtf8(m_process->readAllStandardError());
|
||||
if (chunk.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastRawError += chunk;
|
||||
emit outputReceived(chunk);
|
||||
|
||||
const QStringList lines = chunk.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
|
||||
for (const QString& line : lines) {
|
||||
const QString trimmed = line.trimmed();
|
||||
if (!trimmed.isEmpty()) {
|
||||
emit eventLogged(trimmed);
|
||||
}
|
||||
|
||||
if (trimmed.contains(QStringLiteral("Are you sure you want to continue connecting"),
|
||||
Qt::CaseInsensitive)
|
||||
&& !m_waitingForHostKeyConfirmation) {
|
||||
m_waitingForHostKeyConfirmation = true;
|
||||
emit eventLogged(QStringLiteral("Awaiting host key confirmation from user."));
|
||||
emit hostKeyConfirmationRequested(trimmed);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.contains(QStringLiteral("password:"), Qt::CaseInsensitive)
|
||||
&& profile().authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0
|
||||
&& !m_passwordSubmitted) {
|
||||
if (m_activeOptions.password.isEmpty()) {
|
||||
const QString message = QStringLiteral("Password prompt received but no password is available.");
|
||||
setState(SessionState::Failed, message);
|
||||
emit connectionError(message, trimmed);
|
||||
return;
|
||||
}
|
||||
|
||||
m_waitingForPasswordPrompt = false;
|
||||
m_passwordSubmitted = true;
|
||||
m_process->write((m_activeOptions.password + QStringLiteral("\n")).toUtf8());
|
||||
emit eventLogged(QStringLiteral("Password prompt received; credentials submitted."));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SshSessionBackend::onConnectedProbeTimeout()
|
||||
{
|
||||
if (m_state != SessionState::Connecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_process->state() == QProcess::Running && !m_waitingForHostKeyConfirmation
|
||||
&& !m_waitingForPasswordPrompt) {
|
||||
setState(SessionState::Connected, QStringLiteral("SSH session established."));
|
||||
}
|
||||
}
|
||||
|
||||
void SshSessionBackend::setState(SessionState state, const QString& message)
|
||||
{
|
||||
m_state = state;
|
||||
emit stateChanged(state, message);
|
||||
emit eventLogged(message);
|
||||
|
||||
if (m_state == SessionState::Connected) {
|
||||
applyTerminalSizeIfAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
|
||||
{
|
||||
const Profile& p = profile();
|
||||
|
||||
if (p.host.trimmed().isEmpty()) {
|
||||
const QString message = QStringLiteral("Host is required for SSH connections.");
|
||||
setState(SessionState::Failed, message);
|
||||
emit connectionError(message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (p.port < 1 || p.port > 65535) {
|
||||
const QString message = QStringLiteral("Port must be between 1 and 65535.");
|
||||
setState(SessionState::Failed, message);
|
||||
emit connectionError(message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList args;
|
||||
args << QStringLiteral("-tt") << QStringLiteral("-p") << QString::number(p.port)
|
||||
<< QStringLiteral("-o") << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o")
|
||||
<< QStringLiteral("ServerAliveInterval=20") << QStringLiteral("-o")
|
||||
<< QStringLiteral("ServerAliveCountMax=2");
|
||||
|
||||
const QString policy = options.knownHostsPolicy.trimmed().isEmpty()
|
||||
? p.knownHostsPolicy.trimmed()
|
||||
: options.knownHostsPolicy.trimmed();
|
||||
|
||||
if (policy.compare(QStringLiteral("Ignore"), Qt::CaseInsensitive) == 0) {
|
||||
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=no")
|
||||
<< QStringLiteral("-o")
|
||||
<< QStringLiteral("UserKnownHostsFile=%1").arg(knownHostsFileForNullDevice());
|
||||
} else if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
|
||||
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=accept-new");
|
||||
} else if (policy.compare(QStringLiteral("Ask"), Qt::CaseInsensitive) == 0) {
|
||||
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=ask");
|
||||
} else {
|
||||
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes");
|
||||
}
|
||||
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||
|
||||
if (p.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
|
||||
if (options.password.isEmpty()) {
|
||||
const QString message = QStringLiteral("Password is required for password authentication.");
|
||||
setState(SessionState::Failed, message);
|
||||
emit connectionError(message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
args << QStringLiteral("-o") << QStringLiteral("PreferredAuthentications=password")
|
||||
<< QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no")
|
||||
<< QStringLiteral("-o") << QStringLiteral("NumberOfPasswordPrompts=1");
|
||||
m_waitingForPasswordPrompt = false;
|
||||
|
||||
QString askPassError;
|
||||
if (!configureAskPass(options, environment, askPassError)) {
|
||||
setState(SessionState::Failed, askPassError);
|
||||
emit connectionError(askPassError, askPassError);
|
||||
return false;
|
||||
}
|
||||
} else if (p.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
|
||||
QString keyPath = options.privateKeyPath.trimmed();
|
||||
if (keyPath.isEmpty()) {
|
||||
keyPath = p.privateKeyPath.trimmed();
|
||||
}
|
||||
|
||||
if (keyPath.isEmpty()) {
|
||||
const QString message = QStringLiteral("Private key path is required.");
|
||||
setState(SessionState::Failed, message);
|
||||
emit connectionError(message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!QFileInfo::exists(keyPath)) {
|
||||
const QString message = QStringLiteral("Private key file does not exist: %1")
|
||||
.arg(keyPath);
|
||||
setState(SessionState::Failed, message);
|
||||
emit connectionError(message, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
args << QStringLiteral("-i") << keyPath << QStringLiteral("-o")
|
||||
<< QStringLiteral("PreferredAuthentications=publickey") << QStringLiteral("-o")
|
||||
<< QStringLiteral("PasswordAuthentication=no");
|
||||
}
|
||||
|
||||
const QString target = p.username.trimmed().isEmpty()
|
||||
? p.host.trimmed()
|
||||
: QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed());
|
||||
args << target;
|
||||
|
||||
m_process->setProcessEnvironment(environment);
|
||||
m_process->setProgram(QStringLiteral("ssh"));
|
||||
m_process->setArguments(args);
|
||||
m_process->setProcessChannelMode(QProcess::SeparateChannels);
|
||||
|
||||
m_process->start();
|
||||
if (!m_process->waitForStarted(3000)) {
|
||||
const QString rawError = m_process->errorString();
|
||||
const QString display = mapSshError(rawError);
|
||||
setState(SessionState::Failed, display);
|
||||
emit connectionError(display, rawError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SshSessionBackend::configureAskPass(const SessionConnectOptions& options,
|
||||
QProcessEnvironment& environment,
|
||||
QString& error)
|
||||
{
|
||||
cleanupAskPassScript();
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
m_askPassScriptPath = QDir::temp().filePath(
|
||||
QStringLiteral("orbithub_askpass_%1.cmd")
|
||||
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces)));
|
||||
#else
|
||||
m_askPassScriptPath = QDir::temp().filePath(
|
||||
QStringLiteral("orbithub_askpass_%1.sh")
|
||||
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces)));
|
||||
#endif
|
||||
|
||||
QFile script(m_askPassScriptPath);
|
||||
if (!script.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
|
||||
error = QStringLiteral("Failed to create temporary askpass helper script.");
|
||||
cleanupAskPassScript();
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&script);
|
||||
#ifdef Q_OS_WIN
|
||||
out << "@echo off\r\n";
|
||||
out << "echo " << options.password << "\r\n";
|
||||
#else
|
||||
const QString escapedPassword = escapeForShellSingleQuotes(options.password);
|
||||
out << "#!/bin/sh\n";
|
||||
out << "printf '%s\\n' '" << escapedPassword << "'\n";
|
||||
#endif
|
||||
out.flush();
|
||||
script.close();
|
||||
|
||||
#ifndef Q_OS_WIN
|
||||
if (!QFile::setPermissions(m_askPassScriptPath,
|
||||
QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
|
||||
error = QStringLiteral("Failed to set permissions on askpass helper script.");
|
||||
cleanupAskPassScript();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
environment.insert(QStringLiteral("SSH_ASKPASS"), m_askPassScriptPath);
|
||||
environment.insert(QStringLiteral("SSH_ASKPASS_REQUIRE"), QStringLiteral("force"));
|
||||
if (!environment.contains(QStringLiteral("DISPLAY"))) {
|
||||
environment.insert(QStringLiteral("DISPLAY"), QStringLiteral(":0"));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SshSessionBackend::cleanupAskPassScript()
|
||||
{
|
||||
if (!m_askPassScriptPath.isEmpty()) {
|
||||
QFile::remove(m_askPassScriptPath);
|
||||
m_askPassScriptPath.clear();
|
||||
}
|
||||
}
|
||||
|
||||
QString SshSessionBackend::mapSshError(const QString& rawError) const
|
||||
{
|
||||
const QString raw = rawError.trimmed();
|
||||
if (raw.contains(QStringLiteral("Permission denied"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("Authentication failed. Check username and credentials.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("Host key verification failed"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("Host key verification failed.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("Could not resolve hostname"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("Host could not be resolved.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("Connection timed out"), Qt::CaseInsensitive)
|
||||
|| raw.contains(QStringLiteral("Operation timed out"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("Connection timed out.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("Connection refused"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("Connection refused by remote host.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("No route to host"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("No route to host.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("Identity file"), Qt::CaseInsensitive)
|
||||
&& raw.contains(QStringLiteral("not accessible"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("Private key file is not accessible.");
|
||||
}
|
||||
if (raw.contains(QStringLiteral("No such file or directory"), Qt::CaseInsensitive)) {
|
||||
if (raw.contains(QStringLiteral("ssh-askpass"), Qt::CaseInsensitive)) {
|
||||
return QStringLiteral("SSH password helper is missing or failed to launch.");
|
||||
}
|
||||
return QStringLiteral("Required file was not found.");
|
||||
}
|
||||
if (raw.isEmpty()) {
|
||||
return QStringLiteral("SSH connection failed for an unknown reason.");
|
||||
}
|
||||
|
||||
return QStringLiteral("SSH connection failed.");
|
||||
}
|
||||
|
||||
QString SshSessionBackend::knownHostsFileForNullDevice() const
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return QStringLiteral("NUL");
|
||||
#else
|
||||
return QStringLiteral("/dev/null");
|
||||
#endif
|
||||
}
|
||||
|
||||
void SshSessionBackend::applyTerminalSizeIfAvailable()
|
||||
{
|
||||
if (m_process->state() != QProcess::Running) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_terminalColumns <= 0 || m_terminalRows <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString command = QStringLiteral("stty cols %1 rows %2\\n")
|
||||
.arg(m_terminalColumns)
|
||||
.arg(m_terminalRows);
|
||||
m_process->write(command.toUtf8());
|
||||
emit eventLogged(
|
||||
QStringLiteral("Applied terminal size: %1x%2").arg(m_terminalColumns).arg(m_terminalRows));
|
||||
}
|
||||
61
src/ssh_session_backend.h
Normal file
61
src/ssh_session_backend.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#ifndef ORBITHUB_SSH_SESSION_BACKEND_H
|
||||
#define ORBITHUB_SSH_SESSION_BACKEND_H
|
||||
|
||||
#include "session_backend.h"
|
||||
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
|
||||
class SshSessionBackend : public SessionBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SshSessionBackend(const Profile& profile, QObject* parent = nullptr);
|
||||
~SshSessionBackend() override;
|
||||
|
||||
public slots:
|
||||
void connectSession(const SessionConnectOptions& options) override;
|
||||
void disconnectSession() override;
|
||||
void reconnectSession(const SessionConnectOptions& options) override;
|
||||
void sendInput(const QString& input) override;
|
||||
void confirmHostKey(bool trustHost) override;
|
||||
void updateTerminalSize(int columns, int rows) override;
|
||||
|
||||
private slots:
|
||||
void onProcessStarted();
|
||||
void onProcessErrorOccurred(QProcess::ProcessError error);
|
||||
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
|
||||
void onReadyReadStandardOutput();
|
||||
void onReadyReadStandardError();
|
||||
void onConnectedProbeTimeout();
|
||||
|
||||
private:
|
||||
QProcess* m_process;
|
||||
QTimer* m_connectedProbeTimer;
|
||||
SessionState m_state;
|
||||
bool m_userInitiatedDisconnect;
|
||||
bool m_reconnectPending;
|
||||
SessionConnectOptions m_reconnectOptions;
|
||||
SessionConnectOptions m_activeOptions;
|
||||
QString m_lastRawError;
|
||||
QString m_askPassScriptPath;
|
||||
bool m_waitingForPasswordPrompt;
|
||||
bool m_waitingForHostKeyConfirmation;
|
||||
bool m_passwordSubmitted;
|
||||
int m_terminalColumns;
|
||||
int m_terminalRows;
|
||||
|
||||
void setState(SessionState state, const QString& message);
|
||||
bool startSshProcess(const SessionConnectOptions& options);
|
||||
bool configureAskPass(const SessionConnectOptions& options,
|
||||
QProcessEnvironment& environment,
|
||||
QString& error);
|
||||
void cleanupAskPassScript();
|
||||
QString mapSshError(const QString& rawError) const;
|
||||
QString knownHostsFileForNullDevice() const;
|
||||
void applyTerminalSizeIfAvailable();
|
||||
};
|
||||
|
||||
#endif
|
||||
520
src/terminal_view.cpp
Normal file
520
src/terminal_view.cpp
Normal file
@@ -0,0 +1,520 @@
|
||||
#include "terminal_view.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QColor>
|
||||
#include <QFocusEvent>
|
||||
#include <QFontMetrics>
|
||||
#include <QKeyEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QTimer>
|
||||
#include <QTextCursor>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
QString normalizedThemeName(const QString& value)
|
||||
{
|
||||
return value.trimmed().toLower();
|
||||
}
|
||||
}
|
||||
|
||||
TerminalView::TerminalView(QWidget* parent)
|
||||
: QTextEdit(parent),
|
||||
m_bold(false),
|
||||
m_hasFgColor(false),
|
||||
m_hasBgColor(false)
|
||||
{
|
||||
setReadOnly(false);
|
||||
setUndoRedoEnabled(false);
|
||||
setAcceptRichText(false);
|
||||
setLineWrapMode(QTextEdit::NoWrap);
|
||||
setContextMenuPolicy(Qt::NoContextMenu);
|
||||
setCursorWidth(2);
|
||||
document()->setMaximumBlockCount(4000);
|
||||
|
||||
applyThemePalette(paletteByName(QStringLiteral("Dark")));
|
||||
resetSgrState();
|
||||
|
||||
QTimer::singleShot(0, this, [this]() {
|
||||
moveCursor(QTextCursor::End);
|
||||
emitTerminalSize();
|
||||
});
|
||||
}
|
||||
|
||||
QStringList TerminalView::themeNames()
|
||||
{
|
||||
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
||||
}
|
||||
|
||||
void TerminalView::setThemeName(const QString& themeName)
|
||||
{
|
||||
applyThemePalette(paletteByName(themeName));
|
||||
}
|
||||
|
||||
void TerminalView::appendTerminalData(const QString& data)
|
||||
{
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString merged = m_pendingEscape + data;
|
||||
m_pendingEscape.clear();
|
||||
|
||||
QString plainBuffer;
|
||||
|
||||
for (int i = 0; i < merged.size();) {
|
||||
const QChar ch = merged.at(i);
|
||||
|
||||
if (ch == QChar::fromLatin1('\x1b')) {
|
||||
if (!plainBuffer.isEmpty()) {
|
||||
appendTextChunk(plainBuffer);
|
||||
plainBuffer.clear();
|
||||
}
|
||||
|
||||
if (i + 1 >= merged.size()) {
|
||||
m_pendingEscape = merged.mid(i);
|
||||
break;
|
||||
}
|
||||
|
||||
if (merged.at(i + 1) != QChar::fromLatin1('[')) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
int end = i + 2;
|
||||
while (end < merged.size()) {
|
||||
const ushort c = merged.at(end).unicode();
|
||||
if (c >= 0x40 && c <= 0x7e) {
|
||||
break;
|
||||
}
|
||||
++end;
|
||||
}
|
||||
|
||||
if (end >= merged.size()) {
|
||||
m_pendingEscape = merged.mid(i);
|
||||
break;
|
||||
}
|
||||
|
||||
const QChar finalByte = merged.at(end);
|
||||
const QString params = merged.mid(i + 2, end - (i + 2));
|
||||
|
||||
if (finalByte == QChar::fromLatin1('m')) {
|
||||
handleSgrSequence(params);
|
||||
} else if (finalByte == QChar::fromLatin1('J')) {
|
||||
if (params.isEmpty() || params == QStringLiteral("2")) {
|
||||
clear();
|
||||
}
|
||||
}
|
||||
|
||||
i = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == QChar::fromLatin1('\r')) {
|
||||
const bool hasLfAfter = (i + 1 < merged.size() && merged.at(i + 1) == QChar::fromLatin1('\n'));
|
||||
if (!hasLfAfter) {
|
||||
plainBuffer.append(QChar::fromLatin1('\n'));
|
||||
}
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
plainBuffer.append(ch);
|
||||
++i;
|
||||
}
|
||||
|
||||
if (!plainBuffer.isEmpty()) {
|
||||
appendTextChunk(plainBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalView::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (event == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveCursor(QTextCursor::End);
|
||||
|
||||
const Qt::KeyboardModifiers modifiers = event->modifiers();
|
||||
|
||||
if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier)
|
||||
&& event->key() == Qt::Key_C) {
|
||||
const QString selected = textCursor().selectedText();
|
||||
if (!selected.isEmpty()) {
|
||||
QApplication::clipboard()->setText(selected);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (modifiers == Qt::ControlModifier) {
|
||||
switch (event->key()) {
|
||||
case Qt::Key_C:
|
||||
emit inputGenerated(QStringLiteral("\x03"));
|
||||
return;
|
||||
case Qt::Key_D:
|
||||
emit inputGenerated(QStringLiteral("\x04"));
|
||||
return;
|
||||
case Qt::Key_L:
|
||||
emit inputGenerated(QStringLiteral("\x0c"));
|
||||
return;
|
||||
case Qt::Key_V: {
|
||||
const QString clipboardText = QApplication::clipboard()->text();
|
||||
if (!clipboardText.isEmpty()) {
|
||||
emit inputGenerated(clipboardText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event->key()) {
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
emit inputGenerated(QStringLiteral("\n"));
|
||||
return;
|
||||
case Qt::Key_Backspace:
|
||||
emit inputGenerated(QStringLiteral("\x7f"));
|
||||
return;
|
||||
case Qt::Key_Tab:
|
||||
emit inputGenerated(QStringLiteral("\t"));
|
||||
return;
|
||||
case Qt::Key_Left:
|
||||
emit inputGenerated(QStringLiteral("\x1b[D"));
|
||||
return;
|
||||
case Qt::Key_Right:
|
||||
emit inputGenerated(QStringLiteral("\x1b[C"));
|
||||
return;
|
||||
case Qt::Key_Up:
|
||||
emit inputGenerated(QStringLiteral("\x1b[A"));
|
||||
return;
|
||||
case Qt::Key_Down:
|
||||
emit inputGenerated(QStringLiteral("\x1b[B"));
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const QString text = event->text();
|
||||
if (!text.isEmpty()) {
|
||||
emit inputGenerated(text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void TerminalView::focusInEvent(QFocusEvent* event)
|
||||
{
|
||||
QTextEdit::focusInEvent(event);
|
||||
moveCursor(QTextCursor::End);
|
||||
}
|
||||
|
||||
void TerminalView::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QTextEdit::resizeEvent(event);
|
||||
emitTerminalSize();
|
||||
}
|
||||
|
||||
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
|
||||
{
|
||||
const QString theme = normalizedThemeName(themeName);
|
||||
|
||||
if (theme == QStringLiteral("light")) {
|
||||
return ThemePalette{QStringLiteral("Light"),
|
||||
QColor(QStringLiteral("#ececec")),
|
||||
QColor(QStringLiteral("#000000")),
|
||||
{QColor(QStringLiteral("#000000")),
|
||||
QColor(QStringLiteral("#aa0000")),
|
||||
QColor(QStringLiteral("#008000")),
|
||||
QColor(QStringLiteral("#7a5f00")),
|
||||
QColor(QStringLiteral("#0033cc")),
|
||||
QColor(QStringLiteral("#8a00a8")),
|
||||
QColor(QStringLiteral("#005f87")),
|
||||
QColor(QStringLiteral("#333333"))},
|
||||
{QColor(QStringLiteral("#5c5c5c")),
|
||||
QColor(QStringLiteral("#d30000")),
|
||||
QColor(QStringLiteral("#00a000")),
|
||||
QColor(QStringLiteral("#9a7700")),
|
||||
QColor(QStringLiteral("#0055ff")),
|
||||
QColor(QStringLiteral("#b300db")),
|
||||
QColor(QStringLiteral("#007ea7")),
|
||||
QColor(QStringLiteral("#111111"))}};
|
||||
}
|
||||
|
||||
if (theme == QStringLiteral("solarized dark")) {
|
||||
return ThemePalette{QStringLiteral("Solarized Dark"),
|
||||
QColor(QStringLiteral("#002b36")),
|
||||
QColor(QStringLiteral("#839496")),
|
||||
{QColor(QStringLiteral("#073642")),
|
||||
QColor(QStringLiteral("#dc322f")),
|
||||
QColor(QStringLiteral("#859900")),
|
||||
QColor(QStringLiteral("#b58900")),
|
||||
QColor(QStringLiteral("#268bd2")),
|
||||
QColor(QStringLiteral("#d33682")),
|
||||
QColor(QStringLiteral("#2aa198")),
|
||||
QColor(QStringLiteral("#eee8d5"))},
|
||||
{QColor(QStringLiteral("#586e75")),
|
||||
QColor(QStringLiteral("#cb4b16")),
|
||||
QColor(QStringLiteral("#586e75")),
|
||||
QColor(QStringLiteral("#657b83")),
|
||||
QColor(QStringLiteral("#839496")),
|
||||
QColor(QStringLiteral("#6c71c4")),
|
||||
QColor(QStringLiteral("#93a1a1")),
|
||||
QColor(QStringLiteral("#fdf6e3"))}};
|
||||
}
|
||||
|
||||
return ThemePalette{QStringLiteral("Dark"),
|
||||
QColor(QStringLiteral("#1e1e1e")),
|
||||
QColor(QStringLiteral("#d4d4d4")),
|
||||
{QColor(QStringLiteral("#000000")),
|
||||
QColor(QStringLiteral("#cd3131")),
|
||||
QColor(QStringLiteral("#0dbc79")),
|
||||
QColor(QStringLiteral("#e5e510")),
|
||||
QColor(QStringLiteral("#2472c8")),
|
||||
QColor(QStringLiteral("#bc3fbc")),
|
||||
QColor(QStringLiteral("#11a8cd")),
|
||||
QColor(QStringLiteral("#e5e5e5"))},
|
||||
{QColor(QStringLiteral("#666666")),
|
||||
QColor(QStringLiteral("#f14c4c")),
|
||||
QColor(QStringLiteral("#23d18b")),
|
||||
QColor(QStringLiteral("#f5f543")),
|
||||
QColor(QStringLiteral("#3b8eea")),
|
||||
QColor(QStringLiteral("#d670d6")),
|
||||
QColor(QStringLiteral("#29b8db")),
|
||||
QColor(QStringLiteral("#ffffff"))}};
|
||||
}
|
||||
|
||||
QColor TerminalView::colorFrom256Index(int index)
|
||||
{
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
if (index > 255) {
|
||||
index = 255;
|
||||
}
|
||||
|
||||
if (index < 16) {
|
||||
static const std::array<QColor, 16> base = {
|
||||
QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#800000")),
|
||||
QColor(QStringLiteral("#008000")), QColor(QStringLiteral("#808000")),
|
||||
QColor(QStringLiteral("#000080")), QColor(QStringLiteral("#800080")),
|
||||
QColor(QStringLiteral("#008080")), QColor(QStringLiteral("#c0c0c0")),
|
||||
QColor(QStringLiteral("#808080")), QColor(QStringLiteral("#ff0000")),
|
||||
QColor(QStringLiteral("#00ff00")), QColor(QStringLiteral("#ffff00")),
|
||||
QColor(QStringLiteral("#0000ff")), QColor(QStringLiteral("#ff00ff")),
|
||||
QColor(QStringLiteral("#00ffff")), QColor(QStringLiteral("#ffffff"))};
|
||||
return base.at(static_cast<size_t>(index));
|
||||
}
|
||||
|
||||
if (index >= 16 && index <= 231) {
|
||||
const int c = index - 16;
|
||||
const int r = c / 36;
|
||||
const int g = (c / 6) % 6;
|
||||
const int b = c % 6;
|
||||
|
||||
const auto channel = [](int v) { return v == 0 ? 0 : 55 + v * 40; };
|
||||
return QColor(channel(r), channel(g), channel(b));
|
||||
}
|
||||
|
||||
const int gray = 8 + (index - 232) * 10;
|
||||
return QColor(gray, gray, gray);
|
||||
}
|
||||
|
||||
void TerminalView::applyThemePalette(const ThemePalette& palette)
|
||||
{
|
||||
m_palette = palette;
|
||||
|
||||
const QString stylesheet = QStringLiteral("QTextEdit { background: %1; color: %2; }")
|
||||
.arg(m_palette.background.name(), m_palette.foreground.name());
|
||||
setStyleSheet(stylesheet);
|
||||
|
||||
if (!m_hasFgColor) {
|
||||
m_fgColor = m_palette.foreground;
|
||||
}
|
||||
if (!m_hasBgColor) {
|
||||
m_bgColor = m_palette.background;
|
||||
}
|
||||
applyCurrentFormat();
|
||||
}
|
||||
|
||||
void TerminalView::applyCurrentFormat()
|
||||
{
|
||||
m_currentFormat = QTextCharFormat();
|
||||
m_currentFormat.setForeground(m_hasFgColor ? m_fgColor : m_palette.foreground);
|
||||
if (m_hasBgColor) {
|
||||
m_currentFormat.setBackground(m_bgColor);
|
||||
}
|
||||
QFont font = currentFont();
|
||||
font.setBold(m_bold);
|
||||
m_currentFormat.setFont(font);
|
||||
}
|
||||
|
||||
void TerminalView::resetSgrState()
|
||||
{
|
||||
m_bold = false;
|
||||
m_hasFgColor = false;
|
||||
m_hasBgColor = false;
|
||||
m_fgColor = m_palette.foreground;
|
||||
m_bgColor = m_palette.background;
|
||||
applyCurrentFormat();
|
||||
}
|
||||
|
||||
void TerminalView::handleSgrSequence(const QString& params)
|
||||
{
|
||||
QStringList parts = params.split(QChar::fromLatin1(';'), Qt::KeepEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
parts.push_back(QStringLiteral("0"));
|
||||
}
|
||||
|
||||
for (int i = 0; i < parts.size(); ++i) {
|
||||
const QString part = parts.at(i).trimmed();
|
||||
bool ok = false;
|
||||
const int code = part.isEmpty() ? 0 : part.toInt(&ok);
|
||||
if (!ok && !part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == 0) {
|
||||
resetSgrState();
|
||||
continue;
|
||||
}
|
||||
if (code == 1) {
|
||||
m_bold = true;
|
||||
continue;
|
||||
}
|
||||
if (code == 22) {
|
||||
m_bold = false;
|
||||
continue;
|
||||
}
|
||||
if (code == 39) {
|
||||
m_hasFgColor = false;
|
||||
continue;
|
||||
}
|
||||
if (code == 49) {
|
||||
m_hasBgColor = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code >= 30 && code <= 37) {
|
||||
m_fgColor = paletteColor(false, code - 30, false);
|
||||
m_hasFgColor = true;
|
||||
continue;
|
||||
}
|
||||
if (code >= 90 && code <= 97) {
|
||||
m_fgColor = paletteColor(false, code - 90, true);
|
||||
m_hasFgColor = true;
|
||||
continue;
|
||||
}
|
||||
if (code >= 40 && code <= 47) {
|
||||
m_bgColor = paletteColor(true, code - 40, false);
|
||||
m_hasBgColor = true;
|
||||
continue;
|
||||
}
|
||||
if (code >= 100 && code <= 107) {
|
||||
m_bgColor = paletteColor(true, code - 100, true);
|
||||
m_hasBgColor = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code == 38 || code == 48) {
|
||||
const bool background = (code == 48);
|
||||
if (i + 1 >= parts.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int mode = parts.at(i + 1).toInt(&ok);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == 5 && i + 2 < parts.size()) {
|
||||
const int index = parts.at(i + 2).toInt(&ok);
|
||||
if (ok) {
|
||||
const QColor color = colorFrom256Index(index);
|
||||
if (background) {
|
||||
m_bgColor = color;
|
||||
m_hasBgColor = true;
|
||||
} else {
|
||||
m_fgColor = color;
|
||||
m_hasFgColor = true;
|
||||
}
|
||||
}
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode == 2 && i + 4 < parts.size()) {
|
||||
const int r = parts.at(i + 2).toInt(&ok);
|
||||
if (!ok) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
const int g = parts.at(i + 3).toInt(&ok);
|
||||
if (!ok) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
const int b = parts.at(i + 4).toInt(&ok);
|
||||
if (!ok) {
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
const QColor color(r, g, b);
|
||||
if (background) {
|
||||
m_bgColor = color;
|
||||
m_hasBgColor = true;
|
||||
} else {
|
||||
m_fgColor = color;
|
||||
m_hasFgColor = true;
|
||||
}
|
||||
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyCurrentFormat();
|
||||
}
|
||||
|
||||
void TerminalView::appendTextChunk(const QString& text)
|
||||
{
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTextCursor cursor = textCursor();
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
cursor.insertText(text, m_currentFormat);
|
||||
setTextCursor(cursor);
|
||||
ensureCursorVisible();
|
||||
}
|
||||
|
||||
QColor TerminalView::paletteColor(bool, int index, bool bright) const
|
||||
{
|
||||
const int safeIndex = std::clamp(index, 0, 7);
|
||||
return bright ? m_palette.bright.at(static_cast<size_t>(safeIndex))
|
||||
: m_palette.normal.at(static_cast<size_t>(safeIndex));
|
||||
}
|
||||
|
||||
int TerminalView::terminalColumns() const
|
||||
{
|
||||
const QFontMetrics metrics(font());
|
||||
const int cellWidth = std::max(1, metrics.horizontalAdvance(QChar::fromLatin1('M')));
|
||||
return std::max(1, viewport()->width() / cellWidth);
|
||||
}
|
||||
|
||||
int TerminalView::terminalRows() const
|
||||
{
|
||||
const QFontMetrics metrics(font());
|
||||
const int cellHeight = std::max(1, metrics.lineSpacing());
|
||||
return std::max(1, viewport()->height() / cellHeight);
|
||||
}
|
||||
|
||||
void TerminalView::emitTerminalSize()
|
||||
{
|
||||
emit terminalSizeChanged(terminalColumns(), terminalRows());
|
||||
}
|
||||
66
src/terminal_view.h
Normal file
66
src/terminal_view.h
Normal file
@@ -0,0 +1,66 @@
|
||||
#ifndef ORBITHUB_TERMINAL_VIEW_H
|
||||
#define ORBITHUB_TERMINAL_VIEW_H
|
||||
|
||||
#include <QTextEdit>
|
||||
|
||||
#include <array>
|
||||
|
||||
class QKeyEvent;
|
||||
class QFocusEvent;
|
||||
class QResizeEvent;
|
||||
|
||||
class TerminalView : public QTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TerminalView(QWidget* parent = nullptr);
|
||||
|
||||
static QStringList themeNames();
|
||||
void setThemeName(const QString& themeName);
|
||||
void appendTerminalData(const QString& data);
|
||||
|
||||
signals:
|
||||
void inputGenerated(const QString& input);
|
||||
void terminalSizeChanged(int columns, int rows);
|
||||
|
||||
protected:
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
void focusInEvent(QFocusEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
private:
|
||||
struct ThemePalette {
|
||||
QString name;
|
||||
QColor background;
|
||||
QColor foreground;
|
||||
std::array<QColor, 8> normal;
|
||||
std::array<QColor, 8> bright;
|
||||
};
|
||||
|
||||
ThemePalette m_palette;
|
||||
QString m_pendingEscape;
|
||||
QString m_rawHistory;
|
||||
bool m_bold;
|
||||
bool m_hasFgColor;
|
||||
bool m_hasBgColor;
|
||||
QColor m_fgColor;
|
||||
QColor m_bgColor;
|
||||
QTextCharFormat m_currentFormat;
|
||||
|
||||
static ThemePalette paletteByName(const QString& themeName);
|
||||
static QColor colorFrom256Index(int index);
|
||||
|
||||
void applyThemePalette(const ThemePalette& palette);
|
||||
void applyCurrentFormat();
|
||||
void resetSgrState();
|
||||
void handleSgrSequence(const QString& params);
|
||||
void appendTextChunk(const QString& text);
|
||||
QColor paletteColor(bool background, int index, bool bright) const;
|
||||
void processData(const QString& data, bool storeInHistory);
|
||||
int terminalColumns() const;
|
||||
int terminalRows() const;
|
||||
void emitTerminalSize();
|
||||
};
|
||||
|
||||
#endif
|
||||
39
src/unsupported_session_backend.cpp
Normal file
39
src/unsupported_session_backend.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "unsupported_session_backend.h"
|
||||
|
||||
UnsupportedSessionBackend::UnsupportedSessionBackend(const Profile& profile, QObject* parent)
|
||||
: SessionBackend(profile, parent)
|
||||
{
|
||||
}
|
||||
|
||||
void UnsupportedSessionBackend::connectSession(const SessionConnectOptions&)
|
||||
{
|
||||
const QString message = QStringLiteral("Protocol '%1' is not implemented yet.")
|
||||
.arg(profile().protocol);
|
||||
emit eventLogged(message);
|
||||
emit stateChanged(SessionState::Failed, message);
|
||||
emit connectionError(message, message);
|
||||
}
|
||||
|
||||
void UnsupportedSessionBackend::disconnectSession()
|
||||
{
|
||||
emit stateChanged(SessionState::Disconnected,
|
||||
QStringLiteral("No active connection for this protocol."));
|
||||
}
|
||||
|
||||
void UnsupportedSessionBackend::reconnectSession(const SessionConnectOptions& options)
|
||||
{
|
||||
connectSession(options);
|
||||
}
|
||||
|
||||
void UnsupportedSessionBackend::sendInput(const QString&)
|
||||
{
|
||||
emit eventLogged(QStringLiteral("Input ignored: protocol backend is not interactive."));
|
||||
}
|
||||
|
||||
void UnsupportedSessionBackend::confirmHostKey(bool)
|
||||
{
|
||||
}
|
||||
|
||||
void UnsupportedSessionBackend::updateTerminalSize(int, int)
|
||||
{
|
||||
}
|
||||
22
src/unsupported_session_backend.h
Normal file
22
src/unsupported_session_backend.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef ORBITHUB_UNSUPPORTED_SESSION_BACKEND_H
|
||||
#define ORBITHUB_UNSUPPORTED_SESSION_BACKEND_H
|
||||
|
||||
#include "session_backend.h"
|
||||
|
||||
class UnsupportedSessionBackend : public SessionBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UnsupportedSessionBackend(const Profile& profile, QObject* parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void connectSession(const SessionConnectOptions& options) override;
|
||||
void disconnectSession() override;
|
||||
void reconnectSession(const SessionConnectOptions& options) override;
|
||||
void sendInput(const QString& input) override;
|
||||
void confirmHostKey(bool trustHost) override;
|
||||
void updateTerminalSize(int columns, int rows) override;
|
||||
};
|
||||
|
||||
#endif
|
||||
124
third_party/FreeRDP/.clang-format
vendored
Normal file
124
third_party/FreeRDP/.clang-format
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
AccessModifierOffset: -2
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: true
|
||||
AfterControlStatement: true
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: true
|
||||
AfterObjCDeclaration: true
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
AfterExternBlock: true
|
||||
BeforeCatch: true
|
||||
BeforeElse: true
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Allman
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: false
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: false
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: false
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
TabWidth: 4
|
||||
UseTab: ForIndentation
|
||||
...
|
||||
Language: Cpp
|
||||
Standard: Auto
|
||||
NamespaceIndentation: All
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
...
|
||||
Language: ObjC
|
||||
PointerBindsToType: false
|
||||
SortIncludes: false
|
||||
ObjCBlockIndentWidth: 4
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
...
|
||||
Language: Java
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
...
|
||||
Language: JavaScript
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
...
|
||||
Language: Proto
|
||||
...
|
||||
Language: TableGen
|
||||
...
|
||||
Language: TextProto
|
||||
...
|
||||
138
third_party/FreeRDP/.clang-tidy
vendored
Normal file
138
third_party/FreeRDP/.clang-tidy
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
Checks: >
|
||||
-*,
|
||||
abseil-*,
|
||||
altera-*,
|
||||
bugprone-*,
|
||||
cert-*,
|
||||
clang-analyzer*,
|
||||
concurrency-*,
|
||||
cppcoreguidelines*,
|
||||
google-*,
|
||||
hicpp-*,
|
||||
llvm-*,
|
||||
modernize-*,
|
||||
objc-*,
|
||||
openmp-*,
|
||||
performance-*,
|
||||
portability-*,
|
||||
readability-*,
|
||||
-altera-id-dependent-backward-branch,
|
||||
-altera-struct-pack-align,
|
||||
-altera-unroll-loops,
|
||||
-cppcoreguidelines-interfaces-global-init,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-assignment-in-if-condition,
|
||||
-bugprone-branch-clone,
|
||||
-bugprone-macro-parentheses,
|
||||
-cert-dcl16-c,
|
||||
-cert-env33-c,
|
||||
-cert-dcl50-cpp,
|
||||
-clang-analyzer-webkit.NoUncountedMemberChecker,
|
||||
-clang-analyzer-optin.performance.Padding,
|
||||
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
|
||||
-clang-analyzer-security.VAList,
|
||||
-clang-analyzer-valist.Uninitialized,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-avoid-c-arrays,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-avoid-non-const-global-variables,
|
||||
-cppcoreguidelines-macro-to-enum,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-no-malloc,
|
||||
-cppcoreguidelines-use-enum-class,
|
||||
-google-readability-braces-around-statements,
|
||||
-google-readability-todo,
|
||||
-hicpp-avoid-c-arrays,
|
||||
-hicpp-braces-around-statements,
|
||||
-hicpp-no-array-decay,
|
||||
-hicpp-no-assembler,
|
||||
-hicpp-multiway-paths-covered,
|
||||
-hicpp-signed-bitwise,
|
||||
-hicpp-uppercase-literal-suffix,
|
||||
-hicpp-vararg,
|
||||
-hicpp-no-malloc,
|
||||
-llvm-use-ranges,
|
||||
-llvm-header-guard,
|
||||
-llvm-include-order,
|
||||
-llvm-qualified-auto,
|
||||
-llvm-else-after-return,
|
||||
-readability-else-after-return,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
-modernize-use-using,
|
||||
-modernize-avoid-variadic-functions,
|
||||
-modernize-use-trailing-return-type,
|
||||
-modernize-return-braced-init-list,
|
||||
-modernize-macro-to-enum,
|
||||
-modernize-pass-by-value,
|
||||
-modernize-avoid-c-arrays,
|
||||
-readability-use-anyofallof,
|
||||
-readability-braces-around-statements,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-identifier-length,
|
||||
-readability-implicit-bool-conversion,
|
||||
-readability-magic-numbers,
|
||||
-readability-math-missing-parentheses,
|
||||
-readability-misleading-indentation,
|
||||
-readability-qualified-auto,
|
||||
-readability-redundant-parentheses,
|
||||
-readability-suspicious-call-argument,
|
||||
-readability-string-compare,
|
||||
-readability-uppercase-literal-suffix,
|
||||
-readability-use-concise-preprocessor-directives,
|
||||
-performance-no-int-to-ptr,
|
||||
-performance-enum-size,
|
||||
-performance-avoid-endl,
|
||||
-portability-avoid-pragma-once
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
FormatStyle: file
|
||||
User: nin
|
||||
CheckOptions:
|
||||
- key: readability-implicit-bool-conversion.AllowIntegerConditions
|
||||
value: 'true'
|
||||
- key: llvm-else-after-return.WarnOnConditionVariables
|
||||
value: 'false'
|
||||
- key: modernize-loop-convert.MinConfidence
|
||||
value: reasonable
|
||||
- key: modernize-replace-auto-ptr.IncludeStyle
|
||||
value: llvm
|
||||
- key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons
|
||||
value: 'false'
|
||||
- key: google-readability-namespace-comments.ShortNamespaceLines
|
||||
value: '10'
|
||||
- key: cert-err33-c.CheckedFunctions
|
||||
value: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;'
|
||||
- key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField
|
||||
value: 'false'
|
||||
- key: cert-dcl16-c.NewSuffixes
|
||||
value: 'L;LL;LU;LLU'
|
||||
- key: google-readability-braces-around-statements.ShortStatementLines
|
||||
value: '1'
|
||||
- key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic
|
||||
value: 'true'
|
||||
- key: google-readability-namespace-comments.SpacesBeforeComments
|
||||
value: '2'
|
||||
- key: modernize-loop-convert.MaxCopySize
|
||||
value: '16'
|
||||
- key: modernize-pass-by-value.IncludeStyle
|
||||
value: llvm
|
||||
- key: modernize-use-nullptr.NullMacros
|
||||
value: 'NULL'
|
||||
- key: llvm-qualified-auto.AddConstToQualified
|
||||
value: 'false'
|
||||
- key: modernize-loop-convert.NamingStyle
|
||||
value: CamelCase
|
||||
- key: llvm-else-after-return.WarnOnUnfixable
|
||||
value: 'false'
|
||||
- key: google-readability-function-size.StatementThreshold
|
||||
value: '800'
|
||||
...
|
||||
|
||||
|
||||
3
third_party/FreeRDP/.gitattributes
vendored
Normal file
3
third_party/FreeRDP/.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.github export-ignore
|
||||
28
third_party/FreeRDP/.github/ISSUE_TEMPLATE.md
vendored
Normal file
28
third_party/FreeRDP/.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
## Found a bug? - We would like to help you and smash the bug away.
|
||||
1. __Please don't "report" questions as bugs.__
|
||||
* We are reachable via
|
||||
* Matrix room : #FreeRDP:matrix.org (main)
|
||||
* XMPP channel: #FreeRDP#matrix.org@matrix.org (bridged)
|
||||
* IRC channel : #freerdp @ irc.oftc.net (bridged)
|
||||
* We are reachable via mailing list <freerdp-devel@lists.sourceforge.net>
|
||||
* Try our mailing list for discussions/questions
|
||||
1. Before reporting a bug have a look into our issue tracker to see if the bug was already reported and you can add some additional information.
|
||||
1. If it's a __new__ bug - create a new issue.
|
||||
1. For more details see https://github.com/FreeRDP/FreeRDP/wiki/BugReporting
|
||||
|
||||
## To save time and help us identify the issue a bug report should at least contain the following:
|
||||
* a useful description of the bug - "It's not working" isn't good enough - you must try harder ;)
|
||||
* the steps to reproduce the bug
|
||||
* command line you have used
|
||||
* to what system did you connect to? (win8, 2008, ..)
|
||||
* what did you expect to happen?
|
||||
* what actually happened?
|
||||
* freerdp version (e.g. xfreerdp --version) or package version or git commit
|
||||
* freerdp configuration (e.g. xfreerdp --buildconfig)
|
||||
* operating System, architecture, distribution e.g. linux, amd64, debian
|
||||
* if you built it yourself add some notes which branch you have used, also your cmake parameters can help
|
||||
* extra information helping us to find the bug
|
||||
|
||||
## Please remove this text before submitting your issue!
|
||||
|
||||
_Thank you for reporting a bug!_
|
||||
7
third_party/FreeRDP/.github/ISSUE_TEMPLATE/backport.md
vendored
Normal file
7
third_party/FreeRDP/.github/ISSUE_TEMPLATE/backport.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Backport
|
||||
about: Create a issue to request/track a backport
|
||||
|
||||
---
|
||||
|
||||
Related pull request for master:
|
||||
55
third_party/FreeRDP/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
55
third_party/FreeRDP/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Found a bug? - We would like to help you and smash the bug away.**
|
||||
1. __Please don't "report" questions as bugs. For these (questions/build instructions/...) please use one of the following means of contact:__
|
||||
* We are reachable via:
|
||||
* Matrix room : #FreeRDP:matrix.org (main)
|
||||
* XMPP channel: #FreeRDP#matrix.org@matrix.org (bridged)
|
||||
* IRC channel : #freerdp @ irc.oftc.net (bridged)
|
||||
* We are reachable via mailing list <freerdp-devel@lists.sourceforge.net>
|
||||
* Try our mailing list for discussions/questions
|
||||
1. Before reporting a bug have a look into our issue tracker to see if the bug was already reported and you can add some additional information.
|
||||
1. If it's a __new__ bug - create a new issue.
|
||||
1. For more details see https://github.com/FreeRDP/FreeRDP/wiki/BugReporting
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Application details**
|
||||
* FreeRDP version (`xfreerdp /version`)
|
||||
* Command line used
|
||||
* Output of `xfreerdp /buildconfig`
|
||||
* OS version connecting to (server side)
|
||||
* If available the log output from a run with `/log-level:trace 2>&1 | tee log.txt`
|
||||
* If you built it yourself add some notes which tag/commit/branch you have used, also your cmake parameters and
|
||||
compiler can help
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. Linux/Windows/Android/..]
|
||||
- Version/Distribution: [e.g. Debian 10, Windows 2008, Android 10]
|
||||
- Architecture: [amd64, arm]:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
** Please remove this text before submitting your issue!
|
||||
|
||||
_Thank you for reporting a bug!_
|
||||
17
third_party/FreeRDP/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
third_party/FreeRDP/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
29
third_party/FreeRDP/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
29
third_party/FreeRDP/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
## This is how are pull requests handled by FreeRDP
|
||||
1. Every new pull request needs to build and pass the unit tests at https://ci.freerdp.com
|
||||
1. At least 1 (better two) people need to review and test a pull request and agree to accept
|
||||
|
||||
## Preparations before creating a pull
|
||||
* Rebase your branch to current master, no merges allowed!
|
||||
* Try to clean up your commit history, group changes to commits
|
||||
* Check your formatting! A _clang-format_ script can be found at ```.clang-format```
|
||||
* The cmake target ```clangformat``` reformats the whole codebase
|
||||
* Optional (but higly recommended)
|
||||
* Run a clang scanbuild before and after your changes to avoid introducing new bugs
|
||||
* Run your compiler at pedantic level to check for new warnings
|
||||
|
||||
## To ease accepting your contribution
|
||||
* Give the pull request a proper name so people looking at it have an basic idea what it is for
|
||||
* Add at least a brief description what it does (or should do :) and what it's good for
|
||||
* Give instructions on how to test your changes
|
||||
* Ideally add unit tests if adding new features
|
||||
|
||||
## What you should be prepared for
|
||||
* fix issues found during the review phase
|
||||
* Joining our chat to talk to other developers or help them test your pull might accelerate acceptance
|
||||
* Matrix room : #FreeRDP:matrix.org (main)
|
||||
* XMPP channel: #FreeRDP#matrix.org@matrix.org (bridged)
|
||||
* IRC channel : #freerdp @ irc.oftc.net (bridged)
|
||||
* Joining our mailing list <freerdp-devel@lists.sourceforge.net> may be helpful too.
|
||||
* Check the pull request builder at https://ci.freerdp.com/job/code-quality-checker/ and fix all warnings affecting your code
|
||||
|
||||
## Please remove this text before submitting your pull!
|
||||
57
third_party/FreeRDP/.github/workflows/abi-checker.yml
vendored
Normal file
57
third_party/FreeRDP/.github/workflows/abi-checker.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: abi-checker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
inputs:
|
||||
API_BASE_REF:
|
||||
description: 'Base revision for ABI compatibility check'
|
||||
required: true
|
||||
default: '3.6.0'
|
||||
pull_request:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 4 * * SUN'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Run ABI checker on ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out pull request"
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'pull_request' }}
|
||||
uses: suzuki-shunsuke/get-pr-action@v0.1.0
|
||||
id: pr
|
||||
|
||||
- name: "Check out source"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{steps.pr.outputs.merge_commit_sha}}
|
||||
|
||||
- name: "Prepare environment"
|
||||
run: |
|
||||
sudo apt-get update -q -y
|
||||
sudo apt-get --fix-broken install -q -y
|
||||
sudo apt-get install -q -y devscripts equivs abigail-tools \
|
||||
clang \
|
||||
pylint \
|
||||
curl
|
||||
./packaging/scripts/prepare_deb_freerdp-nightly.sh
|
||||
sudo mk-build-deps -i
|
||||
|
||||
- name: "Prepare configuration"
|
||||
run: |
|
||||
mkdir -p abi-checker
|
||||
cp scripts/abi-diff.sh abi-checker/
|
||||
echo "GITHUB_BASE_REF=$GITHUB_BASE_REF"
|
||||
echo "GITHUB_HEAD_REF=$GITHUB_HEAD_REF"
|
||||
echo "API_BASE_REF=${{ inputs.API_BASE_REF || '3.6.0' }}"
|
||||
echo "HEAD=$(git rev-parse HEAD)"
|
||||
echo "remotes=$(git remote -v)"
|
||||
|
||||
- name: "Run ABI check..."
|
||||
env:
|
||||
BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'workflow_dispatch' && inputs.API_BASE_REF || '3.6.0' }}
|
||||
run: |
|
||||
echo "BASE_REF=$BASE_REF"
|
||||
./abi-checker/abi-diff.sh $BASE_REF
|
||||
56
third_party/FreeRDP/.github/workflows/alt-architectures.yml
vendored
Normal file
56
third_party/FreeRDP/.github/workflows/alt-architectures.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: '[arm,ppc,ricsv] architecture builds'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 5 * * SUN'
|
||||
|
||||
jobs:
|
||||
build_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Test on ${{ matrix.distro }}/${{ matrix.arch }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: armv7
|
||||
distro: bookworm
|
||||
- arch: aarch64
|
||||
distro: bookworm
|
||||
- arch: s390x
|
||||
distro: bookworm
|
||||
- arch: ppc64le
|
||||
distro: bookworm
|
||||
- arch: riscv64
|
||||
distro: ubuntu24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: uraimo/run-on-arch-action@v3.0.1
|
||||
name: "Run tests"
|
||||
id: build
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
distro: ${{ matrix.distro }}
|
||||
githubToken: ${{ github.token }}
|
||||
env: |
|
||||
CTEST_OUTPUT_ON_FAILURE: 1
|
||||
WLOG_LEVEL: 'trace'
|
||||
install: |
|
||||
echo "whoami: $(whoami)"
|
||||
echo "working directory: $(pwd)"
|
||||
apt-get update -q -y
|
||||
apt-get install -q -y devscripts clang ninja-build ccache equivs
|
||||
|
||||
run: |
|
||||
echo "whoami: $(whoami)"
|
||||
echo "working directory: $(pwd)"
|
||||
find . -name control -exec mk-build-deps -i -t "apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y" {} \;
|
||||
cmake -GNinja \
|
||||
-C ci/cmake-preloads/config-linux-alt-arch.txt \
|
||||
-B ci-build \
|
||||
-S . \
|
||||
-DCMAKE_INSTALL_PREFIX=/tmp/ci-test \
|
||||
-DCMAKE_C_COMPILER=/usr/bin/clang \
|
||||
-DCMAKE_CXX_COMPILER=/usr/bin/clang++
|
||||
cmake --build ci-build --parallel $(nproc) --target install
|
||||
cmake --build ci-build --parallel $(nproc) --target test
|
||||
26
third_party/FreeRDP/.github/workflows/bash-format.yml
vendored
Normal file
26
third_party/FreeRDP/.github/workflows/bash-format.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: bash-format
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
pull_request:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 4 * * SUN'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "bash-format"
|
||||
steps:
|
||||
- name: "Check out source"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Prepare environment"
|
||||
run: |
|
||||
sudo apt-get update -q -y
|
||||
sudo apt-get install -q -y \
|
||||
shfmt
|
||||
|
||||
- name: "Run shfmt..."
|
||||
run: |
|
||||
./scripts/bash-format.sh
|
||||
24
third_party/FreeRDP/.github/workflows/clang-tidy-post.yml
vendored
Normal file
24
third_party/FreeRDP/.github/workflows/clang-tidy-post.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Post clang-tidy review comments
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["clang-tidy-review"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: akallabeth/clang-tidy-review/post@master
|
||||
# lgtm_comment_body, max_comments, and annotations need to be set on the posting workflow in a split setup
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
annotations: false
|
||||
max_comments: 10
|
||||
28
third_party/FreeRDP/.github/workflows/clang-tidy.yml
vendored
Normal file
28
third_party/FreeRDP/.github/workflows/clang-tidy.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: clang-tidy-review
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master, stable* ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Run clang-tidy
|
||||
- uses: akallabeth/clang-tidy-review@master
|
||||
id: review
|
||||
with:
|
||||
split_workflow: true
|
||||
clang_tidy_checks: ''
|
||||
apt_packages: devscripts,equivs
|
||||
install_commands: 'ln -s packaging/deb/freerdp-nightly debian; mk-build-deps -i -t "apt-get -o Debug::pkgProblemResolver=yes --no-install-recommends -y"'
|
||||
|
||||
# CMake command to run in order to generate compile_commands.json
|
||||
build_dir: tidy
|
||||
cmake_command: cmake -Btidy -S. -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -C ci/cmake-preloads/config-qa.cmake
|
||||
|
||||
# Uploads an artefact containing clang_fixes.json
|
||||
- uses: akallabeth/clang-tidy-review/upload@master
|
||||
id: upload-review
|
||||
26
third_party/FreeRDP/.github/workflows/cmake-format.yml
vendored
Normal file
26
third_party/FreeRDP/.github/workflows/cmake-format.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: cmake-format
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
pull_request:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 4 * * SUN'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "cmake-format"
|
||||
steps:
|
||||
- name: "Check out source"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Prepare environment"
|
||||
run: |
|
||||
sudo apt-get update -q -y
|
||||
sudo apt-get install -q -y \
|
||||
cmake-format
|
||||
|
||||
- name: "Run cmake-format..."
|
||||
run: |
|
||||
./scripts/cmake-format.sh
|
||||
93
third_party/FreeRDP/.github/workflows/codeql-analysis.yml
vendored
Normal file
93
third_party/FreeRDP/.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '41 2 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: c-cpp
|
||||
build-mode: manual
|
||||
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- if: matrix.build-mode == 'manual'
|
||||
run: |
|
||||
sudo apt-get update -q -y
|
||||
sudo apt-get install -q -y devscripts clang ccache ninja-build equivs
|
||||
./packaging/scripts/prepare_deb_freerdp-nightly.sh
|
||||
sudo mk-build-deps -i
|
||||
mkdir ci-build
|
||||
cd ci-build
|
||||
export CC=/usr/bin/clang
|
||||
export CXX=/usr/bin/clang++
|
||||
export CFLAGS="-Weverything"
|
||||
export CXXFLAGS="-Weverything"
|
||||
cmake -GNinja ../ci/cmake-preloads/config-linux-all.txt ..
|
||||
cmake --build .
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
26
third_party/FreeRDP/.github/workflows/codespell.yml
vendored
Normal file
26
third_party/FreeRDP/.github/workflows/codespell.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: codespell
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
pull_request:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 4 * * SUN'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "codespell"
|
||||
steps:
|
||||
- name: "Check out source"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Prepare environment"
|
||||
run: |
|
||||
sudo apt-get update -q -y
|
||||
sudo apt-get install -q -y \
|
||||
codespell
|
||||
|
||||
- name: "Run codespell..."
|
||||
run: |
|
||||
./scripts/codespell.sh
|
||||
59
third_party/FreeRDP/.github/workflows/coverity.yml
vendored
Normal file
59
third_party/FreeRDP/.github/workflows/coverity.yml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
name: Coverity
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'FreeRDP' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install apt dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
devscripts \
|
||||
ninja-build \
|
||||
equivs \
|
||||
ccache \
|
||||
clang
|
||||
sudo mk-build-deps --install packaging/deb/freerdp-nightly/control
|
||||
|
||||
- name: Download Coverity build tool
|
||||
run: |
|
||||
wget -c -N https://scan.coverity.com/download/linux64 --post-data "token=${{ secrets.COVERITY_SCAN_TOKEN }}&project=FreeRDP" -O coverity_tool.tar.gz
|
||||
mkdir coverity_tool
|
||||
tar xzf coverity_tool.tar.gz --strip 1 -C coverity_tool
|
||||
|
||||
- name: Build with Coverity build tool
|
||||
run: |
|
||||
export PATH=`pwd`/coverity_tool/bin:$PATH
|
||||
export CC=/usr/bin/clang
|
||||
export CXX=/usr/bin/clang++
|
||||
cov-configure --template --compiler clang --comptype clangcc
|
||||
# in source build is used to help coverity to determine relative file path
|
||||
cmake \
|
||||
-GNinja \
|
||||
-C ci/cmake-preloads/config-coverity.txt \
|
||||
-DCOVERITY_BUILD=ON \
|
||||
-Bcov-build \
|
||||
-S.
|
||||
cov-build --dir cov-int cmake --build cov-build
|
||||
|
||||
- name: Submit build result to Coverity Scan
|
||||
run: |
|
||||
tar czvf cov.tar.gz cov-int
|
||||
curl --form token=${{ secrets.COVERITY_SCAN_TOKEN }} \
|
||||
--form email=team+coverity@freerdp.com \
|
||||
--form file=@cov.tar.gz \
|
||||
--form version="Commit $GITHUB_SHA" \
|
||||
--form description="Build submitted via CI" \
|
||||
https://scan.coverity.com/builds?project=FreeRDP
|
||||
71
third_party/FreeRDP/.github/workflows/freebsd.yml
vendored
Normal file
71
third_party/FreeRDP/.github/workflows/freebsd.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
name: '[freebsd] architecture builds'
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 5 * * SAT'
|
||||
|
||||
jobs:
|
||||
freebsd_job:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build on FreeBSD
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test in FreeBSD
|
||||
id: test
|
||||
uses: vmactions/freebsd-vm@v1
|
||||
with:
|
||||
usesh: true
|
||||
copyback: false
|
||||
prepare: |
|
||||
pkg install -y \
|
||||
cmake \
|
||||
ninja \
|
||||
krb5-devel \
|
||||
json-c \
|
||||
libcjson \
|
||||
fdk-aac \
|
||||
libsoxr \
|
||||
sdl2 \
|
||||
sdl3 \
|
||||
sdl2_ttf \
|
||||
sdl2_image \
|
||||
opus \
|
||||
png \
|
||||
webp \
|
||||
openjpeg \
|
||||
libjpeg-turbo \
|
||||
opensc \
|
||||
v4l_compat \
|
||||
libv4l \
|
||||
uriparser \
|
||||
ffmpeg \
|
||||
pulseaudio \
|
||||
pcsc-lite \
|
||||
cups \
|
||||
opencl \
|
||||
openssl34 \
|
||||
gsm \
|
||||
influxpkg-config \
|
||||
icu \
|
||||
fusefs-libs3 \
|
||||
ccache \
|
||||
opencl-clang-llvm15 \
|
||||
faac \
|
||||
faad2 \
|
||||
opus-tools \
|
||||
openh264 \
|
||||
alsa-lib \
|
||||
cairo \
|
||||
ocl-icd
|
||||
|
||||
run: |
|
||||
export LD_LIBRARY_PATH=/usr/lib/clang/18/lib/freebsd
|
||||
export CTEST_OUTPUT_ON_FAILURE=1
|
||||
cmake -GNinja \
|
||||
-C ci/cmake-preloads/config-freebsd.txt \
|
||||
-B ci-build \
|
||||
-S . \
|
||||
-DCMAKE_INSTALL_PREFIX=/tmp/ci-test
|
||||
cmake --build ci-build --parallel $(nproc) --target install
|
||||
cmake --build ci-build --parallel $(nproc) --target test
|
||||
43
third_party/FreeRDP/.github/workflows/fuzzing.yml
vendored
Normal file
43
third_party/FreeRDP/.github/workflows/fuzzing.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Fuzzing testing
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: "0 3 21 * *"
|
||||
|
||||
jobs:
|
||||
fuzzing:
|
||||
if: github.repository == 'FreeRDP/FreeRDP'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sanitizer: [address, undefined]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build fuzzers (${{ matrix.sanitizer }})
|
||||
id: build
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'freerdp'
|
||||
dry-run: false
|
||||
sanitizer: ${{ matrix.sanitizer }}
|
||||
- name: Run fuzzers (${{ matrix.sanitizer }})
|
||||
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
|
||||
with:
|
||||
oss-fuzz-project-name: 'freerdp'
|
||||
fuzz-seconds: 600
|
||||
dry-run: false
|
||||
sanitizer: ${{ matrix.sanitizer }}
|
||||
- name: Upload crash
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: ${{ matrix.sanitizer }}-artifacts
|
||||
retention-days: 21
|
||||
path: ./out/artifacts
|
||||
29
third_party/FreeRDP/.github/workflows/issue-autoclose.yml
vendored
Normal file
29
third_party/FreeRDP/.github/workflows/issue-autoclose.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
schedule:
|
||||
- cron: "33 3 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-stale: 30
|
||||
days-before-close: 30
|
||||
operations-per-run: 90
|
||||
exempt-all-milestones: true
|
||||
exempt-assignees: true
|
||||
exempt-issue-labels: "wip,pinned,help-wanted,blocker,feature"
|
||||
exempt-pr-labels: "wip,pinned,help-wanted,blocker,feature"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
22
third_party/FreeRDP/.github/workflows/macos.yml
vendored
Normal file
22
third_party/FreeRDP/.github/workflows/macos.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: macos-builder
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 5 * * SUN'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
name: "Run macos build on mac-latest"
|
||||
steps:
|
||||
- name: "Check out source"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Prepare environment"
|
||||
run: |
|
||||
brew install autoconf automake git libtool meson
|
||||
|
||||
- name: "Run mac os build..."
|
||||
run: |
|
||||
./scripts/bundle-mac-os.sh
|
||||
35
third_party/FreeRDP/.github/workflows/mingw.yml
vendored
Normal file
35
third_party/FreeRDP/.github/workflows/mingw.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: mingw-builder
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: '30 5 * * SUN'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Run mingw build on ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out source"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Prepare environment"
|
||||
run: |
|
||||
sudo apt-get update -q -y
|
||||
sudo apt-get install -q -y \
|
||||
git \
|
||||
nasm \
|
||||
meson \
|
||||
cmake \
|
||||
ninja-build \
|
||||
mingw-w64 \
|
||||
mingw-w64-tools \
|
||||
binutils-mingw-w64
|
||||
|
||||
- name: "Run mingw [shared] build..."
|
||||
run: |
|
||||
./scripts/mingw.sh
|
||||
|
||||
- name: "Run mingw [static] build..."
|
||||
run: |
|
||||
./scripts/mingw.sh -c -s --clean-first
|
||||
61
third_party/FreeRDP/.github/workflows/timezone-update.yml
vendored
Normal file
61
third_party/FreeRDP/.github/workflows/timezone-update.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# This workflow will build a .NET project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
|
||||
|
||||
name: timezone-update
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master, stable* ]
|
||||
schedule:
|
||||
- cron: "0 5 11 * *"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Configure CMake
|
||||
run: cmake -G"Visual Studio 17 2022" -Bbuild -Swinpr\libwinpr\timezone\utils
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore build\tzextract.sln
|
||||
- name: Build & Install CMake
|
||||
run: cmake --build build --config Release
|
||||
- name: Update timezones
|
||||
run: build\Release\tzextract.exe winpr\libwinpr\timezone
|
||||
- name: Format code
|
||||
run: |
|
||||
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/WindowsZones.c
|
||||
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/TimeZoneNameMap.c
|
||||
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/TimeZoneNameMap_static.h
|
||||
clang-format -i --style=file:.clang-format winpr/libwinpr/timezone/TimeZoneNameMap.json
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: Update timezone definitions
|
||||
committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
||||
author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com>
|
||||
signoff: false
|
||||
branch: timezone-patches
|
||||
branch-suffix: timestamp
|
||||
delete-branch: true
|
||||
title: '[timezones] Update definitions'
|
||||
body: |
|
||||
Timezone update
|
||||
- Auto-generated by [create-pull-request][1]
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
labels: |
|
||||
automated pr
|
||||
assignees: akallabeth
|
||||
reviewers: akallabeth
|
||||
draft: false
|
||||
5
third_party/FreeRDP/.gitignore
vendored
Normal file
5
third_party/FreeRDP/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
**/CMakeCache.txt
|
||||
**/CMakeFiles
|
||||
build
|
||||
checker
|
||||
abi-checker
|
||||
98
third_party/FreeRDP/CMakeCPack.cmake
vendored
Normal file
98
third_party/FreeRDP/CMakeCPack.cmake
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
# Generate .txt license file for CPack (PackageMaker requires a file extension)
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/LICENSE ${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt @ONLY)
|
||||
|
||||
# Workaround to remove c++ compiler macros and defines for Eclipse.
|
||||
# If c++ macros/defines are set __cplusplus is also set which causes
|
||||
# problems when compiling freerdp/jni. To prevent this problem we set the macros to "".
|
||||
|
||||
if(ANDROID AND CMAKE_EXTRA_GENERATOR STREQUAL "Eclipse CDT4")
|
||||
set(CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS "")
|
||||
message(STATUS "Disabled CXX system defines for eclipse (workaround).")
|
||||
endif()
|
||||
|
||||
set(CPACK_SOURCE_IGNORE_FILES "/\\\\.git/;/\\\\.gitignore;/CMakeCache.txt")
|
||||
|
||||
if(NOT WIN32)
|
||||
if(APPLE AND (NOT IOS))
|
||||
|
||||
if(WITH_SERVER)
|
||||
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} "mfreerdp-server")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WITH_X11)
|
||||
set(CPACK_PACKAGE_EXECUTABLES "xfreerdp")
|
||||
|
||||
if(WITH_SERVER)
|
||||
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} "xfreerdp-server")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CPACK_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
set(CPACK_TOPLEVEL_TAG "${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
|
||||
string(TOLOWER ${CMAKE_PROJECT_NAME} CMAKE_PROJECT_NAME_lower)
|
||||
set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME_lower}-${FREERDP_VERSION_FULL}-${CPACK_SYSTEM_NAME}")
|
||||
set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME_lower}-${FREERDP_VERSION_FULL}-${CPACK_SYSTEM_NAME}")
|
||||
|
||||
set(CPACK_PACKAGE_NAME "FreeRDP")
|
||||
set(CPACK_PACKAGE_VENDOR "FreeRDP")
|
||||
set(CPACK_PACKAGE_VERSION ${FREERDP_VERSION_FULL})
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR ${FREERDP_VERSION_MAJOR})
|
||||
set(CPACK_PACKAGE_VERSION_MINOR ${FREERDP_VERSION_MINOR})
|
||||
set(CPACK_PACKAGE_VERSION_PATCH ${FREERDP_VERSION_REVISION})
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "FreeRDP: A Remote Desktop Protocol Implementation")
|
||||
|
||||
set(CPACK_PACKAGE_CONTACT "Marc-Andre Moreau")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "marcandre.moreau@gmail.com")
|
||||
set(CPACK_DEBIAN_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
|
||||
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "FreeRDP")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE.txt")
|
||||
|
||||
set(CPACK_NSIS_MODIFY_PATH ON)
|
||||
set(CPACK_PACKAGE_ICON "${PROJECT_SOURCE_DIR}/resources\\\\FreeRDP_Install.bmp")
|
||||
set(CPACK_NSIS_MUI_ICON "${PROJECT_SOURCE_DIR}/resources\\\\FreeRDP_Icon_96px.ico")
|
||||
set(CPACK_NSIS_MUI_UNICON "${PROJECT_SOURCE_DIR}/resource\\\\FreeRDP_Icon_96px.ico")
|
||||
|
||||
set(CPACK_COMPONENTS_ALL client server libraries headers symbols tools)
|
||||
|
||||
if(MSVC)
|
||||
string(FIND ${CMAKE_MSVC_RUNTIME_LIBRARY} "DLL" IS_SHARED)
|
||||
|
||||
if(NOT IS_SHARED STREQUAL "-1")
|
||||
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
|
||||
include(InstallRequiredSystemLibraries)
|
||||
|
||||
install(PROGRAMS ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT libraries)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CPACK_COMPONENT_CLIENT_DISPLAY_NAME "Client")
|
||||
set(CPACK_COMPONENT_CLIENT_GROUP "Applications")
|
||||
|
||||
set(CPACK_COMPONENT_SERVER_DISPLAY_NAME "Server")
|
||||
set(CPACK_COMPONENT_SERVER_GROUP "Applications")
|
||||
|
||||
set(CPACK_COMPONENT_LIBRARIES_DISPLAY_NAME "Libraries")
|
||||
set(CPACK_COMPONENT_LIBRARIES_GROUP "Runtime")
|
||||
|
||||
set(CPACK_COMPONENT_HEADERS_DISPLAY_NAME "Headers")
|
||||
set(CPACK_COMPONENT_HEADERS_GROUP "Development")
|
||||
|
||||
set(CPACK_COMPONENT_SYMBOLS_DISPLAY_NAME "Symbols")
|
||||
set(CPACK_COMPONENT_SYMBOLS_GROUP "Development")
|
||||
|
||||
set(CPACK_COMPONENT_TOOLS_DISPLAY_NAME "Tools")
|
||||
set(CPACK_COMPONENT_TOOLS_GROUP "Applications")
|
||||
|
||||
set(CPACK_COMPONENT_GROUP_RUNTIME_DESCRIPTION "Runtime")
|
||||
set(CPACK_COMPONENT_GROUP_APPLICATIONS_DESCRIPTION "Applications")
|
||||
set(CPACK_COMPONENT_GROUP_DEVELOPMENT_DESCRIPTION "Development")
|
||||
|
||||
configure_file("${PROJECT_SOURCE_DIR}/CMakeCPackOptions.cmake.in" "${PROJECT_BINARY_DIR}/CMakeCPackOptions.cmake" @ONLY)
|
||||
set(CPACK_PROJECT_CONFIG_FILE "${PROJECT_BINARY_DIR}/CMakeCPackOptions.cmake")
|
||||
|
||||
include(CPack)
|
||||
10
third_party/FreeRDP/CMakeCPackOptions.cmake.in
vendored
Normal file
10
third_party/FreeRDP/CMakeCPackOptions.cmake.in
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# This file is configured at cmake time, and loaded at cpack time.
|
||||
# To pass variables to cpack from cmake, they must be configured in this file.
|
||||
|
||||
if("${CPACK_GENERATOR}" STREQUAL "PackageMaker")
|
||||
if(CMAKE_PACKAGE_QTGUI)
|
||||
set(CPACK_PACKAGE_DEFAULT_LOCATION "/Applications")
|
||||
else()
|
||||
set(CPACK_PACKAGE_DEFAULT_LOCATION "/usr")
|
||||
endif()
|
||||
endif()
|
||||
603
third_party/FreeRDP/CMakeLists.txt
vendored
Normal file
603
third_party/FreeRDP/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,603 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2011 O.S. Systems Software Ltda.
|
||||
# Copyright 2011 Otavio Salvador <otavio@ossystems.com.br>
|
||||
# Copyright 2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
# Copyright 2012 HP Development Company, LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
if(POLICY CMP0091)
|
||||
cmake_policy(SET CMP0091 NEW)
|
||||
endif()
|
||||
project(FreeRDP LANGUAGES C)
|
||||
|
||||
add_custom_target(fuzzers COMMENT "Build fuzzers")
|
||||
|
||||
if(NOT PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)
|
||||
# Git auto-ignore out-of-source build directory
|
||||
file(GENERATE OUTPUT .gitignore CONTENT "*")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED VENDOR)
|
||||
set(VENDOR "FreeRDP" CACHE STRING "FreeRDP package vendor")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED PRODUCT)
|
||||
set(PRODUCT "FreeRDP" CACHE STRING "FreeRDP package name")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED PROJECT_URL)
|
||||
set(PROJECT_URL "https://freerdp.com" CACHE STRING "FreeRDP package url")
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED FREERDP_VENDOR)
|
||||
set(FREERDP_VENDOR 1)
|
||||
endif()
|
||||
|
||||
if(NOT WIN32 AND NOT ANDROID)
|
||||
if(APPLE)
|
||||
set(OPT_DEFAULT_VAL OFF)
|
||||
else()
|
||||
set(OPT_DEFAULT_VAL ON)
|
||||
endif()
|
||||
option(WITH_X11 "build X11 client/server" ${OPT_DEFAULT_VAL})
|
||||
endif()
|
||||
|
||||
# Enable coverity related pragma definitions
|
||||
if(COVERITY_BUILD)
|
||||
add_compile_definitions(COVERITY_BUILD)
|
||||
endif()
|
||||
|
||||
# Include our extra modules
|
||||
list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/)
|
||||
|
||||
include(ProjectCStandard)
|
||||
include(PkgConfigHelpers)
|
||||
|
||||
# Check for cmake compatibility (enable/disable features)
|
||||
include(CheckCmakeCompat)
|
||||
|
||||
# Include cmake modules
|
||||
if(WITH_CLANG_FORMAT)
|
||||
include(ClangFormat)
|
||||
endif()
|
||||
|
||||
include(CompilerFlags)
|
||||
include(CheckIncludeFiles)
|
||||
include(CheckLibraryExists)
|
||||
include(CheckSymbolExists)
|
||||
include(CheckStructHasMember)
|
||||
include(TestBigEndian)
|
||||
include(CompilerDetect)
|
||||
|
||||
include(FindFeature)
|
||||
include(ShowCMakeVars)
|
||||
include(ConfigOptions)
|
||||
include(FeatureSummary)
|
||||
include(CheckCCompilerFlag)
|
||||
include(CMakePackageConfigHelpers)
|
||||
include(InstallFreeRDPMan)
|
||||
include(GetGitRevisionDescription)
|
||||
include(SetFreeRDPCMakeInstallDir)
|
||||
include(Doxygen)
|
||||
include(GetSysconfDir)
|
||||
|
||||
# FreeRDP internal builds should always check results
|
||||
add_compile_definitions(WINPR_DEFINE_ATTR_NODISCARD)
|
||||
|
||||
# Soname versioning
|
||||
set(BUILD_NUMBER 0)
|
||||
if($ENV{BUILD_NUMBER})
|
||||
set(BUILD_NUMBER $ENV{BUILD_NUMBER})
|
||||
endif()
|
||||
|
||||
include(GetProjectVersion)
|
||||
get_project_version(
|
||||
FREERDP_VERSION_MAJOR FREERDP_VERSION_MINOR FREERDP_VERSION_REVISION FREERDP_VERSION_SUFFIX GIT_REVISION
|
||||
)
|
||||
|
||||
set(FREERDP_API_VERSION "${FREERDP_VERSION_MAJOR}")
|
||||
set(FREERDP_VERSION "${FREERDP_VERSION_MAJOR}.${FREERDP_VERSION_MINOR}.${FREERDP_VERSION_REVISION}")
|
||||
if(FREERDP_VERSION_SUFFIX)
|
||||
set(FREERDP_VERSION_FULL "${FREERDP_VERSION}-${FREERDP_VERSION_SUFFIX}")
|
||||
else()
|
||||
set(FREERDP_VERSION_FULL "${FREERDP_VERSION}")
|
||||
endif()
|
||||
message("FREERDP_VERSION=${FREERDP_VERSION_FULL}")
|
||||
|
||||
message(STATUS "Git Revision ${GIT_REVISION}")
|
||||
|
||||
# MSVC compatibility with system headers
|
||||
add_compile_definitions(NONAMELESSUNION)
|
||||
|
||||
# Make the detected version available as default version for all subprojects
|
||||
set(FREERDP_DEFAULT_PROJECT_VERSION ${FREERDP_VERSION} CACHE STRING INTERNAL)
|
||||
|
||||
set(FREERDP_MAJOR_DIR "freerdp${FREERDP_VERSION_MAJOR}")
|
||||
set(FREERDP_INCLUDE_DIR "include/${FREERDP_MAJOR_DIR}/")
|
||||
|
||||
option(WITH_SMARTCARD_EMULATE "Emulate smartcards instead of redirecting readers" ON)
|
||||
if(WITH_SMARTCARD_EMULATE)
|
||||
add_compile_definitions(WITH_SMARTCARD_EMULATE)
|
||||
find_package(ZLIB REQUIRED)
|
||||
endif()
|
||||
|
||||
option(WITH_FREERDP_DEPRECATED "Build FreeRDP deprecated symbols" OFF)
|
||||
if(WITH_FREERDP_DEPRECATED)
|
||||
add_compile_definitions(WITH_FREERDP_DEPRECATED)
|
||||
endif()
|
||||
|
||||
option(WITHOUT_FREERDP_3x_DEPRECATED "Build FreeRDP 3x deprecated symbols" OFF)
|
||||
if(WITH_FREERDP_3x_DEPRECATED)
|
||||
message(WARNING "WITH_FREERDP_3x_DEPRECATED has been replaced with WITHOUT_FREERDP_3x_DEPRECATED")
|
||||
set(WITHOUT_FREERDP_3x_DEPRECATED OFF)
|
||||
endif()
|
||||
|
||||
option(WITH_FREERDP_DEPRECATED_COMMANDLINE "Build FreeRDP deprecated command line options" OFF)
|
||||
if(WITH_FREERDP_DEPRECATED_COMMANDLINE)
|
||||
add_compile_definitions(WITH_FREERDP_DEPRECATED_COMMANDLINE)
|
||||
endif()
|
||||
|
||||
# Make paths absolute
|
||||
if(CMAKE_INSTALL_PREFIX)
|
||||
get_filename_component(CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" ABSOLUTE)
|
||||
endif()
|
||||
if(FREERDP_EXTERNAL_PATH)
|
||||
get_filename_component(FREERDP_EXTERNAL_PATH "${FREERDP_EXTERNAL_PATH}" ABSOLUTE)
|
||||
endif()
|
||||
|
||||
# Allow to search the host machine for git/ccache
|
||||
if(CMAKE_CROSSCOMPILING)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH)
|
||||
endif(CMAKE_CROSSCOMPILING)
|
||||
|
||||
find_program(CCACHE ccache)
|
||||
if(CCACHE AND WITH_CCACHE)
|
||||
if(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER)
|
||||
set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE})
|
||||
endif(NOT DEFINED CMAKE_C_COMPILER_LAUNCHER)
|
||||
endif(CCACHE AND WITH_CCACHE)
|
||||
|
||||
if(CMAKE_CROSSCOMPILING)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ONLY)
|
||||
endif(CMAKE_CROSSCOMPILING)
|
||||
# /Allow to search the host machine for git/ccache
|
||||
|
||||
# Turn on solution folders (2.8.4+)
|
||||
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
|
||||
|
||||
option(CTEST_OUTPUT_ON_FAILURE ON "show verbose output on CTest failures")
|
||||
if(BUILD_TESTING_INTERNAL)
|
||||
set(EXPORT_ALL_SYMBOLS ON CACHE BOOL "testing default" FORCE)
|
||||
add_compile_definitions(BUILD_TESTING_INTERNAL)
|
||||
elseif(BUILD_TESTING)
|
||||
set(EXPORT_ALL_SYMBOLS OFF CACHE BOOL "testing default" FORCE)
|
||||
endif()
|
||||
|
||||
include(ExportAllSymbols)
|
||||
|
||||
set(THREAD_PREFER_PTHREAD_FLAG TRUE)
|
||||
|
||||
if(NOT IOS)
|
||||
find_package(Threads REQUIRED)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
add_compile_definitions(UNICODE _UNICODE)
|
||||
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)
|
||||
add_compile_definitions(WIN32_LEAN_AND_MEAN)
|
||||
add_compile_definitions(_WINSOCK_DEPRECATED_NO_WARNINGS)
|
||||
|
||||
set(CMAKE_DL_LIBS "")
|
||||
set(CMAKE_USE_RELATIVE_PATH ON)
|
||||
string(TIMESTAMP RC_VERSION_YEAR "%Y")
|
||||
|
||||
if(NOT DEFINED CMAKE_WINDOWS_VERSION)
|
||||
set(CMAKE_WINDOWS_VERSION "WIN7")
|
||||
endif()
|
||||
|
||||
if(CMAKE_WINDOWS_VERSION STREQUAL "WINXP")
|
||||
add_compile_definitions(WINVER=0x0501 _WIN32_WINNT=0x0501)
|
||||
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN7")
|
||||
add_compile_definitions(WINVER=0x0601 _WIN32_WINNT=0x0601)
|
||||
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN8")
|
||||
add_compile_definitions(WINVER=0x0602 _WIN32_WINNT=0x0602)
|
||||
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN10")
|
||||
add_compile_definitions(WINVER=0x0A00 _WIN32_WINNT=0x0A00)
|
||||
endif()
|
||||
|
||||
# Set product and vendor for dll and exe version information.
|
||||
set(RC_VERSION_VENDOR ${VENDOR})
|
||||
set(RC_VERSION_PRODUCT ${PRODUCT})
|
||||
set(RC_VERSION_PATCH ${BUILD_NUMBER})
|
||||
set(RC_VERSION_DESCRIPTION
|
||||
"${FREERDP_VERSION_FULL} ${GIT_REVISION} ${CMAKE_WINDOWS_VERSION} ${CMAKE_SYSTEM_PROCESSOR}"
|
||||
)
|
||||
|
||||
if(FREERDP_EXTERNAL_SSL_PATH)
|
||||
set(OPENSSL_ROOT_DIR ${FREERDP_EXTERNAL_SSL_PATH})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_compile_definitions(FREERDP_EXPORTS)
|
||||
|
||||
# Mac OS X
|
||||
if(APPLE)
|
||||
if(IOS)
|
||||
if(NOT FREERDP_IOS_EXTERNAL_SSL_PATH)
|
||||
message(
|
||||
STATUS
|
||||
"FREERDP_IOS_EXTERNAL_SSL_PATH not set! Required if openssl is not found in the iOS SDK (which usually isn't"
|
||||
)
|
||||
endif()
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_FIND_ROOT_PATH} ${FREERDP_IOS_EXTERNAL_SSL_PATH})
|
||||
set_property(GLOBAL PROPERTY XCODE_ATTRIBUTE_SKIP_INSTALL YES)
|
||||
endif(IOS)
|
||||
|
||||
# Temporarily disabled, causes the cmake script to be reexecuted, causing the compilation to fail.
|
||||
# Workaround: specify the parameter in the command-line
|
||||
# if(WITH_CLANG)
|
||||
# set(CMAKE_C_COMPILER "clang")
|
||||
# endif()
|
||||
|
||||
if(WITH_VERBOSE)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -v")
|
||||
endif()
|
||||
endif(APPLE)
|
||||
|
||||
# Android
|
||||
if(ANDROID)
|
||||
set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB64_PATHS ${ANDROID_LIBRARY_USE_LIB64_PATHS})
|
||||
|
||||
if(ANDROID_ABI STREQUAL arm64-v8a)
|
||||
include(CheckCCompilerFlag)
|
||||
check_c_compiler_flag("-mfloat-abi=softfp" ABI_SOFTFP_SUPPORTED)
|
||||
|
||||
if(ABI_SOFTFP_SUPPORTED)
|
||||
# https://github.com/android/ndk/issues/910
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mfloat-abi=softfp")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_compile_definitions("$<$<CONFIG:Debug>:NDK_DEBUG=1>")
|
||||
|
||||
# NOTE: Manually add -gdwarf-3, as newer toolchains default to -gdwarf-4,
|
||||
# which is not supported by the gdbserver binary shipped with
|
||||
# the android NDK (tested with r9b)
|
||||
add_compile_options("$<$<CONFIG:Debug>:-gdwarf-3>")
|
||||
add_link_options(-llog)
|
||||
|
||||
# CMAKE_PREFIX_PATH detection is broken in most Android toolchain files
|
||||
# Append it to CMAKE_FIND_ROOT_PATH and avoid potential duplicates
|
||||
list(APPEND CMAKE_FIND_ROOT_PATH ${CMAKE_PREFIX_PATH})
|
||||
list(REMOVE_DUPLICATES CMAKE_FIND_ROOT_PATH)
|
||||
|
||||
if(NOT FREERDP_EXTERNAL_PATH)
|
||||
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/external/")
|
||||
set(FREERDP_EXTERNAL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/external/")
|
||||
else()
|
||||
message(STATUS "FREERDP_EXTERNAL_PATH not set!")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
list(APPEND CMAKE_INCLUDE_PATH ${FREERDP_EXTERNAL_PATH}/${ANDROID_ABI}/include)
|
||||
list(APPEND CMAKE_LIBRARY_PATH ${FREERDP_EXTERNAL_PATH}/${ANDROID_ABI}/)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
|
||||
if(WITH_GPROF)
|
||||
configure_file(
|
||||
${PROJECT_SOURCE_DIR}/scripts/gprof_generate.sh.in ${PROJECT_BINARY_DIR}/scripts/gprof_generate.sh @ONLY
|
||||
)
|
||||
endif(WITH_GPROF)
|
||||
endif()
|
||||
|
||||
if(UNIX OR CYGWIN)
|
||||
set(WAYLAND_FEATURE_TYPE "RECOMMENDED")
|
||||
else()
|
||||
set(WAYLAND_FEATURE_TYPE "DISABLED")
|
||||
endif()
|
||||
|
||||
if(WITH_PCSC_WINPR)
|
||||
find_package(PCSCWinPR)
|
||||
endif()
|
||||
|
||||
set(WAYLAND_FEATURE_PURPOSE "Wayland")
|
||||
set(WAYLAND_FEATURE_DESCRIPTION "Wayland client")
|
||||
|
||||
set(OPENSSL_FEATURE_TYPE "REQUIRED")
|
||||
set(OPENSSL_FEATURE_PURPOSE "cryptography")
|
||||
set(OPENSSL_FEATURE_DESCRIPTION "encryption, certificate validation, hashing functions")
|
||||
|
||||
set(MBEDTLS_FEATURE_TYPE "OPTIONAL")
|
||||
set(MBEDTLS_FEATURE_PURPOSE "cryptography")
|
||||
set(MBEDTLS_FEATURE_DESCRIPTION "[experimental] encryption, certificate validation, hashing functions")
|
||||
|
||||
set(PCSC_FEATURE_TYPE "RECOMMENDED")
|
||||
set(PCSC_FEATURE_PURPOSE "smart card")
|
||||
set(PCSC_FEATURE_DESCRIPTION "smart card device redirection")
|
||||
|
||||
set(OPENH264_FEATURE_TYPE "OPTIONAL")
|
||||
set(OPENH264_FEATURE_PURPOSE "codec")
|
||||
set(OPENH264_FEATURE_DESCRIPTION "use OpenH264 library")
|
||||
|
||||
set(GSM_FEATURE_TYPE "OPTIONAL")
|
||||
set(GSM_FEATURE_PURPOSE "codec")
|
||||
set(GSM_FEATURE_DESCRIPTION "GSM audio codec library")
|
||||
|
||||
set(LAME_FEATURE_TYPE "OPTIONAL")
|
||||
set(LAME_FEATURE_PURPOSE "codec")
|
||||
set(LAME_FEATURE_DESCRIPTION "lame MP3 audio codec library")
|
||||
|
||||
set(FAAD2_FEATURE_TYPE "OPTIONAL")
|
||||
set(FAAD2_FEATURE_PURPOSE "codec")
|
||||
set(FAAD2_FEATURE_DESCRIPTION "FAAD2 AAC audio codec library")
|
||||
|
||||
set(FAAC_FEATURE_TYPE "OPTIONAL")
|
||||
set(FAAC_FEATURE_PURPOSE "codec")
|
||||
set(FAAC_FEATURE_DESCRIPTION "FAAC AAC audio codec library")
|
||||
|
||||
set(SOXR_FEATURE_TYPE "OPTIONAL")
|
||||
set(SOXR_FEATURE_PURPOSE "codec")
|
||||
set(SOXR_FEATURE_DESCRIPTION "SOX audio resample library")
|
||||
|
||||
if(WIN32)
|
||||
set(WAYLAND_FEATURE_TYPE "DISABLED")
|
||||
set(PCSC_FEATURE_TYPE "DISABLED")
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(WAYLAND_FEATURE_TYPE "DISABLED")
|
||||
if(IOS)
|
||||
set(PCSC_FEATURE_TYPE "DISABLED")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
set(WAYLAND_FEATURE_TYPE "DISABLED")
|
||||
set(PCSC_FEATURE_TYPE "DISABLED")
|
||||
endif()
|
||||
|
||||
if(NOT WITHOUT_FREERDP_3x_DEPRECATED)
|
||||
find_feature(Wayland ${WAYLAND_FEATURE_TYPE} ${WAYLAND_FEATURE_PURPOSE} ${WAYLAND_FEATURE_DESCRIPTION})
|
||||
endif()
|
||||
|
||||
option(WITH_LIBRESSL "[experimental] build with LibreSSL" OFF)
|
||||
if(WITH_LIBRESSL)
|
||||
message(WARNING "-DWITH_LIBRESSL=ON: LibreSSL enabled. Expect incompatibilities.")
|
||||
message(WARNING "Only OpenSSL is tested on each release, other implementations depend on external contributions")
|
||||
|
||||
find_package(LibreSSL REQUIRED)
|
||||
include_directories(SYSTEM ${LibreSSL_INCLUDE_DIRS})
|
||||
set(OPENSSL_INCLUDE_DIR ${LIBRESSL_INCLUDE_DIR})
|
||||
set(OPENSSL_LIBRARIES ${LIBRESSL_LIBRARIES})
|
||||
set(OPENSSL_CRYPTO_LIBRARIES ${LIBRESSL_LIBRARIES})
|
||||
set(WITH_OPENSSL ON)
|
||||
set(OPENSSL_FOUND ON)
|
||||
add_compile_definitions("WITH_LIBRESSL")
|
||||
add_compile_definitions("WITH_OPENSSL")
|
||||
else()
|
||||
find_feature(OpenSSL ${OPENSSL_FEATURE_TYPE} ${OPENSSL_FEATURE_PURPOSE} ${OPENSSL_FEATURE_DESCRIPTION})
|
||||
find_feature(MbedTLS ${MBEDTLS_FEATURE_TYPE} ${MBEDTLS_FEATURE_PURPOSE} ${MBEDTLS_FEATURE_DESCRIPTION})
|
||||
endif()
|
||||
|
||||
find_feature(PCSC ${PCSC_FEATURE_TYPE} ${PCSC_FEATURE_PURPOSE} ${PCSC_FEATURE_DESCRIPTION})
|
||||
|
||||
if(WITH_DSP_FFMPEG OR WITH_VIDEO_FFMPEG OR WITH_FFMPEG)
|
||||
find_package(FFmpeg REQUIRED COMPONENTS AVUTIL AVCODEC)
|
||||
endif()
|
||||
|
||||
find_feature(OpenH264 ${OPENH264_FEATURE_TYPE} ${OPENH264_FEATURE_PURPOSE} ${OPENH264_FEATURE_DESCRIPTION})
|
||||
find_feature(GSM ${GSM_FEATURE_TYPE} ${GSM_FEATURE_PURPOSE} ${GSM_FEATURE_DESCRIPTION})
|
||||
find_feature(LAME ${LAME_FEATURE_TYPE} ${LAME_FEATURE_PURPOSE} ${LAME_FEATURE_DESCRIPTION})
|
||||
find_feature(FAAD2 ${FAAD2_FEATURE_TYPE} ${FAAD2_FEATURE_PURPOSE} ${FAAD2_FEATURE_DESCRIPTION})
|
||||
find_feature(FAAC ${FAAC_FEATURE_TYPE} ${FAAC_FEATURE_PURPOSE} ${FAAC_FEATURE_DESCRIPTION})
|
||||
find_feature(soxr ${SOXR_FEATURE_TYPE} ${SOXR_FEATURE_PURPOSE} ${SOXR_FEATURE_DESCRIPTION})
|
||||
|
||||
option(WITH_OPENCL "[experimental] enable OpenCL support for primitives" OFF)
|
||||
if(WITH_OPENCL)
|
||||
find_package(OpenCL REQUIRED)
|
||||
endif()
|
||||
|
||||
if(WITH_OPENH264 AND NOT WITH_OPENH264_LOADING)
|
||||
option(WITH_OPENH264_LOADING "Use LoadLibrary to load openh264 at runtime" OFF)
|
||||
endif(WITH_OPENH264 AND NOT WITH_OPENH264_LOADING)
|
||||
|
||||
# Version check, if we have detected FFMPEG but the version is too old
|
||||
# deactivate it as sound backend.
|
||||
if(WITH_DSP_FFMPEG)
|
||||
if(AVCODEC_VERSION VERSION_LESS "57.48.101")
|
||||
message(
|
||||
WARNING
|
||||
"FFmpeg version detected (${AVCODEC_VERSION}) is too old. (Require at least 57.48.101 for sound). Deactivating"
|
||||
)
|
||||
set(WITH_DSP_FFMPEG OFF)
|
||||
endif()
|
||||
endif(WITH_DSP_FFMPEG)
|
||||
|
||||
if(WITH_OPENH264 AND NOT OPENH264_FOUND)
|
||||
message(FATAL_ERROR "OpenH264 support requested but not detected")
|
||||
endif()
|
||||
set(WITH_OPENH264 ${OPENH264_FOUND})
|
||||
|
||||
if(OPENSSL_FOUND)
|
||||
add_compile_definitions("WITH_OPENSSL")
|
||||
message(STATUS "Using OpenSSL Version: ${OPENSSL_VERSION}")
|
||||
include_directories(SYSTEM ${OPENSSL_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
if(MBEDTLS_FOUND)
|
||||
message(WARNING "-DWITH_MBEDTLS=ON: Mbed-TLS enabled. Expect incompatibilities")
|
||||
message(WARNING "Only OpenSSL is tested on each release, other implementations depend on external contributions")
|
||||
add_compile_definitions("WITH_MBEDTLS")
|
||||
endif()
|
||||
|
||||
if(WITH_OPENH264 OR WITH_MEDIA_FOUNDATION OR WITH_VIDEO_FFMPEG OR WITH_MEDIACODEC)
|
||||
set(WITH_GFX_H264 ON)
|
||||
else()
|
||||
set(WITH_GFX_H264 OFF)
|
||||
endif()
|
||||
|
||||
# Android expects all libraries to be loadable
|
||||
# without paths.
|
||||
if(ANDROID OR WIN32 OR MAC_BUNDLE)
|
||||
set(PLUGIN_ABS_PATHS_DEFAULT OFF)
|
||||
else()
|
||||
set(PLUGIN_ABS_PATHS_DEFAULT ON)
|
||||
endif()
|
||||
option(WITH_ABSOLUTE_PLUGIN_LOAD_PATHS "Load plugins with absolute paths" ${PLUGIN_ABS_PATHS_DEFAULT})
|
||||
|
||||
if(NOT WITH_ABSOLUTE_PLUGIN_LOAD_PATHS)
|
||||
set(FREERDP_DATA_PATH "share")
|
||||
if(NOT FREERDP_INSTALL_PREFIX)
|
||||
set(FREERDP_INSTALL_PREFIX ".")
|
||||
endif()
|
||||
set(FREERDP_LIBRARY_PATH ".")
|
||||
set(FREERDP_PLUGIN_PATH ".")
|
||||
else()
|
||||
set(FREERDP_DATA_PATH "${CMAKE_INSTALL_PREFIX}/share/${FREERDP_MAJOR_DIR}")
|
||||
if(NOT FREERDP_INSTALL_PREFIX)
|
||||
set(FREERDP_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
|
||||
endif()
|
||||
set(FREERDP_LIBRARY_PATH "${CMAKE_INSTALL_LIBDIR}")
|
||||
if(WIN32)
|
||||
set(FREERDP_PLUGIN_PATH "${CMAKE_INSTALL_BINDIR}/${FREERDP_MAJOR_DIR}")
|
||||
else()
|
||||
set(FREERDP_PLUGIN_PATH "${CMAKE_INSTALL_LIBDIR}/${FREERDP_MAJOR_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
set(FREERDP_ADDIN_PATH "${FREERDP_PLUGIN_PATH}")
|
||||
|
||||
# Path to put extensions
|
||||
set(FREERDP_EXTENSION_POSTFIX "${FREERDP_MAJOR_DIR}/extensions")
|
||||
set(FREERDP_EXTENSION_REL_PATH "${CMAKE_INSTALL_LIBDIR}/${FREERDP_EXTENSION_POSTFIX}")
|
||||
set(FREERDP_EXTENSION_PATH "${CMAKE_INSTALL_FULL_LIBDIR}/${FREERDP_EXTENSION_POSTFIX}")
|
||||
|
||||
# Proxy plugins path
|
||||
if(NOT DEFINED PROXY_PLUGINDIR)
|
||||
message("using default plugins location")
|
||||
set(FREERDP_PROXY_PLUGINDIR "${FREERDP_PLUGIN_PATH}/proxy/")
|
||||
else()
|
||||
set(FREERDP_PROXY_PLUGINDIR "${PROXY_PLUGINDIR}")
|
||||
endif()
|
||||
|
||||
# Unit Tests
|
||||
|
||||
include(CTest)
|
||||
|
||||
if(BUILD_TESTING_INTERNAL OR BUILD_TESTING)
|
||||
enable_testing()
|
||||
|
||||
if(MSVC)
|
||||
set(TESTING_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
|
||||
else()
|
||||
set(TESTING_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/Testing")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include(CommonConfigOptions)
|
||||
|
||||
if(FREERDP_UNIFIED_BUILD)
|
||||
add_subdirectory(winpr)
|
||||
if(NOT WITHOUT_FREERDP_3x_DEPRECATED)
|
||||
if(WITH_WAYLAND)
|
||||
add_subdirectory(uwac)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WITH_SERVER)
|
||||
option(WITH_RDTK "build rdtk toolkit" OFF)
|
||||
if(WITH_RDTK)
|
||||
add_subdirectory(rdtk)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
include_directories(${PROJECT_SOURCE_DIR}/winpr/include)
|
||||
include_directories(${PROJECT_BINARY_DIR}/winpr/include)
|
||||
else()
|
||||
find_package(WinPR 3 REQUIRED)
|
||||
include_directories(SYSTEM ${WinPR_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
option(WITH_AAD "Compile with support for Azure AD authentication" ${WITH_WINPR_JSON})
|
||||
|
||||
# Include directories
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR}/include)
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
|
||||
# Sub-directories
|
||||
|
||||
if(WITH_THIRD_PARTY)
|
||||
add_subdirectory(third-party)
|
||||
if(NOT "${THIRD_PARTY_INCLUDES}" STREQUAL "")
|
||||
include_directories(SYSTEM ${THIRD_PARTY_INCLUDES})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# used in libfreerdp subfolder as well
|
||||
setfreerdpcmakeinstalldir(FREERDP_CMAKE_INSTALL_DIR "FreeRDP${FREERDP_VERSION_MAJOR}")
|
||||
|
||||
add_subdirectory(libfreerdp)
|
||||
|
||||
if(WITH_CHANNELS)
|
||||
add_subdirectory(channels)
|
||||
endif()
|
||||
|
||||
if(WITH_CLIENT_COMMON OR WITH_CLIENT)
|
||||
add_subdirectory(client)
|
||||
endif()
|
||||
|
||||
if(WITH_SERVER)
|
||||
add_subdirectory(server)
|
||||
endif()
|
||||
|
||||
# must be after all targets have been added in libfreerdp and channel
|
||||
install(EXPORT FreeRDPTargets DESTINATION ${FREERDP_CMAKE_INSTALL_DIR})
|
||||
|
||||
# Packaging
|
||||
|
||||
set(CMAKE_CPACK_INCLUDE_FILE "CMakeCPack.cmake")
|
||||
|
||||
if(NOT (VENDOR MATCHES "FreeRDP"))
|
||||
if(DEFINED CLIENT_VENDOR_PATH)
|
||||
if(EXISTS "${PROJECT_SOURCE_DIR}/${CLIENT_VENDOR_PATH}/CMakeCPack.cmake")
|
||||
set(CMAKE_CPACK_INCLUDE_FILE "${CLIENT_VENDOR_PATH}/CMakeCPack.cmake")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
#message("VENDOR: ${VENDOR} CLIENT_VENDOR_PATH: ${CLIENT_VENDOR_PATH} CMAKE_CPACK_INCLUDE_FILE: ${CMAKE_CPACK_INCLUDE_FILE}")
|
||||
|
||||
set(FREERDP_BUILD_CONFIG_LIST "")
|
||||
get_cmake_property(res VARIABLES)
|
||||
foreach(var ${res})
|
||||
if(var MATCHES "^WITH_*|^BUILD_TESTING*|^WINPR_HAVE_*")
|
||||
list(APPEND FREERDP_BUILD_CONFIG_LIST "${var}=${${var}}")
|
||||
endif()
|
||||
endforeach()
|
||||
string(REPLACE ";" " " FREERDP_BUILD_CONFIG "${FREERDP_BUILD_CONFIG_LIST}")
|
||||
|
||||
add_subdirectory(include)
|
||||
|
||||
include(${CMAKE_CPACK_INCLUDE_FILE})
|
||||
|
||||
message(STATUS "Intrinsic path configuration:")
|
||||
#ShowCMakeVars("^CMAKE_INSTALL_PREFIX")
|
||||
#ShowCMakeVars("^CMAKE_INSTALL_LIBDIR")
|
||||
showcmakevars("^FREERDP_INSTALL_PREFIX|^FREERDP_LIBRARY_PATH|^FREERDP_PLUGIN_PATH")
|
||||
showcmakevars("^FREERDP_ADDIN_PATH|^FREERDP_EXTENSION_PATH|^FREERDP_PROXY_PLUGINDIR")
|
||||
1481
third_party/FreeRDP/ChangeLog
vendored
Normal file
1481
third_party/FreeRDP/ChangeLog
vendored
Normal file
File diff suppressed because it is too large
Load Diff
202
third_party/FreeRDP/LICENSE
vendored
Normal file
202
third_party/FreeRDP/LICENSE
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
54
third_party/FreeRDP/README.md
vendored
Normal file
54
third_party/FreeRDP/README.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
|
||||
FreeRDP is a free implementation of the Remote Desktop Protocol (RDP), released under the Apache license.
|
||||
Enjoy the freedom of using your software wherever you want, the way you want it, in a world where
|
||||
interoperability can finally liberate your computing experience.
|
||||
|
||||
## Code Quality Status
|
||||
|
||||
[](https://github.com/FreeRDP/FreeRDP/actions/workflows/abi-checker.yml)
|
||||
[](https://github.com/FreeRDP/FreeRDP/actions/workflows/clang-tidy.yml)
|
||||
[](https://github.com/FreeRDP/FreeRDP/actions/workflows/codeql-analysis.yml)
|
||||
[](https://github.com/FreeRDP/FreeRDP/actions/workflows/mingw.yml)
|
||||
[](https://github.com/FreeRDP/FreeRDP/actions/workflows/macos.yml)
|
||||
[![[arm,ppc,ricsv] architecture builds](https://github.com/FreeRDP/FreeRDP/actions/workflows/alt-architectures.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/alt-architectures.yml)
|
||||
[![[freebsd] architecture builds](https://github.com/FreeRDP/FreeRDP/actions/workflows/freebsd.yml/badge.svg)](https://github.com/FreeRDP/FreeRDP/actions/workflows/freebsd.yml)
|
||||
[](https://scan.coverity.com/projects/freerdp)
|
||||
|
||||
## Resources
|
||||
|
||||
Project website: https://www.freerdp.com/
|
||||
|
||||
Issue tracker: https://github.com/FreeRDP/FreeRDP/issues
|
||||
|
||||
Sources: https://github.com/FreeRDP/FreeRDP/
|
||||
|
||||
Downloads: https://pub.freerdp.com/releases/
|
||||
|
||||
Wiki: https://github.com/FreeRDP/FreeRDP/wiki
|
||||
|
||||
API documentation: https://pub.freerdp.com/api/
|
||||
|
||||
Security policy: https://github.com/FreeRDP/FreeRDP/security/policy
|
||||
|
||||
FAQ: https://github.com/FreeRDP/FreeRDP/wiki/FAQ
|
||||
|
||||
### Contact
|
||||
|
||||
* Matrix room : `#FreeRDP:matrix.org` (main)
|
||||
* ~~XMPP channel: `#FreeRDP#matrix.org@matrix.org` (bridged)~~ no longer available
|
||||
* IRC channel : `#freerdp @ irc.oftc.net` (bridged)
|
||||
* Mailing list: https://lists.sourceforge.net/lists/listinfo/freerdp-devel
|
||||
|
||||
## Microsoft Open Specifications
|
||||
|
||||
Information regarding the Microsoft Open Specifications can be found at:
|
||||
https://www.microsoft.com/openspecifications/
|
||||
|
||||
A list of reference documentation is maintained here:
|
||||
https://github.com/FreeRDP/FreeRDP/wiki/Reference-Documentation
|
||||
|
||||
## Compilation
|
||||
|
||||
Instructions on how to get started compiling FreeRDP can be found on the wiki:
|
||||
https://github.com/FreeRDP/FreeRDP/wiki/Compilation
|
||||
114
third_party/FreeRDP/SECURITY.md
vendored
Normal file
114
third_party/FreeRDP/SECURITY.md
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
# FreeRDP Security Policies and Procedures
|
||||
|
||||
This document describes the security policy and procedures for the [FreeRDP Project](https://github.com/FreeRDP/FreeRDP).
|
||||
The following topics are covered:
|
||||
|
||||
* [Supported Versions](#supported-versions)
|
||||
* [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
* [Disclosure Procedure](#disclosure-procedure)
|
||||
|
||||
|
||||
## Supported versions
|
||||
|
||||
Security is very important for us therefore we try to provide security updates and support for
|
||||
the latest stable version as well as for the development branch.
|
||||
Since our development branch is, like the protocol itself, a moving target we won't request CVEs for issues that are *only* found on the development branch.
|
||||
|
||||
The following table shows the currently supported versions:
|
||||
|
||||
| Version | Branch | Supported |
|
||||
| ------- |--------------| ------------------ |
|
||||
| < 2.0.0 | stable-1.x | :x: |
|
||||
| 2.x.x | stable-2.0 | :x: |
|
||||
| 3.x.x | stable-3.0 | :white_check_mark: |
|
||||
| - | master | :white_check_mark: |
|
||||
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
**IMPORTANT**: Please, do not file security vulnerabilities as public issues on GitHub
|
||||
|
||||
In advance: **Thank you** for reporting a security vulnerability and making FreeRDP more stable! We really appreciate your effort.
|
||||
Please let us know who we should give the credit or attributions to.
|
||||
|
||||
|
||||
If you have found a security vulnerability in FreeRDP you can either directly open an [Advisory on GitHub](https://github.com/FreeRDP/FreeRDP/security/advisories/new)[^1] or send us an email to mailto:security@freerdp.com
|
||||
|
||||
In case of an email you can use the [FreeRDP security team GPG key](#reporting-gpg-key) for encrypted communication.
|
||||
|
||||
Once we receive a report we will review it and respond as soon as possible.
|
||||
|
||||
###
|
||||
|
||||
|
||||
## Disclosure procedure
|
||||
|
||||
When the FreeRDP team receives a report one of the team members will be assigned as primary contact.
|
||||
The primary contact will do all further communications and coordinate the fix and release process.
|
||||
|
||||
How your report will be handled:
|
||||
|
||||
* When a report is received we will acknowledge the reception and review the reported issue(s) as soon as possible.
|
||||
* Once confirmed we will determine the affected versions. If not reported via GitHub a [security advisory draft on GitHub](https://github.com/FreeRDP/FreeRDP/security/advisories) will be created for any issue. If it applies we will request a CVE.
|
||||
* On a private branch we will fix the issue and check the code for any potential similar problem.
|
||||
* After the fix is validated we will create and publish a new release for all supported versions and publish the advisories.
|
||||
|
||||
## Reporting GPG key
|
||||
|
||||
FreeRDP's security reporting public gpg key https://pub.freerdp.com/FreeRDP-security-team.pub.asc
|
||||
|
||||
```
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGBz+jsBEADaIM94hYfn/xDzncQwXl7/q6+06+ssqO3iUGqFr+0EPS+HxRjD
|
||||
BeKjVRSkuo0+QLQoZgCwkoltEj1xRWNqCTDMA+oZkZH8L82eqCnUQqgCOyNWAVMH
|
||||
6u6ValiZH3ruYxergBBHhyR4Ot2ia0xWN8MKTp+emLpzQ7goimGMo0mxR5FiDAdb
|
||||
QKz1q5bgs3bb2pLpERNF+z13OS10Mzk1zdr++1pov5PWOTBRKmvBtPJKswmDpb0y
|
||||
jQGeeqBFZwKzx0n6BTzDZtkqzTwvGhbm9Sb+qO0IO66IV8zQhPG/JUfDkByd6mX9
|
||||
Ykke0gxoRx54XqoRwZGNydOxMN6g3Oj1+ioWisltYLs/SzW20f3AMCoTeYyfjKtf
|
||||
01refrA3aRfhDctvW5/s2LP0OEG2P/yQYXiGhK6uVxShz3Oa5dhFwiS8G63omZRH
|
||||
AEqSk46EhAbbT4xfZ/Np209rhis4KW40cMMpI0F+XpyfT05ZQD6ytHTPgWTxv/OF
|
||||
G9zy2ysT0kq+t+Hb+1RWQUq/2Dz9Lf6xLZPgqtyzg8xiFxZ4i1kf/VDWa3M76zn3
|
||||
qMcj3SPOxKY//wW70jCxf44yD38NvSa1M2Sz/K/RJKWkRWP/jhV1UHYusbzCmsvm
|
||||
M9JkknNMJvGIjBDjHEVy6dlTaHQoHDY+Me9gsrEX0ZS9xXgAiB2IupabEwARAQAB
|
||||
tEJGcmVlUkRQIFNlY3VyaXR5IFRlYW0gKGh0dHBzOi8vZnJlZXJkcC5jb20pIDxz
|
||||
ZWN1cml0eUBmcmVlcmRwLmNvbT6JAk4EEwEKADgWIQRvuAE0sDt7JnxXu0o3Ibww
|
||||
YbfjNAUCYHP6OwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRA3IbwwYbfj
|
||||
NPviD/43NLg7YfjAlvj5GipSmgelLwlIA+L/qbrf4NAB+NZ9oqp3bBdj4e5gZmiI
|
||||
zd6bkANCqk21YiOE31medUfy+nfBQFVvj0oUg1X16C6RaIX5qA3Dwt5qBwKmDkT5
|
||||
j7JlxUS6Eluiau67ePiDYu2Wbp0qYuAmNUNL+Y2NCO9UJiy0Oq6YVXS971D5lC+a
|
||||
SX0x9pizmFV3zro+l6/3kHTVbPednfX99yz9SZge64aWXo3MXVN8JD0lR3+92l99
|
||||
XsFDc+lGeR4azLFIqXC4Cr5Lbk34Hw/VwUC32xxFUaJ2ZmV3pA8bhCtBSxSmxnHS
|
||||
H3hoBaD1WpuApbW8Psx6qsgoaSUdWsjluA4eQ5afJBf9O2NlT1mim5MAINY4PbWP
|
||||
o4zq3p1ABVTzuB8tsGA9o6DeYVUUrj7lCv9STdGRhm0472BDkp/gvKMBoPgg3Qez
|
||||
kvGKK7iVy8R/BOPjh9wP1art5JLVsralXGHA/5Ceid4ojKFzGIC9g3lnAPh+T/eM
|
||||
duyY9XH4un1r73r6DRqUoczSfHYbxhKxWt0cRNdIadcXXusMPV/w4J4j55WcLrBE
|
||||
5nopp/prJ5bYegUvRRrwVSFwLDxkE2dh68Zvlh5VWXIPFge0RPEAijYWR5qR2z+/
|
||||
VHgPYmliOnWFJN1rzekmWjKFtg5A57FkZyk3cp5x0/2xAX+TIbkCDQRgc/o7ARAA
|
||||
vw53CoVkMzBlisSEETNdEKQMaiQ8BtbC438v/b1mOOeoE0YCfSW7RyflA/TXHOah
|
||||
db0s3v/Kk2xmbjeMS9IJXlWviKKnOVMrMZvtJdQ4EKfqc5EpxNx7OiEofA/7n7Xs
|
||||
1YEt6KjYaM/vgANl9HA2UXzqSFiRhkWjj1WA7vhqCWUArpAMGeCDYab2BBfp6Z4f
|
||||
W9178N2vHH+Hh/uBwGUDnShU38GH8Nstkdcyw5puiJqNQBfZ1Fz9luzutp6zAgHz
|
||||
WzobeRPZCCXs7CfxcvpkFS0ctOteQtIRIfP+jbDnldMmClQ87UVcKv0pCCJkMLNk
|
||||
YUCMAb2UC2boCIf0omeeque4+FOphcO4+R/8jc6cYlQpgwUg2/IwBEEnCqtvo3qu
|
||||
k6uzONhfWZPtUdJd158MGKGTogXVXGzoGzxIrKkZ4W1VuuMiEmhIQZO8e7/4Iz4a
|
||||
Zp4qQXI8rsmNJN3lB5a7MWgrZ8mjllYRdfiTEvfQ+PiQqnG6PEHZ82om9kp555gs
|
||||
15UqhjHAqRRtfXzQvZko0ngAxxZNVFPwK8LnxkyEPClRBC5eV3ljI8cvCfnWD01q
|
||||
rCzSlSafFHCEUEQOhOrf/bBbXPkYTJw2KlumH5w9R6xQWgqneiD/+Qmqdclzdn36
|
||||
Pgbhyu6uSNZehbx5ptt/EM66JSAW7Q7W6Qnz5PNnHgEAEQEAAYkCNgQYAQoAIBYh
|
||||
BG+4ATSwO3smfFe7SjchvDBht+M0BQJgc/o7AhsMAAoJEDchvDBht+M0JYUQALlV
|
||||
dwmk6ZFq5dq0utWgutysL47b30BhYwNMVe0/6UW4h4TYaW6B3f58X7ik7EdYciyR
|
||||
68eYfwKGhuv/y90QaGXJMU13XHpoInSaHQRhn5M/GkN16DBXdBok70Fh9Gx89Zhs
|
||||
VKF3qwIVx5AO5CwrVA6F/iOiUEW31xiT7VFkbW1Cfl5H+M6nVXSR1bOdmxTObTz7
|
||||
CEeJMOVrZs36hVLMWLqZF0igVebO2AsDOY63fy/9MLn8ynCHhnAMvsm9ULWuFzGj
|
||||
OsJezChduaHqPkopgwihe7jthUn4qWjABbbzKkS6HLBpGAfCzUun+lMpvIEUf+EJ
|
||||
bpk7gj9xDEP6y96tV/dCeWb4p8N8webR8nVgsRxoEnfIdCkoB80iZGOzKfYYnvdz
|
||||
ngs8MIL6dC4Nc1/t9ECV4O/w4uwIH65nC1ay0YOK/O/j2SEfnVHQmAuOsgTz+pBn
|
||||
u6DIA2HsBzFdOCljtf3m4AeAaTbL7MBSDceApqg0lcrhjclqHJo1aJh3M6aVm3gq
|
||||
yUt7y26Hkh/vYEJwW4gqRho4gb7BvjTZh5LUbrjmRtexFQ1eWM82u23yYS2L+y2Y
|
||||
ejSKIKmJhXHqsgCVGYw5woZEEMzgpkoIWYG/Eoy+oVuU02QITh/Uc5VRsA9DuwSV
|
||||
Vw2F8gu/fHiadawxWIhUH+plFVQZc1KwgPcIMW3S
|
||||
=O0kP
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
```
|
||||
[^1]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability
|
||||
292
third_party/FreeRDP/channels/CMakeLists.txt
vendored
Normal file
292
third_party/FreeRDP/channels/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
include(CMakeParseArguments)
|
||||
include(CMakeDependentOption)
|
||||
|
||||
macro(define_channel_options)
|
||||
set(PREFIX "CHANNEL")
|
||||
|
||||
cmake_parse_arguments(
|
||||
${PREFIX} "" "NAME;TYPE;DESCRIPTION;SPECIFICATIONS;DEFAULT;CLIENT_DEFAULT;SERVER_DEFAULT" "" ${ARGN}
|
||||
)
|
||||
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}" CHANNEL_OPTION)
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_CLIENT_OPTION)
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" CHANNEL_SERVER_OPTION)
|
||||
string(TOUPPER "${CHANNEL_TYPE}" CHANNEL_TYPE)
|
||||
|
||||
if(CHANNEL_DEFAULT)
|
||||
set(OPTION_DEFAULT ${CHANNEL_DEFAULT})
|
||||
elseif(CHANNEL_CLIENT_OPTION OR CHANNEL_SERVER_OPTION)
|
||||
set(OPTION_DEFAULT "ON")
|
||||
endif()
|
||||
|
||||
set(CHANNEL_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel")
|
||||
set(CHANNEL_CLIENT_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel client")
|
||||
set(CHANNEL_SERVER_OPTION_DOC "Build ${CHANNEL_NAME} ${CHANNEL_TYPE} channel server")
|
||||
|
||||
if("${CHANNEL_TYPE}" STREQUAL "DYNAMIC")
|
||||
cmake_dependent_option(${CHANNEL_OPTION} "${CHANNEL_OPTION_DOC}" ${CHANNEL_DEFAULT} "CHANNEL_DRDYNVC" OFF)
|
||||
else()
|
||||
option(${CHANNEL_OPTION} "${CHANNEL_OPTION_DOC}" ${CHANNEL_DEFAULT})
|
||||
endif()
|
||||
|
||||
# If the channel was enabled before the client/server options will stay ensure
|
||||
# they are deleted if the channel is gone.
|
||||
if(NOT ${CHANNEL_OPTION})
|
||||
unset(${CHANNEL_CLIENT_OPTION} CACHE)
|
||||
unset(${CHANNEL_SERVER_OPTION} CACHE)
|
||||
endif()
|
||||
|
||||
cmake_dependent_option(
|
||||
${CHANNEL_CLIENT_OPTION} "${CHANNEL_CLIENT_OPTION_DOC}" ${CHANNEL_CLIENT_DEFAULT} "${CHANNEL_OPTION}" OFF
|
||||
)
|
||||
|
||||
cmake_dependent_option(
|
||||
${CHANNEL_SERVER_OPTION} "${CHANNEL_SERVER_OPTION_DOC}" ${CHANNEL_SERVER_DEFAULT} "${CHANNEL_OPTION}" OFF
|
||||
)
|
||||
endmacro(define_channel_options)
|
||||
|
||||
macro(define_channel _channel_name)
|
||||
set(CHANNEL_NAME ${_channel_name})
|
||||
set(MODULE_NAME ${CHANNEL_NAME})
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}" MODULE_PREFIX)
|
||||
endmacro(define_channel)
|
||||
|
||||
macro(define_channel_client _channel_name)
|
||||
set(CHANNEL_NAME ${_channel_name})
|
||||
set(MODULE_NAME "${CHANNEL_NAME}-client")
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" MODULE_PREFIX)
|
||||
endmacro(define_channel_client)
|
||||
|
||||
macro(define_channel_server _channel_name)
|
||||
set(CHANNEL_NAME ${_channel_name})
|
||||
set(MODULE_NAME "${CHANNEL_NAME}-server")
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_SERVER" MODULE_PREFIX)
|
||||
endmacro(define_channel_server)
|
||||
|
||||
macro(define_channel_client_subsystem _channel_name _subsystem _type)
|
||||
set(CHANNEL_NAME ${_channel_name})
|
||||
set(CHANNEL_SUBSYSTEM ${_subsystem})
|
||||
string(LENGTH "${_type}" _type_length)
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT" CHANNEL_PREFIX)
|
||||
if(_type_length GREATER 0)
|
||||
set(SUBSYSTEM_TYPE ${_type})
|
||||
set(MODULE_NAME "${CHANNEL_NAME}-client-${CHANNEL_SUBSYSTEM}-${SUBSYSTEM_TYPE}")
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT_${CHANNEL_SUBSYSTEM}_${SUBSYSTEM_TYPE}" MODULE_PREFIX)
|
||||
else()
|
||||
set(MODULE_NAME "${CHANNEL_NAME}-client-${CHANNEL_SUBSYSTEM}")
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_CLIENT_${CHANNEL_SUBSYSTEM}" MODULE_PREFIX)
|
||||
endif()
|
||||
endmacro(define_channel_client_subsystem)
|
||||
|
||||
macro(define_channel_server_subsystem _channel_name _subsystem _type)
|
||||
set(CHANNEL_NAME ${_channel_name})
|
||||
set(CHANNEL_SUBSYSTEM ${_subsystem})
|
||||
set(MODULE_NAME "${CHANNEL_NAME}-server-${CHANNEL_SUBSYSTEM}")
|
||||
string(TOUPPER "CHANNEL_${CHANNEL_NAME}_server_${CHANNEL_SUBSYSTEM}" MODULE_PREFIX)
|
||||
endmacro(define_channel_server_subsystem)
|
||||
|
||||
macro(add_channel_client _channel_prefix _channel_name)
|
||||
if(${_channel_prefix}_CLIENT)
|
||||
add_subdirectory(client)
|
||||
if(${${_channel_prefix}_CLIENT_STATIC})
|
||||
set(CHANNEL_STATIC_CLIENT_MODULES ${CHANNEL_STATIC_CLIENT_MODULES} ${_channel_prefix} PARENT_SCOPE)
|
||||
set(${_channel_prefix}_CLIENT_NAME ${${_channel_prefix}_CLIENT_NAME} PARENT_SCOPE)
|
||||
set(${_channel_prefix}_CLIENT_CHANNEL ${${_channel_prefix}_CLIENT_CHANNEL} PARENT_SCOPE)
|
||||
set(${_channel_prefix}_CLIENT_ENTRY ${${_channel_prefix}_CLIENT_ENTRY} PARENT_SCOPE)
|
||||
set(CHANNEL_STATIC_CLIENT_ENTRIES ${CHANNEL_STATIC_CLIENT_ENTRIES} ${${_channel_prefix}_CLIENT_ENTRY}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endmacro(add_channel_client)
|
||||
|
||||
macro(add_channel_server _channel_prefix _channel_name)
|
||||
if(${_channel_prefix}_SERVER)
|
||||
add_subdirectory(server)
|
||||
if(${${_channel_prefix}_SERVER_STATIC})
|
||||
set(CHANNEL_STATIC_SERVER_MODULES ${CHANNEL_STATIC_SERVER_MODULES} ${_channel_prefix} PARENT_SCOPE)
|
||||
set(${_channel_prefix}_SERVER_NAME ${${_channel_prefix}_SERVER_NAME} PARENT_SCOPE)
|
||||
set(${_channel_prefix}_SERVER_CHANNEL ${${_channel_prefix}_SERVER_CHANNEL} PARENT_SCOPE)
|
||||
set(${_channel_prefix}_SERVER_ENTRY ${${_channel_prefix}_SERVER_ENTRY} PARENT_SCOPE)
|
||||
set(CHANNEL_STATIC_SERVER_ENTRIES ${CHANNEL_STATIC_SERVER_ENTRIES} ${${_channel_prefix}_SERVER_ENTRY}
|
||||
PARENT_SCOPE
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endmacro(add_channel_server)
|
||||
|
||||
macro(add_channel_client_subsystem _channel_prefix _channel_name _subsystem _type)
|
||||
add_subdirectory(${_subsystem})
|
||||
set(_channel_module_name "${_channel_name}-client")
|
||||
string(LENGTH "${_type}" _type_length)
|
||||
if(_type_length GREATER 0)
|
||||
string(TOUPPER "CHANNEL_${_channel_name}_CLIENT_${_subsystem}_${_type}" _subsystem_prefix)
|
||||
else()
|
||||
string(TOUPPER "CHANNEL_${_channel_name}_CLIENT_${_subsystem}" _subsystem_prefix)
|
||||
endif()
|
||||
if(${${_subsystem_prefix}_STATIC})
|
||||
get_target_property(CHANNEL_SUBSYSTEMS ${_channel_module_name} SUBSYSTEMS)
|
||||
if(_type_length GREATER 0)
|
||||
set(SUBSYSTEMS ${SUBSYSTEMS} "${_subsystem}-${_type}")
|
||||
else()
|
||||
set(SUBSYSTEMS ${SUBSYSTEMS} ${_subsystem})
|
||||
endif()
|
||||
set_target_properties(${_channel_module_name} PROPERTIES SUBSYSTEMS "${SUBSYSTEMS}")
|
||||
endif()
|
||||
endmacro(add_channel_client_subsystem)
|
||||
|
||||
macro(channel_install _targets _destination _export_target)
|
||||
if(NOT BUILD_SHARED_LIBS)
|
||||
foreach(_target_name IN ITEMS ${_targets})
|
||||
target_include_directories(${_target_name} INTERFACE $<INSTALL_INTERFACE:include>)
|
||||
endforeach()
|
||||
installwithrpath(TARGETS ${_targets} DESTINATION ${_destination} EXPORT ${_export_target})
|
||||
endif()
|
||||
endmacro(channel_install)
|
||||
|
||||
macro(server_channel_install _targets _destination)
|
||||
channel_install(${_targets} ${_destination} "FreeRDP-ServerTargets")
|
||||
endmacro(server_channel_install)
|
||||
|
||||
macro(client_channel_install _targets _destination)
|
||||
channel_install(${_targets} ${_destination} "FreeRDP-ClientTargets")
|
||||
endmacro(client_channel_install)
|
||||
|
||||
macro(add_channel_client_library _module_prefix _module_name _channel_name _dynamic _entry)
|
||||
set(_lnk_dir ${${_module_prefix}_LINK_DIRS})
|
||||
if(NOT "${_lnk_dir}" STREQUAL "")
|
||||
link_directories(${_lnk_dir})
|
||||
endif()
|
||||
|
||||
set(${_module_prefix}_STATIC ON PARENT_SCOPE)
|
||||
set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE)
|
||||
set(${_module_prefix}_CHANNEL ${_channel_name} PARENT_SCOPE)
|
||||
set(${_module_prefix}_ENTRY ${_entry} PARENT_SCOPE)
|
||||
|
||||
add_library(${_module_name} OBJECT ${${_module_prefix}_SRCS})
|
||||
set_property(TARGET ${_module_name} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Client")
|
||||
|
||||
if(${_module_prefix}_LIBS)
|
||||
target_link_libraries(${_module_name} PUBLIC ${${_module_prefix}_LIBS})
|
||||
endif()
|
||||
client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH})
|
||||
endmacro(add_channel_client_library)
|
||||
|
||||
macro(
|
||||
add_channel_client_subsystem_library
|
||||
_module_prefix
|
||||
_module_name
|
||||
_channel_name
|
||||
_type
|
||||
_dynamic
|
||||
_entry
|
||||
)
|
||||
set(_lnk_dir ${${_module_prefix}_LINK_DIRS})
|
||||
if(NOT "${_lnk_dir}" STREQUAL "")
|
||||
link_directories(${_lnk_dir})
|
||||
endif()
|
||||
|
||||
set(${_module_prefix}_STATIC ON PARENT_SCOPE)
|
||||
set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE)
|
||||
set(${_module_prefix}_TYPE ${_type} PARENT_SCOPE)
|
||||
|
||||
add_library(${_module_name} OBJECT ${${_module_prefix}_SRCS})
|
||||
set_property(TARGET ${_module_name} PROPERTY FOLDER "Channels/${_channel_name}/Client/Subsystem/${CHANNEL_SUBSYSTEM}")
|
||||
|
||||
if(${_module_prefix}_LIBS)
|
||||
target_link_libraries(${_module_name} PUBLIC ${${_module_prefix}_LIBS})
|
||||
endif()
|
||||
client_channel_install(${_module_name} ${FREERDP_ADDIN_PATH})
|
||||
endmacro(add_channel_client_subsystem_library)
|
||||
|
||||
macro(add_channel_server_library _module_prefix _module_name _channel_name _dynamic _entry)
|
||||
set(_lnk_dir ${${_module_prefix}_LINK_DIRS})
|
||||
if(NOT "${_lnk_dir}" STREQUAL "")
|
||||
link_directories(${_lnk_dir})
|
||||
endif()
|
||||
|
||||
set(${_module_prefix}_STATIC ON PARENT_SCOPE)
|
||||
set(${_module_prefix}_NAME ${_module_name} PARENT_SCOPE)
|
||||
set(${_module_prefix}_CHANNEL ${_channel_name} PARENT_SCOPE)
|
||||
set(${_module_prefix}_ENTRY ${_entry} PARENT_SCOPE)
|
||||
|
||||
add_library(${_module_name} OBJECT ${${_module_prefix}_SRCS})
|
||||
set_property(TARGET ${_module_name} PROPERTY FOLDER "Channels/${CHANNEL_NAME}/Server")
|
||||
|
||||
if(${_module_prefix}_LIBS)
|
||||
target_link_libraries(${_module_name} PUBLIC ${${_module_prefix}_LIBS})
|
||||
endif()
|
||||
server_channel_install(${_module_name} ${FREERDP_ADDIN_PATH})
|
||||
endmacro(add_channel_server_library)
|
||||
|
||||
set(FILENAME "ChannelOptions.cmake")
|
||||
file(GLOB FILEPATHS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*/${FILENAME}")
|
||||
|
||||
# We need special treatment for drdynvc:
|
||||
# It needs to be the first entry so that every
|
||||
# dynamic channel has the dependent options available.
|
||||
set(DRDYNVC_MATCH "")
|
||||
|
||||
foreach(FILEPATH ${FILEPATHS})
|
||||
if(${FILEPATH} MATCHES "^([^/]*)drdynvc/+${FILENAME}")
|
||||
set(DRDYNVC_MATCH ${FILEPATH})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT "${DRDYNVC_MATCH}" STREQUAL "")
|
||||
list(REMOVE_ITEM FILEPATHS ${DRDYNVC_MATCH})
|
||||
list(APPEND FILEPATHS ${DRDYNVC_MATCH})
|
||||
list(REVERSE FILEPATHS) # list PREPEND is not available on old CMake3
|
||||
endif()
|
||||
|
||||
foreach(FILEPATH ${FILEPATHS})
|
||||
if(${FILEPATH} MATCHES "^([^/]*)/+${FILENAME}")
|
||||
string(REGEX REPLACE "^([^/]*)/+${FILENAME}" "\\1" DIR ${FILEPATH})
|
||||
set(CHANNEL_OPTION)
|
||||
include(${FILEPATH})
|
||||
if(${CHANNEL_OPTION})
|
||||
set(CHANNEL_MESSAGE "Adding ${CHANNEL_TYPE} channel")
|
||||
if(${CHANNEL_CLIENT_OPTION})
|
||||
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} client")
|
||||
endif()
|
||||
if(${CHANNEL_SERVER_OPTION})
|
||||
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} server")
|
||||
endif()
|
||||
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE} \"${CHANNEL_NAME}\"")
|
||||
set(CHANNEL_MESSAGE "${CHANNEL_MESSAGE}: ${CHANNEL_DESCRIPTION}")
|
||||
message(STATUS "${CHANNEL_MESSAGE}")
|
||||
add_subdirectory(${DIR})
|
||||
endif()
|
||||
endif()
|
||||
endforeach(FILEPATH)
|
||||
|
||||
if(WITH_CHANNELS)
|
||||
if(WITH_CLIENT_CHANNELS)
|
||||
add_subdirectory(client)
|
||||
set(FREERDP_CHANNELS_CLIENT_SRCS ${FREERDP_CHANNELS_CLIENT_SRCS} PARENT_SCOPE)
|
||||
set(FREERDP_CHANNELS_CLIENT_LIBS ${FREERDP_CHANNELS_CLIENT_LIBS} PARENT_SCOPE)
|
||||
endif()
|
||||
|
||||
if(WITH_SERVER_CHANNELS)
|
||||
add_subdirectory(server)
|
||||
set(FREERDP_CHANNELS_SERVER_SRCS ${FREERDP_CHANNELS_SERVER_SRCS} PARENT_SCOPE)
|
||||
set(FREERDP_CHANNELS_SERVER_LIBS ${FREERDP_CHANNELS_SERVER_LIBS} PARENT_SCOPE)
|
||||
endif()
|
||||
endif()
|
||||
27
third_party/FreeRDP/channels/ainput/CMakeLists.txt
vendored
Normal file
27
third_party/FreeRDP/channels/ainput/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
# Copyright 2022 Thincast Technologies GmbH
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel("ainput")
|
||||
|
||||
if(WITH_CLIENT_CHANNELS)
|
||||
add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME})
|
||||
endif()
|
||||
|
||||
if(WITH_SERVER_CHANNELS)
|
||||
add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME})
|
||||
endif()
|
||||
20
third_party/FreeRDP/channels/ainput/ChannelOptions.cmake
vendored
Normal file
20
third_party/FreeRDP/channels/ainput/ChannelOptions.cmake
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
set(OPTION_DEFAULT ON)
|
||||
set(OPTION_CLIENT_DEFAULT ON)
|
||||
set(OPTION_SERVER_DEFAULT ON)
|
||||
|
||||
define_channel_options(
|
||||
NAME
|
||||
"ainput"
|
||||
TYPE
|
||||
"dynamic"
|
||||
DESCRIPTION
|
||||
"Advanced Input Virtual Channel Extension"
|
||||
SPECIFICATIONS
|
||||
"[XXXXX]"
|
||||
DEFAULT
|
||||
${OPTION_DEFAULT}
|
||||
CLIENT_DEFAULT
|
||||
${OPTION_CLIENT_DEFAULT}
|
||||
SERVER_DEFAULT
|
||||
${OPTION_SERVER_DEFAULT}
|
||||
)
|
||||
27
third_party/FreeRDP/channels/ainput/client/CMakeLists.txt
vendored
Normal file
27
third_party/FreeRDP/channels/ainput/client/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
# Copyright 2022 Thincast Technologies GmbH
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client("ainput")
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS ainput_main.c ainput_main.h)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr)
|
||||
|
||||
include_directories(..)
|
||||
|
||||
add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry")
|
||||
200
third_party/FreeRDP/channels/ainput/client/ainput_main.c
vendored
Normal file
200
third_party/FreeRDP/channels/ainput/client/ainput_main.c
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Advanced Input Virtual Channel Extension
|
||||
*
|
||||
* Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
* Copyright 2022 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/assert.h>
|
||||
#include <winpr/stream.h>
|
||||
#include <winpr/sysinfo.h>
|
||||
|
||||
#include "ainput_main.h"
|
||||
#include <freerdp/channels/log.h>
|
||||
#include <freerdp/client/channels.h>
|
||||
#include <freerdp/client/ainput.h>
|
||||
#include <freerdp/channels/ainput.h>
|
||||
|
||||
#include "../common/ainput_common.h"
|
||||
|
||||
#define TAG CHANNELS_TAG("ainput.client")
|
||||
|
||||
typedef struct AINPUT_PLUGIN_ AINPUT_PLUGIN;
|
||||
struct AINPUT_PLUGIN_
|
||||
{
|
||||
GENERIC_DYNVC_PLUGIN base;
|
||||
AInputClientContext* context;
|
||||
UINT32 MajorVersion;
|
||||
UINT32 MinorVersion;
|
||||
CRITICAL_SECTION lock;
|
||||
};
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT ainput_on_data_received(IWTSVirtualChannelCallback* pChannelCallback, wStream* data)
|
||||
{
|
||||
UINT16 type = 0;
|
||||
AINPUT_PLUGIN* ainput = nullptr;
|
||||
GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback;
|
||||
|
||||
WINPR_ASSERT(callback);
|
||||
WINPR_ASSERT(data);
|
||||
|
||||
ainput = (AINPUT_PLUGIN*)callback->plugin;
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (!Stream_CheckAndLogRequiredLength(TAG, data, 2))
|
||||
return ERROR_NO_DATA;
|
||||
Stream_Read_UINT16(data, type);
|
||||
switch (type)
|
||||
{
|
||||
case MSG_AINPUT_VERSION:
|
||||
if (!Stream_CheckAndLogRequiredLength(TAG, data, 8))
|
||||
return ERROR_NO_DATA;
|
||||
Stream_Read_UINT32(data, ainput->MajorVersion);
|
||||
Stream_Read_UINT32(data, ainput->MinorVersion);
|
||||
break;
|
||||
default:
|
||||
WLog_WARN(TAG, "Received unsupported message type 0x%04" PRIx16, type);
|
||||
break;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static UINT ainput_send_input_event(AInputClientContext* context, UINT64 flags, INT32 x, INT32 y)
|
||||
{
|
||||
BYTE buffer[32] = WINPR_C_ARRAY_INIT;
|
||||
wStream sbuffer = WINPR_C_ARRAY_INIT;
|
||||
wStream* s = Stream_StaticInit(&sbuffer, buffer, sizeof(buffer));
|
||||
|
||||
WINPR_ASSERT(s);
|
||||
WINPR_ASSERT(context);
|
||||
|
||||
const UINT64 time = GetTickCount64();
|
||||
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)context->handle;
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (ainput->MajorVersion != AINPUT_VERSION_MAJOR)
|
||||
{
|
||||
WLog_WARN(TAG, "Unsupported channel version %" PRIu32 ".%" PRIu32 ", aborting.",
|
||||
ainput->MajorVersion, ainput->MinorVersion);
|
||||
return CHANNEL_RC_UNSUPPORTED_VERSION;
|
||||
}
|
||||
|
||||
{
|
||||
char ebuffer[128] = WINPR_C_ARRAY_INIT;
|
||||
WLog_VRB(TAG, "sending timestamp=0x%08" PRIx64 ", flags=%s, %" PRId32 "x%" PRId32, time,
|
||||
ainput_flags_to_string(flags, ebuffer, sizeof(ebuffer)), x, y);
|
||||
}
|
||||
|
||||
/* Message type */
|
||||
Stream_Write_UINT16(s, MSG_AINPUT_MOUSE);
|
||||
|
||||
/* Event data */
|
||||
Stream_Write_UINT64(s, time);
|
||||
Stream_Write_UINT64(s, flags);
|
||||
Stream_Write_INT32(s, x);
|
||||
Stream_Write_INT32(s, y);
|
||||
Stream_SealLength(s);
|
||||
|
||||
/* ainput back what we have received. AINPUT does not have any message IDs. */
|
||||
EnterCriticalSection(&ainput->lock);
|
||||
GENERIC_CHANNEL_CALLBACK* callback = ainput->base.listener_callback->channel_callback;
|
||||
WINPR_ASSERT(callback);
|
||||
WINPR_ASSERT(callback->channel);
|
||||
WINPR_ASSERT(callback->channel->Write);
|
||||
const UINT rc = callback->channel->Write(callback->channel, (ULONG)Stream_Length(s),
|
||||
Stream_Buffer(s), nullptr);
|
||||
LeaveCriticalSection(&ainput->lock);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT ainput_on_close(IWTSVirtualChannelCallback* pChannelCallback)
|
||||
{
|
||||
GENERIC_CHANNEL_CALLBACK* callback = (GENERIC_CHANNEL_CALLBACK*)pChannelCallback;
|
||||
|
||||
if (callback)
|
||||
{
|
||||
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)callback->plugin;
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
/* Lock here to ensure that no ainput_send_input_event is in progress. */
|
||||
EnterCriticalSection(&ainput->lock);
|
||||
free(callback);
|
||||
LeaveCriticalSection(&ainput->lock);
|
||||
}
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static UINT init_plugin_cb(GENERIC_DYNVC_PLUGIN* base, WINPR_ATTR_UNUSED rdpContext* rcontext,
|
||||
WINPR_ATTR_UNUSED rdpSettings* settings)
|
||||
{
|
||||
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)base;
|
||||
AInputClientContext* context = (AInputClientContext*)calloc(1, sizeof(AInputClientContext));
|
||||
if (!context)
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
|
||||
context->handle = (void*)base;
|
||||
context->AInputSendInputEvent = ainput_send_input_event;
|
||||
|
||||
InitializeCriticalSection(&ainput->lock);
|
||||
|
||||
EnterCriticalSection(&ainput->lock);
|
||||
ainput->context = context;
|
||||
ainput->base.iface.pInterface = context;
|
||||
LeaveCriticalSection(&ainput->lock);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static void terminate_plugin_cb(GENERIC_DYNVC_PLUGIN* base)
|
||||
{
|
||||
AINPUT_PLUGIN* ainput = (AINPUT_PLUGIN*)base;
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
DeleteCriticalSection(&ainput->lock);
|
||||
free(ainput->context);
|
||||
}
|
||||
|
||||
static const IWTSVirtualChannelCallback ainput_functions = { ainput_on_data_received,
|
||||
nullptr, /* Open */
|
||||
ainput_on_close, nullptr };
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE ainput_DVCPluginEntry(IDRDYNVC_ENTRY_POINTS* pEntryPoints))
|
||||
{
|
||||
return freerdp_generic_DVCPluginEntry(pEntryPoints, TAG, AINPUT_DVC_CHANNEL_NAME,
|
||||
sizeof(AINPUT_PLUGIN), sizeof(GENERIC_CHANNEL_CALLBACK),
|
||||
&ainput_functions, init_plugin_cb, terminate_plugin_cb);
|
||||
}
|
||||
40
third_party/FreeRDP/channels/ainput/client/ainput_main.h
vendored
Normal file
40
third_party/FreeRDP/channels/ainput/client/ainput_main.h
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Advanced Input Virtual Channel Extension
|
||||
*
|
||||
* Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
* Copyright 2022 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H
|
||||
#define FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H
|
||||
|
||||
#include <freerdp/config.h>
|
||||
#include <freerdp/dvc.h>
|
||||
#include <freerdp/types.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/log.h>
|
||||
|
||||
#define DVC_TAG CHANNELS_TAG("ainput.client")
|
||||
#ifdef WITH_DEBUG_DVC
|
||||
#define DEBUG_DVC(...) WLog_DBG(DVC_TAG, __VA_ARGS__)
|
||||
#else
|
||||
#define DEBUG_DVC(...) \
|
||||
do \
|
||||
{ \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
#endif /* FREERDP_CHANNEL_AINPUT_CLIENT_MAIN_H */
|
||||
60
third_party/FreeRDP/channels/ainput/common/ainput_common.h
vendored
Normal file
60
third_party/FreeRDP/channels/ainput/common/ainput_common.h
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel
|
||||
*
|
||||
* Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
* Copyright 2022 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FREERDP_INT_AINPUT_COMMON_H
|
||||
#define FREERDP_INT_AINPUT_COMMON_H
|
||||
|
||||
#include <winpr/string.h>
|
||||
|
||||
#include <freerdp/channels/ainput.h>
|
||||
|
||||
WINPR_ATTR_NODISCARD
|
||||
static inline const char* ainput_flags_to_string(UINT64 flags, char* buffer, size_t size)
|
||||
{
|
||||
char number[32] = WINPR_C_ARRAY_INIT;
|
||||
|
||||
if (flags & AINPUT_FLAGS_HAVE_REL)
|
||||
winpr_str_append("AINPUT_FLAGS_HAVE_REL", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_WHEEL)
|
||||
winpr_str_append("AINPUT_FLAGS_WHEEL", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_MOVE)
|
||||
winpr_str_append("AINPUT_FLAGS_MOVE", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_DOWN)
|
||||
winpr_str_append("AINPUT_FLAGS_DOWN", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_REL)
|
||||
winpr_str_append("AINPUT_FLAGS_REL", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_BUTTON1)
|
||||
winpr_str_append("AINPUT_FLAGS_BUTTON1", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_BUTTON2)
|
||||
winpr_str_append("AINPUT_FLAGS_BUTTON2", buffer, size, "|");
|
||||
if (flags & AINPUT_FLAGS_BUTTON3)
|
||||
winpr_str_append("AINPUT_FLAGS_BUTTON3", buffer, size, "|");
|
||||
if (flags & AINPUT_XFLAGS_BUTTON1)
|
||||
winpr_str_append("AINPUT_XFLAGS_BUTTON1", buffer, size, "|");
|
||||
if (flags & AINPUT_XFLAGS_BUTTON2)
|
||||
winpr_str_append("AINPUT_XFLAGS_BUTTON2", buffer, size, "|");
|
||||
|
||||
_snprintf(number, sizeof(number), "[0x%08" PRIx64 "]", flags);
|
||||
winpr_str_append(number, buffer, size, " ");
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
#endif /* FREERDP_INT_AINPUT_COMMON_H */
|
||||
24
third_party/FreeRDP/channels/ainput/server/CMakeLists.txt
vendored
Normal file
24
third_party/FreeRDP/channels/ainput/server/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
# Copyright 2022 Thincast Technologies GmbH
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_server("ainput")
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS ainput_main.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS freerdp)
|
||||
add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry")
|
||||
596
third_party/FreeRDP/channels/ainput/server/ainput_main.c
vendored
Normal file
596
third_party/FreeRDP/channels/ainput/server/ainput_main.c
vendored
Normal file
@@ -0,0 +1,596 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Advanced Input Virtual Channel Extension
|
||||
*
|
||||
* Copyright 2022 Armin Novak <anovak@thincast.com>
|
||||
* Copyright 2022 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/assert.h>
|
||||
#include <winpr/synch.h>
|
||||
#include <winpr/thread.h>
|
||||
#include <winpr/stream.h>
|
||||
#include <winpr/sysinfo.h>
|
||||
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/channels/ainput.h>
|
||||
#include <freerdp/server/ainput.h>
|
||||
#include <freerdp/channels/log.h>
|
||||
|
||||
#include "../common/ainput_common.h"
|
||||
|
||||
#define TAG CHANNELS_TAG("ainput.server")
|
||||
|
||||
typedef enum
|
||||
{
|
||||
AINPUT_INITIAL,
|
||||
AINPUT_OPENED,
|
||||
AINPUT_VERSION_SENT,
|
||||
} eAInputChannelState;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
ainput_server_context context;
|
||||
|
||||
BOOL opened;
|
||||
|
||||
HANDLE stopEvent;
|
||||
|
||||
HANDLE thread;
|
||||
void* ainput_channel;
|
||||
|
||||
DWORD SessionId;
|
||||
|
||||
BOOL isOpened;
|
||||
BOOL externalThread;
|
||||
|
||||
/* Channel state */
|
||||
eAInputChannelState state;
|
||||
|
||||
wStream* buffer;
|
||||
} ainput_server;
|
||||
|
||||
static UINT ainput_server_context_poll(ainput_server_context* context);
|
||||
static BOOL ainput_server_context_handle(ainput_server_context* context, HANDLE* handle);
|
||||
static UINT ainput_server_context_poll_int(ainput_server_context* context);
|
||||
|
||||
static BOOL ainput_server_is_open(ainput_server_context* context)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
return ainput->isOpened;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT ainput_server_open_channel(ainput_server* ainput)
|
||||
{
|
||||
DWORD Error = 0;
|
||||
HANDLE hEvent = nullptr;
|
||||
DWORD StartTick = 0;
|
||||
DWORD BytesReturned = 0;
|
||||
PULONG pSessionId = nullptr;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (WTSQuerySessionInformationA(ainput->context.vcm, WTS_CURRENT_SESSION, WTSSessionId,
|
||||
(LPSTR*)&pSessionId, &BytesReturned) == FALSE)
|
||||
{
|
||||
WLog_ERR(TAG, "WTSQuerySessionInformationA failed!");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
ainput->SessionId = (DWORD)*pSessionId;
|
||||
WTSFreeMemory(pSessionId);
|
||||
hEvent = WTSVirtualChannelManagerGetEventHandle(ainput->context.vcm);
|
||||
StartTick = GetTickCount();
|
||||
|
||||
while (ainput->ainput_channel == nullptr)
|
||||
{
|
||||
if (WaitForSingleObject(hEvent, 1000) == WAIT_FAILED)
|
||||
{
|
||||
Error = GetLastError();
|
||||
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "!", Error);
|
||||
return Error;
|
||||
}
|
||||
|
||||
ainput->ainput_channel = WTSVirtualChannelOpenEx(ainput->SessionId, AINPUT_DVC_CHANNEL_NAME,
|
||||
WTS_CHANNEL_OPTION_DYNAMIC);
|
||||
|
||||
Error = GetLastError();
|
||||
|
||||
if (Error == ERROR_NOT_FOUND)
|
||||
{
|
||||
WLog_DBG(TAG, "Channel %s not found", AINPUT_DVC_CHANNEL_NAME);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ainput->ainput_channel)
|
||||
{
|
||||
UINT32 channelId = 0;
|
||||
BOOL status = TRUE;
|
||||
|
||||
channelId = WTSChannelGetIdByHandle(ainput->ainput_channel);
|
||||
|
||||
IFCALLRET(ainput->context.ChannelIdAssigned, status, &ainput->context, channelId);
|
||||
if (!status)
|
||||
{
|
||||
WLog_ERR(TAG, "context->ChannelIdAssigned failed!");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (GetTickCount() - StartTick > 5000)
|
||||
{
|
||||
WLog_WARN(TAG, "Timeout opening channel %s", AINPUT_DVC_CHANNEL_NAME);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ainput->ainput_channel ? CHANNEL_RC_OK : ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
static UINT ainput_server_send_version(ainput_server* ainput)
|
||||
{
|
||||
ULONG written = 0;
|
||||
wStream* s = nullptr;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
s = ainput->buffer;
|
||||
WINPR_ASSERT(s);
|
||||
|
||||
Stream_ResetPosition(s);
|
||||
if (!Stream_EnsureCapacity(s, 10))
|
||||
{
|
||||
WLog_WARN(TAG, "[%s] out of memory", AINPUT_DVC_CHANNEL_NAME);
|
||||
return ERROR_OUTOFMEMORY;
|
||||
}
|
||||
|
||||
Stream_Write_UINT16(s, MSG_AINPUT_VERSION);
|
||||
Stream_Write_UINT32(s, AINPUT_VERSION_MAJOR); /* Version (4 bytes) */
|
||||
Stream_Write_UINT32(s, AINPUT_VERSION_MINOR); /* Version (4 bytes) */
|
||||
|
||||
WINPR_ASSERT(Stream_GetPosition(s) <= UINT32_MAX);
|
||||
if (!WTSVirtualChannelWrite(ainput->ainput_channel, Stream_BufferAs(s, char),
|
||||
(ULONG)Stream_GetPosition(s), &written))
|
||||
{
|
||||
WLog_ERR(TAG, "WTSVirtualChannelWrite failed!");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static UINT ainput_server_recv_mouse_event(ainput_server* ainput, wStream* s)
|
||||
{
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
UINT64 flags = 0;
|
||||
UINT64 time = 0;
|
||||
INT32 x = 0;
|
||||
INT32 y = 0;
|
||||
char buffer[128] = WINPR_C_ARRAY_INIT;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
WINPR_ASSERT(s);
|
||||
|
||||
if (!Stream_CheckAndLogRequiredLength(TAG, s, 24))
|
||||
return ERROR_NO_DATA;
|
||||
|
||||
Stream_Read_UINT64(s, time);
|
||||
Stream_Read_UINT64(s, flags);
|
||||
Stream_Read_INT32(s, x);
|
||||
Stream_Read_INT32(s, y);
|
||||
|
||||
WLog_VRB(TAG, "received: time=0x%08" PRIx64 ", flags=%s, %" PRId32 "x%" PRId32, time,
|
||||
ainput_flags_to_string(flags, buffer, sizeof(buffer)), x, y);
|
||||
IFCALLRET(ainput->context.MouseEvent, error, &ainput->context, time, flags, x, y);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static HANDLE ainput_server_get_channel_handle(ainput_server* ainput)
|
||||
{
|
||||
void* buffer = nullptr;
|
||||
DWORD BytesReturned = 0;
|
||||
HANDLE ChannelEvent = nullptr;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (WTSVirtualChannelQuery(ainput->ainput_channel, WTSVirtualEventHandle, &buffer,
|
||||
&BytesReturned) == TRUE)
|
||||
{
|
||||
if (BytesReturned == sizeof(HANDLE))
|
||||
ChannelEvent = *(HANDLE*)buffer;
|
||||
|
||||
WTSFreeMemory(buffer);
|
||||
}
|
||||
|
||||
return ChannelEvent;
|
||||
}
|
||||
|
||||
static DWORD WINAPI ainput_server_thread_func(LPVOID arg)
|
||||
{
|
||||
DWORD nCount = 0;
|
||||
HANDLE events[2] = WINPR_C_ARRAY_INIT;
|
||||
ainput_server* ainput = (ainput_server*)arg;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
DWORD status = 0;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
nCount = 0;
|
||||
events[nCount++] = ainput->stopEvent;
|
||||
|
||||
while ((error == CHANNEL_RC_OK) && (WaitForSingleObject(events[0], 0) != WAIT_OBJECT_0))
|
||||
{
|
||||
switch (ainput->state)
|
||||
{
|
||||
case AINPUT_OPENED:
|
||||
events[1] = ainput_server_get_channel_handle(ainput);
|
||||
nCount = 2;
|
||||
status = WaitForMultipleObjects(nCount, events, FALSE, 100);
|
||||
switch (status)
|
||||
{
|
||||
case WAIT_TIMEOUT:
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
case WAIT_OBJECT_0:
|
||||
error = ainput_server_context_poll_int(&ainput->context);
|
||||
break;
|
||||
case WAIT_FAILED:
|
||||
default:
|
||||
WLog_WARN(TAG, "[%s] Wait for open failed", AINPUT_DVC_CHANNEL_NAME);
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case AINPUT_VERSION_SENT:
|
||||
status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE);
|
||||
switch (status)
|
||||
{
|
||||
case WAIT_TIMEOUT:
|
||||
case WAIT_OBJECT_0 + 1:
|
||||
case WAIT_OBJECT_0:
|
||||
error = ainput_server_context_poll_int(&ainput->context);
|
||||
break;
|
||||
|
||||
case WAIT_FAILED:
|
||||
default:
|
||||
WLog_WARN(TAG, "[%s] Wait for version failed", AINPUT_DVC_CHANNEL_NAME);
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
error = ainput_server_context_poll_int(&ainput->context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(void)WTSVirtualChannelClose(ainput->ainput_channel);
|
||||
ainput->ainput_channel = nullptr;
|
||||
|
||||
if (error && ainput->context.rdpcontext)
|
||||
setChannelError(ainput->context.rdpcontext, error,
|
||||
"ainput_server_thread_func reported an error");
|
||||
|
||||
ExitThread(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT ainput_server_open(ainput_server_context* context)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (!ainput->externalThread && (ainput->thread == nullptr))
|
||||
{
|
||||
ainput->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!ainput->stopEvent)
|
||||
{
|
||||
WLog_ERR(TAG, "CreateEvent failed!");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
ainput->thread = CreateThread(nullptr, 0, ainput_server_thread_func, ainput, 0, nullptr);
|
||||
if (!ainput->thread)
|
||||
{
|
||||
WLog_ERR(TAG, "CreateEvent failed!");
|
||||
(void)CloseHandle(ainput->stopEvent);
|
||||
ainput->stopEvent = nullptr;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
ainput->isOpened = TRUE;
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT ainput_server_close(ainput_server_context* context)
|
||||
{
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (!ainput->externalThread && ainput->thread)
|
||||
{
|
||||
(void)SetEvent(ainput->stopEvent);
|
||||
|
||||
if (WaitForSingleObject(ainput->thread, INFINITE) == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error);
|
||||
return error;
|
||||
}
|
||||
|
||||
(void)CloseHandle(ainput->thread);
|
||||
(void)CloseHandle(ainput->stopEvent);
|
||||
ainput->thread = nullptr;
|
||||
ainput->stopEvent = nullptr;
|
||||
}
|
||||
if (ainput->externalThread)
|
||||
{
|
||||
if (ainput->state != AINPUT_INITIAL)
|
||||
{
|
||||
(void)WTSVirtualChannelClose(ainput->ainput_channel);
|
||||
ainput->ainput_channel = nullptr;
|
||||
ainput->state = AINPUT_INITIAL;
|
||||
}
|
||||
}
|
||||
ainput->isOpened = FALSE;
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT ainput_server_initialize(ainput_server_context* context, BOOL externalThread)
|
||||
{
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
if (ainput->isOpened)
|
||||
{
|
||||
WLog_WARN(TAG, "Application error: AINPUT channel already initialized, calling in this "
|
||||
"state is not possible!");
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
ainput->externalThread = externalThread;
|
||||
return error;
|
||||
}
|
||||
|
||||
ainput_server_context* ainput_server_context_new(HANDLE vcm)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)calloc(1, sizeof(ainput_server));
|
||||
|
||||
if (!ainput)
|
||||
return nullptr;
|
||||
|
||||
ainput->context.vcm = vcm;
|
||||
ainput->context.Open = ainput_server_open;
|
||||
ainput->context.IsOpen = ainput_server_is_open;
|
||||
ainput->context.Close = ainput_server_close;
|
||||
ainput->context.Initialize = ainput_server_initialize;
|
||||
ainput->context.Poll = ainput_server_context_poll;
|
||||
ainput->context.ChannelHandle = ainput_server_context_handle;
|
||||
|
||||
ainput->buffer = Stream_New(nullptr, 4096);
|
||||
if (!ainput->buffer)
|
||||
goto fail;
|
||||
return &ainput->context;
|
||||
fail:
|
||||
WINPR_PRAGMA_DIAG_PUSH
|
||||
WINPR_PRAGMA_DIAG_IGNORED_MISMATCHED_DEALLOC
|
||||
ainput_server_context_free(&ainput->context);
|
||||
WINPR_PRAGMA_DIAG_POP
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ainput_server_context_free(ainput_server_context* context)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
if (ainput)
|
||||
{
|
||||
ainput_server_close(context);
|
||||
Stream_Free(ainput->buffer, TRUE);
|
||||
}
|
||||
free(ainput);
|
||||
}
|
||||
|
||||
static UINT ainput_process_message(ainput_server* ainput)
|
||||
{
|
||||
UINT error = ERROR_INTERNAL_ERROR;
|
||||
ULONG BytesReturned = 0;
|
||||
ULONG ActualBytesReturned = 0;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
WINPR_ASSERT(ainput->ainput_channel);
|
||||
|
||||
wStream* s = ainput->buffer;
|
||||
WINPR_ASSERT(s);
|
||||
|
||||
Stream_ResetPosition(s);
|
||||
const BOOL rc = WTSVirtualChannelRead(ainput->ainput_channel, 0, nullptr, 0, &BytesReturned);
|
||||
if (!rc)
|
||||
goto out;
|
||||
|
||||
if (BytesReturned < 2)
|
||||
{
|
||||
error = CHANNEL_RC_OK;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!Stream_EnsureRemainingCapacity(s, BytesReturned))
|
||||
{
|
||||
WLog_ERR(TAG, "Stream_EnsureRemainingCapacity failed!");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (WTSVirtualChannelRead(ainput->ainput_channel, 0, Stream_BufferAs(s, char),
|
||||
(ULONG)Stream_Capacity(s), &ActualBytesReturned) == FALSE)
|
||||
{
|
||||
WLog_ERR(TAG, "WTSVirtualChannelRead failed!");
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (BytesReturned != ActualBytesReturned)
|
||||
{
|
||||
WLog_ERR(TAG, "WTSVirtualChannelRead size mismatch %" PRIu32 ", expected %" PRIu32,
|
||||
ActualBytesReturned, BytesReturned);
|
||||
goto out;
|
||||
}
|
||||
|
||||
Stream_SetLength(s, ActualBytesReturned);
|
||||
{
|
||||
const UINT16 MessageId = Stream_Get_UINT16(s);
|
||||
|
||||
switch (MessageId)
|
||||
{
|
||||
case MSG_AINPUT_MOUSE:
|
||||
error = ainput_server_recv_mouse_event(ainput, s);
|
||||
break;
|
||||
|
||||
default:
|
||||
WLog_ERR(TAG, "audin_server_thread_func: unknown MessageId %" PRIu16 "", MessageId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
if (error)
|
||||
WLog_ERR(TAG, "Response failed with error %" PRIu32 "!", error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
BOOL ainput_server_context_handle(ainput_server_context* context, HANDLE* handle)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
WINPR_ASSERT(ainput);
|
||||
WINPR_ASSERT(handle);
|
||||
|
||||
if (!ainput->externalThread)
|
||||
{
|
||||
WLog_WARN(TAG, "[%s] externalThread fail!", AINPUT_DVC_CHANNEL_NAME);
|
||||
return FALSE;
|
||||
}
|
||||
if (ainput->state == AINPUT_INITIAL)
|
||||
{
|
||||
WLog_WARN(TAG, "[%s] state fail!", AINPUT_DVC_CHANNEL_NAME);
|
||||
return FALSE;
|
||||
}
|
||||
*handle = ainput_server_get_channel_handle(ainput);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
UINT ainput_server_context_poll_int(ainput_server_context* context)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
UINT error = ERROR_INTERNAL_ERROR;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
|
||||
switch (ainput->state)
|
||||
{
|
||||
case AINPUT_INITIAL:
|
||||
error = ainput_server_open_channel(ainput);
|
||||
if (error)
|
||||
WLog_ERR(TAG, "ainput_server_open_channel failed with error %" PRIu32 "!", error);
|
||||
else
|
||||
ainput->state = AINPUT_OPENED;
|
||||
break;
|
||||
case AINPUT_OPENED:
|
||||
{
|
||||
union
|
||||
{
|
||||
BYTE* pb;
|
||||
void* pv;
|
||||
} buffer;
|
||||
DWORD BytesReturned = 0;
|
||||
|
||||
buffer.pv = nullptr;
|
||||
|
||||
if (WTSVirtualChannelQuery(ainput->ainput_channel, WTSVirtualChannelReady, &buffer.pv,
|
||||
&BytesReturned) != TRUE)
|
||||
{
|
||||
WLog_ERR(TAG, "WTSVirtualChannelReady failed,");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (*buffer.pb != 0)
|
||||
{
|
||||
error = ainput_server_send_version(ainput);
|
||||
if (error)
|
||||
WLog_ERR(TAG, "audin_server_send_version failed with error %" PRIu32 "!",
|
||||
error);
|
||||
else
|
||||
ainput->state = AINPUT_VERSION_SENT;
|
||||
}
|
||||
else
|
||||
error = CHANNEL_RC_OK;
|
||||
}
|
||||
WTSFreeMemory(buffer.pv);
|
||||
}
|
||||
break;
|
||||
case AINPUT_VERSION_SENT:
|
||||
error = ainput_process_message(ainput);
|
||||
break;
|
||||
|
||||
default:
|
||||
WLog_ERR(TAG, "AINPUT channel is in invalid state %u", ainput->state);
|
||||
break;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
UINT ainput_server_context_poll(ainput_server_context* context)
|
||||
{
|
||||
ainput_server* ainput = (ainput_server*)context;
|
||||
|
||||
WINPR_ASSERT(ainput);
|
||||
if (!ainput->externalThread)
|
||||
{
|
||||
WLog_WARN(TAG, "[%s] externalThread fail!", AINPUT_DVC_CHANNEL_NAME);
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
return ainput_server_context_poll_int(context);
|
||||
}
|
||||
26
third_party/FreeRDP/channels/audin/CMakeLists.txt
vendored
Normal file
26
third_party/FreeRDP/channels/audin/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel("audin")
|
||||
|
||||
if(WITH_CLIENT_CHANNELS)
|
||||
add_channel_client(${MODULE_PREFIX} ${CHANNEL_NAME})
|
||||
endif()
|
||||
|
||||
if(WITH_SERVER_CHANNELS)
|
||||
add_channel_server(${MODULE_PREFIX} ${CHANNEL_NAME})
|
||||
endif()
|
||||
24
third_party/FreeRDP/channels/audin/ChannelOptions.cmake
vendored
Normal file
24
third_party/FreeRDP/channels/audin/ChannelOptions.cmake
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
set(OPTION_DEFAULT ON)
|
||||
set(OPTION_CLIENT_DEFAULT ON)
|
||||
set(OPTION_SERVER_DEFAULT ON)
|
||||
|
||||
if(ANDROID)
|
||||
set(OPTION_SERVER_DEFAULT OFF)
|
||||
endif()
|
||||
|
||||
define_channel_options(
|
||||
NAME
|
||||
"audin"
|
||||
TYPE
|
||||
"dynamic"
|
||||
DESCRIPTION
|
||||
"Audio Input Redirection Virtual Channel Extension"
|
||||
SPECIFICATIONS
|
||||
"[MS-RDPEAI]"
|
||||
DEFAULT
|
||||
${OPTION_DEFAULT}
|
||||
CLIENT_DEFAULT
|
||||
${OPTION_CLIENT_DEFAULT}
|
||||
SERVER_DEFAULT
|
||||
${OPTION_SERVER_DEFAULT}
|
||||
)
|
||||
58
third_party/FreeRDP/channels/audin/client/CMakeLists.txt
vendored
Normal file
58
third_party/FreeRDP/channels/audin/client/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client("audin")
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_main.c audin_main.h)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS freerdp winpr)
|
||||
|
||||
include_directories(..)
|
||||
|
||||
add_channel_client_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} TRUE "DVCPluginEntry")
|
||||
|
||||
if(WITH_OSS)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "oss" "")
|
||||
endif()
|
||||
|
||||
if(WITH_ALSA)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "alsa" "")
|
||||
endif()
|
||||
|
||||
if(WITH_PULSE)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "pulse" "")
|
||||
endif()
|
||||
|
||||
if(WITH_OPENSLES)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "opensles" "")
|
||||
endif()
|
||||
|
||||
if(WITH_WINMM)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "winmm" "")
|
||||
endif()
|
||||
|
||||
if(WITH_MACAUDIO)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "mac" "")
|
||||
endif()
|
||||
|
||||
if(WITH_SNDIO)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "sndio" "")
|
||||
endif()
|
||||
|
||||
if(WITH_IOSAUDIO)
|
||||
add_channel_client_subsystem(${MODULE_PREFIX} ${CHANNEL_NAME} "ios" "")
|
||||
endif()
|
||||
31
third_party/FreeRDP/channels/audin/client/alsa/CMakeLists.txt
vendored
Normal file
31
third_party/FreeRDP/channels/audin/client/alsa/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "alsa" "")
|
||||
|
||||
find_package(ALSA REQUIRED)
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_alsa.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${ALSA_LIBRARIES})
|
||||
|
||||
freerdp_client_pc_add_requires_private("alsa")
|
||||
|
||||
include_directories(..)
|
||||
include_directories(SYSTEM ${ALSA_INCLUDE_DIRS})
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
466
third_party/FreeRDP/channels/audin/client/alsa/audin_alsa.c
vendored
Normal file
466
third_party/FreeRDP/channels/audin/client/alsa/audin_alsa.c
vendored
Normal file
@@ -0,0 +1,466 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - ALSA implementation
|
||||
*
|
||||
* Copyright 2010-2011 Vic Lee
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/synch.h>
|
||||
#include <winpr/thread.h>
|
||||
#include <winpr/cmdline.h>
|
||||
#include <winpr/wlog.h>
|
||||
|
||||
#include <alsa/asoundlib.h>
|
||||
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/rdpsnd.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
char* device_name;
|
||||
UINT32 frames_per_packet;
|
||||
AUDIO_FORMAT aformat;
|
||||
|
||||
HANDLE thread;
|
||||
HANDLE stopEvent;
|
||||
|
||||
AudinReceive receive;
|
||||
void* user_data;
|
||||
|
||||
rdpContext* rdpcontext;
|
||||
wLog* log;
|
||||
size_t bytes_per_frame;
|
||||
} AudinALSADevice;
|
||||
|
||||
static snd_pcm_format_t audin_alsa_format(UINT32 wFormatTag, UINT32 bitPerChannel)
|
||||
{
|
||||
switch (wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
switch (bitPerChannel)
|
||||
{
|
||||
case 16:
|
||||
return SND_PCM_FORMAT_S16_LE;
|
||||
|
||||
case 8:
|
||||
return SND_PCM_FORMAT_S8;
|
||||
|
||||
default:
|
||||
return SND_PCM_FORMAT_UNKNOWN;
|
||||
}
|
||||
|
||||
default:
|
||||
return SND_PCM_FORMAT_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL audin_alsa_set_params(AudinALSADevice* alsa, snd_pcm_t* capture_handle)
|
||||
{
|
||||
int error = 0;
|
||||
SSIZE_T s = 0;
|
||||
UINT32 channels = alsa->aformat.nChannels;
|
||||
snd_pcm_hw_params_t* hw_params = nullptr;
|
||||
snd_pcm_format_t format =
|
||||
audin_alsa_format(alsa->aformat.wFormatTag, alsa->aformat.wBitsPerSample);
|
||||
|
||||
if ((error = snd_pcm_hw_params_malloc(&hw_params)) < 0)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_hw_params_malloc (%s)", snd_strerror(error));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
snd_pcm_hw_params_any(capture_handle, hw_params);
|
||||
snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED);
|
||||
snd_pcm_hw_params_set_format(capture_handle, hw_params, format);
|
||||
snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &alsa->aformat.nSamplesPerSec,
|
||||
nullptr);
|
||||
snd_pcm_hw_params_set_channels_near(capture_handle, hw_params, &channels);
|
||||
snd_pcm_hw_params(capture_handle, hw_params);
|
||||
snd_pcm_hw_params_free(hw_params);
|
||||
snd_pcm_prepare(capture_handle);
|
||||
if (channels > UINT16_MAX)
|
||||
return FALSE;
|
||||
s = snd_pcm_format_size(format, 1);
|
||||
if ((s < 0) || (s > UINT16_MAX))
|
||||
return FALSE;
|
||||
alsa->aformat.nChannels = (UINT16)channels;
|
||||
alsa->bytes_per_frame = (size_t)s * channels;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static DWORD WINAPI audin_alsa_thread_func(LPVOID arg)
|
||||
{
|
||||
DWORD error = CHANNEL_RC_OK;
|
||||
BYTE* buffer = nullptr;
|
||||
AudinALSADevice* alsa = (AudinALSADevice*)arg;
|
||||
|
||||
WINPR_ASSERT(alsa);
|
||||
|
||||
WLog_Print(alsa->log, WLOG_DEBUG, "in");
|
||||
|
||||
snd_pcm_t* capture_handle = nullptr;
|
||||
const int rc = snd_pcm_open(&capture_handle, alsa->device_name, SND_PCM_STREAM_CAPTURE, 0);
|
||||
if (rc < 0)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_open (%s)", snd_strerror(rc));
|
||||
error = CHANNEL_RC_INITIALIZATION_ERROR;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!audin_alsa_set_params(alsa, capture_handle))
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "audin_alsa_set_params failed");
|
||||
goto out;
|
||||
}
|
||||
|
||||
buffer =
|
||||
(BYTE*)calloc(alsa->frames_per_packet + alsa->aformat.nBlockAlign, alsa->bytes_per_frame);
|
||||
|
||||
if (!buffer)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "calloc failed!");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto out;
|
||||
}
|
||||
|
||||
while (1)
|
||||
{
|
||||
size_t frames = alsa->frames_per_packet;
|
||||
const DWORD status = WaitForSingleObject(alsa->stopEvent, 0);
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "!",
|
||||
error);
|
||||
break;
|
||||
}
|
||||
|
||||
if (status == WAIT_OBJECT_0)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_DEBUG, "alsa->stopEvent requests termination");
|
||||
break;
|
||||
}
|
||||
|
||||
snd_pcm_sframes_t framesRead = snd_pcm_readi(capture_handle, buffer, frames);
|
||||
|
||||
if (framesRead == 0)
|
||||
continue;
|
||||
|
||||
if (framesRead == -EPIPE)
|
||||
{
|
||||
const int res = snd_pcm_recover(capture_handle, (int)framesRead, 0);
|
||||
if (res < 0)
|
||||
WLog_Print(alsa->log, WLOG_WARN, "snd_pcm_recover (%s)", snd_strerror(res));
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (framesRead < 0)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "snd_pcm_readi (%s)", snd_strerror((int)framesRead));
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
error = alsa->receive(&alsa->aformat, buffer,
|
||||
WINPR_ASSERTING_INT_CAST(size_t, framesRead) * alsa->bytes_per_frame,
|
||||
alsa->user_data);
|
||||
|
||||
if (error)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR,
|
||||
"audin_alsa_thread_receive failed with error %" PRIu32, error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
|
||||
if (capture_handle)
|
||||
{
|
||||
const int res = snd_pcm_close(capture_handle);
|
||||
if (res < 0)
|
||||
WLog_Print(alsa->log, WLOG_WARN, "snd_pcm_close (%s)", snd_strerror(res));
|
||||
}
|
||||
|
||||
out:
|
||||
WLog_Print(alsa->log, WLOG_DEBUG, "out");
|
||||
|
||||
if (error && alsa->rdpcontext)
|
||||
setChannelError(alsa->rdpcontext, error, "audin_alsa_thread_func reported an error");
|
||||
|
||||
ExitThread(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_alsa_free(IAudinDevice* device)
|
||||
{
|
||||
AudinALSADevice* alsa = (AudinALSADevice*)device;
|
||||
|
||||
if (alsa)
|
||||
free(alsa->device_name);
|
||||
|
||||
free(alsa);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static BOOL audin_alsa_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
|
||||
{
|
||||
if (!device || !format)
|
||||
return FALSE;
|
||||
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) &&
|
||||
(format->wBitsPerSample == 8 || format->wBitsPerSample == 16) &&
|
||||
(format->nChannels == 1 || format->nChannels == 2))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_alsa_set_format(IAudinDevice* device, const AUDIO_FORMAT* format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinALSADevice* alsa = (AudinALSADevice*)device;
|
||||
|
||||
if (!alsa || !format)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
alsa->aformat = *format;
|
||||
alsa->frames_per_packet = FramesPerPacket;
|
||||
|
||||
if (audin_alsa_format(format->wFormatTag, format->wBitsPerSample) == SND_PCM_FORMAT_UNKNOWN)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_alsa_open(IAudinDevice* device, AudinReceive receive, void* user_data)
|
||||
{
|
||||
AudinALSADevice* alsa = (AudinALSADevice*)device;
|
||||
|
||||
if (!device || !receive || !user_data)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
alsa->receive = receive;
|
||||
alsa->user_data = user_data;
|
||||
|
||||
if (!(alsa->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr)))
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "CreateEvent failed!");
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if (!(alsa->thread = CreateThread(nullptr, 0, audin_alsa_thread_func, alsa, 0, nullptr)))
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "CreateThread failed!");
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
(void)CloseHandle(alsa->stopEvent);
|
||||
alsa->stopEvent = nullptr;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_alsa_close(IAudinDevice* device)
|
||||
{
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
AudinALSADevice* alsa = (AudinALSADevice*)device;
|
||||
|
||||
if (!alsa)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (alsa->stopEvent)
|
||||
{
|
||||
(void)SetEvent(alsa->stopEvent);
|
||||
|
||||
if (WaitForSingleObject(alsa->thread, INFINITE) == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "",
|
||||
error);
|
||||
return error;
|
||||
}
|
||||
|
||||
(void)CloseHandle(alsa->stopEvent);
|
||||
alsa->stopEvent = nullptr;
|
||||
(void)CloseHandle(alsa->thread);
|
||||
alsa->thread = nullptr;
|
||||
}
|
||||
|
||||
alsa->receive = nullptr;
|
||||
alsa->user_data = nullptr;
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_alsa_parse_addin_args(AudinALSADevice* device, const ADDIN_ARGV* args)
|
||||
{
|
||||
int status = 0;
|
||||
DWORD flags = 0;
|
||||
const COMMAND_LINE_ARGUMENT_A* arg = nullptr;
|
||||
AudinALSADevice* alsa = device;
|
||||
COMMAND_LINE_ARGUMENT_A audin_alsa_args[] = {
|
||||
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
|
||||
"audio device name" },
|
||||
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
|
||||
};
|
||||
flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
status = CommandLineParseArgumentsA(args->argc, args->argv, audin_alsa_args, flags, alsa,
|
||||
nullptr, nullptr);
|
||||
|
||||
if (status < 0)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
arg = audin_alsa_args;
|
||||
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
|
||||
{
|
||||
alsa->device_name = _strdup(arg->Value);
|
||||
|
||||
if (!alsa->device_name)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
}
|
||||
CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE alsa_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
const ADDIN_ARGV* args = nullptr;
|
||||
AudinALSADevice* alsa = nullptr;
|
||||
UINT error = 0;
|
||||
alsa = (AudinALSADevice*)calloc(1, sizeof(AudinALSADevice));
|
||||
|
||||
if (!alsa)
|
||||
{
|
||||
WLog_ERR(TAG, "calloc failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
alsa->log = WLog_Get(TAG);
|
||||
alsa->iface.Open = audin_alsa_open;
|
||||
alsa->iface.FormatSupported = audin_alsa_format_supported;
|
||||
alsa->iface.SetFormat = audin_alsa_set_format;
|
||||
alsa->iface.Close = audin_alsa_close;
|
||||
alsa->iface.Free = audin_alsa_free;
|
||||
alsa->rdpcontext = pEntryPoints->rdpcontext;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if ((error = audin_alsa_parse_addin_args(alsa, args)))
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR,
|
||||
"audin_alsa_parse_addin_args failed with errorcode %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if (!alsa->device_name)
|
||||
{
|
||||
alsa->device_name = _strdup("default");
|
||||
|
||||
if (!alsa->device_name)
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "_strdup failed!");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto error_out;
|
||||
}
|
||||
}
|
||||
|
||||
alsa->frames_per_packet = 128;
|
||||
alsa->aformat.nChannels = 2;
|
||||
alsa->aformat.wBitsPerSample = 16;
|
||||
alsa->aformat.wFormatTag = WAVE_FORMAT_PCM;
|
||||
alsa->aformat.nSamplesPerSec = 44100;
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)alsa)))
|
||||
{
|
||||
WLog_Print(alsa->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!",
|
||||
error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
free(alsa->device_name);
|
||||
free(alsa);
|
||||
return error;
|
||||
}
|
||||
1124
third_party/FreeRDP/channels/audin/client/audin_main.c
vendored
Normal file
1124
third_party/FreeRDP/channels/audin/client/audin_main.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
33
third_party/FreeRDP/channels/audin/client/audin_main.h
vendored
Normal file
33
third_party/FreeRDP/channels/audin/client/audin_main.h
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel
|
||||
*
|
||||
* Copyright 2010-2011 Vic Lee
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H
|
||||
#define FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <freerdp/dvc.h>
|
||||
#include <freerdp/types.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/log.h>
|
||||
#include <freerdp/client/audin.h>
|
||||
|
||||
#define TAG CHANNELS_TAG("audin.client")
|
||||
|
||||
#endif /* FREERDP_CHANNEL_AUDIN_CLIENT_MAIN_H */
|
||||
31
third_party/FreeRDP/channels/audin/client/ios/CMakeLists.txt
vendored
Normal file
31
third_party/FreeRDP/channels/audin/client/ios/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright (c) 2015 Armin Novak <armin.novak@thincast.com>
|
||||
# Copyright (c) 2015 Thincast Technologies GmbH
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "ios" "")
|
||||
find_library(CORE_AUDIO CoreAudio)
|
||||
find_library(AVFOUNDATION AVFoundation)
|
||||
find_library(AUDIO_TOOL AudioToolbox)
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_ios.m)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${AVFOUNDATION} ${CORE_AUDIO} ${AUDIO_TOOL})
|
||||
|
||||
include_directories(..)
|
||||
include_directories(SYSTEM ${MAC_INCLUDE_DIRS})
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
335
third_party/FreeRDP/channels/audin/client/ios/audin_ios.m
vendored
Normal file
335
third_party/FreeRDP/channels/audin/client/ios/audin_ios.m
vendored
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - iOS implementation
|
||||
*
|
||||
* Copyright (c) 2015 Armin Novak <armin.novak@thincast.com>
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/synch.h>
|
||||
#include <winpr/string.h>
|
||||
#include <winpr/thread.h>
|
||||
#include <winpr/debug.h>
|
||||
#include <winpr/cmdline.h>
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#define __COREFOUNDATION_CFPLUGINCOM__ 1
|
||||
#define IUNKNOWN_C_GUTS \
|
||||
void *_reserved; \
|
||||
void *QueryInterface; \
|
||||
void *AddRef; \
|
||||
void *Release
|
||||
|
||||
#include <CoreAudio/CoreAudioTypes.h>
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <AudioToolbox/AudioQueue.h>
|
||||
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/rdpsnd.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
#define IOS_AUDIO_QUEUE_NUM_BUFFERS 100
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
AUDIO_FORMAT format;
|
||||
UINT32 FramesPerPacket;
|
||||
int dev_unit;
|
||||
|
||||
AudinReceive receive;
|
||||
void *user_data;
|
||||
|
||||
rdpContext *rdpcontext;
|
||||
|
||||
bool isOpen;
|
||||
AudioQueueRef audioQueue;
|
||||
AudioStreamBasicDescription audioFormat;
|
||||
AudioQueueBufferRef audioBuffers[IOS_AUDIO_QUEUE_NUM_BUFFERS];
|
||||
} AudinIosDevice;
|
||||
|
||||
static AudioFormatID audin_ios_get_format(const AUDIO_FORMAT *format)
|
||||
{
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
return kAudioFormatLinearPCM;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static AudioFormatFlags audin_ios_get_flags_for_format(const AUDIO_FORMAT *format)
|
||||
{
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
return kAudioFormatFlagIsSignedInteger;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL audin_ios_format_supported(IAudinDevice *device, const AUDIO_FORMAT *format)
|
||||
{
|
||||
AudinIosDevice *ios = (AudinIosDevice *)device;
|
||||
AudioFormatID req_fmt = 0;
|
||||
|
||||
if (device == nullptr || format == nullptr)
|
||||
return FALSE;
|
||||
|
||||
req_fmt = audin_ios_get_format(format);
|
||||
|
||||
if (req_fmt == 0)
|
||||
return FALSE;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_ios_set_format(IAudinDevice *device, const AUDIO_FORMAT *format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinIosDevice *ios = (AudinIosDevice *)device;
|
||||
|
||||
if (device == nullptr || format == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
ios->FramesPerPacket = FramesPerPacket;
|
||||
ios->format = *format;
|
||||
WLog_INFO(TAG, "Audio Format %s [channels=%d, samples=%d, bits=%d]",
|
||||
audio_format_get_tag_string(format->wFormatTag), format->nChannels,
|
||||
format->nSamplesPerSec, format->wBitsPerSample);
|
||||
ios->audioFormat.mBitsPerChannel = format->wBitsPerSample;
|
||||
|
||||
if (format->wBitsPerSample == 0)
|
||||
ios->audioFormat.mBitsPerChannel = 16;
|
||||
|
||||
ios->audioFormat.mChannelsPerFrame = ios->format.nChannels;
|
||||
ios->audioFormat.mFramesPerPacket = 1;
|
||||
|
||||
ios->audioFormat.mBytesPerFrame =
|
||||
ios->audioFormat.mChannelsPerFrame * (ios->audioFormat.mBitsPerChannel / 8);
|
||||
ios->audioFormat.mBytesPerPacket =
|
||||
ios->audioFormat.mBytesPerFrame * ios->audioFormat.mFramesPerPacket;
|
||||
|
||||
ios->audioFormat.mFormatFlags = audin_ios_get_flags_for_format(format);
|
||||
ios->audioFormat.mFormatID = audin_ios_get_format(format);
|
||||
ios->audioFormat.mReserved = 0;
|
||||
ios->audioFormat.mSampleRate = ios->format.nSamplesPerSec;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static void ios_audio_queue_input_cb(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer,
|
||||
const AudioTimeStamp *inStartTime, UInt32 inNumPackets,
|
||||
const AudioStreamPacketDescription *inPacketDesc)
|
||||
{
|
||||
AudinIosDevice *ios = (AudinIosDevice *)aqData;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
const BYTE *buffer = inBuffer->mAudioData;
|
||||
int buffer_size = inBuffer->mAudioDataByteSize;
|
||||
(void)inAQ;
|
||||
(void)inStartTime;
|
||||
(void)inNumPackets;
|
||||
(void)inPacketDesc;
|
||||
|
||||
if (buffer_size > 0)
|
||||
error = ios->receive(&ios->format, buffer, buffer_size, ios->user_data);
|
||||
|
||||
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nullptr);
|
||||
|
||||
if (error)
|
||||
{
|
||||
WLog_ERR(TAG, "ios->receive failed with error %" PRIu32 "", error);
|
||||
SetLastError(ERROR_INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static UINT audin_ios_close(IAudinDevice *device)
|
||||
{
|
||||
UINT errCode = CHANNEL_RC_OK;
|
||||
char errString[1024];
|
||||
OSStatus devStat;
|
||||
AudinIosDevice *ios = (AudinIosDevice *)device;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (ios->isOpen)
|
||||
{
|
||||
devStat = AudioQueueStop(ios->audioQueue, true);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueStop failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
}
|
||||
|
||||
ios->isOpen = false;
|
||||
}
|
||||
|
||||
if (ios->audioQueue)
|
||||
{
|
||||
devStat = AudioQueueDispose(ios->audioQueue, true);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueDispose failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
}
|
||||
|
||||
ios->audioQueue = nullptr;
|
||||
}
|
||||
|
||||
ios->receive = nullptr;
|
||||
ios->user_data = nullptr;
|
||||
return errCode;
|
||||
}
|
||||
|
||||
static UINT audin_ios_open(IAudinDevice *device, AudinReceive receive, void *user_data)
|
||||
{
|
||||
AudinIosDevice *ios = (AudinIosDevice *)device;
|
||||
DWORD errCode;
|
||||
char errString[1024];
|
||||
OSStatus devStat;
|
||||
|
||||
ios->receive = receive;
|
||||
ios->user_data = user_data;
|
||||
devStat = AudioQueueNewInput(&(ios->audioFormat), ios_audio_queue_input_cb, ios, nullptr,
|
||||
kCFRunLoopCommonModes, 0, &(ios->audioQueue));
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueNewInput failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < IOS_AUDIO_QUEUE_NUM_BUFFERS; index++)
|
||||
{
|
||||
devStat = AudioQueueAllocateBuffer(ios->audioQueue,
|
||||
ios->FramesPerPacket * 2 * ios->format.nChannels,
|
||||
&ios->audioBuffers[index]);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueAllocateBuffer failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
devStat = AudioQueueEnqueueBuffer(ios->audioQueue, ios->audioBuffers[index], 0, nullptr);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueEnqueueBuffer failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
}
|
||||
|
||||
devStat = AudioQueueStart(ios->audioQueue, nullptr);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueStart failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
ios->isOpen = true;
|
||||
return CHANNEL_RC_OK;
|
||||
err_out:
|
||||
audin_ios_close(device);
|
||||
return CHANNEL_RC_INITIALIZATION_ERROR;
|
||||
}
|
||||
|
||||
static UINT audin_ios_free(IAudinDevice *device)
|
||||
{
|
||||
AudinIosDevice *ios = (AudinIosDevice *)device;
|
||||
int error;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if ((error = audin_ios_close(device)))
|
||||
{
|
||||
WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error);
|
||||
}
|
||||
|
||||
free(ios);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE ios_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
DWORD errCode;
|
||||
char errString[1024];
|
||||
const ADDIN_ARGV *args;
|
||||
AudinIosDevice *ios;
|
||||
UINT error;
|
||||
ios = (AudinIosDevice *)calloc(1, sizeof(AudinIosDevice));
|
||||
|
||||
if (!ios)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "calloc failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
ios->iface.Open = audin_ios_open;
|
||||
ios->iface.FormatSupported = audin_ios_format_supported;
|
||||
ios->iface.SetFormat = audin_ios_set_format;
|
||||
ios->iface.Close = audin_ios_close;
|
||||
ios->iface.Free = audin_ios_free;
|
||||
ios->rdpcontext = pEntryPoints->rdpcontext;
|
||||
ios->dev_unit = -1;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice *)ios)))
|
||||
{
|
||||
WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
free(ios);
|
||||
return error;
|
||||
}
|
||||
32
third_party/FreeRDP/channels/audin/client/mac/CMakeLists.txt
vendored
Normal file
32
third_party/FreeRDP/channels/audin/client/mac/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright (c) 2015 Armin Novak <armin.novak@thincast.com>
|
||||
# Copyright (c) 2015 Thincast Technologies GmbH
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "mac" "")
|
||||
find_library(CORE_AUDIO CoreAudio)
|
||||
find_library(AVFOUNDATION AVFoundation)
|
||||
find_library(AUDIO_TOOL AudioToolbox)
|
||||
find_library(APP_SERVICES ApplicationServices)
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_mac.m)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${AVFOUNDATION} ${CORE_AUDIO} ${AUDIO_TOOL} ${APP_SERVICES})
|
||||
|
||||
include_directories(..)
|
||||
include_directories(SYSTEM ${MAC_INCLUDE_DIRS})
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
465
third_party/FreeRDP/channels/audin/client/mac/audin_mac.m
vendored
Normal file
465
third_party/FreeRDP/channels/audin/client/mac/audin_mac.m
vendored
Normal file
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - Mac OS X implementation
|
||||
*
|
||||
* Copyright (c) 2015 Armin Novak <armin.novak@thincast.com>
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/synch.h>
|
||||
#include <winpr/string.h>
|
||||
#include <winpr/thread.h>
|
||||
#include <winpr/debug.h>
|
||||
#include <winpr/cmdline.h>
|
||||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#define __COREFOUNDATION_CFPLUGINCOM__ 1
|
||||
#define IUNKNOWN_C_GUTS \
|
||||
void *_reserved; \
|
||||
void *QueryInterface; \
|
||||
void *AddRef; \
|
||||
void *Release
|
||||
|
||||
#include <CoreAudio/CoreAudioTypes.h>
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <AudioToolbox/AudioQueue.h>
|
||||
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/rdpsnd.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
#define MAC_AUDIO_QUEUE_NUM_BUFFERS 100
|
||||
|
||||
/* Fix for #4462: Provide type alias if not declared (Mac OS < 10.10)
|
||||
* https://developer.apple.com/documentation/coreaudio/audioformatid
|
||||
*/
|
||||
#ifndef AudioFormatID
|
||||
typedef UInt32 AudioFormatID;
|
||||
#endif
|
||||
|
||||
#ifndef AudioFormatFlags
|
||||
typedef UInt32 AudioFormatFlags;
|
||||
#endif
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
AUDIO_FORMAT format;
|
||||
UINT32 FramesPerPacket;
|
||||
int dev_unit;
|
||||
|
||||
AudinReceive receive;
|
||||
void *user_data;
|
||||
|
||||
rdpContext *rdpcontext;
|
||||
|
||||
bool isAuthorized;
|
||||
bool isOpen;
|
||||
AudioQueueRef audioQueue;
|
||||
AudioStreamBasicDescription audioFormat;
|
||||
AudioQueueBufferRef audioBuffers[MAC_AUDIO_QUEUE_NUM_BUFFERS];
|
||||
} AudinMacDevice;
|
||||
|
||||
static AudioFormatID audin_mac_get_format(const AUDIO_FORMAT *format)
|
||||
{
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
return kAudioFormatLinearPCM;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static AudioFormatFlags audin_mac_get_flags_for_format(const AUDIO_FORMAT *format)
|
||||
{
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
return kAudioFormatFlagIsSignedInteger;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static BOOL audin_mac_format_supported(IAudinDevice *device, const AUDIO_FORMAT *format)
|
||||
{
|
||||
AudinMacDevice *mac = (AudinMacDevice *)device;
|
||||
AudioFormatID req_fmt = 0;
|
||||
|
||||
if (!mac->isAuthorized)
|
||||
return FALSE;
|
||||
|
||||
if (device == nullptr || format == nullptr)
|
||||
return FALSE;
|
||||
|
||||
if (format->nChannels != 2)
|
||||
return FALSE;
|
||||
|
||||
req_fmt = audin_mac_get_format(format);
|
||||
|
||||
if (req_fmt == 0)
|
||||
return FALSE;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_mac_set_format(IAudinDevice *device, const AUDIO_FORMAT *format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinMacDevice *mac = (AudinMacDevice *)device;
|
||||
|
||||
if (!mac->isAuthorized)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
if (device == nullptr || format == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
mac->FramesPerPacket = FramesPerPacket;
|
||||
mac->format = *format;
|
||||
WLog_INFO(TAG, "Audio Format %s [channels=%d, samples=%d, bits=%d]",
|
||||
audio_format_get_tag_string(format->wFormatTag), format->nChannels,
|
||||
format->nSamplesPerSec, format->wBitsPerSample);
|
||||
mac->audioFormat.mBitsPerChannel = format->wBitsPerSample;
|
||||
|
||||
if (format->wBitsPerSample == 0)
|
||||
mac->audioFormat.mBitsPerChannel = 16;
|
||||
|
||||
mac->audioFormat.mChannelsPerFrame = mac->format.nChannels;
|
||||
mac->audioFormat.mFramesPerPacket = 1;
|
||||
|
||||
mac->audioFormat.mBytesPerFrame =
|
||||
mac->audioFormat.mChannelsPerFrame * (mac->audioFormat.mBitsPerChannel / 8);
|
||||
mac->audioFormat.mBytesPerPacket =
|
||||
mac->audioFormat.mBytesPerFrame * mac->audioFormat.mFramesPerPacket;
|
||||
|
||||
mac->audioFormat.mFormatFlags = audin_mac_get_flags_for_format(format);
|
||||
mac->audioFormat.mFormatID = audin_mac_get_format(format);
|
||||
mac->audioFormat.mReserved = 0;
|
||||
mac->audioFormat.mSampleRate = mac->format.nSamplesPerSec;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static void mac_audio_queue_input_cb(void *aqData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer,
|
||||
const AudioTimeStamp *inStartTime, UInt32 inNumPackets,
|
||||
const AudioStreamPacketDescription *inPacketDesc)
|
||||
{
|
||||
AudinMacDevice *mac = (AudinMacDevice *)aqData;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
const BYTE *buffer = inBuffer->mAudioData;
|
||||
int buffer_size = inBuffer->mAudioDataByteSize;
|
||||
(void)inAQ;
|
||||
(void)inStartTime;
|
||||
(void)inNumPackets;
|
||||
(void)inPacketDesc;
|
||||
|
||||
if (buffer_size > 0)
|
||||
error = mac->receive(&mac->format, buffer, buffer_size, mac->user_data);
|
||||
|
||||
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nullptr);
|
||||
|
||||
if (error)
|
||||
{
|
||||
WLog_ERR(TAG, "mac->receive failed with error %" PRIu32 "", error);
|
||||
SetLastError(ERROR_INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static UINT audin_mac_close(IAudinDevice *device)
|
||||
{
|
||||
UINT errCode = CHANNEL_RC_OK;
|
||||
char errString[1024];
|
||||
OSStatus devStat;
|
||||
AudinMacDevice *mac = (AudinMacDevice *)device;
|
||||
|
||||
if (!mac->isAuthorized)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (mac->isOpen)
|
||||
{
|
||||
devStat = AudioQueueStop(mac->audioQueue, true);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueStop failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
}
|
||||
|
||||
mac->isOpen = false;
|
||||
}
|
||||
|
||||
if (mac->audioQueue)
|
||||
{
|
||||
devStat = AudioQueueDispose(mac->audioQueue, true);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueDispose failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
}
|
||||
|
||||
mac->audioQueue = nullptr;
|
||||
}
|
||||
|
||||
mac->receive = nullptr;
|
||||
mac->user_data = nullptr;
|
||||
return errCode;
|
||||
}
|
||||
|
||||
static UINT audin_mac_open(IAudinDevice *device, AudinReceive receive, void *user_data)
|
||||
{
|
||||
AudinMacDevice *mac = (AudinMacDevice *)device;
|
||||
DWORD errCode;
|
||||
char errString[1024];
|
||||
OSStatus devStat;
|
||||
|
||||
if (!mac->isAuthorized)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
mac->receive = receive;
|
||||
mac->user_data = user_data;
|
||||
devStat = AudioQueueNewInput(&(mac->audioFormat), mac_audio_queue_input_cb, mac, nullptr,
|
||||
kCFRunLoopCommonModes, 0, &(mac->audioQueue));
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueNewInput failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
for (size_t index = 0; index < MAC_AUDIO_QUEUE_NUM_BUFFERS; index++)
|
||||
{
|
||||
devStat = AudioQueueAllocateBuffer(mac->audioQueue,
|
||||
mac->FramesPerPacket * 2 * mac->format.nChannels,
|
||||
&mac->audioBuffers[index]);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueAllocateBuffer failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
devStat = AudioQueueEnqueueBuffer(mac->audioQueue, mac->audioBuffers[index], 0, nullptr);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueEnqueueBuffer failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
}
|
||||
|
||||
devStat = AudioQueueStart(mac->audioQueue, nullptr);
|
||||
|
||||
if (devStat != 0)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "AudioQueueStart failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
mac->isOpen = true;
|
||||
return CHANNEL_RC_OK;
|
||||
err_out:
|
||||
audin_mac_close(device);
|
||||
return CHANNEL_RC_INITIALIZATION_ERROR;
|
||||
}
|
||||
|
||||
static UINT audin_mac_free(IAudinDevice *device)
|
||||
{
|
||||
AudinMacDevice *mac = (AudinMacDevice *)device;
|
||||
int error;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if ((error = audin_mac_close(device)))
|
||||
{
|
||||
WLog_ERR(TAG, "audin_oss_close failed with error code %d!", error);
|
||||
}
|
||||
|
||||
free(mac);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static UINT audin_mac_parse_addin_args(AudinMacDevice *device, const ADDIN_ARGV *args)
|
||||
{
|
||||
DWORD errCode;
|
||||
char errString[1024];
|
||||
int status;
|
||||
char *str_num, *eptr;
|
||||
DWORD flags;
|
||||
const COMMAND_LINE_ARGUMENT_A *arg;
|
||||
COMMAND_LINE_ARGUMENT_A audin_mac_args[] = {
|
||||
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
|
||||
"audio device name" },
|
||||
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
|
||||
};
|
||||
|
||||
AudinMacDevice *mac = (AudinMacDevice *)device;
|
||||
|
||||
if (args->argc == 1)
|
||||
return CHANNEL_RC_OK;
|
||||
|
||||
flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
status = CommandLineParseArgumentsA(args->argc, args->argv, audin_mac_args, flags, mac, nullptr,
|
||||
nullptr);
|
||||
|
||||
if (status < 0)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
arg = audin_mac_args;
|
||||
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
|
||||
{
|
||||
str_num = _strdup(arg->Value);
|
||||
|
||||
if (!str_num)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "_strdup failed with %s [%d]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
mac->dev_unit = strtol(str_num, &eptr, 10);
|
||||
|
||||
if (mac->dev_unit < 0 || *eptr != '\0')
|
||||
mac->dev_unit = -1;
|
||||
|
||||
free(str_num);
|
||||
}
|
||||
CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE mac_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
DWORD errCode;
|
||||
char errString[1024];
|
||||
const ADDIN_ARGV *args;
|
||||
AudinMacDevice *mac;
|
||||
UINT error;
|
||||
mac = (AudinMacDevice *)calloc(1, sizeof(AudinMacDevice));
|
||||
|
||||
if (!mac)
|
||||
{
|
||||
errCode = GetLastError();
|
||||
WLog_ERR(TAG, "calloc failed with %s [%" PRIu32 "]",
|
||||
winpr_strerror(errCode, errString, sizeof(errString)), errCode);
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
mac->iface.Open = audin_mac_open;
|
||||
mac->iface.FormatSupported = audin_mac_format_supported;
|
||||
mac->iface.SetFormat = audin_mac_set_format;
|
||||
mac->iface.Close = audin_mac_close;
|
||||
mac->iface.Free = audin_mac_free;
|
||||
mac->rdpcontext = pEntryPoints->rdpcontext;
|
||||
mac->dev_unit = -1;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if ((error = audin_mac_parse_addin_args(mac, args)))
|
||||
{
|
||||
WLog_ERR(TAG, "audin_mac_parse_addin_args failed with %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice *)mac)))
|
||||
{
|
||||
WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
#if defined(MAC_OS_X_VERSION_10_14)
|
||||
if (@available(macOS 10.14, *))
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
AVAuthorizationStatus status =
|
||||
[AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
|
||||
switch (status)
|
||||
{
|
||||
case AVAuthorizationStatusAuthorized:
|
||||
mac->isAuthorized = TRUE;
|
||||
break;
|
||||
case AVAuthorizationStatusNotDetermined:
|
||||
[AVCaptureDevice
|
||||
requestAccessForMediaType:AVMediaTypeAudio
|
||||
completionHandler:^(BOOL granted) {
|
||||
if (granted == YES)
|
||||
{
|
||||
mac->isAuthorized = TRUE;
|
||||
}
|
||||
else
|
||||
WLog_WARN(TAG, "Microphone access denied by user");
|
||||
}];
|
||||
break;
|
||||
case AVAuthorizationStatusRestricted:
|
||||
WLog_WARN(TAG, "Microphone access restricted by policy");
|
||||
break;
|
||||
case AVAuthorizationStatusDenied:
|
||||
WLog_WARN(TAG, "Microphone access denied by policy");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
free(mac);
|
||||
return error;
|
||||
}
|
||||
29
third_party/FreeRDP/channels/audin/client/opensles/CMakeLists.txt
vendored
Normal file
29
third_party/FreeRDP/channels/audin/client/opensles/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2013 Armin Novak <armin.novak@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "opensles" "")
|
||||
|
||||
find_package(OpenSLES REQUIRED)
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS opensl_io.c audin_opensl_es.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${OpenSLES_LIBRARIES})
|
||||
|
||||
include_directories(..)
|
||||
include_directories(SYSTEM ${OpenSLES_INCLUDE_DIRS})
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
334
third_party/FreeRDP/channels/audin/client/opensles/audin_opensl_es.c
vendored
Normal file
334
third_party/FreeRDP/channels/audin/client/opensles/audin_opensl_es.c
vendored
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - OpenSL ES implementation
|
||||
*
|
||||
* Copyright 2013 Armin Novak <armin.novak@gmail.com>
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <winpr/assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/cmdline.h>
|
||||
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/rdpsnd.h>
|
||||
|
||||
#include <SLES/OpenSLES.h>
|
||||
#include <freerdp/client/audin.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
#include "opensl_io.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
char* device_name;
|
||||
OPENSL_STREAM* stream;
|
||||
|
||||
AUDIO_FORMAT format;
|
||||
UINT32 frames_per_packet;
|
||||
|
||||
UINT32 bytes_per_channel;
|
||||
|
||||
AudinReceive receive;
|
||||
|
||||
void* user_data;
|
||||
|
||||
rdpContext* rdpcontext;
|
||||
wLog* log;
|
||||
} AudinOpenSLESDevice;
|
||||
|
||||
static UINT audin_opensles_close(IAudinDevice* device);
|
||||
|
||||
static void audin_receive(void* context, const void* data, size_t size)
|
||||
{
|
||||
UINT error;
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)context;
|
||||
|
||||
if (!opensles || !data)
|
||||
{
|
||||
WLog_ERR(TAG, "Invalid arguments context=%p, data=%p", opensles, data);
|
||||
return;
|
||||
}
|
||||
|
||||
error = opensles->receive(&opensles->format, data, size, opensles->user_data);
|
||||
|
||||
if (error && opensles->rdpcontext)
|
||||
setChannelError(opensles->rdpcontext, error, "audin_receive reported an error");
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_opensles_free(IAudinDevice* device)
|
||||
{
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device;
|
||||
|
||||
if (!opensles)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "device=%p", (void*)device);
|
||||
|
||||
free(opensles->device_name);
|
||||
free(opensles);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static BOOL audin_opensles_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
|
||||
{
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device;
|
||||
|
||||
if (!opensles || !format)
|
||||
return FALSE;
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, format=%p", (void*)opensles, (void*)format);
|
||||
WINPR_ASSERT(format);
|
||||
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM: /* PCM */
|
||||
if (format->cbSize == 0 && (format->nSamplesPerSec <= 48000) &&
|
||||
(format->wBitsPerSample == 8 || format->wBitsPerSample == 16) &&
|
||||
(format->nChannels >= 1 && format->nChannels <= 2))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "Encoding '%s' [0x%04" PRIX16 "] not supported",
|
||||
audio_format_get_tag_string(format->wFormatTag), format->wFormatTag);
|
||||
break;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_opensles_set_format(IAudinDevice* device, const AUDIO_FORMAT* format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device;
|
||||
|
||||
if (!opensles || !format)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, format=%p, FramesPerPacket=%" PRIu32 "",
|
||||
(void*)device, (void*)format, FramesPerPacket);
|
||||
WINPR_ASSERT(format);
|
||||
|
||||
opensles->format = *format;
|
||||
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
opensles->frames_per_packet = FramesPerPacket;
|
||||
|
||||
switch (format->wBitsPerSample)
|
||||
{
|
||||
case 4:
|
||||
opensles->bytes_per_channel = 1;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
opensles->bytes_per_channel = 1;
|
||||
break;
|
||||
|
||||
case 16:
|
||||
opensles->bytes_per_channel = 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
return ERROR_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
WLog_Print(opensles->log, WLOG_ERROR,
|
||||
"Encoding '%" PRIu16 "' [%04" PRIX16 "] not supported", format->wFormatTag,
|
||||
format->wFormatTag);
|
||||
return ERROR_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "frames_per_packet=%" PRIu32,
|
||||
opensles->frames_per_packet);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_opensles_open(IAudinDevice* device, AudinReceive receive, void* user_data)
|
||||
{
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device;
|
||||
|
||||
if (!opensles || !receive || !user_data)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, receive=%p, user_data=%p", (void*)device,
|
||||
(void*)receive, (void*)user_data);
|
||||
|
||||
if (opensles->stream)
|
||||
goto error_out;
|
||||
|
||||
if (!(opensles->stream = android_OpenRecDevice(
|
||||
opensles, audin_receive, opensles->format.nSamplesPerSec, opensles->format.nChannels,
|
||||
opensles->frames_per_packet, opensles->format.wBitsPerSample)))
|
||||
{
|
||||
WLog_Print(opensles->log, WLOG_ERROR, "android_OpenRecDevice failed!");
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
opensles->receive = receive;
|
||||
opensles->user_data = user_data;
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
audin_opensles_close(device);
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
UINT audin_opensles_close(IAudinDevice* device)
|
||||
{
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device;
|
||||
|
||||
if (!opensles)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "device=%p", (void*)device);
|
||||
android_CloseRecDevice(opensles->stream);
|
||||
opensles->receive = nullptr;
|
||||
opensles->user_data = nullptr;
|
||||
opensles->stream = nullptr;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_opensles_parse_addin_args(AudinOpenSLESDevice* device, const ADDIN_ARGV* args)
|
||||
{
|
||||
UINT status;
|
||||
DWORD flags;
|
||||
const COMMAND_LINE_ARGUMENT_A* arg;
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)device;
|
||||
COMMAND_LINE_ARGUMENT_A audin_opensles_args[] = {
|
||||
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
|
||||
"audio device name" },
|
||||
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
|
||||
};
|
||||
|
||||
WLog_Print(opensles->log, WLOG_DEBUG, "device=%p, args=%p", (void*)device, (void*)args);
|
||||
flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
status = CommandLineParseArgumentsA(args->argc, args->argv, audin_opensles_args, flags,
|
||||
opensles, nullptr, nullptr);
|
||||
|
||||
if (status < 0)
|
||||
return status;
|
||||
|
||||
arg = audin_opensles_args;
|
||||
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
|
||||
{
|
||||
opensles->device_name = _strdup(arg->Value);
|
||||
|
||||
if (!opensles->device_name)
|
||||
{
|
||||
WLog_Print(opensles->log, WLOG_ERROR, "_strdup failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
}
|
||||
CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE opensles_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
UINT error = ERROR_INTERNAL_ERROR;
|
||||
AudinOpenSLESDevice* opensles = (AudinOpenSLESDevice*)calloc(1, sizeof(AudinOpenSLESDevice));
|
||||
|
||||
if (!opensles)
|
||||
{
|
||||
WLog_ERR(TAG, "calloc failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
opensles->log = WLog_Get(TAG);
|
||||
opensles->iface.Open = audin_opensles_open;
|
||||
opensles->iface.FormatSupported = audin_opensles_format_supported;
|
||||
opensles->iface.SetFormat = audin_opensles_set_format;
|
||||
opensles->iface.Close = audin_opensles_close;
|
||||
opensles->iface.Free = audin_opensles_free;
|
||||
opensles->rdpcontext = pEntryPoints->rdpcontext;
|
||||
const ADDIN_ARGV* args = pEntryPoints->args;
|
||||
|
||||
if ((error = audin_opensles_parse_addin_args(opensles, args)))
|
||||
{
|
||||
WLog_Print(opensles->log, WLOG_ERROR,
|
||||
"audin_opensles_parse_addin_args failed with errorcode %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)opensles)))
|
||||
{
|
||||
WLog_Print(opensles->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!",
|
||||
error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
free(opensles);
|
||||
return error;
|
||||
}
|
||||
388
third_party/FreeRDP/channels/audin/client/opensles/opensl_io.c
vendored
Normal file
388
third_party/FreeRDP/channels/audin/client/opensles/opensl_io.c
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
opensl_io.c:
|
||||
Android OpenSL input/output module
|
||||
Copyright (c) 2012, Victor Lazzarini
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <winpr/assert.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
#include "opensl_io.h"
|
||||
#define CONV16BIT 32768
|
||||
#define CONVMYFLT (1. / 32768.)
|
||||
|
||||
typedef struct
|
||||
{
|
||||
size_t size;
|
||||
void* data;
|
||||
} queue_element;
|
||||
|
||||
struct opensl_stream
|
||||
{
|
||||
// engine interfaces
|
||||
SLObjectItf engineObject;
|
||||
SLEngineItf engineEngine;
|
||||
|
||||
// device interfaces
|
||||
SLDeviceVolumeItf deviceVolume;
|
||||
|
||||
// recorder interfaces
|
||||
SLObjectItf recorderObject;
|
||||
SLRecordItf recorderRecord;
|
||||
SLAndroidSimpleBufferQueueItf recorderBufferQueue;
|
||||
|
||||
unsigned int inchannels;
|
||||
unsigned int sr;
|
||||
unsigned int buffersize;
|
||||
unsigned int bits_per_sample;
|
||||
|
||||
queue_element* prep;
|
||||
queue_element* next;
|
||||
|
||||
void* context;
|
||||
opensl_receive_t receive;
|
||||
};
|
||||
|
||||
static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void* context);
|
||||
|
||||
// creates the OpenSL ES audio engine
|
||||
static SLresult openSLCreateEngine(OPENSL_STREAM* p)
|
||||
{
|
||||
SLresult result;
|
||||
// create engine
|
||||
result = slCreateEngine(&(p->engineObject), 0, nullptr, 0, nullptr, nullptr);
|
||||
|
||||
if (result != SL_RESULT_SUCCESS)
|
||||
goto engine_end;
|
||||
|
||||
// realize the engine
|
||||
result = (*p->engineObject)->Realize(p->engineObject, SL_BOOLEAN_FALSE);
|
||||
|
||||
if (result != SL_RESULT_SUCCESS)
|
||||
goto engine_end;
|
||||
|
||||
// get the engine interface, which is needed in order to create other objects
|
||||
result = (*p->engineObject)->GetInterface(p->engineObject, SL_IID_ENGINE, &(p->engineEngine));
|
||||
|
||||
if (result != SL_RESULT_SUCCESS)
|
||||
goto engine_end;
|
||||
|
||||
// get the volume interface - important, this is optional!
|
||||
result =
|
||||
(*p->engineObject)->GetInterface(p->engineObject, SL_IID_DEVICEVOLUME, &(p->deviceVolume));
|
||||
|
||||
if (result != SL_RESULT_SUCCESS)
|
||||
{
|
||||
p->deviceVolume = nullptr;
|
||||
result = SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
engine_end:
|
||||
WINPR_ASSERT(SL_RESULT_SUCCESS == result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Open the OpenSL ES device for input
|
||||
static SLresult openSLRecOpen(OPENSL_STREAM* p)
|
||||
{
|
||||
SLresult result;
|
||||
SLuint32 sr = p->sr;
|
||||
SLuint32 channels = p->inchannels;
|
||||
WINPR_ASSERT(!p->recorderObject);
|
||||
|
||||
if (channels)
|
||||
{
|
||||
switch (sr)
|
||||
{
|
||||
case 8000:
|
||||
sr = SL_SAMPLINGRATE_8;
|
||||
break;
|
||||
|
||||
case 11025:
|
||||
sr = SL_SAMPLINGRATE_11_025;
|
||||
break;
|
||||
|
||||
case 16000:
|
||||
sr = SL_SAMPLINGRATE_16;
|
||||
break;
|
||||
|
||||
case 22050:
|
||||
sr = SL_SAMPLINGRATE_22_05;
|
||||
break;
|
||||
|
||||
case 24000:
|
||||
sr = SL_SAMPLINGRATE_24;
|
||||
break;
|
||||
|
||||
case 32000:
|
||||
sr = SL_SAMPLINGRATE_32;
|
||||
break;
|
||||
|
||||
case 44100:
|
||||
sr = SL_SAMPLINGRATE_44_1;
|
||||
break;
|
||||
|
||||
case 48000:
|
||||
sr = SL_SAMPLINGRATE_48;
|
||||
break;
|
||||
|
||||
case 64000:
|
||||
sr = SL_SAMPLINGRATE_64;
|
||||
break;
|
||||
|
||||
case 88200:
|
||||
sr = SL_SAMPLINGRATE_88_2;
|
||||
break;
|
||||
|
||||
case 96000:
|
||||
sr = SL_SAMPLINGRATE_96;
|
||||
break;
|
||||
|
||||
case 192000:
|
||||
sr = SL_SAMPLINGRATE_192;
|
||||
break;
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
// configure audio source
|
||||
SLDataLocator_IODevice loc_dev = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
|
||||
SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr };
|
||||
SLDataSource audioSrc = { &loc_dev, nullptr };
|
||||
// configure audio sink
|
||||
int speakers;
|
||||
|
||||
if (channels > 1)
|
||||
speakers = SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT;
|
||||
else
|
||||
speakers = SL_SPEAKER_FRONT_CENTER;
|
||||
|
||||
SLDataLocator_AndroidSimpleBufferQueue loc_bq = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
|
||||
2 };
|
||||
SLDataFormat_PCM format_pcm;
|
||||
format_pcm.formatType = SL_DATAFORMAT_PCM;
|
||||
format_pcm.numChannels = channels;
|
||||
format_pcm.samplesPerSec = sr;
|
||||
format_pcm.channelMask = speakers;
|
||||
format_pcm.endianness = SL_BYTEORDER_LITTLEENDIAN;
|
||||
|
||||
if (16 == p->bits_per_sample)
|
||||
{
|
||||
format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
|
||||
format_pcm.containerSize = 16;
|
||||
}
|
||||
else if (8 == p->bits_per_sample)
|
||||
{
|
||||
format_pcm.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_8;
|
||||
format_pcm.containerSize = 8;
|
||||
}
|
||||
else
|
||||
WINPR_ASSERT(0);
|
||||
|
||||
SLDataSink audioSnk = { &loc_bq, &format_pcm };
|
||||
// create audio recorder
|
||||
// (requires the RECORD_AUDIO permission)
|
||||
const SLInterfaceID id[] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE };
|
||||
const SLboolean req[] = { SL_BOOLEAN_TRUE };
|
||||
result = (*p->engineEngine)
|
||||
->CreateAudioRecorder(p->engineEngine, &(p->recorderObject), &audioSrc,
|
||||
&audioSnk, 1, id, req);
|
||||
WINPR_ASSERT(!result);
|
||||
|
||||
if (SL_RESULT_SUCCESS != result)
|
||||
goto end_recopen;
|
||||
|
||||
// realize the audio recorder
|
||||
result = (*p->recorderObject)->Realize(p->recorderObject, SL_BOOLEAN_FALSE);
|
||||
WINPR_ASSERT(!result);
|
||||
|
||||
if (SL_RESULT_SUCCESS != result)
|
||||
goto end_recopen;
|
||||
|
||||
// get the record interface
|
||||
result = (*p->recorderObject)
|
||||
->GetInterface(p->recorderObject, SL_IID_RECORD, &(p->recorderRecord));
|
||||
WINPR_ASSERT(!result);
|
||||
|
||||
if (SL_RESULT_SUCCESS != result)
|
||||
goto end_recopen;
|
||||
|
||||
// get the buffer queue interface
|
||||
result = (*p->recorderObject)
|
||||
->GetInterface(p->recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
|
||||
&(p->recorderBufferQueue));
|
||||
WINPR_ASSERT(!result);
|
||||
|
||||
if (SL_RESULT_SUCCESS != result)
|
||||
goto end_recopen;
|
||||
|
||||
// register callback on the buffer queue
|
||||
result = (*p->recorderBufferQueue)
|
||||
->RegisterCallback(p->recorderBufferQueue, bqRecorderCallback, p);
|
||||
WINPR_ASSERT(!result);
|
||||
|
||||
if (SL_RESULT_SUCCESS != result)
|
||||
goto end_recopen;
|
||||
|
||||
end_recopen:
|
||||
return result;
|
||||
}
|
||||
else
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
// close the OpenSL IO and destroy the audio engine
|
||||
static void openSLDestroyEngine(OPENSL_STREAM* p)
|
||||
{
|
||||
// destroy audio recorder object, and invalidate all associated interfaces
|
||||
if (p->recorderObject != nullptr)
|
||||
{
|
||||
(*p->recorderObject)->Destroy(p->recorderObject);
|
||||
p->recorderObject = nullptr;
|
||||
p->recorderRecord = nullptr;
|
||||
p->recorderBufferQueue = nullptr;
|
||||
}
|
||||
|
||||
// destroy engine object, and invalidate all associated interfaces
|
||||
if (p->engineObject != nullptr)
|
||||
{
|
||||
(*p->engineObject)->Destroy(p->engineObject);
|
||||
p->engineObject = nullptr;
|
||||
p->engineEngine = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static queue_element* opensles_queue_element_new(size_t size)
|
||||
{
|
||||
queue_element* q = calloc(1, sizeof(queue_element));
|
||||
|
||||
if (!q)
|
||||
goto fail;
|
||||
|
||||
q->size = size;
|
||||
q->data = malloc(size);
|
||||
|
||||
if (!q->data)
|
||||
goto fail;
|
||||
|
||||
return q;
|
||||
fail:
|
||||
free(q);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void opensles_queue_element_free(void* obj)
|
||||
{
|
||||
queue_element* e = (queue_element*)obj;
|
||||
|
||||
if (e)
|
||||
free(e->data);
|
||||
|
||||
free(e);
|
||||
}
|
||||
|
||||
// open the android audio device for input
|
||||
OPENSL_STREAM* android_OpenRecDevice(void* context, opensl_receive_t receive, int sr,
|
||||
int inchannels, int bufferframes, int bits_per_sample)
|
||||
{
|
||||
OPENSL_STREAM* p;
|
||||
|
||||
if (!context || !receive)
|
||||
return nullptr;
|
||||
|
||||
p = (OPENSL_STREAM*)calloc(1, sizeof(OPENSL_STREAM));
|
||||
|
||||
if (!p)
|
||||
return nullptr;
|
||||
|
||||
p->context = context;
|
||||
p->receive = receive;
|
||||
p->inchannels = inchannels;
|
||||
p->sr = sr;
|
||||
p->buffersize = bufferframes;
|
||||
p->bits_per_sample = bits_per_sample;
|
||||
|
||||
if ((p->bits_per_sample != 8) && (p->bits_per_sample != 16))
|
||||
goto fail;
|
||||
|
||||
if (openSLCreateEngine(p) != SL_RESULT_SUCCESS)
|
||||
goto fail;
|
||||
|
||||
if (openSLRecOpen(p) != SL_RESULT_SUCCESS)
|
||||
goto fail;
|
||||
|
||||
/* Create receive buffers, prepare them and start recording */
|
||||
p->prep = opensles_queue_element_new(p->buffersize * p->bits_per_sample / 8);
|
||||
p->next = opensles_queue_element_new(p->buffersize * p->bits_per_sample / 8);
|
||||
|
||||
if (!p->prep || !p->next)
|
||||
goto fail;
|
||||
|
||||
(*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, p->next->data, p->next->size);
|
||||
(*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, p->prep->data, p->prep->size);
|
||||
(*p->recorderRecord)->SetRecordState(p->recorderRecord, SL_RECORDSTATE_RECORDING);
|
||||
return p;
|
||||
fail:
|
||||
android_CloseRecDevice(p);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// close the android audio device
|
||||
void android_CloseRecDevice(OPENSL_STREAM* p)
|
||||
{
|
||||
if (p == nullptr)
|
||||
return;
|
||||
|
||||
opensles_queue_element_free(p->next);
|
||||
opensles_queue_element_free(p->prep);
|
||||
openSLDestroyEngine(p);
|
||||
free(p);
|
||||
}
|
||||
|
||||
// this callback handler is called every time a buffer finishes recording
|
||||
static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void* context)
|
||||
{
|
||||
OPENSL_STREAM* p = (OPENSL_STREAM*)context;
|
||||
queue_element* e;
|
||||
|
||||
if (!p)
|
||||
return;
|
||||
|
||||
e = p->next;
|
||||
|
||||
if (!e)
|
||||
return;
|
||||
|
||||
if (!p->context || !p->receive)
|
||||
WLog_WARN(TAG, "Missing receive callback=%p, context=%p", p->receive, p->context);
|
||||
else
|
||||
p->receive(p->context, e->data, e->size);
|
||||
|
||||
p->next = p->prep;
|
||||
p->prep = e;
|
||||
(*p->recorderBufferQueue)->Enqueue(p->recorderBufferQueue, e->data, e->size);
|
||||
}
|
||||
67
third_party/FreeRDP/channels/audin/client/opensles/opensl_io.h
vendored
Normal file
67
third_party/FreeRDP/channels/audin/client/opensles/opensl_io.h
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
opensl_io.c:
|
||||
Android OpenSL input/output module header
|
||||
Copyright (c) 2012, Victor Lazzarini
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the <organization> nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#ifndef FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H
|
||||
#define FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H
|
||||
|
||||
#include <SLES/OpenSLES.h>
|
||||
#include <SLES/OpenSLES_Android.h>
|
||||
|
||||
#include <freerdp/api.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
typedef struct opensl_stream OPENSL_STREAM;
|
||||
|
||||
typedef void (*opensl_receive_t)(void* context, const void* data, size_t size);
|
||||
|
||||
/*
|
||||
Close the audio device
|
||||
*/
|
||||
FREERDP_LOCAL void android_CloseRecDevice(OPENSL_STREAM* p);
|
||||
|
||||
/*
|
||||
Open the audio device with a given sampling rate (sr), input and output channels and IO buffer
|
||||
size in frames. Returns a handle to the OpenSL stream
|
||||
*/
|
||||
WINPR_ATTR_MALLOC(android_CloseRecDevice, 1)
|
||||
WINPR_ATTR_NODISCARD
|
||||
FREERDP_LOCAL OPENSL_STREAM* android_OpenRecDevice(void* context, opensl_receive_t receive,
|
||||
int sr, int inchannels, int bufferframes,
|
||||
int bits_per_sample);
|
||||
#ifdef __cplusplus
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif /* FREERDP_CHANNEL_AUDIN_CLIENT_OPENSL_IO_H */
|
||||
31
third_party/FreeRDP/channels/audin/client/oss/CMakeLists.txt
vendored
Normal file
31
third_party/FreeRDP/channels/audin/client/oss/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "oss" "")
|
||||
|
||||
find_package(OSS REQUIRED)
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_oss.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${OSS_LIBRARIES})
|
||||
|
||||
include_directories(..)
|
||||
include_directories(${CMAKE_CURRENT_BINARY_DIR})
|
||||
include_directories(SYSTEM ${OSS_INCLUDE_DIRS})
|
||||
cleaning_configure_file(${CMAKE_SOURCE_DIR}/cmake/oss-includes.h.in ${CMAKE_CURRENT_BINARY_DIR}/oss-includes.h @ONLY)
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
449
third_party/FreeRDP/channels/audin/client/oss/audin_oss.c
vendored
Normal file
449
third_party/FreeRDP/channels/audin/client/oss/audin_oss.c
vendored
Normal file
@@ -0,0 +1,449 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - OSS implementation
|
||||
*
|
||||
* Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com>
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/synch.h>
|
||||
#include <winpr/string.h>
|
||||
#include <winpr/thread.h>
|
||||
#include <winpr/cmdline.h>
|
||||
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#include <oss-includes.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/channels/rdpsnd.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
HANDLE thread;
|
||||
HANDLE stopEvent;
|
||||
|
||||
AUDIO_FORMAT format;
|
||||
UINT32 FramesPerPacket;
|
||||
int dev_unit;
|
||||
|
||||
AudinReceive receive;
|
||||
void* user_data;
|
||||
|
||||
rdpContext* rdpcontext;
|
||||
} AudinOSSDevice;
|
||||
|
||||
static void OSS_LOG_ERR(const char* _text, int _error)
|
||||
{
|
||||
if ((_error) != 0)
|
||||
{
|
||||
char buffer[256] = WINPR_C_ARRAY_INIT;
|
||||
WLog_ERR(TAG, "%s: %i - %s\n", (_text), (_error),
|
||||
winpr_strerror((_error), buffer, sizeof(buffer)));
|
||||
}
|
||||
}
|
||||
|
||||
static UINT32 audin_oss_get_format(const AUDIO_FORMAT* format)
|
||||
{
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
switch (format->wBitsPerSample)
|
||||
{
|
||||
case 8:
|
||||
return AFMT_S8;
|
||||
|
||||
case 16:
|
||||
return AFMT_S16_LE;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static BOOL audin_oss_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
|
||||
{
|
||||
if (device == nullptr || format == nullptr)
|
||||
return FALSE;
|
||||
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
if (format->cbSize != 0 || format->nSamplesPerSec > 48000 ||
|
||||
(format->wBitsPerSample != 8 && format->wBitsPerSample != 16) ||
|
||||
(format->nChannels != 1 && format->nChannels != 2))
|
||||
return FALSE;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_oss_set_format(IAudinDevice* device, const AUDIO_FORMAT* format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinOSSDevice* oss = (AudinOSSDevice*)device;
|
||||
|
||||
if (device == nullptr || format == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
oss->FramesPerPacket = FramesPerPacket;
|
||||
oss->format = *format;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static DWORD WINAPI audin_oss_thread_func(LPVOID arg)
|
||||
{
|
||||
char dev_name[PATH_MAX] = "/dev/dsp";
|
||||
int pcm_handle = -1;
|
||||
BYTE* buffer = nullptr;
|
||||
unsigned long tmp = 0;
|
||||
size_t buffer_size = 0;
|
||||
AudinOSSDevice* oss = (AudinOSSDevice*)arg;
|
||||
UINT error = 0;
|
||||
DWORD status = 0;
|
||||
|
||||
if (oss == nullptr)
|
||||
{
|
||||
error = ERROR_INVALID_PARAMETER;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
if (oss->dev_unit != -1)
|
||||
(void)sprintf_s(dev_name, (PATH_MAX - 1), "/dev/dsp%i", oss->dev_unit);
|
||||
|
||||
WLog_INFO(TAG, "open: %s", dev_name);
|
||||
|
||||
if ((pcm_handle = open(dev_name, O_RDONLY)) < 0)
|
||||
{
|
||||
OSS_LOG_ERR("sound dev open failed", errno);
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
/* Set format. */
|
||||
tmp = audin_oss_get_format(&oss->format);
|
||||
|
||||
if (ioctl(pcm_handle, SNDCTL_DSP_SETFMT, &tmp) == -1)
|
||||
OSS_LOG_ERR("SNDCTL_DSP_SETFMT failed", errno);
|
||||
|
||||
tmp = oss->format.nChannels;
|
||||
|
||||
if (ioctl(pcm_handle, SNDCTL_DSP_CHANNELS, &tmp) == -1)
|
||||
OSS_LOG_ERR("SNDCTL_DSP_CHANNELS failed", errno);
|
||||
|
||||
tmp = oss->format.nSamplesPerSec;
|
||||
|
||||
if (ioctl(pcm_handle, SNDCTL_DSP_SPEED, &tmp) == -1)
|
||||
OSS_LOG_ERR("SNDCTL_DSP_SPEED failed", errno);
|
||||
|
||||
tmp = oss->format.nBlockAlign;
|
||||
|
||||
if (ioctl(pcm_handle, SNDCTL_DSP_SETFRAGMENT, &tmp) == -1)
|
||||
OSS_LOG_ERR("SNDCTL_DSP_SETFRAGMENT failed", errno);
|
||||
|
||||
buffer_size =
|
||||
(1ull * oss->FramesPerPacket * oss->format.nChannels * (oss->format.wBitsPerSample / 8ull));
|
||||
buffer = (BYTE*)calloc((buffer_size + sizeof(void*)), sizeof(BYTE));
|
||||
|
||||
if (nullptr == buffer)
|
||||
{
|
||||
OSS_LOG_ERR("malloc() fail", errno);
|
||||
error = ERROR_NOT_ENOUGH_MEMORY;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
while (1)
|
||||
{
|
||||
SSIZE_T stmp = -1;
|
||||
status = WaitForSingleObject(oss->stopEvent, 0);
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
if (status == WAIT_OBJECT_0)
|
||||
break;
|
||||
|
||||
stmp = read(pcm_handle, buffer, buffer_size);
|
||||
|
||||
/* Error happen. */
|
||||
if (stmp < 0)
|
||||
{
|
||||
OSS_LOG_ERR("read() error", errno);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((size_t)stmp < buffer_size) /* Not enough data. */
|
||||
continue;
|
||||
|
||||
if ((error = oss->receive(&oss->format, buffer, buffer_size, oss->user_data)))
|
||||
{
|
||||
WLog_ERR(TAG, "oss->receive failed with error %" PRIu32 "", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
err_out:
|
||||
|
||||
if (error && oss && oss->rdpcontext)
|
||||
setChannelError(oss->rdpcontext, error, "audin_oss_thread_func reported an error");
|
||||
|
||||
if (pcm_handle != -1)
|
||||
{
|
||||
WLog_INFO(TAG, "close: %s", dev_name);
|
||||
close(pcm_handle);
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
ExitThread(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_oss_open(IAudinDevice* device, AudinReceive receive, void* user_data)
|
||||
{
|
||||
AudinOSSDevice* oss = (AudinOSSDevice*)device;
|
||||
oss->receive = receive;
|
||||
oss->user_data = user_data;
|
||||
|
||||
if (!(oss->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr)))
|
||||
{
|
||||
WLog_ERR(TAG, "CreateEvent failed!");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if (!(oss->thread = CreateThread(nullptr, 0, audin_oss_thread_func, oss, 0, nullptr)))
|
||||
{
|
||||
WLog_ERR(TAG, "CreateThread failed!");
|
||||
(void)CloseHandle(oss->stopEvent);
|
||||
oss->stopEvent = nullptr;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_oss_close(IAudinDevice* device)
|
||||
{
|
||||
UINT error = 0;
|
||||
AudinOSSDevice* oss = (AudinOSSDevice*)device;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (oss->stopEvent != nullptr)
|
||||
{
|
||||
(void)SetEvent(oss->stopEvent);
|
||||
|
||||
if (WaitForSingleObject(oss->thread, INFINITE) == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error);
|
||||
return error;
|
||||
}
|
||||
|
||||
(void)CloseHandle(oss->stopEvent);
|
||||
oss->stopEvent = nullptr;
|
||||
(void)CloseHandle(oss->thread);
|
||||
oss->thread = nullptr;
|
||||
}
|
||||
|
||||
oss->receive = nullptr;
|
||||
oss->user_data = nullptr;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_oss_free(IAudinDevice* device)
|
||||
{
|
||||
AudinOSSDevice* oss = (AudinOSSDevice*)device;
|
||||
UINT error = 0;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if ((error = audin_oss_close(device)))
|
||||
{
|
||||
WLog_ERR(TAG, "audin_oss_close failed with error code %" PRIu32 "!", error);
|
||||
}
|
||||
|
||||
free(oss);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_oss_parse_addin_args(AudinOSSDevice* device, const ADDIN_ARGV* args)
|
||||
{
|
||||
int status = 0;
|
||||
char* str_num = nullptr;
|
||||
char* eptr = nullptr;
|
||||
DWORD flags = 0;
|
||||
const COMMAND_LINE_ARGUMENT_A* arg = nullptr;
|
||||
AudinOSSDevice* oss = device;
|
||||
COMMAND_LINE_ARGUMENT_A audin_oss_args[] = {
|
||||
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
|
||||
"audio device name" },
|
||||
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
|
||||
};
|
||||
|
||||
flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
status = CommandLineParseArgumentsA(args->argc, args->argv, audin_oss_args, flags, oss, nullptr,
|
||||
nullptr);
|
||||
|
||||
if (status < 0)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
arg = audin_oss_args;
|
||||
errno = 0;
|
||||
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
|
||||
{
|
||||
str_num = _strdup(arg->Value);
|
||||
|
||||
if (!str_num)
|
||||
{
|
||||
WLog_ERR(TAG, "_strdup failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
{
|
||||
long val = strtol(str_num, &eptr, 10);
|
||||
|
||||
if ((errno != 0) || (val < INT32_MIN) || (val > INT32_MAX))
|
||||
{
|
||||
free(str_num);
|
||||
return CHANNEL_RC_NULL_DATA;
|
||||
}
|
||||
|
||||
oss->dev_unit = (INT32)val;
|
||||
}
|
||||
|
||||
if (oss->dev_unit < 0 || *eptr != '\0')
|
||||
oss->dev_unit = -1;
|
||||
|
||||
free(str_num);
|
||||
}
|
||||
CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE oss_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
const ADDIN_ARGV* args = nullptr;
|
||||
AudinOSSDevice* oss = nullptr;
|
||||
UINT error = 0;
|
||||
oss = (AudinOSSDevice*)calloc(1, sizeof(AudinOSSDevice));
|
||||
|
||||
if (!oss)
|
||||
{
|
||||
WLog_ERR(TAG, "calloc failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
oss->iface.Open = audin_oss_open;
|
||||
oss->iface.FormatSupported = audin_oss_format_supported;
|
||||
oss->iface.SetFormat = audin_oss_set_format;
|
||||
oss->iface.Close = audin_oss_close;
|
||||
oss->iface.Free = audin_oss_free;
|
||||
oss->rdpcontext = pEntryPoints->rdpcontext;
|
||||
oss->dev_unit = -1;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if ((error = audin_oss_parse_addin_args(oss, args)))
|
||||
{
|
||||
WLog_ERR(TAG, "audin_oss_parse_addin_args failed with errorcode %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)oss)))
|
||||
{
|
||||
WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
free(oss);
|
||||
return error;
|
||||
}
|
||||
30
third_party/FreeRDP/channels/audin/client/pulse/CMakeLists.txt
vendored
Normal file
30
third_party/FreeRDP/channels/audin/client/pulse/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "pulse" "")
|
||||
|
||||
find_package(PulseAudio REQUIRED)
|
||||
freerdp_client_pc_add_requires_private("libpulse")
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_pulse.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${PULSEAUDIO_LIBRARY} ${PULSEAUDIO_MAINLOOP_LIBRARY})
|
||||
|
||||
include_directories(..)
|
||||
include_directories(SYSTEM ${PULSEAUDIO_INCLUDE_DIR})
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
584
third_party/FreeRDP/channels/audin/client/pulse/audin_pulse.c
vendored
Normal file
584
third_party/FreeRDP/channels/audin/client/pulse/audin_pulse.c
vendored
Normal file
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - PulseAudio implementation
|
||||
*
|
||||
* Copyright 2010-2011 Vic Lee
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/cmdline.h>
|
||||
#include <winpr/wlog.h>
|
||||
#include <winpr/cast.h>
|
||||
|
||||
#include <pulse/pulseaudio.h>
|
||||
|
||||
#include <freerdp/types.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/codec/audio.h>
|
||||
#include <freerdp/client/audin.h>
|
||||
#include <freerdp/utils/helpers.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
char* device_name;
|
||||
char* client_name;
|
||||
char* stream_name;
|
||||
UINT32 frames_per_packet;
|
||||
pa_threaded_mainloop* mainloop;
|
||||
pa_context* context;
|
||||
pa_sample_spec sample_spec;
|
||||
pa_stream* stream;
|
||||
AUDIO_FORMAT format;
|
||||
|
||||
size_t bytes_per_frame;
|
||||
size_t buffer_frames;
|
||||
|
||||
AudinReceive receive;
|
||||
void* user_data;
|
||||
|
||||
rdpContext* rdpcontext;
|
||||
wLog* log;
|
||||
} AudinPulseDevice;
|
||||
|
||||
static const char* pulse_context_state_string(pa_context_state_t state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case PA_CONTEXT_UNCONNECTED:
|
||||
return "PA_CONTEXT_UNCONNECTED";
|
||||
case PA_CONTEXT_CONNECTING:
|
||||
return "PA_CONTEXT_CONNECTING";
|
||||
case PA_CONTEXT_AUTHORIZING:
|
||||
return "PA_CONTEXT_AUTHORIZING";
|
||||
case PA_CONTEXT_SETTING_NAME:
|
||||
return "PA_CONTEXT_SETTING_NAME";
|
||||
case PA_CONTEXT_READY:
|
||||
return "PA_CONTEXT_READY";
|
||||
case PA_CONTEXT_FAILED:
|
||||
return "PA_CONTEXT_FAILED";
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
return "PA_CONTEXT_TERMINATED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static const char* pulse_stream_state_string(pa_stream_state_t state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case PA_STREAM_UNCONNECTED:
|
||||
return "PA_STREAM_UNCONNECTED";
|
||||
case PA_STREAM_CREATING:
|
||||
return "PA_STREAM_CREATING";
|
||||
case PA_STREAM_READY:
|
||||
return "PA_STREAM_READY";
|
||||
case PA_STREAM_FAILED:
|
||||
return "PA_STREAM_FAILED";
|
||||
case PA_STREAM_TERMINATED:
|
||||
return "PA_STREAM_TERMINATED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
static void audin_pulse_context_state_callback(pa_context* context, void* userdata)
|
||||
{
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)userdata;
|
||||
pa_context_state_t state = pa_context_get_state(context);
|
||||
|
||||
WLog_Print(pulse->log, WLOG_DEBUG, "context state %s", pulse_context_state_string(state));
|
||||
switch (state)
|
||||
{
|
||||
case PA_CONTEXT_READY:
|
||||
case PA_CONTEXT_FAILED:
|
||||
case PA_CONTEXT_TERMINATED:
|
||||
pa_threaded_mainloop_signal(pulse->mainloop, 0);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_pulse_connect(IAudinDevice* device)
|
||||
{
|
||||
pa_context_state_t state = PA_CONTEXT_FAILED;
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)device;
|
||||
|
||||
if (!pulse->context)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (pa_context_connect(pulse->context, nullptr, PA_CONTEXT_NOFLAGS, nullptr))
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "pa_context_connect failed (%d)",
|
||||
pa_context_errno(pulse->context));
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_lock(pulse->mainloop);
|
||||
|
||||
if (pa_threaded_mainloop_start(pulse->mainloop) < 0)
|
||||
{
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "pa_threaded_mainloop_start failed (%d)",
|
||||
pa_context_errno(pulse->context));
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
state = pa_context_get_state(pulse->context);
|
||||
|
||||
if (state == PA_CONTEXT_READY)
|
||||
break;
|
||||
|
||||
if (!PA_CONTEXT_IS_GOOD(state))
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "bad context state (%s: %d)",
|
||||
pulse_context_state_string(state), pa_context_errno(pulse->context));
|
||||
pa_context_disconnect(pulse->context);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_wait(pulse->mainloop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
WLog_Print(pulse->log, WLOG_DEBUG, "connected");
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_pulse_free(IAudinDevice* device)
|
||||
{
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)device;
|
||||
|
||||
if (!pulse)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (pulse->mainloop)
|
||||
{
|
||||
pa_threaded_mainloop_stop(pulse->mainloop);
|
||||
}
|
||||
|
||||
if (pulse->context)
|
||||
{
|
||||
pa_context_disconnect(pulse->context);
|
||||
pa_context_unref(pulse->context);
|
||||
pulse->context = nullptr;
|
||||
}
|
||||
|
||||
if (pulse->mainloop)
|
||||
{
|
||||
pa_threaded_mainloop_free(pulse->mainloop);
|
||||
pulse->mainloop = nullptr;
|
||||
}
|
||||
|
||||
free(pulse->device_name);
|
||||
free(pulse->client_name);
|
||||
free(pulse->stream_name);
|
||||
free(pulse);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static BOOL audin_pulse_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
|
||||
{
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)device;
|
||||
|
||||
if (!pulse || !format)
|
||||
return FALSE;
|
||||
|
||||
if (!pulse->context)
|
||||
return 0;
|
||||
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM:
|
||||
if (format->cbSize == 0 && (format->nSamplesPerSec <= PA_RATE_MAX) &&
|
||||
(format->wBitsPerSample == 8 || format->wBitsPerSample == 16) &&
|
||||
(format->nChannels >= 1 && format->nChannels <= PA_CHANNELS_MAX))
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_pulse_set_format(IAudinDevice* device, const AUDIO_FORMAT* format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)device;
|
||||
|
||||
if (!pulse || !format)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (!pulse->context)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (FramesPerPacket > 0)
|
||||
pulse->frames_per_packet = FramesPerPacket;
|
||||
|
||||
pa_sample_format_t sformat = PA_SAMPLE_INVALID;
|
||||
switch (format->wFormatTag)
|
||||
{
|
||||
case WAVE_FORMAT_PCM: /* PCM */
|
||||
switch (format->wBitsPerSample)
|
||||
{
|
||||
case 8:
|
||||
sformat = PA_SAMPLE_U8;
|
||||
break;
|
||||
|
||||
case 16:
|
||||
sformat = PA_SAMPLE_S16LE;
|
||||
break;
|
||||
|
||||
default:
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
const pa_sample_spec sample_spec = {
|
||||
.format = sformat,
|
||||
.rate = format->nSamplesPerSec,
|
||||
.channels = WINPR_ASSERTING_INT_CAST(uint8_t, format->nChannels),
|
||||
};
|
||||
|
||||
pulse->sample_spec = sample_spec;
|
||||
pulse->format = *format;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static void audin_pulse_stream_state_callback(pa_stream* stream, void* userdata)
|
||||
{
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)userdata;
|
||||
WINPR_ASSERT(pulse);
|
||||
|
||||
pa_stream_state_t state = pa_stream_get_state(stream);
|
||||
|
||||
WLog_Print(pulse->log, WLOG_DEBUG, "stream state %s", pulse_stream_state_string(state));
|
||||
switch (state)
|
||||
{
|
||||
case PA_STREAM_READY:
|
||||
case PA_STREAM_FAILED:
|
||||
case PA_STREAM_TERMINATED:
|
||||
pa_threaded_mainloop_signal(pulse->mainloop, 0);
|
||||
break;
|
||||
|
||||
case PA_STREAM_UNCONNECTED:
|
||||
case PA_STREAM_CREATING:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void audin_pulse_stream_request_callback(pa_stream* stream, size_t length, void* userdata)
|
||||
{
|
||||
const void* data = nullptr;
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)userdata;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
pa_stream_peek(stream, &data, &length);
|
||||
error =
|
||||
IFCALLRESULT(CHANNEL_RC_OK, pulse->receive, &pulse->format, data, length, pulse->user_data);
|
||||
pa_stream_drop(stream);
|
||||
|
||||
if (error && pulse->rdpcontext)
|
||||
setChannelError(pulse->rdpcontext, error, "audin_pulse_thread_func reported an error");
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_pulse_close(IAudinDevice* device)
|
||||
{
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)device;
|
||||
|
||||
if (!pulse)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (pulse->stream)
|
||||
{
|
||||
pa_threaded_mainloop_lock(pulse->mainloop);
|
||||
pa_stream_disconnect(pulse->stream);
|
||||
pa_stream_unref(pulse->stream);
|
||||
pulse->stream = nullptr;
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
}
|
||||
|
||||
pulse->receive = nullptr;
|
||||
pulse->user_data = nullptr;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_pulse_open(IAudinDevice* device, AudinReceive receive, void* user_data)
|
||||
{
|
||||
pa_stream_state_t state = PA_STREAM_FAILED;
|
||||
pa_buffer_attr buffer_attr = WINPR_C_ARRAY_INIT;
|
||||
AudinPulseDevice* pulse = (AudinPulseDevice*)device;
|
||||
|
||||
if (!pulse || !receive || !user_data)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (!pulse->context)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (!pulse->sample_spec.rate || pulse->stream)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
pulse->receive = receive;
|
||||
pulse->user_data = user_data;
|
||||
pa_threaded_mainloop_lock(pulse->mainloop);
|
||||
pulse->stream = pa_stream_new(pulse->context, pulse->stream_name, &pulse->sample_spec, nullptr);
|
||||
|
||||
if (!pulse->stream)
|
||||
{
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
WLog_Print(pulse->log, WLOG_DEBUG, "pa_stream_new failed (%d)",
|
||||
pa_context_errno(pulse->context));
|
||||
const int rc = pa_context_errno(pulse->context);
|
||||
return (UINT)rc;
|
||||
}
|
||||
|
||||
pulse->bytes_per_frame = pa_frame_size(&pulse->sample_spec);
|
||||
pa_stream_set_state_callback(pulse->stream, audin_pulse_stream_state_callback, pulse);
|
||||
pa_stream_set_read_callback(pulse->stream, audin_pulse_stream_request_callback, pulse);
|
||||
buffer_attr.maxlength = (UINT32)-1;
|
||||
buffer_attr.tlength = (UINT32)-1;
|
||||
buffer_attr.prebuf = (UINT32)-1;
|
||||
buffer_attr.minreq = (UINT32)-1;
|
||||
/* 500ms latency */
|
||||
const size_t frag = pulse->bytes_per_frame * pulse->frames_per_packet;
|
||||
WINPR_ASSERT(frag <= UINT32_MAX);
|
||||
buffer_attr.fragsize = (uint32_t)frag;
|
||||
|
||||
if (buffer_attr.fragsize % pulse->format.nBlockAlign)
|
||||
buffer_attr.fragsize +=
|
||||
pulse->format.nBlockAlign - buffer_attr.fragsize % pulse->format.nBlockAlign;
|
||||
|
||||
if (pa_stream_connect_record(pulse->stream, pulse->device_name, &buffer_attr,
|
||||
PA_STREAM_ADJUST_LATENCY) < 0)
|
||||
{
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "pa_stream_connect_playback failed (%d)",
|
||||
pa_context_errno(pulse->context));
|
||||
const int rc = pa_context_errno(pulse->context);
|
||||
return (UINT)rc;
|
||||
}
|
||||
|
||||
while (pulse->stream)
|
||||
{
|
||||
state = pa_stream_get_state(pulse->stream);
|
||||
|
||||
if (state == PA_STREAM_READY)
|
||||
break;
|
||||
|
||||
if (!PA_STREAM_IS_GOOD(state))
|
||||
{
|
||||
audin_pulse_close(device);
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "bad stream state (%s: %d)",
|
||||
pulse_stream_state_string(state), pa_context_errno(pulse->context));
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
const int rc = pa_context_errno(pulse->context);
|
||||
return (UINT)rc;
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_wait(pulse->mainloop);
|
||||
}
|
||||
|
||||
pa_threaded_mainloop_unlock(pulse->mainloop);
|
||||
pulse->buffer_frames = 0;
|
||||
WLog_Print(pulse->log, WLOG_DEBUG, "connected");
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_pulse_parse_addin_args(AudinPulseDevice* pulse, const ADDIN_ARGV* args)
|
||||
{
|
||||
COMMAND_LINE_ARGUMENT_A audin_pulse_args[] = {
|
||||
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
|
||||
"audio device name" },
|
||||
{ "client_name", COMMAND_LINE_VALUE_REQUIRED, "<client_name>", nullptr, nullptr, -1,
|
||||
nullptr, "name of pulse client" },
|
||||
{ "stream_name", COMMAND_LINE_VALUE_REQUIRED, "<stream_name>", nullptr, nullptr, -1,
|
||||
nullptr, "name of pulse stream" },
|
||||
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
|
||||
};
|
||||
|
||||
const DWORD flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
const int status = CommandLineParseArgumentsA(args->argc, args->argv, audin_pulse_args, flags,
|
||||
pulse, nullptr, nullptr);
|
||||
|
||||
if (status < 0)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
const COMMAND_LINE_ARGUMENT_A* arg = audin_pulse_args;
|
||||
|
||||
const char* client_name = nullptr;
|
||||
const char* stream_name = nullptr;
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
|
||||
{
|
||||
pulse->device_name = _strdup(arg->Value);
|
||||
|
||||
if (!pulse->device_name)
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "_strdup failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
}
|
||||
CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
if (!client_name)
|
||||
client_name = freerdp_getApplicationDetailsString();
|
||||
if (!stream_name)
|
||||
stream_name = freerdp_getApplicationDetailsString();
|
||||
|
||||
pulse->client_name = _strdup(client_name);
|
||||
pulse->stream_name = _strdup(stream_name);
|
||||
if (!pulse->client_name || !pulse->stream_name)
|
||||
return ERROR_OUTOFMEMORY;
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE pulse_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
const ADDIN_ARGV* args = nullptr;
|
||||
AudinPulseDevice* pulse = nullptr;
|
||||
UINT error = 0;
|
||||
pulse = (AudinPulseDevice*)calloc(1, sizeof(AudinPulseDevice));
|
||||
|
||||
if (!pulse)
|
||||
{
|
||||
WLog_ERR(TAG, "calloc failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
pulse->log = WLog_Get(TAG);
|
||||
pulse->iface.Open = audin_pulse_open;
|
||||
pulse->iface.FormatSupported = audin_pulse_format_supported;
|
||||
pulse->iface.SetFormat = audin_pulse_set_format;
|
||||
pulse->iface.Close = audin_pulse_close;
|
||||
pulse->iface.Free = audin_pulse_free;
|
||||
pulse->rdpcontext = pEntryPoints->rdpcontext;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if ((error = audin_pulse_parse_addin_args(pulse, args)))
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR,
|
||||
"audin_pulse_parse_addin_args failed with error %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
pulse->mainloop = pa_threaded_mainloop_new();
|
||||
|
||||
if (!pulse->mainloop)
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "pa_threaded_mainloop_new failed");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
pulse->context =
|
||||
pa_context_new(pa_threaded_mainloop_get_api(pulse->mainloop), pulse->client_name);
|
||||
|
||||
if (!pulse->context)
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "pa_context_new failed");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
pa_context_set_state_callback(pulse->context, audin_pulse_context_state_callback, pulse);
|
||||
|
||||
if ((error = audin_pulse_connect(&pulse->iface)))
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "audin_pulse_connect failed");
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, &pulse->iface)))
|
||||
{
|
||||
WLog_Print(pulse->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!",
|
||||
error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
audin_pulse_free(&pulse->iface);
|
||||
return error;
|
||||
}
|
||||
30
third_party/FreeRDP/channels/audin/client/sndio/CMakeLists.txt
vendored
Normal file
30
third_party/FreeRDP/channels/audin/client/sndio/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com>
|
||||
# Copyright (c) 2020 Ingo Feinerer <feinerer@logic.at>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "sndio" "")
|
||||
|
||||
find_package(SNDIO REQUIRED)
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_sndio.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp ${SNDIO_LIBRARIES})
|
||||
|
||||
include_directories(..)
|
||||
include_directories(SYSTEM ${SNDIO_INCLUDE_DIRS})
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
353
third_party/FreeRDP/channels/audin/client/sndio/audin_sndio.c
vendored
Normal file
353
third_party/FreeRDP/channels/audin/client/sndio/audin_sndio.c
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - sndio implementation
|
||||
*
|
||||
* Copyright (c) 2015 Rozhuk Ivan <rozhuk.im@gmail.com>
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
* Copyright 2020 Ingo Feinerer <feinerer@logic.at>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <sndio.h>
|
||||
|
||||
#include <winpr/cmdline.h>
|
||||
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/channels/rdpsnd.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice device;
|
||||
|
||||
HANDLE thread;
|
||||
HANDLE stopEvent;
|
||||
|
||||
AUDIO_FORMAT format;
|
||||
UINT32 FramesPerPacket;
|
||||
|
||||
AudinReceive receive;
|
||||
void* user_data;
|
||||
|
||||
rdpContext* rdpcontext;
|
||||
} AudinSndioDevice;
|
||||
|
||||
static BOOL audin_sndio_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
|
||||
{
|
||||
if (device == nullptr || format == nullptr)
|
||||
return FALSE;
|
||||
|
||||
return (format->wFormatTag == WAVE_FORMAT_PCM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_sndio_set_format(IAudinDevice* device, AUDIO_FORMAT* format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinSndioDevice* sndio = (AudinSndioDevice*)device;
|
||||
|
||||
if (device == nullptr || format == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (format->wFormatTag != WAVE_FORMAT_PCM)
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
|
||||
sndio->format = *format;
|
||||
sndio->FramesPerPacket = FramesPerPacket;
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static void* audin_sndio_thread_func(void* arg)
|
||||
{
|
||||
struct sio_hdl* hdl;
|
||||
struct sio_par par;
|
||||
BYTE* buffer = nullptr;
|
||||
size_t n, nbytes;
|
||||
AudinSndioDevice* sndio = (AudinSndioDevice*)arg;
|
||||
UINT error = 0;
|
||||
DWORD status;
|
||||
|
||||
if (arg == nullptr)
|
||||
{
|
||||
error = ERROR_INVALID_PARAMETER;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
hdl = sio_open(SIO_DEVANY, SIO_REC, 0);
|
||||
if (hdl == nullptr)
|
||||
{
|
||||
WLog_ERR(TAG, "could not open audio device");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
sio_initpar(&par);
|
||||
par.bits = sndio->format.wBitsPerSample;
|
||||
par.rchan = sndio->format.nChannels;
|
||||
par.rate = sndio->format.nSamplesPerSec;
|
||||
if (!sio_setpar(hdl, &par))
|
||||
{
|
||||
WLog_ERR(TAG, "could not set audio parameters");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto err_out;
|
||||
}
|
||||
if (!sio_getpar(hdl, &par))
|
||||
{
|
||||
WLog_ERR(TAG, "could not get audio parameters");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
if (!sio_start(hdl))
|
||||
{
|
||||
WLog_ERR(TAG, "could not start audio device");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
nbytes =
|
||||
(sndio->FramesPerPacket * sndio->format.nChannels * (sndio->format.wBitsPerSample / 8));
|
||||
buffer = (BYTE*)calloc((nbytes + sizeof(void*)), sizeof(BYTE));
|
||||
|
||||
if (buffer == nullptr)
|
||||
{
|
||||
error = ERROR_NOT_ENOUGH_MEMORY;
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
while (1)
|
||||
{
|
||||
status = WaitForSingleObject(sndio->stopEvent, 0);
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error);
|
||||
goto err_out;
|
||||
}
|
||||
|
||||
if (status == WAIT_OBJECT_0)
|
||||
break;
|
||||
|
||||
n = sio_read(hdl, buffer, nbytes);
|
||||
|
||||
if (n == 0)
|
||||
{
|
||||
WLog_ERR(TAG, "could not read");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (n < nbytes)
|
||||
continue;
|
||||
|
||||
if ((error = sndio->receive(&sndio->format, buffer, nbytes, sndio->user_data)))
|
||||
{
|
||||
WLog_ERR(TAG, "sndio->receive failed with error %" PRIu32 "", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
err_out:
|
||||
if (error && sndio && sndio->rdpcontext)
|
||||
setChannelError(sndio->rdpcontext, error, "audin_sndio_thread_func reported an error");
|
||||
|
||||
if (hdl != nullptr)
|
||||
{
|
||||
WLog_INFO(TAG, "sio_close");
|
||||
sio_stop(hdl);
|
||||
sio_close(hdl);
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
ExitThread(0);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_sndio_open(IAudinDevice* device, AudinReceive receive, void* user_data)
|
||||
{
|
||||
AudinSndioDevice* sndio = (AudinSndioDevice*)device;
|
||||
sndio->receive = receive;
|
||||
sndio->user_data = user_data;
|
||||
|
||||
if (!(sndio->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr)))
|
||||
{
|
||||
WLog_ERR(TAG, "CreateEvent failed");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if (!(sndio->thread = CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)audin_sndio_thread_func,
|
||||
sndio, 0, nullptr)))
|
||||
{
|
||||
WLog_ERR(TAG, "CreateThread failed");
|
||||
(void)CloseHandle(sndio->stopEvent);
|
||||
sndio->stopEvent = nullptr;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_sndio_close(IAudinDevice* device)
|
||||
{
|
||||
UINT error;
|
||||
AudinSndioDevice* sndio = (AudinSndioDevice*)device;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if (sndio->stopEvent != nullptr)
|
||||
{
|
||||
(void)SetEvent(sndio->stopEvent);
|
||||
|
||||
if (WaitForSingleObject(sndio->thread, INFINITE) == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_ERR(TAG, "WaitForSingleObject failed with error %" PRIu32 "", error);
|
||||
return error;
|
||||
}
|
||||
|
||||
(void)CloseHandle(sndio->stopEvent);
|
||||
sndio->stopEvent = nullptr;
|
||||
(void)CloseHandle(sndio->thread);
|
||||
sndio->thread = nullptr;
|
||||
}
|
||||
|
||||
sndio->receive = nullptr;
|
||||
sndio->user_data = nullptr;
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_sndio_free(IAudinDevice* device)
|
||||
{
|
||||
AudinSndioDevice* sndio = (AudinSndioDevice*)device;
|
||||
int error;
|
||||
|
||||
if (device == nullptr)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
if ((error = audin_sndio_close(device)))
|
||||
{
|
||||
WLog_ERR(TAG, "audin_sndio_close failed with error code %d", error);
|
||||
}
|
||||
|
||||
free(sndio);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_sndio_parse_addin_args(AudinSndioDevice* device, ADDIN_ARGV* args)
|
||||
{
|
||||
int status;
|
||||
DWORD flags;
|
||||
COMMAND_LINE_ARGUMENT_A* arg;
|
||||
AudinSndioDevice* sndio = (AudinSndioDevice*)device;
|
||||
COMMAND_LINE_ARGUMENT_A audin_sndio_args[] = { { nullptr, 0, nullptr, nullptr, nullptr, -1,
|
||||
nullptr, nullptr } };
|
||||
flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
status = CommandLineParseArgumentsA(args->argc, (const char**)args->argv, audin_sndio_args,
|
||||
flags, sndio, nullptr, nullptr);
|
||||
|
||||
if (status < 0)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
arg = audin_sndio_args;
|
||||
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE sndio_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
ADDIN_ARGV* args;
|
||||
AudinSndioDevice* sndio;
|
||||
UINT ret = CHANNEL_RC_OK;
|
||||
sndio = (AudinSndioDevice*)calloc(1, sizeof(AudinSndioDevice));
|
||||
|
||||
if (sndio == nullptr)
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
|
||||
sndio->device.Open = audin_sndio_open;
|
||||
sndio->device.FormatSupported = audin_sndio_format_supported;
|
||||
sndio->device.SetFormat = audin_sndio_set_format;
|
||||
sndio->device.Close = audin_sndio_close;
|
||||
sndio->device.Free = audin_sndio_free;
|
||||
sndio->rdpcontext = pEntryPoints->rdpcontext;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if (args->argc > 1)
|
||||
{
|
||||
ret = audin_sndio_parse_addin_args(sndio, args);
|
||||
|
||||
if (ret != CHANNEL_RC_OK)
|
||||
{
|
||||
WLog_ERR(TAG, "error parsing arguments");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
if ((ret = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, (IAudinDevice*)sndio)))
|
||||
{
|
||||
WLog_ERR(TAG, "RegisterAudinDevice failed with error %" PRIu32 "", ret);
|
||||
goto error;
|
||||
}
|
||||
|
||||
return ret;
|
||||
error:
|
||||
audin_sndio_free(&sndio->device);
|
||||
return ret;
|
||||
}
|
||||
26
third_party/FreeRDP/channels/audin/client/winmm/CMakeLists.txt
vendored
Normal file
26
third_party/FreeRDP/channels/audin/client/winmm/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_client_subsystem("audin" "winmm" "")
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin_winmm.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS winpr freerdp winmm.lib)
|
||||
|
||||
include_directories(..)
|
||||
|
||||
add_channel_client_subsystem_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} "" TRUE "")
|
||||
568
third_party/FreeRDP/channels/audin/client/winmm/audin_winmm.c
vendored
Normal file
568
third_party/FreeRDP/channels/audin/client/winmm/audin_winmm.c
vendored
Normal file
@@ -0,0 +1,568 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Audio Input Redirection Virtual Channel - WinMM implementation
|
||||
*
|
||||
* Copyright 2013 Zhang Zhaolong <zhangzl2013@126.com>
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmsystem.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/wtsapi.h>
|
||||
#include <winpr/cmdline.h>
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/addin.h>
|
||||
#include <freerdp/client/audin.h>
|
||||
|
||||
#include "audin_main.h"
|
||||
|
||||
/* fix missing definitions in mingw */
|
||||
#ifndef WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE
|
||||
#define WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE 0x0010
|
||||
#endif
|
||||
|
||||
typedef struct
|
||||
{
|
||||
IAudinDevice iface;
|
||||
|
||||
char* device_name;
|
||||
AudinReceive receive;
|
||||
void* user_data;
|
||||
HANDLE thread;
|
||||
HANDLE stopEvent;
|
||||
HWAVEIN hWaveIn;
|
||||
PWAVEFORMATEX* ppwfx;
|
||||
PWAVEFORMATEX pwfx_cur;
|
||||
UINT32 ppwfx_size;
|
||||
UINT32 cFormats;
|
||||
UINT32 frames_per_packet;
|
||||
rdpContext* rdpcontext;
|
||||
wLog* log;
|
||||
} AudinWinmmDevice;
|
||||
|
||||
static void CALLBACK waveInProc(HWAVEIN hWaveIn, UINT uMsg, DWORD_PTR dwInstance,
|
||||
DWORD_PTR dwParam1, DWORD_PTR dwParam2)
|
||||
{
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)dwInstance;
|
||||
PWAVEHDR pWaveHdr;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
MMRESULT mmResult;
|
||||
|
||||
switch (uMsg)
|
||||
{
|
||||
case WIM_CLOSE:
|
||||
break;
|
||||
|
||||
case WIM_DATA:
|
||||
pWaveHdr = (WAVEHDR*)dwParam1;
|
||||
|
||||
if (WHDR_DONE == (WHDR_DONE & pWaveHdr->dwFlags))
|
||||
{
|
||||
if (pWaveHdr->dwBytesRecorded &&
|
||||
!(WaitForSingleObject(winmm->stopEvent, 0) == WAIT_OBJECT_0))
|
||||
{
|
||||
AUDIO_FORMAT format;
|
||||
format.cbSize = winmm->pwfx_cur->cbSize;
|
||||
format.nBlockAlign = winmm->pwfx_cur->nBlockAlign;
|
||||
format.nAvgBytesPerSec = winmm->pwfx_cur->nAvgBytesPerSec;
|
||||
format.nChannels = winmm->pwfx_cur->nChannels;
|
||||
format.nSamplesPerSec = winmm->pwfx_cur->nSamplesPerSec;
|
||||
format.wBitsPerSample = winmm->pwfx_cur->wBitsPerSample;
|
||||
format.wFormatTag = winmm->pwfx_cur->wFormatTag;
|
||||
|
||||
if ((error = winmm->receive(&format, pWaveHdr->lpData,
|
||||
pWaveHdr->dwBytesRecorded, winmm->user_data)))
|
||||
break;
|
||||
|
||||
mmResult = waveInAddBuffer(hWaveIn, pWaveHdr, sizeof(WAVEHDR));
|
||||
|
||||
if (mmResult != MMSYSERR_NOERROR)
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case WIM_OPEN:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (error && winmm->rdpcontext)
|
||||
setChannelError(winmm->rdpcontext, error, "waveInProc reported an error");
|
||||
}
|
||||
|
||||
static BOOL log_mmresult(AudinWinmmDevice* winmm, const char* what, MMRESULT result)
|
||||
{
|
||||
if (result != MMSYSERR_NOERROR)
|
||||
{
|
||||
CHAR buffer[8192] = WINPR_C_ARRAY_INIT;
|
||||
CHAR msg[8192] = WINPR_C_ARRAY_INIT;
|
||||
CHAR cmsg[8192] = WINPR_C_ARRAY_INIT;
|
||||
waveInGetErrorTextA(result, buffer, sizeof(buffer));
|
||||
|
||||
_snprintf(msg, sizeof(msg) - 1, "%s failed. %" PRIu32 " [%s]", what, result, buffer);
|
||||
_snprintf(cmsg, sizeof(cmsg) - 1, "audin_winmm_thread_func reported an error '%s'", msg);
|
||||
WLog_Print(winmm->log, WLOG_DEBUG, "%s", msg);
|
||||
if (winmm->rdpcontext)
|
||||
setChannelError(winmm->rdpcontext, ERROR_INTERNAL_ERROR, cmsg);
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static BOOL test_format_supported(const PWAVEFORMATEX pwfx)
|
||||
{
|
||||
MMRESULT rc;
|
||||
WAVEINCAPSA caps = WINPR_C_ARRAY_INIT;
|
||||
|
||||
rc = waveInGetDevCapsA(WAVE_MAPPER, &caps, sizeof(caps));
|
||||
if (rc != MMSYSERR_NOERROR)
|
||||
return FALSE;
|
||||
|
||||
switch (pwfx->nChannels)
|
||||
{
|
||||
case 1:
|
||||
if ((caps.dwFormats &
|
||||
(WAVE_FORMAT_1M08 | WAVE_FORMAT_2M08 | WAVE_FORMAT_4M08 | WAVE_FORMAT_96M08 |
|
||||
WAVE_FORMAT_1M16 | WAVE_FORMAT_2M16 | WAVE_FORMAT_4M16 | WAVE_FORMAT_96M16)) == 0)
|
||||
return FALSE;
|
||||
break;
|
||||
case 2:
|
||||
if ((caps.dwFormats &
|
||||
(WAVE_FORMAT_1S08 | WAVE_FORMAT_2S08 | WAVE_FORMAT_4S08 | WAVE_FORMAT_96S08 |
|
||||
WAVE_FORMAT_1S16 | WAVE_FORMAT_2S16 | WAVE_FORMAT_4S16 | WAVE_FORMAT_96S16)) == 0)
|
||||
return FALSE;
|
||||
break;
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
rc = waveInOpen(nullptr, WAVE_MAPPER, pwfx, 0, 0,
|
||||
WAVE_FORMAT_QUERY | WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE);
|
||||
return (rc == MMSYSERR_NOERROR);
|
||||
}
|
||||
|
||||
static DWORD WINAPI audin_winmm_thread_func(LPVOID arg)
|
||||
{
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)arg;
|
||||
char* buffer = nullptr;
|
||||
int size = 0;
|
||||
WAVEHDR waveHdr[4] = WINPR_C_ARRAY_INIT;
|
||||
DWORD status = 0;
|
||||
MMRESULT rc = 0;
|
||||
|
||||
if (!winmm->hWaveIn)
|
||||
{
|
||||
rc = waveInOpen(&winmm->hWaveIn, WAVE_MAPPER, winmm->pwfx_cur, (DWORD_PTR)waveInProc,
|
||||
(DWORD_PTR)winmm,
|
||||
CALLBACK_FUNCTION | WAVE_MAPPED_DEFAULT_COMMUNICATION_DEVICE);
|
||||
if (!log_mmresult(winmm, "waveInOpen", rc))
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
size =
|
||||
(winmm->pwfx_cur->wBitsPerSample * winmm->pwfx_cur->nChannels * winmm->frames_per_packet +
|
||||
7) /
|
||||
8;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
buffer = (char*)malloc(size);
|
||||
|
||||
if (!buffer)
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
|
||||
waveHdr[i].dwBufferLength = size;
|
||||
waveHdr[i].dwFlags = 0;
|
||||
waveHdr[i].lpData = buffer;
|
||||
rc = waveInPrepareHeader(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i]));
|
||||
|
||||
if (!log_mmresult(winmm, "waveInPrepareHeader", rc))
|
||||
{
|
||||
}
|
||||
|
||||
rc = waveInAddBuffer(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i]));
|
||||
|
||||
if (!log_mmresult(winmm, "waveInAddBuffer", rc))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
rc = waveInStart(winmm->hWaveIn);
|
||||
|
||||
if (!log_mmresult(winmm, "waveInStart", rc))
|
||||
{
|
||||
}
|
||||
|
||||
status = WaitForSingleObject(winmm->stopEvent, INFINITE);
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_DEBUG, "WaitForSingleObject failed.");
|
||||
|
||||
if (winmm->rdpcontext)
|
||||
setChannelError(winmm->rdpcontext, ERROR_INTERNAL_ERROR,
|
||||
"audin_winmm_thread_func reported an error");
|
||||
}
|
||||
|
||||
rc = waveInReset(winmm->hWaveIn);
|
||||
|
||||
if (!log_mmresult(winmm, "waveInReset", rc))
|
||||
{
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
rc = waveInUnprepareHeader(winmm->hWaveIn, &waveHdr[i], sizeof(waveHdr[i]));
|
||||
|
||||
if (!log_mmresult(winmm, "waveInUnprepareHeader", rc))
|
||||
{
|
||||
}
|
||||
|
||||
free(waveHdr[i].lpData);
|
||||
}
|
||||
|
||||
rc = waveInClose(winmm->hWaveIn);
|
||||
|
||||
if (!log_mmresult(winmm, "waveInClose", rc))
|
||||
{
|
||||
}
|
||||
|
||||
winmm->hWaveIn = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_winmm_free(IAudinDevice* device)
|
||||
{
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)device;
|
||||
|
||||
if (!winmm)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
for (UINT32 i = 0; i < winmm->cFormats; i++)
|
||||
{
|
||||
free(winmm->ppwfx[i]);
|
||||
}
|
||||
|
||||
free(winmm->ppwfx);
|
||||
free(winmm->device_name);
|
||||
free(winmm);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_winmm_close(IAudinDevice* device)
|
||||
{
|
||||
DWORD status;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)device;
|
||||
|
||||
if (!winmm)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
(void)SetEvent(winmm->stopEvent);
|
||||
status = WaitForSingleObject(winmm->thread, INFINITE);
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "!",
|
||||
error);
|
||||
return error;
|
||||
}
|
||||
|
||||
(void)CloseHandle(winmm->thread);
|
||||
(void)CloseHandle(winmm->stopEvent);
|
||||
winmm->thread = nullptr;
|
||||
winmm->stopEvent = nullptr;
|
||||
winmm->receive = nullptr;
|
||||
winmm->user_data = nullptr;
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_winmm_set_format(IAudinDevice* device, const AUDIO_FORMAT* format,
|
||||
UINT32 FramesPerPacket)
|
||||
{
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)device;
|
||||
|
||||
if (!winmm || !format)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
winmm->frames_per_packet = FramesPerPacket;
|
||||
|
||||
for (UINT32 i = 0; i < winmm->cFormats; i++)
|
||||
{
|
||||
const PWAVEFORMATEX ppwfx = winmm->ppwfx[i];
|
||||
if ((ppwfx->wFormatTag == format->wFormatTag) && (ppwfx->nChannels == format->nChannels) &&
|
||||
(ppwfx->wBitsPerSample == format->wBitsPerSample) &&
|
||||
(ppwfx->nSamplesPerSec == format->nSamplesPerSec))
|
||||
{
|
||||
/* BUG: Many devices report to support stereo recording but fail here.
|
||||
* Ensure we always use mono. */
|
||||
if (ppwfx->nChannels > 1)
|
||||
{
|
||||
ppwfx->nChannels = 1;
|
||||
}
|
||||
|
||||
if (ppwfx->nBlockAlign != 2)
|
||||
{
|
||||
ppwfx->nBlockAlign = 2;
|
||||
ppwfx->nAvgBytesPerSec = ppwfx->nSamplesPerSec * ppwfx->nBlockAlign;
|
||||
}
|
||||
|
||||
if (!test_format_supported(ppwfx))
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
winmm->pwfx_cur = ppwfx;
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
static BOOL audin_winmm_format_supported(IAudinDevice* device, const AUDIO_FORMAT* format)
|
||||
{
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)device;
|
||||
PWAVEFORMATEX pwfx;
|
||||
BYTE* data;
|
||||
|
||||
if (!winmm || !format)
|
||||
return FALSE;
|
||||
|
||||
if (format->wFormatTag != WAVE_FORMAT_PCM)
|
||||
return FALSE;
|
||||
|
||||
if (format->nChannels != 1)
|
||||
return FALSE;
|
||||
|
||||
pwfx = (PWAVEFORMATEX)malloc(sizeof(WAVEFORMATEX) + format->cbSize);
|
||||
|
||||
if (!pwfx)
|
||||
return FALSE;
|
||||
|
||||
pwfx->cbSize = format->cbSize;
|
||||
pwfx->wFormatTag = format->wFormatTag;
|
||||
pwfx->nChannels = format->nChannels;
|
||||
pwfx->nSamplesPerSec = format->nSamplesPerSec;
|
||||
pwfx->nBlockAlign = format->nBlockAlign;
|
||||
pwfx->wBitsPerSample = format->wBitsPerSample;
|
||||
data = (BYTE*)pwfx + sizeof(WAVEFORMATEX);
|
||||
memcpy(data, format->data, format->cbSize);
|
||||
|
||||
pwfx->nAvgBytesPerSec = pwfx->nSamplesPerSec * pwfx->nBlockAlign;
|
||||
|
||||
if (!test_format_supported(pwfx))
|
||||
goto fail;
|
||||
|
||||
if (winmm->cFormats >= winmm->ppwfx_size)
|
||||
{
|
||||
PWAVEFORMATEX* tmp_ppwfx;
|
||||
tmp_ppwfx = realloc(winmm->ppwfx, sizeof(PWAVEFORMATEX) * winmm->ppwfx_size * 2);
|
||||
|
||||
if (!tmp_ppwfx)
|
||||
goto fail;
|
||||
|
||||
winmm->ppwfx_size *= 2;
|
||||
winmm->ppwfx = tmp_ppwfx;
|
||||
}
|
||||
|
||||
winmm->ppwfx[winmm->cFormats++] = pwfx;
|
||||
return TRUE;
|
||||
|
||||
fail:
|
||||
free(pwfx);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_winmm_open(IAudinDevice* device, AudinReceive receive, void* user_data)
|
||||
{
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)device;
|
||||
|
||||
if (!winmm || !receive || !user_data)
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
|
||||
winmm->receive = receive;
|
||||
winmm->user_data = user_data;
|
||||
|
||||
if (!(winmm->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr)))
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "CreateEvent failed!");
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
if (!(winmm->thread = CreateThread(nullptr, 0, audin_winmm_thread_func, winmm, 0, nullptr)))
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "CreateThread failed!");
|
||||
(void)CloseHandle(winmm->stopEvent);
|
||||
winmm->stopEvent = nullptr;
|
||||
return ERROR_INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
static UINT audin_winmm_parse_addin_args(AudinWinmmDevice* device, const ADDIN_ARGV* args)
|
||||
{
|
||||
int status;
|
||||
DWORD flags;
|
||||
const COMMAND_LINE_ARGUMENT_A* arg;
|
||||
AudinWinmmDevice* winmm = (AudinWinmmDevice*)device;
|
||||
COMMAND_LINE_ARGUMENT_A audin_winmm_args[] = {
|
||||
{ "dev", COMMAND_LINE_VALUE_REQUIRED, "<device>", nullptr, nullptr, -1, nullptr,
|
||||
"audio device name" },
|
||||
{ nullptr, 0, nullptr, nullptr, nullptr, -1, nullptr, nullptr }
|
||||
};
|
||||
|
||||
flags =
|
||||
COMMAND_LINE_SIGIL_NONE | COMMAND_LINE_SEPARATOR_COLON | COMMAND_LINE_IGN_UNKNOWN_KEYWORD;
|
||||
status = CommandLineParseArgumentsA(args->argc, args->argv, audin_winmm_args, flags, winmm,
|
||||
nullptr, nullptr);
|
||||
arg = audin_winmm_args;
|
||||
|
||||
do
|
||||
{
|
||||
if (!(arg->Flags & COMMAND_LINE_VALUE_PRESENT))
|
||||
continue;
|
||||
|
||||
CommandLineSwitchStart(arg) CommandLineSwitchCase(arg, "dev")
|
||||
{
|
||||
winmm->device_name = _strdup(arg->Value);
|
||||
|
||||
if (!winmm->device_name)
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "_strdup failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
}
|
||||
CommandLineSwitchEnd(arg)
|
||||
} while ((arg = CommandLineFindNextArgumentA(arg)) != nullptr);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function description
|
||||
*
|
||||
* @return 0 on success, otherwise a Win32 error code
|
||||
*/
|
||||
FREERDP_ENTRY_POINT(UINT VCAPITYPE winmm_freerdp_audin_client_subsystem_entry(
|
||||
PFREERDP_AUDIN_DEVICE_ENTRY_POINTS pEntryPoints))
|
||||
{
|
||||
const ADDIN_ARGV* args;
|
||||
AudinWinmmDevice* winmm;
|
||||
UINT error;
|
||||
|
||||
if (waveInGetNumDevs() == 0)
|
||||
{
|
||||
WLog_Print(WLog_Get(TAG), WLOG_ERROR, "No microphone available!");
|
||||
return ERROR_DEVICE_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
winmm = (AudinWinmmDevice*)calloc(1, sizeof(AudinWinmmDevice));
|
||||
|
||||
if (!winmm)
|
||||
{
|
||||
WLog_ERR(TAG, "calloc failed!");
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
|
||||
winmm->log = WLog_Get(TAG);
|
||||
winmm->iface.Open = audin_winmm_open;
|
||||
winmm->iface.FormatSupported = audin_winmm_format_supported;
|
||||
winmm->iface.SetFormat = audin_winmm_set_format;
|
||||
winmm->iface.Close = audin_winmm_close;
|
||||
winmm->iface.Free = audin_winmm_free;
|
||||
winmm->rdpcontext = pEntryPoints->rdpcontext;
|
||||
args = pEntryPoints->args;
|
||||
|
||||
if ((error = audin_winmm_parse_addin_args(winmm, args)))
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR,
|
||||
"audin_winmm_parse_addin_args failed with error %" PRIu32 "!", error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if (!winmm->device_name)
|
||||
{
|
||||
winmm->device_name = _strdup("default");
|
||||
|
||||
if (!winmm->device_name)
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "_strdup failed!");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto error_out;
|
||||
}
|
||||
}
|
||||
|
||||
winmm->ppwfx_size = 10;
|
||||
winmm->ppwfx = calloc(winmm->ppwfx_size, sizeof(PWAVEFORMATEX));
|
||||
|
||||
if (!winmm->ppwfx)
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "malloc failed!");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
if ((error = pEntryPoints->pRegisterAudinDevice(pEntryPoints->plugin, &winmm->iface)))
|
||||
{
|
||||
WLog_Print(winmm->log, WLOG_ERROR, "RegisterAudinDevice failed with error %" PRIu32 "!",
|
||||
error);
|
||||
goto error_out;
|
||||
}
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
error_out:
|
||||
free(winmm->ppwfx);
|
||||
free(winmm->device_name);
|
||||
free(winmm);
|
||||
return error;
|
||||
}
|
||||
23
third_party/FreeRDP/channels/audin/server/CMakeLists.txt
vendored
Normal file
23
third_party/FreeRDP/channels/audin/server/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# FreeRDP: A Remote Desktop Protocol Implementation
|
||||
# FreeRDP cmake build script
|
||||
#
|
||||
# Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
define_channel_server("audin")
|
||||
|
||||
set(${MODULE_PREFIX}_SRCS audin.c)
|
||||
|
||||
set(${MODULE_PREFIX}_LIBS freerdp)
|
||||
add_channel_server_library(${MODULE_PREFIX} ${MODULE_NAME} ${CHANNEL_NAME} FALSE "DVCPluginEntry")
|
||||
927
third_party/FreeRDP/channels/audin/server/audin.c
vendored
Normal file
927
third_party/FreeRDP/channels/audin/server/audin.c
vendored
Normal file
@@ -0,0 +1,927 @@
|
||||
/**
|
||||
* FreeRDP: A Remote Desktop Protocol Implementation
|
||||
* Server Audio Input Virtual Channel
|
||||
*
|
||||
* Copyright 2012 Vic Lee
|
||||
* Copyright 2015 Thincast Technologies GmbH
|
||||
* Copyright 2015 DI (FH) Martin Haimberger <martin.haimberger@thincast.com>
|
||||
* Copyright 2022 Pascal Nowack <Pascal.Nowack@gmx.de>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <freerdp/config.h>
|
||||
|
||||
#include <winpr/crt.h>
|
||||
#include <winpr/assert.h>
|
||||
#include <winpr/synch.h>
|
||||
#include <winpr/thread.h>
|
||||
#include <winpr/stream.h>
|
||||
|
||||
#include <freerdp/freerdp.h>
|
||||
#include <freerdp/server/server-common.h>
|
||||
#include <freerdp/server/audin.h>
|
||||
#include <freerdp/channels/log.h>
|
||||
|
||||
#define AUDIN_TAG CHANNELS_TAG("audin.server")
|
||||
|
||||
#define SNDIN_HEADER_SIZE 1
|
||||
|
||||
typedef enum
|
||||
{
|
||||
MSG_SNDIN_VERSION = 0x01,
|
||||
MSG_SNDIN_FORMATS = 0x02,
|
||||
MSG_SNDIN_OPEN = 0x03,
|
||||
MSG_SNDIN_OPEN_REPLY = 0x04,
|
||||
MSG_SNDIN_DATA_INCOMING = 0x05,
|
||||
MSG_SNDIN_DATA = 0x06,
|
||||
MSG_SNDIN_FORMATCHANGE = 0x07,
|
||||
} MSG_SNDIN;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
audin_server_context context;
|
||||
|
||||
HANDLE stopEvent;
|
||||
|
||||
HANDLE thread;
|
||||
void* audin_channel;
|
||||
|
||||
DWORD SessionId;
|
||||
|
||||
AUDIO_FORMAT* audin_server_formats;
|
||||
UINT32 audin_n_server_formats;
|
||||
AUDIO_FORMAT* audin_negotiated_format;
|
||||
UINT32 audin_client_format_idx;
|
||||
wLog* log;
|
||||
} audin_server;
|
||||
|
||||
static UINT audin_server_recv_version(audin_server_context* context, wStream* s,
|
||||
const SNDIN_PDU* header)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
SNDIN_VERSION pdu = WINPR_C_ARRAY_INIT;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(header);
|
||||
|
||||
pdu.Header = *header;
|
||||
|
||||
if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4))
|
||||
return ERROR_NO_DATA;
|
||||
|
||||
{
|
||||
const UINT32 version = Stream_Get_UINT32(s);
|
||||
switch (version)
|
||||
{
|
||||
case SNDIN_VERSION_Version_1:
|
||||
pdu.Version = SNDIN_VERSION_Version_1;
|
||||
break;
|
||||
case SNDIN_VERSION_Version_2:
|
||||
pdu.Version = SNDIN_VERSION_Version_2;
|
||||
break;
|
||||
default:
|
||||
pdu.Version = SNDIN_VERSION_Version_2;
|
||||
WLog_Print(audin->log, WLOG_WARN,
|
||||
"Received unsupported channel version %" PRIu32
|
||||
", using highest supported version %u",
|
||||
version, pdu.Version);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
IFCALLRET(context->ReceiveVersion, error, context, &pdu);
|
||||
if (error)
|
||||
WLog_Print(audin->log, WLOG_ERROR, "context->ReceiveVersion failed with error %" PRIu32 "",
|
||||
error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT audin_server_recv_formats(audin_server_context* context, wStream* s,
|
||||
const SNDIN_PDU* header)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
SNDIN_FORMATS pdu = WINPR_C_ARRAY_INIT;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(header);
|
||||
|
||||
pdu.Header = *header;
|
||||
|
||||
/* Implementations MUST, at a minimum, support WAVE_FORMAT_PCM (0x0001) */
|
||||
if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4 + 4 + 18))
|
||||
return ERROR_NO_DATA;
|
||||
|
||||
Stream_Read_UINT32(s, pdu.NumFormats);
|
||||
Stream_Read_UINT32(s, pdu.cbSizeFormatsPacket);
|
||||
|
||||
if (pdu.NumFormats == 0)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Sound Formats PDU contains no formats");
|
||||
return ERROR_INVALID_DATA;
|
||||
}
|
||||
|
||||
pdu.SoundFormats = audio_formats_new(pdu.NumFormats);
|
||||
if (!pdu.SoundFormats)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Failed to allocate %u SoundFormats", pdu.NumFormats);
|
||||
return ERROR_NOT_ENOUGH_MEMORY;
|
||||
}
|
||||
|
||||
for (UINT32 i = 0; i < pdu.NumFormats; ++i)
|
||||
{
|
||||
AUDIO_FORMAT* format = &pdu.SoundFormats[i];
|
||||
|
||||
if (!audio_format_read(s, format))
|
||||
goto fail;
|
||||
|
||||
audio_format_print(audin->log, WLOG_DEBUG, format);
|
||||
}
|
||||
|
||||
if (pdu.cbSizeFormatsPacket != Stream_GetPosition(s))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_WARN,
|
||||
"cbSizeFormatsPacket is invalid! Expected: %u Got: %zu. Fixing size",
|
||||
pdu.cbSizeFormatsPacket, Stream_GetPosition(s));
|
||||
const size_t pos = Stream_GetPosition(s);
|
||||
if (pos > UINT32_MAX)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Stream too long, %" PRIuz " exceeds UINT32_MAX",
|
||||
pos);
|
||||
error = ERROR_INVALID_PARAMETER;
|
||||
goto fail;
|
||||
}
|
||||
pdu.cbSizeFormatsPacket = (UINT32)pos;
|
||||
}
|
||||
|
||||
pdu.ExtraDataSize = Stream_GetRemainingLength(s);
|
||||
|
||||
IFCALLRET(context->ReceiveFormats, error, context, &pdu);
|
||||
if (error)
|
||||
WLog_Print(audin->log, WLOG_ERROR, "context->ReceiveFormats failed with error %" PRIu32 "",
|
||||
error);
|
||||
|
||||
fail:
|
||||
audio_formats_free(pdu.SoundFormats, pdu.NumFormats);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT audin_server_recv_open_reply(audin_server_context* context, wStream* s,
|
||||
const SNDIN_PDU* header)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
SNDIN_OPEN_REPLY pdu = WINPR_C_ARRAY_INIT;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(header);
|
||||
|
||||
pdu.Header = *header;
|
||||
|
||||
if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4))
|
||||
return ERROR_NO_DATA;
|
||||
|
||||
Stream_Read_UINT32(s, pdu.Result);
|
||||
|
||||
IFCALLRET(context->OpenReply, error, context, &pdu);
|
||||
if (error)
|
||||
WLog_Print(audin->log, WLOG_ERROR, "context->OpenReply failed with error %" PRIu32 "",
|
||||
error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT audin_server_recv_data_incoming(audin_server_context* context,
|
||||
WINPR_ATTR_UNUSED wStream* s, const SNDIN_PDU* header)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
SNDIN_DATA_INCOMING pdu = WINPR_C_ARRAY_INIT;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(header);
|
||||
|
||||
pdu.Header = *header;
|
||||
|
||||
IFCALLRET(context->IncomingData, error, context, &pdu);
|
||||
if (error)
|
||||
WLog_Print(audin->log, WLOG_ERROR, "context->IncomingData failed with error %" PRIu32 "",
|
||||
error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT audin_server_recv_data(audin_server_context* context, wStream* s,
|
||||
const SNDIN_PDU* header)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
SNDIN_DATA pdu = WINPR_C_ARRAY_INIT;
|
||||
wStream dataBuffer = WINPR_C_ARRAY_INIT;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(header);
|
||||
|
||||
pdu.Header = *header;
|
||||
|
||||
pdu.Data = Stream_StaticInit(&dataBuffer, Stream_Pointer(s), Stream_GetRemainingLength(s));
|
||||
|
||||
IFCALLRET(context->Data, error, context, &pdu);
|
||||
if (error)
|
||||
WLog_Print(audin->log, WLOG_ERROR, "context->Data failed with error %" PRIu32 "", error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT audin_server_recv_format_change(audin_server_context* context, wStream* s,
|
||||
const SNDIN_PDU* header)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
SNDIN_FORMATCHANGE pdu = WINPR_C_ARRAY_INIT;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(header);
|
||||
|
||||
pdu.Header = *header;
|
||||
|
||||
if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, 4))
|
||||
return ERROR_NO_DATA;
|
||||
|
||||
Stream_Read_UINT32(s, pdu.NewFormat);
|
||||
|
||||
IFCALLRET(context->ReceiveFormatChange, error, context, &pdu);
|
||||
if (error)
|
||||
WLog_Print(audin->log, WLOG_ERROR,
|
||||
"context->ReceiveFormatChange failed with error %" PRIu32 "", error);
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
static DWORD WINAPI audin_server_thread_func(LPVOID arg)
|
||||
{
|
||||
wStream* s = nullptr;
|
||||
void* buffer = nullptr;
|
||||
DWORD nCount = 0;
|
||||
HANDLE events[8] = WINPR_C_ARRAY_INIT;
|
||||
BOOL ready = FALSE;
|
||||
HANDLE ChannelEvent = nullptr;
|
||||
DWORD BytesReturned = 0;
|
||||
audin_server* audin = (audin_server*)arg;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
DWORD status = ERROR_INTERNAL_ERROR;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
|
||||
if (WTSVirtualChannelQuery(audin->audin_channel, WTSVirtualEventHandle, &buffer,
|
||||
&BytesReturned) == TRUE)
|
||||
{
|
||||
if (BytesReturned == sizeof(HANDLE))
|
||||
ChannelEvent = *(HANDLE*)buffer;
|
||||
|
||||
WTSFreeMemory(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelQuery failed");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto out;
|
||||
}
|
||||
|
||||
nCount = 0;
|
||||
events[nCount++] = audin->stopEvent;
|
||||
events[nCount++] = ChannelEvent;
|
||||
|
||||
/* Wait for the client to confirm that the Audio Input dynamic channel is ready */
|
||||
|
||||
while (1)
|
||||
{
|
||||
status = WaitForMultipleObjects(nCount, events, FALSE, 100);
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_Print(audin->log, WLOG_ERROR,
|
||||
"WaitForMultipleObjects failed with error %" PRIu32 "", error);
|
||||
goto out;
|
||||
}
|
||||
if (status == WAIT_OBJECT_0)
|
||||
goto out;
|
||||
|
||||
if (WTSVirtualChannelQuery(audin->audin_channel, WTSVirtualChannelReady, &buffer,
|
||||
&BytesReturned) == FALSE)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelQuery failed");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto out;
|
||||
}
|
||||
|
||||
ready = *((BOOL*)buffer);
|
||||
WTSFreeMemory(buffer);
|
||||
|
||||
if (ready)
|
||||
break;
|
||||
}
|
||||
|
||||
s = Stream_New(nullptr, 4096);
|
||||
|
||||
if (!s)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Stream_New failed!");
|
||||
error = CHANNEL_RC_NO_MEMORY;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (ready)
|
||||
{
|
||||
SNDIN_VERSION version = WINPR_C_ARRAY_INIT;
|
||||
|
||||
version.Version = audin->context.serverVersion;
|
||||
|
||||
if ((error = audin->context.SendVersion(&audin->context, &version)))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "SendVersion failed with error %" PRIu32 "!", error);
|
||||
goto out_capacity;
|
||||
}
|
||||
}
|
||||
|
||||
while (ready)
|
||||
{
|
||||
SNDIN_PDU header = WINPR_C_ARRAY_INIT;
|
||||
|
||||
if ((status = WaitForMultipleObjects(nCount, events, FALSE, INFINITE)) == WAIT_OBJECT_0)
|
||||
break;
|
||||
|
||||
if (status == WAIT_FAILED)
|
||||
{
|
||||
error = GetLastError();
|
||||
WLog_Print(audin->log, WLOG_ERROR,
|
||||
"WaitForMultipleObjects failed with error %" PRIu32 "", error);
|
||||
break;
|
||||
}
|
||||
if (status == WAIT_OBJECT_0)
|
||||
break;
|
||||
|
||||
Stream_ResetPosition(s);
|
||||
|
||||
if (!WTSVirtualChannelRead(audin->audin_channel, 0, nullptr, 0, &BytesReturned))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelRead failed!");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
if (BytesReturned < 1)
|
||||
continue;
|
||||
|
||||
if (!Stream_EnsureRemainingCapacity(s, BytesReturned))
|
||||
break;
|
||||
|
||||
WINPR_ASSERT(Stream_Capacity(s) <= UINT32_MAX);
|
||||
if (WTSVirtualChannelRead(audin->audin_channel, 0, Stream_BufferAs(s, char),
|
||||
(ULONG)Stream_Capacity(s), &BytesReturned) == FALSE)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelRead failed!");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
Stream_SetLength(s, BytesReturned);
|
||||
if (!Stream_CheckAndLogRequiredLengthWLog(audin->log, s, SNDIN_HEADER_SIZE))
|
||||
{
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
Stream_Read_UINT8(s, header.MessageId);
|
||||
|
||||
switch (header.MessageId)
|
||||
{
|
||||
case MSG_SNDIN_VERSION:
|
||||
error = audin_server_recv_version(&audin->context, s, &header);
|
||||
break;
|
||||
case MSG_SNDIN_FORMATS:
|
||||
error = audin_server_recv_formats(&audin->context, s, &header);
|
||||
break;
|
||||
case MSG_SNDIN_OPEN_REPLY:
|
||||
error = audin_server_recv_open_reply(&audin->context, s, &header);
|
||||
break;
|
||||
case MSG_SNDIN_DATA_INCOMING:
|
||||
error = audin_server_recv_data_incoming(&audin->context, s, &header);
|
||||
break;
|
||||
case MSG_SNDIN_DATA:
|
||||
error = audin_server_recv_data(&audin->context, s, &header);
|
||||
break;
|
||||
case MSG_SNDIN_FORMATCHANGE:
|
||||
error = audin_server_recv_format_change(&audin->context, s, &header);
|
||||
break;
|
||||
default:
|
||||
WLog_Print(audin->log, WLOG_ERROR,
|
||||
"audin_server_thread_func: unknown or invalid MessageId %" PRIu8 "",
|
||||
header.MessageId);
|
||||
error = ERROR_INVALID_DATA;
|
||||
break;
|
||||
}
|
||||
if (error)
|
||||
break;
|
||||
}
|
||||
|
||||
out_capacity:
|
||||
Stream_Free(s, TRUE);
|
||||
out:
|
||||
(void)WTSVirtualChannelClose(audin->audin_channel);
|
||||
audin->audin_channel = nullptr;
|
||||
|
||||
if (error && audin->context.rdpcontext)
|
||||
setChannelError(audin->context.rdpcontext, error,
|
||||
"audin_server_thread_func reported an error");
|
||||
|
||||
ExitThread(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
static BOOL audin_server_open(audin_server_context* context)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
if (!audin->thread)
|
||||
{
|
||||
PULONG pSessionId = nullptr;
|
||||
DWORD BytesReturned = 0;
|
||||
audin->SessionId = WTS_CURRENT_SESSION;
|
||||
UINT32 channelId = 0;
|
||||
BOOL status = TRUE;
|
||||
|
||||
if (WTSQuerySessionInformationA(context->vcm, WTS_CURRENT_SESSION, WTSSessionId,
|
||||
(LPSTR*)&pSessionId, &BytesReturned))
|
||||
{
|
||||
audin->SessionId = (DWORD)*pSessionId;
|
||||
WTSFreeMemory(pSessionId);
|
||||
}
|
||||
|
||||
audin->audin_channel = WTSVirtualChannelOpenEx(audin->SessionId, AUDIN_DVC_CHANNEL_NAME,
|
||||
WTS_CHANNEL_OPTION_DYNAMIC);
|
||||
|
||||
if (!audin->audin_channel)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelOpenEx failed!");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
channelId = WTSChannelGetIdByHandle(audin->audin_channel);
|
||||
|
||||
IFCALLRET(context->ChannelIdAssigned, status, context, channelId);
|
||||
if (!status)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "context->ChannelIdAssigned failed!");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!(audin->stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr)))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "CreateEvent failed!");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!(audin->thread =
|
||||
CreateThread(nullptr, 0, audin_server_thread_func, (void*)audin, 0, nullptr)))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "CreateThread failed!");
|
||||
(void)CloseHandle(audin->stopEvent);
|
||||
audin->stopEvent = nullptr;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
WLog_Print(audin->log, WLOG_ERROR, "thread already running!");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static BOOL audin_server_is_open(audin_server_context* context)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
return audin->thread != nullptr;
|
||||
}
|
||||
|
||||
static BOOL audin_server_close(audin_server_context* context)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
|
||||
if (audin->thread)
|
||||
{
|
||||
(void)SetEvent(audin->stopEvent);
|
||||
|
||||
if (WaitForSingleObject(audin->thread, INFINITE) == WAIT_FAILED)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WaitForSingleObject failed with error %" PRIu32 "",
|
||||
GetLastError());
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
(void)CloseHandle(audin->thread);
|
||||
(void)CloseHandle(audin->stopEvent);
|
||||
audin->thread = nullptr;
|
||||
audin->stopEvent = nullptr;
|
||||
}
|
||||
|
||||
if (audin->audin_channel)
|
||||
{
|
||||
(void)WTSVirtualChannelClose(audin->audin_channel);
|
||||
audin->audin_channel = nullptr;
|
||||
}
|
||||
|
||||
audin->audin_negotiated_format = nullptr;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static wStream* audin_server_packet_new(wLog* log, size_t size, BYTE MessageId)
|
||||
{
|
||||
WINPR_ASSERT(log);
|
||||
|
||||
/* Allocate what we need plus header bytes */
|
||||
wStream* s = Stream_New(nullptr, size + SNDIN_HEADER_SIZE);
|
||||
if (!s)
|
||||
{
|
||||
WLog_Print(log, WLOG_ERROR, "Stream_New failed!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Stream_Write_UINT8(s, MessageId);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
static UINT audin_server_packet_send(audin_server_context* context, wStream* s)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
UINT error = CHANNEL_RC_OK;
|
||||
ULONG written = 0;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(s);
|
||||
|
||||
const size_t pos = Stream_GetPosition(s);
|
||||
WINPR_ASSERT(pos <= UINT32_MAX);
|
||||
if (!WTSVirtualChannelWrite(audin->audin_channel, Stream_BufferAs(s, char), (UINT32)pos,
|
||||
&written))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "WTSVirtualChannelWrite failed!");
|
||||
error = ERROR_INTERNAL_ERROR;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (written < Stream_GetPosition(s))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_WARN, "Unexpected bytes written: %" PRIu32 "/%" PRIuz "",
|
||||
written, Stream_GetPosition(s));
|
||||
}
|
||||
|
||||
out:
|
||||
Stream_Free(s, TRUE);
|
||||
return error;
|
||||
}
|
||||
|
||||
static UINT audin_server_send_version(audin_server_context* context, const SNDIN_VERSION* version)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(version);
|
||||
|
||||
wStream* s = audin_server_packet_new(audin->log, 4, MSG_SNDIN_VERSION);
|
||||
if (!s)
|
||||
return ERROR_NOT_ENOUGH_MEMORY;
|
||||
|
||||
Stream_Write_UINT32(s, version->Version);
|
||||
|
||||
return audin_server_packet_send(context, s);
|
||||
}
|
||||
|
||||
static UINT audin_server_send_formats(audin_server_context* context, const SNDIN_FORMATS* formats)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(formats);
|
||||
|
||||
wStream* s = audin_server_packet_new(audin->log, 4 + 4 + 18, MSG_SNDIN_FORMATS);
|
||||
if (!s)
|
||||
return ERROR_NOT_ENOUGH_MEMORY;
|
||||
|
||||
Stream_Write_UINT32(s, formats->NumFormats);
|
||||
Stream_Write_UINT32(s, formats->cbSizeFormatsPacket);
|
||||
|
||||
for (UINT32 i = 0; i < formats->NumFormats; ++i)
|
||||
{
|
||||
AUDIO_FORMAT* format = &formats->SoundFormats[i];
|
||||
|
||||
if (!audio_format_write(s, format))
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Failed to write audio format");
|
||||
Stream_Free(s, TRUE);
|
||||
return CHANNEL_RC_NO_MEMORY;
|
||||
}
|
||||
}
|
||||
|
||||
return audin_server_packet_send(context, s);
|
||||
}
|
||||
|
||||
static UINT audin_server_send_open(audin_server_context* context, const SNDIN_OPEN* open)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(open);
|
||||
|
||||
wStream* s = audin_server_packet_new(audin->log, 4 + 4 + 18 + 22, MSG_SNDIN_OPEN);
|
||||
if (!s)
|
||||
return ERROR_NOT_ENOUGH_MEMORY;
|
||||
|
||||
Stream_Write_UINT32(s, open->FramesPerPacket);
|
||||
Stream_Write_UINT32(s, open->initialFormat);
|
||||
|
||||
Stream_Write_UINT16(s, open->captureFormat.wFormatTag);
|
||||
Stream_Write_UINT16(s, open->captureFormat.nChannels);
|
||||
Stream_Write_UINT32(s, open->captureFormat.nSamplesPerSec);
|
||||
Stream_Write_UINT32(s, open->captureFormat.nAvgBytesPerSec);
|
||||
Stream_Write_UINT16(s, open->captureFormat.nBlockAlign);
|
||||
Stream_Write_UINT16(s, open->captureFormat.wBitsPerSample);
|
||||
|
||||
if (open->ExtraFormatData)
|
||||
{
|
||||
Stream_Write_UINT16(s, 22); /* cbSize */
|
||||
|
||||
Stream_Write_UINT16(s, open->ExtraFormatData->Samples.wReserved);
|
||||
Stream_Write_UINT32(s, open->ExtraFormatData->dwChannelMask);
|
||||
|
||||
Stream_Write_UINT32(s, open->ExtraFormatData->SubFormat.Data1);
|
||||
Stream_Write_UINT16(s, open->ExtraFormatData->SubFormat.Data2);
|
||||
Stream_Write_UINT16(s, open->ExtraFormatData->SubFormat.Data3);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[0]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[1]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[2]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[3]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[4]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[5]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[6]);
|
||||
Stream_Write_UINT8(s, open->ExtraFormatData->SubFormat.Data4[7]);
|
||||
}
|
||||
else
|
||||
{
|
||||
WINPR_ASSERT(open->captureFormat.wFormatTag != WAVE_FORMAT_EXTENSIBLE);
|
||||
|
||||
Stream_Write_UINT16(s, 0); /* cbSize */
|
||||
}
|
||||
|
||||
return audin_server_packet_send(context, s);
|
||||
}
|
||||
|
||||
static UINT audin_server_send_format_change(audin_server_context* context,
|
||||
const SNDIN_FORMATCHANGE* format_change)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
WINPR_ASSERT(context);
|
||||
WINPR_ASSERT(format_change);
|
||||
|
||||
wStream* s = audin_server_packet_new(audin->log, 4, MSG_SNDIN_FORMATCHANGE);
|
||||
if (!s)
|
||||
return ERROR_NOT_ENOUGH_MEMORY;
|
||||
|
||||
Stream_Write_UINT32(s, format_change->NewFormat);
|
||||
|
||||
return audin_server_packet_send(context, s);
|
||||
}
|
||||
|
||||
static UINT audin_server_receive_version_default(audin_server_context* audin_ctx,
|
||||
const SNDIN_VERSION* version)
|
||||
{
|
||||
audin_server* audin = (audin_server*)audin_ctx;
|
||||
SNDIN_FORMATS formats = WINPR_C_ARRAY_INIT;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(version);
|
||||
|
||||
if (version->Version == 0)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Received invalid AUDIO_INPUT version from client");
|
||||
return ERROR_INVALID_DATA;
|
||||
}
|
||||
|
||||
WLog_Print(audin->log, WLOG_DEBUG, "AUDIO_INPUT version of client: %u", version->Version);
|
||||
|
||||
formats.NumFormats = audin->audin_n_server_formats;
|
||||
formats.SoundFormats = audin->audin_server_formats;
|
||||
|
||||
return audin->context.SendFormats(&audin->context, &formats);
|
||||
}
|
||||
|
||||
static UINT send_open(audin_server* audin)
|
||||
{
|
||||
SNDIN_OPEN open = WINPR_C_ARRAY_INIT;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
|
||||
open.FramesPerPacket = 441;
|
||||
open.initialFormat = audin->audin_client_format_idx;
|
||||
open.captureFormat.wFormatTag = WAVE_FORMAT_PCM;
|
||||
open.captureFormat.nChannels = 2;
|
||||
open.captureFormat.nSamplesPerSec = 44100;
|
||||
open.captureFormat.nAvgBytesPerSec = 44100 * 2 * 2;
|
||||
open.captureFormat.nBlockAlign = 4;
|
||||
open.captureFormat.wBitsPerSample = 16;
|
||||
|
||||
WINPR_ASSERT(audin->context.SendOpen);
|
||||
return audin->context.SendOpen(&audin->context, &open);
|
||||
}
|
||||
|
||||
static UINT audin_server_receive_formats_default(audin_server_context* context,
|
||||
const SNDIN_FORMATS* formats)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(formats);
|
||||
|
||||
if (audin->audin_negotiated_format)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR,
|
||||
"Received client formats, but negotiation was already done");
|
||||
return ERROR_INVALID_DATA;
|
||||
}
|
||||
|
||||
for (UINT32 i = 0; i < audin->audin_n_server_formats; ++i)
|
||||
{
|
||||
for (UINT32 j = 0; j < formats->NumFormats; ++j)
|
||||
{
|
||||
if (audio_format_compatible(&audin->audin_server_formats[i], &formats->SoundFormats[j]))
|
||||
{
|
||||
audin->audin_negotiated_format = &audin->audin_server_formats[i];
|
||||
audin->audin_client_format_idx = i;
|
||||
return send_open(audin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WLog_Print(audin->log, WLOG_ERROR, "Could not agree on a audio format with the server");
|
||||
|
||||
return ERROR_INVALID_DATA;
|
||||
}
|
||||
|
||||
static UINT audin_server_receive_format_change_default(audin_server_context* context,
|
||||
const SNDIN_FORMATCHANGE* format_change)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(format_change);
|
||||
|
||||
if (format_change->NewFormat != audin->audin_client_format_idx)
|
||||
{
|
||||
WLog_Print(audin->log, WLOG_ERROR,
|
||||
"NewFormat in FormatChange differs from requested format");
|
||||
return ERROR_INVALID_DATA;
|
||||
}
|
||||
|
||||
WLog_Print(audin->log, WLOG_DEBUG, "Received Format Change PDU: %u", format_change->NewFormat);
|
||||
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static UINT
|
||||
audin_server_incoming_data_default(audin_server_context* context,
|
||||
WINPR_ATTR_UNUSED const SNDIN_DATA_INCOMING* data_incoming)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(data_incoming);
|
||||
|
||||
/* TODO: Implement bandwidth measure of clients uplink */
|
||||
WLog_Print(audin->log, WLOG_DEBUG, "Received Incoming Data PDU");
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
static UINT audin_server_open_reply_default(audin_server_context* context,
|
||||
const SNDIN_OPEN_REPLY* open_reply)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
WINPR_ASSERT(open_reply);
|
||||
|
||||
/* TODO: Implement failure handling */
|
||||
WLog_Print(audin->log, WLOG_DEBUG, "Open Reply PDU: Result: %" PRIu32, open_reply->Result);
|
||||
return CHANNEL_RC_OK;
|
||||
}
|
||||
|
||||
audin_server_context* audin_server_context_new(HANDLE vcm)
|
||||
{
|
||||
audin_server* audin = (audin_server*)calloc(1, sizeof(audin_server));
|
||||
|
||||
if (!audin)
|
||||
{
|
||||
WLog_ERR(AUDIN_TAG, "calloc failed!");
|
||||
return nullptr;
|
||||
}
|
||||
audin->log = WLog_Get(AUDIN_TAG);
|
||||
audin->context.vcm = vcm;
|
||||
audin->context.Open = audin_server_open;
|
||||
audin->context.IsOpen = audin_server_is_open;
|
||||
audin->context.Close = audin_server_close;
|
||||
|
||||
audin->context.SendVersion = audin_server_send_version;
|
||||
audin->context.SendFormats = audin_server_send_formats;
|
||||
audin->context.SendOpen = audin_server_send_open;
|
||||
audin->context.SendFormatChange = audin_server_send_format_change;
|
||||
|
||||
/* Default values */
|
||||
audin->context.serverVersion = SNDIN_VERSION_Version_2;
|
||||
audin->context.ReceiveVersion = audin_server_receive_version_default;
|
||||
audin->context.ReceiveFormats = audin_server_receive_formats_default;
|
||||
audin->context.ReceiveFormatChange = audin_server_receive_format_change_default;
|
||||
audin->context.IncomingData = audin_server_incoming_data_default;
|
||||
audin->context.OpenReply = audin_server_open_reply_default;
|
||||
|
||||
return &audin->context;
|
||||
}
|
||||
|
||||
void audin_server_context_free(audin_server_context* context)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
|
||||
if (!audin)
|
||||
return;
|
||||
|
||||
audin_server_close(context);
|
||||
audio_formats_free(audin->audin_server_formats, audin->audin_n_server_formats);
|
||||
audin->audin_server_formats = nullptr;
|
||||
free(audin);
|
||||
}
|
||||
|
||||
BOOL audin_server_set_formats(audin_server_context* context, SSIZE_T count,
|
||||
const AUDIO_FORMAT* formats)
|
||||
{
|
||||
audin_server* audin = (audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
|
||||
audio_formats_free(audin->audin_server_formats, audin->audin_n_server_formats);
|
||||
audin->audin_n_server_formats = 0;
|
||||
audin->audin_server_formats = nullptr;
|
||||
audin->audin_negotiated_format = nullptr;
|
||||
|
||||
if (count < 0)
|
||||
{
|
||||
const size_t audin_n_server_formats =
|
||||
server_audin_get_formats(&audin->audin_server_formats);
|
||||
WINPR_ASSERT(audin_n_server_formats <= UINT32_MAX);
|
||||
|
||||
audin->audin_n_server_formats = (UINT32)audin_n_server_formats;
|
||||
}
|
||||
else
|
||||
{
|
||||
const size_t scount = (size_t)count;
|
||||
AUDIO_FORMAT* audin_server_formats = audio_formats_new(scount);
|
||||
if (!audin_server_formats)
|
||||
return count == 0;
|
||||
|
||||
for (SSIZE_T x = 0; x < count; x++)
|
||||
{
|
||||
if (!audio_format_copy(&formats[x], &audin_server_formats[x]))
|
||||
{
|
||||
audio_formats_free(audin_server_formats, scount);
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
WINPR_ASSERT(count <= UINT32_MAX);
|
||||
audin->audin_server_formats = audin_server_formats;
|
||||
audin->audin_n_server_formats = (UINT32)count;
|
||||
}
|
||||
return audin->audin_n_server_formats > 0;
|
||||
}
|
||||
|
||||
const AUDIO_FORMAT* audin_server_get_negotiated_format(const audin_server_context* context)
|
||||
{
|
||||
const audin_server* audin = (const audin_server*)context;
|
||||
WINPR_ASSERT(audin);
|
||||
|
||||
return audin->audin_negotiated_format;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user