Repository: stateful/vscode-runme
Pull request #404 (2023-04-12 19:31:15)

Use PID for background task
 - When running, "Background Task" becomes "PID: ####" (for background tasks)
   - When clicked on, this opens the task's integrated terminal
 - Removed PID from "Open Terminal" button

Note that this feature requires the latest stateful/runme executable (on main branch, not release)


Patch:
diff --git a/package-lock.json b/package-lock.json
index 0fa9b394..184ab47f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,7 @@
       "license": "Apache-2.0",
       "dependencies": {
         "@buf/grpc_grpc.community_timostamm-protobuf-ts": "^2.8.2-20230310150327-f6c75491ea13.1",
-        "@buf/stateful_runme.community_timostamm-protobuf-ts": "^2.8.2-20230307185721-114a366533aa.1",
+        "@buf/stateful_runme.community_timostamm-protobuf-ts": "^2.8.3-20230411173542-ee7abafe0a79.1",
         "@protobuf-ts/grpc-transport": "^2.8.2",
         "@protobuf-ts/runtime": "^2.8.2",
         "@protobuf-ts/runtime-rpc": "^2.8.2",
@@ -1804,11 +1804,11 @@
       }
     },
     "node_modules/@buf/stateful_runme.community_timostamm-protobuf-ts": {
-      "version": "2.8.2-20230307185721-114a366533aa.1",
-      "resolved": "https://buf.build/gen/npm/v1/@buf/stateful_runme.community_timostamm-protobuf-ts/2.8.2-20230307185721-114a366533aa.1/tarball",
+      "version": "2.8.3-20230411173542-ee7abafe0a79.1",
+      "resolved": "https://buf.build/gen/npm/v1/@buf/stateful_runme.community_timostamm-protobuf-ts/-/stateful_runme.community_timostamm-protobuf-ts-2.8.3-20230411173542-ee7abafe0a79.1.tgz",
       "peerDependencies": {
-        "@protobuf-ts/runtime": "^2.8.2",
-        "@protobuf-ts/runtime-rpc": "^2.8.2"
+        "@protobuf-ts/runtime": "^2.8.3",
+        "@protobuf-ts/runtime-rpc": "^2.8.3"
       }
     },
     "node_modules/@cspotcode/source-map-support": {
@@ -24768,8 +24768,8 @@
       "requires": {}
     },
     "@buf/stateful_runme.community_timostamm-protobuf-ts": {
-      "version": "2.8.2-20230307185721-114a366533aa.1",
-      "resolved": "https://buf.build/gen/npm/v1/@buf/stateful_runme.community_timostamm-protobuf-ts/2.8.2-20230307185721-114a366533aa.1/tarball",
+      "version": "2.8.3-20230411173542-ee7abafe0a79.1",
+      "resolved": "https://buf.build/gen/npm/v1/@buf/stateful_runme.community_timostamm-protobuf-ts/-/stateful_runme.community_timostamm-protobuf-ts-2.8.3-20230411173542-ee7abafe0a79.1.tgz",
       "requires": {}
     },
     "@cspotcode/source-map-support": {
diff --git a/package.json b/package.json
index 07479b7e..d1ebe0be 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,10 @@
         "command": "runme.openTerminal",
         "title": "Open Terminal for Background Task"
       },
+      {
+        "command": "runme.openIntegratedTerminal",
+        "title": "Open Integrated Terminal for Background Task"
+      },
       {
         "command": "runme.copyCellToClipboard",
         "title": "Copy Cell Content into Clipboard"
@@ -139,6 +143,10 @@
           "command": "runme.openTerminal",
           "when": "false"
         },
