Удаленная отладка приложения на .NET 5.0 в Visual Studio Code для ARM на примере Banana Pi BPI-M64 и Cubietruck (Armbian, Linux)

Пост содержит подробное руководство как организовать удаленную отладку разрабатываемого приложения на .NET 5.0 в Visual Studio Code для устройства на ARM процессоре, на устройстве установлена Armbian (Linux). Благодаря кроссплатформенности .NET 5.0, разработанное приложение будет одинаково работать как в Windows, так и в Linux. Но все становится сложнее, если необходимо взаимодействовать с подсистемами Linux. Каждый раз компилировать в Windows и переносить исполняемые файлы ручным способом на Linux не очень удобно. Один из рабочих примеров для подобного решения является задача отладки взаимодействия приложения на C# в Linux с устройством подключенным по протоколу RS232. В качестве платформы запуска будем использовать Cubietruck (ARM32), и Banana Pi BPI-M64(ARM64), работающие на Armbian.

Постановка задачи

В Visual Studio Code под ОС Windows x64 разрабатываем проект для ARM. Отладка приложения будет состоять из трех задач:

  • компиляция под архитектуру ARM
  • копирование исполняемых файлов на конечное устройство
  • запуск удаленного отладчика на конечном устройстве.

Компиляция под архитектуру ARM будет выполняться с помощью  .NET SDK, командой:  dotnet build . Копирование файлов будем производить программой Rsync. В качестве pipeTransport для связи с удаленной системой будет использоваться программа PLINK, из пакета PuTTY. Удаленный отладчик от Microsoft — vsdbg.

Предварительные требования

  • одноплатный компьютер на ARM процессоре не ниже ARMv7.
  • ОС Linux на плате ARM с ядром Linux 5.8 (с более старыми ядрами не проводилась проверка)
  • Локальный компьютер для разработки с Visual Studio Code, работающий под управлением Windows 7+ x64, с установленным расширением C# for Visual Studio Code (powered by OmniSharp) и SDK .NET

Тестирование производилось на следующие устройствах:

Шаг 1 — Установка SDK .NET, Visual Studio Code и расширение поддержки C# в Windows

Установка SDK .NET, Visual Studio Code и расширение поддержки C# в Windows подробно описана в посте Установка .NET 5.0 для ARM на примере Banana Pi BPI-M64 и Cubietruck (Armbian, Linux) и Создание первого приложения на .NET 5.0 в Visual Studio Code для ARM.

Шаг 2 — Установка программ MobaXterm, cwRsync и PuTTY, в Windows

MobaXterm

Терминал MobaXterm — существенно удобнее в работе по сравнению с PuTTY терминалом, и позволяет выполнять загрузку/копирование файлов и папок в один клик. Загрузим по ссылке и установим.

cwRsync

Для копирования сборок и файлов проекта будем использовать программу Rsync. Rsync — программа для UNIX-подобных систем, которая эффективно выполняет синхронизацию файлов и каталогов в двух местах (необязательно локальных) с минимизированием трафика, используя кодирование данных при необходимости. Важным отличием rsync от многих других программ/протоколов является то, что зеркалирование осуществляется одним потоком в каждом направлении (а не по одному или несколько потоков на каждый файл). rsync может копировать или отображать содержимое каталога и копировать файлы, опционально используя сжатие и рекурсию. rsync передаёт только изменения файлов, что отражается на производительности программы.

Но Rsync не предназначена для платформы Windows, поэтому воспользуемся пакетом cwRsync в который входит Rsync для Windows.
cwRsync — это пакет, состоящий из графической оболочки (начиная с версии 5), утилиты Rsync и библиотеки Cygwin. Пакет cwRsync позволяет организовать удалённое резервное копирование и синхронизацию файлов между серверами Windows. Также с помощью cwRsync можно осуществлять резервное копирование файлов Unix сервера на сервер Windows и наоборот. Конфигурирование Rsync под Windows в пакете cwRsync отличается от Unix только указанием путей к каталогам для синхронизации: пути Windows стиля Диск:\путь преобразуются (по правилам cygwin) в /cygdrive/диск/путь/, например, c:\windows нужно указывать как /cygdrive/c/windows.

