├── .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 | [](https://www.nuget.org/packages/dotnet-sshdeploy/)
5 | [](https://github.com/igrigorik/ga-beacon)
6 | [](https://travis-ci.org/unosquare/sshdeploy)
7 | [](https://ci.appveyor.com/project/geoperez/sshdeploy)
8 | [](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
--------------------------------------------------------------------------------