Skip to content
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
31 changes: 29 additions & 2 deletions App/InjectionNext.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
BBDD84542C4FEB16000F3124 /* TupleRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBDD84532C4FEB16000F3124 /* TupleRegex.swift */; };
BBDD84552C4FEC98000F3124 /* SimpleSocket.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB67DBB31FB0CDA8000EAC8A /* SimpleSocket.mm */; };
BBDD845A2C4FF646000F3124 /* ClientBoot.mm in Sources */ = {isa = PBXBuildFile; fileRef = BBDD84592C4FF645000F3124 /* ClientBoot.mm */; };
BBF00D012FBCE00100F0D001 /* InjectionTouchEvents.mm in Sources */ = {isa = PBXBuildFile; fileRef = BBF00D002FBCE00100F0D001 /* InjectionTouchEvents.mm */; };
BBEB87122C74FD930044578A /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = BBEB87112C74FD930044578A /* Quick */; };
BBF6572E2DAFF00200638170 /* NIMBLE.md in Resources */ = {isa = PBXBuildFile; fileRef = BBF6572C2DAFF00200638170 /* NIMBLE.md */; };
BBF6572F2DAFF00200638170 /* QUICK.md in Resources */ = {isa = PBXBuildFile; fileRef = BBF6572D2DAFF00200638170 /* QUICK.md */; };
Expand Down Expand Up @@ -255,8 +256,16 @@
BB5C8E8E2FA503D500646A0C /* UpdateChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateChecker.swift; sourceTree = "<group>"; };
BB67DBB21FB0CDA8000EAC8A /* SimpleSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SimpleSocket.h; path = ../../Sources/InjectionNextC/include/SimpleSocket.h; sourceTree = "<group>"; };
BB67DBB31FB0CDA8000EAC8A /* SimpleSocket.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SimpleSocket.mm; path = ../../Sources/InjectionNextC/SimpleSocket.mm; sourceTree = "<group>"; };
BBF00CFF2FBCE00100F0D001 /* InjectionTouchEvents.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = InjectionTouchEvents.h; path = ../../Sources/InjectionNextC/include/InjectionTouchEvents.h; sourceTree = "<group>"; };
BBF00D002FBCE00100F0D001 /* InjectionTouchEvents.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = InjectionTouchEvents.mm; path = ../../Sources/InjectionNextC/InjectionTouchEvents.mm; sourceTree = "<group>"; };
BB6A56EF2C50E73600C92112 /* ConfigStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigStore.swift; sourceTree = "<group>"; };
BB6A56F22C51154F00C92112 /* InjectionBundle-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "InjectionBundle-Bridging-Header.h"; sourceTree = "<group>"; };
BB7AC1522FBCEDEF009269AF /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
BB7AC1532FBCEDEF009269AF /* index.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = index.js; sourceTree = "<group>"; };
BB7AC1542FBCEDEF009269AF /* package.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = package.json; sourceTree = "<group>"; };
BB7AC1552FBCEDEF009269AF /* package-lock.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "package-lock.json"; sourceTree = "<group>"; };
BB7AC1562FBCEDEF009269AF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
BB7AC1572FBCEDEF009269AF /* take-screenshot.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "take-screenshot.js"; sourceTree = "<group>"; };
BB85A4242D6B9C5300FA29D0 /* feedcommands */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = feedcommands; sourceTree = BUILT_PRODUCTS_DIR; };
BBA082352D6BC32D00FFFB2F /* FrontendServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrontendServer.swift; sourceTree = "<group>"; };
BBA082372D6BD6CB00FFFB2F /* swift-frontend.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "swift-frontend.sh"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -456,6 +465,7 @@
BBCA01FF1FB0F10300E45F0F /* InjectionNext */,
BBDD84172C4FE79E000F3124 /* InjectionBundle */,
BB85A4252D6B9C5300FA29D0 /* feedcommands */,
BB7AC1582FBCEDEF009269AF /* mcp-server */,
BB439B661FABA64300B4F50B /* Products */,
BBB040621FB1798A007DDD0A /* Frameworks */,
);
Expand Down Expand Up @@ -519,6 +529,20 @@
path = ../SwiftTrace/SwiftTraceGuts;
sourceTree = SOURCE_ROOT;
};
BB7AC1582FBCEDEF009269AF /* mcp-server */ = {
isa = PBXGroup;
children = (
BB7AC1522FBCEDEF009269AF /* .gitignore */,
BB7AC1532FBCEDEF009269AF /* index.js */,
BB7AC1542FBCEDEF009269AF /* package.json */,
BB7AC1552FBCEDEF009269AF /* package-lock.json */,
BB7AC1562FBCEDEF009269AF /* README.md */,
BB7AC1572FBCEDEF009269AF /* take-screenshot.js */,
);
name = "mcp-server";
path = "../mcp-server";
sourceTree = SOURCE_ROOT;
};
BBB040621FB1798A007DDD0A /* Frameworks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -567,6 +591,8 @@
isa = PBXGroup;
children = (
BB42F9302EB63AAF00FDBBCC /* InjectionImplC.h */,
BBF00CFF2FBCE00100F0D001 /* InjectionTouchEvents.h */,
BBF00D002FBCE00100F0D001 /* InjectionTouchEvents.mm */,
BBDD84592C4FF645000F3124 /* ClientBoot.mm */,
BBDD843A2C4FE892000F3124 /* InjectionBoot.mm */,
BBDD84182C4FE7E6000F3124 /* InjectionNext.swift */,
Expand Down Expand Up @@ -893,6 +919,7 @@
BB42F8EC2EB626F100FDBBCC /* SwiftMeta.swift in Sources */,
BB4788802EB6B6E700464AB4 /* InjectionBoot.mm in Sources */,
BBDD845A2C4FF646000F3124 /* ClientBoot.mm in Sources */,
BBF00D012FBCE00100F0D001 /* InjectionTouchEvents.mm in Sources */,
BB42F92D2EB639E500FDBBCC /* DLKitC.c in Sources */,
BB42F92E2EB639E500FDBBCC /* trie_dladdr.mm in Sources */,
BB42F92F2EB639E500FDBBCC /* trie_dlops.mm in Sources */,
Expand Down Expand Up @@ -1122,7 +1149,7 @@
INFOPLIST_FILE = InjectionNext/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.0.0;
MARKETING_VERSION = 2.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.johnholdsworth.InjectionNext;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -1144,7 +1171,7 @@
INFOPLIST_FILE = InjectionNext/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.0.0;
MARKETING_VERSION = 2.0.1;
PRODUCT_BUNDLE_IDENTIFIER = com.johnholdsworth.InjectionNext;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
2 changes: 0 additions & 2 deletions App/InjectionNext/ConfigStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -475,8 +475,6 @@ final class ConfigStore: ObservableObject {
hostChangeWarned = true }
}
let injectionPort: String = HOTRELOADING_PORT
let controlPort: UInt16 = 8919

