+
+
+
+
Developer Tooling
+
+
Remotely inspect Pear applications.
+
+
Some application setup is required to enable remote debugging
+
First install the pear-inspect module into the application
+
npm install pear-inspect
+
Then add the following JavaScript code to the application, at the top:
+
+
if (Pear.config.dev) {
+ const { Inspector } = await import('pear-inspect')
+ const inpector = await new Inspector()
+ const key = await inpector.enable()
+ console.log('Pear Inspector key:', key.toString('hex'))
+}
+
+
When the application is opened in development mode the inspection key will be logged.
+
Paste the logged key into the input and use a compatible inspect protocol tool, such as chrome://inspect to view the remote target
+
+
+
+
Apps
+
No apps added. Add an inspect key to start debugging.
+
+
+
+ `
+ this.root = this.attachShadow({ mode: 'open' })
+ this.root.appendChild(this.template.content.cloneNode(true))
+
+ this.addKeyFormElem = this.root.querySelector('#add-key-form')
+ this.addKeyInputElem = this.root.querySelector('#add-key-input')
+ this.addKeyErrorElem = this.root.querySelector('#add-key-error')
+ this.appsElem = this.root.querySelector('#apps')
+ this.noAppsElem = this.root.querySelector('#no-apps')
+ this.apps = new Map()
+
+ this.addKeyInputElem.addEventListener('keypress', e => {
+ this.addKeyErrorElem.textContent = ''
+ })
+
+ this.addKeyFormElem.addEventListener('submit', e => {
+ e.preventDefault()
+ const inspectorKey = this.addKeyInputElem.value
+ if (inspectorKey.length !== 64) {
+ this.addKeyErrorElem.textContent = 'Key needs to be 64 characters long'
+ return
+ }
+
+ const sessionId = generateUuid()
+ const inspectorSession = new Session({ inspectorKey: b4a.from(inspectorKey, 'hex') })
+ const app = {
+ inspectorKey,
+ title: '',
+ url: '',
+ inspectorSession
+ }
+
+ inspectorSession.on('close', () => {
+ this.apps.delete(sessionId)
+ this.renderApps()
+ })
+ inspectorSession.on('info', ({ filename }) => {
+ app.url = filename
+ app.title = filename.split('/').pop()
+ this.apps.set(sessionId, app)
+ this.renderApps()
+ })
+
+ this.addKeyInputElem.value = ''
+ this.addKeyErrorElem.textContent = ''
+ this.renderApps()
+ })
+
+ const devtoolsHttpServer = http.createServer()
+ const devToolsWsServer = new WebSocketServer({ noServer: true })
+
+ devtoolsHttpServer.listen(9229, () => console.log('[devtoolsHttpServer] running on port 9229'))
+ devtoolsHttpServer.on('request', (req, res) => {
+ if (req.url !== '/json/list') {
+ res.writeHead(404)
+ res.end()
+ return
+ }
+
+ const targets = [...this.apps].map(([sessionId, app]) => ({
+ description: 'node.js instance', // `Pear app: ${app.name}`,
+ devtoolsFrontendUrl: `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/${sessionId}`,
+ devtoolsFrontendUrlCompat: `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/${sessionId}`,
+ faviconUrl: 'https://nodejs.org/static/images/favicons/favicon.ico',
+ id: sessionId,
+ title: app.title,
+ type: 'node',
+ url: `file://${app.url}`,
+ webSocketDebuggerUrl: `ws://127.0.0.1:9229/${sessionId}`
+ }))
+
+ res.writeHead(200, {
+ 'Content-Type': 'application/json; charset=UTF-8',
+ 'Cache-Control': 'no-cache',
+ 'Content-Length': JSON.stringify(targets).length
+ })
+ res.end(JSON.stringify(targets))
+ })
+ devtoolsHttpServer.on('upgrade', (request, socket, head) => {
+ console.log(`[devtoolsHttpServer] UPGRADE. url=${request.url}`)
+ const sessionId = request.url.substr(1)
+ const sessionIdExists = this.apps.has(sessionId)
+ if (!sessionIdExists) return socket.destroy()
+
+ devToolsWsServer.handleUpgrade(request, socket, head, ws => devToolsWsServer.emit('connection', ws, request))
+ })
+
+ devToolsWsServer.on('connection', (devtoolsSocket, request) => {
+ const sessionId = request.url.substr(1)
+ const app = this.apps.get(sessionId)
+ if (!app) return devtoolsSocket.destroy()
+
+ const { inspectorSession } = app
+
+ const onMessage = msg => {
+ const { pearInspectMethod } = msg
+ const isACDPMessage = !pearInspectMethod
+
+ if (isACDPMessage) return devtoolsSocket.send(JSON.stringify(msg))
+ }
+ inspectorSession.connect()
+ inspectorSession.on('message', onMessage)
+ devtoolsSocket.on('message', msg => inspectorSession.post(JSON.parse(msg)))
+ devtoolsSocket.on('close', () => {
+ inspectorSession.disconnect()
+ inspectorSession.off('message', onMessage)
+ app.connected = false
+ this.renderApps()
+ })
+
+ app.connected = true
+ this.renderApps()
+ })
+ }
+
+ renderApps () {
+ this.appsElem.replaceChildren(...[...this.apps].map(([sessionId, app]) => {
+ const div = document.createElement('div')
+ div.innerHTML = `
+
+
✕
+
${app.title} (${app.url})
+
+ `
+ div.querySelector('.remove').addEventListener('click', () => {
+ this.apps.delete(sessionId)
+ this.renderApps()
+ })
+
+ return div
+ }))
+
+ if (this.apps.size > 0) {
+ this.noAppsElem.classList.add('hidden')
+ } else {
+ this.noAppsElem.classList.remove('hidden')
+ }
+
+ }
+
+ load () {
+ this.style.display = ''
+ }
+
+ unload () {
+ this.style.display = 'none'
+ }
+})
+
+// Can't use `uuid` module for some reason as it results in a throw with `crypto` when importing
+function generateUuid () {
+ let result, i, j
+ result = ''
+ for (j = 0; j < 32; j++) {
+ if (j === 8 || j === 12 || j === 16 || j === 20) {
+ result = result + '-'
+ }
+ i = Math.floor(Math.random() * 16).toString(16)
+ result = result + i
+ }
+ return result
+}
diff --git a/package.json b/package.json
index 7e911e7..7a53994 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,10 @@
}
},
"dependencies": {
- "redhat-overpass-font": "^1.0.0"
+ "@holepunchto/pear-inspect": "^3.0.1",
+ "b4a": "^1.6.4",
+ "bare-path": "^2.1.0",
+ "redhat-overpass-font": "^1.0.0",
+ "ws": "^8.16.0"
}
}