+        {
+          "command": "runme.openIntegratedTerminal",
+          "when": "false"
+        },
         {
           "command": "runme.openAsRunmeNotebook",
           "when": "false"
@@ -510,7 +518,7 @@
     "webpack-cli": "^5.0.1"
   },
   "dependencies": {
-    "@buf/stateful_runme.community_timostamm-protobuf-ts": "^2.8.2-20230307185721-114a366533aa.1",
+    "@buf/stateful_runme.community_timostamm-protobuf-ts": "^2.8.3-20230411173542-ee7abafe0a79.1",
     "@buf/grpc_grpc.community_timostamm-protobuf-ts": "^2.8.2-20230310150327-f6c75491ea13.1",
     "@protobuf-ts/grpc-transport": "^2.8.2",
     "@protobuf-ts/runtime": "^2.8.2",
diff --git a/src/extension/commands/index.ts b/src/extension/commands/index.ts
index 40eb4a27..e16f3e8a 100644
--- a/src/extension/commands/index.ts
+++ b/src/extension/commands/index.ts
@@ -17,14 +17,14 @@ function showWarningMessage () {
   return window.showWarningMessage('Couldn\'t find terminal! Was it already closed?')
 }
 
-export function openTerminal (kernel: Kernel, grpcRunner: boolean, existingExecution?: NotebookCellExecution) {
+export function openTerminal (kernel: Kernel, notebookTerminal: boolean, existingExecution?: NotebookCellExecution) {
   return async function (cell: NotebookCell) {
     const terminal = getTerminalByCell(cell)
     if (!terminal) {
       return showWarningMessage()
     }
 
-    if (isNotebookTerminalEnabledForCell(cell) && grpcRunner) {
+    if (isNotebookTerminalEnabledForCell(cell) && notebookTerminal) {
       const terminalOutput = kernel.getCellTerminalOutputPayload(cell)
 
       if (terminalOutput) {
diff --git a/src/extension/executors/runner.ts b/src/extension/executors/runner.ts
index 5fe9789f..c9867ad4 100644
--- a/src/extension/executors/runner.ts
+++ b/src/extension/executors/runner.ts
@@ -110,7 +110,8 @@ export async function executeRunner(
     exec: execution,
     envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`),
     cwd,
-    tty: interactive
+    background,
+    tty: interactive,
   })
 
   context.subscriptions.push(program)
@@ -275,16 +276,19 @@ export async function executeRunner(
       const taskId = (e.execution as any)['_id']
       const executionId = (execution as any)['_id']
 
-      if (
-        taskId !== executionId
-      ) {
-        return
-      }
+      if (taskId !== executionId) { return }
 
       const terminal = getTerminalByCell(exec.cell)
       if (!terminal) { return }
 
       terminal.runnerSession = program
+
+      // proxy pid value
+      Object.defineProperty(terminal, 'processId', {
+        get: function () {
+          return program.pid
+        }
+      })
     })
 
     tasks.onDidEndTaskProcess((e) => {
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 18f5ba8f..392db476 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -93,6 +93,7 @@ export class RunmeExtension {
 
       commands.registerCommand('runme.resetEnv', resetEnv),
       RunmeExtension.registerCommand('runme.openTerminal', openTerminal(kernel, !!grpcRunner)),
+      RunmeExtension.registerCommand('runme.openIntegratedTerminal', openTerminal(kernel, false)),
       RunmeExtension.registerCommand(
         'runme.runCliCommand',
         runCLICommand(context.extensionUri, !!grpcRunner, server, kernel)
diff --git a/src/extension/provider/background.ts b/src/extension/provider/background.ts
index 7bbada7a..061876b5 100644
--- a/src/extension/provider/background.ts
+++ b/src/extension/provider/background.ts
@@ -24,9 +24,8 @@ export class ShowTerminalProvider implements vscode.NotebookCellStatusBarItemPro
     }
 
     const terminal = getTerminalByCell(cell)
-    const pid = await terminal?.processId
 
-    if (!Boolean(terminal) || !pid) {
+    if (!Boolean(terminal)) {
       return
     }
 
@@ -35,10 +34,6 @@ export class ShowTerminalProvider implements vscode.NotebookCellStatusBarItemPro
       'Open Terminal',
     ]
 
-    if (pid > -1) {
-      terminalButtonParts.push(`(PID: ${pid})`)
-    }
-
     const item = new NotebookCellStatusBarItem(
       terminalButtonParts.join(' '),
       NotebookCellStatusBarAlignment.Right
@@ -58,9 +53,18 @@ export class ShowTerminalProvider implements vscode.NotebookCellStatusBarItemPro
 }
 
 export class BackgroundTaskProvider implements vscode.NotebookCellStatusBarItemProvider {
-  provideCellStatusBarItems(cell: vscode.NotebookCell): vscode.NotebookCellStatusBarItem | undefined {
+  async provideCellStatusBarItems(cell: vscode.NotebookCell): Promise<vscode.NotebookCellStatusBarItem | undefined> {
     const annotations = getAnnotations(cell)
 
+    const terminal = getTerminalByCell(cell)
+    const pid = await terminal?.processId
+
+    let text = 'Background Task'
+
+    if (pid !== undefined && pid > -1) {
+      text = `PID: ${pid}`
+    }
+
     /**
      * don't show if not a background task
      */
@@ -69,9 +73,14 @@ export class BackgroundTaskProvider implements vscode.NotebookCellStatusBarItemP
     }
 
     const item = new NotebookCellStatusBarItem(
-      'Background Task',
+      text,
       NotebookCellStatusBarAlignment.Right
     )
+
+    if (terminal) {
+      item.command = 'runme.openIntegratedTerminal'
+    }
+
     return item
   }
 }
diff --git a/src/extension/runner.ts b/src/extension/runner.ts
index 44e12314..16f79fc2 100644
--- a/src/extension/runner.ts
+++ b/src/extension/runner.ts
@@ -41,11 +41,11 @@ export interface RunProgramOptions {
   args?: string[]
   cwd?: string
   envs?: string[]
-  exec?:
-    RunProgramExecution
+  exec?: RunProgramExecution
   tty?: boolean
   environment?: IRunnerEnvironment
   terminalDimensions?: TerminalDimensions
+  background?: boolean
 }
 
 export interface IRunner extends Disposable {
@@ -120,6 +120,8 @@ export interface IRunnerProgramSession extends IRunnerChild, Pseudoterminal {
    */
   readonly numTerminalWindows: number
 
+  readonly pid: Promise<number|undefined>
+
   handleInput(message: string): Promise<void>
 
   setRunOptions(opts: RunProgramOptions): void
@@ -337,6 +339,7 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
   readonly _onDidClose    = this.register(new EventEmitter<number | void>())
   readonly _onStdoutRaw   = this.register(new EventEmitter<Uint8Array>())
   readonly _onStderrRaw   = this.register(new EventEmitter<Uint8Array>())
+  readonly _onPid        = this.register(new EventEmitter<number|undefined>())
 
   readonly onDidWrite = this._onDidWrite.event
   readonly onDidErr = this._onDidErr.event
@@ -358,6 +361,8 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
   protected activeTerminalWindow?: TerminalWindow
   protected terminalWindows = new Map<TerminalWindow, TerminalWindowState>()
 
+  pid = new Promise<number|undefined>(this._onPid.event)
+
   constructor(
     private readonly client: RunnerServiceClient,
     protected opts: RunProgramOptions
@@ -383,7 +388,7 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
     this.register( this._onDidClose.event(() => this.dispose()) )
     this.register( this._onInternalErr.event(() => this.dispose()) )
 
-    this.session.responses.onMessage(({ stderrData, stdoutData, exitCode }) => {
+    this.session.responses.onMessage(({ stderrData, stdoutData, exitCode, pid }) => {
       if(stdoutData.length > 0) {
         this._onStdoutRaw.fire(stdoutData)
       }
@@ -396,6 +401,10 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
         this._close({ type: 'exit', code: exitCode.value })
         this.dispose()
       }
+
+      if(pid) {
+        this._onPid.fire(Number(pid.pid))
+      }
     })
 
     this.session.responses.onComplete(() => {
@@ -437,6 +446,10 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
       this.opts.envs.push('TERM=')
     }
 
+    if (!this.opts.background) {
+      this._onPid.fire(undefined)
+    }
+
     await this.session.requests.send(GrpcRunnerProgramSession.runOptionsToExecuteRequest(this.opts))
   }
 
@@ -645,7 +658,7 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
   }
 
   static runOptionsToExecuteRequest(
-    { programName, args, cwd, environment, exec, tty, envs, terminalDimensions }: RunProgramOptions
+    { programName, args, cwd, environment, exec, tty, envs, terminalDimensions, background }: RunProgramOptions
   ): ExecuteRequest {
     if(environment && !(environment instanceof GrpcRunnerEnvironment)) {
       throw new Error('Expected gRPC environment!')
@@ -658,6 +671,7 @@ export class GrpcRunnerProgramSession implements IRunnerProgramSession {
       tty,
       sessionId: environment?.getSessionId(),
       programName,
+      background: background,
       ...exec?.type === 'commands' && { commands: exec.commands },
       ...exec?.type === 'script' && { script: exec.script },
       ...terminalDimensions && { winsize: terminalDimensionsToWinsize(terminalDimensions) },
diff --git a/src/types.ts b/src/types.ts
index 487d6933..a3040e37 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,4 @@
-import { NotebookCellKind, TaskDefinition, Terminal, TerminalDimensions } from 'vscode'
+import { NotebookCellKind, TaskDefinition, type Terminal, TerminalDimensions } from 'vscode'
 import { z } from 'zod'
 
 import { OutputType, ClientMessages } from './constants'


Test patch:
diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts
index 47aaf576..c3950442 100644
--- a/tests/extension/extension.test.ts
+++ b/tests/extension/extension.test.ts
@@ -54,7 +54,7 @@ test('initializes all providers', async () => {
   await ext.initialize(context)
   expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(6)
   expect(workspace.registerNotebookSerializer).toBeCalledTimes(1)
-  expect(commands.registerCommand).toBeCalledTimes(17)
+  expect(commands.registerCommand).toBeCalledTimes(18)
   expect(window.registerTreeDataProvider).toBeCalledTimes(1)
   expect(window.registerUriHandler).toBeCalledTimes(1)
 
diff --git a/tests/extension/provider/background.test.ts b/tests/extension/provider/background.test.ts
index e40c8ebf..9c60aabe 100644
--- a/tests/extension/provider/background.test.ts
+++ b/tests/extension/provider/background.test.ts
@@ -47,7 +47,7 @@ describe('ShowTerminalProvider', () => {
     const p = new ShowTerminalProvider()
     const item = await p.provideCellStatusBarItems('cell' as any)
     expect(item).toEqual({
-      label: '$(terminal) Open Terminal (PID: 123)',
+      label: '$(terminal) Open Terminal',
       command: 'runme.openTerminal',
       alignment: 2
     })
diff --git a/tests/extension/runner.test.ts b/tests/extension/runner.test.ts
index d25e7197..ffc811d2 100644
--- a/tests/extension/runner.test.ts
+++ b/tests/extension/runner.test.ts
@@ -1,7 +1,6 @@
 import path from 'node:path'
 
 import { vi, suite, test, expect, beforeEach } from 'vitest'
-import type { ExecuteResponse } from '@buf/stateful_runme.community_timostamm-protobuf-ts/runme/runner/v1/runner_pb'
 import { type GrpcTransport } from '@protobuf-ts/grpc-transport'
 import { EventEmitter } from 'vscode'
 
@@ -11,6 +10,7 @@ import {
   GrpcRunnerProgramSession,
   RunProgramOptions
 } from '../../src/extension/runner'
+import type { ExecuteResponse } from '../../src/extension/grpc/runnerTypes'
 
 
 vi.mock('../../src/extension/utils', () => ({
@@ -211,6 +211,32 @@ suite('grpc Runner', () => {
       expect(stderrListener).toBeCalledWith(Buffer.from('test'))
     })
 
+    test('duplex onMessage exposes PID in background mode', async () => {
+      const { duplex, session } = await createNewSession({ background: true })
+
+      duplex._onMessage.fire({
+        stdoutData: Buffer.from(''),
+        stderrData: Buffer.from(''),
+        pid: {
+          pid: '1234'
+        }
+      })
+
+      const pid = await session.pid
+
+      expect(pid).toStrictEqual(1234)
+    })
+
+    test('PID is undefined in non-background mode', async () => {
+      const { session } = await createNewSession()
+
+      await session.run()
+
+      const pid = await session.pid
+
+      expect(pid).toBeUndefined()
+    })
+
     test('duplex onMessage calls onDidWrite', async () => {
       const { duplex, writeListener } = await createNewSession()