Repository: stateful/vscode-runme
Pull request #376 (2023-04-04 01:49:00)

Windows shell survey
Respectfully ask Windows users to share what default shell they are using so we can prioritize better.

<img width="1040" alt="image" src="https://user-images.githubusercontent.com/250527/229665646-73fff4b6-207d-4ff1-a075-826580ee1d50.png">



Patch:
diff --git a/package.json b/package.json
index cfe70006..8b0aa291 100644
--- a/package.json
+++ b/package.json
@@ -83,6 +83,11 @@
         "category": "Runme",
         "title": "Reset Environment Variables for current VS Code session"
       },
+      {
+        "command": "runme.surveyWinDefaultShell",
+        "category": "Runme",
+        "title": "Prompt to participate in Windows Default Shell survey"
+      },
       {
         "command": "runme.openAsRunmeNotebook",
         "category": "Runme",
diff --git a/src/extension/executors/utils.ts b/src/extension/executors/utils.ts
index 5c58d8fd..d9da2d14 100644
--- a/src/extension/executors/utils.ts
+++ b/src/extension/executors/utils.ts
@@ -1,6 +1,5 @@
 import cp from 'node:child_process'
 import path from 'node:path'
-import os from 'node:os'
 
 import { NotebookCellOutput, NotebookCellExecution, NotebookCellOutputItem, window } from 'vscode'
 
@@ -251,7 +250,3 @@ export async function parseCommandSeq(
   return parsedCommandBlocks
     .flatMap(({ type, content }) => (type === 'block' && parseBlock?.(content)) || (content ? [content] : []))
 }
-
-export function isWindows(): boolean {
-  return os.platform().startsWith('win')
-}
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 7c74d0a9..77363a86 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -27,6 +27,7 @@ import { RunmeUriHandler } from './handler/uri'
 import { BOOTFILE } from './constants'
 import { GrpcRunner, IRunner } from './runner'
 import { CliProvider } from './provider/cli'
+import * as survey from './survey'
 
 export class RunmeExtension {
   async initialize(context: ExtensionContext) {
@@ -64,12 +65,14 @@ export class RunmeExtension {
 
     const treeViewer = new RunmeLauncherProvider(getDefaultWorkspace())
     const uriHandler = new RunmeUriHandler(context)
+    const winSurvey = new survey.WinDefaultShell(context)
 
     context.subscriptions.push(
       kernel,
       serializer,
       server,
       treeViewer,
+      winSurvey,
       workspace.registerNotebookSerializer(Kernel.type, serializer, {
         transientOutputs: true,
         transientCellMetadata: {
diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts
index e68d47d9..48dcff74 100644
--- a/src/extension/kernel.ts
+++ b/src/extension/kernel.ts
@@ -21,11 +21,10 @@ import { API } from '../utils/deno/api'
 
 import executor, { type IEnvironmentManager, ENV_STORE_MANAGER } from './executors'
 import { DENO_ACCESS_TOKEN_KEY } from './constants'
-import { resetEnv, getKey, getAnnotations, hashDocumentUri, processEnviron } from './utils'
+import { resetEnv, getKey, getAnnotations, hashDocumentUri, processEnviron, isWindows } from './utils'
 import './wasm/wasm_exec.js'
 import { IRunner, IRunnerEnvironment } from './runner'
 import { executeRunner } from './executors/runner'
-import { isWindows } from './executors/utils'
 import { ITerminalState, NotebookTerminalType, XTermState } from './terminal/terminalState'
 
 enum ConfirmationItems {
diff --git a/src/extension/survey.ts b/src/extension/survey.ts
new file mode 100644
index 00000000..ee9ead70
--- /dev/null
+++ b/src/extension/survey.ts
@@ -0,0 +1,150 @@
+import path from 'node:path'
+import { mkdirSync, readFileSync, unlinkSync } from 'node:fs'
+
+import {
+  Disposable,
+  NotebookDocument,
+  workspace,
+  window,
+  ExtensionContext,
+  Task,
+  TaskScope,
+  ShellExecution,
+  TaskRevealKind,
+  TaskPanelKind,
+  tasks,
+  Uri,
+  commands,
+} from 'vscode'
+import { TelemetryReporter } from 'vscode-telemetry'
+
+import { Kernel } from './kernel'
+import { isWindows } from './utils'
+
+export class WinDefaultShell implements Disposable {
+  static readonly #id: string = 'runme.surveyWinDefaultShell'
+  readonly #tempDir: Uri
+  readonly #context: ExtensionContext
+  readonly #disposables: Disposable[] = []
+
+  constructor(context: ExtensionContext) {
+    commands.registerCommand(WinDefaultShell.#id, this.#prompt.bind(this))
+
+    this.#tempDir = context.globalStorageUri
+    this.#context = context
+
+    // Only prompt on Windows
+    if (!isWindows()) {
+      return
+    }
+
+    this.#disposables.push(
+      workspace.onDidOpenNotebookDocument(this.#handleOpenNotebook.bind(this))
+    )
+  }
+
+  async #handleOpenNotebook({ notebookType }: NotebookDocument) {
+    if (
+      notebookType !== Kernel.type ||
+      this.#context.globalState.get<boolean>(
+        WinDefaultShell.#id,
+        false
+      )
+    ) {
+      return
+    }
+
+    await new Promise<void>(resolve => setTimeout(resolve, 2000))
+    await commands.executeCommand(WinDefaultShell.#id, false)
+  }
+
+  async #prompt(runDirect = true) {
+    // reset done when run from command palette
+    if (runDirect) {
+      await this.#undo()
+    }
+
+    const option = await window.showInformationMessage(
+      'Please help us improve Runme on Windows: Click OK to share what default shell you are using.',
+      'OK',
+      'Don\'t ask again',
+      'Dismiss'
+    )
+    if (option === 'Dismiss') {
+      return
+    } else if (option !== 'OK') {
+      await this.#done()
+      return
+    }
+
+    mkdirSync(this.#tempDir.fsPath, { recursive: true })
+
+    const name = 'Runme Windows Shell'
+    const tmpfile = path.join(this.#tempDir.fsPath, 'defaultShell')
+    try {
+      unlinkSync(tmpfile)
+    } catch (err) {
+      if (err instanceof Error) {
+        console.log(err.message)
+      }
+    }
+    // eslint-disable-next-line max-len
+    const cmdline = `echo $SHELL > "${tmpfile}"; echo $PSVersionTable | Out-File -Encoding utf8 "${tmpfile}"`
+    const taskExecution = new Task(
+      { type: 'shell', name },
+      TaskScope.Workspace,
+      name,
+      'exec',
+      new ShellExecution(cmdline)
+    )
+
+    taskExecution.isBackground = true
+    taskExecution.presentationOptions = {
+      focus: false,
+      reveal: TaskRevealKind.Never,
+      panel: TaskPanelKind.Dedicated
+    }
+
+    const exitCode = await new Promise<number>((resolve) => {
+      tasks.executeTask(taskExecution).then((execution) => {
+        this.#disposables.push(tasks.onDidEndTaskProcess((e) => {
+          const taskId = (e.execution as any)['_id']
+          const executionId = (execution as any)['_id']
+
+          if (
+            taskId !== executionId ||
+            typeof e.exitCode === 'undefined'
+          ) {
+            return
+          }
+
+          // non-zero exit code does not mean failure
+          resolve(e.exitCode)
+        }))
+      })
+    })
+
+    try {
+      const output = readFileSync(tmpfile, { encoding: 'utf-8' }).trim()
+      TelemetryReporter.sendTelemetryEvent('survey.WinDefaultShell', { output, exitCode: exitCode.toString() })
+      await this.#done()
+      unlinkSync(tmpfile)
+    } catch (err) {
+      if (err instanceof Error) {
+        console.log(err.message)
+      }
+    }
+  }
+
+  async #undo() {
+    await this.#context.globalState.update(WinDefaultShell.#id, false)
+  }
+
+  async #done() {
+    await this.#context.globalState.update(WinDefaultShell.#id, true)
+  }
+
+  dispose() {
+    this.#disposables.forEach(d => d.dispose())
+  }
+}
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index d5e277a0..6719b58a 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -1,6 +1,7 @@
 import path from 'node:path'
 import util from 'node:util'
 import cp from 'node:child_process'
+import os from 'node:os'
 
 import vscode, {
   FileType,
@@ -319,3 +320,7 @@ export async function isPortAvailable(port: number): Promise<boolean> {
 export function processEnviron(): string[] {
   return Object.entries(process.env).map(([k, v]) => `${k}=${v || ''}`)
 }
+
+export function isWindows(): boolean {
+  return os.platform().startsWith('win')
+}
diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts
index f0b96f97..78e04d4a 100644
--- a/src/utils/configuration.ts
+++ b/src/utils/configuration.ts
@@ -5,8 +5,7 @@ import { NotebookCell, Uri, workspace } from 'vscode'
 import { z } from 'zod'
 import { v4 as uuidv4 } from 'uuid'
 
-import { isWindows } from '../extension/executors/utils'
-import { getAnnotations } from '../extension/utils'
+import { getAnnotations, isWindows } from '../extension/utils'
 
 const SERVER_SECTION_NAME = 'runme.server'
 const TERMINAL_SECTION_NAME= 'runme.terminal'
@@ -19,7 +18,7 @@ const configurationSchema = {
         port: z
             .number()
             .positive()
-            .min(8080)
+            .min(7070)
             .max(9090)
             .default(7863),
         binaryPath: z


Test patch:
diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts
index c5899bfb..06ff1be6 100644
--- a/tests/extension/extension.test.ts
+++ b/tests/extension/extension.test.ts
@@ -42,7 +42,7 @@ test('initializes all providers', async () => {
   await ext.initialize(context)
   expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(6)
   expect(workspace.registerNotebookSerializer).toBeCalledTimes(1)
-  expect(commands.registerCommand).toBeCalledTimes(16)
+  expect(commands.registerCommand).toBeCalledTimes(17)
   expect(window.registerTreeDataProvider).toBeCalledTimes(1)
   expect(window.registerUriHandler).toBeCalledTimes(1)
 
diff --git a/tests/extension/server/runmeServer.test.ts b/tests/extension/server/runmeServer.test.ts
index 1a6013b7..713192ee 100644
--- a/tests/extension/server/runmeServer.test.ts
+++ b/tests/extension/server/runmeServer.test.ts
@@ -11,6 +11,7 @@ vi.mock('../../../src/extension/grpc/client', () => ({ initParserClient: vi.fn()
 
 vi.mock('../../../src/extension/utils', () => ({
   isPortAvailable: vi.fn(async () => true),
+  isWindows: vi.fn().mockReturnValue(false)
 }))
 
 vi.mock('node:fs/promises', async () => ({
diff --git a/vitest.conf.ts b/vitest.conf.ts
index 187d6d24..b87c1dd8 100644
--- a/vitest.conf.ts
+++ b/vitest.conf.ts
@@ -12,7 +12,7 @@ export default defineConfig({
       enabled: true,
       exclude: ['**/build/**', '**/__fixtures__/**', '**/*.test.ts'],
       statements: 43,
-      branches: 88.4,
+      branches: 88.3,
       functions: 32.6,
       lines: 43
     }