Skip to content

Commit

Permalink
Feature/terminal (#75)
Browse files Browse the repository at this point in the history
* ✨ Terminal >> Adding new Page: User can create multiple terminal sessions on the host system.
* 👷 Build >> Changing build process. User needs to install one dependency on production.
* 👷 Build >> Reducing package.json for production. Stuff like scripts, dependencies...
  • Loading branch information
borsTiHD authored Aug 5, 2021
1 parent a425af7 commit 5f4ce0d
Show file tree
Hide file tree
Showing 17 changed files with 1,079 additions and 20 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Terminal: New page to start terminals on the host system.

## [0.1.0] - 2021-07-31
### Added
Expand Down
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,21 @@ Welcome to Pi-Control 🐱‍👤.
This is an App for controlling your raspberry pi.
It was designed to help with information gathering, as well as to simplify control.


## Requirements
- NodeJS: 16.x
- Yarn: 1.22.x
- **NodeJS**: `16.x`
- **Yarn**: `1.22.x`
- **Npm**: _not testet (optional)_

## Install & running the app

Download the latest version from releases: https://github.com/borsTiHD/pi-control/releases
Extract the archive, change to the subdirectory and start the app with the following command:
Download the latest version from releases: [Release / Download](https://github.com/borsTiHD/pi-control/releases)
Extract the archive, change to the subdirectory, install dependencies and start the app:
```bash
$ sudo yarn start
$ cd pi-control
$ yarn install # npm install
$ sudo yarn start # sudo npm start
```

## Deploy with pm2 (run as a service with start on reboot, or crash)

# Deploy with [pm2](https://pm2.keymetrics.io/) (run as a service with start on reboot, or crash)
If you want to run the app in the background, or do you want to open the app automatically on restart, you can use pm2.
```bash
# maybe you need to add 'sudo' for every command, even to start 'pm2' service so it can edit files for example
Expand All @@ -33,7 +33,6 @@ $ sudo pm2 start ecosystem.json
```

### Additional pm2 commands

```bash
# check status by
$ sudo pm2 ls
Expand All @@ -52,7 +51,6 @@ $ sudo pm2 stop pi-control
$ sudo pm2 delete all
```

## Contribution

# Contribution
If you want to contribute to this project, please take a look into the wiki:
- https://github.com/borsTiHD/pi-control/wiki
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nuxtjs/auth-next": "5.0.0-1624817847.21691f1",
"@nuxtjs/axios": "^5.13.6",
"core-js": "^3.15.2",
"node-pty": "^0.10.1",
"nuxt": "^2.15.7"
},
"devDependencies": {
Expand Down Expand Up @@ -67,6 +68,9 @@
"vuex-persistedstate": "^4.0.0",
"webpack": "^5.46.0",
"webpack-cli": "^4.7.2",
"webpack-node-externals": "^3.0.0"
"webpack-node-externals": "^3.0.0",
"xterm": "^4.13.0",
"xterm-addon-fit": "^0.5.0",
"xterm-addon-unicode11": "^0.2.0"
}
}
56 changes: 56 additions & 0 deletions src/api/socket.io/controllers/shellSession.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pty from 'node-pty'

const isWin = process.platform === 'win32'
const isLinux = process.platform === 'linux'

export default (cbHandler = () => {}, cbOnClose = () => {}) => {
// Callback function will emit data output from the session
// Create child process
function spawnPty() {
const shell = isWin ? 'powershell.exe' : 'bash'
if (isLinux || isWin) {
const ptyProcess = pty.spawn(shell, [], {
name: 'xterm-color',
cols: 80,
rows: 30,
cwd: process.env.HOME,
env: process.env
})
return ptyProcess
} else {
// Not supporting os
throw new Error(`Could not spawn a terminal session. The operating system used is not supported: ${process.platform}`)
}
}

// Create session
const session = {
terminal: spawnPty(),
handler: cbHandler,
send(data) {
this.terminal.write(data)
},
resize(cols, rows) {
this.terminal.resize(Number(cols), Number(rows))
},
kill() {
this.terminal.kill()
},
getPid() {
return this?.terminal?.pid
}
}

// Handle Data
session.terminal.onData((data) => {
session.handler({ _status: 'ok', type: 'data', data })
})

// Handle Closure
session.terminal.onExit(({ exitCode, signal }) => {
session.handler({ _status: 'ok', type: 'closure', data: exitCode, signal })
cbOnClose({ exitCode, signal })
})

return session
}
6 changes: 5 additions & 1 deletion src/api/socket.io/listeners/connection.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import initRoomHandlers from './roomHandlers.js'
import initTerminal from './terminal.js'

