├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── core ├── CMakeLists.txt ├── conptyprocess.cpp ├── conptyprocess.h ├── iptyprocess.h ├── ptyqt.cpp ├── ptyqt.h ├── unixptyprocess.cpp ├── unixptyprocess.h ├── winptyprocess.cpp └── winptyprocess.h ├── examples ├── CMakeLists.txt └── xtermjs │ ├── CMakeLists.txt │ ├── index.html │ ├── screens │ ├── far_manager_cmd_windows.png │ └── midnight_commander_bash_unix.png │ ├── style.css │ └── xtermjs.cpp └── tests ├── CMakeLists.txt └── ptyqt_tests.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeCache.txt 2 | CMakeFiles 3 | CMakeScripts 4 | Testing 5 | Makefile 6 | cmake_install.cmake 7 | install_manifest.txt 8 | compile_commands.json 9 | CTestTestfile.cmake 10 | /examples/xtermjs/node_modules/xterm/LICENSE 11 | /examples/xtermjs/node_modules/xterm/README.md 12 | /examples/xtermjs/node_modules/xterm/dist/addons/attach/attach.js 13 | /examples/xtermjs/node_modules/xterm/dist/addons/attach/attach.js.map 14 | /examples/xtermjs/node_modules/xterm/dist/addons/fit/fit.js 15 | /examples/xtermjs/node_modules/xterm/dist/addons/fit/fit.js.map 16 | /examples/xtermjs/node_modules/xterm/dist/addons/fullscreen/fullscreen.css 17 | /examples/xtermjs/node_modules/xterm/dist/addons/fullscreen/fullscreen.js 18 | /examples/xtermjs/node_modules/xterm/dist/addons/fullscreen/fullscreen.js.map 19 | /examples/xtermjs/node_modules/xterm/dist/addons/search/search.js 20 | /examples/xtermjs/node_modules/xterm/dist/addons/search/search.js.map 21 | /examples/xtermjs/node_modules/xterm/dist/addons/terminado/terminado.js 22 | /examples/xtermjs/node_modules/xterm/dist/addons/terminado/terminado.js.map 23 | /examples/xtermjs/node_modules/xterm/dist/addons/webLinks/webLinks.js 24 | /examples/xtermjs/node_modules/xterm/dist/addons/webLinks/webLinks.js.map 25 | /examples/xtermjs/node_modules/xterm/dist/addons/winptyCompat/winptyCompat.js 26 | /examples/xtermjs/node_modules/xterm/dist/addons/winptyCompat/winptyCompat.js.map 27 | /examples/xtermjs/node_modules/xterm/dist/addons/zmodem/zmodem.js 28 | /examples/xtermjs/node_modules/xterm/dist/addons/zmodem/zmodem.js.map 29 | /examples/xtermjs/node_modules/xterm/dist/xterm.css 30 | /examples/xtermjs/node_modules/xterm/dist/xterm.js 31 | /examples/xtermjs/node_modules/xterm/dist/xterm.js.map 32 | /examples/xtermjs/node_modules/xterm/gulpfile.js 33 | /examples/xtermjs/node_modules/xterm/lib/AccessibilityManager.js 34 | /examples/xtermjs/node_modules/xterm/lib/AccessibilityManager.js.map 35 | /examples/xtermjs/node_modules/xterm/lib/Buffer.js 36 | /examples/xtermjs/node_modules/xterm/lib/Buffer.js.map 37 | /examples/xtermjs/node_modules/xterm/lib/BufferLine.js 38 | /examples/xtermjs/node_modules/xterm/lib/BufferLine.js.map 39 | /examples/xtermjs/node_modules/xterm/lib/BufferReflow.js 40 | /examples/xtermjs/node_modules/xterm/lib/BufferReflow.js.map 41 | /examples/xtermjs/node_modules/xterm/lib/BufferSet.js 42 | /examples/xtermjs/node_modules/xterm/lib/BufferSet.js.map 43 | /examples/xtermjs/node_modules/xterm/lib/CharWidth.js 44 | /examples/xtermjs/node_modules/xterm/lib/CharWidth.js.map 45 | /examples/xtermjs/node_modules/xterm/lib/CompositionHelper.js 46 | /examples/xtermjs/node_modules/xterm/lib/CompositionHelper.js.map 47 | /examples/xtermjs/node_modules/xterm/lib/EscapeSequenceParser.js 48 | /examples/xtermjs/node_modules/xterm/lib/EscapeSequenceParser.js.map 49 | /examples/xtermjs/node_modules/xterm/lib/InputHandler.js 50 | /examples/xtermjs/node_modules/xterm/lib/InputHandler.js.map 51 | /examples/xtermjs/node_modules/xterm/lib/Linkifier.js 52 | /examples/xtermjs/node_modules/xterm/lib/Linkifier.js.map 53 | /examples/xtermjs/node_modules/xterm/lib/SelectionManager.js 54 | /examples/xtermjs/node_modules/xterm/lib/SelectionManager.js.map 55 | /examples/xtermjs/node_modules/xterm/lib/SelectionModel.js 56 | /examples/xtermjs/node_modules/xterm/lib/SelectionModel.js.map 57 | /examples/xtermjs/node_modules/xterm/lib/SoundManager.js 58 | /examples/xtermjs/node_modules/xterm/lib/SoundManager.js.map 59 | /examples/xtermjs/node_modules/xterm/lib/Strings.js 60 | /examples/xtermjs/node_modules/xterm/lib/Strings.js.map 61 | /examples/xtermjs/node_modules/xterm/lib/Terminal.integration.js 62 | /examples/xtermjs/node_modules/xterm/lib/Terminal.integration.js.map 63 | /examples/xtermjs/node_modules/xterm/lib/Terminal.js 64 | /examples/xtermjs/node_modules/xterm/lib/Terminal.js.map 65 | /examples/xtermjs/node_modules/xterm/lib/Types.js 66 | /examples/xtermjs/node_modules/xterm/lib/Types.js.map 67 | /examples/xtermjs/node_modules/xterm/lib/Viewport.js 68 | /examples/xtermjs/node_modules/xterm/lib/Viewport.js.map 69 | /examples/xtermjs/node_modules/xterm/lib/addons/attach/Interfaces.d.ts 70 | /examples/xtermjs/node_modules/xterm/lib/addons/attach/Interfaces.js 71 | /examples/xtermjs/node_modules/xterm/lib/addons/attach/Interfaces.js.map 72 | /examples/xtermjs/node_modules/xterm/lib/addons/attach/attach.d.ts 73 | /examples/xtermjs/node_modules/xterm/lib/addons/attach/attach.js 74 | /examples/xtermjs/node_modules/xterm/lib/addons/attach/attach.js.map 75 | /examples/xtermjs/node_modules/xterm/lib/addons/fit/fit.d.ts 76 | /examples/xtermjs/node_modules/xterm/lib/addons/fit/fit.js 77 | /examples/xtermjs/node_modules/xterm/lib/addons/fit/fit.js.map 78 | /examples/xtermjs/node_modules/xterm/lib/addons/fullscreen/fullscreen.css 79 | /examples/xtermjs/node_modules/xterm/lib/addons/fullscreen/fullscreen.d.ts 80 | /examples/xtermjs/node_modules/xterm/lib/addons/fullscreen/fullscreen.js 81 | /examples/xtermjs/node_modules/xterm/lib/addons/fullscreen/fullscreen.js.map 82 | /examples/xtermjs/node_modules/xterm/lib/addons/search/Interfaces.d.ts 83 | /examples/xtermjs/node_modules/xterm/lib/addons/search/Interfaces.js 84 | /examples/xtermjs/node_modules/xterm/lib/addons/search/Interfaces.js.map 85 | /examples/xtermjs/node_modules/xterm/lib/addons/search/SearchHelper.d.ts 86 | /examples/xtermjs/node_modules/xterm/lib/addons/search/SearchHelper.js 87 | /examples/xtermjs/node_modules/xterm/lib/addons/search/SearchHelper.js.map 88 | /examples/xtermjs/node_modules/xterm/lib/addons/search/search.d.ts 89 | /examples/xtermjs/node_modules/xterm/lib/addons/search/search.js 90 | /examples/xtermjs/node_modules/xterm/lib/addons/search/search.js.map 91 | /examples/xtermjs/node_modules/xterm/lib/addons/terminado/Interfaces.d.ts 92 | /examples/xtermjs/node_modules/xterm/lib/addons/terminado/Interfaces.js 93 | /examples/xtermjs/node_modules/xterm/lib/addons/terminado/Interfaces.js.map 94 | /examples/xtermjs/node_modules/xterm/lib/addons/terminado/terminado.d.ts 95 | /examples/xtermjs/node_modules/xterm/lib/addons/terminado/terminado.js 96 | /examples/xtermjs/node_modules/xterm/lib/addons/terminado/terminado.js.map 97 | /examples/xtermjs/node_modules/xterm/lib/addons/webLinks/webLinks.d.ts 98 | /examples/xtermjs/node_modules/xterm/lib/addons/webLinks/webLinks.js 99 | /examples/xtermjs/node_modules/xterm/lib/addons/webLinks/webLinks.js.map 100 | /examples/xtermjs/node_modules/xterm/lib/addons/winptyCompat/Interfaces.d.ts 101 | /examples/xtermjs/node_modules/xterm/lib/addons/winptyCompat/Interfaces.js 102 | /examples/xtermjs/node_modules/xterm/lib/addons/winptyCompat/Interfaces.js.map 103 | /examples/xtermjs/node_modules/xterm/lib/addons/winptyCompat/winptyCompat.d.ts 104 | /examples/xtermjs/node_modules/xterm/lib/addons/winptyCompat/winptyCompat.js 105 | /examples/xtermjs/node_modules/xterm/lib/addons/winptyCompat/winptyCompat.js.map 106 | /examples/xtermjs/node_modules/xterm/lib/addons/zmodem/zmodem.d.ts 107 | /examples/xtermjs/node_modules/xterm/lib/addons/zmodem/zmodem.js 108 | /examples/xtermjs/node_modules/xterm/lib/addons/zmodem/zmodem.js.map 109 | /examples/xtermjs/node_modules/xterm/lib/common/CircularList.js 110 | /examples/xtermjs/node_modules/xterm/lib/common/CircularList.js.map 111 | /examples/xtermjs/node_modules/xterm/lib/common/Clone.js 112 | /examples/xtermjs/node_modules/xterm/lib/common/Clone.js.map 113 | /examples/xtermjs/node_modules/xterm/lib/common/EventEmitter.js 114 | /examples/xtermjs/node_modules/xterm/lib/common/EventEmitter.js.map 115 | /examples/xtermjs/node_modules/xterm/lib/common/Lifecycle.js 116 | /examples/xtermjs/node_modules/xterm/lib/common/Lifecycle.js.map 117 | /examples/xtermjs/node_modules/xterm/lib/common/TypedArrayUtils.js 118 | /examples/xtermjs/node_modules/xterm/lib/common/TypedArrayUtils.js.map 119 | /examples/xtermjs/node_modules/xterm/lib/common/Types.js 120 | /examples/xtermjs/node_modules/xterm/lib/common/Types.js.map 121 | /examples/xtermjs/node_modules/xterm/lib/common/data/EscapeSequences.js 122 | /examples/xtermjs/node_modules/xterm/lib/common/data/EscapeSequences.js.map 123 | /examples/xtermjs/node_modules/xterm/lib/core/Platform.js 124 | /examples/xtermjs/node_modules/xterm/lib/core/Platform.js.map 125 | /examples/xtermjs/node_modules/xterm/lib/core/Types.js 126 | /examples/xtermjs/node_modules/xterm/lib/core/Types.js.map 127 | /examples/xtermjs/node_modules/xterm/lib/core/data/Charsets.js 128 | /examples/xtermjs/node_modules/xterm/lib/core/data/Charsets.js.map 129 | /examples/xtermjs/node_modules/xterm/lib/core/input/Keyboard.js 130 | /examples/xtermjs/node_modules/xterm/lib/core/input/Keyboard.js.map 131 | /examples/xtermjs/node_modules/xterm/lib/core/input/TextDecoder.js 132 | /examples/xtermjs/node_modules/xterm/lib/core/input/TextDecoder.js.map 133 | /examples/xtermjs/node_modules/xterm/lib/handlers/AltClickHandler.js 134 | /examples/xtermjs/node_modules/xterm/lib/handlers/AltClickHandler.js.map 135 | /examples/xtermjs/node_modules/xterm/lib/public/Terminal.js 136 | /examples/xtermjs/node_modules/xterm/lib/public/Terminal.js.map 137 | /examples/xtermjs/node_modules/xterm/lib/renderer/BaseRenderLayer.js 138 | /examples/xtermjs/node_modules/xterm/lib/renderer/BaseRenderLayer.js.map 139 | /examples/xtermjs/node_modules/xterm/lib/renderer/CharacterJoinerRegistry.js 140 | /examples/xtermjs/node_modules/xterm/lib/renderer/CharacterJoinerRegistry.js.map 141 | /examples/xtermjs/node_modules/xterm/lib/renderer/ColorManager.js 142 | /examples/xtermjs/node_modules/xterm/lib/renderer/ColorManager.js.map 143 | /examples/xtermjs/node_modules/xterm/lib/renderer/CursorRenderLayer.js 144 | /examples/xtermjs/node_modules/xterm/lib/renderer/CursorRenderLayer.js.map 145 | /examples/xtermjs/node_modules/xterm/lib/renderer/GridCache.js 146 | /examples/xtermjs/node_modules/xterm/lib/renderer/GridCache.js.map 147 | /examples/xtermjs/node_modules/xterm/lib/renderer/LinkRenderLayer.js 148 | /examples/xtermjs/node_modules/xterm/lib/renderer/LinkRenderLayer.js.map 149 | /examples/xtermjs/node_modules/xterm/lib/renderer/Renderer.js 150 | /examples/xtermjs/node_modules/xterm/lib/renderer/Renderer.js.map 151 | /examples/xtermjs/node_modules/xterm/lib/renderer/SelectionRenderLayer.js 152 | /examples/xtermjs/node_modules/xterm/lib/renderer/SelectionRenderLayer.js.map 153 | /examples/xtermjs/node_modules/xterm/lib/renderer/TextRenderLayer.js 154 | /examples/xtermjs/node_modules/xterm/lib/renderer/TextRenderLayer.js.map 155 | /examples/xtermjs/node_modules/xterm/lib/renderer/Types.js 156 | /examples/xtermjs/node_modules/xterm/lib/renderer/Types.js.map 157 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/BaseCharAtlas.js 158 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/BaseCharAtlas.js.map 159 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/CharAtlasCache.js 160 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/CharAtlasCache.js.map 161 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/CharAtlasGenerator.js 162 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/CharAtlasGenerator.js.map 163 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/CharAtlasUtils.js 164 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/CharAtlasUtils.js.map 165 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/DynamicCharAtlas.js 166 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/DynamicCharAtlas.js.map 167 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/LRUMap.js 168 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/LRUMap.js.map 169 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/NoneCharAtlas.js 170 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/NoneCharAtlas.js.map 171 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/StaticCharAtlas.js 172 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/StaticCharAtlas.js.map 173 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/Types.js 174 | /examples/xtermjs/node_modules/xterm/lib/renderer/atlas/Types.js.map 175 | /examples/xtermjs/node_modules/xterm/lib/renderer/dom/DomRenderer.js 176 | /examples/xtermjs/node_modules/xterm/lib/renderer/dom/DomRenderer.js.map 177 | /examples/xtermjs/node_modules/xterm/lib/renderer/dom/DomRendererRowFactory.js 178 | /examples/xtermjs/node_modules/xterm/lib/renderer/dom/DomRendererRowFactory.js.map 179 | /examples/xtermjs/node_modules/xterm/lib/ui/CharMeasure.js 180 | /examples/xtermjs/node_modules/xterm/lib/ui/CharMeasure.js.map 181 | /examples/xtermjs/node_modules/xterm/lib/ui/Clipboard.js 182 | /examples/xtermjs/node_modules/xterm/lib/ui/Clipboard.js.map 183 | /examples/xtermjs/node_modules/xterm/lib/ui/Lifecycle.js 184 | /examples/xtermjs/node_modules/xterm/lib/ui/Lifecycle.js.map 185 | /examples/xtermjs/node_modules/xterm/lib/ui/MouseHelper.js 186 | /examples/xtermjs/node_modules/xterm/lib/ui/MouseHelper.js.map 187 | /examples/xtermjs/node_modules/xterm/lib/ui/MouseZoneManager.js 188 | /examples/xtermjs/node_modules/xterm/lib/ui/MouseZoneManager.js.map 189 | /examples/xtermjs/node_modules/xterm/lib/ui/RenderDebouncer.js 190 | /examples/xtermjs/node_modules/xterm/lib/ui/RenderDebouncer.js.map 191 | /examples/xtermjs/node_modules/xterm/lib/ui/ScreenDprMonitor.js 192 | /examples/xtermjs/node_modules/xterm/lib/ui/ScreenDprMonitor.js.map 193 | /examples/xtermjs/node_modules/xterm/lib/ui/Types.js 194 | /examples/xtermjs/node_modules/xterm/lib/ui/Types.js.map 195 | /examples/xtermjs/node_modules/xterm/lib/xterm.css 196 | /examples/xtermjs/node_modules/xterm/lib/xterm.js 197 | /examples/xtermjs/node_modules/xterm/lib/xterm.js.map 198 | /examples/xtermjs/node_modules/xterm/package.json 199 | /examples/xtermjs/node_modules/xterm/src/AccessibilityManager.ts 200 | /examples/xtermjs/node_modules/xterm/src/Buffer.ts 201 | /examples/xtermjs/node_modules/xterm/src/BufferLine.ts 202 | /examples/xtermjs/node_modules/xterm/src/BufferReflow.ts 203 | /examples/xtermjs/node_modules/xterm/src/BufferSet.ts 204 | /examples/xtermjs/node_modules/xterm/src/CharWidth.ts 205 | /examples/xtermjs/node_modules/xterm/src/CompositionHelper.ts 206 | /examples/xtermjs/node_modules/xterm/src/EscapeSequenceParser.ts 207 | /examples/xtermjs/node_modules/xterm/src/InputHandler.ts 208 | /examples/xtermjs/node_modules/xterm/src/Linkifier.ts 209 | /examples/xtermjs/node_modules/xterm/src/SelectionManager.ts 210 | /examples/xtermjs/node_modules/xterm/src/SelectionModel.ts 211 | /examples/xtermjs/node_modules/xterm/src/SoundManager.ts 212 | /examples/xtermjs/node_modules/xterm/src/Strings.ts 213 | /examples/xtermjs/node_modules/xterm/src/Terminal.integration.ts 214 | /examples/xtermjs/node_modules/xterm/src/Terminal.ts 215 | /examples/xtermjs/node_modules/xterm/src/Types.ts 216 | /examples/xtermjs/node_modules/xterm/src/Viewport.ts 217 | /examples/xtermjs/node_modules/xterm/src/addons/attach/Interfaces.ts 218 | /examples/xtermjs/node_modules/xterm/src/addons/attach/attach.ts 219 | /examples/xtermjs/node_modules/xterm/src/addons/attach/package.json 220 | /examples/xtermjs/node_modules/xterm/src/addons/attach/tsconfig.json 221 | /examples/xtermjs/node_modules/xterm/src/addons/fit/README.md 222 | /examples/xtermjs/node_modules/xterm/src/addons/fit/fit.ts 223 | /examples/xtermjs/node_modules/xterm/src/addons/fit/package.json 224 | /examples/xtermjs/node_modules/xterm/src/addons/fit/tsconfig.json 225 | /examples/xtermjs/node_modules/xterm/src/addons/fullscreen/fullscreen.css 226 | /examples/xtermjs/node_modules/xterm/src/addons/fullscreen/fullscreen.ts 227 | /examples/xtermjs/node_modules/xterm/src/addons/fullscreen/package.json 228 | /examples/xtermjs/node_modules/xterm/src/addons/fullscreen/tsconfig.json 229 | /examples/xtermjs/node_modules/xterm/src/addons/search/Interfaces.ts 230 | /examples/xtermjs/node_modules/xterm/src/addons/search/SearchHelper.ts 231 | /examples/xtermjs/node_modules/xterm/src/addons/search/package.json 232 | /examples/xtermjs/node_modules/xterm/src/addons/search/search.ts 233 | /examples/xtermjs/node_modules/xterm/src/addons/search/tsconfig.json 234 | /examples/xtermjs/node_modules/xterm/src/addons/terminado/Interfaces.ts 235 | /examples/xtermjs/node_modules/xterm/src/addons/terminado/package.json 236 | /examples/xtermjs/node_modules/xterm/src/addons/terminado/terminado.ts 237 | /examples/xtermjs/node_modules/xterm/src/addons/terminado/tsconfig.json 238 | /examples/xtermjs/node_modules/xterm/src/addons/webLinks/package.json 239 | /examples/xtermjs/node_modules/xterm/src/addons/webLinks/tsconfig.json 240 | /examples/xtermjs/node_modules/xterm/src/addons/webLinks/webLinks.ts 241 | /examples/xtermjs/node_modules/xterm/src/addons/winptyCompat/Interfaces.ts 242 | /examples/xtermjs/node_modules/xterm/src/addons/winptyCompat/package.json 243 | /examples/xtermjs/node_modules/xterm/src/addons/winptyCompat/tsconfig.json 244 | /examples/xtermjs/node_modules/xterm/src/addons/winptyCompat/winptyCompat.ts 245 | /examples/xtermjs/node_modules/xterm/src/addons/zmodem/package.json 246 | /examples/xtermjs/node_modules/xterm/src/addons/zmodem/tsconfig.json 247 | /examples/xtermjs/node_modules/xterm/src/addons/zmodem/zmodem.ts 248 | /examples/xtermjs/node_modules/xterm/src/common/CircularList.ts 249 | /examples/xtermjs/node_modules/xterm/src/common/Clone.ts 250 | /examples/xtermjs/node_modules/xterm/src/common/EventEmitter.ts 251 | /examples/xtermjs/node_modules/xterm/src/common/Lifecycle.ts 252 | /examples/xtermjs/node_modules/xterm/src/common/TypedArrayUtils.ts 253 | /examples/xtermjs/node_modules/xterm/src/common/Types.ts 254 | /examples/xtermjs/node_modules/xterm/src/common/data/EscapeSequences.ts 255 | /examples/xtermjs/node_modules/xterm/src/common/tsconfig.json 256 | /examples/xtermjs/node_modules/xterm/src/core/Platform.ts 257 | /examples/xtermjs/node_modules/xterm/src/core/Types.ts 258 | /examples/xtermjs/node_modules/xterm/src/core/data/Charsets.ts 259 | /examples/xtermjs/node_modules/xterm/src/core/input/Keyboard.ts 260 | /examples/xtermjs/node_modules/xterm/src/core/input/TextDecoder.ts 261 | /examples/xtermjs/node_modules/xterm/src/core/tsconfig.json 262 | /examples/xtermjs/node_modules/xterm/src/handlers/AltClickHandler.ts 263 | /examples/xtermjs/node_modules/xterm/src/public/Terminal.ts 264 | /examples/xtermjs/node_modules/xterm/src/renderer/BaseRenderLayer.ts 265 | /examples/xtermjs/node_modules/xterm/src/renderer/CharacterJoinerRegistry.ts 266 | /examples/xtermjs/node_modules/xterm/src/renderer/ColorManager.ts 267 | /examples/xtermjs/node_modules/xterm/src/renderer/CursorRenderLayer.ts 268 | /examples/xtermjs/node_modules/xterm/src/renderer/GridCache.ts 269 | /examples/xtermjs/node_modules/xterm/src/renderer/LinkRenderLayer.ts 270 | /examples/xtermjs/node_modules/xterm/src/renderer/Renderer.ts 271 | /examples/xtermjs/node_modules/xterm/src/renderer/SelectionRenderLayer.ts 272 | /examples/xtermjs/node_modules/xterm/src/renderer/TextRenderLayer.ts 273 | /examples/xtermjs/node_modules/xterm/src/renderer/Types.ts 274 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/BaseCharAtlas.ts 275 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/CharAtlasCache.ts 276 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/CharAtlasGenerator.ts 277 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/CharAtlasUtils.ts 278 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/DynamicCharAtlas.ts 279 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/LRUMap.ts 280 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/NoneCharAtlas.ts 281 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/StaticCharAtlas.ts 282 | /examples/xtermjs/node_modules/xterm/src/renderer/atlas/Types.ts 283 | /examples/xtermjs/node_modules/xterm/src/renderer/dom/DomRenderer.ts 284 | /examples/xtermjs/node_modules/xterm/src/renderer/dom/DomRendererRowFactory.ts 285 | /examples/xtermjs/node_modules/xterm/src/ui/CharMeasure.ts 286 | /examples/xtermjs/node_modules/xterm/src/ui/Clipboard.ts 287 | /examples/xtermjs/node_modules/xterm/src/ui/Lifecycle.ts 288 | /examples/xtermjs/node_modules/xterm/src/ui/MouseHelper.ts 289 | /examples/xtermjs/node_modules/xterm/src/ui/MouseZoneManager.ts 290 | /examples/xtermjs/node_modules/xterm/src/ui/RenderDebouncer.ts 291 | /examples/xtermjs/node_modules/xterm/src/ui/ScreenDprMonitor.ts 292 | /examples/xtermjs/node_modules/xterm/src/ui/Types.ts 293 | /examples/xtermjs/node_modules/xterm/src/xterm.css 294 | /examples/xtermjs/node_modules/xterm/src/xterm.ts 295 | /examples/xtermjs/node_modules/xterm/tsconfig.json 296 | /examples/xtermjs/node_modules/xterm/tslint.json 297 | /examples/xtermjs/node_modules/xterm/typings/xterm.d.ts 298 | /CMakeLists.txt.user 299 | /examples/xtermjs/package-lock.json 300 | /include/iptyprocess.h 301 | /include/ptyqt.h 302 | /lib/ptyqt.lib 303 | build_android/ 304 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "qt-android-cmake"] 2 | path = qt-android-cmake 3 | url = https://github.com/ptyio/qt-android-cmake.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2.0) 2 | 3 | project(ptyqt VERSION 0.6.5 LANGUAGES CXX) 4 | 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 7 | set(CMAKE_AUTOMOC ON) 8 | 9 | #params 10 | #available params: 11 | # - NO_BUILD_TESTS=1 12 | # - NO_BUILD_EXAMPLES=1 13 | IF("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") 14 | set(PTYQT_DEBUG TRUE) 15 | add_definitions(-DPTYQT_DEBUG) 16 | else() 17 | set(PTYQT_DEBUG FALSE) 18 | endif() 19 | 20 | if("${BUILD_TYPE}" STREQUAL "SHARED") 21 | set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) 22 | message("Build type: ${BUILD_TYPE}") 23 | endif() 24 | 25 | # Android 26 | if (ANDROID) 27 | message(STATUS "Android build detected") 28 | if ("$ENV{QT_ANDROID_PATH}" STREQUAL "") 29 | message(FATAL_ERROR "QT_ANDROID_PATH not defined") 30 | endif() 31 | 32 | include(qt-android-cmake/AddQtAndroidApk.cmake) 33 | 34 | set(QT_ANDROID_SOURCE_DIR ${QT_ANDROID_SOURCE_DIR} CACHE STRING "Source directory of AddQtAndroidApk.cmake") 35 | set(QT_ANDROID_QT_ROOT ${QT_ANDROID_QT_ROOT} CACHE STRING "Qt SDK root folder") 36 | set(QT_ANDROID_SDK_ROOT ${QT_ANDROID_SDK_ROOT} CACHE STRING "" FORCE) 37 | set(QT_ANDROID_NDK_ROOT ${QT_ANDROID_NDK_ROOT} CACHE STRING "" FORCE) 38 | endif() 39 | 40 | # Windows 41 | if (MSVC) 42 | #target arch for find winpty libs 43 | if ("${TARGET_ARCH}" STREQUAL "x86_amd64" OR "${TARGET_ARCH}" STREQUAL "") 44 | set(TARGET_ARCH x64) # x64 45 | endif() 46 | if ("${VCPKG_TARGET_TRIPLET}" MATCHES "x86") 47 | set(TARGET_ARCH x86) #x86 48 | endif() 49 | endif() 50 | 51 | #dependencies 52 | find_package(Qt5Core REQUIRED) 53 | 54 | if (MSVC) 55 | find_library(WINPTY_LIBRARIES NAMES winpty) 56 | find_package(Qt5Network REQUIRED) 57 | 58 | message("WINPTY_LIBRARIES ${WINPTY_LIBRARIES}") 59 | 60 | if ("${WINPTY_LIBRARIES}" STREQUAL "") 61 | message(FATAL "WinPty libs not set!") 62 | endif() 63 | endif() 64 | 65 | #install 66 | set(PTYQT_INSTALL_INCLUDE_DIR ${CMAKE_INSTALL_PREFIX}/include) 67 | set(PTYQT_INSTALL_BIN_DIR ${CMAKE_INSTALL_PREFIX}/bin) 68 | set(PTYQT_INSTALL_LIB_DIR ${CMAKE_INSTALL_PREFIX}/lib) 69 | 70 | set(PTYQTLIB_HEADERS_DIR ${PROJECT_SOURCE_DIR}/core) 71 | 72 | include_directories(${PTYQT_INSTALL_INCLUDE_DIR}) 73 | include_directories(${PTYQTLIB_HEADERS_DIR}) 74 | if (MSVC) 75 | include_directories(${WINPTY_ROOT_DIR}/include) 76 | endif() 77 | 78 | #sub projects 79 | add_subdirectory(core) 80 | 81 | if (NOT "${NO_BUILD_TESTS}" STREQUAL "1") 82 | enable_testing() 83 | add_subdirectory(tests) 84 | endif() 85 | 86 | if (NOT "${NO_BUILD_EXAMPLES}" STREQUAL "1") 87 | add_subdirectory(examples) 88 | endif() 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vitaly Petrov, v31337@gmail.com 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pty-Qt - C++ library for work with PseudoTerminals 2 | 3 | Pty-Qt is small library for access to console applications by pseudo-terminal interface on Mac, Linux and Windows. On Mac and Linux it uses standard PseudoTerminal API and on Windows it uses WinPty(prefer) or ConPty. 4 | 5 | **NOTE** Versions >= 0.6.0 distribute by vcpkg. Versions 0.5.x can be built manually by README manual. 6 | 7 | **NOTE 2** Vcpkg PR and build status for all platforms: https://github.com/microsoft/vcpkg/pull/21440 8 | 9 | ## Pre-Requirements and build 10 | - ConPty part works only on Windows 10 >= 1903 (build > 18309) and can be built only with Windows SDK >= 10.0.18346.0 11 | - WinPty part requires winpty sdk to build and winpty.dll and winpty-agent.exe to be deployed with target application. WinPty works on Windows XP and later (depended on build SDK: vc140 / vc140_xp). 12 | - UnixPty part works on both Linux/Mac versions, because it based on standard POSIX pseudo terminals API 13 | - Target platforms: x86 or x64 14 | - Required Qt >= 5.10 15 | - On Windows should be installed: Git for Windows, Visual Studio >=2015 16 | 17 | ### Build on Windows (cmd.exe) 18 | ```sh 19 | git clone https://github.com/microsoft/vcpkg 20 | cd vcpkg 21 | .\bootstrap-vcpkg.bat 22 | .\vcpkg.exe install ptyqt 23 | ``` 24 | 25 | ### Build on Linux/MacOS/Other UNIX 26 | ```sh 27 | git clone https://github.com/microsoft/vcpkg 28 | cd vcpkg 29 | ./bootstrap-vcpkg.sh 30 | ./vcpkg install ptyqt 31 | ``` 32 | 33 | ## Usage 34 | Standard way: build and install library then link it to your project and check examples for sample code. 35 | 36 | For example, this code snipped works on Windows, Linux and Mac and make interface from Pty to XTermJS: 37 | ```cpp 38 | #include 39 | #include 40 | #include 41 | #include "ptyqt.h" 42 | #include 43 | #include 44 | #include 45 | 46 | #define PORT 4242 47 | 48 | #define COLS 87 49 | #define ROWS 26 50 | 51 | int main(int argc, char *argv[]) 52 | { 53 | QCoreApplication app(argc, argv); 54 | 55 | //start WebSockets server for receive connections from xterm.js 56 | QWebSocketServer wsServer("TestServer", QWebSocketServer::NonSecureMode); 57 | if (!wsServer.listen(QHostAddress::Any, PORT)) 58 | return 1; 59 | 60 | QMap sessions; 61 | 62 | //create new session on new connection 63 | QObject::connect(&wsServer, &QWebSocketServer::newConnection, [&wsServer, &sessions]() 64 | { 65 | //handle new connection 66 | QWebSocket *wSocket = wsServer.nextPendingConnection(); 67 | 68 | //use cmd.exe or bash, depends on target platform 69 | IPtyProcess::PtyType ptyType = IPtyProcess::WinPty; 70 | qint32 buildNumber = QSysInfo::kernelVersion().split(".").last().toInt(); 71 | if (buildNumber >= CONPTY_MINIMAL_WINDOWS_VERSION) 72 | { 73 | qDebug() << "Use ConPty except of WinPty"; 74 | ptyType = IPtyProcess::ConPty; 75 | } 76 | 77 | QString shellPath = "c:\\Windows\\system32\\cmd.exe"; 78 | //shellPath = "C:\\Program\ Files\\Git\\bin\\bash.exe"; 79 | #ifdef Q_OS_UNIX 80 | shellPath = "/bin/sh"; 81 | ptyType = IPtyProcess::UnixPty; 82 | #endif 83 | 84 | //create new Pty instance 85 | IPtyProcess *pty = PtyQt::createPtyProcess(ptyType); 86 | 87 | qDebug() << "New connection" << wSocket->peerAddress() << wSocket->peerPort() << pty->pid(); 88 | 89 | //start Pty process () 90 | pty->startProcess(shellPath, QProcessEnvironment::systemEnvironment().toStringList(), COLS, ROWS); 91 | 92 | //connect read channel from Pty process to write channel on websocket 93 | QObject::connect(pty->notifier(), &QIODevice::readyRead, [wSocket, pty]() 94 | { 95 | wSocket->sendTextMessage(pty->readAll()); 96 | }); 97 | 98 | //connect read channel of Websocket to write channel of Pty process 99 | QObject::connect(wSocket, &QWebSocket::textMessageReceived, [wSocket, pty](const QString &message) 100 | { 101 | pty->write(message.toLatin1()); 102 | }); 103 | 104 | //... 105 | //for example handle disconnections, process crashes and stuff like that... 106 | //... 107 | 108 | //add connection to list of active connections 109 | sessions.insert(wSocket, pty); 110 | 111 | qDebug() << pty->size(); 112 | }); 113 | 114 | //stop eventloop if needed 115 | //QTimer::singleShot(5000, [](){ qApp->quit(); }); 116 | 117 | //exec eventloop 118 | bool res = app.exec(); 119 | 120 | QMapIterator it(sessions); 121 | while (it.hasNext()) 122 | { 123 | it.next(); 124 | 125 | it.key()->deleteLater(); 126 | delete it.value(); 127 | } 128 | sessions.clear(); 129 | 130 | return res; 131 | } 132 | ``` 133 | 134 | ## Examples 135 | ### XtermJS 136 | - build and run example from cmd.exe 137 | - install nodejs (your prefer way) 138 | - open console and run: 139 | ```sh 140 | cd ptyqt/examples/xtermjs 141 | npm install xterm 142 | npm install http-server -g 143 | http-server ./ 144 | ``` 145 | - open http://127.0.0.1:8080/ in Web browser 146 | - use your terminal, for example install and run 'Midnight Commander' or 'Far' for test pseduo-graphic interface 147 | 148 | **IMPORTANT** 149 | - do not use Git Bash for run 'xtermjs_sample.exe' on Windows, it has some issues: https://github.com/git-for-windows/git/wiki/FAQ#some-native-console-programs-dont-work-when-run-from-git-bash-how-to-fix-it 150 | - Only Far manager >= 3.0 supported by XTermJS, all old versioans are unsupported 151 | - ConPty requires to run your application from existing terminal session, in another case it just not work. For example in Qt Creator on Windows check "Run in Terminal" in project run settings before run examples or tests 152 | 153 | Also, you can find for example projects like https://github.com/lxqt/qterminal, they all based on QTermWidget and they all not-crossplatform and support only UNIX. But QTermWidget is support full VT100 protocol because it's fork from Linux/KDE/Konsole application. 154 | 155 | ## More information 156 | Resources used to develop this library: 157 | - https://github.com/Microsoft/node-pty 158 | - https://github.com/Microsoft/console 159 | - https://github.com/rprichard/winpty 160 | - https://github.com/xtermjs/xterm.js 161 | - https://github.com/lxqt/qterminal 162 | - https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/ 163 | - https://devblogs.microsoft.com/commandline/windows-command-line-backgrounder/ 164 | - https://github.com/sebcaux/QVTerminal 165 | 166 | ## XtermJS + PTY-Qt + C++ sample screenshots 167 | 168 | ![Midnight Commander](https://github.com/kafeg/ptyqt/raw/master/examples/xtermjs/screens/midnight_commander_bash_unix.png) 169 | 170 | ![Far Manager](https://github.com/kafeg/ptyqt/raw/master/examples/xtermjs/screens/far_manager_cmd_windows.png) 171 | -------------------------------------------------------------------------------- /core/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(ptyqt-core) 2 | 3 | set(SOURCE_FILES 4 | ptyqt.h 5 | ptyqt.cpp 6 | iptyprocess.h 7 | ) 8 | 9 | if (MSVC) 10 | set(SOURCE_FILES 11 | ${SOURCE_FILES} 12 | winptyprocess.h 13 | winptyprocess.cpp 14 | conptyprocess.h 15 | conptyprocess.cpp 16 | ) 17 | else() 18 | set(SOURCE_FILES 19 | ${SOURCE_FILES} 20 | unixptyprocess.cpp 21 | unixptyprocess.h 22 | ) 23 | endif() 24 | 25 | if("${BUILD_TYPE}" STREQUAL "STATIC") 26 | add_library( ptyqt STATIC ${SOURCE_FILES} ) 27 | add_definitions(-DPTYQT_BUILD_STATIC) 28 | if (MSVC) 29 | add_definitions(-DBUILD_STATIC) #for WinPty 30 | endif() 31 | else() 32 | add_library( ptyqt SHARED ${SOURCE_FILES} ) 33 | add_definitions(-DPTYQT_BUILD_DYNAMIC) 34 | endif() 35 | 36 | #libs 37 | set(ADDITIONAL_LIBS "") 38 | 39 | if (NOT ANDROID) 40 | if("${BUILD_TYPE}" STREQUAL "STATIC") 41 | find_library(PCRE2_LIBRARY_DEBUG NAMES pcre2-8d pcre2-8-staticd HINTS ${INSTALLED_LIB_PATH}) 42 | find_library(PCRE2_LIBRARY_RELEASE NAMES pcre2-8 pcre2-8-static HINTS ${INSTALLED_LIB_PATH}) 43 | include(SelectLibraryConfigurations) 44 | select_library_configurations(PCRE2) 45 | set(PCRE2_LIBRARIES ${PCRE2_LIBRARY}) 46 | else() 47 | find_library(PCRE2_LIBRARIES NAMES pcre2 pcre2-16) 48 | endif() 49 | 50 | find_path(PCRE2_INCLUDE_DIRS pcre2.h) 51 | if(PCRE2_LIBRARIES AND PCRE2_INCLUDE_DIRS) 52 | message(STATUS "PCRE2 libs: ${PCRE2_LIBRARIES}") 53 | message(STATUS "PCRE2 include directory: ${PCRE2_INCLUDE_DIRS}") 54 | set(PCRE2_FOUND TRUE CACHE BOOL "Found PCRE2 libraries" FORCE) 55 | else() 56 | set(PCRE2_FOUND FALSE CACHE BOOL "Found PCRE2 libraries" FORCE) 57 | message(STATUS "PCRE2 library not found.") 58 | endif() 59 | 60 | find_package(OpenSSL REQUIRED) 61 | find_package(BZip2 REQUIRED) 62 | find_package(ZLIB REQUIRED) 63 | find_library(DBLCONV_LIBRARIES NAMES pcre2 double-conversion) 64 | 65 | list(APPEND ADDITIONAL_LIBS ${PCRE2_LIBRARIES} OpenSSL::SSL OpenSSL::Crypto ${BZIP2_LIBRARIES} ${ZLIB_LIBRARIES} ${DBLCONV_LIBRARIES}) 66 | endif() 67 | 68 | if (MSVC) 69 | list(APPEND ADDITIONAL_LIBS wsock32 ws2_32 crypt32 iphlpapi netapi32 version winmm userenv) 70 | elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin" 71 | OR (UNIX AND APPLE) 72 | OR "${CMAKE_CXX_COMPILER}" MATCHES "/usr/bin/clang" 73 | ) 74 | 75 | set(LIBS_MACOS 76 | "-framework Security -framework AppKit -framework CoreFoundation -framework IOKit" 77 | "-framework CoreGraphics -framework CFNetwork -framework CoreText -framework Carbon" 78 | "-framework CoreServices -framework ApplicationServices -framework SystemConfiguration" 79 | ) 80 | 81 | list(APPEND ADDITIONAL_LIBS ${LIBS_MACOS}) 82 | 83 | elseif(UNIX AND NOT APPLE) 84 | set(LIBS_LINUX 85 | "-lpthread -ldl -static-libstdc++" 86 | ) 87 | list(APPEND ADDITIONAL_LIBS ${LIBS_LINUX}) 88 | endif() 89 | 90 | #link 91 | target_link_libraries(ptyqt Qt5::Core ${ADDITIONAL_LIBS}) 92 | 93 | if (MSVC) 94 | target_link_libraries( ptyqt Qt5::Network ${WINPTY_LIBRARIES}) 95 | endif() 96 | 97 | if("${BUILD_TYPE}" STREQUAL "STATIC") 98 | install(TARGETS ptyqt DESTINATION ${PTYQT_INSTALL_LIB_DIR}) 99 | else() 100 | message("CMAKE_CURRENT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR} ${PTYQT_INSTALL_BIN_DIR} ${PTYQT_INSTALL_LIB_DIR}") 101 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ptyqt.dll DESTINATION ${PTYQT_INSTALL_BIN_DIR}) 102 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ptyqt.lib DESTINATION ${PTYQT_INSTALL_LIB_DIR}) 103 | endif() 104 | install(FILES ptyqt.h iptyprocess.h DESTINATION ${PTYQT_INSTALL_INCLUDE_DIR}) 105 | -------------------------------------------------------------------------------- /core/conptyprocess.cpp: -------------------------------------------------------------------------------- 1 | #include "conptyprocess.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #define READ_INTERVAL_MSEC 500 11 | 12 | HRESULT ConPtyProcess::createPseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut, qint16 cols, qint16 rows) 13 | { 14 | HRESULT hr{ E_UNEXPECTED }; 15 | HANDLE hPipePTYIn{ INVALID_HANDLE_VALUE }; 16 | HANDLE hPipePTYOut{ INVALID_HANDLE_VALUE }; 17 | 18 | // Create the pipes to which the ConPTY will connect 19 | if (CreatePipe(&hPipePTYIn, phPipeOut, NULL, 0) && 20 | CreatePipe(phPipeIn, &hPipePTYOut, NULL, 0)) 21 | { 22 | // Create the Pseudo Console of the required size, attached to the PTY-end of the pipes 23 | hr = m_winContext.createPseudoConsole({cols, rows}, hPipePTYIn, hPipePTYOut, 0, phPC); 24 | 25 | // Note: We can close the handles to the PTY-end of the pipes here 26 | // because the handles are dup'ed into the ConHost and will be released 27 | // when the ConPTY is destroyed. 28 | if (INVALID_HANDLE_VALUE != hPipePTYOut) CloseHandle(hPipePTYOut); 29 | if (INVALID_HANDLE_VALUE != hPipePTYIn) CloseHandle(hPipePTYIn); 30 | } 31 | 32 | return hr; 33 | } 34 | 35 | // Initializes the specified startup info struct with the required properties and 36 | // updates its thread attribute list with the specified ConPTY handle 37 | HRESULT ConPtyProcess::initializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX* pStartupInfo, HPCON hPC) 38 | { 39 | HRESULT hr{ E_UNEXPECTED }; 40 | 41 | if (pStartupInfo) 42 | { 43 | SIZE_T attrListSize{}; 44 | 45 | pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX); 46 | 47 | // Get the size of the thread attribute list. 48 | InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize); 49 | 50 | // Allocate a thread attribute list of the correct size 51 | pStartupInfo->lpAttributeList = 52 | reinterpret_cast(malloc(attrListSize)); 53 | 54 | // Initialize thread attribute list 55 | if (pStartupInfo->lpAttributeList 56 | && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize)) 57 | { 58 | // Set Pseudo Console attribute 59 | hr = UpdateProcThreadAttribute( 60 | pStartupInfo->lpAttributeList, 61 | 0, 62 | PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, 63 | hPC, 64 | sizeof(HPCON), 65 | NULL, 66 | NULL) 67 | ? S_OK 68 | : HRESULT_FROM_WIN32(GetLastError()); 69 | } 70 | else 71 | { 72 | hr = HRESULT_FROM_WIN32(GetLastError()); 73 | } 74 | } 75 | return hr; 76 | } 77 | 78 | ConPtyProcess::ConPtyProcess() 79 | : IPtyProcess() 80 | , m_ptyHandler { INVALID_HANDLE_VALUE } 81 | , m_hPipeIn { INVALID_HANDLE_VALUE } 82 | , m_hPipeOut { INVALID_HANDLE_VALUE } 83 | , m_readThread(nullptr) 84 | { 85 | 86 | } 87 | 88 | ConPtyProcess::~ConPtyProcess() 89 | { 90 | kill(); 91 | } 92 | 93 | bool ConPtyProcess::startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows) 94 | { 95 | if (!isAvailable()) 96 | { 97 | m_lastError = m_winContext.lastError(); 98 | return false; 99 | } 100 | 101 | //already running 102 | if (m_ptyHandler != INVALID_HANDLE_VALUE) 103 | return false; 104 | 105 | QFileInfo fi(shellPath); 106 | if (fi.isRelative() || !QFile::exists(shellPath)) 107 | { 108 | //todo add auto-find executable in PATH env var 109 | m_lastError = QString("ConPty Error: shell file path must be absolute"); 110 | return false; 111 | } 112 | 113 | m_shellPath = shellPath; 114 | m_size = QPair(cols, rows); 115 | 116 | //env 117 | std::stringstream envBlock; 118 | foreach (QString line, environment) 119 | { 120 | envBlock << line.toStdString() << '\0'; 121 | } 122 | envBlock << '\0'; 123 | std::string env = envBlock.str(); 124 | auto envV = vectorFromString(env); 125 | LPSTR envArg = envV.empty() ? nullptr : envV.data(); 126 | 127 | LPSTR cmdArg = new char[m_shellPath.toStdString().length() + 1]; 128 | std::strcpy(cmdArg, m_shellPath.toStdString().c_str()); 129 | //qDebug() << "m_shellPath" << m_shellPath << cmdArg << m_shellPath.toStdString().c_str(); 130 | 131 | HRESULT hr{ E_UNEXPECTED }; 132 | 133 | // Create the Pseudo Console and pipes to it 134 | hr = createPseudoConsoleAndPipes(&m_ptyHandler, &m_hPipeIn, &m_hPipeOut, cols, rows); 135 | 136 | if (S_OK != hr) 137 | { 138 | m_lastError = QString("ConPty Error: CreatePseudoConsoleAndPipes fail"); 139 | return false; 140 | } 141 | 142 | // Initialize the necessary startup info struct 143 | STARTUPINFOEX startupInfo{}; 144 | if (S_OK != initializeStartupInfoAttachedToPseudoConsole(&startupInfo, m_ptyHandler)) 145 | { 146 | m_lastError = QString("ConPty Error: InitializeStartupInfoAttachedToPseudoConsole fail"); 147 | return false; 148 | } 149 | 150 | // Launch ping to emit some text back via the pipe 151 | PROCESS_INFORMATION piClient{}; 152 | hr = CreateProcess( 153 | NULL, // No module name - use Command Line 154 | cmdArg, // Command Line 155 | NULL, // Process handle not inheritable 156 | NULL, // Thread handle not inheritable 157 | FALSE, // Inherit handles 158 | EXTENDED_STARTUPINFO_PRESENT, // Creation flags 159 | envArg, //NULL, // Use parent's environment block 160 | NULL, // Use parent's starting directory 161 | &startupInfo.StartupInfo, // Pointer to STARTUPINFO 162 | &piClient) // Pointer to PROCESS_INFORMATION 163 | ? S_OK 164 | : GetLastError(); 165 | 166 | if (S_OK != hr) 167 | { 168 | m_lastError = QString("ConPty Error: Cannot create process -> %1").arg(hr); 169 | return false; 170 | } 171 | m_pid = piClient.dwProcessId; 172 | 173 | //this code runned in separate thread 174 | m_readThread = QThread::create([this, &piClient, &startupInfo]() 175 | { 176 | forever 177 | { 178 | //buffers 179 | const DWORD BUFF_SIZE{ 512 }; 180 | char szBuffer[BUFF_SIZE]{}; 181 | 182 | //DWORD dwBytesWritten{}; 183 | DWORD dwBytesRead{}; 184 | BOOL fRead{ FALSE }; 185 | 186 | // Read from the pipe 187 | fRead = ReadFile(m_hPipeIn, szBuffer, BUFF_SIZE, &dwBytesRead, NULL); 188 | 189 | { 190 | QMutexLocker locker(&m_bufferMutex); 191 | m_buffer.m_readBuffer.append(szBuffer, dwBytesRead); 192 | m_buffer.emitReadyRead(); 193 | } 194 | 195 | if (QThread::currentThread()->isInterruptionRequested()) 196 | break; 197 | 198 | QCoreApplication::processEvents(); 199 | } 200 | 201 | // Now safe to clean-up client app's process-info & thread 202 | CloseHandle(piClient.hThread); 203 | CloseHandle(piClient.hProcess); 204 | 205 | // Cleanup attribute list 206 | DeleteProcThreadAttributeList(startupInfo.lpAttributeList); 207 | //free(startupInfo.lpAttributeList); 208 | }); 209 | 210 | //start read thread 211 | m_readThread->start(); 212 | 213 | return true; 214 | } 215 | 216 | bool ConPtyProcess::resize(qint16 cols, qint16 rows) 217 | { 218 | if (m_ptyHandler == nullptr) 219 | { 220 | return false; 221 | } 222 | 223 | bool res = SUCCEEDED(m_winContext.resizePseudoConsole(m_ptyHandler, {cols, rows})); 224 | 225 | if (res) 226 | { 227 | m_size = QPair(cols, rows); 228 | } 229 | 230 | return res; 231 | 232 | return true; 233 | } 234 | 235 | bool ConPtyProcess::kill() 236 | { 237 | bool exitCode = false; 238 | 239 | if ( m_ptyHandler != INVALID_HANDLE_VALUE ) 240 | { 241 | m_readThread->requestInterruption(); 242 | QThread::msleep(200); 243 | m_readThread->quit(); 244 | m_readThread->deleteLater(); 245 | m_readThread = nullptr; 246 | 247 | // Close ConPTY - this will terminate client process if running 248 | m_winContext.closePseudoConsole(m_ptyHandler); 249 | 250 | // Clean-up the pipes 251 | if (INVALID_HANDLE_VALUE != m_hPipeOut) CloseHandle(m_hPipeOut); 252 | if (INVALID_HANDLE_VALUE != m_hPipeIn) CloseHandle(m_hPipeIn); 253 | m_pid = 0; 254 | m_ptyHandler = INVALID_HANDLE_VALUE; 255 | m_hPipeIn = INVALID_HANDLE_VALUE; 256 | m_hPipeOut = INVALID_HANDLE_VALUE; 257 | 258 | exitCode = true; 259 | } 260 | 261 | return exitCode; 262 | } 263 | 264 | IPtyProcess::PtyType ConPtyProcess::type() 265 | { 266 | return PtyType::ConPty; 267 | } 268 | 269 | QString ConPtyProcess::dumpDebugInfo() 270 | { 271 | #ifdef PTYQT_DEBUG 272 | return QString("PID: %1, Type: %2, Cols: %3, Rows: %4") 273 | .arg(m_pid).arg(type()) 274 | .arg(m_size.first).arg(m_size.second); 275 | #else 276 | return QString("Nothing..."); 277 | #endif 278 | } 279 | 280 | QIODevice *ConPtyProcess::notifier() 281 | { 282 | return &m_buffer; 283 | } 284 | 285 | QByteArray ConPtyProcess::readAll() 286 | { 287 | QMutexLocker locker(&m_bufferMutex); 288 | return m_buffer.m_readBuffer; 289 | } 290 | 291 | qint64 ConPtyProcess::write(const QByteArray &byteArray) 292 | { 293 | DWORD dwBytesWritten{}; 294 | WriteFile(m_hPipeOut, byteArray.data(), byteArray.size(), &dwBytesWritten, NULL); 295 | return dwBytesWritten; 296 | } 297 | 298 | bool ConPtyProcess::isAvailable() 299 | { 300 | #ifdef TOO_OLD_WINSDK 301 | return false; //very importnant! ConPty can be built, but it doesn't work if built with old sdk and Win10 < 1903 302 | #endif 303 | 304 | qint32 buildNumber = QSysInfo::kernelVersion().split(".").last().toInt(); 305 | if (buildNumber < CONPTY_MINIMAL_WINDOWS_VERSION) 306 | return false; 307 | return m_winContext.init(); 308 | } 309 | 310 | void ConPtyProcess::moveToThread(QThread *targetThread) 311 | { 312 | //nothing for now... 313 | } 314 | -------------------------------------------------------------------------------- /core/conptyprocess.h: -------------------------------------------------------------------------------- 1 | #ifndef CONPTYPROCESS_H 2 | #define CONPTYPROCESS_H 3 | 4 | #include "iptyprocess.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | //Taken from the RS5 Windows SDK, but redefined here in case we're targeting <= 17733 14 | //Just for compile, ConPty doesn't work with Windows SDK < 17733 15 | #ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE 16 | #define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \ 17 | ProcThreadAttributeValue(22, FALSE, TRUE, FALSE) 18 | 19 | typedef VOID* HPCON; 20 | 21 | #define TOO_OLD_WINSDK 22 | #endif 23 | 24 | template 25 | std::vector vectorFromString(const std::basic_string &str) 26 | { 27 | return std::vector(str.begin(), str.end()); 28 | } 29 | 30 | //ConPTY available only on Windows 10 releazed after 1903 (19H1) Windows release 31 | class WindowsContext 32 | { 33 | public: 34 | typedef HRESULT (*CreatePseudoConsolePtr)( 35 | COORD size, // ConPty Dimensions 36 | HANDLE hInput, // ConPty Input 37 | HANDLE hOutput, // ConPty Output 38 | DWORD dwFlags, // ConPty Flags 39 | HPCON* phPC); // ConPty Reference 40 | 41 | typedef HRESULT (*ResizePseudoConsolePtr)(HPCON hPC, COORD size); 42 | 43 | typedef VOID (*ClosePseudoConsolePtr)(HPCON hPC); 44 | 45 | WindowsContext() 46 | : createPseudoConsole(nullptr) 47 | , resizePseudoConsole(nullptr) 48 | , closePseudoConsole(nullptr) 49 | { 50 | 51 | } 52 | 53 | bool init() 54 | { 55 | //already initialized 56 | if (createPseudoConsole) 57 | return true; 58 | 59 | //try to load symbols from library 60 | //if it fails -> we can't use ConPty API 61 | HANDLE kernel32Handle = LoadLibraryExW(L"kernel32.dll", 0, 0); 62 | 63 | if (kernel32Handle != nullptr) 64 | { 65 | createPseudoConsole = (CreatePseudoConsolePtr)GetProcAddress((HMODULE)kernel32Handle, "CreatePseudoConsole"); 66 | resizePseudoConsole = (ResizePseudoConsolePtr)GetProcAddress((HMODULE)kernel32Handle, "ResizePseudoConsole"); 67 | closePseudoConsole = (ClosePseudoConsolePtr)GetProcAddress((HMODULE)kernel32Handle, "ClosePseudoConsole"); 68 | if (createPseudoConsole == NULL || resizePseudoConsole == NULL || closePseudoConsole == NULL) 69 | { 70 | m_lastError = QString("WindowsContext/ConPty error: %1").arg("Invalid on load API functions"); 71 | return false; 72 | } 73 | } 74 | else 75 | { 76 | m_lastError = QString("WindowsContext/ConPty error: %1").arg("Unable to load kernel32"); 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | 83 | QString lastError() 84 | { 85 | return m_lastError; 86 | } 87 | 88 | public: 89 | //vars 90 | CreatePseudoConsolePtr createPseudoConsole; 91 | ResizePseudoConsolePtr resizePseudoConsole; 92 | ClosePseudoConsolePtr closePseudoConsole; 93 | 94 | private: 95 | QString m_lastError; 96 | }; 97 | 98 | class PtyBuffer : public QIODevice 99 | { 100 | friend class ConPtyProcess; 101 | Q_OBJECT 102 | public: 103 | 104 | PtyBuffer() { } 105 | ~PtyBuffer() { } 106 | 107 | //just empty realization, we need only 'readyRead' signal of this class 108 | qint64 readData(char *data, qint64 maxlen) { return 0; } 109 | qint64 writeData(const char *data, qint64 len) { return 0; } 110 | 111 | bool isSequential() { return true; } 112 | qint64 bytesAvailable() { return m_readBuffer.size(); } 113 | qint64 size() { return m_readBuffer.size(); } 114 | 115 | void emitReadyRead() 116 | { 117 | //for emit signal from PtyBuffer own thread 118 | QTimer::singleShot(1, this, [this]() 119 | { 120 | emit readyRead(); 121 | }); 122 | } 123 | 124 | private: 125 | QByteArray m_readBuffer; 126 | }; 127 | 128 | class ConPtyProcess : public IPtyProcess 129 | { 130 | public: 131 | ConPtyProcess(); 132 | ~ConPtyProcess(); 133 | 134 | bool startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows); 135 | bool resize(qint16 cols, qint16 rows); 136 | bool kill(); 137 | PtyType type(); 138 | QString dumpDebugInfo(); 139 | virtual QIODevice *notifier(); 140 | virtual QByteArray readAll(); 141 | virtual qint64 write(const QByteArray &byteArray); 142 | bool isAvailable(); 143 | void moveToThread(QThread *targetThread); 144 | 145 | private: 146 | HRESULT createPseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut, qint16 cols, qint16 rows); 147 | HRESULT initializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX* pStartupInfo, HPCON hPC); 148 | 149 | private: 150 | WindowsContext m_winContext; 151 | HPCON m_ptyHandler; 152 | HANDLE m_hPipeIn, m_hPipeOut; 153 | 154 | QThread *m_readThread; 155 | QMutex m_bufferMutex; 156 | PtyBuffer m_buffer; 157 | 158 | }; 159 | 160 | #endif // CONPTYPROCESS_H 161 | -------------------------------------------------------------------------------- /core/iptyprocess.h: -------------------------------------------------------------------------------- 1 | #ifndef IPTYPROCESS_H 2 | #define IPTYPROCESS_H 3 | 4 | #include 5 | #include 6 | 7 | #ifdef Q_OS_WIN 8 | #include 9 | #endif 10 | 11 | #define CONPTY_MINIMAL_WINDOWS_VERSION 18309 12 | 13 | class IPtyProcess 14 | { 15 | public: 16 | enum PtyType 17 | { 18 | UnixPty = 0, 19 | WinPty = 1, 20 | ConPty = 2, 21 | AutoPty = 3 22 | }; 23 | 24 | IPtyProcess() 25 | : m_pid(0) 26 | , m_trace(false) 27 | { } 28 | virtual ~IPtyProcess() { } 29 | 30 | virtual bool startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows) = 0; 31 | virtual bool resize(qint16 cols, qint16 rows) = 0; 32 | virtual bool kill() = 0; 33 | virtual PtyType type() = 0; 34 | virtual QString dumpDebugInfo() = 0; 35 | virtual QIODevice *notifier() = 0; 36 | virtual QByteArray readAll() = 0; 37 | virtual qint64 write(const QByteArray &byteArray) = 0; 38 | virtual bool isAvailable() = 0; 39 | virtual void moveToThread(QThread *targetThread) = 0; 40 | qint64 pid() { return m_pid; } 41 | QPair size() { return m_size; } 42 | const QString lastError() { return m_lastError; } 43 | bool toggleTrace() { m_trace = !m_trace; return m_trace; } 44 | 45 | protected: 46 | QString m_shellPath; 47 | QString m_lastError; 48 | qint64 m_pid; 49 | QPair m_size; //cols / rows 50 | bool m_trace; 51 | }; 52 | 53 | #endif // IPTYPROCESS_H 54 | -------------------------------------------------------------------------------- /core/ptyqt.cpp: -------------------------------------------------------------------------------- 1 | #include "ptyqt.h" 2 | #include 3 | 4 | #ifdef Q_OS_WIN 5 | #include "winptyprocess.h" 6 | #include "conptyprocess.h" 7 | #endif 8 | 9 | #ifdef Q_OS_UNIX 10 | #include "unixptyprocess.h" 11 | #endif 12 | 13 | IPtyProcess *PtyQt::createPtyProcess(IPtyProcess::PtyType ptyType) 14 | { 15 | switch (ptyType) 16 | { 17 | #ifdef Q_OS_WIN 18 | case IPtyProcess::PtyType::WinPty: 19 | return new WinPtyProcess(); 20 | break; 21 | case IPtyProcess::PtyType::ConPty: 22 | return new ConPtyProcess(); 23 | break; 24 | #endif 25 | #ifdef Q_OS_UNIX 26 | case IPtyProcess::PtyType::UnixPty: 27 | return new UnixPtyProcess(); 28 | break; 29 | #endif 30 | case IPtyProcess::PtyType::AutoPty: 31 | default: 32 | break; 33 | } 34 | 35 | #ifdef Q_OS_WIN 36 | if (ConPtyProcess().isAvailable()) 37 | return new ConPtyProcess(); 38 | else 39 | return new WinPtyProcess(); 40 | #endif 41 | #ifdef Q_OS_UNIX 42 | return new UnixPtyProcess(); 43 | #endif 44 | } 45 | -------------------------------------------------------------------------------- /core/ptyqt.h: -------------------------------------------------------------------------------- 1 | #ifndef PTYQT_H 2 | #define PTYQT_H 3 | 4 | #include "iptyprocess.h" 5 | 6 | class PtyQt 7 | { 8 | public: 9 | static IPtyProcess *createPtyProcess(IPtyProcess::PtyType ptyType); 10 | }; 11 | 12 | #endif // PTYQT_H 13 | -------------------------------------------------------------------------------- /core/unixptyprocess.cpp: -------------------------------------------------------------------------------- 1 | #include "unixptyprocess.h" 2 | #include 3 | 4 | #include 5 | #include 6 | #if !defined(Q_OS_ANDROID) && !defined(Q_OS_FREEBSD) 7 | #include 8 | #endif 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | UnixPtyProcess::UnixPtyProcess() 17 | : IPtyProcess() 18 | , m_readMasterNotify(0) 19 | { 20 | m_shellProcess.setWorkingDirectory(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); 21 | } 22 | 23 | UnixPtyProcess::~UnixPtyProcess() 24 | { 25 | kill(); 26 | } 27 | 28 | bool UnixPtyProcess::startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows) 29 | { 30 | if (!isAvailable()) 31 | { 32 | m_lastError = QString("UnixPty Error: unavailable"); 33 | return false; 34 | } 35 | 36 | if (m_shellProcess.state() == QProcess::Running) 37 | return false; 38 | 39 | QFileInfo fi(shellPath); 40 | if (fi.isRelative() || !QFile::exists(shellPath)) 41 | { 42 | //todo add auto-find executable in PATH env var 43 | m_lastError = QString("UnixPty Error: shell file path must be absolute"); 44 | return false; 45 | } 46 | 47 | m_shellPath = shellPath; 48 | m_size = QPair(cols, rows); 49 | 50 | int rc = 0; 51 | 52 | m_shellProcess.m_handleMaster = ::posix_openpt(O_RDWR | O_NOCTTY); 53 | if (m_shellProcess.m_handleMaster <= 0) 54 | { 55 | m_lastError = QString("UnixPty Error: unable to open master -> %1").arg(strerror(errno)); 56 | kill(); 57 | return false; 58 | } 59 | 60 | m_shellProcess.m_handleSlaveName = ptsname(m_shellProcess.m_handleMaster); 61 | if ( m_shellProcess.m_handleSlaveName.isEmpty()) 62 | { 63 | m_lastError = QString("UnixPty Error: unable to get slave name -> %1").arg(strerror(errno)); 64 | kill(); 65 | return false; 66 | } 67 | 68 | rc = grantpt(m_shellProcess.m_handleMaster); 69 | if (rc != 0) 70 | { 71 | m_lastError = QString("UnixPty Error: unable to change perms for slave -> %1").arg(strerror(errno)); 72 | kill(); 73 | return false; 74 | } 75 | 76 | rc = unlockpt(m_shellProcess.m_handleMaster); 77 | if (rc != 0) 78 | { 79 | m_lastError = QString("UnixPty Error: unable to unlock slave -> %1").arg(strerror(errno)); 80 | kill(); 81 | return false; 82 | } 83 | 84 | m_shellProcess.m_handleSlave = ::open(m_shellProcess.m_handleSlaveName.toLatin1().data(), O_RDWR | O_NOCTTY); 85 | if (m_shellProcess.m_handleSlave < 0) 86 | { 87 | m_lastError = QString("UnixPty Error: unable to open slave -> %1").arg(strerror(errno)); 88 | kill(); 89 | return false; 90 | } 91 | 92 | rc = fcntl(m_shellProcess.m_handleMaster, F_SETFD, FD_CLOEXEC); 93 | if (rc == -1) 94 | { 95 | m_lastError = QString("UnixPty Error: unable to set flags for master -> %1").arg(strerror(errno)); 96 | kill(); 97 | return false; 98 | } 99 | 100 | rc = fcntl(m_shellProcess.m_handleSlave, F_SETFD, FD_CLOEXEC); 101 | if (rc == -1) 102 | { 103 | m_lastError = QString("UnixPty Error: unable to set flags for slave -> %1").arg(strerror(errno)); 104 | kill(); 105 | return false; 106 | } 107 | 108 | struct ::termios ttmode; 109 | rc = tcgetattr(m_shellProcess.m_handleMaster, &ttmode); 110 | if (rc != 0) 111 | { 112 | m_lastError = QString("UnixPty Error: termios fail -> %1").arg(strerror(errno)); 113 | kill(); 114 | return false; 115 | } 116 | 117 | ttmode.c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; 118 | #if defined(IUTF8) 119 | ttmode.c_iflag |= IUTF8; 120 | #endif 121 | 122 | ttmode.c_oflag = OPOST | ONLCR; 123 | ttmode.c_cflag = CREAD | CS8 | HUPCL; 124 | ttmode.c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL; 125 | 126 | ttmode.c_cc[VEOF] = 4; 127 | ttmode.c_cc[VEOL] = -1; 128 | ttmode.c_cc[VEOL2] = -1; 129 | ttmode.c_cc[VERASE] = 0x7f; 130 | ttmode.c_cc[VWERASE] = 23; 131 | ttmode.c_cc[VKILL] = 21; 132 | ttmode.c_cc[VREPRINT] = 18; 133 | ttmode.c_cc[VINTR] = 3; 134 | ttmode.c_cc[VQUIT] = 0x1c; 135 | ttmode.c_cc[VSUSP] = 26; 136 | ttmode.c_cc[VSTART] = 17; 137 | ttmode.c_cc[VSTOP] = 19; 138 | ttmode.c_cc[VLNEXT] = 22; 139 | ttmode.c_cc[VDISCARD] = 15; 140 | ttmode.c_cc[VMIN] = 1; 141 | ttmode.c_cc[VTIME] = 0; 142 | 143 | #if (__APPLE__) 144 | ttmode.c_cc[VDSUSP] = 25; 145 | ttmode.c_cc[VSTATUS] = 20; 146 | #endif 147 | 148 | cfsetispeed(&ttmode, B38400); 149 | cfsetospeed(&ttmode, B38400); 150 | 151 | rc = tcsetattr(m_shellProcess.m_handleMaster, TCSANOW, &ttmode); 152 | if (rc != 0) 153 | { 154 | m_lastError = QString("UnixPty Error: unabble to set associated params -> %1").arg(strerror(errno)); 155 | kill(); 156 | return false; 157 | } 158 | 159 | m_readMasterNotify = new QSocketNotifier(m_shellProcess.m_handleMaster, QSocketNotifier::Read, &m_shellProcess); 160 | m_readMasterNotify->setEnabled(true); 161 | m_readMasterNotify->moveToThread(m_shellProcess.thread()); 162 | QObject::connect(m_readMasterNotify, &QSocketNotifier::activated, [this](int socket) 163 | { 164 | Q_UNUSED(socket) 165 | 166 | QByteArray buffer; 167 | int size = 1025; 168 | int readSize = 1024; 169 | QByteArray data; 170 | do 171 | { 172 | char nativeBuffer[size]; 173 | int len = ::read(m_shellProcess.m_handleMaster, nativeBuffer, readSize); 174 | data = QByteArray(nativeBuffer, len); 175 | buffer.append(data); 176 | } while (data.size() == readSize); //last data block always < readSize 177 | 178 | m_shellReadBuffer.append(buffer); 179 | m_shellProcess.emitReadyRead(); 180 | }); 181 | 182 | QStringList defaultVars; 183 | 184 | defaultVars.append("TERM=xterm-256color"); 185 | defaultVars.append("ITERM_PROFILE=Default"); 186 | defaultVars.append("XPC_FLAGS=0x0"); 187 | defaultVars.append("XPC_SERVICE_NAME=0"); 188 | defaultVars.append("LANG=en_US.UTF-8"); 189 | defaultVars.append("LC_ALL=en_US.UTF-8"); 190 | defaultVars.append("LC_CTYPE=UTF-8"); 191 | defaultVars.append("INIT_CWD=" + QCoreApplication::applicationDirPath()); 192 | defaultVars.append("COMMAND_MODE=unix2003"); 193 | defaultVars.append("COLORTERM=truecolor"); 194 | 195 | QStringList varNames; 196 | foreach (QString line, environment) 197 | { 198 | varNames.append(line.split("=").first()); 199 | } 200 | 201 | //append default env vars only if they don't exists in current env 202 | foreach (QString defVar, defaultVars) 203 | { 204 | if (!varNames.contains(defVar.split("=").first())) 205 | environment.append(defVar); 206 | } 207 | 208 | QProcessEnvironment envFormat; 209 | foreach (QString line, environment) 210 | { 211 | envFormat.insert(line.split("=").first(), line.split("=").last()); 212 | } 213 | m_shellProcess.setWorkingDirectory(QCoreApplication::applicationDirPath()); 214 | m_shellProcess.setProcessEnvironment(envFormat); 215 | m_shellProcess.setReadChannel(QProcess::StandardOutput); 216 | m_shellProcess.start(m_shellPath, QStringList()); 217 | m_shellProcess.waitForStarted(); 218 | 219 | m_pid = m_shellProcess.processId(); 220 | 221 | resize(cols, rows); 222 | 223 | return true; 224 | } 225 | 226 | bool UnixPtyProcess::resize(qint16 cols, qint16 rows) 227 | { 228 | struct winsize winp; 229 | winp.ws_col = cols; 230 | winp.ws_row = rows; 231 | winp.ws_xpixel = 0; 232 | winp.ws_ypixel = 0; 233 | 234 | bool res = ( (ioctl(m_shellProcess.m_handleMaster, TIOCSWINSZ, &winp) != -1) && (ioctl(m_shellProcess.m_handleSlave, TIOCSWINSZ, &winp) != -1) ); 235 | 236 | if (res) 237 | { 238 | m_size = QPair(cols, rows); 239 | } 240 | 241 | return res; 242 | } 243 | 244 | bool UnixPtyProcess::kill() 245 | { 246 | m_shellProcess.m_handleSlaveName = QString(); 247 | if (m_shellProcess.m_handleSlave >= 0) 248 | { 249 | ::close(m_shellProcess.m_handleSlave); 250 | m_shellProcess.m_handleSlave = -1; 251 | } 252 | if (m_shellProcess.m_handleMaster >= 0) 253 | { 254 | ::close(m_shellProcess.m_handleMaster); 255 | m_shellProcess.m_handleMaster = -1; 256 | } 257 | 258 | if (m_shellProcess.state() == QProcess::Running) 259 | { 260 | m_readMasterNotify->disconnect(); 261 | m_readMasterNotify->deleteLater(); 262 | 263 | m_shellProcess.terminate(); 264 | m_shellProcess.waitForFinished(1000); 265 | 266 | if (m_shellProcess.state() == QProcess::Running) 267 | { 268 | QProcess::startDetached( QString("kill -9 %1").arg( pid() ) ); 269 | m_shellProcess.kill(); 270 | m_shellProcess.waitForFinished(1000); 271 | } 272 | 273 | return (m_shellProcess.state() == QProcess::NotRunning); 274 | } 275 | return false; 276 | } 277 | 278 | IPtyProcess::PtyType UnixPtyProcess::type() 279 | { 280 | return IPtyProcess::UnixPty; 281 | } 282 | 283 | QString UnixPtyProcess::dumpDebugInfo() 284 | { 285 | #ifdef PTYQT_DEBUG 286 | return QString("PID: %1, In: %2, Out: %3, Type: %4, Cols: %5, Rows: %6, IsRunning: %7, Shell: %8, SlaveName: %9") 287 | .arg(m_pid).arg(m_shellProcess.m_handleMaster).arg(m_shellProcess.m_handleSlave).arg(type()) 288 | .arg(m_size.first).arg(m_size.second).arg(m_shellProcess.state() == QProcess::Running) 289 | .arg(m_shellPath).arg(m_shellProcess.m_handleSlaveName); 290 | #else 291 | return QString("Nothing..."); 292 | #endif 293 | } 294 | 295 | QIODevice *UnixPtyProcess::notifier() 296 | { 297 | return &m_shellProcess; 298 | } 299 | 300 | QByteArray UnixPtyProcess::readAll() 301 | { 302 | QByteArray tmpBuffer = m_shellReadBuffer; 303 | m_shellReadBuffer.clear(); 304 | return tmpBuffer; 305 | } 306 | 307 | qint64 UnixPtyProcess::write(const QByteArray &byteArray) 308 | { 309 | int result = ::write(m_shellProcess.m_handleMaster, byteArray.constData(), byteArray.size()); 310 | Q_UNUSED(result) 311 | 312 | return byteArray.size(); 313 | } 314 | 315 | bool UnixPtyProcess::isAvailable() 316 | { 317 | //todo check something more if required 318 | return true; 319 | } 320 | 321 | void UnixPtyProcess::moveToThread(QThread *targetThread) 322 | { 323 | m_shellProcess.moveToThread(targetThread); 324 | } 325 | 326 | void ShellProcess::setupChildProcess() 327 | { 328 | dup2(m_handleSlave, STDIN_FILENO); 329 | dup2(m_handleSlave, STDOUT_FILENO); 330 | dup2(m_handleSlave, STDERR_FILENO); 331 | 332 | pid_t sid = setsid(); 333 | ioctl(m_handleSlave, TIOCSCTTY, 0); 334 | tcsetpgrp(m_handleSlave, sid); 335 | 336 | #if !defined(Q_OS_ANDROID) && !defined(Q_OS_FREEBSD) 337 | // on Android imposible to put record to the 'utmp' file 338 | struct utmpx utmpxInfo; 339 | memset(&utmpxInfo, 0, sizeof(utmpxInfo)); 340 | 341 | strncpy(utmpxInfo.ut_user, qgetenv("USER"), sizeof(utmpxInfo.ut_user)); 342 | 343 | QString device(m_handleSlaveName); 344 | if (device.startsWith("/dev/")) 345 | device = device.mid(5); 346 | 347 | const char *d = device.toLatin1().constData(); 348 | 349 | strncpy(utmpxInfo.ut_line, d, sizeof(utmpxInfo.ut_line)); 350 | 351 | strncpy(utmpxInfo.ut_id, d + strlen(d) - sizeof(utmpxInfo.ut_id), sizeof(utmpxInfo.ut_id)); 352 | 353 | struct timeval tv; 354 | gettimeofday(&tv, 0); 355 | utmpxInfo.ut_tv.tv_sec = tv.tv_sec; 356 | utmpxInfo.ut_tv.tv_usec = tv.tv_usec; 357 | 358 | utmpxInfo.ut_type = USER_PROCESS; 359 | utmpxInfo.ut_pid = getpid(); 360 | 361 | utmpxname(_PATH_UTMPX); 362 | setutxent(); 363 | pututxline(&utmpxInfo); 364 | endutxent(); 365 | 366 | #if !defined(Q_OS_UNIX) 367 | updwtmpx(_PATH_UTMPX, &loginInfo); 368 | #endif 369 | 370 | #endif 371 | } 372 | -------------------------------------------------------------------------------- /core/unixptyprocess.h: -------------------------------------------------------------------------------- 1 | #ifndef UNIXPTYPROCESS_H 2 | #define UNIXPTYPROCESS_H 3 | 4 | #include "iptyprocess.h" 5 | #include 6 | #include 7 | 8 | 9 | // support for build with MUSL on Alpine Linux 10 | #ifndef _PATH_UTMPX 11 | #include 12 | # define _PATH_UTMPX "/var/log/utmp" 13 | #endif 14 | 15 | class ShellProcess : public QProcess 16 | { 17 | friend class UnixPtyProcess; 18 | Q_OBJECT 19 | public: 20 | ShellProcess( ) 21 | : QProcess( ) 22 | , m_handleMaster(-1) 23 | , m_handleSlave(-1) 24 | { 25 | setProcessChannelMode(QProcess::SeparateChannels); 26 | } 27 | 28 | void emitReadyRead() 29 | { 30 | emit readyRead(); 31 | } 32 | 33 | protected: 34 | virtual void setupChildProcess(); 35 | 36 | private: 37 | int m_handleMaster, m_handleSlave; 38 | QString m_handleSlaveName; 39 | }; 40 | 41 | class UnixPtyProcess : public IPtyProcess 42 | { 43 | public: 44 | UnixPtyProcess(); 45 | virtual ~UnixPtyProcess(); 46 | 47 | virtual bool startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows); 48 | virtual bool resize(qint16 cols, qint16 rows); 49 | virtual bool kill(); 50 | virtual PtyType type(); 51 | virtual QString dumpDebugInfo(); 52 | virtual QIODevice *notifier(); 53 | virtual QByteArray readAll(); 54 | virtual qint64 write(const QByteArray &byteArray); 55 | virtual bool isAvailable(); 56 | void moveToThread(QThread *targetThread); 57 | 58 | private: 59 | ShellProcess m_shellProcess; 60 | QSocketNotifier *m_readMasterNotify; 61 | QByteArray m_shellReadBuffer; 62 | 63 | }; 64 | 65 | #endif // UNIXPTYPROCESS_H 66 | -------------------------------------------------------------------------------- /core/winptyprocess.cpp: -------------------------------------------------------------------------------- 1 | #include "winptyprocess.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #define DEBUG_VAR_LEGACY "WINPTYDBG" 8 | #define DEBUG_VAR_ACTUAL "WINPTY_DEBUG" 9 | #define SHOW_CONSOLE_VAR "WINPTY_SHOW_CONSOLE" 10 | #define WINPTY_AGENT_NAME "winpty-agent.exe" 11 | #define WINPTY_DLL_NAME "winpty.dll" 12 | 13 | QString castErrorToString(winpty_error_ptr_t error_ptr) 14 | { 15 | return QString::fromStdWString(winpty_error_msg(error_ptr)); 16 | } 17 | 18 | WinPtyProcess::WinPtyProcess() 19 | : IPtyProcess() 20 | , m_ptyHandler(nullptr) 21 | , m_innerHandle(nullptr) 22 | , m_inSocket(nullptr) 23 | , m_outSocket(nullptr) 24 | { 25 | 26 | } 27 | 28 | WinPtyProcess::~WinPtyProcess() 29 | { 30 | kill(); 31 | } 32 | 33 | bool WinPtyProcess::startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows) 34 | { 35 | if (!isAvailable()) 36 | { 37 | m_lastError = QString("WinPty Error: winpty-agent.exe or winpty.dll not found!"); 38 | return false; 39 | } 40 | 41 | //already running 42 | if (m_ptyHandler != nullptr) 43 | return false; 44 | 45 | QFileInfo fi(shellPath); 46 | if (fi.isRelative() || !QFile::exists(shellPath)) 47 | { 48 | //todo add auto-find executable in PATH env var 49 | m_lastError = QString("WinPty Error: shell file path must be absolute"); 50 | return false; 51 | } 52 | 53 | m_shellPath = shellPath; 54 | m_size = QPair(cols, rows); 55 | 56 | #ifdef PTYQT_DEBUG 57 | if (m_trace) 58 | { 59 | environment.append(QString("%1=1").arg(DEBUG_VAR_LEGACY)); 60 | environment.append(QString("%1=trace").arg(DEBUG_VAR_ACTUAL)); 61 | environment.append(QString("%1=1").arg(SHOW_CONSOLE_VAR)); 62 | SetEnvironmentVariable(DEBUG_VAR_LEGACY, "1"); 63 | SetEnvironmentVariable(DEBUG_VAR_ACTUAL, "trace"); 64 | SetEnvironmentVariable(SHOW_CONSOLE_VAR, "1"); 65 | } 66 | #endif 67 | 68 | //env 69 | std::wstringstream envBlock; 70 | foreach (QString line, environment) 71 | { 72 | envBlock << line.toStdWString() << L'\0'; 73 | } 74 | std::wstring env = envBlock.str(); 75 | 76 | //create start config 77 | winpty_error_ptr_t errorPtr = nullptr; 78 | winpty_config_t* startConfig = winpty_config_new(0, &errorPtr); 79 | if (startConfig == nullptr) 80 | { 81 | m_lastError = QString("WinPty Error: create start config -> %1").arg(castErrorToString(errorPtr)); 82 | return false; 83 | } 84 | winpty_error_free(errorPtr); 85 | 86 | //set params 87 | winpty_config_set_initial_size(startConfig, cols, rows); 88 | winpty_config_set_mouse_mode(startConfig, WINPTY_MOUSE_MODE_AUTO); 89 | //winpty_config_set_agent_timeout(); 90 | 91 | //start agent 92 | m_ptyHandler = winpty_open(startConfig, &errorPtr); 93 | winpty_config_free(startConfig); //start config is local var, free it after use 94 | 95 | if (m_ptyHandler == nullptr) 96 | { 97 | m_lastError = QString("WinPty Error: start agent -> %1").arg(castErrorToString(errorPtr)); 98 | return false; 99 | } 100 | winpty_error_free(errorPtr); 101 | 102 | //create spawn config 103 | winpty_spawn_config_t* spawnConfig = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, m_shellPath.toStdWString().c_str(), 104 | //commandLine.toStdWString().c_str(), cwd.toStdWString().c_str(), 105 | NULL, NULL, 106 | env.c_str(), 107 | &errorPtr); 108 | 109 | if (spawnConfig == nullptr) 110 | { 111 | m_lastError = QString("WinPty Error: create spawn config -> %1").arg(castErrorToString(errorPtr)); 112 | return false; 113 | } 114 | winpty_error_free(errorPtr); 115 | 116 | //spawn the new process 117 | BOOL spawnSuccess = winpty_spawn(m_ptyHandler, spawnConfig, &m_innerHandle, nullptr, nullptr, &errorPtr); 118 | winpty_spawn_config_free(spawnConfig); //spawn config is local var, free it after use 119 | if (!spawnSuccess) 120 | { 121 | m_lastError = QString("WinPty Error: start terminal process -> %1").arg(castErrorToString(errorPtr)); 122 | return false; 123 | } 124 | winpty_error_free(errorPtr); 125 | 126 | m_pid = (int)GetProcessId(m_innerHandle); 127 | 128 | //get pipe names 129 | LPCWSTR conInPipeName = winpty_conin_name(m_ptyHandler); 130 | m_conInName = QString::fromStdWString(std::wstring(conInPipeName)); 131 | m_inSocket = new QLocalSocket(); 132 | m_inSocket->connectToServer(m_conInName, QIODevice::WriteOnly); 133 | m_inSocket->waitForConnected(); 134 | 135 | LPCWSTR conOutPipeName = winpty_conout_name(m_ptyHandler); 136 | m_conOutName = QString::fromStdWString(std::wstring(conOutPipeName)); 137 | m_outSocket = new QLocalSocket(); 138 | m_outSocket->connectToServer(m_conOutName, QIODevice::ReadOnly); 139 | m_outSocket->waitForConnected(); 140 | 141 | if (m_inSocket->state() != QLocalSocket::ConnectedState && m_outSocket->state() != QLocalSocket::ConnectedState) 142 | { 143 | m_lastError = QString("WinPty Error: Unable to connect local sockets -> %1 / %2").arg(m_inSocket->errorString()).arg(m_outSocket->errorString()); 144 | m_inSocket->deleteLater(); 145 | m_outSocket->deleteLater(); 146 | m_inSocket = nullptr; 147 | m_outSocket = nullptr; 148 | return false; 149 | } 150 | 151 | return true; 152 | } 153 | 154 | bool WinPtyProcess::resize(qint16 cols, qint16 rows) 155 | { 156 | if (m_ptyHandler == nullptr) 157 | { 158 | return false; 159 | } 160 | 161 | bool res = winpty_set_size(m_ptyHandler, cols, rows, nullptr); 162 | 163 | if (res) 164 | { 165 | m_size = QPair(cols, rows); 166 | } 167 | 168 | return res; 169 | } 170 | 171 | bool WinPtyProcess::kill() 172 | { 173 | bool exitCode = false; 174 | if (m_innerHandle != nullptr && m_ptyHandler != nullptr) 175 | { 176 | //disconnect all signals (readyRead, ...) 177 | m_inSocket->disconnect(); 178 | m_outSocket->disconnect(); 179 | 180 | //disconnect for server 181 | m_inSocket->disconnectFromServer(); 182 | m_outSocket->disconnectFromServer(); 183 | 184 | m_inSocket->deleteLater(); 185 | m_outSocket->deleteLater(); 186 | 187 | m_inSocket = nullptr; 188 | m_outSocket = nullptr; 189 | 190 | winpty_free(m_ptyHandler); 191 | exitCode = CloseHandle(m_innerHandle); 192 | 193 | m_ptyHandler = nullptr; 194 | m_innerHandle = nullptr; 195 | m_conInName = QString(); 196 | m_conOutName = QString(); 197 | m_pid = 0; 198 | } 199 | return exitCode; 200 | } 201 | 202 | IPtyProcess::PtyType WinPtyProcess::type() 203 | { 204 | return PtyType::WinPty; 205 | } 206 | 207 | QString WinPtyProcess::dumpDebugInfo() 208 | { 209 | #ifdef PTYQT_DEBUG 210 | return QString("PID: %1, ConIn: %2, ConOut: %3, Type: %4, Cols: %5, Rows: %6, IsRunning: %7, Shell: %8") 211 | .arg(m_pid).arg(m_conInName).arg(m_conOutName).arg(type()) 212 | .arg(m_size.first).arg(m_size.second).arg(m_ptyHandler != nullptr) 213 | .arg(m_shellPath); 214 | #else 215 | return QString("Nothing..."); 216 | #endif 217 | } 218 | 219 | QIODevice *WinPtyProcess::notifier() 220 | { 221 | return m_outSocket; 222 | } 223 | 224 | QByteArray WinPtyProcess::readAll() 225 | { 226 | return m_outSocket->readAll(); 227 | } 228 | 229 | qint64 WinPtyProcess::write(const QByteArray &byteArray) 230 | { 231 | return m_inSocket->write(byteArray); 232 | } 233 | 234 | bool WinPtyProcess::isAvailable() 235 | { 236 | #ifdef PTYQT_BUILD_STATIC 237 | return QFile::exists(QCoreApplication::applicationDirPath() + "/" + WINPTY_AGENT_NAME); 238 | #elif PTYQT_BUILD_DYNAMIC 239 | return QFile::exists(QCoreApplication::applicationDirPath() + "/" + WINPTY_AGENT_NAME) 240 | && QFile::exists(QCoreApplication::applicationDirPath() + "/" + WINPTY_DLL_NAME); 241 | #endif 242 | 243 | } 244 | 245 | void WinPtyProcess::moveToThread(QThread *targetThread) 246 | { 247 | m_inSocket->moveToThread(targetThread); 248 | m_outSocket->moveToThread(targetThread); 249 | } 250 | -------------------------------------------------------------------------------- /core/winptyprocess.h: -------------------------------------------------------------------------------- 1 | #ifndef WINPTYPROCESS_H 2 | #define WINPTYPROCESS_H 3 | 4 | #include "iptyprocess.h" 5 | #include "winpty.h" 6 | 7 | class WinPtyProcess : public IPtyProcess 8 | { 9 | public: 10 | WinPtyProcess(); 11 | ~WinPtyProcess(); 12 | 13 | bool startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows); 14 | bool resize(qint16 cols, qint16 rows); 15 | bool kill(); 16 | PtyType type(); 17 | QString dumpDebugInfo(); 18 | QIODevice *notifier(); 19 | QByteArray readAll(); 20 | qint64 write(const QByteArray &byteArray); 21 | bool isAvailable(); 22 | void moveToThread(QThread *targetThread); 23 | 24 | private: 25 | winpty_t *m_ptyHandler; 26 | HANDLE m_innerHandle; 27 | QString m_conInName; 28 | QString m_conOutName; 29 | QLocalSocket *m_inSocket; 30 | QLocalSocket *m_outSocket; 31 | }; 32 | 33 | #endif // WINPTYPROCESS_H 34 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(xtermjs) 2 | -------------------------------------------------------------------------------- /examples/xtermjs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(ptyqt-xtermjs) 2 | 3 | find_package(Qt5WebSockets CONFIG REQUIRED) 4 | 5 | add_executable(xtermjs_sample xtermjs.cpp) 6 | add_dependencies(xtermjs_sample ptyqt) 7 | 8 | target_link_libraries(xtermjs_sample ptyqt Qt5::Core ${Qt5WebSockets_LIBRARIES}) 9 | 10 | if (MSVC) 11 | target_link_libraries(xtermjs_sample Qt5::Network ${WINPTY_LIBS}) 12 | endif() 13 | 14 | foreach( file_i ${WINPTY_DIST_FILES}) 15 | add_custom_command( 16 | TARGET xtermjs_sample 17 | POST_BUILD 18 | COMMAND ${CMAKE_COMMAND} 19 | ARGS -E copy ${file_i} ${CMAKE_CURRENT_BINARY_DIR} 20 | ) 21 | endforeach( file_i ) 22 | -------------------------------------------------------------------------------- /examples/xtermjs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

