├── .github ├── ISSUE_TEMPLATE │ ├── Bug_report.md │ └── Feature_request.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── StyleCop.Analyzers.ruleset ├── Unosquare.Labs.SshDeploy.sln ├── Unosquare.Labs.SshDeploy ├── Attributes │ ├── MonitorAttribute.cs │ ├── PushAttribute.cs │ ├── RunAttribute.cs │ ├── ShellAttribute.cs │ └── VerbAttributeBase.cs ├── DeploymentManager.Monitor.cs ├── DeploymentManager.Push.cs ├── DeploymentManager.Shell.cs ├── DeploymentManager.cs ├── FileSystemEntry.cs ├── FileSystemEntryChangeType.cs ├── FileSystemEntryChangedEventArgs.cs ├── FileSystemEntryDictionary.cs ├── FileSystemMonitor.cs ├── Options │ ├── CliExecuteOptionsBase.cs │ ├── CliOptions.cs │ ├── CliVerbOptionsBase.cs │ ├── MonitorVerbOptions.cs │ ├── PushVerbOptions.cs │ ├── RunVerbOptions.cs │ └── ShellVerbOptions.cs ├── Program.cs ├── Unosquare.Labs.SshDeploy.csproj └── Utils │ ├── CsProjFile.cs │ ├── CsProjMetadataBase.cs │ └── CsProjNuGetMetadata.cs ├── _config.yml └── sshdeploy.png /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | nuget-feed-npm-pkg-github-com-unosquare: 4 | type: nuget-feed 5 | url: https://npm.pkg.github.com/unosquare 6 | token: "${{secrets.NUGET_FEED_NPM_PKG_GITHUB_COM_UNOSQUARE_TOKEN}}" 7 | nuget-feed-nuget-pkg-github-com-unosquare-index-json: 8 | type: nuget-feed 9 | url: https://nuget.pkg.github.com/unosquare/index.json 10 | username: "${{secrets.NUGET_FEED_NUGET_PKG_GITHUB_COM_UNOSQUARE_INDEX_JSON_USERNAME}}" 11 | password: "${{secrets.NUGET_FEED_NUGET_PKG_GITHUB_COM_UNOSQUARE_INDEX_JSON_PASSWORD}}" 12 | 13 | updates: 14 | - package-ecosystem: nuget 15 | directory: "/" 16 | schedule: 17 | interval: daily 18 | time: "11:00" 19 | open-pull-requests-limit: 10 20 | registries: 21 | - nuget-feed-npm-pkg-github-com-unosquare 22 | - nuget-feed-nuget-pkg-github-com-unosquare-index-json 23 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Test on .NET Core ${{ matrix.os }} 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 3.1.100 16 | - name: Test with dotnet 17 | run: | 18 | dotnet build 19 | dotnet pack 20 | dotnet tool install -g dotnet-sshdeploy --add-source ./ 21 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 21 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['csharp'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Unosquare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project has been archived** 2 | 3 | # dotnet-sshdeploy 4 | [![NuGet](https://img.shields.io/nuget/dt/dotnet-sshdeploy.svg)](https://www.nuget.org/packages/dotnet-sshdeploy/) 5 | [![Analytics](https://ga-beacon.appspot.com/UA-8535255-2/unosquare/sshdeploy/)](https://github.com/igrigorik/ga-beacon) 6 | [![Build Status](https://travis-ci.org/unosquare/sshdeploy.svg?branch=master)](https://travis-ci.org/unosquare/sshdeploy) 7 | [![Build status](https://ci.appveyor.com/api/projects/status/p6c0whp2xfajuu0c?svg=true)](https://ci.appveyor.com/project/geoperez/sshdeploy) 8 | [![NuGet version](https://badge.fury.io/nu/dotnet-sshdeploy.svg)](https://badge.fury.io/nu/dotnet-sshdeploy) 9 | 10 | :star: *Please star this project if you find it useful!* 11 | 12 | A `dotnet` CLI command that enables quick deployments over SSH. This program was specifically designed to streamline .NET application development for the Raspberry Pi running Raspbian. 13 | 14 | **If you came here looking for our old version of SSHDeploy please click [here](https://www.nuget.org/packages/SSHDeploy/), otherwise you are in the right place** 15 | 16 | The following commands are currently available: 17 | * `dotnet-sshdeploy monitor` - Watches changes on a single file, if this event is raised then it proceeds to send the specified source path files over SSH 18 | * `dotnet-sshdeploy push` - Single-use command that transfers files over SSH 19 | 20 | ## Installation 21 | We are using the brand new implementation of the global tool in .NET Core Apps 2.1+. Now you can easily download the package by running the next command 22 | 23 | ```console 24 | dotnet tool install -g dotnet-sshdeploy 25 | ``` 26 | 27 | ### Custom installation 28 | If you download the project and want to test installing your own version of the project you need to pack and then install the nuget 29 | 30 | ```console 31 | // In the root of your project run 32 | dotnet pack 33 | 34 | // Run the following command where you nupkg was created 35 | dotnet tool install -g dotnet-sshdeploy --add-source ./ 36 | 37 | ``` 38 | ### Update 39 | To update ssh-deploy to the latest version, use the dotnet tool update command 40 | 41 | ```console 42 | dotnet tool update -g dotnet-sshdeploy 43 | ``` 44 | 45 | ## Usage 46 | **There are two ways of passing arguments: the old school way using the cli and our approach using the csproj file.** 47 | 48 | ### Using the csproj file 49 | 50 | #### Push 51 | 1. Edit your csproj file and add: 52 | 53 | ```xml 54 | 55 | 192.168.2.194 56 | 57 | /home/pi/libfprint-cs 58 | pi 59 | raspberry 60 | OnBuildSuccess 61 | 62 | ``` 63 | 2. We need a post build event as well: 64 | 65 | ```xml 66 | 67 | 68 | 69 | 70 | ``` 71 | *Voilà! sshdeploy finds the necessary arguments provided using proper xml tags and deploys after a successful build* 72 | 73 | * **Be sure you are using ' */* ' with *RemoteTargetPath* otherwise it will not work.** 74 | * **You MUST use the property** `BuildingInsideSshDeploy` **to make sure this event will not be executed within sshdeploy's build method to avoid an infinite loop** 75 | * **If no RuntimeIdentifier is provided a [Framework-dependent deployment](https://docs.microsoft.com/en-us/dotnet/core/deploying/) will be created otherwise a [Self-contained deployment](https://docs.microsoft.com/en-us/dotnet/core/deploying/) will** 76 | * **The command needs to be excuted in the same folder as the csproj** 77 | 78 | If your project happens to target multiple runtimes, i.e. `win-x64` and `linux-arm`, then sshdeploy does not necessarily know which binaries to deploy. Also, you might want to control that i.e. only the `linux-arm` build should be automatically deployed. In this case, you can change the post build event and add an additional condition to the target (only run on builds for linux), and also pass the desired runtime identifier to the actual deployment call as follows: 79 | 80 | ```xml 81 | 82 | 83 | 84 | 85 | ``` 86 | 87 | #### Monitor 88 | 1. Go to your Visual Studio Solution (the one you intend to continuously deploy to the Raspberry Pi). 89 | 2. Right-click on the project and click on the menu item "Properties" 90 | 3. Go to the "Build Events" tab, and under Post-build events, enter the following: 91 | * `echo %DATE% %TIME% >> "$(TargetDir)sshdeploy.ready"` 92 | *This simply writes the date and time to the `sshdeploy.ready` file. Whenever this file CHANGES, the deployment tool will perform a deployment. 93 | 4. Edit your csproj file and add: 94 | ```xml 95 | 192.168.2.194 96 | C:\projects\Unosquare.Labs.RasPiConsole\Unosquare.Labs.RasPiConsole\bin\Debug 97 | /home/pi/libfprint-cs 98 | pi 99 | raspberry 100 | ``` 101 | 5. Execute 102 | ``` 103 | dotnet-sshdeploy monitor 104 | ``` 105 | 106 | **FYI: Arguments passed using the csproj file will not override the ones provided using the cli** 107 | ### XML Tags 108 | Heres a complete list of arguments with their corresponding XML tag. 109 | 110 | | Args | XML Tag | 111 | | :-------------- | :----------------------------: | 112 | | -m,--monitor | `` | 113 | | -f,--framework | `` | 114 | | -r,--runtime | `` | 115 | | -s, --source | `` | 116 | | -t,--target | `` | 117 | | --pre | `` | 118 | | --post | `` | 119 | | --clean | `` | 120 | | --exclude | `` | 121 | | -v,--verbose | `` | 122 | | -h,--host | `` | 123 | | -p,--port | `` | 124 | | -u,--username | `` | 125 | | -w,--password | `` | 126 | | -l,--legacy | `` | 127 | | -x, --execute | `` | 128 | 129 | ### Old school way 130 | #### Push 131 | 1. Navigate to your project folder where the csproj file resides. Example: 132 | ``` 133 | cd C:\projects\Unosquare.Labs.RasPiConsole\Unosquare.Labs.RasPiConsole\ 134 | ``` 135 | 2. Execute this command with some arguments. Here's a simple example: 136 | ``` 137 | dotnet-sshdeploy push -f netcoreapp2.0 -t "/home/pi/libfprint-cs" -h 192.168.2.194 138 | ``` 139 | * In the command shown above : 140 | * `-f` refers to the source framework 141 | * `-t` refers to the target path 142 | * `-h` refers to the host (IP address of the Raspberry Pi) 143 | * For a detailed list of all the arguments available please see [below](#push-mode) or execute `dotnet-sshdeploy push` 144 | 145 | #### Monitor 146 | 147 | The following steps outline a continuous deployment of a Visual Studio solution to a Raspberry Pi running the default Raspbian SSH daemon. 148 | 1. Go to your Visual Studio Solution (the one you intend to continously deploy to the Raspberry Pi). 149 | 2. Right-click on the project and click on the menu item "Properties" 150 | 3. Go to the "Build Events" tab, and under Post-build events, enter the following: 151 | * `echo %DATE% %TIME% >> "$(TargetDir)sshdeploy.ready"` 152 | *This simply writes the date and time to the `sshdeploy.ready` file. Whenever this file CHANGES, the deployment tool will perform a deployment. 153 | 4. Open a Command Prompt (Start, Run, cmd, [Enter Key]) 154 | 5. Navigate to your project folder where the csproj file resides 155 | * Example: `cd "C:\projects\Unosquare.Labs.RasPiConsole\Unosquare.Labs.RasPiConsole\"` 156 | 6. Run this tool with some arguments. Here is an example so you can get started quickly. 157 | ``` 158 | dotnet-sshdeploy monitor -s "C:\projects\Unosquare.Labs.RasPiConsole\Unosquare.Labs.RasPiConsole\bin\Debug" -t "/home/pi/target" -h 192.168.2.194 -u pi -w raspberry 159 | ``` 160 | * In the above command, 161 | * `-s` refers to the source path of the files to transfer. 162 | * `t` refers to the full path of the target directory. 163 | * `-h` refers to the host (IP address of the Raspberry Pi). 164 | * `-u` refers to the login. 165 | * `-w` refers to the password. 166 | * Note that there are many more arguments you can use. Simply issue 167 | ``` 168 | dotnet-sshdeploy monitor 169 | ``` 170 | This will get you all the options you can use. 171 | 172 | * If all goes well you will see output similar to this: 173 | ``` 174 | SSH Deployment Tool [Version 0.3.1.0] 175 | (c)2015 - 2017 Unosquare SA de CV. All Rights Reserved. 176 | For additional help, please visit https://github.com/unosquare/sshdeploy 177 | 178 | Monitor mode starting 179 | Monitor parameters follow: 180 | Monitor File C:\projects\Unosquare.Labs.RasPiConsole\Unosquare.Labs.RasPiConsole\bin\Debug\sshdeploy.ready 181 | Source Path C:\projects\Unosquare.Labs.RasPiConsole\Unosquare.Labs.RasPiConsole\bin\Debug 182 | Excluded Files .ready|.vshost.exe|.vshost.exe.config 183 | Target Address 192.168.2.194:22 184 | Username pi 185 | Target Path /home/pi/target 186 | Clean Target YES 187 | Pre Deployment 188 | Post Deployment 189 | Connecting to host 192.168.2.194:22 via SSH. 190 | Connecting to host 192.168.2.194:22 via SFTP. 191 | File System Monitor is now running. 192 | Writing a new monitor file will trigger a new deployment. 193 | Remember: Press Q to quit. 194 | Ground Control to Major Tom: Have a nice trip in space! 195 | ``` 196 | 7. Now go back to your Visual Studio Solution, right click on the project, a select "Rebuild". You should see the output in the command line similar to the following: 197 | ``` 198 | Starting deployment ID 1 - Sunday, June 14, 2015 10:16:20 PM 199 | Cleaning Target Path '/home/pi/target' 200 | Deploying 3 files. 201 | Finished deployment in 0.88 seconds. 202 | ``` 203 | * Every time you rebuild your project, it will be automatically deployed! 204 | 205 | * *In order to make this tool much more useful, we need to take advantage of the pre and post commands. The idea is to find the process and kill it if it is currently running on the pre-command, and run the process once the deployment has been completed using the post-command argument. The hope is that this will make the deploy, run, and debug cycle, much less tedious for a .NET developer using a Raspberry Pi.* 206 | 207 | * Here's a good example of using pre and post commands to acocmplish the above: 208 | ```dotnet-sshdeploy monitor -s "C:\projects\libfprint-cs\trunk\Unosquare.Labs.LibFprint.Tests\bin\Debug" -t "/home/pi/libfprint-cs" -h 192.168.2.194 --pre "pgrep -f 'Unosquare.Labs.LibFprint.Tests.exe' | xargs -r kill" --post "mono /home/pi/libfprint-cs/Unosquare.Labs.LibFprint.Tests.exe" --clean False``` 209 | ## References 210 | ### Monitor Mode 211 | 212 | 213 | |Short Argument | Long Argument | Description | Default | Required | 214 | |:------------- | :------------ | :---------------------------------------------------- | :-----------:| :----------------: | 215 | | -m | --monitor | The path to the file used as a signal that the files are ready to be deployed. Once the deploymetn is completed,the file is deleted. | sshdeploy.ready | :heavy_check_mark:| 216 | | -s | --source | The source path for the files to transfer. | | :heavy_check_mark: | 217 | | -t | --target | The target path of the files to transfer. | | :heavy_check_mark: | 218 | | | --pre | Command to execute prior file transfer to target. | | :x: | 219 | | | --post | Command to execute after file transfer to target. | | :x: | 220 | | | --clean | Deletes all files and folders on the target before pushing the new files | True | :x: | 221 | | | --exclude | a pipe (\|) separated list of file suffixes to ignore while deploying. | .ready\|.vshost.exe\|.vshost.exe.config |:x:| 222 | | -v | --verbose |Add this option to print messages to standard error and standard output streams. | True | :x: | 223 | | -h | --host | Hostname or IP Address of the target. -- Must be running an SSH server. | | :heavy_check_mark: | 224 | | -p | --port | Port on which SSH is running. | 22 | :x: | 225 | | -u | --username | The username under which the connection will be established. | pi | :x: | 226 | | -w | --password |The password for the given username. | raspberry | :x: | 227 | | -l | --legacy | Monitor files using legacy method | False | :x: | 228 | 229 | ### Push Mode 230 | 231 | 232 | |Short Argument | Long Argument | Description | Default | Required | 233 | |:------------- | :-------------- | :---------------------------------------------------- | :-----------: | :---------------: | 234 | | -c | --configuration | Target configuration. | Debug | :x: | 235 | | -f | --framework | The source framework. | | :heavy_check_mark:| 236 | | | --pre | Command to execute prior file transfer to target. | | :x: | 237 | | | --post | Command to execute after file transfer to target. | | :x: | 238 | | | --clean | Deletes all files and folders on the target before pushing the new files. | True | :x: | 239 | | | --exclude | a pipe (\|) separated list of file suffixes to ignore while deploying. |.ready\|.vshost.exe\|.vshost.exe.config | :x: | 240 | | -v | --verbose | Add this option to print messages to standard error and standard output streams. | True | :x: | 241 | | -h | --host | Hostname or IP Address of the target. -- Must be running an SSH server. | | :heavy_check_mark: | 242 | | -p | --port | Port on which SSH is running. | 22 | :x: | 243 | | -u | --username | The username under which the connection will be established. | pi | :x: | 244 | | -w | --password | The password for the given username. | raspberry | :x: | 245 | | -x | --execute | Adds user execute permissions to the deployed files. | False | :x: | 246 | 247 | 248 | ## Special Thanks 249 | 250 | This code uses the very cool Renci's [SSH.NET library](https://github.com/sshnet/SSH.NET) and our awesome [SWAN library](https://github.com/unosquare/swan). 251 | -------------------------------------------------------------------------------- /StyleCop.Analyzers.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29519.181 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.Labs.SshDeploy", "Unosquare.Labs.SshDeploy\Unosquare.Labs.SshDeploy.csproj", "{24BAC92F-A5E9-44EE-BD09-071B6ED9C189}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Folder", "Solution Folder", "{E2C911E5-AE4D-48FA-BB43-CE8C6B12C393}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | README.md = README.md 12 | StyleCop.Analyzers.ruleset = StyleCop.Analyzers.ruleset 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {24BAC92F-A5E9-44EE-BD09-071B6ED9C189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {24BAC92F-A5E9-44EE-BD09-071B6ED9C189}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {24BAC92F-A5E9-44EE-BD09-071B6ED9C189}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {24BAC92F-A5E9-44EE-BD09-071B6ED9C189}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(ExtensibilityGlobals) = postSolution 30 | SolutionGuid = {43A0E767-C8DA-447B-AF83-9578D6B96A3B} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Attributes/MonitorAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Attributes 2 | { 3 | internal class MonitorAttribute : VerbAttributeBase 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Attributes/PushAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Attributes 2 | { 3 | internal class PushAttribute : VerbAttributeBase 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Attributes/RunAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Attributes 2 | { 3 | internal class RunAttribute : VerbAttributeBase 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Attributes/ShellAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Attributes 2 | { 3 | internal class ShellAttribute : VerbAttributeBase 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Attributes/VerbAttributeBase.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Attributes 2 | { 3 | using System; 4 | 5 | [AttributeUsage(AttributeTargets.Property)] 6 | internal class VerbAttributeBase : Attribute 7 | { 8 | public string? ShortName { get; set; } 9 | 10 | public string? LongName { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/DeploymentManager.Monitor.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using Options; 4 | using Renci.SshNet; 5 | using Renci.SshNet.Common; 6 | using Swan; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Diagnostics; 10 | using System.IO; 11 | using System.Linq; 12 | using Swan.Logging; 13 | 14 | public partial class DeploymentManager 15 | { 16 | #region State Variables 17 | 18 | private static bool _forwardShellStreamOutput; 19 | private static bool _forwardShellStreamInput; 20 | private static bool _isDeploying; 21 | private static int _deploymentNumber; 22 | 23 | #endregion 24 | 25 | #region Main Verb Methods 26 | 27 | /// 28 | /// Executes the monitor verb. Using a legacy method. 29 | /// 30 | /// The verb options. 31 | /// Source Path ' + sourcePath + ' was not found. 32 | internal static void ExecuteMonitorVerbLegacy(MonitorVerbOptions verbOptions) 33 | { 34 | // Initialize Variables 35 | _isDeploying = false; 36 | _deploymentNumber = 1; 37 | 38 | // Normalize and show the options to the user so he knows what he's doing 39 | NormalizeMonitorVerbOptions(verbOptions); 40 | PrintMonitorOptions(verbOptions); 41 | 42 | // Create the FS Monitor and connection info 43 | var fsmonitor = new FileSystemMonitor(1, verbOptions.SourcePath); 44 | var simpleConnectionInfo = new PasswordConnectionInfo( 45 | verbOptions.Host, 46 | verbOptions.Port, 47 | verbOptions.Username, 48 | verbOptions.Password); 49 | 50 | // Validate source path exists 51 | if (Directory.Exists(verbOptions.SourcePath) == false) 52 | throw new DirectoryNotFoundException("Source Path '" + verbOptions.SourcePath + "' was not found."); 53 | 54 | // Instantiate an SFTP client and an SSH client 55 | // SFTP will be used to transfer the files and SSH to execute pre-deployment and post-deployment commands 56 | using var sftpClient = new SftpClient(simpleConnectionInfo); 57 | 58 | // SSH will be used to execute commands and to get the output back from the program we are running 59 | using var sshClient = new SshClient(simpleConnectionInfo); 60 | 61 | // Connect SSH and SFTP clients 62 | EnsureMonitorConnection(sshClient, sftpClient, verbOptions); 63 | 64 | // Create the shell stream so we can get debugging info from the post-deployment command 65 | using var shellStream = CreateShellStream(sshClient); 66 | 67 | // Starts the FS Monitor and binds the event handler 68 | StartMonitorMode(fsmonitor, sshClient, sftpClient, shellStream, verbOptions); 69 | 70 | // Allows user interaction with the shell 71 | StartUserInteraction(sshClient, sftpClient, shellStream, verbOptions); 72 | 73 | // When we quit, we stop the monitor and disconnect the clients 74 | StopMonitorMode(sftpClient, sshClient, fsmonitor); 75 | } 76 | 77 | /// 78 | /// Executes the monitor verb. This is the main method. 79 | /// 80 | /// The verb options. 81 | /// Source Path ' + sourcePath + ' was not found. 82 | internal static void ExecuteMonitorVerb(MonitorVerbOptions verbOptions) 83 | { 84 | // Initialize Variables 85 | _isDeploying = false; 86 | _deploymentNumber = 1; 87 | 88 | // Normalize and show the options to the user so he knows what he's doing 89 | NormalizeMonitorVerbOptions(verbOptions); 90 | PrintMonitorOptions(verbOptions); 91 | 92 | // Create connection info 93 | var simpleConnectionInfo = new PasswordConnectionInfo( 94 | verbOptions.Host, 95 | verbOptions.Port, 96 | verbOptions.Username, 97 | verbOptions.Password); 98 | 99 | // Create a file watcher 100 | var watcher = new FileSystemWatcher 101 | { 102 | Path = verbOptions.SourcePath, 103 | NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName, 104 | Filter = Path.GetFileName(verbOptions.MonitorFile), 105 | }; 106 | 107 | // Validate source path exists 108 | if (Directory.Exists(verbOptions.SourcePath) == false) 109 | throw new DirectoryNotFoundException($"Source Path \'{verbOptions.SourcePath}\' was not found."); 110 | 111 | // Instantiate an SFTP client and an SSH client 112 | // SFTP will be used to transfer the files and SSH to execute pre-deployment and post-deployment commands 113 | using var sftpClient = new SftpClient(simpleConnectionInfo); 114 | 115 | // SSH will be used to execute commands and to get the output back from the program we are running 116 | using var sshClient = new SshClient(simpleConnectionInfo); 117 | 118 | // Connect SSH and SFTP clients 119 | EnsureMonitorConnection(sshClient, sftpClient, verbOptions); 120 | 121 | // Create the shell stream so we can get debugging info from the post-deployment command 122 | using var shellStream = CreateShellStream(sshClient); 123 | 124 | // Adds an onChange event and enables it 125 | watcher.Changed += (s, e) => 126 | CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); 127 | watcher.EnableRaisingEvents = true; 128 | 129 | Terminal.WriteLine("File System Monitor is now running."); 130 | Terminal.WriteLine("Writing a new monitor file will trigger a new deployment."); 131 | Terminal.WriteLine("Press H for help!"); 132 | Terminal.WriteLine("Ground Control to Major Tom: Have a nice trip in space!.", ConsoleColor.DarkCyan); 133 | 134 | // Allows user interaction with the shell 135 | StartUserInteraction(sshClient, sftpClient, shellStream, verbOptions); 136 | 137 | // When we quit, we stop the monitor and disconnect the clients 138 | StopMonitorMode(sftpClient, sshClient, watcher); 139 | } 140 | 141 | #endregion 142 | 143 | #region Supporting Methods 144 | 145 | /// 146 | /// Deletes the linux directory recursively. 147 | /// 148 | /// The client. 149 | /// The path. 150 | private static void DeleteLinuxDirectoryRecursive(SftpClient client, string path) 151 | { 152 | var files = client.ListDirectory(path); 153 | 154 | foreach (var file in files) 155 | { 156 | if (file.Name.Equals(LinuxCurrentDirectory) || file.Name.Equals(LinuxParentDirectory)) 157 | continue; 158 | 159 | if (file.IsDirectory) 160 | { 161 | DeleteLinuxDirectoryRecursive(client, file.FullName); 162 | } 163 | 164 | try 165 | { 166 | client.Delete(file.FullName); 167 | } 168 | catch 169 | { 170 | $"WARNING: Failed to delete file or folder '{file.FullName}'".Error(nameof(DeleteLinuxDirectoryRecursive)); 171 | } 172 | } 173 | } 174 | 175 | /// 176 | /// Creates the linux directory recursively. 177 | /// 178 | /// The client. 179 | /// The path. 180 | /// Argument path must start with + LinuxDirectorySeparator. 181 | private static void CreateLinuxDirectoryRecursive(SftpClient client, string path) 182 | { 183 | if (path.StartsWith(LinuxDirectorySeparator) == false) 184 | throw new ArgumentException("Argument path must start with " + LinuxDirectorySeparator); 185 | 186 | if (client.Exists(path)) 187 | { 188 | var info = client.GetAttributes(path); 189 | if (info.IsDirectory) 190 | return; 191 | } 192 | 193 | var pathParts = path.Split(new[] { LinuxDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); 194 | 195 | pathParts = pathParts.Skip(0).Take(pathParts.Length - 1).ToArray(); 196 | var priorPath = LinuxDirectorySeparator + string.Join(LinuxDirectorySeparator, pathParts); 197 | 198 | if (pathParts.Length > 1) 199 | CreateLinuxDirectoryRecursive(client, priorPath); 200 | 201 | client.CreateDirectory(path); 202 | } 203 | 204 | /// 205 | /// Runs pre and post deployment commands over the SSH client. 206 | /// 207 | /// The shell stream. 208 | /// The verb options. 209 | private static void RunShellStreamCommand(ShellStream shellStream, CliExecuteOptionsBase verbOptions) 210 | { 211 | var commandText = verbOptions.PostCommand; 212 | if (string.IsNullOrWhiteSpace(commandText)) return; 213 | 214 | Terminal.WriteLine(" Executing shell command.", ConsoleColor.Green); 215 | shellStream.Write($"{commandText}\r\n"); 216 | shellStream.Flush(); 217 | Terminal.WriteLine($" TX: {commandText}", ConsoleColor.DarkYellow); 218 | } 219 | 220 | /// 221 | /// Runs the deployment command. 222 | /// 223 | /// The SSH client. 224 | /// The verb options. 225 | private static void RunSshClientCommand(SshClient sshClient, CliExecuteOptionsBase verbOptions) 226 | { 227 | var commandText = verbOptions.PreCommand; 228 | if (string.IsNullOrWhiteSpace(commandText)) return; 229 | 230 | Terminal.WriteLine(" Executing SSH client command.", ConsoleColor.Green); 231 | 232 | var result = RunCommand(sshClient, commandText); 233 | Terminal.WriteLine($" SSH TX: {commandText}", ConsoleColor.DarkYellow); 234 | Terminal.WriteLine($" SSH RX: [{result.ExitStatus}] {result.Result} {result.Error}", ConsoleColor.DarkYellow); 235 | } 236 | 237 | private static void RunCommand(SshClient sshClient, string type, string command) 238 | { 239 | if (string.IsNullOrWhiteSpace(command)) return; 240 | 241 | Terminal.WriteLine($" Executing SSH {type} command.", ConsoleColor.Green); 242 | 243 | var result = RunCommand(sshClient, command); 244 | Terminal.WriteLine($" SSH TX: {command}", ConsoleColor.DarkYellow); 245 | Terminal.WriteLine($" SSH RX: [{result.ExitStatus}] {result.Result} {result.Error}", ConsoleColor.DarkYellow); 246 | } 247 | 248 | private static void AllowExecute(SshClient sshClient, PushVerbOptions verbOptions) 249 | { 250 | if (!bool.TryParse(verbOptions.Execute, out var value) || !value) return; 251 | 252 | Terminal.WriteLine(" Changing mode.", ConsoleColor.Green); 253 | var target = Path.Combine(verbOptions.TargetPath, "*").Replace(WindowsDirectorySeparatorChar, LinuxDirectorySeparatorChar); 254 | var command = $"chmod -R u+x {target}"; 255 | 256 | var result = RunCommand(sshClient, command); 257 | Terminal.WriteLine($" SSH TX: {command}", ConsoleColor.DarkYellow); 258 | Terminal.WriteLine($" SSH RX: [{result.ExitStatus}] {result.Result} {result.Error}", ConsoleColor.DarkYellow); 259 | } 260 | 261 | private static SshCommand RunCommand(SshClient sshClient, string command) => 262 | sshClient.RunCommand(command); 263 | 264 | /// 265 | /// Prints the currently supplied monitor mode options. 266 | /// 267 | /// The verb options. 268 | private static void PrintMonitorOptions(MonitorVerbOptions verbOptions) 269 | { 270 | Terminal.WriteLine(); 271 | Terminal.WriteLine("Monitor mode starting"); 272 | Terminal.WriteLine("Monitor parameters follow: "); 273 | Terminal.WriteLine($" Monitor File {verbOptions.MonitorFile}", ConsoleColor.DarkYellow); 274 | Terminal.WriteLine($" Source Path {verbOptions.SourcePath}", ConsoleColor.DarkYellow); 275 | Terminal.WriteLine($" Excluded Files {string.Join("|", verbOptions.ExcludeFileSuffixes)}", ConsoleColor.DarkYellow); 276 | Terminal.WriteLine($" Target Address {verbOptions.Host}:{verbOptions.Port}", ConsoleColor.DarkYellow); 277 | Terminal.WriteLine($" Username {verbOptions.Username}", ConsoleColor.DarkYellow); 278 | Terminal.WriteLine($" Target Path {verbOptions.TargetPath}", ConsoleColor.DarkYellow); 279 | Terminal.WriteLine($" Clean Target {(verbOptions.CleanTarget ? "YES" : "NO")}", ConsoleColor.DarkYellow); 280 | Terminal.WriteLine($" Pre Deployment {verbOptions.PreCommand}", ConsoleColor.DarkYellow); 281 | Terminal.WriteLine($" Post Deployment {verbOptions.PostCommand}", ConsoleColor.DarkYellow); 282 | } 283 | 284 | /// 285 | /// Checks that both, SFTP and SSH clients have a working connection. If they don't it attempts to reconnect. 286 | /// 287 | /// The SSH client. 288 | /// The SFTP client. 289 | /// The verb options. 290 | private static void EnsureMonitorConnection( 291 | SshClient sshClient, 292 | SftpClient sftpClient, 293 | CliVerbOptionsBase verbOptions) 294 | { 295 | if (sshClient.IsConnected == false) 296 | { 297 | Terminal.WriteLine($"Connecting to host {verbOptions.Host}:{verbOptions.Port} via SSH."); 298 | sshClient.Connect(); 299 | } 300 | 301 | if (sftpClient.IsConnected == false) 302 | { 303 | Terminal.WriteLine($"Connecting to host {verbOptions.Host}:{verbOptions.Port} via SFTP."); 304 | sftpClient.Connect(); 305 | } 306 | } 307 | 308 | /// 309 | /// Creates the given directory structure on the target machine. 310 | /// 311 | /// The SFTP client. 312 | /// The verb options. 313 | private static void CreateTargetPath(SftpClient sftpClient, CliExecuteOptionsBase verbOptions) 314 | { 315 | if (sftpClient.Exists(verbOptions.TargetPath)) return; 316 | 317 | Terminal.WriteLine($" Target Path '{verbOptions.TargetPath}' does not exist. -- Will attempt to create.", ConsoleColor.Green); 318 | CreateLinuxDirectoryRecursive(sftpClient, verbOptions.TargetPath); 319 | Terminal.WriteLine($" Target Path '{verbOptions.TargetPath}' created successfully.", ConsoleColor.Green); 320 | } 321 | 322 | /// 323 | /// Prepares the given target path for deployment. If clean target is false, it does nothing. 324 | /// 325 | /// The SFTP client. 326 | /// The verb options. 327 | private static void PrepareTargetPath(SftpClient sftpClient, CliExecuteOptionsBase verbOptions) 328 | { 329 | if (!verbOptions.CleanTarget) return; 330 | Terminal.WriteLine($" Cleaning Target Path '{verbOptions.TargetPath}'", ConsoleColor.Green); 331 | DeleteLinuxDirectoryRecursive(sftpClient, verbOptions.TargetPath); 332 | } 333 | 334 | /// 335 | /// Uploads the files in the source Windows path to the target Linux path. 336 | /// 337 | /// The SFTP client. 338 | /// The source path. 339 | /// The target path. 340 | /// The exclude file suffixes. 341 | private static void UploadFilesToTarget( 342 | SftpClient sftpClient, 343 | string sourcePath, 344 | string targetPath, 345 | string[] excludeFileSuffixes) 346 | { 347 | var filesInSource = Directory.GetFiles( 348 | sourcePath, 349 | FileSystemMonitor.AllFilesPattern, 350 | SearchOption.AllDirectories); 351 | var filesToDeploy = filesInSource.Where(file => !excludeFileSuffixes.Any(file.EndsWith)) 352 | .ToList(); 353 | 354 | Terminal.WriteLine($" Deploying {filesToDeploy.Count} files.", ConsoleColor.Green); 355 | 356 | foreach (var file in filesToDeploy) 357 | { 358 | var relativePath = MakeRelativePath(file, sourcePath + Path.DirectorySeparatorChar); 359 | var fileTargetPath = Path.Combine(targetPath, relativePath) 360 | .Replace(WindowsDirectorySeparatorChar, LinuxDirectorySeparatorChar); 361 | var targetDirectory = Path.GetDirectoryName(fileTargetPath) 362 | .Replace(WindowsDirectorySeparatorChar, LinuxDirectorySeparatorChar); 363 | 364 | CreateLinuxDirectoryRecursive(sftpClient, targetDirectory); 365 | 366 | using var fileStream = File.OpenRead(file); 367 | sftpClient.UploadFile(fileStream, fileTargetPath); 368 | } 369 | } 370 | 371 | /// 372 | /// Makes the given path relative to an absolute path. 373 | /// 374 | /// The file path. 375 | /// The reference path. 376 | /// Relative path. 377 | private static string MakeRelativePath(string filePath, string referencePath) 378 | { 379 | var fileUri = new Uri(filePath); 380 | var referenceUri = new Uri(referencePath); 381 | return referenceUri.MakeRelativeUri(fileUri).ToString(); 382 | } 383 | 384 | private static void StopMonitorMode(SftpClient sftpClient, SshClient sshClient, FileSystemMonitor fsmonitor) 385 | { 386 | Terminal.WriteLine(); 387 | 388 | fsmonitor.Stop(); 389 | Terminal.WriteLine("File System monitor was stopped."); 390 | 391 | if (sftpClient.IsConnected) 392 | sftpClient.Disconnect(); 393 | 394 | Terminal.WriteLine("SFTP client disconnected."); 395 | 396 | if (sshClient.IsConnected) 397 | sshClient.Disconnect(); 398 | 399 | Terminal.WriteLine("SSH client disconnected."); 400 | Terminal.WriteLine("Application will exit now."); 401 | } 402 | 403 | private static void StopMonitorMode(SftpClient sftpClient, SshClient sshClient, FileSystemWatcher watcher) 404 | { 405 | Terminal.WriteLine(); 406 | 407 | watcher.EnableRaisingEvents = false; 408 | Terminal.WriteLine("File System monitor was stopped."); 409 | 410 | if (sftpClient.IsConnected) 411 | sftpClient.Disconnect(); 412 | 413 | Terminal.WriteLine("SFTP client disconnected."); 414 | 415 | if (sshClient.IsConnected) 416 | sshClient.Disconnect(); 417 | 418 | Terminal.WriteLine("SSH client disconnected."); 419 | Terminal.WriteLine("Application will exit now."); 420 | } 421 | 422 | /// 423 | /// Prints the given exception using the Console Manager. 424 | /// 425 | /// The ex. 426 | private static void PrintException(Exception ex) 427 | { 428 | "Deployment failed.".Error(); 429 | $" Error - {ex.GetType().Name}".Error(); 430 | $" {ex.Message}".Error(); 431 | $" {ex.StackTrace}".Error(); 432 | } 433 | 434 | /// 435 | /// Prints the deployment number the Monitor is currently in. 436 | /// 437 | /// The deployment number. 438 | private static void PrintDeploymentNumber(int deploymentNumber) 439 | { 440 | Terminal.WriteLine($" Starting deployment ID {deploymentNumber} - {DateTime.Now.ToLongDateString()} {DateTime.Now.ToLongTimeString()}", ConsoleColor.Green); 441 | } 442 | 443 | private static ShellStream CreateShellStream(SshClient sshClient) 444 | { 445 | var shell = CreateBaseShellStream(sshClient); 446 | 447 | shell.DataReceived += OnShellDataRx; 448 | 449 | shell.ErrorOccurred += (s, e) => PrintException(e.Exception); 450 | 451 | return shell; 452 | } 453 | 454 | private static void OnShellDataRx(object sender, ShellDataEventArgs e) 455 | { 456 | var escapeSequenceBytes = new List(128); 457 | var isInEscapeSequence = false; 458 | byte rxbyteprevious = 0; 459 | byte escapeSequenceType = 0; 460 | var rxbuffer = e.Data; 461 | 462 | foreach (var rxByte in rxbuffer) 463 | { 464 | // We've found the beginning of an escapr sequence 465 | if (isInEscapeSequence == false && rxByte == Escape) 466 | { 467 | isInEscapeSequence = true; 468 | escapeSequenceBytes.Clear(); 469 | rxbyteprevious = rxByte; 470 | continue; 471 | } 472 | 473 | // Print out the character if we are not in an escape sequence and it is a printable character 474 | if (isInEscapeSequence == false) 475 | { 476 | if (rxByte >= 32 || (rxByte >= 8 && rxByte <= 13)) 477 | { 478 | if (_forwardShellStreamOutput) 479 | Console.Write((char)rxByte); 480 | } 481 | else if (rxByte == 7) 482 | { 483 | if (_forwardShellStreamOutput) 484 | Console.Beep(); 485 | } 486 | else 487 | { 488 | if (_forwardShellStreamOutput) 489 | Terminal.WriteLine($"[NPC {rxByte}]", ConsoleColor.DarkYellow); 490 | } 491 | 492 | rxbyteprevious = rxByte; 493 | continue; 494 | } 495 | 496 | // If we are already inside an escape sequence . . . 497 | // Add the byte to the escape sequence 498 | escapeSequenceBytes.Add(rxByte); 499 | 500 | // Ignore the second escape byte 91 '[' or ']' 501 | if (rxbyteprevious == Escape) 502 | { 503 | rxbyteprevious = rxByte; 504 | if (ControlSequenceInitiators.Contains(rxByte)) 505 | { 506 | escapeSequenceType = rxByte; 507 | continue; 508 | } 509 | 510 | escapeSequenceType = 0; 511 | } 512 | 513 | // Detect if it's the last byte of the escape sequence (64 to 126) 514 | // This last character determines the command to execute 515 | var endOfSequenceType91 = escapeSequenceType == (byte)'[' && (rxByte >= 64 && rxByte <= 126); 516 | var endOfSequenceType93 = escapeSequenceType == (byte)']' && (rxByte == 7); 517 | if (endOfSequenceType91 || endOfSequenceType93) 518 | { 519 | try 520 | { 521 | // Execute the command of the given escape sequence 522 | HandleShellEscapeSequence(escapeSequenceBytes.ToArray()); 523 | } 524 | finally 525 | { 526 | isInEscapeSequence = false; 527 | escapeSequenceBytes.Clear(); 528 | rxbyteprevious = rxByte; 529 | } 530 | 531 | continue; 532 | } 533 | 534 | rxbyteprevious = rxByte; 535 | } 536 | } 537 | 538 | /// 539 | /// Creates a new deployment cycle. 540 | /// 541 | /// The SSH client. 542 | /// The SFTP client. 543 | /// The shell stream. 544 | /// The verb options. 545 | private static void CreateNewDeployment( 546 | SshClient sshClient, 547 | SftpClient sftpClient, 548 | ShellStream shellStream, 549 | MonitorVerbOptions verbOptions) 550 | { 551 | // At this point the change has been detected; Make sure we are not deploying 552 | Terminal.WriteLine(); 553 | 554 | if (_isDeploying) 555 | { 556 | Terminal.WriteLine("WARNING: Deployment already in progress. Deployment will not occur.", ConsoleColor.DarkYellow); 557 | return; 558 | } 559 | 560 | // Lock Deployment 561 | _isDeploying = true; 562 | var stopwatch = new Stopwatch(); 563 | stopwatch.Start(); 564 | 565 | try 566 | { 567 | _forwardShellStreamOutput = false; 568 | PrintDeploymentNumber(_deploymentNumber); 569 | RunSshClientCommand(sshClient, verbOptions); 570 | CreateTargetPath(sftpClient, verbOptions); 571 | PrepareTargetPath(sftpClient, verbOptions); 572 | UploadFilesToTarget( 573 | sftpClient, 574 | verbOptions.SourcePath, 575 | verbOptions.TargetPath, 576 | verbOptions.ExcludeFileSuffixes); 577 | } 578 | catch (Exception ex) 579 | { 580 | PrintException(ex); 581 | } 582 | finally 583 | { 584 | // Unlock deployment 585 | _isDeploying = false; 586 | _deploymentNumber++; 587 | stopwatch.Stop(); 588 | Terminal.WriteLine($" Finished deployment in {Math.Round(stopwatch.Elapsed.TotalSeconds, 2)} seconds.", ConsoleColor.Green); 589 | 590 | _forwardShellStreamOutput = true; 591 | RunShellStreamCommand(shellStream, verbOptions); 592 | } 593 | } 594 | 595 | /// 596 | /// Normalizes the monitor verb options. 597 | /// 598 | /// The verb options. 599 | private static void NormalizeMonitorVerbOptions(MonitorVerbOptions verbOptions) 600 | { 601 | var sourcePath = verbOptions.SourcePath.Trim(); 602 | var targetPath = verbOptions.TargetPath.Trim(); 603 | var monitorFile = Path.IsPathRooted(verbOptions.MonitorFile) 604 | ? Path.GetFullPath(verbOptions.MonitorFile) 605 | : Path.Combine(sourcePath, verbOptions.MonitorFile); 606 | 607 | verbOptions.TargetPath = targetPath; 608 | verbOptions.MonitorFile = monitorFile; 609 | verbOptions.SourcePath = sourcePath; 610 | } 611 | 612 | /// 613 | /// Starts the monitor mode. 614 | /// 615 | /// The fs monitor. 616 | /// The SSH client. 617 | /// The SFTP client. 618 | /// The shell stream. 619 | /// The verb options. 620 | private static void StartMonitorMode( 621 | FileSystemMonitor fsmonitor, 622 | SshClient sshClient, 623 | SftpClient sftpClient, 624 | ShellStream shellStream, 625 | MonitorVerbOptions verbOptions) 626 | { 627 | fsmonitor.FileSystemEntryChanged += (s, e) => 628 | { 629 | // Detect changes to the monitor file by ignoring deletions and checking file paths. 630 | if (e.ChangeType != FileSystemEntryChangeType.FileAdded && 631 | e.ChangeType != FileSystemEntryChangeType.FileModified) 632 | return; 633 | 634 | // If the change was not in the monitor file, then ignore it 635 | if (e.Path.ToLowerInvariant().Equals(verbOptions.MonitorFile.ToLowerInvariant()) == false) 636 | return; 637 | 638 | // Create a new deployment once 639 | CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); 640 | }; 641 | 642 | Terminal.WriteLine("File System Monitor is now running."); 643 | Terminal.WriteLine("Writing a new monitor file will trigger a new deployment."); 644 | Terminal.WriteLine("Press H for help!"); 645 | Terminal.WriteLine("Ground Control to Major Tom: Have a nice trip in space!.", ConsoleColor.DarkCyan); 646 | } 647 | 648 | /// 649 | /// Starts the user interaction. 650 | /// 651 | /// The SSH client. 652 | /// The SFTP client. 653 | /// The shell stream. 654 | /// The verb options. 655 | private static void StartUserInteraction( 656 | SshClient sshClient, 657 | SftpClient sftpClient, 658 | ShellStream shellStream, 659 | MonitorVerbOptions verbOptions) 660 | { 661 | _forwardShellStreamInput = false; 662 | 663 | while (true) 664 | { 665 | var readKey = Console.ReadKey(true); 666 | 667 | if (readKey.Key == ConsoleKey.F1) 668 | { 669 | _forwardShellStreamInput = !_forwardShellStreamInput; 670 | if (_forwardShellStreamInput) 671 | { 672 | Program.Title = "Monitor (Interactive)"; 673 | Terminal.WriteLine(" >> Entered console input forwarding.", ConsoleColor.Green); 674 | _forwardShellStreamOutput = true; 675 | } 676 | else 677 | { 678 | Program.Title = "Monitor (Press H for Help)"; 679 | Terminal.WriteLine(" >> Left console input forwarding.", ConsoleColor.Red); 680 | } 681 | 682 | continue; 683 | } 684 | 685 | if (_forwardShellStreamInput) 686 | { 687 | if (readKey.Key == ConsoleKey.Enter) 688 | { 689 | shellStream.Write("\r\n"); 690 | } 691 | else 692 | { 693 | shellStream.WriteByte((byte)readKey.KeyChar); 694 | } 695 | 696 | shellStream.Flush(); 697 | continue; 698 | } 699 | 700 | switch (readKey.Key) 701 | { 702 | case ConsoleKey.Q: 703 | return; 704 | case ConsoleKey.C: 705 | Console.Clear(); 706 | break; 707 | case ConsoleKey.N: 708 | CreateNewDeployment(sshClient, sftpClient, shellStream, verbOptions); 709 | break; 710 | case ConsoleKey.E: 711 | RunSshClientCommand(sshClient, verbOptions); 712 | break; 713 | case ConsoleKey.S: 714 | RunShellStreamCommand(shellStream, verbOptions); 715 | break; 716 | case ConsoleKey.H: 717 | 718 | const ConsoleColor helpColor = ConsoleColor.Cyan; 719 | Terminal.WriteLine("Console help", helpColor); 720 | Terminal.WriteLine(" H Prints this screen", helpColor); 721 | Terminal.WriteLine(" Q Quits this application", helpColor); 722 | Terminal.WriteLine(" C Clears the screen", helpColor); 723 | Terminal.WriteLine(" N Force a deployment cycle", helpColor); 724 | Terminal.WriteLine(" E Run the Pre-deployment command", helpColor); 725 | Terminal.WriteLine(" S Run the Post-deployment command", helpColor); 726 | Terminal.WriteLine(" F1 Toggle shell-interactive mode", helpColor); 727 | 728 | Terminal.WriteLine(); 729 | break; 730 | default: 731 | Terminal.WriteLine($"Unrecognized command '{readKey.KeyChar}' -- Press 'H' to get a list of available commands.", ConsoleColor.Red); 732 | break; 733 | } 734 | } 735 | } 736 | 737 | #endregion 738 | } 739 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/DeploymentManager.Push.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using Options; 4 | using Renci.SshNet; 5 | using Swan; 6 | using System; 7 | using System.Diagnostics; 8 | using System.IO; 9 | 10 | public partial class DeploymentManager 11 | { 12 | internal static void ExecutePushVerb(PushVerbOptions verbOptions) 13 | { 14 | NormalizePushVerbOptions(verbOptions); 15 | PrintPushOptions(verbOptions); 16 | 17 | var psi = new ProcessStartInfo 18 | { 19 | FileName = "dotnet", 20 | Arguments = " msbuild -restore /t:Publish " + 21 | $" /p:Configuration={verbOptions.Configuration};BuildingInsideSshDeploy=true;" + 22 | $"TargetFramework={verbOptions.Framework};RuntimeIdentifier={verbOptions.Runtime};" + 23 | "PreBuildEvent=\"\";PostBuildEvent=\"\"", 24 | }; 25 | 26 | var process = Process.Start(psi); 27 | process.WaitForExit(); 28 | 29 | if (process.ExitCode != 0) 30 | { 31 | Console.Error.WriteLine("Invoking MSBuild target failed"); 32 | Environment.ExitCode = 0; 33 | return; 34 | } 35 | 36 | if (Directory.Exists(verbOptions.SourcePath) == false) 37 | throw new DirectoryNotFoundException($"Source Path \'{verbOptions.SourcePath}\' was not found."); 38 | 39 | // Create connection info 40 | var simpleConnectionInfo = new PasswordConnectionInfo(verbOptions.Host, verbOptions.Port, verbOptions.Username, verbOptions.Password); 41 | 42 | // Instantiate an SFTP client and an SSH client 43 | // SFTP will be used to transfer the files and SSH to execute pre-deployment and post-deployment commands 44 | using var sftpClient = new SftpClient(simpleConnectionInfo); 45 | 46 | // SSH will be used to execute commands and to get the output back from the program we are running 47 | using var sshClient = new SshClient(simpleConnectionInfo); 48 | 49 | // Connect SSH and SFTP clients 50 | EnsureMonitorConnection(sshClient, sftpClient, verbOptions); 51 | CreateNewDeployment(sshClient, sftpClient, verbOptions); 52 | } 53 | 54 | private static void NormalizePushVerbOptions(PushVerbOptions verbOptions) 55 | { 56 | verbOptions.TargetPath = verbOptions.TargetPath.Trim(); 57 | } 58 | 59 | private static void PrintPushOptions(PushVerbOptions verbOptions) 60 | { 61 | Terminal.WriteLine(); 62 | Terminal.WriteLine("Deploying...."); 63 | Terminal.WriteLine($" Configuration {verbOptions.Configuration}", ConsoleColor.DarkYellow); 64 | Terminal.WriteLine($" Framework {verbOptions.Framework}", ConsoleColor.DarkYellow); 65 | Terminal.WriteLine($" Source Path {verbOptions.SourcePath}", ConsoleColor.DarkYellow); 66 | Terminal.WriteLine($" Excluded Files {string.Join("|", verbOptions.ExcludeFileSuffixes)}", ConsoleColor.DarkYellow); 67 | Terminal.WriteLine($" Target Address {verbOptions.Host}:{verbOptions.Port}", ConsoleColor.DarkYellow); 68 | Terminal.WriteLine($" Username {verbOptions.Username}", ConsoleColor.DarkYellow); 69 | Terminal.WriteLine($" Target Path {verbOptions.TargetPath}", ConsoleColor.DarkYellow); 70 | Terminal.WriteLine($" Clean Target {(verbOptions.CleanTarget ? "YES" : "NO")}", ConsoleColor.DarkYellow); 71 | Terminal.WriteLine($" Pre Deployment {verbOptions.PreCommand}", ConsoleColor.DarkYellow); 72 | Terminal.WriteLine($" Post Deployment {verbOptions.PostCommand}", ConsoleColor.DarkYellow); 73 | } 74 | 75 | private static void CreateNewDeployment( 76 | SshClient sshClient, 77 | SftpClient sftpClient, 78 | PushVerbOptions verbOptions) 79 | { 80 | // At this point the change has been detected; Make sure we are not deploying 81 | Terminal.WriteLine(); 82 | 83 | // Lock Deployment 84 | _isDeploying = true; 85 | var stopwatch = new Stopwatch(); 86 | stopwatch.Start(); 87 | 88 | try 89 | { 90 | _forwardShellStreamOutput = false; 91 | RunCommand(sshClient, "client", verbOptions.PreCommand); 92 | CreateTargetPath(sftpClient, verbOptions); 93 | PrepareTargetPath(sftpClient, verbOptions); 94 | UploadFilesToTarget(sftpClient, verbOptions.SourcePath, verbOptions.TargetPath,verbOptions.ExcludeFileSuffixes); 95 | AllowExecute(sshClient, verbOptions); 96 | } 97 | catch (Exception ex) 98 | { 99 | PrintException(ex); 100 | } 101 | finally 102 | { 103 | // Unlock deployment 104 | _isDeploying = false; 105 | _deploymentNumber++; 106 | stopwatch.Stop(); 107 | Terminal.WriteLine($" Finished deployment in {Math.Round(stopwatch.Elapsed.TotalSeconds, 2)} seconds.", ConsoleColor.Green); 108 | RunCommand(sshClient, "shell", verbOptions.PostCommand); 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/DeploymentManager.Shell.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using Options; 4 | using Swan.Logging; 5 | using System; 6 | using System.Text; 7 | 8 | public partial class DeploymentManager 9 | { 10 | public static void ExecuteShellVerb(ShellVerbOptions invokedVerbOptions) 11 | { 12 | using var sshClient = CreateClient(invokedVerbOptions); 13 | sshClient.Connect(); 14 | 15 | var encoding = Encoding.ASCII; 16 | 17 | using var shell = CreateBaseShellStream(sshClient); 18 | 19 | shell.DataReceived += OnShellDataRx; 20 | shell.ErrorOccurred += (s, e) => e.Exception.Message.Debug(); 21 | 22 | _forwardShellStreamOutput = true; 23 | 24 | while (true) 25 | { 26 | var line = Console.ReadLine(); 27 | var lineData = encoding.GetBytes(line + "\r\n"); 28 | shell.Write(lineData, 0, lineData.Length); 29 | shell.Flush(); 30 | 31 | if (!line.Equals("exit")) continue; 32 | 33 | var expectResult = shell.Expect("logout", TimeSpan.FromSeconds(2)); 34 | if (string.IsNullOrWhiteSpace(expectResult) == false && expectResult.Trim().EndsWith("logout")) 35 | { 36 | break; 37 | } 38 | } 39 | 40 | sshClient.Disconnect(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/DeploymentManager.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using Options; 4 | using Renci.SshNet; 5 | using Renci.SshNet.Common; 6 | using Swan; 7 | using Swan.Logging; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Text; 12 | 13 | public static partial class DeploymentManager 14 | { 15 | private const string TerminalName = "xterm"; // "vanilla" works well; "xterm" is also a good option 16 | private const string LinuxCurrentDirectory = "."; 17 | private const string LinuxParentDirectory = ".."; 18 | private const char LinuxDirectorySeparatorChar = '/'; 19 | private const char WindowsDirectorySeparatorChar = '\\'; 20 | private const string LinuxDirectorySeparator = "/"; 21 | private const byte Escape = 27; // Escape sequence character 22 | private static readonly byte[] ControlSequenceInitiators = { (byte) '[', (byte) ']' }; 23 | 24 | public static void ExecuteRunVerb(RunVerbOptions invokedVerbOptions) 25 | { 26 | using var client = CreateClient(invokedVerbOptions); 27 | client.Connect(); 28 | var command = ExecuteCommand(client, invokedVerbOptions.Command); 29 | Environment.ExitCode = command.ExitStatus; 30 | client.Disconnect(); 31 | } 32 | 33 | private static ShellStream CreateBaseShellStream(SshClient sshClient) 34 | { 35 | var bufferSize = Console.BufferWidth * Console.BufferHeight; 36 | 37 | return sshClient.CreateShellStream( 38 | TerminalName, 39 | (uint) Console.BufferWidth, 40 | (uint) Console.BufferHeight, 41 | (uint) Console.WindowWidth, 42 | (uint) Console.WindowHeight, 43 | bufferSize, 44 | new Dictionary {{TerminalModes.ECHO, 0}, {TerminalModes.IGNCR, 1}}); 45 | } 46 | 47 | private static SshClient CreateClient(CliVerbOptionsBase options) 48 | { 49 | var simpleConnectionInfo = 50 | new PasswordConnectionInfo(options.Host, options.Port, options.Username, options.Password); 51 | return new SshClient(simpleConnectionInfo); 52 | } 53 | 54 | private static SshCommand ExecuteCommand(SshClient client, string commandText) 55 | { 56 | Terminal.WriteLine("SSH TX:"); 57 | Terminal.WriteLine(commandText, ConsoleColor.Green); 58 | 59 | using var command = client.CreateCommand(commandText); 60 | var result = command.Execute(); 61 | Terminal.WriteLine("SSH RX:"); 62 | 63 | if (command.ExitStatus != 0) 64 | { 65 | Terminal.WriteLine($"Error {command.ExitStatus}"); 66 | Terminal.WriteLine(command.Error); 67 | } 68 | 69 | if (!string.IsNullOrWhiteSpace(result)) 70 | Terminal.WriteLine(result, ConsoleColor.Yellow); 71 | 72 | return command; 73 | } 74 | 75 | private static void HandleShellEscapeSequence(byte[] escapeSequence) 76 | { 77 | var controlSequenceChars = ControlSequenceInitiators.Select(s => (char) s).ToArray(); 78 | var escapeString = Encoding.ASCII.GetString(escapeSequence); 79 | var command = escapeString.Last(); 80 | var arguments = escapeString 81 | .TrimStart(controlSequenceChars) 82 | .TrimEnd(command) 83 | .Split(new[] {';'}, StringSplitOptions.RemoveEmptyEntries); 84 | 85 | if (command == 'm' | command == '\a') 86 | { 87 | var background = "40"; 88 | var foreground = "37"; 89 | 90 | if (arguments.Length == 2) 91 | { 92 | foreground = arguments[1]; 93 | } 94 | 95 | if (arguments.Length == 3) 96 | { 97 | foreground = arguments[1]; 98 | background = arguments[0]; 99 | } 100 | 101 | Console.ForegroundColor = foreground switch 102 | { 103 | "30" => ConsoleColor.Black, 104 | "31" => ConsoleColor.Red, 105 | "32" => ConsoleColor.Green, 106 | "33" => ConsoleColor.Yellow, 107 | "34" => ConsoleColor.Cyan, 108 | "35" => ConsoleColor.Magenta, 109 | "36" => ConsoleColor.Cyan, 110 | "37" => ConsoleColor.Gray, 111 | _ => Console.ForegroundColor 112 | }; 113 | 114 | Console.BackgroundColor = background switch 115 | { 116 | "40" => ConsoleColor.Black, 117 | "41" => ConsoleColor.Red, 118 | "42" => ConsoleColor.Green, 119 | "43" => ConsoleColor.Yellow, 120 | "44" => ConsoleColor.DarkBlue, 121 | "45" => ConsoleColor.Magenta, 122 | "46" => ConsoleColor.Cyan, 123 | "47" => ConsoleColor.Gray, 124 | _ => Console.BackgroundColor 125 | }; 126 | } 127 | else 128 | { 129 | $"Unhandled escape sequence.\r\n Text: {escapeString}\r\n Bytes: {string.Join(" ", escapeSequence.Select(s => s.ToString()).ToArray())}" 130 | .Debug(); 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/FileSystemEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using System; 4 | using System.IO; 5 | 6 | /// 7 | /// Represents a trackable file system entry. 8 | /// 9 | public class FileSystemEntry 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The path. 15 | public FileSystemEntry(string path) 16 | { 17 | var info = new FileInfo(path); 18 | Path = info.DirectoryName; 19 | Filename = info.Name; 20 | Size = info.Length; 21 | DateCreatedUtc = info.CreationTimeUtc; 22 | DateModifiedUtc = info.LastWriteTimeUtc; 23 | } 24 | 25 | public string Filename { get; set; } 26 | public string Path { get; set; } 27 | public long Size { get; set; } 28 | public DateTime DateCreatedUtc { get; set; } 29 | public DateTime DateModifiedUtc { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/FileSystemEntryChangeType.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | public enum FileSystemEntryChangeType 4 | { 5 | FileAdded, 6 | FileRemoved, 7 | FileModified, 8 | } 9 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/FileSystemEntryChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using System; 4 | 5 | internal class FileSystemEntryChangedEventArgs : EventArgs 6 | { 7 | public FileSystemEntryChangedEventArgs(FileSystemEntryChangeType changeType, string path) 8 | { 9 | ChangeType = changeType; 10 | Path = path; 11 | } 12 | 13 | public FileSystemEntryChangeType ChangeType { get; } 14 | public string Path { get; } 15 | 16 | public override string ToString() => $"{ChangeType}: {Path}"; 17 | } 18 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/FileSystemEntryDictionary.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | /// 7 | /// A dictionary of file system entries. Keys are string paths, values are file system entries. 8 | /// 9 | public class FileSystemEntryDictionary : Dictionary 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public FileSystemEntryDictionary() 15 | : base(1024, StringComparer.InvariantCultureIgnoreCase) 16 | { 17 | // placeholder 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/FileSystemMonitor.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using System; 4 | using System.ComponentModel; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading; 8 | 9 | /// 10 | /// Represents a long-running file system monitor based on polling 11 | /// FileSystemWatcher does not handle some scenarios well enough and this is why 12 | /// a custom monitor was implemented. This class is not meant for monitoring a large 13 | /// amount of file or directories. In other words, do not monitor the root of a drive, 14 | /// or a folder with thousands of files. 15 | /// 16 | internal class FileSystemMonitor 17 | { 18 | public const string AllFilesPattern = "*.*"; 19 | private readonly FileSystemEntryDictionary _entries = new FileSystemEntryDictionary(); 20 | private readonly BackgroundWorker _worker; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The poll interval seconds. 26 | /// The file system path. 27 | public FileSystemMonitor(int pollIntervalSeconds, string fileSystemPath) 28 | { 29 | PollIntervalSeconds = pollIntervalSeconds; 30 | FileSystemPath = fileSystemPath; 31 | _worker = new BackgroundWorker 32 | { 33 | WorkerReportsProgress = true, 34 | WorkerSupportsCancellation = true, 35 | }; 36 | 37 | _worker.DoWork += DoWork; 38 | } 39 | 40 | public delegate void FileSystemEntryChangedHandler(object sender, FileSystemEntryChangedEventArgs e); 41 | 42 | public event FileSystemEntryChangedHandler FileSystemEntryChanged; 43 | 44 | /// 45 | /// The polling interval in seconds at which the file system is monitored for changes. 46 | /// 47 | public int PollIntervalSeconds { get; } 48 | 49 | /// 50 | /// The root path that is monitored for changes. 51 | /// 52 | public string FileSystemPath { get; private set; } 53 | 54 | /// 55 | /// Stops the File System Monitor 56 | /// This is a blocking call. 57 | /// 58 | public void Stop() 59 | { 60 | if (_worker.CancellationPending) 61 | return; 62 | 63 | _worker.CancelAsync(); 64 | ClearMonitorEntries(); 65 | } 66 | 67 | /// 68 | /// Starts this instance. 69 | /// 70 | /// Service is already running. 71 | public virtual void Start() 72 | { 73 | if (_worker.IsBusy) 74 | throw new InvalidOperationException("Service is already running."); 75 | 76 | _worker.RunWorkerAsync(); 77 | } 78 | 79 | /// 80 | /// Does the work when the Start method is called. 81 | /// 82 | /// The sender. 83 | /// The instance containing the event data. 84 | protected void DoWork(object sender, DoWorkEventArgs e) 85 | { 86 | const int minimumInterval = 1; 87 | const int maximumInterval = 60; 88 | 89 | // validate arguments 90 | if (PollIntervalSeconds < minimumInterval || PollIntervalSeconds > maximumInterval) 91 | throw new ArgumentException("PollIntervalSeconds must be between 2 and 60"); 92 | 93 | if (Directory.Exists(FileSystemPath) == false) 94 | throw new ArgumentException("Configuration item InputFolderPath does not point to a valid folder"); 95 | 96 | // normalize file system path parameter 97 | FileSystemPath = Path.GetFullPath(FileSystemPath); 98 | 99 | // Only new files shall be taken into account. 100 | InitializeMonitorEntries(); 101 | 102 | // keep track of a timeout interval 103 | var lastPollTime = DateTime.Now; 104 | while (!_worker.CancellationPending) 105 | { 106 | try 107 | { 108 | // check for polling interval before processing changes 109 | if (DateTime.Now.Subtract(lastPollTime).TotalSeconds > PollIntervalSeconds) 110 | { 111 | lastPollTime = DateTime.Now; 112 | ProcessMonitorEntryChanges(); 113 | _worker.ReportProgress(1, DateTime.Now); 114 | } 115 | } 116 | catch (Exception ex) 117 | { 118 | // Report the exception 119 | _worker.ReportProgress(0, ex); 120 | } 121 | finally 122 | { 123 | // sleep some so we don't overload the CPU 124 | Thread.Sleep(10); 125 | } 126 | } 127 | } 128 | 129 | /// 130 | /// Raises the file system entry changed event. 131 | /// 132 | /// Type of the change. 133 | /// The path. 134 | private void RaiseFileSystemEntryChangedEvent(FileSystemEntryChangeType changeType, string path) 135 | { 136 | FileSystemEntryChanged?.Invoke(this, new FileSystemEntryChangedEventArgs(changeType, path)); 137 | } 138 | 139 | /// 140 | /// We don't want to fire events for files that were there before we started the worker. 141 | /// 142 | private void InitializeMonitorEntries() 143 | { 144 | ClearMonitorEntries(); 145 | var files = Directory.GetFiles(FileSystemPath, AllFilesPattern, SearchOption.AllDirectories); 146 | 147 | foreach (var file in files) 148 | { 149 | try 150 | { 151 | var entry = new FileSystemEntry(file); 152 | _entries[file] = entry; 153 | } 154 | catch 155 | { 156 | // swallow; no access 157 | } 158 | } 159 | } 160 | 161 | /// 162 | /// Compares what we are tracking under Entries and what the file system reports 163 | /// Based on such comparisons it raises the necessary events. 164 | /// 165 | private void ProcessMonitorEntryChanges() 166 | { 167 | var files = Directory.GetFiles(FileSystemPath, AllFilesPattern, SearchOption.AllDirectories); 168 | 169 | // check for any missing files 170 | var existingKeys = _entries.Keys.ToArray(); 171 | foreach (var existingKey in existingKeys) 172 | { 173 | if (files.Any(f => f.Equals(existingKey, StringComparison.InvariantCultureIgnoreCase)) == false) 174 | { 175 | _entries.Remove(existingKey); 176 | RaiseFileSystemEntryChangedEvent(FileSystemEntryChangeType.FileRemoved, existingKey); 177 | } 178 | } 179 | 180 | // now, compare each entry and add the new ones (if any) 181 | foreach (var file in files) 182 | { 183 | try 184 | { 185 | var entry = new FileSystemEntry(file); 186 | 187 | if (_entries.ContainsKey(file)) 188 | { 189 | // in the case we already have it in the tracking collection 190 | var existingEntry = _entries[file]; 191 | 192 | if (existingEntry.DateCreatedUtc != entry.DateCreatedUtc || 193 | existingEntry.DateModifiedUtc != entry.DateModifiedUtc || 194 | existingEntry.Size != entry.Size) 195 | { 196 | // update the entry and raise the change event 197 | _entries[file] = entry; 198 | RaiseFileSystemEntryChangedEvent(FileSystemEntryChangeType.FileModified, file); 199 | } 200 | } 201 | else 202 | { 203 | // add the entry and raise the added event 204 | _entries[file] = entry; 205 | RaiseFileSystemEntryChangedEvent(FileSystemEntryChangeType.FileAdded, file); 206 | } 207 | } 208 | catch 209 | { 210 | // swallow 211 | } 212 | } 213 | } 214 | 215 | /// 216 | /// Clears all the dictionary entries. 217 | /// This method is used when we startup or reset the file system monitor. 218 | /// 219 | private void ClearMonitorEntries() => _entries.Clear(); 220 | } 221 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/CliExecuteOptionsBase.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | using Swan.Parsers; 4 | 5 | public class CliExecuteOptionsBase : CliVerbOptionsBase 6 | { 7 | [ArgumentOption('t', "target", HelpText = "The target path of the files to transfer", Required = true)] 8 | public string TargetPath { get; set; } 9 | 10 | [ArgumentOption("pre", HelpText = "Command to execute prior file transfer to target", Required = false)] 11 | public string? PreCommand { get; set; } 12 | 13 | [ArgumentOption("post", HelpText = "Command to execute after file transfer to target", Required = false)] 14 | public string? PostCommand { get; set; } 15 | 16 | [ArgumentOption("clean", DefaultValue = false, HelpText = "Deletes all files and folders on the target before pushing the new files.", Required = false)] 17 | public bool CleanTarget { get; set; } 18 | 19 | [ArgumentOption("exclude", Separator = '|', DefaultValue = ".ready|.vshost.exe|.vshost.exe.config", HelpText = "a pipe (|) separated list of file suffixes to ignore while deploying.", Required = false)] 20 | public string[]? ExcludeFileSuffixes { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/CliOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | using Swan.Parsers; 4 | 5 | public class CliOptions 6 | { 7 | [VerbOption("push", HelpText = "Transfers the files and folders from a source path in the local machine to a target path in the remote machine")] 8 | public PushVerbOptions PushVerbOptions { get; set; } 9 | 10 | [VerbOption("monitor", HelpText = "Monitors a folder for a deployment and automatically transfers the files over the target.")] 11 | public MonitorVerbOptions MonitorVerbOptions { get; set; } 12 | 13 | [VerbOption("run", HelpText = "Runs the specified command on the target machine")] 14 | public RunVerbOptions RunVerbOptions { get; set; } 15 | 16 | [VerbOption("shell", HelpText = "Opens an interactive mode shell")] 17 | public ShellVerbOptions ShellVerbOptions { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/CliVerbOptionsBase.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | using Swan.Parsers; 4 | 5 | public abstract class CliVerbOptionsBase 6 | { 7 | [ArgumentOption( 8 | 'v', 9 | "verbose", 10 | DefaultValue = true, 11 | HelpText = "Add this option to print messages to standard error and output streams.", 12 | Required = false)] 13 | public bool Verbose { get; set; } 14 | 15 | [ArgumentOption( 16 | 'h', 17 | "host", 18 | HelpText = "Hostname or IP Address of the target. -- Must be running an SSH server.", 19 | Required = true)] 20 | public string Host { get; set; } 21 | 22 | [ArgumentOption('p', "port", DefaultValue = 22, HelpText = "Port on which SSH is running.")] 23 | public int Port { get; set; } 24 | 25 | [ArgumentOption( 26 | 'u', 27 | "username", 28 | DefaultValue = "pi", 29 | HelpText = "The username under which the connection will be established.")] 30 | public string Username { get; set; } 31 | 32 | [ArgumentOption( 33 | 'w', 34 | "password", 35 | DefaultValue = "raspberry", 36 | HelpText = "The password for the given username.", 37 | Required = false)] 38 | public string? Password { get; set; } 39 | } 40 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/MonitorVerbOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | using Swan.Parsers; 4 | 5 | public class MonitorVerbOptions : CliExecuteOptionsBase 6 | { 7 | [ArgumentOption('s', "source", HelpText = "The source path for the files to transfer", Required = true)] 8 | public string SourcePath { get; set; } 9 | 10 | [ArgumentOption('m', "monitor", DefaultValue = "sshdeploy.ready", HelpText = "The command to run on the target machine", Required = false)] 11 | public string? MonitorFile { get; set; } 12 | 13 | [ArgumentOption('l', "legacy", DefaultValue = false, HelpText = "Monitor files using legacy method", Required = false)] 14 | public bool Legacy { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/PushVerbOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | using System.IO; 4 | using Swan.Parsers; 5 | 6 | public class PushVerbOptions : CliExecuteOptionsBase 7 | { 8 | private const string BinFolder = "bin"; 9 | private const string PublishFolder = "publish"; 10 | 11 | public static bool IgnoreTargetFrameworkToOutputPath { get; set; } 12 | 13 | [ArgumentOption('c', "configuration", DefaultValue = "Debug", HelpText = "Target configuration. The default for most projects is 'Debug'.", Required = false)] 14 | public string? Configuration { get; set; } 15 | 16 | [ArgumentOption('f', "framework", HelpText = "The target framework has to be specified in the project file.", Required = true)] 17 | public string Framework { get; set; } 18 | 19 | [ArgumentOption('r', "runtime", HelpText = "The given runtime used for creating a self-contained deployment.", DefaultValue = "",Required = false)] 20 | public string? Runtime { get; set; } 21 | 22 | [ArgumentOption('x', "execute", HelpText = "Adds user execute mode permission to files transferred.", DefaultValue = "", Required = false)] 23 | public string? Execute { get; set; } 24 | 25 | public string SourcePath => IgnoreTargetFrameworkToOutputPath ? 26 | Path.Combine(Program.CurrentDirectory, BinFolder, Configuration, Runtime, PublishFolder) : 27 | Path.Combine(Program.CurrentDirectory, BinFolder, Configuration, Framework, Runtime, PublishFolder); 28 | } 29 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/RunVerbOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | using Swan.Parsers; 4 | 5 | public class RunVerbOptions : CliVerbOptionsBase 6 | { 7 | [ArgumentOption('c', "command", HelpText = "The command to run on the target machine", Required = true)] 8 | public string Command { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Options/ShellVerbOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Options 2 | { 3 | public class ShellVerbOptions : CliVerbOptionsBase 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy 2 | { 3 | using Options; 4 | using Swan; 5 | using Swan.Logging; 6 | using Swan.Parsers; 7 | using System; 8 | using System.IO; 9 | using System.Linq; 10 | using Utils; 11 | 12 | public static class Program 13 | { 14 | public static string Title 15 | { 16 | get => Console.Title; 17 | set => Console.Title = value + TitleSuffix; 18 | } 19 | 20 | public static string CurrentDirectory { get; } = Directory.GetCurrentDirectory(); 21 | 22 | public static string TitleSuffix { get; set; } = " - SSH Deploy"; 23 | 24 | public static string ResolveProjectFile() 25 | { 26 | var csproj = Directory 27 | .EnumerateFiles(Directory.GetCurrentDirectory(), "*.csproj", SearchOption.TopDirectoryOnly) 28 | .FirstOrDefault(); 29 | 30 | return !string.IsNullOrWhiteSpace(csproj) 31 | ? csproj 32 | : Directory.EnumerateFiles(Directory.GetCurrentDirectory(), "*.fsproj", SearchOption.TopDirectoryOnly) 33 | .FirstOrDefault(); 34 | } 35 | 36 | private static void Main(string[] args) 37 | { 38 | Title = "Unosquare"; 39 | 40 | Terminal.WriteLine($"SSH Deployment Tool [Version {typeof(Program).Assembly.GetName().Version}]"); 41 | Terminal.WriteLine("(c)2015 - 2019 Unosquare SA de CV. All Rights Reserved."); 42 | Terminal.WriteLine("For additional help, please visit https://github.com/unosquare/sshdeploy"); 43 | 44 | try 45 | { 46 | using var csproj = new CsProjFile(ResolveProjectFile()); 47 | csproj.Metadata.ParseCsProjTags(ref args); 48 | } 49 | catch (UnauthorizedAccessException) 50 | { 51 | Terminal.WriteLine("Access to csproj file denied", ConsoleColor.Red); 52 | } 53 | catch (ArgumentNullException) 54 | { 55 | Terminal.WriteLine("No csproj file was found", ConsoleColor.DarkRed); 56 | } 57 | 58 | if (!ArgumentParser.Current.ParseArguments(args, out var options)) 59 | { 60 | Environment.ExitCode = 1; 61 | Terminal.Flush(); 62 | return; 63 | } 64 | 65 | try 66 | { 67 | if (options.RunVerbOptions != null) 68 | { 69 | TitleSuffix = $" - Run Mode{TitleSuffix}"; 70 | Title = "Command"; 71 | DeploymentManager.ExecuteRunVerb(options.RunVerbOptions); 72 | } 73 | else if (options.ShellVerbOptions != null) 74 | { 75 | TitleSuffix = $" - Shell Mode{TitleSuffix}"; 76 | Title = "Interactive"; 77 | DeploymentManager.ExecuteShellVerb(options.ShellVerbOptions); 78 | } 79 | else if (options.MonitorVerbOptions != null) 80 | { 81 | TitleSuffix = $" - Monitor Mode{TitleSuffix}"; 82 | Title = "Monitor"; 83 | 84 | if (options.MonitorVerbOptions.Legacy) 85 | { 86 | DeploymentManager.ExecuteMonitorVerbLegacy(options.MonitorVerbOptions); 87 | } 88 | else 89 | { 90 | DeploymentManager.ExecuteMonitorVerb(options.MonitorVerbOptions); 91 | } 92 | } 93 | else if (options.PushVerbOptions != null) 94 | { 95 | TitleSuffix = $" - Push Mode{TitleSuffix}"; 96 | Title = "Push"; 97 | DeploymentManager.ExecutePushVerb(options.PushVerbOptions); 98 | } 99 | } 100 | catch (Exception ex) 101 | { 102 | $"Error - {ex.GetType().Name}".Error(); 103 | ex.Message.Error(); 104 | ex.StackTrace.Error(); 105 | Environment.ExitCode = 1; 106 | } 107 | 108 | if (Environment.ExitCode != 0) 109 | { 110 | $"Completed with errors. Exit Code {Environment.ExitCode}".Error(); 111 | } 112 | else 113 | { 114 | Terminal.WriteLine("Completed."); 115 | } 116 | 117 | Terminal.Flush(); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Unosquare.Labs.SshDeploy.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.2;netcoreapp3.1 6 | true 7 | true 8 | ./nupkg 9 | dotnet-sshdeploy 10 | dotnet-sshdeploy 11 | 0.4.0 12 | 0.4.0 13 | Unosquare 14 | 8.0 15 | enable 16 | A command-line tool that enables quick deployments over SSH. This is program was specifically designed to streamline .NET application development for the Raspberry Pi running Raspbian. 17 | 18 | ..\StyleCop.Analyzers.ruleset 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | All 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Utils/CsProjFile.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Utils 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Xml.Linq; 7 | 8 | /// 9 | /// Represents a CsProjFile (and FsProjFile) parser. 10 | /// 11 | /// 12 | /// Based on https://github.com/maartenba/dotnetcli-init. 13 | /// 14 | /// The type of CsProjMetadataBase. 15 | /// 16 | public class CsProjFile 17 | : IDisposable 18 | where T : CsProjMetadataBase 19 | { 20 | private readonly Stream _stream; 21 | private readonly bool _leaveOpen; 22 | private readonly XDocument _xmlDocument; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The filename. 28 | public CsProjFile(string? filename = null) 29 | : this(OpenFile(filename)) 30 | { 31 | // placeholder 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// The stream. 38 | /// if set to true [leave open]. 39 | /// Project file is not of the new .csproj type. 40 | public CsProjFile(Stream stream, bool leaveOpen = false) 41 | { 42 | _stream = stream; 43 | _leaveOpen = leaveOpen; 44 | 45 | _xmlDocument = XDocument.Load(stream); 46 | 47 | var projectElement = _xmlDocument.Descendants("Project").FirstOrDefault(); 48 | var sdkAttribute = projectElement?.Attribute("Sdk"); 49 | var sdk = sdkAttribute?.Value; 50 | 51 | if (sdk != "Microsoft.NET.Sdk" && sdk != "Microsoft.NET.Sdk.Web") 52 | throw new ArgumentException("Project file is not of the new .csproj type."); 53 | 54 | Metadata = Activator.CreateInstance(); 55 | Metadata.SetData(_xmlDocument); 56 | } 57 | 58 | /// 59 | /// Gets the metadata. 60 | /// 61 | /// 62 | /// The nu get metadata. 63 | /// 64 | public T Metadata { get; } 65 | 66 | /// 67 | /// Saves this instance. 68 | /// 69 | public void Save() 70 | { 71 | _stream.SetLength(0); 72 | _stream.Position = 0; 73 | 74 | _xmlDocument.Save(_stream); 75 | } 76 | 77 | /// 78 | public void Dispose() 79 | { 80 | if (!_leaveOpen) 81 | { 82 | _stream?.Dispose(); 83 | } 84 | } 85 | 86 | private static FileStream OpenFile(string? filename) 87 | { 88 | if (filename == null) 89 | { 90 | filename = Directory 91 | .EnumerateFiles(Directory.GetCurrentDirectory(), "*.csproj", SearchOption.TopDirectoryOnly) 92 | .FirstOrDefault() ?? 93 | Directory 94 | .EnumerateFiles(Directory.GetCurrentDirectory(), "*.fsproj", SearchOption.TopDirectoryOnly) 95 | .FirstOrDefault() ?? 96 | Directory 97 | .EnumerateFiles(Directory.GetCurrentDirectory(), "*.vbproj", SearchOption.TopDirectoryOnly) 98 | .FirstOrDefault(); 99 | } 100 | 101 | if (string.IsNullOrWhiteSpace(filename)) 102 | throw new ArgumentNullException(nameof(filename)); 103 | 104 | return File.Open(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Utils/CsProjMetadataBase.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Utils 2 | { 3 | using System.Linq; 4 | using System.Xml.Linq; 5 | 6 | /// 7 | /// Represents a CsProj metadata abstract class 8 | /// to use with CsProjFile parser. 9 | /// 10 | public abstract class CsProjMetadataBase 11 | { 12 | private XDocument _xmlDocument; 13 | 14 | /// 15 | /// Gets the package identifier. 16 | /// 17 | /// 18 | /// The package identifier. 19 | /// 20 | public string? PackageId => FindElement(nameof(PackageId))?.Value; 21 | 22 | /// 23 | /// Gets the name of the assembly. 24 | /// 25 | /// 26 | /// The name of the assembly. 27 | /// 28 | public string? AssemblyName => FindElement(nameof(AssemblyName))?.Value; 29 | 30 | /// 31 | /// Gets the target frameworks. 32 | /// 33 | /// 34 | /// The target frameworks. 35 | /// 36 | public string? TargetFrameworks => FindElement(nameof(TargetFrameworks))?.Value; 37 | 38 | /// 39 | /// Gets the target framework. 40 | /// 41 | /// 42 | /// The target framework. 43 | /// 44 | public string? TargetFramework => FindElement(nameof(TargetFramework))?.Value; 45 | 46 | /// 47 | /// Gets the version. 48 | /// 49 | /// 50 | /// The version. 51 | /// 52 | public string? Version => FindElement(nameof(Version))?.Value; 53 | 54 | /// 55 | /// Parses the cs proj tags. 56 | /// 57 | /// The arguments. 58 | public abstract void ParseCsProjTags(ref string[] args); 59 | 60 | /// 61 | /// Sets the data. 62 | /// 63 | /// The XML document. 64 | public void SetData(XDocument xmlDocument) => _xmlDocument = xmlDocument; 65 | 66 | /// 67 | /// Finds the element. 68 | /// 69 | /// Name of the element. 70 | /// A XElement. 71 | protected XElement FindElement(string elementName) => _xmlDocument.Descendants(elementName).FirstOrDefault(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Unosquare.Labs.SshDeploy/Utils/CsProjNuGetMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Unosquare.Labs.SshDeploy.Utils 2 | { 3 | using Attributes; 4 | using Options; 5 | using System; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | public class CsProjNuGetMetadata : CsProjMetadataBase 10 | { 11 | [Push(ShortName = "-f", LongName = "--framework")] 12 | public new string? TargetFramework => FindElement(nameof(TargetFramework))?.Value; 13 | 14 | [Monitor(ShortName = "-l", LongName = "--legacy")] 15 | public bool SshDeployLegacy => FindElement(nameof(SshDeployLegacy)) != null; 16 | 17 | [Monitor(ShortName = "-m", LongName = "--monitor")] 18 | public string? SshDeployMonitorFile => FindElement(nameof(SshDeployMonitorFile))?.Value; 19 | 20 | [Push(ShortName = "-c", LongName = "--configuration")] 21 | public string? SshDeployConfiguration => FindElement(nameof(SshDeployConfiguration))?.Value; 22 | 23 | [Run(ShortName = "-c", LongName = "--command")] 24 | public string? SshDeployCommand => FindElement(nameof(SshDeployCommand))?.Value; 25 | 26 | [Push(LongName = "--pre")] 27 | [Monitor(LongName = "--pre")] 28 | public string? SshDeployPreCommand => FindElement(nameof(SshDeployPreCommand))?.Value; 29 | 30 | [Push(LongName = "--post")] 31 | [Monitor(LongName = "--post")] 32 | public string? SshDeployPostCommand => FindElement(nameof(SshDeployPostCommand))?.Value; 33 | 34 | [Push(LongName = "--clean")] 35 | [Monitor(LongName = "--clean")] 36 | public bool SshDeployClean => FindElement(nameof(SshDeployClean)) != null; 37 | 38 | [Push(LongName = "--exclude")] 39 | [Monitor(LongName = "--exclude")] 40 | public string? SshDeployExclude => FindElement(nameof(SshDeployExclude))?.Value; 41 | 42 | [Push(ShortName = "-h", LongName = "--host")] 43 | [Monitor(ShortName = "-h", LongName = "--host")] 44 | [Shell(ShortName = "-h", LongName = "--host")] 45 | [Run(ShortName = "-h", LongName = "--host")] 46 | public string? SshDeployHost => FindElement(nameof(SshDeployHost))?.Value; 47 | 48 | [Push(ShortName = "-p", LongName = "--port")] 49 | [Monitor(ShortName = "-p", LongName = "--port")] 50 | [Shell(ShortName = "-p", LongName = "--port")] 51 | [Run(ShortName = "-p", LongName = "--port")] 52 | public string? SshDeployPort => FindElement(nameof(SshDeployPort))?.Value; 53 | 54 | [Push(ShortName = "-u", LongName = "--username")] 55 | [Monitor(ShortName = "-u", LongName = "--username")] 56 | [Shell(ShortName = "-u", LongName = "--username")] 57 | [Run(ShortName = "-u", LongName = "--username")] 58 | public string? SshDeployUsername => FindElement(nameof(SshDeployUsername))?.Value; 59 | 60 | [Push(ShortName = "-w", LongName = "--password")] 61 | [Monitor(ShortName = "-w", LongName = "--password")] 62 | [Shell(ShortName = "-w", LongName = "--password")] 63 | [Run(ShortName = "-w", LongName = "--password")] 64 | public string? SshDeployPassword => FindElement(nameof(SshDeployPassword))?.Value; 65 | 66 | [Monitor(ShortName = "-s", LongName = "--source")] 67 | public string? SshDeploySourcePath => FindElement(nameof(SshDeploySourcePath))?.Value; 68 | 69 | [Monitor(ShortName = "-t", LongName = "--target")] 70 | [Push(ShortName = "-t", LongName = "--target")] 71 | public string? SshDeployTargetPath => FindElement(nameof(SshDeployTargetPath))?.Value; 72 | 73 | [Push(ShortName = "-r", LongName = "--runtime")] 74 | public string? RuntimeIdentifier => FindElement(nameof(RuntimeIdentifier))?.Value; 75 | 76 | [Push(ShortName = "-x", LongName = "--execute")] 77 | public string? SshDeployExecutePermission => FindElement(nameof(SshDeployExecutePermission))?.Value; 78 | 79 | public override void ParseCsProjTags(ref string[] args) 80 | { 81 | var argsList = args.ToList(); 82 | var type = GetAttributeType(args); 83 | var props = GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, type)); 84 | 85 | foreach (var propertyInfo in props) 86 | { 87 | var value = propertyInfo.GetValue(this); 88 | if (value == null) 89 | continue; 90 | 91 | var attribute = (VerbAttributeBase)propertyInfo.GetCustomAttribute(type); 92 | 93 | if (args.Contains(attribute.LongName) || args.Contains(attribute.ShortName)) 94 | continue; 95 | 96 | if (!(value is bool)) 97 | { 98 | argsList.Add(!string.IsNullOrWhiteSpace(attribute.ShortName) ? attribute.ShortName : attribute.LongName); 99 | argsList.Add(value.ToString()); 100 | } 101 | else if ((bool)value) 102 | { 103 | argsList.Add(!string.IsNullOrWhiteSpace(attribute.ShortName) ? attribute.ShortName : attribute.LongName); 104 | } 105 | } 106 | 107 | args = argsList.ToArray(); 108 | PushVerbOptions.IgnoreTargetFrameworkToOutputPath = FindElement("AppendTargetFrameworkToOutputPath")?.Value.ToLowerInvariant() == "false"; 109 | } 110 | 111 | private static Type GetAttributeType(string[] args) 112 | { 113 | if (args.Contains("push")) 114 | return typeof(PushAttribute); 115 | if (args.Contains("monitor")) 116 | return typeof(MonitorAttribute); 117 | 118 | return args.Contains("run") ? typeof(RunAttribute) : typeof(ShellAttribute); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /sshdeploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unosquare/sshdeploy/05001fe5d3f2a23f068826f82c19b2c4a792c2a9/sshdeploy.png --------------------------------------------------------------------------------