Repository: oliverschwendener/ueli (ts)

Issue:
[Bug] "Include apps from windows store" does not work
Application Search is still broken for me on 9.4.0. This could potentially be related to #1124 and/or #1030.

Steps to reproduce the behavior:
1. Open Ueli (via the global keyboard shortcut or right click on tray icon -> Show)
2. Try to search for any installed application
3. Notice that no applications show up


Debug logs:
I noticed the following debug log message appears every time the scanner re-searches for applications:

```
ERROR] Failed to get search result items for extension with id 'ApplicationSearch.Reason: RangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]: stdout maxBuffer length exceeded'
```

Environment:
 - OS: Windows 11
 - Version 9.4.0

Notes:
Sometimes playing around with the settings under the _Search Engine_ section, some of the installed applications start to show, but not all of them, and not all the time.


Pull request description:
refactor(CommandlineUtility): added `maxBuffer` option
This PR adds a parameter `maxBuffer` when executing cli programs.

Might fix #1189

Patch:
diff --git a/src/main/Core/CommandlineUtility/Contract/CommandlineUtility.ts b/src/main/Core/CommandlineUtility/Contract/CommandlineUtility.ts
index 6958ed4d..f480203b 100644
--- a/src/main/Core/CommandlineUtility/Contract/CommandlineUtility.ts
+++ b/src/main/Core/CommandlineUtility/Contract/CommandlineUtility.ts
@@ -5,9 +5,17 @@ export interface CommandlineUtility {
     /**
      * Executes a command and returns the output.
      * @param command The command to execute, e.g. `"echo Hello World"`.
-     * @param ignoreErr If `true`, the error is ignored and not thrown as an error.
-     * @param ignoreStdErr If `true`, the standard error output is ignored and not thrown as an error.
+     * @param options.ignoreErr If `true`, the error is ignored and not thrown as an error.
+     * @param options.ignoreStdErr If `true`, the standard error output is ignored and not thrown as an error.
+     * @param options.maxBuffer The maximum buffer size in bytes allowed for the standard output and standard error streams.
      * @returns A promise that resolves with the output of the command.
      */
-    executeCommand(command: string, ignoreStdErr?: boolean, ignoreErr?: boolean): Promise<string>;
+    executeCommand(
+        command: string,
+        options?: {
+            ignoreStdErr?: boolean;
+            ignoreErr?: boolean;
+            maxBuffer?: number;
+        },
+    ): Promise<string>;
 }