Создадим папку  C:\RemoteCode\cwrsync  и распакуем в нее содержимое пакета cwrsync_6.2.0_x64_free.zip, так что бы программа  rsync.exe  была доступна по пути  c:\RemoteCode\cwrsync\rsync.exe 

PuTTY

Загрузим PuTTY в виде putty.zip(x64) и распакуем пакет, так что бы программа  PUTTY.EXE  была доступна по пути  c:\RemoteCode\putty\PUTTY.EXE 

Шаг 3 — Настройка удаленного входа под root для SSH

Более подробнее можно почитать в руководстве Использование SSH для подключения к удаленному серверу

Для доступа пользователя root по SSH необходимо внести изменения в конфигурационный файл  /etc/ssh/sshd_config  в Ubuntu. Войдем в систему и установим Midnight Commander в состав которого входит редактор mcedit:

$ sudo apt-get update
$ sudo apt-get install -y openssh-server mc
$ sudo mcedit /etc/ssh/sshd_config

Удалим строчку #PermitRootLogin prohibit-password и заменим на строку PermitRootLogin yes. Сохраним изменение в файле кнопка «F2» и выйдем из редактора «F10».

Remote debug .NET Linux

Перезагрузим сервер sshd, чтобы изменения вступили в силу, выполним команду:

$ sudo systemctl reload ssh

Теперь можно зайти по SSH на Armbian под пользователем root используя терминал MobaXterm.

Шаг 3 — Настройка ключей аутентификации для доступа SSH в Armbian

Более подробнее можно почитать в руководстве Как настроить ключи SSH в Ubuntu 18.04.
Ключ доступа необходим для избежания хранения пароля root пользователя в открытом виде в конфигурационном файле проекта Visual Studio Code, т.к. все проекты хранятся на корпоративном Git-сервере, и утечка пароля нежелательна. Аутентификация с помощью ключей реализуется путем создания пары ключей: приватного ключа и публичного ключа.
Приватный ключ располагается на клиентском компьютере, этот ключ защищен и хранится в секрете.
Публичный ключ может передаваться любому лицу или размещаться на сервере, доступ к которому вы хотите получить.

Приватный и публичный ключ сгенерируем на плате в Armbian, затем скопируем себе приватный ключ в Windows, и удалим этот ключ в Armbian.
Используя терминал MobaXterm выполним следующую команду, для генерации пары ключей:

$ ssh-keygen -t rsa

Нажмите ENTER, чтобы принять используемые по умолчанию значения. Пароль для ключа устанавливать не требуется, нажать ENTER:

root@bananapim64:~# ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:xayvVRQvFSxNCiLl1zwqmtfhAn+sMcU7oDCI7SAzQJ0 root@bananapim64
The key's randomart image is:
+---[RSA 2048]----+
| .. . ..o . .++. |
|. E o + +.=o |
|. . = B.. |
|.o . = o o |
|* o o . S = . |
|.= o * O + |
| . + * X |
| . O . |
| o |
+----[SHA256]-----+
root@bananapim64:~#

По результату выполнения команды в каталоге  ~/.ssh  будут сгенерированы два ключа:

  •  /id_rsa.pub  — публичный ключ, остается на Armbian
  •  /id_rsa  — приватный ключ, копируем на компьютер с Windows в папку  C:\RemoteCode , и удаляем этот файл в Armbian.

Копирование ключа
Remote debug .NET Linux

Теперь необходимо прописать публичный ключ в системе для доступа по SSH под пользователем root, для этого необходимо внести содержимое файла  id_rsa.pub  в файл  ~/.ssh/authorized_keys  , выполним следующие команды для этого:

$ sudo touch ~/.ssh/authorized_keys
$ sudo cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
$ sudo systemctl reload ssh

Теперь необходимо проверить доступность с помощью PuTTY, который был ранее загружен в папку  c:\RemoteCode\putty . Для использование ключа  id_rsa  необходимо его конвертировать из старого формата PEM с помощью программы  c:\RemoteCode\putty\PUTTYGEN.EXE  . Запускаем программу  PUTTYGEN.EXE  ,выбираем пункт меню «Conversions», затем нажимаем на пункт«Import key», после чего выбираем ключ  c:\RemoteCode\id_rsa . Для конвертации ключа после импорта нажимаем на кнопку «Save private key». Пароль для ключа не задаем, поэтому подтверждаем сохранение ключа без пароля, отвечаем: Да. Сохраняем файл с в новом формате (.ppk), путь  c:\RemoteCode\id_rsa.ppk .