export default (io) => {
// On client connection
io.on('connection', (socket) => {
console.log('[Socket.io] -> Client connected...')
console.log(`[Socket.io] -> Client '${socket.id}' connected...`)

// Event: 'room:join' / 'room:leave'
initRoomHandlers(io, socket)

// Event: Terminal Events
initTerminal(io, socket)

// Event: 'disconnect' - Fires when a client disconnects
socket.on('disconnect', function() {
console.log('[Socket.io] -> Client disconnected...')
Expand Down
192 changes: 192 additions & 0 deletions src/api/socket.io/listeners/terminal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import Terminal from '../models/terminal.js'
import shellSession from '../controllers/shellSession.js'

export default (io, socket) => {
// Create user for every socket
function init() {
// Create a Terminal User on database on connection
Terminal.CreateUser(socket.id)
}
init()

// Updating existing terminals for frontend
function sendAllTerminals() {
clearTerminals() // Cleanup empty terminal data

// Sending all terminals, but only the id's
const user = Terminal.GetUser(socket.id)
socket.emit('getAllTerminals', { _status: 'ok', terminals: user.terminals.map((x) => { return { id: x.id } }) }) // Map returns only the id property
}

// Deleting not working terminals from database
function clearTerminals() {
function deleteTerminalId(terminalId, terminalObj) {
const errorInfo = `Terminal ID ${terminalId} not working. Deleting ID from database...`
console.error(`[Socket.io] -> ${errorInfo}`, terminalObj)
socket.emit('terminalMessage', { type: 'error', data: errorInfo })
// Deleting terminal from database
Terminal.DeleteTerminal(socket.id, terminalId)
}

// Getting user data
const user = Terminal.GetUser(socket.id)
user.terminals.forEach((obj) => {
// Trying to detect defectice terminals
try {
// Check if terminal object exists
if (!obj.terminal) {
console.error(`[Socket.io] -> Terminal ID "${obj.id}": Object not existing`)
deleteTerminalId(obj.id, obj.terminal)
}

// Check if pid can be determined
const pid = obj.terminal.getPid()
if (!pid) {
console.error(`[Socket.io] -> Terminal ID "${obj.id}": Pid could not be determined`)
deleteTerminalId(obj.id, obj.terminal)
}
} catch (error) {
console.error(`[Socket.io] -> Terminal ID "${obj.id}": Undefined error occurred`)
deleteTerminalId(obj.id, obj.terminal)
}
})
}

// Event: 'new-terminal' - Create a new Terminal instance
socket.on('new-terminal', (message) => {
if (message) {
console.log(`[Socket.io] -> Terminal: Client '${socket.id}' wants to start a new terminal`)
try {
// Adding an empty terminal to database
// Return value is the id for the terminal object
const id = Terminal.NewTerminal(socket.id)
if (!id) {
// No session could be established, because user does not exists
throw new Error('Could not create a terminal because the user does not exist')
}

// Creating new terminal session
// Callback function will emit data output
const session = shellSession((data) => {
// data = { _status: 'ok', type: 'data', data: 'buffer.toString()' }
socket.emit('terminal', { id, ...data })
})

// Saving session instance in database
Terminal.AddTerminal(socket.id, id, session)

// Sending updated terminals to frontend
sendAllTerminals()
} catch (error) {
console.error('[Socket.io] -> Cant create a new terminal session:', error)
socket.emit('terminalMessage', { type: 'error', data: error.message })
clearTerminals() // Cleanup empty terminal data
}
}
})

// Event: 'get-all-terminals' - Get all open terminal ID's
socket.on('get-all-terminals', (message) => {
if (message) {
console.log(`[Socket.io] -> Terminal: Client '${socket.id}' wants to get all terminals`)
sendAllTerminals() // Sending updated terminals to frontend
}
})

// Event: 'resize-terminal' - Resize terminal
socket.on('resize-terminal', (message) => {
// Message = { id: terminalId, cols: number, rows: number }
if (message) {
try {
const terminal = Terminal.GetTerminal(socket.id, message.id)
if (terminal) {
terminal.resize(message.cols, message.rows)
} else {
throw new Error('Terminal does not exist')
}
} catch (error) {
// console.error(`[Socket.io] -> Terminal: Client '${socket.id}' - Error on resizing terminal session:`, error)
// socket.emit('terminalMessage', { type: 'error', data: `Error on resizing terminal session: ${error.message}` })
}
}
})

// Event: 'close-terminal' - User wants to close terminal with id
socket.on('close-terminal', (terminalID) => {
if (terminalID) {
console.log(`[Socket.io] -> Terminal: Client '${socket.id}' wants to close terminal ID '${terminalID}'`)

// Getting terminal and kill process
const terminal = Terminal.GetTerminal(socket.id, terminalID)
if (terminal) {
terminal.kill()
} else {
throw new Error('Terminal does not exist')
}

// Deleting terminal from database
Terminal.DeleteTerminal(socket.id, terminalID)

// Sending updated terminals to frontend
sendAllTerminals()
}
})

// Event: 'close-all-terminals' - User wants to close all terminals
socket.on('close-all-terminals', (message) => {
if (message) {
console.log(`[Socket.io] -> Terminal: Client '${socket.id}' wants to close all terminals`)

// Killing all existing terminals for this user
const user = Terminal.GetUser(socket.id)
user.terminals.forEach((obj) => {
obj.terminal.kill()
})

// Deleting all terminals
Terminal.DeleteAllTerminals(socket.id)

// Sending updated terminals to frontend
sendAllTerminals()
}
})

// Event: 'send-to-terminal'
socket.on('send-to-terminal', (message) => {
// Message: { id: terminalID, data: '...' }
const terminalId = message.id
const data = message.data
try {
// Getting terminal and sending data
const terminal = Terminal.GetTerminal(socket.id, terminalId)
if (terminal) {
terminal.send(data)
} else {
throw new Error('Terminal does not exist')
}
} catch (error) {
console.error(`[Socket.io] -> Terminal: Client '${socket.id}' - Error on sending data to terminal session:`, error)
socket.emit('terminalMessage', { type: 'error', data: `Error on sending data to terminal session: ${error.message}` })
}
})

// Event: 'disconnect' - Fires when a client disconnects
socket.on('disconnect', () => {
console.log(`[Socket.io] -> Terminal: Client '${socket.id}' disconnects - closing all terminals and deleting user from database`)

// Killing all existing terminals for this user
const user = Terminal.GetUser(socket.id)
user.terminals.forEach((obj) => {
if (obj.terminal) {
try {
obj.terminal.kill()
} catch (error) {
// Terminal session not existing...
}
}
})

// Deleting all user Data
Terminal.DeleteUser(socket.id)
})
}
Loading

0 comments on commit 5f4ce0d

Please sign in to comment.