diff --git a/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.ts b/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.ts
index b01bcb69..0ed2731b 100644
--- a/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.ts
+++ b/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.ts
@@ -2,12 +2,19 @@ import { exec } from "child_process";
 import type { CommandlineUtility } from "./Contract";
 
 export class NodeJsCommandlineUtility implements CommandlineUtility {
-    public executeCommand(command: string, ignoreStdErr?: boolean, ignoreErr?: boolean): Promise<string> {
+    public executeCommand(
+        command: string,
+        options?: {
+            ignoreStdErr?: boolean;
+            ignoreErr?: boolean;
+            maxBuffer?: number;
+        },
+    ): Promise<string> {
         return new Promise((resolve, reject) => {
-            exec(command, (error, stdout, stderr) => {
-                if (error && !ignoreErr) {
+            exec(command, { maxBuffer: options?.maxBuffer }, (error, stdout, stderr) => {
+                if (error && !options?.ignoreErr) {
                     reject(error);
-                } else if (stderr && !ignoreStdErr) {
+                } else if (stderr && !options?.ignoreStdErr) {
                     reject(stderr);
                 } else {
                     resolve(stdout.toString());
diff --git a/src/main/Core/PowershellUtility/Contract/PowershellUtility.ts b/src/main/Core/PowershellUtility/Contract/PowershellUtility.ts
index b8d300bf..1a640475 100644
--- a/src/main/Core/PowershellUtility/Contract/PowershellUtility.ts
+++ b/src/main/Core/PowershellUtility/Contract/PowershellUtility.ts
@@ -5,9 +5,10 @@ export interface PowershellUtility {
     /**
      * Executes a PowerShell command.
      * @param command The command to execute, e.g. `Get-Process`.
+     * @param options.maxBuffer The maximum buffer size in bytes allowed for the standard output and standard error streams.
      * @returns The output of the powershell command.
      */
-    executeCommand(command: string): Promise<string>;
+    executeCommand(command: string, options?: { maxBuffer?: number }): Promise<string>;
 
     /**
      * Executes a PowerShell script. The script can also be multi-line and include functions.
@@ -17,7 +18,8 @@ export interface PowershellUtility {
      * Write-Host "This is a PowerShell script."
      * Write-Host "It is executed by the PowerShellUtility."
      * ```
+     * @param options.maxBuffer The maximum buffer size in bytes allowed for the standard output and standard error streams.
      * @returns The output of the powershell script.
      */
-    executeScript(script: string): Promise<string>;
+    executeScript(script: string, options?: { maxBuffer?: number }): Promise<string>;
 }
diff --git a/src/main/Core/PowershellUtility/PowershellUtility.ts b/src/main/Core/PowershellUtility/PowershellUtility.ts
index f6e60a57..65323a24 100644
--- a/src/main/Core/PowershellUtility/PowershellUtility.ts
+++ b/src/main/Core/PowershellUtility/PowershellUtility.ts
@@ -16,28 +16,25 @@ export class PowershellUtility implements PowershellUtilityInterface {
         private readonly randomStringProvider: RandomStringProvider,
     ) {}
 
-    public async executeCommand(command: string): Promise<string> {
-        return this.commandlineUtility.executeCommand(`${PowershellUtility.PowershellPath} -Command "& {${command}}"`);
+    public async executeCommand(command: string, options?: { maxBuffer: number }): Promise<string> {
+        return await this.commandlineUtility.executeCommand(
+            `${PowershellUtility.PowershellPath} -Command "& {${command}}"`,
+            options,
+        );
     }
 
-    public async executeScript(script: string): Promise<string> {
-        const filePath = this.getTemporaryPowershellFilePath();
+    public async executeScript(script: string, options?: { maxBuffer: number }): Promise<string> {
+        const filePath = join(this.temporaryDirectoryFilePath, `${this.randomStringProvider.getRandomUUid()}.ps1`);
 
         await this.fileSystemUtility.writeTextFile(`${this.byteOrderMark}${script}`, filePath);
 
-        const powershellCommand = `${PowershellUtility.PowershellPath} -NoProfile -NonInteractive -ExecutionPolicy bypass -File "${filePath}"`;
-        const stdout = await this.commandlineUtility.executeCommand(powershellCommand);
+        const stdout = await this.commandlineUtility.executeCommand(
+            `${PowershellUtility.PowershellPath} -NoProfile -NonInteractive -ExecutionPolicy bypass -File "${filePath}"`,
+            options,
+        );
 
         await this.fileSystemUtility.removeFile(filePath);
 
         return stdout;
     }
-
-    private getTemporaryPowershellFilePath(): string {
-        return join(this.temporaryDirectoryFilePath, this.getRandomFileName());
-    }
-
-    private getRandomFileName(): string {
-        return `${this.randomStringProvider.getRandomUUid()}.ps1`;
-    }
 }
diff --git a/src/main/Extensions/ApplicationSearch/Linux/LaunchDesktopFileActionHandler.ts b/src/main/Extensions/ApplicationSearch/Linux/LaunchDesktopFileActionHandler.ts
index 45df13f3..c87d03eb 100644
--- a/src/main/Extensions/ApplicationSearch/Linux/LaunchDesktopFileActionHandler.ts
+++ b/src/main/Extensions/ApplicationSearch/Linux/LaunchDesktopFileActionHandler.ts
@@ -16,6 +16,6 @@ export class LaunchDesktopFileActionHandler implements ActionHandler {
      * Throws an error if the file could not be launched or desktop environment isn't supported.
      */
     public async invokeAction(action: SearchResultItemAction): Promise<void> {
-        await this.commandlineUtility.executeCommand(`gio launch ${action.argument}`, true);
+        await this.commandlineUtility.executeCommand(`gio launch ${action.argument}`, { ignoreStdErr: true });
     }
 }
diff --git a/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts b/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts
index 0f484a05..e92dd797 100644
--- a/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts
+++ b/src/main/Extensions/ApplicationSearch/Windows/WindowsApplicationRepository.ts
@@ -12,6 +12,8 @@ import type { WindowsStoreApplication } from "./WindowsStoreApplication";
 import { usePowershellScripts } from "./usePowershellScripts";
 
 export class WindowsApplicationRepository implements ApplicationRepository {
+    private static readonly POWERSHELL_MAX_BUFFER_SIZE = 4096 * 4096;
+
     public constructor(
         private readonly powershellUtility: PowershellUtility,
         private readonly settings: Settings,
@@ -37,6 +39,7 @@ export class WindowsApplicationRepository implements ApplicationRepository {
 
         const stdout = await this.powershellUtility.executeScript(
             this.getPowershellScript(folderPaths, fileExtensions),
+            { maxBuffer: WindowsApplicationRepository.POWERSHELL_MAX_BUFFER_SIZE },
         );
 
         const windowsApplicationRetrieverResults = <WindowsApplicationRetrieverResult[]>JSON.parse(stdout);
@@ -64,7 +67,9 @@ export class WindowsApplicationRepository implements ApplicationRepository {
 
         const { getWindowsStoreApps } = usePowershellScripts();
 
-        const stdout = await this.powershellUtility.executeScript(getWindowsStoreApps);
+        const stdout = await this.powershellUtility.executeScript(getWindowsStoreApps, {
+            maxBuffer: WindowsApplicationRepository.POWERSHELL_MAX_BUFFER_SIZE,
+        });
 
         const windowStoreApplications = <WindowsStoreApplication[]>JSON.parse(stdout);
 
diff --git a/src/main/Extensions/FileSearch/macOS/MdfindFileSearcher.ts b/src/main/Extensions/FileSearch/macOS/MdfindFileSearcher.ts
index 0fefafc0..033b65d5 100644
--- a/src/main/Extensions/FileSearch/macOS/MdfindFileSearcher.ts
+++ b/src/main/Extensions/FileSearch/macOS/MdfindFileSearcher.ts
@@ -6,7 +6,7 @@ export class MdfindFileSearcher implements FileSearcher {
 
     public async getFilePathsBySearchTerm(searchTerm: string, maxSearchResultCount: number): Promise<string[]> {
         const commands = [`mdfind -name "${searchTerm}"`, `head -n ${maxSearchResultCount}`];
-        const stdout = await this.commandlineUtility.executeCommand(commands.join(" | "), true);
+        const stdout = await this.commandlineUtility.executeCommand(commands.join(" | "), { ignoreStdErr: true });
         return stdout.split("\n").filter((filePath) => filePath.trim().length > 0);
     }
 }


Test patch:
diff --git a/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.test.ts b/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.test.ts
index 0c398090..3265a4b5 100644
--- a/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.test.ts
+++ b/src/main/Core/CommandlineUtility/NodeJsCommandlineUtility.test.ts
@@ -13,9 +13,16 @@ vi.mock("child_process", async (importOriginal) => {
 
     return {
         ...original,
-        exec: (command: string, callback: (_: Error | null, __: string, ___?: string) => void) => {
-            execMock(command);
-            callback(error, stdout, stderr);
+        exec: (
+            command: string,
+            options?: { maxBuffer: number },
+            callback?: (_: Error | null, __: string, ___?: string) => void,
+        ) => {
+            execMock(command, options);
+
+            if (callback) {
+                callback(error, stdout, stderr);
+            }
         },
     };
 });
@@ -32,10 +39,13 @@ describe(NodeJsCommandlineUtility, () => {
             stdout = "output";
             stderr = undefined;
 
-            const output = await new NodeJsCommandlineUtility().executeCommand("test", false, false);
+            const output = await new NodeJsCommandlineUtility().executeCommand("test", {
+                ignoreStdErr: false,
+                ignoreErr: false,
+            });
 
             expect(output).toBe("output");
-            expect(execMock).toHaveBeenCalledWith("test");
+            expect(execMock).toHaveBeenCalledWith("test", { maxBuffer: undefined });
         });
 
         it("should execute the command and return the stdout if error occurs but ignore error is set to true", async () => {
@@ -43,10 +53,13 @@ describe(NodeJsCommandlineUtility, () => {
             stdout = "output";
             stderr = undefined;
 
-            const output = await new NodeJsCommandlineUtility().executeCommand("test", false, true);
+            const output = await new NodeJsCommandlineUtility().executeCommand("test", {
+                ignoreStdErr: false,
+                ignoreErr: true,
+            });
 
             expect(output).toBe("output");
-            expect(execMock).toHaveBeenCalledWith("test");
+            expect(execMock).toHaveBeenCalledWith("test", { maxBuffer: undefined });
         });
 
         it("should execute the command and return the stdout if stderr occurs but ignore stderr is set to true", async () => {
@@ -54,10 +67,13 @@ describe(NodeJsCommandlineUtility, () => {
             stdout = "output";
             stderr = undefined;
 
-            const output = await new NodeJsCommandlineUtility().executeCommand("test", true, false);
+            const output = await new NodeJsCommandlineUtility().executeCommand("test", {
+                ignoreStdErr: true,
+                ignoreErr: false,
+            });
 
             expect(output).toBe("output");
-            expect(execMock).toHaveBeenCalledWith("test");
+            expect(execMock).toHaveBeenCalledWith("test", { maxBuffer: undefined });
         });
 
         it("should reject the promise if an error occurs and ignore error is set to false", async () => {
@@ -66,10 +82,14 @@ describe(NodeJsCommandlineUtility, () => {
             stderr = undefined;
 
             expect(
-                async () => await new NodeJsCommandlineUtility().executeCommand("test", false, false),
+                async () =>
+                    await new NodeJsCommandlineUtility().executeCommand("test", {
+                        ignoreStdErr: false,
+                        ignoreErr: false,
+                    }),
             ).rejects.toThrow(error.message);
 
-            expect(execMock).toHaveBeenCalledWith("test");
+            expect(execMock).toHaveBeenCalledWith("test", { maxBuffer: undefined });
         });
 
         it("should reject the promise if a std error occurs and ignore std error is set to false", async () => {
@@ -78,10 +98,23 @@ describe(NodeJsCommandlineUtility, () => {
             stderr = "This is a std err";
 
             expect(
-                async () => await new NodeJsCommandlineUtility().executeCommand("test", false, false),
+                async () =>
+                    await new NodeJsCommandlineUtility().executeCommand("test", {
+                        ignoreStdErr: false,
+                        ignoreErr: false,
+                    }),
             ).rejects.toThrow("This is a std err");
 
-            expect(execMock).toHaveBeenCalledWith("test");
+            expect(execMock).toHaveBeenCalledWith("test", { maxBuffer: undefined });
+        });
+
+        it("should respect the max buffer size", async () => {
+            error = null;
+            stdout = "output";
+            stderr = "";
+
+            await new NodeJsCommandlineUtility().executeCommand("test", { maxBuffer: 100 });
+            expect(execMock).toHaveBeenCalledWith("test", { maxBuffer: 100 });
         });
     });
 });
diff --git a/src/main/Core/PowershellUtility/PowershellUtility.test.ts b/src/main/Core/PowershellUtility/PowershellUtility.test.ts
index 899237a0..7cc26d11 100644
--- a/src/main/Core/PowershellUtility/PowershellUtility.test.ts
+++ b/src/main/Core/PowershellUtility/PowershellUtility.test.ts
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from "vitest";
 import { PowershellUtility } from "./PowershellUtility";
 
 describe(PowershellUtility, () => {
-    it("should call commandline utility to execute powershell command", async () => {
+    const testExecuteCommand = async ({ command, options }: { command: string; options?: { maxBuffer: number } }) => {
         const executeCommandMock = vi.fn().mockReturnValue("test output");
 
         const commandlineUtility = <CommandlineUtility>{
@@ -20,13 +20,21 @@ describe(PowershellUtility, () => {
             <RandomStringProvider>{},
         );
 
-        expect(await powershellUtility.executeCommand("test command")).toBe("test output");
+        expect(await powershellUtility.executeCommand(command, options)).toBe("test output");
         expect(executeCommandMock).toHaveBeenCalledWith(
-            `${PowershellUtility.PowershellPath} -Command "& {test command}"`,
+            `${PowershellUtility.PowershellPath} -Command "& {${command}}"`,
         );
+    };
+
+    it("should call commandline utility to execute powershell command", async () => {
+        await testExecuteCommand({ command: "test command" });
     });
 
-    it("should write a temporary file, execute it and remove the file again", async () => {
+    it("should call commandline utility to execute powershell command with maxBuffer", async () => {
+        await testExecuteCommand({ command: "test command", options: { maxBuffer: 100 } });
+    });
+
+    const testExecuteScript = async ({ script, options }: { script: string; options?: { maxBuffer: number } }) => {
         const writeTextFileMock = vi.fn().mockReturnValue(Promise.resolve());
         const removeFileMock = vi.fn().mockReturnValue(Promise.resolve());
         const executeCommandMock = vi.fn().mockReturnValue("test output");
@@ -37,7 +45,7 @@ describe(PowershellUtility, () => {
             removeFile: (filePath) => removeFileMock(filePath),
         };
 
-        const commandlineUtility = <CommandlineUtility>{ executeCommand: (command) => executeCommandMock(command) };
+        const commandlineUtility = <CommandlineUtility>{ executeCommand: (c, o) => executeCommandMock(c, o) };
         const randomStringProvider = <RandomStringProvider>{ getRandomUUid: () => getRandomHexStringMock() };
 
         const powershellUtility = new PowershellUtility(
@@ -47,14 +55,26 @@ describe(PowershellUtility, () => {
             randomStringProvider,
         );
 
-        expect(await powershellUtility.executeScript("my script")).toBe("test output");
+        expect(await powershellUtility.executeScript(script, options)).toBe("test output");
+
         expect(writeTextFileMock).toHaveBeenCalledWith(
             "\ufeffmy script",
             join("temp", "directory", "randomHexString.ps1"),
         );
+
         expect(executeCommandMock).toHaveBeenCalledWith(
             `${PowershellUtility.PowershellPath} -NoProfile -NonInteractive -ExecutionPolicy bypass -File "${join("temp", "directory", "randomHexString.ps1")}"`,
+            options,
         );
+
         expect(getRandomHexStringMock).toHaveBeenCalledOnce();
+    };
+
+    it("should write a temporary file, execute it and remove the file again", async () => {
+        await testExecuteScript({ script: "my script" });
+    });
+
+    it("should write a temporary file, execute it and remove the file again, with maxBuffer", async () => {
+        await testExecuteScript({ script: "my script", options: { maxBuffer: 100 } });
     });
 });
diff --git a/src/main/Extensions/ApplicationSearch/macOS/MacOsApplicationRepository.test.ts b/src/main/Extensions/ApplicationSearch/macOS/MacOsApplicationRepository.test.ts
index 3e191e30..0c08bcc6 100644
--- a/src/main/Extensions/ApplicationSearch/macOS/MacOsApplicationRepository.test.ts
+++ b/src/main/Extensions/ApplicationSearch/macOS/MacOsApplicationRepository.test.ts
@@ -42,7 +42,7 @@ describe(MacOsApplicationRepository, () => {
             `);
 
             const commandlineUtility = <CommandlineUtility>{
-                executeCommand: (c, istd, ierr) => executeCommandMock(c, istd, ierr),
+                executeCommand: (c, options) => executeCommandMock(c, options),
             };
 
             const getImagesMock = vi.fn().mockResolvedValue({
@@ -78,11 +78,7 @@ describe(MacOsApplicationRepository, () => {
                 new MacOsApplication("Finder", "/Applications/Finder.app", { url: "file://genericIcon.png" }),
             ]);
 
-            expect(executeCommandMock).toHaveBeenCalledWith(
-                `mdfind "kMDItemKind == 'Application'"`,
-                undefined,
-                undefined,
-            );
+            expect(executeCommandMock).toHaveBeenCalledWith(`mdfind "kMDItemKind == 'Application'"`, undefined);
 
             expect(getImagesMock).toHaveBeenCalledWith([
                 "/Applications/iTerm.app",