Проверим подключение к Armbian с использованием ключа  id_rsa.ppk  при помощи программы  c:\RemoteCode\putty\PLINK.EXE , запустим командную строку в Windows и выполним, где 192.168.43.225 — IP-адрес компьютера на Armbian, и введем пароль входа в учетную запись root:

C:\RemoteCode\putty\PLINK.EXE -ssh root@192.168.43.225 -pw exit

После выполнение этой команды потребуется согласиться с хэшированием ключа удаленного компьютера, если этого не сделать, то потом возникнут проблемы при удаленном подключение по SSH, после выполнения команды закрыть окно терминала.

Затем снова запустить консольное окно и выполнить команду:

C:\RemoteCode\putty\PLINK.EXE -i c:\RemoteCode\id_rsa.ppk root@192.168.43.225 -batch -T echo "hello world"

В результате мы должны увидеть: «hello world». Подключение настроено.

Remote debug .NET Linux

Шаг 4 — Создание консольного приложения и добавление логики

Создадим папку  c:\RemoteCode\Projects\ , перейдем в нее и запустим командную строку. В командной стоке выполним команду:  dotnet new console -o RemoteAppArm64 , где RemoteAppArm64 — название нового проекта для ARM64, проект для ARM32 будет называться — RemoteAppArm.

Remote debug .NET Linux

Теперь необходимо добавить дополнительную логику в новое приложение. Приложение будет выводить информацию о системе, в которой оно работает. Запустим Visual Studio Code и откроем папку с проектом  c:\RemoteCode\Projects\RemoteAppArm64\ .

В функцию Main() поместим следующий код с вызовом исключения для проверки режима отладки, на GitHub версия проекта для ARM64 доступна по ссылке RemoteAppArm64, версия для ARM32 RemoteAppArm:

static void Main(string[] args)
{
	Console.WriteLine("Test .NET console application!");
	var str_Framework=RuntimeInformation.FrameworkDescription;
	var str_OSArch=RuntimeInformation.OSArchitecture.ToString();            
	var str_OSDesc= RuntimeInformation.OSDescription;  
	var str_OSIdent=RuntimeInformation.RuntimeIdentifier;          
	//output
	Console.WriteLine($"Версия .NET: {str_Framework}");
	Console.WriteLine($"Архитектура ОС: {str_OSArch}");
	Console.WriteLine($"Версия ОС: {str_OSDesc}");            
	Console.WriteLine($"Идентификатор ОС: {str_OSIdent}");
	//error
	Console.WriteLine("Создание исключения");                        
	throw new Exception("А вот и ошибочка!");
	
	Console.WriteLine("Завершение работы программы");     
}

Сохраним изменения. Далее, откроем меню Terminal => New Terminal:

Remote debug .NET Linux

Укажем команду сборки проекта:  dotnet build 

Затем команду для запуска приложения:  dotnet run 

После запуска, приложение вылетит с ошибкой.

Теперь запустим отладку, меню Run => Start Debugging, будет вызвано исключение.

В разделе Run => VARIABLES => Locals можно посмотреть текущее состояние переменных, так же при наведение указателя на переменную в коде, всплывает подсказка с содержанием переменной.

Отладка в Windows успешно работает!

Шаг 5 — Установка удаленного отладчика vsdbg в Armbian

Установка отладчика vsdbg

Установка vsdbg достаточно простая, при установке обязательно необходимо обратить внимание на архитектуру выбора отладчика, исходя из Runtime Identifiers (RIDs), для нас требуется значение: linux-arm и linux-arm64.

Выполним команду в Armbian, где linux-arm, linux-arm64 — архитектура платформы, в результате будет установлен отладчик vsdbg по пути  /usr/share/vsdbg :

ARM64ARM32
$ mkdir -p /usr/share/vsdbg
$ curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -r linux-arm64 -v latest -l /usr/share/vsdbg
$ mkdir -p /usr/share/vsdbg
$ curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -r linux-arm -v latest -l /usr/share/vsdbg