// MARK: - Advanced

@Published var verboseLogging: Bool {
Expand Down
127 changes: 106 additions & 21 deletions App/InjectionNext/ControlServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// ControlServer.swift
// InjectionNext
//
// Local TCP control server for MCP integration.
// Listens on localhost:8919 for JSON commands and
// Local Unix domain socket control server for MCP integration.
// Listens for JSON commands from the bundled MCP server and
// maps them to existing AppDelegate actions.
//

Expand All @@ -13,14 +13,17 @@ import Cocoa

class ControlServer {

static let port: UInt16 = 8919
static let compatibilityPort: UInt16 = 8919
static let socketPath = "/tmp/InjectionNext-control.sock"
static var servicedRequest = false
static var shared: ControlServer?

private var serverSocket: Int32 = -1
private let queue = DispatchQueue(label: "ControlServer", attributes: .concurrent)

static func start() {
guard shared == nil else { return }
CompatibilityControlServer.startServer(":\(compatibilityPort)")
shared = ControlServer()
shared?.listen()
}
Expand All @@ -29,49 +32,57 @@ class ControlServer {
queue.async { [weak self] in
guard let self = self else { return }

self.serverSocket = socket(AF_INET, SOCK_STREAM, 0)
self.serverSocket = socket(AF_UNIX, SOCK_STREAM, 0)
guard self.serverSocket >= 0 else {
NSLog("\(APP_PREFIX)ControlServer: socket() failed")
return
}

var reuse: Int32 = 1
setsockopt(self.serverSocket, SOL_SOCKET, SO_REUSEADDR, &reuse, socklen_t(MemoryLayout<Int32>.size))
let socketPath = Self.socketPath
guard socketPath.utf8.count < MemoryLayout.size(ofValue: sockaddr_un().sun_path) else {
NSLog("\(APP_PREFIX)ControlServer: socket path too long: \(socketPath)")
close(self.serverSocket)
return
}

var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = Self.port.bigEndian
addr.sin_addr.s_addr = inet_addr("127.0.0.1")
unlink(socketPath)

var addr = sockaddr_un()
addr.sun_len = UInt8(MemoryLayout<sockaddr_un>.size)
addr.sun_family = sa_family_t(AF_UNIX)
let sunPathSize = MemoryLayout.size(ofValue: sockaddr_un().sun_path)
withUnsafeMutableBytes(of: &addr.sun_path) { buffer in
socketPath.withCString { source in
if let baseAddress = buffer.baseAddress {
strncpy(baseAddress.assumingMemoryBound(to: CChar.self),
source, sunPathSize - 1)
}
}
}

let bindResult = withUnsafePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
bind(self.serverSocket, $0, socklen_t(MemoryLayout<sockaddr_in>.size))
bind(self.serverSocket, $0, socklen_t(MemoryLayout<sockaddr_un>.size))
}
}

guard bindResult == 0 else {
NSLog("\(APP_PREFIX)ControlServer: bind() failed on port \(Self.port): \(String(cString: strerror(errno)))")
NSLog("\(APP_PREFIX)ControlServer: bind() failed at \(socketPath): \(String(cString: strerror(errno)))")
close(self.serverSocket)
return
}
chmod(socketPath, S_IRUSR | S_IWUSR)

guard Darwin.listen(self.serverSocket, 5) == 0 else {
NSLog("\(APP_PREFIX)ControlServer: listen() failed")
close(self.serverSocket)
return
}

NSLog("\(APP_PREFIX)ControlServer: listening on localhost:\(Self.port)")
NSLog("\(APP_PREFIX)ControlServer: listening at \(socketPath)")

while true {
var clientAddr = sockaddr_in()
var clientLen = socklen_t(MemoryLayout<sockaddr_in>.size)
let clientSocket = withUnsafeMutablePointer(to: &clientAddr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
accept(self.serverSocket, $0, &clientLen)
}
}
let clientSocket = accept(self.serverSocket, nil, nil)
guard clientSocket >= 0 else { continue }
self.queue.async {
self.handleClient(clientSocket)
Expand Down Expand Up @@ -106,6 +117,7 @@ class ControlServer {

let result = executeAction(action, params: json)
sendResponse(sock, success: result.success, data: result.data, error: result.error)
Self.servicedRequest = true
Comment thread
johnno1962 marked this conversation as resolved.
}

private func sendResponse(_ sock: Int32, success: Bool, data: [String: Any]? = nil, error: String? = nil) {
Expand Down Expand Up @@ -164,6 +176,15 @@ class ControlServer {
case "get_last_error":
return getLastError()

case "take_screenshot":
return takeScreenshot()

case "get_touch_events":
return getTouchEvents()

case "replay_touch_events":
return replayTouchEvents(params: params)

case "prepare_swiftui_source":
return prepareSwiftUISource()

Expand Down Expand Up @@ -268,6 +289,51 @@ class ControlServer {
return .ok(["error": error])
}

private func takeScreenshot() -> ActionResult {
guard let client = InjectionServer.currentClient else {
return .fail("No connected client app")
}
guard let screenshot = client.requestScreenshot() else {
return .fail("Unable to capture screenshot from client app")
}
return .ok([
"mimeType": screenshot.mimeType,
"data": screenshot.data.base64EncodedString(),
"bytes": screenshot.data.count
])
}

private func getTouchEvents() -> ActionResult {
guard let client = InjectionServer.currentClient else {
return .fail("No connected client app")
}
let events = client.drainTouchEvents().compactMap { json -> Any? in
guard let data = json.data(using: .utf8) else { return nil }
return try? JSONSerialization.jsonObject(with: data)
}
return .ok(["events": events])
}

private func replayTouchEvents(params: [String: Any]) -> ActionResult {
guard let client = InjectionServer.currentClient else {
return .fail("No connected client app")
}
let payload: Any
if let events = params["events"] as? [Any] {
payload = ["events": events]
} else {
payload = params
}
guard JSONSerialization.isValidJSONObject(payload),
let data = try? JSONSerialization.data(withJSONObject: payload),
let json = String(data: data, encoding: .utf8) else {
return .fail("Invalid touch event JSON payload")
}
client.replayTouchEvents(json)
let count = (payload as? [String: Any])?["events"] as? [Any]
return .ok(["events": count?.count ?? 0])
}

private func prepareSwiftUISource() -> ActionResult {
guard let lastSource = NextCompiler.lastSource else {
return .fail("No source file currently being edited")
Expand Down Expand Up @@ -305,4 +371,23 @@ class ControlServer {
LogBuffer.shared.clear()
return .ok()
}

private final class CompatibilityControlServer: SimpleSocket {
override func run() {
let response: [String: Any] = [
"success": false,
"error": "InjectionNext now uses a Unix domain socket for MCP control. " +
"Upgrade your mcp-server or use the version bundled inside InjectionNext.app."
]
guard let data = try? JSONSerialization.data(withJSONObject: response),
let json = String(data: data, encoding: .utf8),
let stream = fdopen(forMode: "w") else {
return
}
defer { fclose(stream) }
(json + "\n").withCString {
_ = fputs($0, stream)
}
}
}
}
2 changes: 1 addition & 1 deletion App/InjectionNext/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>14720</string>
<string>14807</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
Expand Down
Loading