Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SpeakApi & refactor named pipe messages to use a binary format. #9

Merged
merged 2 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
extension String.SubSequence {
public extension String.SubSequence {
func trimmed() -> String {
return String(self).trimmed()
}
}

public extension String {
func trimmed() -> String {
return String(
self.drop(while: { $0.isWhitespace })
Expand Down
10 changes: 9 additions & 1 deletion windows/Sources/Runtime/Runtime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public func setCursorPos(x: Int32, y: Int32) {
sendMessage(.setCursorPos(Pos(x: x, y: y)))
}

public func speak(text: String, flags: UInt32) {
sendMessage(.speak(Speak(text: text, flags: flags)))
}

public func speakSkip() {
sendMessage(.speakSkip)
}

public func processDetach() {
// Disconnect and close the pipe client
pipeClient = nil
Expand All @@ -58,7 +66,7 @@ func sendMessage(_ message: PipeMessages) {
}

do {
try pipeClient.send(message.toString())
try pipeClient.sendBytes(message.toBytes())
} catch {
fatalError("Failed to send pipe message")
}
Expand Down
41 changes: 38 additions & 3 deletions windows/Sources/Sandbox/SandboxNamedPipeServer.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import Shared
import WinSDK
import WindowsUtils
import WinSDKExtras
import CxxStdlib

// A named pipe server that listens for messages from the sandbox
// and performs the requested privileged operations

public class SandboxNamedPipeServer: NamedPipeServer {
private var speech: Speech? = nil

public override init(pipeName: String) throws {
try super.init(pipeName: pipeName)
}

public override func onMessage(_ message: String) -> Bool {
let message = PipeMessages.fromString(message)
public override func onMessage(_ data: [UInt16]) -> Bool {
let message = PipeMessages.fromBytes(data)
guard let message = message else {
print("Unknown pipe message")
print("Failed to parse message")
return true
}

Expand All @@ -34,7 +38,38 @@ public class SandboxNamedPipeServer: NamedPipeServer {
}
case .setCursorPos(let pos):
SetCursorPos(pos.x, pos.y)
case .speak(let speak):
if speech == nil {
speech = Speech()
}
speech!.Speak(speak.text, speak.flags)
case .speakSkip:
if speech == nil {
speech = Speech()
}
speech!.Skip()
}
return false
}
}

private class Speech {
var speakApi: SpeakApi

init() {
CoInitializeEx(nil, 0)
speakApi = SpeakApi()
}

deinit {
CoUninitialize()
}

func Speak(_ text: String, _ flags: UInt32) {
speakApi.Speak(std.string(text), flags)
}

func Skip() {
speakApi.Skip()
}
}
210 changes: 185 additions & 25 deletions windows/Sources/Shared/PipeMessages.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import WinSDK

public struct Pos {
public var x: Int32
public var y: Int32
Expand All @@ -22,53 +24,211 @@ public struct Rect {
}
}

public struct Speak {
public var text: String
public var flags: UInt32

public init(text: String, flags: UInt32) {
self.text = text
self.flags = flags
}
}

public enum PipeMessages {
case exit
case clipCursor(Rect)
case setCursorPos(Pos)
case speak(Speak)
case speakSkip

// Convert the message from a cvs string
public static func fromString(_ message: String) -> PipeMessages? {
let csv = message.split(separator: ",")
guard csv.count > 1 else {
private var rawValue: UInt8 {
switch self {
case .exit: return 0
case .clipCursor: return 1
case .setCursorPos: return 2
case .speak: return 3
case .speakSkip: return 4
}
}

// Convert the message from a byte array
public static func fromBytes(_ bytes: [UInt16]) -> PipeMessages? {
guard bytes.count >= 1 else {
return nil
}

switch csv[0] {
case "exit":
let buffer = ByteBuffer(data: bytes)
let type = buffer.readUInt8()!

switch type {
case 0:
return .exit
case "clipCursor":
guard csv.count == 5,
let left = Int32(csv[1]),
let top = Int32(csv[2]),
let right = Int32(csv[3]),
let bottom = Int32(csv[4])
else {
case 1:
let left = buffer.readInt32()
let top = buffer.readInt32()
let right = buffer.readInt32()
let bottom = buffer.readInt32()
guard let left = left, let top = top, let right = right, let bottom = bottom else {
return nil
}
return .clipCursor(Rect(
left: left,
top: top,
right: right,
bottom: bottom
))
case 2:
let x = buffer.readInt32()
let y = buffer.readInt32()
guard let x = x, let y = y else {
return nil
}
return .clipCursor(Rect(left: left, top: top, right: right, bottom: bottom))
case "setCursorPos":
guard csv.count == 3,
let x = Int32(csv[1]),
let y = Int32(csv[2])
else {
return .setCursorPos(Pos(
x: x,
y: y
))
case 3:
let text = buffer.readString()
let flags = buffer.readUInt32()
guard let text = text, let flags = flags else {
return nil
}
return .setCursorPos(Pos(x: x, y: y))
return .speak(Speak(
text: text,
flags: flags
))
case 4:
return .speakSkip
default:
return nil
}
}

// Convert the message to a csv string
public func toString() -> String {
// Convert the message to a byte array
// The first byte is the message type, the rest is the message data
public func toBytes() -> [UInt16] {
let buffer = ByteBuffer()
buffer.appendUInt8(rawValue)

switch self {
case .exit:
return "exit"
break
case .clipCursor(let rect):
return "clipCursor,\(rect.left),\(rect.top),\(rect.right),\(rect.bottom)"
buffer.appendInt32(rect.left)
buffer.appendInt32(rect.top)
buffer.appendInt32(rect.right)
buffer.appendInt32(rect.bottom)
case .setCursorPos(let pos):
return "setCursorPos,\(pos.x),\(pos.y)"
buffer.appendInt32(pos.x)
buffer.appendInt32(pos.y)
case .speak(let speak):
buffer.appendString(speak.text)
buffer.appendUInt32(speak.flags)
case .speakSkip:
break
}
return buffer.data
}
}

private class ByteBuffer {
var data: [UInt16]

init() {
data = []
}

init(data: [UInt16]) {
self.data = data
}

var size : Int {
return data.count
}

func appendUInt8(_ value: UInt8) {
data.append(UInt16(value))
}

func appendUInt(_ value: UInt) {
appendUInt8(UInt8(value & 0xFF))
appendUInt8(UInt8((value >> 8) & 0xFF))
}

func appendInt(_ value: Int) {
appendUInt(UInt(bitPattern: value))
}

func appendUInt32(_ value: UInt32) {
appendUInt8(UInt8(value & 0xFF))
appendUInt8(UInt8((value >> 8) & 0xFF))
appendUInt8(UInt8((value >> 16) & 0xFF))
appendUInt8(UInt8((value >> 24) & 0xFF))
}

func appendInt32(_ value: Int32) {
appendUInt32(UInt32(bitPattern: value))
}

func appendString(_ string: String) {
appendInt(string.utf8.count)
data.append(contentsOf: string.utf16)
}

func readUInt8() -> UInt8? {
guard !data.isEmpty else {
return nil
}
let value = data[0]
data.removeFirst()
return UInt8(value)
}

func readUInt() -> UInt? {
let one = readUInt8()
let two = readUInt8()
guard let one = one, let two = two else {
return nil
}
return UInt(one) | UInt(two) << 8
}

func readInt() -> Int? {
guard let value = readUInt() else {
return nil
}
return Int(bitPattern: value)
}

func readUInt32() -> UInt32? {
let one = readUInt8()
let two = readUInt8()
let three = readUInt8()
let four = readUInt8()
guard let one = one, let two = two, let three = three, let four = four else {
return nil
}
return UInt32(one) | UInt32(two) << 8 | UInt32(three) << 16 | UInt32(four) << 24
}

func readInt32() -> Int32? {
guard let value = readUInt32() else {
return nil
}
return Int32(bitPattern: value)
}

func readString() -> String? {
guard let length = readInt() else {
return nil
}

guard data.count >= length else {
return nil
}

let string = String(decoding: data[0..<Int(length)], as: UTF16.self)
data.removeFirst(Int(length))
return string
}
}
36 changes: 36 additions & 0 deletions windows/Sources/WinSDKExtras/SpeakApi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "SpeakApi.h"

#include <atlbase.h>
#include <atlconv.h>
#include <sapi.h>
#include <stdexcept>

class SpeakApi::Impl {
public:
Impl() {
auto result = spVoice.CoCreateInstance(CLSID_SpVoice);
if (!SUCCEEDED(result)) {
throw std::runtime_error("Failed to create ISpVoice instance");
}
}

CComPtr<ISpVoice> spVoice;
};

SpeakApi::SpeakApi(): pImpl{std::make_shared<Impl>()} {
}

SpeakApi::~SpeakApi() {
}

SpeakApi::SpeakApi(const SpeakApi& other): pImpl{other.pImpl} {
}

HRESULT SpeakApi::Speak(std::string text, DWORD dwFlags) {
CA2W textw(text.c_str());
return pImpl->spVoice->Speak(textw, dwFlags, NULL);
}

HRESULT SpeakApi::Skip() {
return pImpl->spVoice->Skip(L"Sentence", 0x7fffffff, NULL);
}
Loading
Loading