Результат выполнения:

ARM64ARM32
root@bananapim64:~# mkdir -p /usr/share/vsdbg
root@bananapim64:~# curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -r linux-arm64 -v latest -l /usr/share/vsdbg
Info: Previous installation at '/usr/share/vsdbg' not found
Info: Using vsdbg version '16.9.20122.2'
Using arguments
    Version                    : 'latest'
    Location                   : '/usr/share/vsdbg'
    SkipDownloads              : 'false'
    LaunchVsDbgAfter           : 'false'
    RemoveExistingOnUpgrade    : 'false'
Info: Using Runtime ID 'linux-arm64'
Downloading https://vsdebugger.azureedge.net/vsdbg-16-9-20122-2/vsdbg-linux-arm64.tar.gz
Info: Successfully installed vsdbg at '/usr/share/vsdbg'
root@cubietruck:~# mkdir -p /usr/share/vsdbg
root@cubietruck:~# curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -r linux-arm -v latest -l /usr/share/vsdbg
Info: Previous installation at '/usr/share/vsdbg' not found
Info: Using vsdbg version '16.9.20122.2'
Using arguments
    Version                    : 'latest'
    Location                   : '/usr/share/vsdbg'
    SkipDownloads              : 'false'
    LaunchVsDbgAfter           : 'false'
    RemoveExistingOnUpgrade    : 'false'
Info: Using Runtime ID 'linux-arm'
Downloading https://vsdebugger.azureedge.net/vsdbg-16-9-20122-2/vsdbg-linux-arm.tar.gz
Info: Successfully installed vsdbg at '/usr/share/vsdbg'

Проверить работу отладчика и узнать параметры запуска можно командой:  vsdbg -? 

root@bananapim64:/usr/share/vsdbg# ./vsdbg -?
Microsoft .NET Core Debugger (vsdbg)
Copyright (C) Microsoft Corporation.  All rights reserved.

Options:
--interpreter=vscode                  Standard input and output will contain
    Debug Adapter Protocol commands. This is currently the only supported
    interpreter mode and it is used for all Visual Studio products.
--pauseEngineForDebugger              Wait for a debugger to attach during startup
--engineLogging[=]  Enable logging to VsDbg-UI or file for the engine.
--elapsedTiming                       Include elapsed timing in engine logging.
--connection=                     Connect to the specified VsDbg-UI instance.
--heartbeat=                  Send a heartbeat message with given period (in
    seconds).
--tty=      

Шаг 6 — Конфигурирование launch.json и tasks.json для удаленной отладки

Все подготовительные шаги для удаленной отладки выполнены, остается внести изменения в конфигурацию проекта. Начнем с задач, файл tasks.json содержит выполняемые задачи. Для удаленной отладки необходимо последовательно выполнить две задачи: скомпилировать проект для Linux ARM, скопировать полученные файлы на плату с Armbian, и в конечном итоге запустить удаленный отладчик.

По умолчанию в файле tasks.json определены три задачи: build, publish и watch. Эти три задачи изменять не будем, создадим новую задачу build-linux-arm на основе задачи build. И задачу copy-to-device, которая будет запускать программу rsync для копирование файлов.

На GitHub версия проекта для ARM64 доступна по ссылке RemoteAppArm64, версия для ARM32 RemoteAppArm.

Файл tasks.json, задача build-linux-arm:

ARM64ARM32
{
	"label": "build-linux-arm",
	"command": "dotnet",
	"type": "process",
	"args": [
		"build",
		"${workspaceFolder}/RemoteAppArm64.csproj",
		"--configuration","Debug",
		"/property:GenerateFullPaths=true",
		"/consoleloggerparameters:NoSummary",
		"-r","linux-arm64"                
	],
	"problemMatcher": "$msCompile"
}
{
	"label": "build-linux-arm",
	"command": "dotnet",
	"type": "process",
	"args": [
		"build",
		"${workspaceFolder}/RemoteAppArm.csproj",
		"--configuration","Debug",
		"/property:GenerateFullPaths=true",
		"/consoleloggerparameters:NoSummary",
		"-r","linux-arm"                
	],
	"problemMatcher": "$msCompile"
}

