├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PowerSession.Cli ├── PowerSession.Cli.csproj └── Program.cs ├── PowerSession.ConPTY ├── Native │ ├── ConsoleApi.cs │ ├── ProcessApi.cs │ └── PseudoConsoleApi.cs ├── PowerSession.ConPTY.csproj ├── Processes │ ├── Process.cs │ └── ProcessFactory.cs ├── PseudoConsole.cs ├── PseudoConsolePipe.cs └── Terminal.cs ├── PowerSession.Main ├── Api │ ├── AsciinemaApi.cs │ └── IApiService.cs ├── AppConfig.cs ├── Commands │ ├── AuthCommand.cs │ ├── BaseCommand.cs │ ├── PlayCommand.cs │ ├── RecordCommand.cs │ ├── RecordSession.cs │ └── UploadCommand.cs ├── PowerSession.Main.csproj └── Utils │ └── Writer.cs ├── PowerSession.Tests ├── PowerSession.Tests.csproj └── TestCommands.cs ├── PowerSession.sln └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ibigbug 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: lvm 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "nuget" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Publish CSharp Release Asset 8 | 9 | jobs: 10 | publish: 11 | strategy: 12 | matrix: 13 | runtime: ['win10-x64'] 14 | name: Upload Release Asset 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup .NET Core SDK 18 | uses: actions/setup-dotnet@v2 19 | with: 20 | dotnet-version: '6.0.x' 21 | include-prerelease: true 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | - name: Build project # This would actually build your project, using zip for an example artifact 25 | run: | 26 | dotnet publish PowerSession.Cli -o bin/${{matrix.runtime}} -c Release -r ${{matrix.runtime}} -p:PublishSingleFile=true --self-contained 27 | ls bin/${{matrix.runtime}} 28 | mv bin/${{matrix.runtime}}/PowerSession.exe bin/${{matrix.runtime}}/PowerSession-${{matrix.runtime}}.exe 29 | dotnet publish PowerSession.Cli -o bin/no-self-contained/ -c Release -r ${{matrix.runtime}} -p:PublishSingleFile=true --no-self-contained 30 | ls bin/no-self-contained/ 31 | mv bin/no-self-contained/PowerSession.exe bin/no-self-contained/PowerSession-no-self-contained.exe 32 | dotnet pack PowerSession.Cli -c Release -o nuget 33 | - name: Upload Release 34 | uses: softprops/action-gh-release@v1 35 | if: startsWith(github.ref, 'refs/tags/') 36 | with: 37 | files: | 38 | ./bin/${{matrix.runtime}}/PowerSession-${{matrix.runtime}}.exe 39 | ./bin/no-self-contained/PowerSession-no-self-contained.exe 40 | env: 41 | GITHUB_REPOSITORY: my_gh_org/my_gh_repo 42 | TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | - name: Publish Nuget Package 44 | run: | 45 | dotnet nuget push "nuget/*.nupkg" --api-key ${{ secrets.NUGET_API_TOKEN }} --source https://api.nuget.org/v3/index.json 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | contact@watfaq.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing to PowerSession 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 7 | 8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's readme 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | 14 | 15 | ## Table of Contents 16 | 17 | - [Code of Conduct](#code-of-conduct) 18 | - [I Have a Question](#i-have-a-question) 19 | - [I Want To Contribute](#i-want-to-contribute) 20 | - [Reporting Bugs](#reporting-bugs) 21 | - [Suggesting Enhancements](#suggesting-enhancements) 22 | - [Your First Code Contribution](#your-first-code-contribution) 23 | - [Improving The Documentation](#improving-the-documentation) 24 | - [Styleguides](#styleguides) 25 | - [Commit Messages](#commit-messages) 26 | - [Join The Project Team](#join-the-project-team) 27 | 28 | 29 | ## Code of Conduct 30 | 31 | This project and everyone participating in it is governed by the 32 | [PowerSession Code of Conduct](https://github.com/Watfaq/PowerSession/blob/master/CODE_OF_CONDUCT.md). 33 | By participating, you are expected to uphold this code. Please report unacceptable behavior 34 | to . 35 | 36 | 37 | ## I Have a Question 38 | 39 | > If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/Watfaq/PowerSession/blob/master/README.md). 40 | 41 | Before you ask a question, it is best to search for existing [Issues](https://github.com/Watfaq/PowerSession/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 42 | 43 | If you then still feel the need to ask a question and need clarification, we recommend the following: 44 | 45 | - Open an [Issue](https://github.com/Watfaq/PowerSession/issues/new). 46 | - Provide as much context as you can about what you're running into. 47 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 48 | 49 | We will then take care of the issue as soon as possible. 50 | 51 | 65 | 66 | ## I Want To Contribute 67 | 68 | > ### Legal Notice 69 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. 70 | 71 | ### Reporting Bugs 72 | 73 | 74 | #### Before Submitting a Bug Report 75 | 76 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 77 | 78 | - Make sure that you are using the latest version. 79 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/Watfaq/PowerSession/blob/master/README.md). If you are looking for support, you might want to check [this section](#i-have-a-question)). 80 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Watfaq/PowerSession/issues?q=label%3Abug). 81 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 82 | - Collect information about the bug: 83 | - Stack trace (Traceback) 84 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 85 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 86 | - Possibly your input and the output 87 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 88 | 89 | 90 | #### How Do I Submit a Good Bug Report? 91 | 92 | > You must never report security related issues, vulnerabilities or bugs to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to . 93 | 94 | 95 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 96 | 97 | - Open an [Issue](https://github.com/Watfaq/PowerSession/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 98 | - Explain the behavior you would expect and the actual behavior. 99 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 100 | - Provide the information you collected in the previous section. 101 | 102 | Once it's filed: 103 | 104 | - The project team will label the issue accordingly. 105 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 106 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 107 | 108 | 109 | 110 | 111 | ### Suggesting Enhancements 112 | 113 | This section guides you through submitting an enhancement suggestion for PowerSession, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 114 | 115 | 116 | #### Before Submitting an Enhancement 117 | 118 | - Make sure that you are using the latest version. 119 | - Read the [documentation](https://github.com/Watfaq/PowerSession/blob/master/README.md) carefully and find out if the functionality is already covered, maybe by an individual configuration. 120 | - Perform a [search](https://github.com/Watfaq/PowerSession/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 121 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 122 | 123 | 124 | #### How Do I Submit a Good Enhancement Suggestion? 125 | 126 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/Watfaq/PowerSession/issues). 127 | 128 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 129 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 130 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 131 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 132 | - **Explain why this enhancement would be useful** to most PowerSession users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 133 | 134 | 135 | 136 | ### Your First Code Contribution 137 | 138 | You'll need to have .NET development environment setup on you machine. 139 | 140 | Recommended IDE 141 | 142 | * Rider 143 | * Visual Studio 144 | 145 | 146 | ## Attribution 147 | This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Watfaq. 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 | -------------------------------------------------------------------------------- /PowerSession.Cli/PowerSession.Cli.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | win-x64 7 | PowerSession 8 | PowerSession 9 | PowerSession 10 | Yuwei Ba 11 | true 12 | 1.4.6 13 | 1.4.6 14 | 1.4.6 15 | 1.4.6 16 | true 17 | PowerSession 18 | https://github.com/ibigbug/PowerSession 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /PowerSession.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Cli 2 | { 3 | using System.CommandLine; 4 | using System.IO; 5 | using Commands; 6 | using Main.Commands; 7 | 8 | internal static class Program 9 | { 10 | private static void Main(string[] args) 11 | { 12 | var recordArg = new Argument("file") {Description = "The filename to save the record"}; 13 | var recordOpt = new Option(new[] {"--command", "-c"}, "The command to record, defaults to $SHELL"); 14 | 15 | var record = new Command("rec") 16 | { 17 | recordArg, 18 | recordOpt, 19 | }; 20 | record.Description = "Record and save a session"; 21 | record.SetHandler((FileInfo file, string command) => 22 | { 23 | var recordCmd = new RecordCommand(new RecordArgs 24 | { 25 | Filename = file.FullName, 26 | Command = command 27 | }); 28 | 29 | recordCmd.Execute(); 30 | }, recordArg, recordOpt); 31 | 32 | var playArg = new Argument("file") {Description = "The record session"}; 33 | var play = new Command("play") 34 | { 35 | playArg, 36 | }; 37 | play.Description = "Play a recorded session"; 38 | play.SetHandler((FileInfo file) => 39 | { 40 | var playCommand = new PlayCommand(new PlayArgs{Filename = file.FullName, EnableAnsiEscape = true}); 41 | playCommand.Execute(); 42 | }, playArg); 43 | 44 | var auth = new Command("auth") 45 | { 46 | Description = "Auth with asciinema.org" 47 | }; 48 | auth.SetHandler(() => 49 | { 50 | var authCommand = new AuthCommand(); 51 | authCommand.Execute(); 52 | }); 53 | 54 | var uploadArg = new Argument("file") {Description = "The file to be uploaded"}; 55 | var upload = new Command("upload") 56 | { 57 | uploadArg, 58 | }; 59 | upload.Description = "Upload a session to ascinema.org"; 60 | upload.SetHandler((FileInfo file) => 61 | { 62 | var uploadCommand = new UploadCommand(file.FullName); 63 | uploadCommand.Execute(); 64 | }, uploadArg); 65 | 66 | var rooCommand = new RootCommand 67 | { 68 | record, 69 | play, 70 | auth, 71 | upload 72 | }; 73 | 74 | rooCommand.Description = "Record, Play and Share your PowerShell Session."; 75 | 76 | rooCommand.InvokeAsync(args).Wait(); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/Native/ConsoleApi.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Main.ConPTY.Native 2 | { 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | public static class ConsoleApi 7 | { 8 | public const int STD_OUTPUT_HANDLE = -11; 9 | public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; 10 | public const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008; 11 | 12 | [DllImport("kernel32.dll", SetLastError = true)] 13 | public static extern IntPtr GetStdHandle(int nStdHandle); 14 | 15 | [DllImport("kernel32.dll")] 16 | public static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); 17 | 18 | [DllImport("kernel32.dll")] 19 | public static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); 20 | 21 | [DllImport("kernel32.dll", SetLastError = true)] 22 | internal static extern bool SetConsoleCtrlHandler(ConsoleEventDelegate callback, bool add); 23 | 24 | internal delegate bool ConsoleEventDelegate(CtrlTypes ctrl); 25 | 26 | internal enum CtrlTypes : uint 27 | { 28 | CTRL_C_EVENT = 0, 29 | CTRL_BREAK_EVENT, 30 | CTRL_CLOSE_EVENT, 31 | CTRL_LOGOFF_EVENT = 5, 32 | CTRL_SHUTDOWN_EVENT 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/Native/ProcessApi.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Main.ConPTY.Native 2 | { 3 | using System; 4 | using System.Runtime.InteropServices; 5 | 6 | internal static class ProcessApi 7 | { 8 | internal const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000; 9 | 10 | [DllImport("kernel32.dll", SetLastError = true)] 11 | [return: MarshalAs(UnmanagedType.Bool)] 12 | internal static extern bool InitializeProcThreadAttributeList( 13 | IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize); 14 | 15 | [DllImport("kernel32.dll", SetLastError = true)] 16 | [return: MarshalAs(UnmanagedType.Bool)] 17 | internal static extern bool UpdateProcThreadAttribute( 18 | IntPtr lpAttributeList, uint dwFlags, IntPtr attribute, IntPtr lpValue, 19 | IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize); 20 | 21 | [DllImport("kernel32.dll")] 22 | [return: MarshalAs(UnmanagedType.Bool)] 23 | internal static extern bool CreateProcess( 24 | string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, 25 | ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, 26 | IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo, 27 | out PROCESS_INFORMATION lpProcessInformation); 28 | 29 | [DllImport("kernel32.dll", SetLastError = true)] 30 | [return: MarshalAs(UnmanagedType.Bool)] 31 | internal static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList); 32 | 33 | [DllImport("kernel32.dll", SetLastError = true)] 34 | internal static extern bool CloseHandle(IntPtr hObject); 35 | 36 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 37 | internal struct STARTUPINFOEX 38 | { 39 | public STARTUPINFO StartupInfo; 40 | public IntPtr lpAttributeList; 41 | } 42 | 43 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 44 | internal struct STARTUPINFO 45 | { 46 | public int cb; 47 | public string lpReserved; 48 | public string lpDesktop; 49 | public string lpTitle; 50 | public int dwX; 51 | public int dwY; 52 | public int dwXSize; 53 | public int dwYSize; 54 | public int dwXCountChars; 55 | public int dwYCountChars; 56 | public int dwFillAttribute; 57 | public int dwFlags; 58 | public short wShowWindow; 59 | public short cbReserved2; 60 | public IntPtr lpReserved2; 61 | public IntPtr hStdInput; 62 | public IntPtr hStdOutput; 63 | public IntPtr hStdError; 64 | } 65 | 66 | [StructLayout(LayoutKind.Sequential)] 67 | internal struct PROCESS_INFORMATION 68 | { 69 | public IntPtr hProcess; 70 | public IntPtr hThread; 71 | public int dwProcessId; 72 | public int dwThreadId; 73 | } 74 | 75 | [StructLayout(LayoutKind.Sequential)] 76 | internal struct SECURITY_ATTRIBUTES 77 | { 78 | public int nLength; 79 | public IntPtr lpSecurityDescriptor; 80 | public int bInheritHandle; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/Native/PseudoConsoleApi.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Main.ConPTY.Native 2 | { 3 | using System; 4 | using System.Runtime.InteropServices; 5 | using Microsoft.Win32.SafeHandles; 6 | 7 | internal static class PseudoConsoleApi 8 | { 9 | internal const uint PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016; 10 | 11 | [DllImport("kernel32.dll", SetLastError = true)] 12 | internal static extern int CreatePseudoConsole(COORD size, SafeFileHandle hInput, SafeFileHandle hOutput, 13 | uint dwFlags, out IntPtr phPC); 14 | 15 | [DllImport("kernel32.dll", SetLastError = true)] 16 | internal static extern int ClosePseudoConsole(IntPtr hPC); 17 | 18 | [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] 19 | internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, 20 | IntPtr lpPipeAttributes, int nSize); 21 | 22 | [StructLayout(LayoutKind.Sequential)] 23 | internal struct COORD 24 | { 25 | public short X; 26 | public short Y; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/PowerSession.ConPTY.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PowerSession.ConPTY/Processes/Process.cs: -------------------------------------------------------------------------------- 1 | using static PowerSession.Main.ConPTY.Native.ProcessApi; 2 | 3 | namespace PowerSession.Main.ConPTY.Processes 4 | { 5 | using System; 6 | using System.Runtime.InteropServices; 7 | using Native; 8 | 9 | internal sealed class Process : IDisposable 10 | { 11 | public Process(ProcessApi.STARTUPINFOEX startupInfo, ProcessApi.PROCESS_INFORMATION processInfo) 12 | { 13 | StartupInfo = startupInfo; 14 | ProcessInfo = processInfo; 15 | } 16 | 17 | public ProcessApi.STARTUPINFOEX StartupInfo { get; } 18 | public ProcessApi.PROCESS_INFORMATION ProcessInfo { get; } 19 | 20 | #region IDisposable Support 21 | 22 | private bool disposedValue; // To detect redundant calls 23 | 24 | private void Dispose(bool disposing) 25 | { 26 | if (!disposedValue) 27 | { 28 | if (disposing) 29 | { 30 | // dispose managed state (managed objects). 31 | } 32 | 33 | // dispose unmanaged state 34 | 35 | // Free the attribute list 36 | if (StartupInfo.lpAttributeList != IntPtr.Zero) 37 | { 38 | DeleteProcThreadAttributeList(StartupInfo.lpAttributeList); 39 | Marshal.FreeHGlobal(StartupInfo.lpAttributeList); 40 | } 41 | 42 | // Close process and thread handles 43 | if (ProcessInfo.hProcess != IntPtr.Zero) CloseHandle(ProcessInfo.hProcess); 44 | if (ProcessInfo.hThread != IntPtr.Zero) CloseHandle(ProcessInfo.hThread); 45 | 46 | disposedValue = true; 47 | } 48 | } 49 | 50 | ~Process() 51 | { 52 | Dispose(false); 53 | } 54 | 55 | public void Dispose() 56 | { 57 | Dispose(true); 58 | GC.SuppressFinalize(this); 59 | } 60 | 61 | #endregion 62 | } 63 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/Processes/ProcessFactory.cs: -------------------------------------------------------------------------------- 1 | using static PowerSession.Main.ConPTY.Native.ProcessApi; 2 | 3 | namespace PowerSession.Main.ConPTY.Processes 4 | { 5 | using System; 6 | using System.ComponentModel; 7 | using System.Runtime.InteropServices; 8 | using Native; 9 | 10 | internal static class ProcessFactory 11 | { 12 | /// 13 | /// Start and configure a process. The return value represents the process and should be disposed. 14 | /// 15 | internal static Process Start(string command, IntPtr attributes, IntPtr hPC) 16 | { 17 | var startupInfo = ConfigureProcessThread(hPC, attributes); 18 | var processInfo = RunProcess(ref startupInfo, command); 19 | return new Process(startupInfo, processInfo); 20 | } 21 | 22 | private static ProcessApi.STARTUPINFOEX ConfigureProcessThread(IntPtr hPC, IntPtr attributes) 23 | { 24 | // this method implements the behavior described in https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#preparing-for-creation-of-the-child-process 25 | 26 | var lpSize = IntPtr.Zero; 27 | var success = InitializeProcThreadAttributeList( 28 | IntPtr.Zero, 29 | 1, 30 | 0, 31 | ref lpSize 32 | ); 33 | if (success || lpSize == IntPtr.Zero 34 | ) // we're not expecting `success` here, we just want to get the calculated lpSize 35 | throw new Win32Exception(Marshal.GetLastWin32Error(), 36 | "Could not calculate the number of bytes for the attribute list."); 37 | 38 | var startupInfo = new ProcessApi.STARTUPINFOEX(); 39 | startupInfo.StartupInfo.cb = Marshal.SizeOf(); 40 | startupInfo.lpAttributeList = Marshal.AllocHGlobal(lpSize); 41 | 42 | success = InitializeProcThreadAttributeList( 43 | startupInfo.lpAttributeList, 44 | 1, 45 | 0, 46 | ref lpSize 47 | ); 48 | if (!success) throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set up attribute list."); 49 | 50 | success = UpdateProcThreadAttribute( 51 | startupInfo.lpAttributeList, 52 | 0, 53 | attributes, 54 | hPC, 55 | (IntPtr) IntPtr.Size, 56 | IntPtr.Zero, 57 | IntPtr.Zero 58 | ); 59 | if (!success) 60 | throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not set pseudoconsole thread attribute."); 61 | 62 | return startupInfo; 63 | } 64 | 65 | private static ProcessApi.PROCESS_INFORMATION RunProcess(ref ProcessApi.STARTUPINFOEX sInfoEx, string commandLine) 66 | { 67 | var securityAttributeSize = Marshal.SizeOf(); 68 | var pSec = new ProcessApi.SECURITY_ATTRIBUTES {nLength = securityAttributeSize}; 69 | var tSec = new ProcessApi.SECURITY_ATTRIBUTES {nLength = securityAttributeSize}; 70 | var success = CreateProcess( 71 | null, 72 | commandLine, 73 | ref pSec, 74 | ref tSec, 75 | false, 76 | EXTENDED_STARTUPINFO_PRESENT, 77 | IntPtr.Zero, 78 | null, 79 | ref sInfoEx, 80 | out var pInfo 81 | ); 82 | if (!success) throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not create process."); 83 | 84 | return pInfo; 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/PseudoConsole.cs: -------------------------------------------------------------------------------- 1 | using static PowerSession.Main.ConPTY.Native.PseudoConsoleApi; 2 | 3 | namespace PowerSession.Main.ConPTY 4 | { 5 | using System; 6 | using System.ComponentModel; 7 | using Microsoft.Win32.SafeHandles; 8 | using Native; 9 | 10 | internal sealed class PseudoConsole : IDisposable 11 | { 12 | public static readonly IntPtr PseudoConsoleThreadAttribute = (IntPtr) PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE; 13 | 14 | private PseudoConsole(IntPtr handle) 15 | { 16 | Handle = handle; 17 | } 18 | 19 | public IntPtr Handle { get; } 20 | 21 | public void Dispose() 22 | { 23 | ClosePseudoConsole(Handle); 24 | } 25 | 26 | internal static PseudoConsole Create(SafeFileHandle inputReadSide, SafeFileHandle outputWriteSide, int width, 27 | int height) 28 | { 29 | var createResult = CreatePseudoConsole( 30 | new PseudoConsoleApi.COORD {X = (short) width, Y = (short) height}, 31 | inputReadSide, outputWriteSide, 32 | 0, out var hPC); 33 | if (createResult != 0) throw new Win32Exception(createResult, "Failed to create pseudo console."); 34 | return new PseudoConsole(hPC); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/PseudoConsolePipe.cs: -------------------------------------------------------------------------------- 1 | using static PowerSession.Main.ConPTY.Native.PseudoConsoleApi; 2 | 3 | namespace PowerSession.Main.ConPTY 4 | { 5 | using System; 6 | using System.ComponentModel; 7 | using System.Runtime.InteropServices; 8 | using Microsoft.Win32.SafeHandles; 9 | 10 | public sealed class PseudoConsolePipe : IDisposable 11 | { 12 | public readonly SafeFileHandle ReadSide; 13 | public readonly SafeFileHandle WriteSide; 14 | 15 | public PseudoConsolePipe() 16 | { 17 | if (!CreatePipe(out ReadSide, out WriteSide, IntPtr.Zero, 0)) 18 | throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create pipe."); 19 | } 20 | 21 | #region IDisposable 22 | 23 | private void Dispose(bool disposing) 24 | { 25 | if (disposing) 26 | { 27 | ReadSide?.Dispose(); 28 | WriteSide?.Dispose(); 29 | } 30 | } 31 | 32 | public void Dispose() 33 | { 34 | Dispose(true); 35 | GC.SuppressFinalize(this); 36 | } 37 | 38 | #endregion 39 | } 40 | } -------------------------------------------------------------------------------- /PowerSession.ConPTY/Terminal.cs: -------------------------------------------------------------------------------- 1 | using static PowerSession.Main.ConPTY.Native.ConsoleApi; 2 | 3 | namespace PowerSession.Main.ConPTY 4 | { 5 | using System; 6 | using System.Collections; 7 | using System.IO; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Win32.SafeHandles; 11 | using Native; 12 | using Processes; 13 | 14 | public sealed class Terminal 15 | { 16 | private const string CtrlC_Command = "\x3"; 17 | private char[] UpArrow = new []{(char) 0x1b, (char) 0x5b, 'A'}; 18 | private char[] DownArrow = new []{(char) 0x1b, (char) 0x5b, 'B'}; 19 | private char[] RightArrow = new []{(char) 0x1b, (char) 0x5b, 'C'}; 20 | private char[] LeftArrow = new []{(char) 0x1b, (char) 0x5b, 'D'}; 21 | 22 | private readonly Stream _inputReader; 23 | private readonly Stream _outputWriter; 24 | private readonly int _height; 25 | private readonly int _width; 26 | 27 | private SafeFileHandle _consoleInputPipeWriteHandle; 28 | private StreamWriter _consoleInputWriter; 29 | private FileStream _consoleOutStream; 30 | 31 | private readonly CancellationTokenSource _tokenSource; 32 | private readonly CancellationToken _token; 33 | 34 | public Terminal(Stream inputReader = null, Stream outputWriter = null, bool enableAnsiEscape = true, 35 | int width = default, int height = default) 36 | { 37 | _inputReader = inputReader; 38 | _outputWriter = outputWriter; 39 | 40 | if (enableAnsiEscape) 41 | { 42 | var stdout = GetStdHandle(STD_OUTPUT_HANDLE); 43 | if (GetConsoleMode(stdout, out var consoleMode)) 44 | { 45 | consoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; 46 | if (!SetConsoleMode(stdout, consoleMode)) 47 | { 48 | throw new NotSupportedException("VIRTUAL_TERMINAL_PROCESSING"); 49 | } 50 | } 51 | } 52 | 53 | _width = width == 0 ? Console.WindowWidth : width; 54 | _height = height == 0 ? Console.WindowHeight : height; 55 | 56 | _tokenSource = new CancellationTokenSource(); 57 | _token = _tokenSource.Token; 58 | } 59 | 60 | public void Record(string command, IDictionary environment = null) 61 | { 62 | using var inputPipe = new PseudoConsolePipe(); 63 | using var outputPipe = new PseudoConsolePipe(); 64 | using var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, _width, _height); 65 | 66 | using var process = ProcessFactory.Start($"powershell.exe -c \"Set-Item -Path Env:POWERSESSION_RECORDING -Value 1;{command}\"", PseudoConsole.PseudoConsoleThreadAttribute, 67 | pseudoConsole.Handle); 68 | _consoleOutStream = new FileStream(outputPipe.ReadSide, FileAccess.Read); 69 | 70 | _consoleInputPipeWriteHandle = inputPipe.WriteSide; 71 | _consoleInputWriter = new StreamWriter(new FileStream(_consoleInputPipeWriteHandle, FileAccess.Write)) 72 | {AutoFlush = true}; 73 | 74 | AttachStdin(); 75 | ConnectOutput(_outputWriter); 76 | 77 | OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe, _consoleInputWriter, 78 | _consoleOutStream)); 79 | WaitForExit(process).WaitOne(Timeout.Infinite); 80 | 81 | _tokenSource.Cancel(); 82 | _consoleOutStream.Close(); 83 | _consoleInputWriter.Close(); 84 | _outputWriter.Close(); 85 | } 86 | 87 | private void AttachStdin() 88 | { 89 | Console.CancelKeyPress += (sender, args) => 90 | { 91 | args.Cancel = true; 92 | _consoleInputWriter.Write(CtrlC_Command); 93 | }; 94 | 95 | Task.Factory.StartNew(() => 96 | { 97 | while (!_token.IsCancellationRequested) 98 | { 99 | var key = Console.ReadKey(true); 100 | switch (key.Key) 101 | { 102 | case ConsoleKey.UpArrow: 103 | _consoleInputWriter.Write(UpArrow); 104 | break; 105 | case ConsoleKey.DownArrow: 106 | _consoleInputWriter.Write(DownArrow); 107 | break; 108 | case ConsoleKey.RightArrow: 109 | _consoleInputWriter.Write(RightArrow); 110 | break; 111 | case ConsoleKey.LeftArrow: 112 | _consoleInputWriter.Write(LeftArrow); 113 | break; 114 | default: 115 | _consoleInputWriter.Write(key.KeyChar); 116 | break; 117 | } 118 | } 119 | }, TaskCreationOptions.LongRunning); 120 | } 121 | 122 | 123 | private void ConnectOutput(Stream outputStream) 124 | { 125 | if (_outputWriter == null) return; 126 | 127 | Task.Factory.StartNew(() => { _consoleOutStream.CopyTo(outputStream); }, TaskCreationOptions.LongRunning); 128 | } 129 | 130 | private static AutoResetEvent WaitForExit(Process process) 131 | { 132 | return new AutoResetEvent(false) 133 | { 134 | SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, false) 135 | }; 136 | } 137 | 138 | private static void OnClose(Action handler) 139 | { 140 | SetConsoleCtrlHandler(eventType => 141 | { 142 | if (eventType == ConsoleApi.CtrlTypes.CTRL_CLOSE_EVENT) handler(); 143 | return false; 144 | }, true); 145 | } 146 | 147 | private static void DisposeResources(params IDisposable[] disposables) 148 | { 149 | foreach (var disposable in disposables) disposable?.Dispose(); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /PowerSession.Main/Api/AsciinemaApi.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace PowerSession.Main.Api 3 | { 4 | using System; 5 | using System.IO; 6 | using System.Net.Http; 7 | using System.Linq; 8 | using System.Net.Http.Headers; 9 | using System.Text; 10 | using PowerSession.Api; 11 | using System.Reflection; 12 | 13 | public class AsciinemaApi : IApiService 14 | { 15 | private readonly HttpClient _httpClient; 16 | private string ApiHost = "https://asciinema.org"; 17 | private string AuthUrl => $"{ApiHost}/connect/{AppConfig.InstallId}"; 18 | private string UploadUrl => $"{ApiHost}/api/asciicasts"; 19 | 20 | 21 | private static readonly OperatingSystem Os = Environment.OSVersion; 22 | private static readonly string RuntimeFramework = $"dotnet/{Environment.Version.Major}.{Environment.Version.Minor}.{Environment.Version.Build}"; 23 | private static readonly string OperatingSystem = $"Windows/{Os.Version.Major}-{Os.Version.Major}.{Os.Version.Minor}.{Os.Version.Build}-SP{Os.ServicePack.Split(' ').Skip(2).FirstOrDefault() ?? "0"}"; 24 | 25 | public string Upload(string filePath) 26 | { 27 | var req = new MultipartFormDataContent(); 28 | var cast = new ByteArrayContent(File.ReadAllBytes(filePath)); 29 | cast.Headers.ContentType = MediaTypeHeaderValue.Parse(System.Net.Mime.MediaTypeNames.Text.Plain); 30 | req.Add(cast, "asciicast", "ascii.cast"); 31 | 32 | var res = _httpClient.PostAsync(UploadUrl, req).Result; 33 | if (!res.IsSuccessStatusCode) 34 | { 35 | Console.WriteLine("Upload Failed:"); 36 | Console.WriteLine(res.Content.ReadAsStringAsync().Result); 37 | return null; 38 | } 39 | return res.Headers.Location.ToString(); 40 | } 41 | 42 | public AsciinemaApi() 43 | { 44 | _httpClient = new HttpClient(); 45 | _httpClient.DefaultRequestHeaders.Accept.Clear(); 46 | _httpClient.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(System.Net.Mime.MediaTypeNames.Application.Json)); 47 | 48 | _httpClient.DefaultRequestHeaders.Add("User-Agent",$"asciinema/2.0.0 {RuntimeFramework} {OperatingSystem}"); 49 | 50 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"user:{AppConfig.InstallId}"))); 51 | } 52 | 53 | public void Auth() 54 | { 55 | Console.WriteLine("Open the following URL in a web browser to link your" + 56 | $"install ID with your {ApiHost} user account:\n\n" + 57 | $"{AuthUrl}\n\n" + 58 | "This will associate all recordings uploaded from this machine " + 59 | "(past and future ones) to your account, " + 60 | $"and allow you to manage them (change title/theme, delete) at {ApiHost}."); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /PowerSession.Main/Api/IApiService.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Api 2 | { 3 | public interface IApiService 4 | { 5 | void Auth(); 6 | string Upload(string filePath); 7 | } 8 | } -------------------------------------------------------------------------------- /PowerSession.Main/AppConfig.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession 2 | { 3 | using System; 4 | using System.IO; 5 | using Newtonsoft.Json; 6 | 7 | public struct Config 8 | { 9 | [JsonProperty("install_id")] public string InstallId; 10 | } 11 | 12 | public static class AppConfig 13 | { 14 | private static readonly string HomeFolder = 15 | Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PowerSession"); 16 | private static readonly string ConfigFile = Path.Join(HomeFolder, "config.json"); 17 | private static readonly Config Config; 18 | 19 | public static string InstallId => Config.InstallId; 20 | 21 | static AppConfig() 22 | { 23 | if (!Directory.Exists(HomeFolder)) 24 | { 25 | Directory.CreateDirectory(HomeFolder); 26 | } 27 | 28 | if (File.Exists(ConfigFile)) 29 | { 30 | Config = JsonConvert.DeserializeObject(File.ReadAllText(ConfigFile)); 31 | } 32 | else 33 | { 34 | var installId = Guid.NewGuid(); 35 | Config = new Config 36 | { 37 | InstallId = installId.ToString() 38 | }; 39 | var json = JsonConvert.SerializeObject(Config); 40 | File.WriteAllText(ConfigFile, json); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /PowerSession.Main/Commands/AuthCommand.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Commands 2 | { 3 | public class AuthCommand : BaseCommand, ICommand 4 | { 5 | public int Execute() 6 | { 7 | Api.Auth(); 8 | return 0; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /PowerSession.Main/Commands/BaseCommand.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Commands 2 | { 3 | using System; 4 | using System.IO; 5 | using Api; 6 | using Main.Api; 7 | using static Main.ConPTY.Native.ConsoleApi; 8 | 9 | public interface ICommand 10 | { 11 | int Execute(); 12 | } 13 | 14 | public abstract class BaseCommand 15 | { 16 | protected static IApiService Api; 17 | 18 | protected BaseCommand() 19 | { 20 | Api = new AsciinemaApi(); 21 | } 22 | 23 | protected BaseCommand(bool enableAnsiEscape) : this() 24 | { 25 | if (enableAnsiEscape) 26 | { 27 | var stdout = GetStdHandle(STD_OUTPUT_HANDLE); 28 | if (GetConsoleMode(stdout, out var consoleMode)) 29 | { 30 | consoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; 31 | if (!SetConsoleMode(stdout, consoleMode)) 32 | throw new NotSupportedException("VIRTUAL_TERMINAL_PROCESSING"); 33 | } 34 | } 35 | } 36 | 37 | protected void Log(string text, TextWriter output = null) 38 | { 39 | if (output == null) output = Console.Out; 40 | output.WriteLine(text); 41 | } 42 | 43 | protected void LogInfo(string text) 44 | { 45 | Log($"\x1b[0;32mPowerSession: {text}\x1b[0m"); 46 | } 47 | 48 | protected void LogWarning(string text) 49 | { 50 | Log($"\x1b[0;33mPowerSession: {text}\x1b[0m"); 51 | } 52 | 53 | protected void LogError(string text) 54 | { 55 | Log($"\x1b[0;33mPowerSession: {text}\x1b[0m"); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /PowerSession.Main/Commands/PlayCommand.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PowerSession.Commands 4 | { 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Main.Commands; 9 | 10 | public struct PlayArgs 11 | { 12 | public string Filename; 13 | public bool EnableAnsiEscape; 14 | } 15 | 16 | public class PlayCommand : BaseCommand, ICommand 17 | { 18 | private readonly CancellationTokenSource _cancellationTokenSource; 19 | private readonly AutoResetEvent _dataReceivedEvent; 20 | private readonly AutoResetEvent _readEvent; 21 | 22 | private readonly CancellationToken _readlineCancellationToken; 23 | private readonly RecordSession _session; 24 | private ConsoleKeyInfo _consoleKeyInfo; 25 | 26 | public PlayCommand(PlayArgs args) : base(args.EnableAnsiEscape) 27 | { 28 | if (!File.Exists(args.Filename)) 29 | { 30 | Console.Out.WriteLine($"File {args.Filename} not found."); 31 | Environment.Exit(1); 32 | } 33 | _session = new RecordSession(args.Filename); 34 | 35 | _cancellationTokenSource = new CancellationTokenSource(); 36 | _readlineCancellationToken = _cancellationTokenSource.Token; 37 | _readEvent = new AutoResetEvent(false); 38 | _dataReceivedEvent = new AutoResetEvent(false); 39 | Task.Factory.StartNew(ReadKey, TaskCreationOptions.LongRunning); 40 | } 41 | 42 | public int Execute() 43 | { 44 | foreach (var line in _session.StdoutRelativeTime()) 45 | { 46 | _readEvent.Set(); 47 | var dataReceived = _dataReceivedEvent.WaitOne(TimeSpan.FromSeconds(line.Timestamp)); 48 | if (dataReceived) 49 | switch (_consoleKeyInfo.KeyChar) 50 | { 51 | case '\x3': //Control C 52 | _cancellationTokenSource.Cancel(); 53 | break; 54 | } 55 | 56 | Console.Out.Write(line.Content); 57 | } 58 | 59 | return 0; 60 | } 61 | 62 | private void ReadKey() 63 | { 64 | while (!_readlineCancellationToken.IsCancellationRequested) 65 | { 66 | _readEvent.WaitOne(); 67 | _consoleKeyInfo = Console.ReadKey(); 68 | _dataReceivedEvent.Set(); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /PowerSession.Main/Commands/RecordCommand.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Main.Commands 2 | { 3 | using ConPTY; 4 | using Newtonsoft.Json; 5 | using PowerSession.Commands; 6 | using System; 7 | using System.Collections; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | using Utils; 11 | 12 | public struct RecordHeader 13 | { 14 | [JsonProperty("version")] public int Version; 15 | [JsonProperty("width")] public int Width; 16 | [JsonProperty("height")] public int Height; 17 | [JsonProperty("timestamp")] public long Timestamp; 18 | [JsonProperty("env")] public IDictionary Environment; 19 | public bool Valid => Version == 2; 20 | } 21 | 22 | public struct RecordArgs 23 | { 24 | public string Filename; 25 | public string Command; 26 | public bool Overwrite; 27 | } 28 | 29 | public class RecordCommand : BaseCommand, ICommand 30 | { 31 | private readonly RecordArgs _args; 32 | private readonly string _command; 33 | private IDictionary _env; 34 | private string _filename; 35 | 36 | public RecordCommand(RecordArgs args, IDictionary env = null) 37 | { 38 | _filename = args.Filename; 39 | _command = args.Command; 40 | _env = env; 41 | _args = args; 42 | } 43 | 44 | public int Execute() 45 | { 46 | if (string.IsNullOrEmpty(_filename)) _filename = Path.GetTempFileName(); 47 | 48 | if (File.Exists(_filename)) 49 | if (_args.Overwrite) 50 | File.Delete(_filename); 51 | 52 | var fd = File.Create(_filename); 53 | fd.Dispose(); 54 | 55 | try 56 | { 57 | _record(_filename, _command); 58 | return 0; 59 | } 60 | catch (Exception e) 61 | { 62 | Console.WriteLine(e); 63 | throw; 64 | } 65 | } 66 | 67 | private static string GetTerm() 68 | { 69 | return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("WT_SESSION")) ? "ms-terminal" : 70 | Environment.GetEnvironmentVariable("TERM"); 71 | } 72 | 73 | private void _record(string filename, string command = null) 74 | { 75 | if (string.IsNullOrEmpty(command)) 76 | { 77 | command = Environment.GetEnvironmentVariable("SHELL") ?? "powershell.exe"; 78 | } 79 | 80 | _env ??= new Dictionary(); 81 | _env.Add("SHELL", Environment.GetEnvironmentVariable("SHELL") ?? "powershell.exe"); 82 | 83 | string term = GetTerm(); 84 | if (!string.IsNullOrWhiteSpace(term)) 85 | { 86 | _env.Add("TERM", term); 87 | } 88 | 89 | using var writer = new FileWriter(filename); 90 | var headerInfo = new RecordHeader 91 | { 92 | Version = 2, 93 | Width = Console.WindowWidth, 94 | Height = Console.WindowHeight, 95 | Environment = _env 96 | }; 97 | writer.SetHeader(headerInfo); 98 | 99 | var terminal = new Terminal(writer.GetInputStream(), writer.GetWriteStream(), width: headerInfo.Width, height: headerInfo.Height); 100 | terminal.Record(command, _env); 101 | Console.WriteLine("Record Finished"); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /PowerSession.Main/Commands/RecordSession.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Commands 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | using Main.Commands; 8 | using Newtonsoft.Json; 9 | 10 | struct SessionLine 11 | { 12 | public double Timestamp; 13 | public bool Stdout; 14 | public string Content; 15 | } 16 | 17 | internal class RecordSession : IDisposable 18 | { 19 | private string _filepath; 20 | private readonly RecordHeader _header; 21 | private readonly StreamReader _reader; 22 | 23 | internal RecordSession(string filepath) 24 | { 25 | _filepath = filepath; 26 | if (!File.Exists(filepath)) 27 | { 28 | throw new FileNotFoundException(filepath); 29 | } 30 | 31 | _reader = new StreamReader(File.OpenRead(filepath));File.OpenRead(filepath); 32 | 33 | var headerLine = _reader.ReadLine(); 34 | _header = JsonConvert.DeserializeObject(headerLine); 35 | if (!_header.Valid) 36 | { 37 | throw new InvalidDataException("Unsupported file format"); 38 | } 39 | } 40 | 41 | internal IEnumerable Lines() 42 | { 43 | while (!_reader.EndOfStream) 44 | { 45 | var line = _reader.ReadLine(); 46 | var lineData = JsonConvert.DeserializeObject>(line); 47 | if (lineData.Count != 3) throw new InvalidDataException("Invalid record data"); 48 | var rv = new SessionLine 49 | { 50 | Timestamp = Convert.ToDouble(lineData[0]), 51 | Stdout = (string) lineData[1] == "o", 52 | Content = (string) lineData[2] 53 | }; 54 | yield return rv; 55 | } 56 | } 57 | 58 | internal IEnumerable Stdout() 59 | { 60 | return Lines().Where(line => line.Stdout); 61 | } 62 | 63 | internal IEnumerable StdoutRelativeTime() 64 | { 65 | double previousTimestamp = 0; 66 | 67 | foreach (var line in Stdout()) 68 | { 69 | var newLine = new SessionLine 70 | { 71 | Timestamp = line.Timestamp - previousTimestamp, Content = line.Content, Stdout = line.Stdout 72 | }; 73 | previousTimestamp = line.Timestamp; 74 | yield return newLine; 75 | } 76 | } 77 | 78 | public void Dispose() 79 | { 80 | _reader?.Dispose(); 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /PowerSession.Main/Commands/UploadCommand.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Commands 2 | { 3 | public class UploadCommand: BaseCommand, ICommand 4 | { 5 | private readonly string _filePath; 6 | 7 | public UploadCommand(string filePath) : base(true) 8 | { 9 | _filePath = filePath; 10 | } 11 | public int Execute() 12 | { 13 | var result = Api.Upload(_filePath); 14 | if (!string.IsNullOrEmpty(result)) 15 | { 16 | LogInfo($"Result Url: {result}"); 17 | return 0; 18 | } 19 | 20 | return 1; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /PowerSession.Main/PowerSession.Main.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | PowerSession 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /PowerSession.Main/Utils/Writer.cs: -------------------------------------------------------------------------------- 1 | namespace PowerSession.Main.Utils 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.IO.Pipes; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Commands; 12 | using Newtonsoft.Json; 13 | 14 | public class FileWriter : IDisposable 15 | { 16 | private static readonly ManualResetEvent WaitForWriter = new ManualResetEvent(false); 17 | private readonly FileStream _fileStream; 18 | private readonly DateTimeOffset _startTimeStamp; 19 | private RecordHeader _header; 20 | 21 | public FileWriter(string output) 22 | { 23 | if (!File.Exists(output)) throw new FileNotFoundException(output); 24 | _fileStream = File.OpenWrite(output); 25 | _startTimeStamp = DateTimeOffset.Now; 26 | } 27 | 28 | public void Dispose() 29 | { 30 | WaitForWriter.WaitOne(); 31 | _fileStream?.Dispose(); 32 | } 33 | 34 | public void SetHeader(RecordHeader header) 35 | { 36 | header.Timestamp = _startTimeStamp.ToUnixTimeSeconds(); 37 | _header = header; 38 | } 39 | 40 | /// 41 | /// Record stdin 42 | /// 43 | /// 44 | public Stream GetInputStream() 45 | { 46 | var pipeServer = new AnonymousPipeServerStream(); 47 | var pipeClient = new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 48 | 49 | return pipeClient; 50 | } 51 | 52 | public Stream GetWriteStream() 53 | { 54 | var pipeServer = new AnonymousPipeServerStream(); 55 | var pipeClient = new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 56 | 57 | Task.Factory.StartNew(() => 58 | { 59 | using var sr = new StreamReader(pipeClient); 60 | using var sw = new StreamWriter(_fileStream, new UTF8Encoding(false)); 61 | sw.WriteLine(JsonConvert.SerializeObject(_header)); 62 | var buf = new char[1024]; 63 | int bytesRead; 64 | while ((bytesRead = sr.Read(buf, 0, buf.Length)) != 0) 65 | { 66 | var ts = DateTimeOffset.Now - _startTimeStamp; 67 | var chars = string.Join("", buf.Take(bytesRead).Select(c => c.ToString())); 68 | var data = new List {ts.TotalSeconds, "o", chars}; 69 | sw.WriteLine(JsonConvert.SerializeObject(data)); // asciinema compatible 70 | Console.Out.Write(buf.Take(bytesRead).ToArray()); 71 | } 72 | 73 | WaitForWriter.Set(); 74 | }, TaskCreationOptions.LongRunning); 75 | 76 | return pipeServer; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /PowerSession.Tests/PowerSession.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | PowerSession.Commands.Tests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /PowerSession.Tests/TestCommands.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace PowerSession.Main.Commands.Tests 4 | { 5 | using System; 6 | using System.IO; 7 | using System.IO.Pipes; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | public class CommandsTests 12 | { 13 | private RecordCommand _recordCommand; 14 | private string _filePath; 15 | private StreamWriter _stdinWriter; 16 | 17 | [SetUp] 18 | public void Setup() 19 | { 20 | var pipeServer = new AnonymousPipeServerStream(); 21 | var pipeClient = new AnonymousPipeClientStream(pipeServer.GetClientHandleAsString()); 22 | Console.SetIn(new StreamReader(pipeClient)); 23 | _stdinWriter = new StreamWriter(pipeServer); 24 | 25 | var tempFile = Path.GetTempFileName(); 26 | var recordArgs = new RecordArgs 27 | { 28 | Filename = tempFile 29 | }; 30 | _recordCommand = new RecordCommand(recordArgs); 31 | _filePath = tempFile; 32 | 33 | } 34 | 35 | [Test] 36 | [Ignore("TODO")] 37 | public void TestExecute() 38 | { 39 | var exitSend = new AutoResetEvent(false); 40 | Task.Factory.StartNew(() => 41 | { 42 | _recordCommand.Execute(); 43 | exitSend.Set(); 44 | }); 45 | 46 | _stdinWriter.Write("\x3"); 47 | exitSend.WaitOne(); 48 | StringAssert.Contains("PowerShell", File.ReadAllText(_filePath), "Record result should contain keyword"); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /PowerSession.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerSession.ConPTY", "PowerSession.ConPTY\PowerSession.ConPTY.csproj", "{0E1F00C7-4F6B-4A7D-9A2F-6AD18A3D2F0A}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerSession.Cli", "PowerSession.Cli\PowerSession.Cli.csproj", "{2FAE0812-EE84-4619-A21C-12288BB42544}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerSession.Main", "PowerSession.Main\PowerSession.Main.csproj", "{DEB7E1B3-9DC7-40BF-B37C-4335BE2376CA}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerSession.Tests", "PowerSession.Tests\PowerSession.Tests.csproj", "{ECA69182-3A02-4121-94BC-CC19DEFE0F4F}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {0E1F00C7-4F6B-4A7D-9A2F-6AD18A3D2F0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {0E1F00C7-4F6B-4A7D-9A2F-6AD18A3D2F0A}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {0E1F00C7-4F6B-4A7D-9A2F-6AD18A3D2F0A}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {0E1F00C7-4F6B-4A7D-9A2F-6AD18A3D2F0A}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {2FAE0812-EE84-4619-A21C-12288BB42544}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {2FAE0812-EE84-4619-A21C-12288BB42544}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {2FAE0812-EE84-4619-A21C-12288BB42544}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {2FAE0812-EE84-4619-A21C-12288BB42544}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {DEB7E1B3-9DC7-40BF-B37C-4335BE2376CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {DEB7E1B3-9DC7-40BF-B37C-4335BE2376CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {DEB7E1B3-9DC7-40BF-B37C-4335BE2376CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {DEB7E1B3-9DC7-40BF-B37C-4335BE2376CA}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {ECA69182-3A02-4121-94BC-CC19DEFE0F4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {ECA69182-3A02-4121-94BC-CC19DEFE0F4F}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {ECA69182-3A02-4121-94BC-CC19DEFE0F4F}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {ECA69182-3A02-4121-94BC-CC19DEFE0F4F}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerSession 2 | 3 | > Record a Session in PowerShell. 4 | 5 | **‼️ DEPRECATED Checkout the brand new Rust implementation: https://github.com/Watfaq/PowerSession-rs ‼️** 6 | 7 | PowerShell version of [asciinema](https://github.com/asciinema/asciinema) based on [Windows Pseudo Console(ConPTY)](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) 8 | 9 | ## Checkout A Demo 10 | 11 | [![asciicast](https://asciinema.org/a/272866.svg)](https://asciinema.org/a/272866) 12 | 13 | ## Compatibilities 14 | 15 | * The output is comptible with asciinema v2 standard and can be played by `ascinnema`. 16 | * The `auth` and `upload` functionalities are agains `asciinema.org`. 17 | 18 | ## Installation 19 | 20 | ### using dotnet tool 21 | 22 | ``` 23 | > dotnet tool install --global PowerSession 24 | ``` 25 | 26 | ### Using [Scoop](https://scoop.sh) 27 | 28 | ``` 29 | > scoop install PowerSession 30 | ``` 31 | 32 | ### Manual Download 33 | 34 | Download `PowerSession.exe` at Release Page https://github.com/ibigbug/PowerSession/releases 35 | 36 | 37 | ## Usage 38 | 39 | ### Record 40 | 41 | ```PowerShell 42 | $ PowerSession.Cli.exe rec a.txt 43 | ``` 44 | 45 | ### Play 46 | 47 | ```PowerShell 48 | $ PowerSession.Cli.exe play a.txt 49 | ``` 50 | 51 | ### Auth 52 | 53 | ```PowerShell 54 | $ PowerSession.Cli.exe auth 55 | ``` 56 | 57 | ### Upload 58 | 59 | ```PowerShell 60 | $ PowerSession.Cli.exe upload a.txt 61 | ``` 62 | 63 | ### Get Help 64 | 65 | ```PowerShell 66 | $ PowerSession.exe rec -h 67 | 68 | rec: 69 | Record and save a session 70 | 71 | Usage: 72 | PowerSession rec [options] 73 | 74 | Arguments: 75 | The filename to save the record 76 | 77 | Options: 78 | -c, --command The command to record, default to be powershell.exe 79 | ``` 80 | 81 | ## Supporters 82 | - [GitBook](https://www.gitbook.com/) Community License 83 | --------------------------------------------------------------------------------