xterm.js + Pty-Qt + C++

13 |
14 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/xtermjs/screens/far_manager_cmd_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kafeg/ptyqt/045ca5f09fcc6417fb5fd5e42fdeedddbf68841c/examples/xtermjs/screens/far_manager_cmd_windows.png -------------------------------------------------------------------------------- /examples/xtermjs/screens/midnight_commander_bash_unix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kafeg/ptyqt/045ca5f09fcc6417fb5fd5e42fdeedddbf68841c/examples/xtermjs/screens/midnight_commander_bash_unix.png -------------------------------------------------------------------------------- /examples/xtermjs/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: helvetica, sans-serif, arial; 3 | font-size: 1em; 4 | color: #111; 5 | } 6 | 7 | h1 { 8 | text-align: center; 9 | } 10 | 11 | #terminal-container { 12 | width: 800px; 13 | height: 450px; 14 | margin: 0 auto; 15 | padding: 2px; 16 | } 17 | 18 | p { 19 | font-size: 0.9em; 20 | font-style: italic 21 | } 22 | 23 | #option-container { 24 | display: flex; 25 | justify-content: center; 26 | } 27 | 28 | .option-group { 29 | display: inline-block; 30 | padding-left: 20px; 31 | vertical-align: top; 32 | } 33 | -------------------------------------------------------------------------------- /examples/xtermjs/xtermjs.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "ptyqt.h" 5 | #include 6 | #include 7 | #include 8 | 9 | #define PORT 4242 10 | 11 | #define COLS 87 12 | #define ROWS 26 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | QCoreApplication app(argc, argv); 17 | 18 | //start WebSockets server for receive connections from xterm.js 19 | QWebSocketServer wsServer("TestServer", QWebSocketServer::NonSecureMode); 20 | if (!wsServer.listen(QHostAddress::Any, PORT)) 21 | return 1; 22 | 23 | QMap sessions; 24 | 25 | //create new session on new connection 26 | QObject::connect(&wsServer, &QWebSocketServer::newConnection, [&wsServer, &sessions]() 27 | { 28 | //handle new connection 29 | QWebSocket *wSocket = wsServer.nextPendingConnection(); 30 | 31 | //use cmd.exe or bash, depends on target platform 32 | IPtyProcess::PtyType ptyType = IPtyProcess::WinPty; 33 | qint32 buildNumber = QSysInfo::kernelVersion().split(".").last().toInt(); 34 | if (buildNumber >= CONPTY_MINIMAL_WINDOWS_VERSION) 35 | { 36 | qDebug() << "Use ConPty instead of WinPty"; 37 | ptyType = IPtyProcess::ConPty; 38 | } 39 | 40 | //force select WinPty 41 | ptyType = IPtyProcess::WinPty; 42 | 43 | QString shellPath = "c:\\Windows\\system32\\cmd.exe"; 44 | //shellPath = "C:\\Program\ Files\\Git\\bin\\bash.exe"; 45 | #ifdef Q_OS_UNIX 46 | shellPath = "/bin/sh"; 47 | ptyType = IPtyProcess::UnixPty; 48 | #endif 49 | 50 | //create new Pty instance 51 | IPtyProcess *pty = PtyQt::createPtyProcess(ptyType); 52 | 53 | qDebug() << "New connection" << wSocket->peerAddress() << wSocket->peerPort() << pty->pid(); 54 | 55 | //start Pty process () 56 | pty->startProcess(shellPath, QProcessEnvironment::systemEnvironment().toStringList(), COLS, ROWS); 57 | 58 | if (!pty->lastError().isEmpty()) 59 | { 60 | qDebug() << pty->lastError(); 61 | delete pty; 62 | return; 63 | } 64 | 65 | //connect read channel from Pty process to write channel on websocket 66 | QObject::connect(pty->notifier(), &QIODevice::readyRead, [wSocket, pty]() 67 | { 68 | QByteArray data = pty->readAll(); 69 | //qDebug() << "< " << data; 70 | wSocket->sendTextMessage(data); 71 | }); 72 | 73 | //connect read channel of Websocket to write channel of Pty process 74 | QObject::connect(wSocket, &QWebSocket::textMessageReceived, [wSocket, pty](const QString &message) 75 | { 76 | //qDebug() << "> " << message.size() << message.at(0) << message << message.toUtf8() << QString::fromUtf8(message.toUtf8()); 77 | pty->write(message.toUtf8()); 78 | }); 79 | 80 | //for example handle disconnections, process crashes and stuff like that... 81 | auto endSessionHandler = [wSocket, &sessions]() 82 | { 83 | IPtyProcess *pty = sessions.value(wSocket); 84 | if (pty == 0) 85 | return; //because can be called twice 86 | 87 | sessions.remove(wSocket); 88 | 89 | qDebug() << "wSockMn" << wSocket << pty; 90 | 91 | if (wSocket->isValid()) 92 | wSocket->close(); 93 | wSocket->deleteLater(); 94 | 95 | pty->kill(); 96 | delete pty; 97 | }; 98 | 99 | QObject::connect(wSocket, &QWebSocket::disconnected, endSessionHandler); 100 | 101 | #ifdef Q_OS_UNIX 102 | QProcess *shellProcess = qobject_cast(pty->notifier()); 103 | QObject::connect(shellProcess, QOverload::of(&QProcess::finished), 104 | [endSessionHandler](int, QProcess::ExitStatus ) { endSessionHandler(); }); 105 | #else 106 | QLocalSocket *localSocket = qobject_cast(pty->notifier()); 107 | QObject::connect(localSocket, &QLocalSocket::disconnected, endSessionHandler); 108 | #endif 109 | 110 | //add connection to list of active connections 111 | sessions.insert(wSocket, pty); 112 | 113 | qDebug() << pty->size(); 114 | }); 115 | 116 | //stop eventloop if needed 117 | //QTimer::singleShot(5000, [](){ qApp->quit(); }); 118 | 119 | //exec eventloop 120 | bool res = app.exec(); 121 | 122 | QMapIterator it(sessions); 123 | while (it.hasNext()) 124 | { 125 | it.next(); 126 | 127 | it.key()->deleteLater(); 128 | delete it.value(); 129 | } 130 | sessions.clear(); 131 | 132 | return res; 133 | } 134 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(ptyqt-tests) 2 | 3 | enable_testing() 4 | 5 | find_package(Qt5Test REQUIRED) 6 | 7 | add_executable(ptyqt_tests ptyqt_tests.cpp) 8 | add_dependencies(ptyqt_tests ptyqt) 9 | add_test(ptyqt_tests ptyqt_tests) 10 | 11 | if (MSVC) 12 | if ("${PTYQT_DEBUG}") 13 | set(WINPTY_DEBUG_SERVER_PATH ${WINPTY_ROOT_DIR}/${TARGET_ARCH}/bin/winpty-debugserver.exe) 14 | add_definitions(-DWINPTY_DEBUG_SRV_PATH="${WINPTY_DEBUG_SERVER_PATH}") 15 | message("WinPty debug server path " ${WINPTY_DEBUG_SERVER_PATH}) 16 | endif() 17 | endif() 18 | 19 | target_link_libraries(ptyqt_tests ptyqt Qt5::Core Qt5::Test) 20 | 21 | if (MSVC) 22 | target_link_libraries(ptyqt_tests Qt5::Network ${WINPTY_LIBS}) 23 | endif() 24 | 25 | foreach( file_i ${WINPTY_DIST_FILES}) 26 | add_custom_command( 27 | TARGET ptyqt_tests 28 | POST_BUILD 29 | COMMAND ${CMAKE_COMMAND} 30 | ARGS -E copy ${file_i} ${CMAKE_CURRENT_BINARY_DIR} 31 | ) 32 | endforeach( file_i ) 33 | 34 | #file(COPY ${WINPTY_DIST_FILES} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) 35 | -------------------------------------------------------------------------------- /tests/ptyqt_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ptyqt.h" 3 | #include 4 | #include 5 | #ifdef Q_OS_WIN 6 | #include 7 | #include 8 | #endif 9 | #include 10 | #include 11 | 12 | #ifdef Q_OS_WIN 13 | #ifndef _WINDEF_ 14 | typedef unsigned long DWORD; 15 | #endif 16 | #endif 17 | 18 | #define WINPTY_DBG_SERVER_NAME "winpty-debugserver.exe" 19 | #define WINPTY_AGENT_NAME "winpty-agent.exe" 20 | 21 | //increase it for visual control each shell 22 | #define DEBUG_SLEEP_SEC 1 23 | 24 | void sleepByEventLoop(int seconds) 25 | { 26 | QEventLoop sleepLoop; 27 | QTimer sleepTimer; 28 | QObject::connect(&sleepTimer, &QTimer::timeout, &sleepLoop, &QEventLoop::quit); 29 | sleepTimer.setInterval(seconds * 1000); 30 | sleepTimer.setSingleShot(true); 31 | sleepTimer.start(); 32 | sleepLoop.exec(); 33 | } 34 | 35 | #ifdef Q_OS_WIN 36 | DWORD findProcessId(const std::string& processName, int parentProcessId = 0) 37 | { 38 | PROCESSENTRY32 processInfo; 39 | processInfo.dwSize = sizeof(processInfo); 40 | 41 | HANDLE processesSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); 42 | if (processesSnapshot == INVALID_HANDLE_VALUE) 43 | { 44 | return 0; 45 | } 46 | 47 | Process32First(processesSnapshot, &processInfo); 48 | if (!processName.compare(processInfo.szExeFile)) 49 | { 50 | if (parentProcessId == 0 || parentProcessId == processInfo.th32ParentProcessID) 51 | { 52 | CloseHandle(processesSnapshot); 53 | return processInfo.th32ProcessID; 54 | } 55 | } 56 | 57 | while (Process32Next(processesSnapshot, &processInfo)) 58 | { 59 | if (!processName.compare(processInfo.szExeFile)) 60 | { 61 | if (parentProcessId == 0 || parentProcessId == processInfo.th32ParentProcessID) 62 | { 63 | CloseHandle(processesSnapshot); 64 | return processInfo.th32ProcessID; 65 | } 66 | } 67 | } 68 | 69 | CloseHandle(processesSnapshot); 70 | return 0; 71 | } 72 | 73 | void killProcessByName(QString processName) 74 | { 75 | if (findProcessId(processName.toStdString())) 76 | { 77 | system(QString("taskkill /im %1 /f").arg(processName).toStdString().c_str()); 78 | sleepByEventLoop(1); 79 | } 80 | } 81 | 82 | QStringList getShells() 83 | { 84 | QString systemRoot = QProcessEnvironment::systemEnvironment().value("windir"); 85 | if (systemRoot.trimmed().isEmpty()) 86 | systemRoot = QProcessEnvironment::systemEnvironment().value("WINDIR"); 87 | if (systemRoot.trimmed().isEmpty()) 88 | systemRoot = "c:\\Windows"; 89 | 90 | QStringList possibleShells; 91 | possibleShells << (systemRoot + "\\system32\\cmd.exe"); //reversed slashes 92 | //possibleShells << ("C:/Windows/system32/cmd.exe"); //normal slashes 93 | //possibleShells << (systemRoot + "\\system32\\WindowsPowerShell\\v1.0\\powershell.exe"); 94 | //possibleShells << "C:\\Program\ Files\\Git\\bin\\bash.exe"; 95 | //possibleShells << "C:\\Python27\\python.exe"; 96 | //possibleShells << "C:/Python27/pythonw.exe"; 97 | 98 | QStringList shells; 99 | foreach (QString possibleShell, possibleShells) 100 | { 101 | if (QFile::exists(possibleShell)) 102 | shells << possibleShell; 103 | } 104 | 105 | return shells; 106 | } 107 | #endif 108 | 109 | class PtyQtTests : public QObject 110 | { 111 | Q_OBJECT 112 | private slots: 113 | 114 | //unix unit tests 115 | #ifdef Q_OS_UNIX 116 | void unixpty() 117 | { 118 | QString shellPath = "/bin/bash"; 119 | 120 | QScopedPointer unixPty(PtyQt::createPtyProcess(IPtyProcess::UnixPty)); 121 | QCOMPARE(unixPty->type(), IPtyProcess::UnixPty); 122 | QVERIFY(unixPty->isAvailable()); 123 | 124 | //start UnixPty agent and cmd.exe 125 | bool startResult = unixPty->startProcess(shellPath, QProcessEnvironment::systemEnvironment().toStringList(), 200, 80); 126 | #ifdef PTYQT_DEBUG 127 | if (!startResult) 128 | qDebug() << unixPty->lastError() << unixPty->dumpDebugInfo(); 129 | #endif 130 | QVERIFY(startResult); 131 | 132 | //check pid 133 | QVERIFY(unixPty->pid() != 0); 134 | 135 | //check shell welcome 136 | QEventLoop el; 137 | auto connection = QObject::connect(unixPty->notifier(), &QIODevice::readyRead, [&unixPty, &el]() { 138 | sleepByEventLoop(1); 139 | qDebug() << "unixPty.read" << unixPty->readAll(); 140 | el.quit(); 141 | }); 142 | el.exec(); 143 | unixPty->notifier()->disconnect(connection); 144 | 145 | //check shell read after write 146 | bool testRes = false; 147 | connection = QObject::connect(unixPty->notifier(), &QIODevice::readyRead, [&unixPty, &el, &testRes]() { 148 | sleepByEventLoop(1); 149 | QString res = QString::fromUtf8(unixPty->readAll()); 150 | //qDebug() << res; 151 | 152 | //for e.g. bash return empty strings after needed data 153 | if (res.isEmpty() && testRes) 154 | return; 155 | 156 | testRes = res.contains("ptyqt_tests"); 157 | testRes = testRes && res.contains("Makefile"); 158 | testRes = testRes && res.contains("cmake_install.cmake"); 159 | el.quit(); 160 | }); 161 | qDebug() << "ptyin:" << unixPty->write("ls\n"); 162 | el.exec(); 163 | QVERIFY(testRes); 164 | qDebug() << "ptyin:" << unixPty->write("ls -alh\n"); 165 | el.exec(); 166 | QVERIFY(testRes); 167 | unixPty->notifier()->disconnect(connection); 168 | 169 | //resize window 170 | sleepByEventLoop(1); 171 | QVERIFY(unixPty->resize(240, 90)); 172 | } 173 | #endif 174 | 175 | //windows unit tests 176 | #ifdef Q_OS_WIN 177 | 178 | //ConPty available only on Windows 10 released after 1903 (19H1) Windows release 179 | void conpty() 180 | { 181 | qint32 buildNumber = QSysInfo::kernelVersion().split(".").last().toInt(); 182 | if (buildNumber < CONPTY_MINIMAL_WINDOWS_VERSION) 183 | { 184 | qDebug() << QString("Your Windows version doesn't support ConPty. Minimal version: %1. Your version: %2").arg(CONPTY_MINIMAL_WINDOWS_VERSION).arg(buildNumber) << QSysInfo::kernelVersion(); 185 | return; 186 | } 187 | 188 | qsrand(QDateTime::currentMSecsSinceEpoch()); 189 | 190 | QStringList shells = getShells(); 191 | 192 | foreach (QString shellPath, shells) 193 | { 194 | qDebug() << "Test" << shellPath; 195 | IPtyProcess::PtyType ptyType = IPtyProcess::ConPty; 196 | QScopedPointer conPty(PtyQt::createPtyProcess(ptyType)); 197 | QCOMPARE(conPty->type(), ptyType); 198 | QVERIFY(conPty->isAvailable()); 199 | 200 | //check shell welcome 201 | QEventLoop el; 202 | auto connection = QObject::connect(conPty->notifier(), &QIODevice::readyRead, [&conPty, &el]() { 203 | sleepByEventLoop(1); 204 | //qDebug() << "conPty.read" << conPty->readAll(); 205 | conPty->readAll(); 206 | el.quit(); 207 | }); 208 | 209 | //start ConPty agent and cmd.exe 210 | bool startResult = conPty->startProcess(shellPath, QProcessEnvironment::systemEnvironment().toStringList(), 200, 80); 211 | #ifdef PTYQT_DEBUG 212 | if (!startResult) 213 | qDebug() << conPty->lastError() << conPty->dumpDebugInfo(); 214 | #endif 215 | QVERIFY(startResult); 216 | 217 | //check pid 218 | QVERIFY(conPty->pid() != 0); 219 | 220 | //check shell welcome 221 | el.exec(); 222 | conPty->notifier()->disconnect(connection); 223 | 224 | //check shell read after write 225 | bool testRes = false; 226 | connection = QObject::connect(conPty->notifier(), &QIODevice::readyRead, [&conPty, &el, &testRes]() { 227 | sleepByEventLoop(1); 228 | QString res = QString::fromUtf8(conPty->readAll()); 229 | //qDebug() << res; 230 | 231 | //for e.g. bash return empty strings after needed data 232 | if (res.isEmpty() && testRes) 233 | return; 234 | 235 | testRes = res.contains("winpty-agent.exe"); 236 | testRes = testRes && res.contains("winpty.dll"); 237 | testRes = testRes && res.contains("ptyqt_tests.exe"); 238 | el.quit(); 239 | }); 240 | qDebug() << conPty->write("dir\r\n\r\n"); 241 | //sleepByEventLoop(1); 242 | //qDebug() << conPty->readAll(); 243 | el.exec(); 244 | QVERIFY(testRes); 245 | conPty->notifier()->disconnect(connection); 246 | 247 | //resize window 248 | sleepByEventLoop(1); 249 | QVERIFY(conPty->resize(240, 90)); 250 | 251 | //kill shell process 252 | #ifdef PTYQT_DEBUG 253 | qDebug() << conPty->dumpDebugInfo(); 254 | sleepByEventLoop(DEBUG_SLEEP_SEC); 255 | #endif 256 | QVERIFY(conPty->kill()); 257 | sleepByEventLoop(1); 258 | } 259 | } 260 | 261 | void winpty() 262 | { 263 | //QVERIFY(false); //force quit 264 | #ifdef PTYQT_DEBUG 265 | //run debug server 266 | killProcessByName(WINPTY_DBG_SERVER_NAME); 267 | 268 | qint64 debugServerPid; 269 | QProcess::startDetached(QString(WINPTY_DEBUG_SRV_PATH), QStringList() << "--everyone", 270 | QCoreApplication::applicationDirPath(), &debugServerPid); 271 | QVERIFY(debugServerPid != 0); 272 | #endif 273 | 274 | QStringList shells = getShells(); 275 | 276 | foreach (QString shellPath, shells) 277 | { 278 | qDebug() << "Test" << shellPath; 279 | 280 | //create object 281 | QScopedPointer winPty(PtyQt::createPtyProcess(IPtyProcess::WinPty)); 282 | QCOMPARE(winPty->type(), IPtyProcess::WinPty); 283 | QVERIFY(winPty->isAvailable()); 284 | 285 | //prepare to check shell welcome 286 | QEventLoop el; 287 | auto connection = QObject::connect(winPty->notifier(), &QIODevice::readyRead, [&winPty, &el]() { 288 | sleepByEventLoop(1); 289 | //qDebug() << "winPty.read" << winPty->readAll(); 290 | winPty->readAll(); 291 | el.quit(); 292 | }); 293 | 294 | //start WinPty agent and cmd.exe 295 | bool startResult = winPty->startProcess(shellPath, QProcessEnvironment::systemEnvironment().toStringList(), 200, 80); 296 | #ifdef PTYQT_DEBUG 297 | if (!startResult) 298 | qDebug() << winPty->lastError() << winPty->dumpDebugInfo(); 299 | #endif 300 | QVERIFY(startResult); 301 | 302 | //check pid (winPty->pid() - PID of child process of winpty-agent.exe) 303 | QVERIFY(winPty->pid() != 0); 304 | //DWORD winPtyAgentPid = findProcessId(QString(WINPTY_AGENT_NAME).toStdString()); 305 | //DWORD winPtyShellPid = findProcessId(QFileInfo(shellPath).fileName().toStdString(), winPtyAgentPid); 306 | //QCOMPARE(winPty->pid(), winPtyShellPid); 307 | 308 | //check shell welcome 309 | el.exec(); 310 | winPty->notifier()->disconnect(connection); 311 | 312 | //check shell read after write 313 | bool testRes = false; 314 | connection = QObject::connect(winPty->notifier(), &QIODevice::readyRead, [&winPty, &el, &testRes]() { 315 | sleepByEventLoop(1); 316 | QString res = QString::fromUtf8(winPty->readAll()); 317 | //qDebug() << res; 318 | 319 | //for e.g. bash return empty strings after needed data 320 | if (res.isEmpty() && testRes) 321 | return; 322 | 323 | testRes = res.contains("winpty-agent.exe"); 324 | testRes = testRes && res.contains("winpty.dll"); 325 | testRes = testRes && res.contains("ptyqt_tests.exe"); 326 | el.quit(); 327 | }); 328 | winPty->write("dir\r\n"); 329 | el.exec(); 330 | QVERIFY(testRes); 331 | winPty->notifier()->disconnect(connection); 332 | 333 | //resize window 334 | sleepByEventLoop(1); 335 | QVERIFY(winPty->resize(240, 90)); 336 | 337 | //kill shell and winpty-agent processes 338 | #ifdef PTYQT_DEBUG 339 | qDebug() << winPty->dumpDebugInfo(); 340 | sleepByEventLoop(DEBUG_SLEEP_SEC); 341 | #endif 342 | QVERIFY(winPty->kill()); 343 | sleepByEventLoop(1); 344 | //QCOMPARE(findProcessId(QFileInfo(shellPath).fileName().toStdString(), winPtyAgentPid), 0); 345 | //QCOMPARE(findProcessId(QString(WINPTY_AGENT_NAME).toStdString()), 0); 346 | } 347 | 348 | #ifdef PTYQT_DEBUG 349 | killProcessByName(WINPTY_DBG_SERVER_NAME); 350 | #endif 351 | } 352 | #endif 353 | }; 354 | 355 | QTEST_MAIN(PtyQtTests) 356 | #include "ptyqt_tests.moc" 357 | --------------------------------------------------------------------------------