Добавляем параметры:

  • «—configuration»,»Debug» — для включения отладки;
  • «-r»,»linux-arm64″ — для компиляции под архитектуру ARM.

Файл tasks.json, задача copy-to-device

ARM64ARM32
{
	"label": "copy-to-device",
	"dependsOn":"build-linux-arm",
	"command": "C:\\RemoteCode\\cwrsync\\rsync.exe",
	"type": "process",
	"args": [
		"--log-file=rsync.log",
		"--progress",
		"-avz" ,
		"-e",
		"C:\\RemoteCode\\cwrsync\\ssh.exe -i C:\\RemoteCode\\id_rsa -o 'StrictHostKeyChecking no'",
		"/cygdrive/c/RemoteCode/Projects/RemoteAppArm64/bin/Debug/net5.0/linux-arm64/",
		"root@192.168.43.225:/root/RemoteAppArm64"
	],
	 "problemMatcher": "$msCompile"
}
{
	"label": "copy-to-device",
	"dependsOn":"build-linux-arm",
	"command": "C:\\RemoteCode\\cwrsync\\rsync.exe",
	"type": "process",
	"args": [
		"--log-file=rsync.log",
		"--progress",
		"-avz" ,
		"-e",
		"C:\\RemoteCode\\cwrsync\\ssh.exe -i C:\\RemoteCode\\id_rsa -o 'StrictHostKeyChecking no'",
		"/cygdrive/c/RemoteCode/Projects/RemoteAppArm/bin/Debug/net5.0/linux-arm/",
		"root@192.168.43.12:/root/RemoteAppArm"
	],
	 "problemMatcher": "$msCompile"
}

Описание параметров:

  • «dependsOn»:»build-linux-arm» — зависимость от задачи build-linux-arm, задача copy-to-device выполнится только после задачи build-linux-arm. Параметр dependsOn позволяет указывать несколько задач, но в этом случае они будут выполняться параллельно. Если требуется выполнять последовательно три задачи, то тогда следует в задачу task3 добавить параметр «dependsOn»:»task2″, а в задачу task2 параметр «dependsOn»:»task1″.
  • «command»: «c:\\RemoteCode\\cwrsync\\rsync.exe» — запускаемая программа rsync.exe для копирования файлов раздел «args» — содержит список аргументов передаваемые программе rsync.exe.
  • «—log-file=rsync.log» — включим журналирование в файл rsync.log, для проверки какие файлы были скопированы.
  • «—progress» — визуализация процесса копирования файлов в режиме отладки
  • «c:\\RemoteCode\\cwrsync\\ssh.exe -i c:\\RemoteCode\\id_rsa», — приватный ключ доступа к плате ARM на Armbian
  • «/cygdrive/c/RemoteCode/Projects/RemoteAppArm64/bin/Debug/net5.0/linux-arm64/» — папка со сборками и файлами, которые необходимо скопировать на плату ARM
  • «root@192.168.43.225:/root/RemoteAppArm64» — Сборки и файлы будут скопирована по пути  /root/RemoteAppArm64  на плату ARM с IP-адресом 192.168.43.225. На плате ARM64 потребуется создать папку по пути  /root/RemoteAppArm64 , на ARM32 —  /root/RemoteAppArm !

Создание папки для запускаемых файлов на плате:

ARM64ARM32
$ mkdir - p ~/RemoteAppArm64
$ mkdir - p ~/RemoteAppArm

Файл launch.json. Содержание текущего файла полностью удалим и заменим на представленный вариант:

ARM64ARM32
"configurations": [
        {
            "name": ".NET Core Remote Launch - Framework Dependent (console)",
            "type": "coreclr",
            "request": "launch",
            "program": "dotnet",
            "args": ["./RemoteAppArm64.dll"],
            "cwd": "~/RemoteAppArm64",
            "stopAtEntry": false,
            "console": "internalConsole",
            "pipeTransport": {
                "pipeCwd": "${workspaceRoot}",
                "pipeProgram": "C:\\RemoteCode\\putty\\PLINK.EXE",
                "pipeArgs": [
                    "-i",
                    "C:\\RemoteCode\\id_rsa.ppk",
                    "root@192.168.43.225"
                ],
                "debuggerPath": "/usr/share/vsdbg/vsdbg --engineLogging=/var/log/vsdbg.log"
            },            
            "preLaunchTask": "copy-to-device", 
        },        
        {
            "name": ".NET Core Attach",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickProcess}"
        }
    ]
