From c5f051166df0a123ba3b9cc82a28c642433f2252 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Thu, 7 Sep 2023 18:56:53 -0400 Subject: [PATCH 01/10] Tried a Async interface but it is awful - The AsyncPublisher will block, and the result is not cancellable. The way to run this is using actors but we already have other patterns for this. --- Blink.xcodeproj/project.pbxproj | 24 +++++- Blink/Commands/mosh/MoshBootstrap.swift | 97 +++++++++++++++++++++++++ BlinkTests/MoshBootstrapTests.swift | 73 +++++++++++++++++++ 3 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 Blink/Commands/mosh/MoshBootstrap.swift create mode 100644 BlinkTests/MoshBootstrapTests.swift diff --git a/Blink.xcodeproj/project.pbxproj b/Blink.xcodeproj/project.pbxproj index 568a080fb..12ca7c830 100644 --- a/Blink.xcodeproj/project.pbxproj +++ b/Blink.xcodeproj/project.pbxproj @@ -93,6 +93,8 @@ BD11E9E6270CD0FD003EA5AE /* openssl.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2F64C9425CA99AD00F2225D /* openssl.xcframework */; }; BD1758AC26EA8C5400AEC545 /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1758AB26EA8C5400AEC545 /* MenuController.swift */; }; BD2E27B529BAA8DA003AF1DA /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2E27B429BAA8DA003AF1DA /* ReplaySubject.swift */; }; + BD33F7822AAA426D00CD16EE /* MoshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD33F7802AAA426D00CD16EE /* MoshBootstrap.swift */; }; + BD33F7872AAA7C4300CD16EE /* MoshBootstrapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD33F7862AAA7C4300CD16EE /* MoshBootstrapTests.swift */; }; BD3E1E53278D190500333C44 /* Archive.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3E1E4F278D190500333C44 /* Archive.swift */; }; BD44DCE626D6BEAC00054338 /* BlinkItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD44DCE526D6BEAC00054338 /* BlinkItemIdentifier.swift */; }; BD67FC79272B30F300C1EE75 /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD67FC78272B30F300C1EE75 /* Messages.swift */; }; @@ -824,6 +826,8 @@ BD028AF22A8EC509002F5F54 /* TrialSupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrialSupportView.swift; sourceTree = ""; }; BD1758AB26EA8C5400AEC545 /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = ""; }; BD2E27B429BAA8DA003AF1DA /* ReplaySubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = ""; }; + BD33F7802AAA426D00CD16EE /* MoshBootstrap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoshBootstrap.swift; sourceTree = ""; }; + BD33F7862AAA7C4300CD16EE /* MoshBootstrapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoshBootstrapTests.swift; sourceTree = ""; }; BD3E1E4F278D190500333C44 /* Archive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Archive.swift; sourceTree = ""; }; BD44DCE526D6BEAC00054338 /* BlinkItemIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlinkItemIdentifier.swift; sourceTree = ""; }; BD67FC78272B30F300C1EE75 /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; @@ -1685,6 +1689,14 @@ path = BlinkCode/Publisher; sourceTree = SOURCE_ROOT; }; + BD33F77F2AAA426D00CD16EE /* mosh */ = { + isa = PBXGroup; + children = ( + BD33F7802AAA426D00CD16EE /* MoshBootstrap.swift */, + ); + path = mosh; + sourceTree = ""; + }; BD835DCF27A0BD19002C37D7 /* Publisher */ = { isa = PBXGroup; children = ( @@ -2086,13 +2098,14 @@ D265FBBB2317DD3C0017EAC4 /* BlinkTests */ = { isa = PBXGroup; children = ( - D265FBBE2317DD3C0017EAC4 /* Info.plist */, + BD9EA215271F83B400874007 /* BlinkLoggingTests.swift */, D20CBA56236031D700D93301 /* CompleteUtilsTests.swift */, - D265FBC42317E5090017EAC4 /* SessionParamsTests.swift */, + BDE7C45B29DCAEFA005E033E /* FileLocationPathTests.swift */, + D265FBBE2317DD3C0017EAC4 /* Info.plist */, BD8BBF0825F819970084705F /* SEKeyTests.swift */, - BD9EA215271F83B400874007 /* BlinkLoggingTests.swift */, + D265FBC42317E5090017EAC4 /* SessionParamsTests.swift */, BD74A7C12905BD5800ED01CF /* WhatsNewModelTests.swift */, - BDE7C45B29DCAEFA005E033E /* FileLocationPathTests.swift */, + BD33F7862AAA7C4300CD16EE /* MoshBootstrapTests.swift */, ); path = BlinkTests; sourceTree = ""; @@ -2253,6 +2266,7 @@ D2F330C520A6C8E20074ADD7 /* Commands */ = { isa = PBXGroup; children = ( + BD33F77F2AAA426D00CD16EE /* mosh */, 07FAB8E925C8E6C500E1CC2C /* ssh */, D2334D1221495DAE00D26AC3 /* udptunnel */, D240806020BC8DF800F30099 /* tool_main.c */, @@ -3231,6 +3245,7 @@ D265FBC52317E5090017EAC4 /* SessionParamsTests.swift in Sources */, BD9EA218271F846400874007 /* Publisher.swift in Sources */, BD8BBF5525F829B00084705F /* SEKeyTests.swift in Sources */, + BD33F7872AAA7C4300CD16EE /* MoshBootstrapTests.swift in Sources */, BD9EA216271F83B400874007 /* BlinkLoggingTests.swift in Sources */, D20CBA57236031D700D93301 /* CompleteUtilsTests.swift in Sources */, D20CBA5B2360327900D93301 /* CompleteUtils.swift in Sources */, @@ -3325,6 +3340,7 @@ BD9EA1FE271A148700874007 /* Migrator.swift in Sources */, D2C24417238E44AB0082C69C /* KeyConfig.swift in Sources */, D28F301A21AD8A6B00E5259F /* DeviceInfo.m in Sources */, + BD33F7822AAA426D00CD16EE /* MoshBootstrap.swift in Sources */, D2179F2F2136DBC600B0850A /* geo.m in Sources */, D2C24414238E44AB0082C69C /* KeyAction.swift in Sources */, D28B0337243EF5F2008F38F6 /* Set+UIScene.swift in Sources */, diff --git a/Blink/Commands/mosh/MoshBootstrap.swift b/Blink/Commands/mosh/MoshBootstrap.swift new file mode 100644 index 000000000..430b1b68c --- /dev/null +++ b/Blink/Commands/mosh/MoshBootstrap.swift @@ -0,0 +1,97 @@ +import Combine + +import SSH + +// We can test MoshBootstrap +// We could use different Strategies for bootstrap + +enum Platform { + case Darwin + case Linux +} + +extension Platform { + init?(from str: String) { + switch str { + case "Darwin": + self = .Darwin + case "linux": + self = .Linux + default: + return nil + } + } +} + +enum Architecture { + case X86_64 + case Arm64 +} + +struct MoshBootstrap { + let client: SSHClient + let blinkRemoteLocation: String + + init(client: SSHClient) { + self.client = client + // TODO We could also read this from env variable. + self.blinkRemoteLocation = "~/.blink/" + } + + // Return an AnyPublisher + func start() async { + // Create SSH connection. + // - We should be able to use the same client as SSH. + // Check the version for mosh-server installed at .blink/mosh-server + // - If we do it by checking version on file, we will have to delete and re-upload + // Run mosh-server and capture + + // 1 - Figure out platform and architecture + // This could be part of the SSH library, and we could use this from our side later. + // Get the special publisher from Snips. + + // dialWithTestConfig -> SSHClient + // Do we have any other way to convert to string directly? + + // This has the problem that the model is not cancellable. + // They added Actors and a lot of other complexities for this, but not worth it. + // https://stackoverflow.com/questions/71837201/task-blocks-main-thread-when-calling-async-function-inside + let platform = await client + .requestExec(command: "uname") + .flatMap { s -> AnyPublisher in + s.read(max: 1024) + } + .map { String(decoding: $0 as AnyObject as! Data, as: UTF8.self) } + .map { Platform(from: $0) } + .assertNoFailure() + .values + .first() + + guard let platform = platform else { + print("no platform found") + return + } + + print("Platform is \(platform)") + + //await client.execute("").map { SystemArch(output.parse) } + + // client.sftp_client.map { $0.translator() } + // 1 - List folder and search for mosh-server + // 2 - Resolve from symlink. Test it, I'm not sure it will work. + + } + +// async func platformAndArchitecture() throws -> (Platform, Architecture) { +// +// } +} + +// Could not find any implementation atm. But looks like it may be coming. +// https://forums.swift.org/t/concurrency-asyncsequence/42417 +extension AsyncSequence { + func first() async rethrows -> AsyncIterator.Element? { + var iter = self.makeAsyncIterator() + return try await iter.next() + } +} diff --git a/BlinkTests/MoshBootstrapTests.swift b/BlinkTests/MoshBootstrapTests.swift new file mode 100644 index 000000000..79a3db7dc --- /dev/null +++ b/BlinkTests/MoshBootstrapTests.swift @@ -0,0 +1,73 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2023 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + +import Combine +import XCTest + +import SSH + +@testable import Blink + +final class MoshBootstrapTests: XCTestCase { + var cancellableBag: Set = [] + + func testMoshBootstrap2() throws { + print("connecting...") + + //let client = try await SSHClient.dial("localhost", with: .testConfig).values.first()! + let expectConn = self.expectation(description: "Connection established") + + var connection: SSHClient? + SSHClient.dial("localhost", with: .testConfig) + .sink( + receiveCompletion: { _ in }, + receiveValue: { conn in + connection = conn + expectConn.fulfill() + }).store(in: &cancellableBag) + + wait(for: [expectConn], timeout: 5) + + print("connected") + //let boot = MoshBootstrap(client: client) + //await boot.start() + } + +} + +extension SSHClientConfig { + static let testConfig = SSHClientConfig( + user: "carloscabanero", + port: "22", + authMethods: [AuthPassword(with: "asdfzxcv")], + loggingVerbosity: .debug + ) +} From 415528d4efdd342ae89228748e15ffa3b2f13a86 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Fri, 8 Sep 2023 17:02:23 -0400 Subject: [PATCH 02/10] Mosh Server bootstrap flow --- Blink/Commands/mosh/MoshBootstrap.swift | 146 +++++++++++++++++------- BlinkTests/MoshBootstrapTests.swift | 19 ++- 2 files changed, 121 insertions(+), 44 deletions(-) diff --git a/Blink/Commands/mosh/MoshBootstrap.swift b/Blink/Commands/mosh/MoshBootstrap.swift index 430b1b68c..5d9ca8251 100644 --- a/Blink/Commands/mosh/MoshBootstrap.swift +++ b/Blink/Commands/mosh/MoshBootstrap.swift @@ -1,9 +1,13 @@ import Combine +import BlinkFiles import SSH // We can test MoshBootstrap // We could use different Strategies for bootstrap +// TODO Just for testing, this needs a version. +let MoshServerBinaryName = "mosh-server" +let MoshServerDownloadURL = URL(string: "https://github.com/dtinth/mosh-static/releases/latest/download//(MoshServerBinaryName)")! enum Platform { case Darwin @@ -12,8 +16,8 @@ enum Platform { extension Platform { init?(from str: String) { - switch str { - case "Darwin": + switch str.lowercased() { + case "darwin": self = .Darwin case "linux": self = .Linux @@ -28,10 +32,28 @@ enum Architecture { case Arm64 } +extension Architecture { + init?(from str: String) { + switch str.lowercased() { + case "x86_64": + self = .X86_64 + case "arm64": + self = .Arm64 + default: + return nil + } + } +} + +enum MoshBootstrapError: Error { + case NoBinaryAvailable +} + struct MoshBootstrap { let client: SSHClient let blinkRemoteLocation: String + init(client: SSHClient) { self.client = client // TODO We could also read this from env variable. @@ -39,7 +61,7 @@ struct MoshBootstrap { } // Return an AnyPublisher - func start() async { + func start() -> AnyPublisher { // Create SSH connection. // - We should be able to use the same client as SSH. // Check the version for mosh-server installed at .blink/mosh-server @@ -49,49 +71,93 @@ struct MoshBootstrap { // 1 - Figure out platform and architecture // This could be part of the SSH library, and we could use this from our side later. // Get the special publisher from Snips. - - // dialWithTestConfig -> SSHClient - // Do we have any other way to convert to string directly? - // This has the problem that the model is not cancellable. - // They added Actors and a lot of other complexities for this, but not worth it. - // https://stackoverflow.com/questions/71837201/task-blocks-main-thread-when-calling-async-function-inside - let platform = await client - .requestExec(command: "uname") - .flatMap { s -> AnyPublisher in - s.read(max: 1024) + return Just(()) + .flatMap { platformAndArchitecture() } + .tryMap { pa in + guard let platform = pa?.0, + let architecture = pa?.1 else { + throw MoshBootstrapError.NoBinaryAvailable + } + + return (platform, architecture) } - .map { String(decoding: $0 as AnyObject as! Data, as: UTF8.self) } - .map { Platform(from: $0) } + .flatMap { getMoshServer(platform: $0, architecture: $1) } + .flatMap { installMoshServer(localMoshServerBinary: $0) } + // Select binary. We separate as then we can return proper errors from just this function. + // Check binary on remote (resolve link over sftp) + // - Download and upload + // Return binary location + .print() .assertNoFailure() - .values - .first() - - guard let platform = platform else { - print("no platform found") - return - } - - print("Platform is \(platform)") - - //await client.execute("").map { SystemArch(output.parse) } + .eraseToAnyPublisher() + // -> Below can be done by a separate flow. This way we reuse + // We can also include a "which" mosh-server call in an alternative method. + // Run binary on remote + // Capture Mosh parameters - // client.sftp_client.map { $0.translator() } - // 1 - List folder and search for mosh-server - // 2 - Resolve from symlink. Test it, I'm not sure it will work. - } -// async func platformAndArchitecture() throws -> (Platform, Architecture) { -// -// } -} + func platformAndArchitecture() -> AnyPublisher<(Platform, Architecture)?, Error> { + self.client.requestExec(command: "uname && uname -m") + .flatMap { s -> AnyPublisher in + s.read(max: 1024) + } + .map { String(decoding: $0 as AnyObject as! Data, as: UTF8.self).components(separatedBy: .newlines) } + .map { lines -> (Platform, Architecture)? in + if lines.count != 3 { + return nil + } + + guard let platform = Platform(from: lines[0]), + let architecture = Architecture(from: lines[1]) else { + return nil + } -// Could not find any implementation atm. But looks like it may be coming. -// https://forums.swift.org/t/concurrency-asyncsequence/42417 -extension AsyncSequence { - func first() async rethrows -> AsyncIterator.Element? { - var iter = self.makeAsyncIterator() - return try await iter.next() + return (platform, architecture) + }.eraseToAnyPublisher() + } + + func getMoshServer(platform: Platform, architecture: Architecture) -> AnyPublisher { + let localMoshServerURL = BlinkPaths.blinkURL().appending(path: MoshServerBinaryName) + return URLSession.shared.dataTaskPublisher(for: MoshServerDownloadURL) + .map(\.data) + .tryMap { data in + try data.write(to: localMoshServerURL) + return localMoshServerURL + } + .flatMap { + Local().cloneWalkTo($0.path) + } + .eraseToAnyPublisher() + } + + // try to use .local + func installMoshServer(localMoshServerBinary: Translator) -> AnyPublisher { + let RemoteBlinkLocation = ".blink" + + return self.client.requestSFTP() + .tryMap { try SFTPTranslator(on: $0) } + .flatMap { sftp in + // We may not need this check if we are hard-coding the version to Blink. + // Unless we also want to clean things up. + // A device if not updated can still install its version. + sftp.cloneWalkTo("~/\(RemoteBlinkLocation)/\(MoshServerBinaryName)") + .catch { _ in + sftp.cloneWalkTo(RemoteBlinkLocation) + .catch { _ in + sftp.mkdir(name: RemoteBlinkLocation) + } + // Upload file + .flatMap { dest in + dest.copy(from: [localMoshServerBinary]) + } + .last() + .flatMap { _ in sftp.cloneWalkTo("~/\(RemoteBlinkLocation)/\(MoshServerBinaryName)") } + } + .map { $0.current } + } + .eraseToAnyPublisher() + } } diff --git a/BlinkTests/MoshBootstrapTests.swift b/BlinkTests/MoshBootstrapTests.swift index 79a3db7dc..1bd272414 100644 --- a/BlinkTests/MoshBootstrapTests.swift +++ b/BlinkTests/MoshBootstrapTests.swift @@ -45,7 +45,7 @@ final class MoshBootstrapTests: XCTestCase { //let client = try await SSHClient.dial("localhost", with: .testConfig).values.first()! let expectConn = self.expectation(description: "Connection established") - var connection: SSHClient? + var connection: SSHClient! SSHClient.dial("localhost", with: .testConfig) .sink( receiveCompletion: { _ in }, @@ -57,10 +57,21 @@ final class MoshBootstrapTests: XCTestCase { wait(for: [expectConn], timeout: 5) print("connected") - //let boot = MoshBootstrap(client: client) - //await boot.start() - } + + let expectBootstrap = self.expectation(description: "Mosh bootstrapped") + MoshBootstrap(client: connection) + .start() + .sink( + receiveCompletion: { _ in }, + receiveValue: { moshServerPath in + print("Mosh server path at: \(moshServerPath)") + } + ).store(in: &cancellableBag) + + wait(for: [expectBootstrap], timeout: 30) + + } } extension SSHClientConfig { From e41dae39e010cf04dc9362ed17d64aefd3093cb0 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Tue, 12 Sep 2023 12:49:38 -0400 Subject: [PATCH 03/10] Running Mosh --- Blink.xcodeproj/project.pbxproj | 4 + Blink/Commands/mosh/mosh.swift | 129 ++++++++++++++++++++++++ Resources/blinkCommandsDictionary.plist | 7 ++ 3 files changed, 140 insertions(+) create mode 100644 Blink/Commands/mosh/mosh.swift diff --git a/Blink.xcodeproj/project.pbxproj b/Blink.xcodeproj/project.pbxproj index 12ca7c830..28de9d5da 100644 --- a/Blink.xcodeproj/project.pbxproj +++ b/Blink.xcodeproj/project.pbxproj @@ -114,6 +114,7 @@ BD81522D2739A91D002BB169 /* BlinkLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9EA20A271F62ED00874007 /* BlinkLogging.swift */; }; BD81522E2739A91D002BB169 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9EA20C271F664D00874007 /* Publisher.swift */; }; BD8152542743FF84002BB169 /* skstore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8152532743FF84002BB169 /* skstore.swift */; }; + BD818A052AAFC18400956488 /* mosh.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A042AAFC18400956488 /* mosh.swift */; }; BD835DD427A0BD19002C37D7 /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD835DD027A0BD19002C37D7 /* ReplaySubject.swift */; }; BD896F7B26CEAD37004313E6 /* FileTranslatorCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD896F7A26CEAD37004313E6 /* FileTranslatorCache.swift */; }; BD8BBF5525F829B00084705F /* SEKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8BBF0825F819970084705F /* SEKeyTests.swift */; }; @@ -838,6 +839,7 @@ BD792A442A3B6A78009EE35F /* GitHubSnippets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubSnippets.swift; sourceTree = ""; }; BD81521C27387D1F002BB169 /* Certificates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Certificates.swift; sourceTree = ""; }; BD8152532743FF84002BB169 /* skstore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = skstore.swift; sourceTree = ""; }; + BD818A042AAFC18400956488 /* mosh.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mosh.swift; sourceTree = ""; }; BD835DD027A0BD19002C37D7 /* ReplaySubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = ""; }; BD896F7A26CEAD37004313E6 /* FileTranslatorCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTranslatorCache.swift; sourceTree = ""; }; BD8BBF0825F819970084705F /* SEKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEKeyTests.swift; sourceTree = ""; }; @@ -1692,6 +1694,7 @@ BD33F77F2AAA426D00CD16EE /* mosh */ = { isa = PBXGroup; children = ( + BD818A042AAFC18400956488 /* mosh.swift */, BD33F7802AAA426D00CD16EE /* MoshBootstrap.swift */, ); path = mosh; @@ -3268,6 +3271,7 @@ D241CBD923040734003D64A5 /* KBKeyView.swift in Sources */, 803B99D72582869200DC99C8 /* BKNotificationsView.swift in Sources */, BD8152542743FF84002BB169 /* skstore.swift in Sources */, + BD818A052AAFC18400956488 /* mosh.swift in Sources */, C94E9B631D6BA21C00DA4DD6 /* DismissSegue.m in Sources */, D29B4A92274D206C00C66ED9 /* BrowserController.swift in Sources */, 803B99E3258381B200DC99C8 /* SettingsHostingController.swift in Sources */, diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift new file mode 100644 index 000000000..9b685d86a --- /dev/null +++ b/Blink/Commands/mosh/mosh.swift @@ -0,0 +1,129 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2019 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + +import Combine + +import SSH +import ios_system + +@_cdecl("blink_mosh_main") +public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { + setvbuf(thread_stdin, nil, _IONBF, 0) + setvbuf(thread_stdout, nil, _IONBF, 0) + setvbuf(thread_stderr, nil, _IONBF, 0) + + let session = Unmanaged.fromOpaque(thread_context).takeUnretainedValue() + let cmd = BlinkMosh() + return cmd.start(argc, argv: argv.args(count: argc)) +} + +// struct MoshCommand: ParsableCommand { + +// } + +@objc public class BlinkMosh: NSObject { + var sshCancellable: AnyCancellable? + let device = tty() + let currentRunLoop = RunLoop.current + var stdout = OutputStream(file: thread_stdout) + var stderr = OutputStream(file: thread_stderr) + + @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { + // ssh config from command + ssh setup + let host: BKSSHHost + let config: SSHClientConfig + let hostName: String + + do { + // TODO + host = try BKConfig().bkSSHHost("loc")//moshCommand.hostAlias) // extending: moshCommand.bkSSHHost()) + hostName = host.hostName! // ?? moshCommand.hostName + config = try SSHClientConfigProvider.config(host: host, using: device) + } catch { + print("Configuration error - \(error)", to: &stderr) + return -1 + } + + // TODO pass MoshCommand + let moshServerArgs = getMoshServerArgs(port: nil, colors: nil, exec: nil) + + // TODO The bootstrap returns a mosh-serve route, but we usually just run it. + // We could enforce a "which", but that is not standard mosh bootstrap. + // We should keep everything under one connection anyway. + // - Try to run "mosh-server" as-is or from server route. + // - If the output does not match... + // - Try to bootstrap "mosh-server". Ask user before uploading binary (but the check can be done) + // ["mosh-server || provided-location", "bootstrap", ".fail"] + // ["bootstrap"] only with the flag. + // Do force-bootstrap only through flag. + sshCancellable = SSHClient.dial(hostName, with: config) + .flatMap { self.startMoshServer(on: $0, args: moshServerArgs) } + .sink( + receiveCompletion: { _ in + awake(runLoop: self.currentRunLoop) + }, + receiveValue: { conn in + // From Combine, output from running mosh-server. + print(conn) + }) + + awaitRunLoop(currentRunLoop) + + // parse output + // Connect to server separately. + return 0 + + // connection, bootstrap and start mosh-server + // connect to server + } + + // TODO Pass command too for CLI configuration + private func getMoshServerArgs(port: String?, + colors: String?, + exec: String?) -> String { + // TODO Locale as args + var moshServerArgs = ["new", "-s", "-c", colors ?? "256", "-l LC_ALL=en_US.UTF-8"] + + if let port = port { + moshServerArgs.append(contentsOf: ["-p", port]) + } + if let exec = exec { + moshServerArgs.append(contentsOf: ["--", exec]) + } + + return moshServerArgs.joined(separator: " ") + } + + private func startMoshServer(on: SSHClient, args: String) -> AnyPublisher<(), Never> { + + Just(()).eraseToAnyPublisher() + } +} diff --git a/Resources/blinkCommandsDictionary.plist b/Resources/blinkCommandsDictionary.plist index 1a608cac0..995fca774 100644 --- a/Resources/blinkCommandsDictionary.plist +++ b/Resources/blinkCommandsDictionary.plist @@ -2,6 +2,13 @@ + mosh2 + + MAIN + blink_mosh_main + + no + skstore MAIN From 971d7cec8944c0f49e097411d30fb589c067659a Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Tue, 12 Sep 2023 22:46:51 -0400 Subject: [PATCH 04/10] Added mosh to Swift bridge --- Blink.xcodeproj/project.pbxproj | 4 + Blink/Blink-bridge.h | 2 + Blink/Commands/mosh/MoshBootstrap.swift | 107 +++++++++++++-------- Blink/Commands/mosh/MoshServerParams.swift | 74 ++++++++++++++ Blink/Commands/mosh/mosh.swift | 79 ++++++++++----- 5 files changed, 202 insertions(+), 64 deletions(-) create mode 100644 Blink/Commands/mosh/MoshServerParams.swift diff --git a/Blink.xcodeproj/project.pbxproj b/Blink.xcodeproj/project.pbxproj index 28de9d5da..71ad64127 100644 --- a/Blink.xcodeproj/project.pbxproj +++ b/Blink.xcodeproj/project.pbxproj @@ -115,6 +115,7 @@ BD81522E2739A91D002BB169 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD9EA20C271F664D00874007 /* Publisher.swift */; }; BD8152542743FF84002BB169 /* skstore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8152532743FF84002BB169 /* skstore.swift */; }; BD818A052AAFC18400956488 /* mosh.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A042AAFC18400956488 /* mosh.swift */; }; + BD818A0C2AB120B800956488 /* MoshServerParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A0B2AB120B800956488 /* MoshServerParams.swift */; }; BD835DD427A0BD19002C37D7 /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD835DD027A0BD19002C37D7 /* ReplaySubject.swift */; }; BD896F7B26CEAD37004313E6 /* FileTranslatorCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD896F7A26CEAD37004313E6 /* FileTranslatorCache.swift */; }; BD8BBF5525F829B00084705F /* SEKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8BBF0825F819970084705F /* SEKeyTests.swift */; }; @@ -840,6 +841,7 @@ BD81521C27387D1F002BB169 /* Certificates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Certificates.swift; sourceTree = ""; }; BD8152532743FF84002BB169 /* skstore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = skstore.swift; sourceTree = ""; }; BD818A042AAFC18400956488 /* mosh.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mosh.swift; sourceTree = ""; }; + BD818A0B2AB120B800956488 /* MoshServerParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoshServerParams.swift; sourceTree = ""; }; BD835DD027A0BD19002C37D7 /* ReplaySubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = ""; }; BD896F7A26CEAD37004313E6 /* FileTranslatorCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTranslatorCache.swift; sourceTree = ""; }; BD8BBF0825F819970084705F /* SEKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEKeyTests.swift; sourceTree = ""; }; @@ -1696,6 +1698,7 @@ children = ( BD818A042AAFC18400956488 /* mosh.swift */, BD33F7802AAA426D00CD16EE /* MoshBootstrap.swift */, + BD818A0B2AB120B800956488 /* MoshServerParams.swift */, ); path = mosh; sourceTree = ""; @@ -3375,6 +3378,7 @@ D264D2B228F84592002B1B14 /* GridView.swift in Sources */, D2EFE1F520B7FAFC0087888B /* link_files.m in Sources */, D2A54CB129801062009D79FE /* BuildAccountModel.swift in Sources */, + BD818A0C2AB120B800956488 /* MoshServerParams.swift in Sources */, D2C24418238E44AB0082C69C /* KBConfigView.swift in Sources */, D2BF5F7F265BA0A80070F839 /* UserDefaults.swift in Sources */, D2F330CA20A6CB840074ADD7 /* help.m in Sources */, diff --git a/Blink/Blink-bridge.h b/Blink/Blink-bridge.h index 81a972452..7dcd35b87 100644 --- a/Blink/Blink-bridge.h +++ b/Blink/Blink-bridge.h @@ -70,5 +70,7 @@ extern void ios_exit(int errorCode) __dead2; // set error code and exits from th #import "TokioSignals.h" #import "BlinkMenu.h" #import "GeoManager.h" +#import "mosh/moshiosbridge.h" + #endif /* Blink_bridge_h */ diff --git a/Blink/Commands/mosh/MoshBootstrap.swift b/Blink/Commands/mosh/MoshBootstrap.swift index 5d9ca8251..2c3a61d3f 100644 --- a/Blink/Commands/mosh/MoshBootstrap.swift +++ b/Blink/Commands/mosh/MoshBootstrap.swift @@ -1,3 +1,34 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2021 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + import Combine import BlinkFiles @@ -6,6 +37,7 @@ import SSH // We can test MoshBootstrap // We could use different Strategies for bootstrap // TODO Just for testing, this needs a version. +// We have decided to hard-code the version of Blink so both can be related. let MoshServerBinaryName = "mosh-server" let MoshServerDownloadURL = URL(string: "https://github.com/dtinth/mosh-static/releases/latest/download//(MoshServerBinaryName)")! @@ -45,35 +77,41 @@ extension Architecture { } } +// TODO Move this errors out of here as they will be for all mosh enum MoshBootstrapError: Error { case NoBinaryAvailable + case NoMoshServerArgs +} + +protocol MoshBootstrap { + func start(on client: SSHClient) -> AnyPublisher } -struct MoshBootstrap { - let client: SSHClient +// TODO We could enforce a "which", but that is not standard mosh bootstrap. +class UseMoshOnPath: MoshBootstrap { + let path: String + + init(path: String? = nil) { + self.path = path ?? MoshServerBinaryName + } + + func start(on client: SSHClient) -> AnyPublisher { + Just(self.path).setFailureType(to: Error.self).eraseToAnyPublisher() + } +} + +class UseStaticMosh: MoshBootstrap { let blinkRemoteLocation: String - - init(client: SSHClient) { - self.client = client + init() { // TODO We could also read this from env variable. self.blinkRemoteLocation = "~/.blink/" } // Return an AnyPublisher - func start() -> AnyPublisher { - // Create SSH connection. - // - We should be able to use the same client as SSH. - // Check the version for mosh-server installed at .blink/mosh-server - // - If we do it by checking version on file, we will have to delete and re-upload - // Run mosh-server and capture - - // 1 - Figure out platform and architecture - // This could be part of the SSH library, and we could use this from our side later. - // Get the special publisher from Snips. - - return Just(()) - .flatMap { platformAndArchitecture() } + func start(on client: SSHClient) -> AnyPublisher { + Just(()) + .flatMap { self.platformAndArchitecture(on: client) } .tryMap { pa in guard let platform = pa?.0, let architecture = pa?.1 else { @@ -82,24 +120,14 @@ struct MoshBootstrap { return (platform, architecture) } - .flatMap { getMoshServer(platform: $0, architecture: $1) } - .flatMap { installMoshServer(localMoshServerBinary: $0) } - // Select binary. We separate as then we can return proper errors from just this function. - // Check binary on remote (resolve link over sftp) - // - Download and upload - // Return binary location + .flatMap { self.getMoshServerBinary(platform: $0, architecture: $1) } + .flatMap { self.installMoshServerBinary(on: client, localMoshServerBinary: $0) } .print() - .assertNoFailure() .eraseToAnyPublisher() - // -> Below can be done by a separate flow. This way we reuse - // We can also include a "which" mosh-server call in an alternative method. - // Run binary on remote - // Capture Mosh parameters - } - func platformAndArchitecture() -> AnyPublisher<(Platform, Architecture)?, Error> { - self.client.requestExec(command: "uname && uname -m") + private func platformAndArchitecture(on client: SSHClient) -> AnyPublisher<(Platform, Architecture)?, Error> { + client.requestExec(command: "uname && uname -m") .flatMap { s -> AnyPublisher in s.read(max: 1024) } @@ -118,7 +146,7 @@ struct MoshBootstrap { }.eraseToAnyPublisher() } - func getMoshServer(platform: Platform, architecture: Architecture) -> AnyPublisher { + private func getMoshServerBinary(platform: Platform, architecture: Architecture) -> AnyPublisher { let localMoshServerURL = BlinkPaths.blinkURL().appending(path: MoshServerBinaryName) return URLSession.shared.dataTaskPublisher(for: MoshServerDownloadURL) .map(\.data) @@ -132,16 +160,16 @@ struct MoshBootstrap { .eraseToAnyPublisher() } - // try to use .local - func installMoshServer(localMoshServerBinary: Translator) -> AnyPublisher { + private func installMoshServerBinary(on client: SSHClient, localMoshServerBinary: Translator) -> AnyPublisher { + // TODO Try to use .local let RemoteBlinkLocation = ".blink" - return self.client.requestSFTP() + return client.requestSFTP() .tryMap { try SFTPTranslator(on: $0) } .flatMap { sftp in - // We may not need this check if we are hard-coding the version to Blink. - // Unless we also want to clean things up. - // A device if not updated can still install its version. + // TODO We may still need the mosh-server link if we use a prompt. + // Ie. On update, names won't match, but we may not want to prompt the user again if we + // previously installed. sftp.cloneWalkTo("~/\(RemoteBlinkLocation)/\(MoshServerBinaryName)") .catch { _ in sftp.cloneWalkTo(RemoteBlinkLocation) @@ -158,6 +186,5 @@ struct MoshBootstrap { .map { $0.current } } .eraseToAnyPublisher() - } } diff --git a/Blink/Commands/mosh/MoshServerParams.swift b/Blink/Commands/mosh/MoshServerParams.swift new file mode 100644 index 000000000..8dd673ee2 --- /dev/null +++ b/Blink/Commands/mosh/MoshServerParams.swift @@ -0,0 +1,74 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2023 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + + +import Foundation + + +struct MoshServerParams { + let key: String + let udpPort: String + // Only used when we expect the IP to be resolved at the remote. + let remoteIP: String? +} + +extension MoshServerParams { + init(parsing output: String) throws { + let connectPattern = try! NSRegularExpression( + pattern: "^MOSH CONNECT (\\d+) (\\S*)$", + options: [] + ) + if let connectMatch = connectPattern.firstMatch( + in: output, + options: [], + range: NSRange(output.startIndex..., in: output) + ) { + self.udpPort = String(output[Range(connectMatch.range(at: 1), in: output)!]) + self.key = String(output[Range(connectMatch.range(at: 2), in: output)!]) + } else { + throw MoshBootstrapError.NoMoshServerArgs + } + + let remoteIPPattern = try! NSRegularExpression( + pattern: "^MOSH SSH_CONNECTION (\\S*) (\\d*) (\\S*) (\\d*)$", + options: [] + ) + if let remoteIPMatch = remoteIPPattern.firstMatch( + in: output, + options: [], + range: NSRange(location: 0, length: output.utf8.count) + ) { + self.remoteIP = String(output[Range(remoteIPMatch.range(at: 3), in: output)!]) + } else { + self.remoteIP = nil + } + } +} diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index 9b685d86a..1538b7f73 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -46,24 +46,26 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { } // struct MoshCommand: ParsableCommand { - // } + @objc public class BlinkMosh: NSObject { var sshCancellable: AnyCancellable? let device = tty() let currentRunLoop = RunLoop.current var stdout = OutputStream(file: thread_stdout) var stderr = OutputStream(file: thread_stderr) + var bootstrapSequence: [MoshBootstrap] = [] + // TODO A different main will process if there is any initial state first, otherwise + // call here. @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { - // ssh config from command + ssh setup let host: BKSSHHost let config: SSHClientConfig let hostName: String do { - // TODO + // TODO ssh config from command + ssh setup host = try BKConfig().bkSSHHost("loc")//moshCommand.hostAlias) // extending: moshCommand.bkSSHHost()) hostName = host.hostName! // ?? moshCommand.hostName config = try SSHClientConfigProvider.config(host: host, using: device) @@ -72,40 +74,40 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { return -1 } - // TODO pass MoshCommand let moshServerArgs = getMoshServerArgs(port: nil, colors: nil, exec: nil) - // TODO The bootstrap returns a mosh-serve route, but we usually just run it. - // We could enforce a "which", but that is not standard mosh bootstrap. - // We should keep everything under one connection anyway. - // - Try to run "mosh-server" as-is or from server route. - // - If the output does not match... - // - Try to bootstrap "mosh-server". Ask user before uploading binary (but the check can be done) - // ["mosh-server || provided-location", "bootstrap", ".fail"] - // ["bootstrap"] only with the flag. - // Do force-bootstrap only through flag. + // TODO Enforce path only or push-only depending on flags?. + bootstrapSequence = [UseMoshOnPath()] // UseStaticMosh + sshCancellable = SSHClient.dial(hostName, with: config) .flatMap { self.startMoshServer(on: $0, args: moshServerArgs) } .sink( receiveCompletion: { _ in awake(runLoop: self.currentRunLoop) }, - receiveValue: { conn in + receiveValue: { moshParams in // From Combine, output from running mosh-server. - print(conn) + print(moshParams) }) awaitRunLoop(currentRunLoop) - // parse output - // Connect to server separately. + // TODO Connect to server. +// let _selfRef = CFBridgingRetain(self); +// mosh_main( +// _stream.in, _stream.out, &_device->win, +// &__state_callback, (void *)_selfRef, +// [self.sessionParams.ip UTF8String], +// [self.sessionParams.port UTF8String], +// [self.sessionParams.key UTF8String], +// [self.sessionParams.predictionMode UTF8String], +// encodedState.bytes, +// encodedState.length, +// [self.sessionParams.predictOverwrite UTF8String] +// ); return 0 - - // connection, bootstrap and start mosh-server - // connect to server } - // TODO Pass command too for CLI configuration private func getMoshServerArgs(port: String?, colors: String?, exec: String?) -> String { @@ -122,8 +124,37 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { return moshServerArgs.joined(separator: " ") } - private func startMoshServer(on: SSHClient, args: String) -> AnyPublisher<(), Never> { - - Just(()).eraseToAnyPublisher() + private func startMoshServer(on client: SSHClient, args: String) -> AnyPublisher<(), Error> { + if bootstrapSequence.isEmpty { + return Fail(error: MoshBootstrapError.NoBinaryAvailable).eraseToAnyPublisher() + } + + return Just(bootstrapSequence.removeFirst()) + .flatMap { $0.start(on: client) } + // .catch - NoBinary. Should it continue or should it stop? + // It should stop, because that would be the user expectation. + // And if it shouldn't then it is the start responsibility to indicate how. + .map { moshServerPath in + "\(moshServerPath) \(args)" + } + // TODO Special SSH exec features like starting in a pty, etc... + .flatMap { + client.requestExec(command: $0) + } + .flatMap { s -> AnyPublisher in + s.read(max: 1024) + } + .map { + // TODO If Data is empty, it means there was no mosh-server binary. + // In this case, we try with the next method. + // TODO If mosh-server run but NoMoshServerArgs, then we crash. + String(decoding: $0 as AnyObject as! Data, as: UTF8.self) + } + .tryMap { try MoshServerParams(parsing: $0) } + .map { moshParams in + print(moshParams) + return () + } + .eraseToAnyPublisher() } } From 7425dfe081987190d7489374ce3c63d1e6114cab Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Thu, 14 Sep 2023 12:42:12 -0400 Subject: [PATCH 05/10] Mosh connected --- Blink/Commands/mosh/MoshBootstrap.swift | 29 ++---- Blink/Commands/mosh/MoshServerParams.swift | 3 +- Blink/Commands/mosh/mosh.swift | 114 ++++++++++++++++----- 3 files changed, 100 insertions(+), 46 deletions(-) diff --git a/Blink/Commands/mosh/MoshBootstrap.swift b/Blink/Commands/mosh/MoshBootstrap.swift index 2c3a61d3f..9cb5da101 100644 --- a/Blink/Commands/mosh/MoshBootstrap.swift +++ b/Blink/Commands/mosh/MoshBootstrap.swift @@ -34,12 +34,10 @@ import Combine import BlinkFiles import SSH -// We can test MoshBootstrap -// We could use different Strategies for bootstrap -// TODO Just for testing, this needs a version. -// We have decided to hard-code the version of Blink so both can be related. -let MoshServerBinaryName = "mosh-server" -let MoshServerDownloadURL = URL(string: "https://github.com/dtinth/mosh-static/releases/latest/download//(MoshServerBinaryName)")! +// We have decided to hard-code the version of Blink so client-server match. +let MoshServerBinaryName = "mosh-server" +let MoshServerVersion = "1.32.0" +let MoshServerDownloadURL = URL(string: "https://github.com/dtinth/mosh-static/releases/latest/download//(MoshServerBinaryName)-/(MoshServerVersion)")! enum Platform { case Darwin @@ -77,17 +75,11 @@ extension Architecture { } } -// TODO Move this errors out of here as they will be for all mosh -enum MoshBootstrapError: Error { - case NoBinaryAvailable - case NoMoshServerArgs -} - protocol MoshBootstrap { func start(on client: SSHClient) -> AnyPublisher } -// TODO We could enforce a "which", but that is not standard mosh bootstrap. +// NOTE We could enforce "which" on interactive shell, but that is not standard mosh bootstrap. class UseMoshOnPath: MoshBootstrap { let path: String @@ -108,16 +100,15 @@ class UseStaticMosh: MoshBootstrap { self.blinkRemoteLocation = "~/.blink/" } - // Return an AnyPublisher func start(on client: SSHClient) -> AnyPublisher { Just(()) .flatMap { self.platformAndArchitecture(on: client) } .tryMap { pa in guard let platform = pa?.0, let architecture = pa?.1 else { - throw MoshBootstrapError.NoBinaryAvailable + throw MoshError.NoBinaryAvailable } - + return (platform, architecture) } .flatMap { self.getMoshServerBinary(platform: $0, architecture: $1) } @@ -136,7 +127,7 @@ class UseStaticMosh: MoshBootstrap { if lines.count != 3 { return nil } - + guard let platform = Platform(from: lines[0]), let architecture = Architecture(from: lines[1]) else { return nil @@ -145,7 +136,7 @@ class UseStaticMosh: MoshBootstrap { return (platform, architecture) }.eraseToAnyPublisher() } - + private func getMoshServerBinary(platform: Platform, architecture: Architecture) -> AnyPublisher { let localMoshServerURL = BlinkPaths.blinkURL().appending(path: MoshServerBinaryName) return URLSession.shared.dataTaskPublisher(for: MoshServerDownloadURL) @@ -159,7 +150,7 @@ class UseStaticMosh: MoshBootstrap { } .eraseToAnyPublisher() } - + private func installMoshServerBinary(on client: SSHClient, localMoshServerBinary: Translator) -> AnyPublisher { // TODO Try to use .local let RemoteBlinkLocation = ".blink" diff --git a/Blink/Commands/mosh/MoshServerParams.swift b/Blink/Commands/mosh/MoshServerParams.swift index 8dd673ee2..a80dd5a4e 100644 --- a/Blink/Commands/mosh/MoshServerParams.swift +++ b/Blink/Commands/mosh/MoshServerParams.swift @@ -36,7 +36,6 @@ import Foundation struct MoshServerParams { let key: String let udpPort: String - // Only used when we expect the IP to be resolved at the remote. let remoteIP: String? } @@ -54,7 +53,7 @@ extension MoshServerParams { self.udpPort = String(output[Range(connectMatch.range(at: 1), in: output)!]) self.key = String(output[Range(connectMatch.range(at: 2), in: output)!]) } else { - throw MoshBootstrapError.NoMoshServerArgs + throw MoshError.NoMoshServerArgs } let remoteIPPattern = try! NSRegularExpression( diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index 1538b7f73..9b5b8e2f3 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -45,25 +45,41 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { return cmd.start(argc, argv: argv.args(count: argc)) } +// TODO Move this errors out of here as they will be for all mosh +enum MoshError: Error { + case NoBinaryAvailable + case NoMoshServerArgs +} + + +// TODO // struct MoshCommand: ParsableCommand { // } @objc public class BlinkMosh: NSObject { - var sshCancellable: AnyCancellable? + var exitCode: Int32 = 0 + var sshCancellable: AnyCancellable? = nil let device = tty() let currentRunLoop = RunLoop.current + var stdin = InputStream(file: thread_stdin) var stdout = OutputStream(file: thread_stdout) var stderr = OutputStream(file: thread_stderr) var bootstrapSequence: [MoshBootstrap] = [] // TODO A different main will process if there is any initial state first, otherwise // call here. + @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { let host: BKSSHHost let config: SSHClientConfig let hostName: String + let originalRawMode = device.rawMode + defer { + device.rawMode = originalRawMode + } + do { // TODO ssh config from command + ssh setup host = try BKConfig().bkSSHHost("loc")//moshCommand.hostAlias) // extending: moshCommand.bkSSHHost()) @@ -77,34 +93,66 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { let moshServerArgs = getMoshServerArgs(port: nil, colors: nil, exec: nil) // TODO Enforce path only or push-only depending on flags?. - bootstrapSequence = [UseMoshOnPath()] // UseStaticMosh + bootstrapSequence = [UseMoshOnPath(path: "/usr/local/bin/mosh-server")] // UseStaticMosh + var moshServerParams: MoshServerParams? = nil sshCancellable = SSHClient.dial(hostName, with: config) .flatMap { self.startMoshServer(on: $0, args: moshServerArgs) } .sink( - receiveCompletion: { _ in - awake(runLoop: self.currentRunLoop) + receiveCompletion: { completion in + switch completion { + case .failure(let error): + print("Mosh error. \(error)", to: &self.stderr) + self.exitCode = -1 + self.kill() + default: + break + } }, - receiveValue: { moshParams in + receiveValue: { params in // From Combine, output from running mosh-server. - print(moshParams) + print(params) + moshServerParams = params + awake(runLoop: self.currentRunLoop) }) awaitRunLoop(currentRunLoop) - // TODO Connect to server. -// let _selfRef = CFBridgingRetain(self); -// mosh_main( -// _stream.in, _stream.out, &_device->win, -// &__state_callback, (void *)_selfRef, -// [self.sessionParams.ip UTF8String], -// [self.sessionParams.port UTF8String], -// [self.sessionParams.key UTF8String], -// [self.sessionParams.predictionMode UTF8String], -// encodedState.bytes, -// encodedState.length, -// [self.sessionParams.predictOverwrite UTF8String] -// ); + // Early exit if we could not connect + guard let moshServerParams = moshServerParams else { + // TODO Not sure I need this one here as we have no other thread. It should close as-is. + // It does not look like we do. But will keep an eye on Stream Deinit. + //RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) + return exitCode + } + + self.device.rawMode = true + + // TODO Using SSH Host, but the Host may not be resolved, + // we need to expose one level deeper, at the socket level. + var _selfRef = CFBridgingRetain(self); + mosh_main( + self.stdin.file, + self.stdout.file, + &self.device.win, + nil,//&__state_callback, + &_selfRef, + moshServerParams.remoteIP, + moshServerParams.udpPort, + moshServerParams.key, + "adaptive", // predictionMode, + [], // encoded state *CChar U8 + 0, // encoded state bytes + "no" // predictoverwrite + // [self.sessionParams.ip UTF8String], + // [self.sessionParams.port UTF8String], + // [self.sessionParams.key UTF8String], + // [self.sessionParams.predictionMode UTF8String], + // encodedState.bytes, + // encodedState.length, + // [self.sessionParams.predictOverwrite UTF8String] + ) + return 0 } @@ -124,9 +172,9 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { return moshServerArgs.joined(separator: " ") } - private func startMoshServer(on client: SSHClient, args: String) -> AnyPublisher<(), Error> { + private func startMoshServer(on client: SSHClient, args: String) -> AnyPublisher { if bootstrapSequence.isEmpty { - return Fail(error: MoshBootstrapError.NoBinaryAvailable).eraseToAnyPublisher() + return Fail(error: MoshError.NoBinaryAvailable).eraseToAnyPublisher() } return Just(bootstrapSequence.removeFirst()) @@ -150,11 +198,27 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { // TODO If mosh-server run but NoMoshServerArgs, then we crash. String(decoding: $0 as AnyObject as! Data, as: UTF8.self) } - .tryMap { try MoshServerParams(parsing: $0) } - .map { moshParams in - print(moshParams) - return () + .tryMap { output in + // TODO Take into account the way to resolve the IP instead. + var params = try MoshServerParams(parsing: output) + if params.remoteIP == nil { + params = MoshServerParams(key: params.key, udpPort: params.udpPort, remoteIP: client.clientAddressIP()) + } + return params + } + .map { params in + print(params) + return params } .eraseToAnyPublisher() } + + @objc public func kill() { + // Cancelling here makes sure the flows are cancelled. + // Trying to do it at the runloop has the issue that flows may continue running. + print("Kill received") + sshCancellable = nil + + awake(runLoop: currentRunLoop) + } } From 397c80a9cb7f9bd9f5f1e56b00d8c3a24c3e7d6a Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Thu, 14 Sep 2023 15:43:03 -0400 Subject: [PATCH 06/10] Mosh Command parser - Also refactored SSH Command to share same parameter constructs - MoshClientParams as a way to process Hoset Config - command --- Blink.xcodeproj/project.pbxproj | 12 +- Blink/Commands/mosh/MoshClientParams.swift | 48 +++++++ Blink/Commands/mosh/MoshCommand.swift | 149 +++++++++++++++++++++ Blink/Commands/mosh/mosh.swift | 118 +++++++++------- Blink/Commands/ssh/SSHConfig.swift | 25 +++- 5 files changed, 301 insertions(+), 51 deletions(-) create mode 100644 Blink/Commands/mosh/MoshClientParams.swift create mode 100644 Blink/Commands/mosh/MoshCommand.swift diff --git a/Blink.xcodeproj/project.pbxproj b/Blink.xcodeproj/project.pbxproj index 71ad64127..547f81d93 100644 --- a/Blink.xcodeproj/project.pbxproj +++ b/Blink.xcodeproj/project.pbxproj @@ -116,6 +116,8 @@ BD8152542743FF84002BB169 /* skstore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8152532743FF84002BB169 /* skstore.swift */; }; BD818A052AAFC18400956488 /* mosh.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A042AAFC18400956488 /* mosh.swift */; }; BD818A0C2AB120B800956488 /* MoshServerParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A0B2AB120B800956488 /* MoshServerParams.swift */; }; + BD818A132AB3865F00956488 /* MoshCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A122AB3865F00956488 /* MoshCommand.swift */; }; + BD818A152AB3A40100956488 /* MoshClientParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD818A142AB3A40100956488 /* MoshClientParams.swift */; }; BD835DD427A0BD19002C37D7 /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD835DD027A0BD19002C37D7 /* ReplaySubject.swift */; }; BD896F7B26CEAD37004313E6 /* FileTranslatorCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD896F7A26CEAD37004313E6 /* FileTranslatorCache.swift */; }; BD8BBF5525F829B00084705F /* SEKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8BBF0825F819970084705F /* SEKeyTests.swift */; }; @@ -842,6 +844,8 @@ BD8152532743FF84002BB169 /* skstore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = skstore.swift; sourceTree = ""; }; BD818A042AAFC18400956488 /* mosh.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mosh.swift; sourceTree = ""; }; BD818A0B2AB120B800956488 /* MoshServerParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoshServerParams.swift; sourceTree = ""; }; + BD818A122AB3865F00956488 /* MoshCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoshCommand.swift; sourceTree = ""; }; + BD818A142AB3A40100956488 /* MoshClientParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoshClientParams.swift; sourceTree = ""; }; BD835DD027A0BD19002C37D7 /* ReplaySubject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = ""; }; BD896F7A26CEAD37004313E6 /* FileTranslatorCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTranslatorCache.swift; sourceTree = ""; }; BD8BBF0825F819970084705F /* SEKeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEKeyTests.swift; sourceTree = ""; }; @@ -1698,6 +1702,8 @@ children = ( BD818A042AAFC18400956488 /* mosh.swift */, BD33F7802AAA426D00CD16EE /* MoshBootstrap.swift */, + BD818A142AB3A40100956488 /* MoshClientParams.swift */, + BD818A122AB3865F00956488 /* MoshCommand.swift */, BD818A0B2AB120B800956488 /* MoshServerParams.swift */, ); path = mosh; @@ -3332,6 +3338,7 @@ D2A52227231304FF0010AC04 /* UIGestureRecognizer.swift in Sources */, D2AD8E7C27A2BAFA00DED28D /* PurchasePageView.swift in Sources */, D22278012A26204900D4C708 /* EditorViewController.swift in Sources */, + BD818A152AB3A40100956488 /* MoshClientParams.swift in Sources */, D266A9DC272A77A100C85EED /* code.swift in Sources */, D2C24425238E44AB0082C69C /* KeyModifierPicker.swift in Sources */, D259479C269C671F008B5305 /* MoshCustomOptionsPickerView.swift in Sources */, @@ -3415,6 +3422,7 @@ D23EA9592604CB4C00BCF1FF /* FixedTextField.swift in Sources */, D2C24437239104250082C69C /* ShortcutsConfigView.swift in Sources */, B7D450361DD3A87200CE0DBE /* BKiCloudSyncHandler.m in Sources */, + BD818A132AB3865F00956488 /* MoshCommand.swift in Sources */, D248E67622DDDF130057FE67 /* UIStateRestorable.swift in Sources */, C9B2E0341D6B612400B89F69 /* BKTheme.m in Sources */, D241CBD123040734003D64A5 /* KBKeyViewArrows.swift in Sources */, @@ -4899,7 +4907,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 831; + CURRENT_PROJECT_VERSION = 833; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; @@ -4950,7 +4958,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 831; + CURRENT_PROJECT_VERSION = 833; DEAD_CODE_STRIPPING = NO; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = A2H2CL32AG; diff --git a/Blink/Commands/mosh/MoshClientParams.swift b/Blink/Commands/mosh/MoshClientParams.swift new file mode 100644 index 000000000..56cf4d0af --- /dev/null +++ b/Blink/Commands/mosh/MoshClientParams.swift @@ -0,0 +1,48 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2023 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + + +import Foundation + + +struct MoshClientParams { + // let predictionMode: MoshPredictionMode + let customUDPPort: String? + let server: String? + + init(extending cmd: MoshCommand) { + let bkHost = BKHosts.withHost(cmd.hostAlias) + + let customUDPPort: String? = if let moshPort = bkHost?.moshPort { String(describing: moshPort) } else { nil } + self.customUDPPort = cmd.customUDPPort ?? customUDPPort + self.server = cmd.server ?? bkHost?.moshStartup + } +} diff --git a/Blink/Commands/mosh/MoshCommand.swift b/Blink/Commands/mosh/MoshCommand.swift new file mode 100644 index 000000000..1c6d6e5f4 --- /dev/null +++ b/Blink/Commands/mosh/MoshCommand.swift @@ -0,0 +1,149 @@ +////////////////////////////////////////////////////////////////////////////////// +// +// B L I N K +// +// Copyright (C) 2016-2019 Blink Mobile Shell Project +// +// This file is part of Blink. +// +// Blink is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Blink is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Blink. If not, see . +// +// In addition, Blink is also subject to certain additional terms under +// GNU GPL version 3 section 7. +// +// You should have received a copy of these additional terms immediately +// following the terms and conditions of the GNU General Public License +// which accompanied the Blink Source Code. If not, see +// . +// +//////////////////////////////////////////////////////////////////////////////// + +// mosh [options] [user@]host|IP [--] [command] +// "anop:I:P:k:T2" +// {"server", required_argument, 0, 's'}, +// {"predict", required_argument, 0, 'r'}, +// {"port", required_argument, 0, 'p'}, +// {"ip", optional_argument, 0, 'i'}, +// {"key", optional_argument, 0, 'k'}, +// {"no-ssh-pty", optional_argument, 0, 'T'}, +// {"predict-overwrite", no_argument, 0, 'o'}, +// //{"ssh", required_argument, 0, 'S'}, +// {"verbose", no_argument, &_debug, 1}, +// {"help", no_argument, &help, 1}, +// {"experimental-remote-ip", required_argument, 0, 'R'}, +import Foundation +import ArgumentParser + +fileprivate let Version = "1.4.0" + +struct MoshCommand: ParsableCommand { + static var configuration = CommandConfiguration( + abstract: "", + discussion: """ + """, + version: Version) + + @Option(name: .shortAndLong) + var server: String? + + // Mosh Key + @Option( + name: [.customShort("k")], + help: "Use the provided server-side key for mosh connection." + ) + var customKey: String? + + // UDP Port + @Option( + name: [.customShort("p")], + help: "Use a particular server-side UDP port or port range, for example, if this is the only port that is forwarded through a firewall to the server. Otherwise, mosh will choose a port between 60000 and 61000." + ) + var customUDPPort: String? + + // SSH Port + @Option( + name: [.customShort("P")], + help: "Specifies the SSH port to initialize mosh-server on remote host." + ) + var customSSHPort: UInt16? + + // Identity + @Option( + name: [.customShort("i")], + help: .init( + """ + Selects a file from which the identity (private key) for public key authentication is read. The default is ~/.ssh/id_dsa, ~/.ssh/id_ecdsa, ~/.ssh/id_ed25519 and ~/.ssh/id_rsa. Identity files may also be specified on a per-host basis in the configuration pane in the Settings of Blink. + """, + valueName: "identity" + ) + ) + var identityFile: String? + + // TODO Reuse fields + // Connect to User at Host + @Argument(help: "[user@]host[#port]", + transform: { UserAtHostAndPort($0) }) + var userAtHostAndPort: UserAtHostAndPort + var hostAlias: String { userAtHostAndPort.hostAlias } + var user: String? { userAtHostAndPort.user } + var sshPort: UInt16? { + get { if let port = customSSHPort { port } else { userAtHostAndPort.port } } + } + + @Argument( + parsing: .unconditionalRemaining, + help: .init( + "If a is specified, it is executed on the remote host instead of a login shell", + valueName: "remoteCommand" + ) + ) + + fileprivate var cmd: [String] = [] + var remoteCommand: [String] { + get { + if cmd.first == "--" { + return Array(cmd.dropFirst()) + } else { + return cmd + } + } + } +} + +extension MoshCommand { + func bkSSHHost() throws -> BKSSHHost { + var params: [String:Any] = [:] + + if let user = self.user { + params["user"] = user + } + + if let port = self.sshPort { + params["port"] = String(port) + } + + if let identityFile = self.identityFile { + params["identityfile"] = identityFile + } + + // TODO Request tty parameter + // if disableTTY { + // params["requesttty"] = "no" + // } else if forceTTY { + // params["requesttty"] = "force" + // } + + return try BKSSHHost(content: params) + } +} diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index 9b5b8e2f3..208cd4d86 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -45,21 +45,15 @@ public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { return cmd.start(argc, argv: argv.args(count: argc)) } -// TODO Move this errors out of here as they will be for all mosh enum MoshError: Error { case NoBinaryAvailable case NoMoshServerArgs } - -// TODO -// struct MoshCommand: ParsableCommand { -// } - - @objc public class BlinkMosh: NSObject { var exitCode: Int32 = 0 var sshCancellable: AnyCancellable? = nil + var command: MoshCommand! let device = tty() let currentRunLoop = RunLoop.current var stdin = InputStream(file: thread_stdin) @@ -71,52 +65,75 @@ enum MoshError: Error { // call here. @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { - let host: BKSSHHost - let config: SSHClientConfig - let hostName: String - let originalRawMode = device.rawMode defer { device.rawMode = originalRawMode } do { - // TODO ssh config from command + ssh setup - host = try BKConfig().bkSSHHost("loc")//moshCommand.hostAlias) // extending: moshCommand.bkSSHHost()) - hostName = host.hostName! // ?? moshCommand.hostName - config = try SSHClientConfigProvider.config(host: host, using: device) + self.command = try MoshCommand.parse(Array(argv[1...])) } catch { - print("Configuration error - \(error)", to: &stderr) + let message = MoshCommand.message(for: error) + print("\(message)", to: &stderr) return -1 } - let moshServerArgs = getMoshServerArgs(port: nil, colors: nil, exec: nil) + let host: BKSSHHost + let config: SSHClientConfig + let hostName: String + do { + host = try BKConfig().bkSSHHost(self.command.hostAlias, extending: self.command.bkSSHHost()) + hostName = host.hostName ?? self.command.hostAlias + config = try SSHClientConfigProvider.config(host: host, using: device) + } catch { + return die(message: "Configuration error - \(error)") + } - // TODO Enforce path only or push-only depending on flags?. - bootstrapSequence = [UseMoshOnPath(path: "/usr/local/bin/mosh-server")] // UseStaticMosh + // prediction modes, etc... + // IP resolution, + // This will come from host + command + let moshClientParams = MoshClientParams(extending: self.command) var moshServerParams: MoshServerParams? = nil - sshCancellable = SSHClient.dial(hostName, with: config) - .flatMap { self.startMoshServer(on: $0, args: moshServerArgs) } - .sink( - receiveCompletion: { completion in - switch completion { - case .failure(let error): - print("Mosh error. \(error)", to: &self.stderr) - self.exitCode = -1 - self.kill() - default: - break - } - }, - receiveValue: { params in - // From Combine, output from running mosh-server. - print(params) - moshServerParams = params - awake(runLoop: self.currentRunLoop) - }) - - awaitRunLoop(currentRunLoop) + // TODO Figure out how to continue splitting this function? + // If we have a key, we do not need moshServerArgs to query the connection. + if let customKey = self.command.customKey { + // TODO Set exitCode -1 if there is no port - IP + guard let customUDPPort = moshClientParams.customUDPPort else { + return die(message: "If MOSH_KEY is set port is required. (-p)") + } + + moshServerParams = MoshServerParams(key: customKey, udpPort: customUDPPort, remoteIP: nil) + } else { + let moshServerStartupArgs = getMoshServerStartupArgs(udpPort: moshClientParams.customUDPPort, + colors: nil, + exec: self.command.remoteCommand) + + // TODO Enforce path only or push-only depending on flags?. + bootstrapSequence = [UseMoshOnPath(path: moshClientParams.server)] // UseStaticMosh + + sshCancellable = SSHClient.dial(hostName, with: config) + .flatMap { self.startMoshServer(on: $0, args: moshServerStartupArgs) } + .sink( + receiveCompletion: { completion in + switch completion { + case .failure(let error): + print("Mosh error. \(error)", to: &self.stderr) + self.exitCode = -1 + self.kill() + default: + break + } + }, + receiveValue: { params in + // From Combine, output from running mosh-server. + print(params) + moshServerParams = params + awake(runLoop: self.currentRunLoop) + }) + + awaitRunLoop(currentRunLoop) + } // Early exit if we could not connect guard let moshServerParams = moshServerParams else { @@ -156,20 +173,20 @@ enum MoshError: Error { return 0 } - private func getMoshServerArgs(port: String?, + private func getMoshServerStartupArgs(udpPort: String?, colors: String?, - exec: String?) -> String { + exec: [String]?) -> String { // TODO Locale as args - var moshServerArgs = ["new", "-s", "-c", colors ?? "256", "-l LC_ALL=en_US.UTF-8"] + var args = ["new", "-s", "-c", colors ?? "256", "-l LC_ALL=en_US.UTF-8"] - if let port = port { - moshServerArgs.append(contentsOf: ["-p", port]) + if let udpPort = udpPort { + args.append(contentsOf: ["-p", udpPort]) } if let exec = exec { - moshServerArgs.append(contentsOf: ["--", exec]) + args.append(contentsOf: ["--", exec.joined(separator: " ")]) } - return moshServerArgs.joined(separator: " ") + return args.joined(separator: " ") } private func startMoshServer(on client: SSHClient, args: String) -> AnyPublisher { @@ -198,7 +215,7 @@ enum MoshError: Error { // TODO If mosh-server run but NoMoshServerArgs, then we crash. String(decoding: $0 as AnyObject as! Data, as: UTF8.self) } - .tryMap { output in + .tryMap { output in // TODO Take into account the way to resolve the IP instead. var params = try MoshServerParams(parsing: output) if params.remoteIP == nil { @@ -221,4 +238,9 @@ enum MoshError: Error { awake(runLoop: currentRunLoop) } + + func die(message: String) -> Int32 { + print(message, to: &stderr) + return -1 + } } diff --git a/Blink/Commands/ssh/SSHConfig.swift b/Blink/Commands/ssh/SSHConfig.swift index db5c0d65f..513d5e54f 100644 --- a/Blink/Commands/ssh/SSHConfig.swift +++ b/Blink/Commands/ssh/SSHConfig.swift @@ -56,7 +56,7 @@ struct SSHCommand: ParsableCommand { var localForward: [String] = [] // Remote Port forwarding - @Option(name: [.customShort("R")], + @Option(name: [.customShort("R")], help: "port:host:hostport Specifies that the given port on the remote (server) host is to be forwarded to the given host and port on the local side." ) var remoteForward: [String] = [] @@ -346,3 +346,26 @@ enum SSHControlCommands: String, CaseIterable, ExpressibleByArgument { case cancel = "cancel" case stop = "stop" } + +class UserAtHostAndPort { + let user: String? + let hostAlias: String + let port: UInt16? + + init(_ input: String) { + var userAtHost = input.components(separatedBy: "@") + let hostAndPort: String + if userAtHost.count > 1 { + hostAndPort = userAtHost.removeLast() + // A user may have multiple @ symbols + self.user = userAtHost.joined(separator: "@") + } else { + self.user = nil + hostAndPort = userAtHost[0] + } + + var hostAndPortComponents = hostAndPort.components(separatedBy: "#") + self.hostAlias = hostAndPortComponents.removeFirst() + self.port = if hostAndPortComponents.count == 0 { nil } else { UInt16(hostAndPortComponents[0]) } + } +} From 8e28e292a1d693579984151075e657a684b862b6 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Thu, 21 Sep 2023 17:19:48 -0400 Subject: [PATCH 07/10] All command parameters completed --- Blink/Commands/mosh/MoshClientParams.swift | 6 ++-- Blink/Commands/mosh/MoshCommand.swift | 39 ++++++++++++++++++++++ Blink/Commands/mosh/mosh.swift | 5 ++- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/Blink/Commands/mosh/MoshClientParams.swift b/Blink/Commands/mosh/MoshClientParams.swift index 56cf4d0af..4b552192d 100644 --- a/Blink/Commands/mosh/MoshClientParams.swift +++ b/Blink/Commands/mosh/MoshClientParams.swift @@ -32,9 +32,9 @@ import Foundation - struct MoshClientParams { - // let predictionMode: MoshPredictionMode + let predictionMode: BKMoshPrediction + let predictOverwrite: String? let customUDPPort: String? let server: String? @@ -44,5 +44,7 @@ struct MoshClientParams { let customUDPPort: String? = if let moshPort = bkHost?.moshPort { String(describing: moshPort) } else { nil } self.customUDPPort = cmd.customUDPPort ?? customUDPPort self.server = cmd.server ?? bkHost?.moshStartup + self.predictionMode = cmd.predict ?? BKMoshPrediction(UInt32(truncating: bkHost?.prediction ?? 0)) + self.predictOverwrite = cmd.predictOverwrite ? "yes" : bkHost?.moshPredictOverwrite } } diff --git a/Blink/Commands/mosh/MoshCommand.swift b/Blink/Commands/mosh/MoshCommand.swift index 1c6d6e5f4..f03dd7c3d 100644 --- a/Blink/Commands/mosh/MoshCommand.swift +++ b/Blink/Commands/mosh/MoshCommand.swift @@ -57,6 +57,12 @@ struct MoshCommand: ParsableCommand { @Option(name: .shortAndLong) var server: String? + @Option(help: "Prediction mode", + transform: { BKMoshPrediction(parsing: $0) }) + var predict: BKMoshPrediction? + + @Flag var predictOverwrite: Bool = false + // Mosh Key @Option( name: [.customShort("k")], @@ -147,3 +153,36 @@ extension MoshCommand { return try BKSSHHost(content: params) } } + +extension BKMoshPrediction: CustomStringConvertible { + init(parsing: String) { + switch parsing.lowercased() { + case "adaptive": + self = BKMoshPredictionAdaptive + case "always": + self = BKMoshPredictionAlways + case "never": + self = BKMoshPredictionNever + case "experimental": + self = BKMoshPredictionExperimental + default: + self = BKMoshPredictionUnknown + } + } + + public var description: String { + switch self { + case BKMoshPredictionAdaptive: + "adaptive" + case BKMoshPredictionAlways: + "always" + case BKMoshPredictionNever: + "never" + case BKMoshPredictionExperimental: + "experimental" + default: + "unknown" + } + } +} + diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index 208cd4d86..aa6310b96 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -98,7 +98,6 @@ enum MoshError: Error { // TODO Figure out how to continue splitting this function? // If we have a key, we do not need moshServerArgs to query the connection. if let customKey = self.command.customKey { - // TODO Set exitCode -1 if there is no port - IP guard let customUDPPort = moshClientParams.customUDPPort else { return die(message: "If MOSH_KEY is set port is required. (-p)") } @@ -157,10 +156,10 @@ enum MoshError: Error { moshServerParams.remoteIP, moshServerParams.udpPort, moshServerParams.key, - "adaptive", // predictionMode, + String(describing: moshClientParams.predictionMode), [], // encoded state *CChar U8 0, // encoded state bytes - "no" // predictoverwrite + moshClientParams.predictOverwrite // predictoverwrite // [self.sessionParams.ip UTF8String], // [self.sessionParams.port UTF8String], // [self.sessionParams.key UTF8String], From 524a90bbaf1c3779f35e3f08dd44e6b8d9484ab2 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Tue, 26 Sep 2023 13:35:16 -0400 Subject: [PATCH 08/10] Mosh as Session Draft --- Blink/Blink-bridge.h | 2 + Blink/Commands/mosh/mosh.swift | 86 ++++++++++++++++++++++++---------- Sessions/MCPSession.m | 29 +++++++----- 3 files changed, 80 insertions(+), 37 deletions(-) diff --git a/Blink/Blink-bridge.h b/Blink/Blink-bridge.h index 7dcd35b87..a7dca6741 100644 --- a/Blink/Blink-bridge.h +++ b/Blink/Blink-bridge.h @@ -48,6 +48,8 @@ extern void __thread_ssh_execute_command(const char *command, socket_t in, socke extern int ios_dup2(int fd1, int fd2); extern void ios_exit(int errorCode) __dead2; // set error code and exits from the thread. +typedef void (*mosh_state_callback) (const void *context, const void *buffer, size_t size); + #import "BLKDefaults.h" #import "UIDevice+DeviceName.h" #import "BKHosts.h" diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index aa6310b96..9189de92d 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -34,35 +34,51 @@ import Combine import SSH import ios_system -@_cdecl("blink_mosh_main") -public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { - setvbuf(thread_stdin, nil, _IONBF, 0) - setvbuf(thread_stdout, nil, _IONBF, 0) - setvbuf(thread_stderr, nil, _IONBF, 0) - - let session = Unmanaged.fromOpaque(thread_context).takeUnretainedValue() - let cmd = BlinkMosh() - return cmd.start(argc, argv: argv.args(count: argc)) -} +// @_cdecl("blink_mosh_main") +// public func blink_mosh_main(argc: Int32, argv: Argv) -> Int32 { +// setvbuf(thread_stdin, nil, _IONBF, 0) +// setvbuf(thread_stdout, nil, _IONBF, 0) +// setvbuf(thread_stderr, nil, _IONBF, 0) + +// let session = Unmanaged.fromOpaque(thread_context).takeUnretainedValue() +// // TODO How about register and deregister here? +// let cmd = BlinkMosh() +// return cmd.start(argc, argv: argv.args(count: argc)) +// } enum MoshError: Error { case NoBinaryAvailable case NoMoshServerArgs } -@objc public class BlinkMosh: NSObject { +@objc public class BlinkMosh: Session { var exitCode: Int32 = 0 var sshCancellable: AnyCancellable? = nil var command: MoshCommand! - let device = tty() + // let device = tty() let currentRunLoop = RunLoop.current - var stdin = InputStream(file: thread_stdin) - var stdout = OutputStream(file: thread_stdout) - var stderr = OutputStream(file: thread_stderr) + var stdin: InputStream! + var stdout: OutputStream! + var stderr: OutputStream! var bootstrapSequence: [MoshBootstrap] = [] + var moshParams: MoshParams? = nil + let mcpSession: MCPSession + + let stateCallback: mosh_state_callback = { (context, buffer, size) in + // TODO buffer nil? + let data = Data(bytes: buffer!, count: size) + let session = Unmanaged.fromOpaque(context!).takeUnretainedValue() + session.onStateEncoded(data) + } - // TODO A different main will process if there is any initial state first, otherwise - // call here. + @objc init!(mcpSession: MCPSession, device: TermDevice!, andParams params: SessionParams!) { + self.mcpSession = mcpSession + super.init(device: device, andParams: params) + + self.stdin = InputStream(file: stream.in) + self.stdout = OutputStream(file: stream.out) + self.stderr = OutputStream(file: stream.err) + } @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { let originalRawMode = device.rawMode @@ -138,12 +154,15 @@ enum MoshError: Error { guard let moshServerParams = moshServerParams else { // TODO Not sure I need this one here as we have no other thread. It should close as-is. // It does not look like we do. But will keep an eye on Stream Deinit. - //RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) + RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) return exitCode } self.device.rawMode = true + let moshParams = self.moshParams ?? MoshParams(server: moshServerParams, client: moshClientParams) + self.sessionParams = moshParams + // TODO Using SSH Host, but the Host may not be resolved, // we need to expose one level deeper, at the socket level. var _selfRef = CFBridgingRetain(self); @@ -153,13 +172,13 @@ enum MoshError: Error { &self.device.win, nil,//&__state_callback, &_selfRef, - moshServerParams.remoteIP, - moshServerParams.udpPort, - moshServerParams.key, - String(describing: moshClientParams.predictionMode), + moshParams.ip, + moshParams.port, + moshParams.key, + moshParams.predictionMode, [], // encoded state *CChar U8 0, // encoded state bytes - moshClientParams.predictOverwrite // predictoverwrite + moshParams.predictOverwrite // predictoverwrite // [self.sessionParams.ip UTF8String], // [self.sessionParams.port UTF8String], // [self.sessionParams.key UTF8String], @@ -229,7 +248,11 @@ enum MoshError: Error { .eraseToAnyPublisher() } - @objc public func kill() { + func onStateEncoded(_ encodedState: Data) { + // self.encodedState = encodedState + } + + @objc public override func kill() { // Cancelling here makes sure the flows are cancelled. // Trying to do it at the runloop has the issue that flows may continue running. print("Kill received") @@ -243,3 +266,18 @@ enum MoshError: Error { return -1 } } + +extension MoshParams { + convenience init(server: MoshServerParams, client: MoshClientParams) { + self.init() + + self.key = server.key + self.port = server.udpPort + self.ip = server.remoteIP + self.predictionMode = String(describing: client.predictionMode) + self.predictOverwrite = client.predictOverwrite + self.serverPath = client.server + // TODO self.startupCmd - maybe even use on SSH as well? + // TODO self.experimentalRemoteIp + } +} diff --git a/Sessions/MCPSession.m b/Sessions/MCPSession.m index 15a132c5e..4ebcd95e2 100644 --- a/Sessions/MCPSession.m +++ b/Sessions/MCPSession.m @@ -99,18 +99,19 @@ - (void)executeWithArgs:(NSString *)args { thread_stderr = nil; ios_setStreams(_stream.in, _stream.out, _stream.err); - + // We are restoring mosh session if possible first. - if ([@"mosh" isEqualToString:self.sessionParams.childSessionType] && self.sessionParams.hasEncodedState) { - MoshSession *mosh = [[MoshSession alloc] initWithDevice:_device andParams:self.sessionParams.childSessionParams]; - mosh.mcpSession = self; - _childSession = mosh; - [_childSession executeAttachedWithArgs:@""]; - _childSession = nil; - if (self.sessionParams.hasEncodedState) { - return; - } - } + // TODO Restore BlinkMosh +// if ([@"mosh" isEqualToString:self.sessionParams.childSessionType] && self.sessionParams.hasEncodedState) { +// MoshSession *mosh = [[MoshSession alloc] initWithDevice:_device andParams:self.sessionParams.childSessionParams]; +// mosh.mcpSession = self; +// _childSession = mosh; +// [_childSession executeAttachedWithArgs:@""]; +// _childSession = nil; +// if (self.sessionParams.hasEncodedState) { +// return; +// } +// } #if TARGET_OS_MACCATALYST BKHosts *localhost = [BKHosts withHost:@"localhost"]; if (localhost) { @@ -275,8 +276,10 @@ - (void)_runMoshWithArgs:(NSString *)args { self.sessionParams.childSessionParams = [[MoshParams alloc] init]; self.sessionParams.childSessionType = @"mosh"; - MoshSession *mosh = [[MoshSession alloc] initWithDevice:_device andParams:self.sessionParams.childSessionParams]; - mosh.mcpSession = self; + BlinkMosh *mosh = [[BlinkMosh alloc] initWithMcpSession: self device:_device andParams:self.sessionParams.childSessionParams]; + // TODO Connect previous mosh + //MoshSession *mosh = [[MoshSession alloc] initWithDevice:_device andParams:self.sessionParams.childSessionParams]; + //mosh.mcpSession = self; _childSession = mosh; // duplicate args From 40db1917b07cc4fe63e2f574c62fd4fa89bb0733 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Tue, 26 Sep 2023 18:12:42 -0400 Subject: [PATCH 09/10] Session copied from MoshParameters. - Started to draft how to initialze it. --- Blink/Commands/mosh/mosh.swift | 116 ++++++++++++++++++++++++++++----- Sessions/MCPSession.m | 21 +++--- Sessions/SessionParams.swift | 11 ++++ 3 files changed, 123 insertions(+), 25 deletions(-) diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index 9189de92d..4cba893d1 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -30,6 +30,7 @@ //////////////////////////////////////////////////////////////////////////////// import Combine +import Dispatch import SSH import ios_system @@ -54,15 +55,16 @@ enum MoshError: Error { @objc public class BlinkMosh: Session { var exitCode: Int32 = 0 var sshCancellable: AnyCancellable? = nil - var command: MoshCommand! + //var command: MoshCommand! // let device = tty() let currentRunLoop = RunLoop.current var stdin: InputStream! var stdout: OutputStream! var stderr: OutputStream! var bootstrapSequence: [MoshBootstrap] = [] - var moshParams: MoshParams? = nil - let mcpSession: MCPSession + private var initialMoshParams: MoshParams? = nil + private let mcpSession: MCPSession + private var suspendSemaphore: DispatchSemaphore? = nil let stateCallback: mosh_state_callback = { (context, buffer, size) in // TODO buffer nil? @@ -80,12 +82,44 @@ enum MoshError: Error { self.stderr = OutputStream(file: stream.err) } - @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { - let originalRawMode = device.rawMode - defer { - device.rawMode = originalRawMode + @objc public override func main(_ argc: Int32, argv: Argv) -> Int32 { + mcpSession.setActiveSession() + + // TODO MOSH_ESCAPE_KEY + + // In objc, sessionParams is a covariable for moshparams. + // Here we need to force transform. + if let initialMoshParams = self.sessionParams as? MoshParams, + let _ = initialMoshParams.encodedState { + moshMain(initialMoshParams) + return 0 + } else { + let command: MoshCommand + do { + command = try MoshCommand.parse(argv.args(count: argc)) + //command = try MoshCommand.parse(Array(argv[1...])) + } catch { + let message = MoshCommand.message(for: error) + print("\(message)", to: &stderr) + return -1 + } + + let moshParams: MoshParams + do { + try connectMoshServer(command) + } catch { + return die(message: "(\(error)") + } + + //return start(argc, argv: argv.args(count: argc)) } + } + func connectMoshServer(_ command: MoshCommand) throws { + + } + + @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { do { self.command = try MoshCommand.parse(Array(argv[1...])) } catch { @@ -160,18 +194,21 @@ enum MoshError: Error { self.device.rawMode = true - let moshParams = self.moshParams ?? MoshParams(server: moshServerParams, client: moshClientParams) - self.sessionParams = moshParams + // TODO the self.initialMoshParams here may not be needed anymore. Depends on how we structure the main now. + let moshParams = self.initialMoshParams ?? MoshParams(server: moshServerParams, client: moshClientParams) + self.copyToSession(moshParams: moshParams) + // TODO This is incorrect. We cannot replace sessionParams as there are multiple objects pointing to it. + // We need to copy moshParams into sessionParams // TODO Using SSH Host, but the Host may not be resolved, // we need to expose one level deeper, at the socket level. - var _selfRef = CFBridgingRetain(self); + let _selfRef = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) mosh_main( self.stdin.file, self.stdout.file, &self.device.win, - nil,//&__state_callback, - &_selfRef, + self.stateCallback, + _selfRef, moshParams.ip, moshParams.port, moshParams.key, @@ -191,6 +228,36 @@ enum MoshError: Error { return 0 } + private func moshMain(_ moshParams: MoshParams) { + let originalRawMode = device.rawMode + defer { + device.rawMode = originalRawMode + } + + let _selfRef = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + mosh_main( + self.stdin.file, + self.stdout.file, + &self.device.win, + self.stateCallback, + _selfRef, + moshParams.ip, + moshParams.port, + moshParams.key, + moshParams.predictionMode, + [], // encoded state *CChar U8 + 0, // encoded state bytes + moshParams.predictOverwrite // predictoverwrite + // [self.sessionParams.ip UTF8String], + // [self.sessionParams.port UTF8String], + // [self.sessionParams.key UTF8String], + // [self.sessionParams.predictionMode UTF8String], + // encodedState.bytes, + // encodedState.length, + // [self.sessionParams.predictOverwrite UTF8String] + ) + } + private func getMoshServerStartupArgs(udpPort: String?, colors: String?, exec: [String]?) -> String { @@ -248,8 +315,10 @@ enum MoshError: Error { .eraseToAnyPublisher() } - func onStateEncoded(_ encodedState: Data) { - // self.encodedState = encodedState + private func copyToSession(moshParams: MoshParams) { + if let sessionParams = self.sessionParams as? MoshParams { + sessionParams.copy(from: moshParams) + } } @objc public override func kill() { @@ -257,10 +326,27 @@ enum MoshError: Error { // Trying to do it at the runloop has the issue that flows may continue running. print("Kill received") sshCancellable = nil - awake(runLoop: currentRunLoop) } + @objc public override func suspend() { + suspendSemaphore = DispatchSemaphore(value: 0) + // TODO NSString stringWithFormat:@"%@%@", _escapeKey ?: @"\x1e", @"\x1a" + self.device.write(String("\u{1e}\u{1a}")) + print("Session suspend called") + let _ = suspendSemaphore!.wait(timeout: (DispatchTime.now() + 2.0)) + print("Session suspended") + //dispatch_semaphore_wait(_sema, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); + } + + func onStateEncoded(_ encodedState: Data) { + self.sessionParams.encodedState = encodedState + print("Encoding session") + if let sema = suspendSemaphore { + sema.signal() + } + } + func die(message: String) -> Int32 { print(message, to: &stderr) return -1 diff --git a/Sessions/MCPSession.m b/Sessions/MCPSession.m index 4ebcd95e2..769d2cb73 100644 --- a/Sessions/MCPSession.m +++ b/Sessions/MCPSession.m @@ -102,16 +102,17 @@ - (void)executeWithArgs:(NSString *)args { // We are restoring mosh session if possible first. // TODO Restore BlinkMosh -// if ([@"mosh" isEqualToString:self.sessionParams.childSessionType] && self.sessionParams.hasEncodedState) { -// MoshSession *mosh = [[MoshSession alloc] initWithDevice:_device andParams:self.sessionParams.childSessionParams]; -// mosh.mcpSession = self; -// _childSession = mosh; -// [_childSession executeAttachedWithArgs:@""]; -// _childSession = nil; -// if (self.sessionParams.hasEncodedState) { -// return; -// } -// } + if ([@"mosh" isEqualToString:self.sessionParams.childSessionType] && self.sessionParams.hasEncodedState) { + BlinkMosh *mosh = [[BlinkMosh alloc] initWithMcpSession: self device:_device andParams:self.sessionParams.childSessionParams]; +// MoshSession *mosh = [[MoshSession alloc] initWithDevice:_device andParams:self.sessionParams.childSessionParams]; +// mosh.mcpSession = self; + _childSession = mosh; + [_childSession executeAttachedWithArgs:@""]; + _childSession = nil; + if (self.sessionParams.hasEncodedState) { + return; + } + } #if TARGET_OS_MACCATALYST BKHosts *localhost = [BKHosts withHost:@"localhost"]; if (localhost) { diff --git a/Sessions/SessionParams.swift b/Sessions/SessionParams.swift index 015035ee1..055d5e40c 100644 --- a/Sessions/SessionParams.swift +++ b/Sessions/SessionParams.swift @@ -117,6 +117,17 @@ import UIKit static var secureCoding2 = true override class var supportsSecureCoding: Bool { secureCoding2 } + + public func copy(from params: MoshParams) { + self.ip = params.ip + self.port = params.port + self.key = params.key + self.predictionMode = params.predictionMode + self.predictOverwrite = params.predictOverwrite + self.startupCmd = params.startupCmd + self.serverPath = params.serverPath + self.experimentalRemoteIp = params.experimentalRemoteIp + } } @objc class MCPParams: SessionParams { From 28b87dbee2072f4705e0b115f8250f1f3c4c9059 Mon Sep 17 00:00:00 2001 From: Carlos Cabanero Date: Wed, 27 Sep 2023 12:44:15 -0400 Subject: [PATCH 10/10] State restoration Re-did the whole mosh startup flow --- Blink/Commands/mosh/mosh.swift | 165 ++++++++++++--------------------- 1 file changed, 57 insertions(+), 108 deletions(-) diff --git a/Blink/Commands/mosh/mosh.swift b/Blink/Commands/mosh/mosh.swift index 4cba893d1..72701204d 100644 --- a/Blink/Commands/mosh/mosh.swift +++ b/Blink/Commands/mosh/mosh.swift @@ -50,6 +50,7 @@ import ios_system enum MoshError: Error { case NoBinaryAvailable case NoMoshServerArgs + case StartMoshServerError(String) } @objc public class BlinkMosh: Session { @@ -57,7 +58,7 @@ enum MoshError: Error { var sshCancellable: AnyCancellable? = nil //var command: MoshCommand! // let device = tty() - let currentRunLoop = RunLoop.current + var currentRunLoop: RunLoop! var stdin: InputStream! var stdout: OutputStream! var stderr: OutputStream! @@ -84,157 +85,110 @@ enum MoshError: Error { @objc public override func main(_ argc: Int32, argv: Argv) -> Int32 { mcpSession.setActiveSession() - + self.currentRunLoop = RunLoop.current // TODO MOSH_ESCAPE_KEY - // In objc, sessionParams is a covariable for moshparams. - // Here we need to force transform. + // In ObjC, sessionParams is a covariable for MoshParams. + // In Swift we need to cast. if let initialMoshParams = self.sessionParams as? MoshParams, let _ = initialMoshParams.encodedState { - moshMain(initialMoshParams) - return 0 + return moshMain(initialMoshParams) } else { let command: MoshCommand do { - command = try MoshCommand.parse(argv.args(count: argc)) - //command = try MoshCommand.parse(Array(argv[1...])) + command = try MoshCommand.parse(Array(argv.args(count: argc)[1...])) } catch { let message = MoshCommand.message(for: error) - print("\(message)", to: &stderr) - return -1 + return die(message: message) } - + let moshParams: MoshParams do { - try connectMoshServer(command) + moshParams = try startMoshServer(using: command) + self.copyToSession(moshParams: moshParams) } catch { return die(message: "(\(error)") } - - //return start(argc, argv: argv.args(count: argc)) - } - } - func connectMoshServer(_ command: MoshCommand) throws { - - } - - @objc public func start(_ argc: Int32, argv: [String]) -> Int32 { - do { - self.command = try MoshCommand.parse(Array(argv[1...])) - } catch { - let message = MoshCommand.message(for: error) - print("\(message)", to: &stderr) - return -1 + return moshMain(moshParams) } + } + func startMoshServer(using command: MoshCommand) throws -> MoshParams { let host: BKSSHHost let config: SSHClientConfig let hostName: String - do { - host = try BKConfig().bkSSHHost(self.command.hostAlias, extending: self.command.bkSSHHost()) - hostName = host.hostName ?? self.command.hostAlias - config = try SSHClientConfigProvider.config(host: host, using: device) - } catch { - return die(message: "Configuration error - \(error)") - } - // prediction modes, etc... - // IP resolution, - // This will come from host + command - let moshClientParams = MoshClientParams(extending: self.command) + // TODO Wrap the error into a SSHconfig error or dump it as-is. + host = try BKConfig().bkSSHHost(command.hostAlias, extending: command.bkSSHHost()) + hostName = host.hostName ?? command.hostAlias + config = try SSHClientConfigProvider.config(host: host, using: device) - var moshServerParams: MoshServerParams? = nil - // TODO Figure out how to continue splitting this function? - // If we have a key, we do not need moshServerArgs to query the connection. - if let customKey = self.command.customKey { + // TODO IP resolution, + // This will come from host + command + let moshClientParams = MoshClientParams(extending: command) + let moshServerParams: MoshServerParams + if let customKey = command.customKey { guard let customUDPPort = moshClientParams.customUDPPort else { - return die(message: "If MOSH_KEY is set port is required. (-p)") + throw MoshError.StartMoshServerError("If MOSH_KEY is set, port is required. (-p)") } moshServerParams = MoshServerParams(key: customKey, udpPort: customUDPPort, remoteIP: nil) } else { let moshServerStartupArgs = getMoshServerStartupArgs(udpPort: moshClientParams.customUDPPort, colors: nil, - exec: self.command.remoteCommand) + exec: command.remoteCommand) // TODO Enforce path only or push-only depending on flags?. bootstrapSequence = [UseMoshOnPath(path: moshClientParams.server)] // UseStaticMosh - sshCancellable = SSHClient.dial(hostName, with: config) + var sshError: Error? = nil + var _moshServerParams: MoshServerParams? = nil + self.sshCancellable = SSHClient.dial(hostName, with: config) .flatMap { self.startMoshServer(on: $0, args: moshServerStartupArgs) } + // .print() .sink( receiveCompletion: { completion in switch completion { case .failure(let error): - print("Mosh error. \(error)", to: &self.stderr) - self.exitCode = -1 + sshError = error self.kill() - default: - break + return + case .finished: + awake(runLoop: self.currentRunLoop) } }, receiveValue: { params in // From Combine, output from running mosh-server. - print(params) - moshServerParams = params - awake(runLoop: self.currentRunLoop) + _moshServerParams = params }) awaitRunLoop(currentRunLoop) - } - // Early exit if we could not connect - guard let moshServerParams = moshServerParams else { - // TODO Not sure I need this one here as we have no other thread. It should close as-is. - // It does not look like we do. But will keep an eye on Stream Deinit. - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.1)) - return exitCode - } - - self.device.rawMode = true - - // TODO the self.initialMoshParams here may not be needed anymore. Depends on how we structure the main now. - let moshParams = self.initialMoshParams ?? MoshParams(server: moshServerParams, client: moshClientParams) - self.copyToSession(moshParams: moshParams) - // TODO This is incorrect. We cannot replace sessionParams as there are multiple objects pointing to it. - // We need to copy moshParams into sessionParams + if let error = sshError { + throw error + } - // TODO Using SSH Host, but the Host may not be resolved, - // we need to expose one level deeper, at the socket level. - let _selfRef = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) - mosh_main( - self.stdin.file, - self.stdout.file, - &self.device.win, - self.stateCallback, - _selfRef, - moshParams.ip, - moshParams.port, - moshParams.key, - moshParams.predictionMode, - [], // encoded state *CChar U8 - 0, // encoded state bytes - moshParams.predictOverwrite // predictoverwrite - // [self.sessionParams.ip UTF8String], - // [self.sessionParams.port UTF8String], - // [self.sessionParams.key UTF8String], - // [self.sessionParams.predictionMode UTF8String], - // encodedState.bytes, - // encodedState.length, - // [self.sessionParams.predictOverwrite UTF8String] - ) + guard let _moshServerParams = _moshServerParams else { + throw MoshError.NoMoshServerArgs + } + moshServerParams = _moshServerParams + } - return 0 + return MoshParams(server: moshServerParams, client: moshClientParams) } - private func moshMain(_ moshParams: MoshParams) { + private func moshMain(_ moshParams: MoshParams) -> Int32 { let originalRawMode = device.rawMode + self.device.rawMode = true + defer { device.rawMode = originalRawMode } - + let _selfRef = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + let encodedState = [UInt8](moshParams.encodedState ?? Data()) + mosh_main( self.stdin.file, self.stdout.file, @@ -245,19 +199,14 @@ enum MoshError: Error { moshParams.port, moshParams.key, moshParams.predictionMode, - [], // encoded state *CChar U8 - 0, // encoded state bytes - moshParams.predictOverwrite // predictoverwrite - // [self.sessionParams.ip UTF8String], - // [self.sessionParams.port UTF8String], - // [self.sessionParams.key UTF8String], - // [self.sessionParams.predictionMode UTF8String], - // encodedState.bytes, - // encodedState.length, - // [self.sessionParams.predictOverwrite UTF8String] + encodedState, + encodedState.count, + moshParams.predictOverwrite ) + + return 0 } - + private func getMoshServerStartupArgs(udpPort: String?, colors: String?, exec: [String]?) -> String { @@ -325,8 +274,8 @@ enum MoshError: Error { // Cancelling here makes sure the flows are cancelled. // Trying to do it at the runloop has the issue that flows may continue running. print("Kill received") - sshCancellable = nil awake(runLoop: currentRunLoop) + sshCancellable = nil } @objc public override func suspend() { @@ -346,7 +295,7 @@ enum MoshError: Error { sema.signal() } } - + func die(message: String) -> Int32 { print(message, to: &stderr) return -1