From e337896f79abd901cd3c46a76215dd4f80579782 Mon Sep 17 00:00:00 2001 From: Chung Shing Hin Date: Thu, 31 Aug 2023 17:40:11 +0800 Subject: [PATCH 1/2] ssh: Port forwarding --- Code.xcodeproj/project.pbxproj | 18 +++ .../Localization/de.lproj/Localizable.strings | 10 ++ .../Localization/en.lproj/Localizable.strings | 10 ++ .../Localization/ja.lproj/Localizable.strings | 10 ++ .../Localization/ko.lproj/Localizable.strings | 10 ++ .../zh-Hans.lproj/Localizable.strings | 10 ++ CodeApp/Managers/ActivityBarManager.swift | 15 +-- .../FTP/FTPFileSystemProvider.swift | 1 + .../FileSystem/FileSystemProvider.swift | 1 + .../Local/LocalFileSystemProvider.swift | 1 + .../PortForwardServiceProvider.swift | 30 +++++ .../SFTP/SFTPFileSystemProvider.swift | 57 +++++++++- .../FileSystem/WorkSpaceStorage.swift | 5 +- CodeApp/Managers/MainApp.swift | 12 +- CodeApp/Views/ActivityBar.swift | 1 + CodeApp/Views/ActivityBarItemView.swift | 6 +- CodeApp/Views/RegularSidebar.swift | 2 +- .../RemoteAuxiliaryExtension.swift | 17 +++ .../Views/PortForwardContainer.swift | 92 +++++++++++++++ .../Views/RemotePortForwardSection.swift | 106 ++++++++++++++++++ downloadFrameworks.sh | 2 +- 21 files changed, 400 insertions(+), 16 deletions(-) create mode 100644 CodeApp/Managers/FileSystem/PortForwardServiceProvider.swift create mode 100644 Extensions/RemoteAuxiliary/Views/PortForwardContainer.swift create mode 100644 Extensions/RemoteAuxiliary/Views/RemotePortForwardSection.swift diff --git a/Code.xcodeproj/project.pbxproj b/Code.xcodeproj/project.pbxproj index 082a85f47..02453e78d 100644 --- a/Code.xcodeproj/project.pbxproj +++ b/Code.xcodeproj/project.pbxproj @@ -716,6 +716,12 @@ 9FC03E72291F6FE000DECD1B /* ExplorerFileTreeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC03E70291F6FE000DECD1B /* ExplorerFileTreeSection.swift */; }; 9FC03E742920CF1700DECD1B /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC03E732920CF1700DECD1B /* Notification.swift */; }; 9FC03E752920CF1700DECD1B /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC03E732920CF1700DECD1B /* Notification.swift */; }; + 9FC6737E2AA03A4800346FD7 /* RemotePortForwardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC6737D2AA03A4800346FD7 /* RemotePortForwardSection.swift */; }; + 9FC6737F2AA03A4800346FD7 /* RemotePortForwardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC6737D2AA03A4800346FD7 /* RemotePortForwardSection.swift */; }; + 9FC673812AA057F100346FD7 /* PortForwardContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC673802AA057F100346FD7 /* PortForwardContainer.swift */; }; + 9FC673822AA057F100346FD7 /* PortForwardContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC673802AA057F100346FD7 /* PortForwardContainer.swift */; }; + 9FC673842AA068EE00346FD7 /* PortForwardServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC673832AA068EE00346FD7 /* PortForwardServiceProvider.swift */; }; + 9FC673852AA068EE00346FD7 /* PortForwardServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC673832AA068EE00346FD7 /* PortForwardServiceProvider.swift */; }; 9FD0FAF02938A14C007170F5 /* EncodingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD0FAEF2938A14C007170F5 /* EncodingMenu.swift */; }; 9FD0FAF12938A14C007170F5 /* EncodingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD0FAEF2938A14C007170F5 /* EncodingMenu.swift */; }; 9FD0FAF42939AD86007170F5 /* SimpleWebPreviewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD0FAF32939AD86007170F5 /* SimpleWebPreviewExtension.swift */; }; @@ -1743,6 +1749,9 @@ 9FC03E6D291F6EE700DECD1B /* ExplorerEditorListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerEditorListSection.swift; sourceTree = ""; }; 9FC03E70291F6FE000DECD1B /* ExplorerFileTreeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExplorerFileTreeSection.swift; sourceTree = ""; }; 9FC03E732920CF1700DECD1B /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + 9FC6737D2AA03A4800346FD7 /* RemotePortForwardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePortForwardSection.swift; sourceTree = ""; }; + 9FC673802AA057F100346FD7 /* PortForwardContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortForwardContainer.swift; sourceTree = ""; }; + 9FC673832AA068EE00346FD7 /* PortForwardServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortForwardServiceProvider.swift; sourceTree = ""; }; 9FD0FAEF2938A14C007170F5 /* EncodingMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodingMenu.swift; sourceTree = ""; }; 9FD0FAF32939AD86007170F5 /* SimpleWebPreviewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleWebPreviewExtension.swift; sourceTree = ""; }; 9FD5BCE42923897200F20C4B /* PanelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelManager.swift; sourceTree = ""; }; @@ -2421,6 +2430,7 @@ 941969242802AB2C008AAEB2 /* FileSystemProvider.swift */, 94A0461228049A7A00182275 /* GitServiceProvider.swift */, 942E3200280877C900233441 /* SearchServiceProvider.swift */, + 9FC673832AA068EE00346FD7 /* PortForwardServiceProvider.swift */, ); path = FileSystem; sourceTree = ""; @@ -2644,7 +2654,9 @@ 9FA122742A8B5A3B00E7B417 /* Views */ = { isa = PBXGroup; children = ( + 9FC6737D2AA03A4800346FD7 /* RemotePortForwardSection.swift */, 9FA122752A8B5A4800E7B417 /* RemoteConnectedLabel.swift */, + 9FC673802AA057F100346FD7 /* PortForwardContainer.swift */, ); path = Views; sourceTree = ""; @@ -3064,6 +3076,7 @@ 94196944280316C7008AAEB2 /* getRootDirectory.swift in Sources */, 94A045F5280481A900182275 /* RemoteTypeLabel.swift in Sources */, 94A045FF2804853200182275 /* DescriptionText.swift in Sources */, + 9FC673822AA057F100346FD7 /* PortForwardContainer.swift in Sources */, 94196945280316C7008AAEB2 /* SourceControlContainer.swift in Sources */, 94196946280316C7008AAEB2 /* shortcutsMapping.swift in Sources */, 94196947280316C7008AAEB2 /* BottomBar.swift in Sources */, @@ -3099,6 +3112,7 @@ 94196959280316C7008AAEB2 /* SearchManager.swift in Sources */, 9419695A280316C7008AAEB2 /* ArchiveDir.swift in Sources */, 9419695B280316C7008AAEB2 /* MarkdownView.swift in Sources */, + 9FC6737F2AA03A4800346FD7 /* RemotePortForwardSection.swift in Sources */, 9F046C3629222D8E00BDE4E9 /* ExtensionManager.swift in Sources */, 9FD5BCFC2928E02300F20C4B /* LocalExecutionExtension.swift in Sources */, 9419695C280316C7008AAEB2 /* MonacoEditor.swift in Sources */, @@ -3126,6 +3140,7 @@ 94196967280316C7008AAEB2 /* Executor.swift in Sources */, 94196968280316C7008AAEB2 /* RemoteAuthView.swift in Sources */, 94196969280316C7008AAEB2 /* CodeApp.swift in Sources */, + 9FC673852AA068EE00346FD7 /* PortForwardServiceProvider.swift in Sources */, 9419696A280316C7008AAEB2 /* openFilesApp.swift in Sources */, 9419696B280316C7008AAEB2 /* View+If.swift in Sources */, 9419696C280316C7008AAEB2 /* WorkSpaceStorage.swift in Sources */, @@ -3232,6 +3247,7 @@ 94A7782D257BCEE2008FE7B2 /* getRootDirectory.swift in Sources */, 94A045F4280481A900182275 /* RemoteTypeLabel.swift in Sources */, 94A045FE2804853200182275 /* DescriptionText.swift in Sources */, + 9FC673812AA057F100346FD7 /* PortForwardContainer.swift in Sources */, 94A777E0257B8D8E008FE7B2 /* SourceControlContainer.swift in Sources */, 94C893E6279D0C3000DFBF29 /* shortcutsMapping.swift in Sources */, 94A7FFB3268D085300369147 /* BottomBar.swift in Sources */, @@ -3267,6 +3283,7 @@ 947BF349262453040015DAEB /* SearchManager.swift in Sources */, 94A7781E257BC473008FE7B2 /* ArchiveDir.swift in Sources */, 94A5682B257CBDE4008A6530 /* MarkdownView.swift in Sources */, + 9FC6737E2AA03A4800346FD7 /* RemotePortForwardSection.swift in Sources */, 9F046C3529222D8E00BDE4E9 /* ExtensionManager.swift in Sources */, 9FD5BCFB2928E02300F20C4B /* LocalExecutionExtension.swift in Sources */, 94A777AC257B66CF008FE7B2 /* MonacoEditor.swift in Sources */, @@ -3294,6 +3311,7 @@ 948D12222583F2A5008F877A /* Executor.swift in Sources */, 9437155B26C3C745000376FB /* RemoteAuthView.swift in Sources */, 944EEBF42563C381009D77FE /* CodeApp.swift in Sources */, + 9FC673842AA068EE00346FD7 /* PortForwardServiceProvider.swift in Sources */, 94A777DC257B8C99008FE7B2 /* openFilesApp.swift in Sources */, 94B3DDFA260526D200C4F2B1 /* View+If.swift in Sources */, 94A777A2257ABDC3008FE7B2 /* WorkSpaceStorage.swift in Sources */, diff --git a/CodeApp/Localization/de.lproj/Localizable.strings b/CodeApp/Localization/de.lproj/Localizable.strings index e3bc26814..fb448d358 100644 --- a/CodeApp/Localization/de.lproj/Localizable.strings +++ b/CodeApp/Localization/de.lproj/Localizable.strings @@ -410,6 +410,7 @@ "common.import" = "Importieren"; "common.add" = "Hinzufügen"; "common.create" = "Erstellen"; +"common.remove" = "Entfernen"; "notification.source" = "Quelle: %@"; @@ -450,6 +451,9 @@ "errors.failed_to_save_file.encoding.failed" = "Fehler beim Speichern der Datei. Die Zeichenkodierung ist möglicherweise nicht unterstützt."; "errors.file_modified_by_another_process" = "Die Datei wurde von einem anderen Prozess geändert."; "errors.failed_to_import_key" = "Fehler beim Importieren des Schlüssels."; +"errors.port_forward.service_unavailable" = "Dienst nicht verfügbar"; +"errors.port_forward.invalid_address" = "Ungültige Adresse"; +"errors.port_forward.address_already_in_use" = "Errno 48: Adresse wird bereits verwendet"; "file.copy" = "Kopieren nach.."; "file.download" = "Herunterladen auf.."; @@ -541,3 +545,9 @@ "remote.private_key_content" = "Inhalt des privaten Schlüssels"; "remote.setup_note" = "Um Schlüsselpaare zu generieren, führen Sie ssh-keygen im Terminal aus. Lassen Sie die Passphrase leer, falls keine vorhanden ist."; "remote.import_from_file" = "Importieren aus Datei"; +"remote.port_forward.new_port_forward" = "Neue Portweiterleitung"; +"remote.port_forward.local_port_or_address" = "Lokaler Port oder Adresse"; +"remote.port_forward.remote_port_or_address" = "Remote-Port oder Adresse"; +"remote.port_forward.port_forwarding" = "Portweiterleitung"; +"remote.port_forward.configure_description" = "Um eine Portweiterleitung zu konfigurieren, geben Sie die lokale und die Remote-Adresse ein. Wenn Sie eine Adresse eingeben, wird der Port 22 verwendet."; +"remote.port_forward.address_example" = "z.B. 6000 oder 127.0.0.1:6000"; \ No newline at end of file diff --git a/CodeApp/Localization/en.lproj/Localizable.strings b/CodeApp/Localization/en.lproj/Localizable.strings index 111ec71d7..64c287071 100644 --- a/CodeApp/Localization/en.lproj/Localizable.strings +++ b/CodeApp/Localization/en.lproj/Localizable.strings @@ -300,6 +300,7 @@ are licensed under [BSD-3-Clause License](https://en.wikipedia.org/wiki/BSD_lice "common.import" = "Import"; "common.add" = "Add"; "common.create" = "Create"; +"common.remove" = "Remove"; "notification.source" = "Source: %@"; @@ -340,6 +341,9 @@ are licensed under [BSD-3-Clause License](https://en.wikipedia.org/wiki/BSD_lice "errors.failed_to_save_file.encoding.failed" = "Failed to save file. Encoding failed."; "errors.file_modified_by_another_process" = "File modified by another process"; "errors.failed_to_import_key" = "Failed to import key"; +"errors.port_forward.service_unavailable" = "Service unavailable"; +"errors.port_forward.invalid_address" = "Invalid address"; +"errors.port_forward.address_already_in_use" = "Errno 48: Address already in use"; "file.copy" = "Copy to.."; "file.download" = "Download to.."; @@ -431,3 +435,9 @@ are licensed under [BSD-3-Clause License](https://en.wikipedia.org/wiki/BSD_lice "remote.private_key_content" = "Private key content"; "remote.setup_note" = "To generate key-pairs, run ssh-keygen in the terminal. Leave the passphrase empty if there isn't one."; "remote.import_from_file" = "Import from file"; +"remote.port_forward.new_port_forward" = "New Port Forward"; +"remote.port_forward.local_port_or_address" = "Local port or address"; +"remote.port_forward.remote_port_or_address" = "Remote port or address"; +"remote.port_forward.port_forwarding" = "Port Forwarding"; +"remote.port_forward.configure_description" = "Configure port forwarding to access a port on the remote machine."; +"remote.port_forward.address_example" = "e.g. 6000 or 127.0.0.1:6000"; \ No newline at end of file diff --git a/CodeApp/Localization/ja.lproj/Localizable.strings b/CodeApp/Localization/ja.lproj/Localizable.strings index 27eaac4c9..822944bf1 100644 --- a/CodeApp/Localization/ja.lproj/Localizable.strings +++ b/CodeApp/Localization/ja.lproj/Localizable.strings @@ -411,6 +411,7 @@ "common.import" = "インポート"; "common.add" = "追加"; "common.create" = "作成"; +"common.remove" = "削除"; "notification.source" = "Source: %@"; @@ -451,6 +452,9 @@ "errors.failed_to_save_file.encoding.failed" = "ファイルの保存に失敗しました。 エンコードに失敗しました。"; "errors.file_modified_by_another_process" = "別のプロセスによって変更されたファイルです"; "errors.failed_to_import_key" = "キーのインポートに失敗しました"; +"errors.port_forward.service_unavailable" = "サービスが利用できません"; +"errors.port_forward.invalid_address" = "無効なアドレスです"; +"errors.port_forward.address_already_in_use" = "Errno 48: アドレスはすでに使用されています"; "file.copy" = "コピー.."; "file.download" = "ダウンロード.."; @@ -542,3 +546,9 @@ "remote.private_key_content" = "秘密鍵の内容"; "remote.setup_note" = "キーペアを生成するには、ターミナルで ssh-keygen を実行します。 パスフレーズがない場合は空のままにします。"; "remote.import_from_file" = "ファイルからインポート"; +"remote.port_forward.new_port_forward" = "新しいポートフォワード"; +"remote.port_forward.local_port_or_address" = "ローカル ポートまたはアドレス"; +"remote.port_forward.remote_port_or_address" = "リモート ポートまたはアドレス"; +"remote.port_forward.port_forwarding" = "ポートフォワーディング"; +"remote.port_forward.configure_description" = "ポートフォワーディングを使用すると、リモート ホストのポートをローカル ホストに転送できます。"; +"remote.port_forward.address_example" = "例えば 6000 または 127.0.0.1:6000"; \ No newline at end of file diff --git a/CodeApp/Localization/ko.lproj/Localizable.strings b/CodeApp/Localization/ko.lproj/Localizable.strings index 4aaa480d1..a3c64edc8 100644 --- a/CodeApp/Localization/ko.lproj/Localizable.strings +++ b/CodeApp/Localization/ko.lproj/Localizable.strings @@ -410,6 +410,7 @@ "common.import" = "가져오기"; "common.add" = "추가"; "common.create" = "생성"; +"common.rename" = "이름 바꾸기"; "notification.source" = "원천: %@"; @@ -450,6 +451,9 @@ "errors.failed_to_save_file.encoding.failed" = "파일을 저장하지 못했습니다. 인코딩에 실패했습니다."; "errors.file_modified_by_another_process" = "파일이 다른 프로세스에 의해 수정되었습니다."; "errors.failed_to_import_key" = "키를 가져 오지 못했습니다."; +"errors.port_forward.service_unavailable" = "서비스를 사용할 수 없습니다."; +"errors.port_forward.invalid_address" = "잘못된 주소입니다."; +"errors.port_forward.address_already_in_use" = "Errno 48: 주소가 이미 사용 중입니다."; "file.copy" = "에게 복사.."; "file.download" = "다운로드.."; @@ -541,3 +545,9 @@ "remote.private_key_content" = "개인 키 내용"; "remote.setup_note" = "키 쌍을 생성하려면 터미널에서 ssh-keygen을 실행하십시오. 암호가 없는 경우 암호를 비워 둡니다."; "remote.import_from_file" = "파일에서 가져 오기"; +"remote.port_forward.new_port_forward" = "새 포트 포워드"; +"remote.port_forward.local_port_or_address" = "로컬 포트 또는 주소"; +"remote.port_forward.remote_port_or_address" = "원격 포트 또는 주소"; +"remote.port_forward.port_forwarding" = "포트 포워딩"; +"remote.port_forward.configure_description" = "원격 시스템의 포트에 액세스하도록 포트 전달을 구성합니다."; +"remote.port_forward.address_example" = "예를 들어 6000 또는 127.0.0.1:6000"; \ No newline at end of file diff --git a/CodeApp/Localization/zh-Hans.lproj/Localizable.strings b/CodeApp/Localization/zh-Hans.lproj/Localizable.strings index 0186ec506..4b2966fc5 100644 --- a/CodeApp/Localization/zh-Hans.lproj/Localizable.strings +++ b/CodeApp/Localization/zh-Hans.lproj/Localizable.strings @@ -401,6 +401,7 @@ "common.import" = "导入"; "common.add" = "添加"; "common.create" = "创建"; +"common.remove" = "移除"; "notification.source" = "来源: %@"; @@ -441,6 +442,9 @@ "errors.failed_to_save_file.encoding.failed" = "无法保存文件。 编码失败。"; "errors.file_modified_by_another_process" = "文件已被另一个进程修改。"; "errors.failed_to_import_key" = "无法导入密钥。"; +"errors.port_forward.service_unavailable" = "服务不可用"; +"errors.port_forward.invalid_address" = "无效的地址"; +"errors.port_forward.address_already_in_use" = "地址已在使用中"; "file.copy" = "复制到.."; "file.download" = "下载到.."; @@ -516,3 +520,9 @@ "remote.private_key_content" = "私钥内容"; "remote.setup_note" = "要生成密钥,请在终端中运行 ssh-keygen。如果没有密钥密码,请留空。"; "remote.import_from_file" = "从文件导入"; +"remote.port_forward.new_port_forward" = "新建端口转发"; +"remote.port_forward.local_port_or_address" = "本地端口或地址"; +"remote.port_forward.remote_port_or_address" = "远程端口或地址"; +"remote.port_forward.port_forwarding" = "端口转发"; +"remote.port_forward.configure_description" = "您可以使用端口转发功能将本地端口转发到远程服务器。"; +"remote.port_forward.address_example" = "例如 6000 或 127.0.0.1:6000"; \ No newline at end of file diff --git a/CodeApp/Managers/ActivityBarManager.swift b/CodeApp/Managers/ActivityBarManager.swift index 2e56afcf1..67e72715e 100644 --- a/CodeApp/Managers/ActivityBarManager.swift +++ b/CodeApp/Managers/ActivityBarManager.swift @@ -15,14 +15,15 @@ enum ActivityBarBubble { struct ActivityBarItem: Identifiable { let id = UUID() let itemID: String - let iconSystemName: String - let title: LocalizedStringKey - let shortcutKey: KeyEquivalent - let modifiers: EventModifiers - let view: AnyView - let positionPrecedence: Int = 0 - let contextMenuItems: (() -> [ContextMenuItem])? + var iconSystemName: String + var title: LocalizedStringKey + var shortcutKey: KeyEquivalent? + var modifiers: EventModifiers? + var view: AnyView + var contextMenuItems: (() -> [ContextMenuItem])? + var positionPrecedence: Int = 0 var bubble: () -> ActivityBarBubble? + var isVisible: (() -> Bool) } class ActivityBarManager: CodeAppContributionPointManager { diff --git a/CodeApp/Managers/FileSystem/FTP/FTPFileSystemProvider.swift b/CodeApp/Managers/FileSystem/FTP/FTPFileSystemProvider.swift index a64b77a56..87387ffce 100644 --- a/CodeApp/Managers/FileSystem/FTP/FTPFileSystemProvider.swift +++ b/CodeApp/Managers/FileSystem/FTP/FTPFileSystemProvider.swift @@ -14,6 +14,7 @@ class FTPFileSystemProvider: FileSystemProvider { var gitServiceProvider: GitServiceProvider? = nil var searchServiceProvider: SearchServiceProvider? = nil var terminalServiceProvider: TerminalServiceProvider? = nil + var portforwardServiceProvider: (any PortForwardServiceProvider)? = nil private var fs: FTPFileProvider diff --git a/CodeApp/Managers/FileSystem/FileSystemProvider.swift b/CodeApp/Managers/FileSystem/FileSystemProvider.swift index bb1453e41..6fc0457a1 100644 --- a/CodeApp/Managers/FileSystem/FileSystemProvider.swift +++ b/CodeApp/Managers/FileSystem/FileSystemProvider.swift @@ -12,6 +12,7 @@ protocol FileSystemProvider { var gitServiceProvider: GitServiceProvider? { get } var searchServiceProvider: SearchServiceProvider? { get } var terminalServiceProvider: TerminalServiceProvider? { get } + var portforwardServiceProvider: (any PortForwardServiceProvider)? { get } func contentsOfDirectory(at url: URL, completionHandler: @escaping ([URL]?, Error?) -> Void) func fileExists(at url: URL, completionHandler: @escaping (Bool) -> Void) diff --git a/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift b/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift index a1a0343f5..55ebdf276 100644 --- a/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift +++ b/CodeApp/Managers/FileSystem/Local/LocalFileSystemProvider.swift @@ -13,6 +13,7 @@ class LocalFileSystemProvider: FileSystemProvider { var gitServiceProvider: GitServiceProvider? = nil var searchServiceProvider: SearchServiceProvider? = nil var terminalServiceProvider: TerminalServiceProvider? = nil + var portforwardServiceProvider: (any PortForwardServiceProvider)? = nil func write( at: URL, content: Data, atomically: Bool, overwrite: Bool, diff --git a/CodeApp/Managers/FileSystem/PortForwardServiceProvider.swift b/CodeApp/Managers/FileSystem/PortForwardServiceProvider.swift new file mode 100644 index 000000000..7108e2816 --- /dev/null +++ b/CodeApp/Managers/FileSystem/PortForwardServiceProvider.swift @@ -0,0 +1,30 @@ +// +// PortForwardServiceProvider.swift +// Code +// +// Created by Ken Chung on 31/8/2023. +// + +import Foundation + +struct Address { + var address: String + var port: Int +} + +enum PortForwardType { + // Local Address, Remote Address + case forward(Address, Address) +} + +protocol PortForwardSocket { + var type: PortForwardType { get } + func closeSocket() throws +} + +protocol PortForwardServiceProvider { + associatedtype Socket: PortForwardSocket + var sockets: [Socket] { get } + func bindLocalPortToRemote(localAddress: Address, remoteAddress: Address) async throws -> Socket + func onSocketClosed(_ callback: @escaping (Socket) -> Void) +} diff --git a/CodeApp/Managers/FileSystem/SFTP/SFTPFileSystemProvider.swift b/CodeApp/Managers/FileSystem/SFTP/SFTPFileSystemProvider.swift index c2dec9743..2e65a09d0 100644 --- a/CodeApp/Managers/FileSystem/SFTP/SFTPFileSystemProvider.swift +++ b/CodeApp/Managers/FileSystem/SFTP/SFTPFileSystemProvider.swift @@ -8,8 +8,16 @@ import Foundation import NMSSH -class SFTPFileSystemProvider: NSObject, FileSystemProvider { +struct SFTPSocket: PortForwardSocket { + var socket: NMSSHSocket + var type: PortForwardType + func closeSocket() throws { + close(socket.sock) + } +} + +class SFTPFileSystemProvider: NSObject, FileSystemProvider, PortForwardServiceProvider { static var registeredScheme: String = "sftp" var gitServiceProvider: GitServiceProvider? = nil var searchServiceProvider: SearchServiceProvider? = nil @@ -17,11 +25,14 @@ class SFTPFileSystemProvider: NSObject, FileSystemProvider { _terminalServiceProvider } var _terminalServiceProvider: SFTPTerminalServiceProvider? = nil + var portforwardServiceProvider: (any PortForwardServiceProvider)? { self } var homePath: String? = "" var fingerPrint: String? = nil + var sockets: [SFTPSocket] = [] private var didDisconnect: (Error) -> Void + private var onSocketClosed: ((SFTPSocket) -> Void)? = nil private var session: NMSSHSession! private let queue = DispatchQueue(label: "sftp.serial.queue") @@ -43,6 +54,7 @@ class SFTPFileSystemProvider: NSObject, FileSystemProvider { queue.async { self.session = NMSSHSession(host: host, port: port, andUsername: username) self.session.delegate = self + self.session.channel.socketDelegate = self } self._terminalServiceProvider = SFTPTerminalServiceProvider( @@ -55,11 +67,44 @@ class SFTPFileSystemProvider: NSObject, FileSystemProvider { } deinit { + sockets.forEach { try? $0.closeSocket() } self._terminalServiceProvider?.disconnect() self.session.sftp.disconnect() self.session.disconnect() } + func bindLocalPortToRemote(localAddress: Address, remoteAddress: Address) async throws + -> SFTPSocket + { + return try await withUnsafeThrowingContinuation { continuation in + queue.async { + do { + let socket = NMSSHChannel.createSocket() + try self.session.channel.bindLocalPortToRemoteHost( + with: socket, + localListenIP: localAddress.address, + localPort: localAddress.port, + host: remoteAddress.address, + port: remoteAddress.port, + in: self.queue + ) + let sftpSocket = SFTPSocket( + socket: socket, type: .forward(localAddress, remoteAddress)) + DispatchQueue.main.async { + self.sockets.append(sftpSocket) + } + continuation.resume(returning: sftpSocket) + } catch { + continuation.resume(throwing: error) + } + } + } + } + + func onSocketClosed(_ callback: @escaping (SFTPSocket) -> Void) { + self.onSocketClosed = callback + } + func connect( authentication: RemoteAuthenticationMode, completionHandler: @escaping (Error?) -> Void @@ -251,3 +296,13 @@ extension SFTPFileSystemProvider: NMSSHSessionDelegate { didDisconnect(error) } } + +extension SFTPFileSystemProvider: NMSSHSocketDelegate { + func socketDidClose(_ socket: NMSSHSocket) { + let sftpSocket = sockets.first { $0.socket.sock == socket.sock } + sockets = sockets.filter { $0.socket.sock != socket.sock } + if let sftpSocket { + self.onSocketClosed?(sftpSocket) + } + } +} diff --git a/CodeApp/Managers/FileSystem/WorkSpaceStorage.swift b/CodeApp/Managers/FileSystem/WorkSpaceStorage.swift index fd3a386db..4964e7733 100644 --- a/CodeApp/Managers/FileSystem/WorkSpaceStorage.swift +++ b/CodeApp/Managers/FileSystem/WorkSpaceStorage.swift @@ -399,7 +399,6 @@ extension WorkSpaceStorage { } extension WorkSpaceStorage: FileSystemProvider { - static var registeredScheme: String { "nil" } @@ -416,6 +415,10 @@ extension WorkSpaceStorage: FileSystemProvider { fs?.terminalServiceProvider } + var portforwardServiceProvider: (any PortForwardServiceProvider)? { + fs?.portforwardServiceProvider + } + func write( at: URL, content: Data, atomically: Bool, overwrite: Bool, completionHandler: @escaping (Error?) -> Void diff --git a/CodeApp/Managers/MainApp.swift b/CodeApp/Managers/MainApp.swift index 6528b5eda..f30e1a7b7 100644 --- a/CodeApp/Managers/MainApp.swift +++ b/CodeApp/Managers/MainApp.swift @@ -234,7 +234,8 @@ class MainApp: ObservableObject { ) ]) }, - bubble: { nil } + bubble: { nil }, + isVisible: { true } ) let search = ActivityBarItem( itemID: "SEARCH", @@ -244,7 +245,8 @@ class MainApp: ObservableObject { modifiers: [.command, .shift], view: AnyView(SearchContainer()), contextMenuItems: nil, - bubble: { nil } + bubble: { nil }, + isVisible: { true } ) let sourceControl = ActivityBarItem( itemID: "SOURCE_CONTROL", @@ -261,7 +263,8 @@ class MainApp: ObservableObject { } else { return self.gitTracks.isEmpty ? nil : .text("\(self.gitTracks.count)") } - } + }, + isVisible: { true } ) let remote = ActivityBarItem( itemID: "REMOTE", @@ -271,7 +274,8 @@ class MainApp: ObservableObject { modifiers: [.command, .shift], view: AnyView(RemoteContainer()), contextMenuItems: nil, - bubble: { self.workSpaceStorage.remoteConnected ? .text("") : nil } + bubble: { self.workSpaceStorage.remoteConnected ? .text("") : nil }, + isVisible: { true } ) extensionManager.activityBarManager.registerItem(item: explorer) diff --git a/CodeApp/Views/ActivityBar.swift b/CodeApp/Views/ActivityBar.swift index d4779fd02..32cd3d1e5 100644 --- a/CodeApp/Views/ActivityBar.swift +++ b/CodeApp/Views/ActivityBar.swift @@ -75,6 +75,7 @@ struct ActivityBar: View { var items: [ActivityBarItem] { activityBarManager.items .sorted { $0.positionPrecedence > $1.positionPrecedence } + .filter { $0.isVisible() } } func removeFocus() { diff --git a/CodeApp/Views/ActivityBarItemView.swift b/CodeApp/Views/ActivityBarItemView.swift index c95d6ed95..5f24b534c 100644 --- a/CodeApp/Views/ActivityBarItemView.swift +++ b/CodeApp/Views/ActivityBarItemView.swift @@ -53,7 +53,11 @@ struct ActivityBarIconView: View { .padding(5) }.frame(maxWidth: .infinity, minHeight: 60.0) } - .keyboardShortcut(activityBarItem.shortcutKey, modifiers: activityBarItem.modifiers) + .if(activityBarItem.shortcutKey != nil && activityBarItem.modifiers != nil) { view in + view + .keyboardShortcut( + activityBarItem.shortcutKey!, modifiers: activityBarItem.modifiers!) + } .if(activityBarItem.contextMenuItems != nil) { view in view .contextMenu { diff --git a/CodeApp/Views/RegularSidebar.swift b/CodeApp/Views/RegularSidebar.swift index bcdfeca38..53e9fbc83 100644 --- a/CodeApp/Views/RegularSidebar.swift +++ b/CodeApp/Views/RegularSidebar.swift @@ -43,7 +43,7 @@ struct RegularSidebar: View { var body: some View { ZStack(alignment: .center) { Color.init(id: "sideBar.background") - if let item = activityBarManager.itemForItemID(itemID: activeItemId) { + if let item = activityBarManager.itemForItemID(itemID: activeItemId), item.isVisible() { item.view } else { ProgressView() diff --git a/Extensions/RemoteAuxiliary/RemoteAuxiliaryExtension.swift b/Extensions/RemoteAuxiliary/RemoteAuxiliaryExtension.swift index a0e47eac6..3bf7aa609 100644 --- a/Extensions/RemoteAuxiliary/RemoteAuxiliaryExtension.swift +++ b/Extensions/RemoteAuxiliary/RemoteAuxiliaryExtension.swift @@ -17,5 +17,22 @@ class RemoteAuxiliaryExtension: CodeAppExtension { positionPrecedence: Int.min ) contribution.statusBar.registerItem(item: item) + + var portForwardActivityBarItem = ActivityBarItem( + itemID: "REMOTE_PORT_FORWARD", + iconSystemName: "point.3.filled.connected.trianglepath.dotted", + title: "Port Forward", + shortcutKey: nil, + modifiers: nil, + view: AnyView(PortForwardContainer()), + contextMenuItems: nil, + bubble: { nil }, + isVisible: { + app.workSpaceStorage.remoteConnected + && app.workSpaceStorage.currentDirectory._url?.scheme == "sftp" + } + ) + portForwardActivityBarItem.positionPrecedence = -10 + contribution.activityBar.registerItem(item: portForwardActivityBarItem) } } diff --git a/Extensions/RemoteAuxiliary/Views/PortForwardContainer.swift b/Extensions/RemoteAuxiliary/Views/PortForwardContainer.swift new file mode 100644 index 000000000..71f131c42 --- /dev/null +++ b/Extensions/RemoteAuxiliary/Views/PortForwardContainer.swift @@ -0,0 +1,92 @@ +// +// PortForwardContainer.swift +// Code +// +// Created by Ken Chung on 31/8/2023. +// + +import SwiftUI + +enum PortForwardServiceError: String { + case portForwardServiceProviderUnavailable = "errors.port_forward.service_unavailable" + case invalidAddress = "errors.port_forward.invalid_address" +} + +extension PortForwardServiceError: LocalizedError { + var errorDescription: String? { + NSLocalizedString(self.rawValue, comment: "") + } +} + +struct PortForwardContainer: View { + + @EnvironmentObject var App: MainApp + @State var ports: [RemotePortForwardSetup] = [] + + func parseAddress(addressString: String) throws -> Address { + let validPortRange = 1...65535 + if let port = Int(addressString), + validPortRange ~= port + { + return Address(address: "127.0.0.1", port: port) + } + let parts = addressString.split(separator: ":") + guard parts.count == 2, + let port = Int(parts[1]), + validPortRange ~= port + else { + throw PortForwardServiceError.invalidAddress + } + return Address(address: String(parts[0]), port: port) + } + + func createPortForwardSetup(localAddressString: String, remoteAddressString: String) + async throws + { + guard let serviceProvider = App.workSpaceStorage.portforwardServiceProvider else { + throw PortForwardServiceError.portForwardServiceProviderUnavailable + } + let localAddress = try parseAddress(addressString: localAddressString) + let remoteAddress = try parseAddress(addressString: remoteAddressString) + _ = try await serviceProvider.bindLocalPortToRemote( + localAddress: localAddress, remoteAddress: remoteAddress) + loadPortForwardSetups() + } + + func removePortForwardSetup(setup: RemotePortForwardSetup) { + try? setup.socket.closeSocket() + loadPortForwardSetups() + } + + func loadPortForwardSetups() { + guard let serviceProvider = App.workSpaceStorage.portforwardServiceProvider else { + return + } + self.ports = serviceProvider.sockets.map { + switch $0.type { + case let .forward(localAddress, remoteAddress): + return RemotePortForwardSetup( + label: "\(localAddress.port) -> \(remoteAddress.port)", socket: $0) + } + } + } + + var body: some View { + List { + Group { + RemotePortForwardListSection( + ports: ports, + removePortForwardSetup: removePortForwardSetup + ) + RemotePortForwardCreateSection( + createPortForwardSetup: createPortForwardSetup + ) + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .onAppear { + loadPortForwardSetups() + } + }.listStyle(SidebarListStyle()) + } +} diff --git a/Extensions/RemoteAuxiliary/Views/RemotePortForwardSection.swift b/Extensions/RemoteAuxiliary/Views/RemotePortForwardSection.swift new file mode 100644 index 000000000..29ee023ed --- /dev/null +++ b/Extensions/RemoteAuxiliary/Views/RemotePortForwardSection.swift @@ -0,0 +1,106 @@ +// +// RemotePortForwardSection.swift +// Code +// +// Created by Ken Chung on 31/8/2023. +// + +import SwiftUI + +struct RemotePortForwardSetup: Identifiable { + var id: UUID = UUID() + var label: String + var socket: any PortForwardSocket +} + +struct RemotePortForwardCreateSection: View { + + var createPortForwardSetup: (String, String) async throws -> Void + + @EnvironmentObject var App: MainApp + @State var localAddress: String = "" + @State var remoteAddress: String = "" + + var body: some View { + Section( + header: + Text("remote.port_forward.new_port_forward") + .foregroundColor(Color(id: "sideBarSectionHeader.foreground")) + ) { + + Group { + TextField("remote.port_forward.local_port_or_address", text: $localAddress) + TextField("remote.port_forward.remote_port_or_address", text: $remoteAddress) + } + .autocapitalization(.none) + .disableAutocorrection(true) + .padding(7) + .background(Color.init(id: "input.background")) + .cornerRadius(15) + + DescriptionText("remote.port_forward.address_example") + + SideBarButton("common.add") { + Task { + do { + try await createPortForwardSetup(localAddress, remoteAddress) + localAddress = "" + remoteAddress = "" + } catch let error as NSError { + if let code = (error.underlyingErrors.first as? NSError)?.code, + code == EADDRINUSE + { + App.notificationManager.showErrorMessage( + "errors.port_forward.address_already_in_use") + } else { + App.notificationManager.showErrorMessage( + error.localizedDescription) + } + } + } + } + } + } +} + +struct RemotePortForwardListSection: View { + + var ports: [RemotePortForwardSetup] + var removePortForwardSetup: (RemotePortForwardSetup) -> Void + + var body: some View { + Section( + header: + Text("remote.port_forward.port_forwarding") + .foregroundColor(Color(id: "sideBarSectionHeader.foreground")) + ) { + if ports.isEmpty { + DescriptionText("remote.port_forward.configure_description") + } else { + ForEach(ports) { port in + Menu { + Section { + Button(role: .destructive) { + removePortForwardSetup(port) + } label: { + Label("common.remove", systemImage: "xmark") + } + } header: { + Text(port.label) + } + } label: { + HStack { + Image(systemName: "point.3.connected.trianglepath.dotted") + Text(port.label) + Spacer() + Circle() + .fill(.green) + .frame(width: 14, height: 14) + } + } + } + } + + } + } +} diff --git a/downloadFrameworks.sh b/downloadFrameworks.sh index 1b435c44b..3240d8330 100755 --- a/downloadFrameworks.sh +++ b/downloadFrameworks.sh @@ -67,7 +67,7 @@ unzip -q php.xcframework.zip -d PHP rm -f php.xcframework.zip # NMSSH -curl -OL https://github.com/thebaselab/NMSSH/releases/download/2.3.1-p2/NMSSH.xcframework.zip +curl -OL https://github.com/thebaselab/NMSSH/releases/download/2.3.1-p3/NMSSH.xcframework.zip unzip -q NMSSH.xcframework.zip rm -f NMSSH.xcframework.zip From 9c4bf12f989e8f907cc44db9fe4b0de877a238dd Mon Sep 17 00:00:00 2001 From: Chung Shing Hin Date: Fri, 1 Sep 2023 13:03:27 +0800 Subject: [PATCH 2/2] app: No sidebar section selected message --- CodeApp/Localization/de.lproj/Localizable.strings | 1 + CodeApp/Localization/en.lproj/Localizable.strings | 1 + CodeApp/Localization/ja.lproj/Localizable.strings | 1 + CodeApp/Localization/ko.lproj/Localizable.strings | 1 + CodeApp/Localization/zh-Hans.lproj/Localizable.strings | 1 + CodeApp/Managers/MainApp.swift | 2 ++ CodeApp/Views/RegularSidebar.swift | 9 +++++++-- 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CodeApp/Localization/de.lproj/Localizable.strings b/CodeApp/Localization/de.lproj/Localizable.strings index fb448d358..4235efac0 100644 --- a/CodeApp/Localization/de.lproj/Localizable.strings +++ b/CodeApp/Localization/de.lproj/Localizable.strings @@ -428,6 +428,7 @@ "settings.editor.font.ligatures" = "Schrift Ligaturen"; "settings.editor.font.show_all_fonts" = "Alle Schriftarten anzeigen"; "panels.no_panel_selected" = "Kein Panel ausgewählt"; +"sidebar.no_section_selected" = "Kein Abschnitt ausgewählt"; "errors.script_already_running" = "Ein Skript wird bereits ausgeführt. Beenden Sie es, bevor Sie ein anderes ausführen."; "errors.fs.not_implemented" = "Diese Funktion ist noch nicht implementiert."; diff --git a/CodeApp/Localization/en.lproj/Localizable.strings b/CodeApp/Localization/en.lproj/Localizable.strings index 64c287071..ee688bc93 100644 --- a/CodeApp/Localization/en.lproj/Localizable.strings +++ b/CodeApp/Localization/en.lproj/Localizable.strings @@ -318,6 +318,7 @@ are licensed under [BSD-3-Clause License](https://en.wikipedia.org/wiki/BSD_lice "settings.editor.font.ligatures" = "Font Ligatures"; "settings.editor.font.show_all_fonts" = "Show all fonts"; "panels.no_panel_selected" = "No panel selected"; +"sidebar.no_section_selected" = "No section selected."; "errors.script_already_running" = "A script is already running. Finish it before running another one."; "errors.fs.not_implemented" = "Operation not implemented"; diff --git a/CodeApp/Localization/ja.lproj/Localizable.strings b/CodeApp/Localization/ja.lproj/Localizable.strings index 822944bf1..a0bf8e438 100644 --- a/CodeApp/Localization/ja.lproj/Localizable.strings +++ b/CodeApp/Localization/ja.lproj/Localizable.strings @@ -429,6 +429,7 @@ "settings.editor.font.ligatures" = "フォント合字"; "settings.editor.font.show_all_fonts" = "すべてのフォントを表示"; "panels.no_panel_selected" = "パネルが選択されていません"; +"sidebar.no_section_selected" = "セクションが選択されていません"; "errors.script_already_running" = "スクリプトがすでに実行されています。次のスクリプトを実行する前に終了してください。"; "errors.fs.not_implemented" = "その操作は実装されていません"; diff --git a/CodeApp/Localization/ko.lproj/Localizable.strings b/CodeApp/Localization/ko.lproj/Localizable.strings index a3c64edc8..9f07f89b9 100644 --- a/CodeApp/Localization/ko.lproj/Localizable.strings +++ b/CodeApp/Localization/ko.lproj/Localizable.strings @@ -428,6 +428,7 @@ "settings.editor.font.ligatures" = "글꼴 합자"; "settings.editor.font.show_all_fonts" = "모든 글꼴 표시"; "panels.no_panel_selected" = "패널이 선택되지 않았습니다."; +"sidebar.no_section_selected" = "섹션이 선택되지 않았습니다."; "errors.script_already_running" = "스크립트가 이미 실행 중입니다. 다른 것을 실행하기 전에 완료하십시오."; "errors.fs.not_implemented" = "이 기능은 아직 구현되지 않았습니다."; diff --git a/CodeApp/Localization/zh-Hans.lproj/Localizable.strings b/CodeApp/Localization/zh-Hans.lproj/Localizable.strings index 4b2966fc5..843ea6b63 100644 --- a/CodeApp/Localization/zh-Hans.lproj/Localizable.strings +++ b/CodeApp/Localization/zh-Hans.lproj/Localizable.strings @@ -419,6 +419,7 @@ "settings.editor.font.ligatures" = "合字"; "settings.editor.font.show_all_fonts" = "显示所有字体"; "panels.no_panel_selected" = "没有选中的面板"; +"sidebar.no_section_selected" = "没有选中的版面"; "errors.script_already_running" = "脚本已在运行。 在运行另一个之前完成它。"; "errors.fs.not_implemented" = "文件系统不支持此操作。"; diff --git a/CodeApp/Managers/MainApp.swift b/CodeApp/Managers/MainApp.swift index f30e1a7b7..c75e86ec9 100644 --- a/CodeApp/Managers/MainApp.swift +++ b/CodeApp/Managers/MainApp.swift @@ -61,6 +61,7 @@ class MainStateManager: ObservableObject { @Published var availableCheckoutDestination: [CheckoutDestination] = [] @Published var gitServiceIsBusy = false @Published var isMonacoEditorInitialized = false + @Published var isSystemExtensionsInitialized = false } class MainApp: ObservableObject { @@ -191,6 +192,7 @@ class MainApp: ObservableObject { Task { await MainActor.run { setUpActivityBarItems() + stateManager.isSystemExtensionsInitialized = true } } } diff --git a/CodeApp/Views/RegularSidebar.swift b/CodeApp/Views/RegularSidebar.swift index 53e9fbc83..6bfd77d3d 100644 --- a/CodeApp/Views/RegularSidebar.swift +++ b/CodeApp/Views/RegularSidebar.swift @@ -12,6 +12,7 @@ private var REGULAR_SIDEBAR_MIN_WIDTH: CGFloat = DefaultUIState.SIDEBAR_WIDTH struct RegularSidebar: View { @EnvironmentObject var activityBarManager: ActivityBarManager + @EnvironmentObject var stateManager: MainStateManager @SceneStorage("sidebar.width") var sideBarWidth: Double = DefaultUIState.SIDEBAR_WIDTH @SceneStorage("activitybar.selected.item") var activeItemId: String = DefaultUIState @@ -43,10 +44,14 @@ struct RegularSidebar: View { var body: some View { ZStack(alignment: .center) { Color.init(id: "sideBar.background") - if let item = activityBarManager.itemForItemID(itemID: activeItemId), item.isVisible() { + if !stateManager.isSystemExtensionsInitialized { + ProgressView() + } else if let item = activityBarManager.itemForItemID(itemID: activeItemId), + item.isVisible() + { item.view } else { - ProgressView() + DescriptionText("sidebar.no_section_selected") } } .gesture(