"configurations": [
        {
            "name": ".NET Core Remote Launch - Framework Dependent (console)",
            "type": "coreclr",
            "request": "launch",
            "program": "dotnet",
            "args": ["./RemoteAppArm.dll"],
            "cwd": "~/RemoteAppArm",
            "stopAtEntry": false,
            "console": "internalConsole",
            "pipeTransport": {
                "pipeCwd": "${workspaceRoot}",
                "pipeProgram": "C:\\RemoteCode\\putty\\PLINK.EXE",
                "pipeArgs": [
                    "-i",
                    "C:\\RemoteCode\\id_rsa.ppk",
                    "root@192.168.43.12"
                ],
                "debuggerPath": "/usr/share/vsdbg/vsdbg --engineLogging=/var/log/vsdbg.log"
            },            
            "preLaunchTask": "copy-to-device", 
        },        
        {
            "name": ".NET Core Attach",
            "type": "coreclr",
            "request": "attach",
            "processId": "${command:pickProcess}"
        }
    ]
Описание параметров:

  • «args»: [«./RemoteAppArm64.dll»] — запускаемая сборка   RemoteAppArm64.dll 
  • «cwd»: «~/СonsoleAppUbuntu» — папка  ~/СonsoleAppUbuntu  с бинарными файлами для запуска
  • «pipeTransport» — раздел для связи отладчика используя транспорт в виде программы  PLINK.EXE  , указываем ключ и IP-адрес подключения
  • «debuggerPath»: «~/vsdbg/vsdbg» — путь с программе отладчика на плате в Armbian
  • «preLaunchTask»: «copy-to-device» — до выполнения отладки необходимо выполнить задачу copy-to-device, которая соберет проект и выполнить копирование его на плату Arm под Armbian.

Сохраним все изменения, и перейдем к следующему шагу.

Шаг 7 — Проверка удаленной отладки путем вызова исключения.

Если вы все верно выполнили на предыдущих шагах, то после запуска Run => Start Debugging, произойдет компиляция проекта под архитектуру ARM32/ARM64, затем результат будет скопирован на отладочную плату ARM. Для проверки какие файлы были скопированы посмотрите файл rsync.log в папке проекта. Если в файле будет указана сборка включая  /RemoteAppArm64.dll  или  /RemoteAppArm.dll , значит все отлично, и остается последний шаг это связь c  отладчиком. После запуска приложения в Visual Studio Code вы увидите:

Remote debug .NET Linux

Удаленная отладка на ARM работает!

Проекты на GitHub — Remote-Debugging-with-VS-Code-On-Windows-to-a-ARM-using—NET

Литература

  1. Debug .NET Core on Linux using SSH by attaching to a process — Visual Studio Docs
  2. Remote Debugging On Linux Arm — omnisharp-vscode
  3. Debug .NET apps on Raspberry Pi — .NET IoT Libraries
  4. Remote debugging with VS Code on Windows to a Raspberry Pi using .NET Core on ARM — SCOTT HANSELMAN
  5. Attaching to remote processes — omnisharp-vscode
  6. Offroad Debugging of .NET Core on Linux OSX from Visual Studio — microsoft/MIEngine
  7. Remote Debugging of a .Net Core application with VS Code on PLCnext — KAY SUTTKUS Makers Blog
  8. Tasks (legacy version) — Visual Studio Code
  9. How to chain tasks in Visual Studio Code using only tasks.json? — stackoverflow
  10. Debugging — Visual Studio Code
  11. Visual Studio Code remote debugging of a .Net Core application running on Raspberry Pi and Ubuntu Linux — jenx.si
  12. Как настроить ключи SSH в Ubuntu 18.04 — DigitalOcean
  13. How To Use Rsync to Sync Local and Remote Directories — DigitalOcean
  14. Using “preLaunchTasks” and Naming a Task in Visual Studio Code — stackoverflow
  15. PuTTY выдает ошибку Unable to use key file — Atlex
  16. windows rsync from different local drive, other than c, to remote — serverfault

Вам также может понравиться

About the Author: Anton

Programistik