Repository: oliverschwendener/ueli (ts)

Issue:
[Bug] Autostart option does not work when Ueli is installed via Microsoft Store
Autostart option does not work when Ueli is installed via Microsoft Store

**To Reproduce**
Steps to reproduce the behavior:
1. Open settings
2. Go to "General" settings
3. Enable "Autostart"
4. Check if app has been added to autostart apps in Windows Task Manager
5. The app is not listed

**Expected behavior**
That the app is added/removed to/from the autostart apps when toggling the "Autostart" option

**Environment**
 - OS: Windows
 - Version 9.0.2


Pull request:
Fix Windows store app autostart
This fixes #1137 by creating a shortcut link with target `shell:AppsFolder\\1915OliverSchwendener.Ueli_a397x08q5x7rp!OliverSchwendener.Ueli`.

I had to use a quite ugly workaround to check if the file contains the correct target since Electron's `readShortcutLink` method does return an empty target for Windows store app lnk files.

Patch:
diff --git a/src/main/Core/Autostart/AutostartModule.ts b/src/main/Core/Autostart/AutostartModule.ts
index 1e43cf03..b93b1e92 100644
--- a/src/main/Core/Autostart/AutostartModule.ts
+++ b/src/main/Core/Autostart/AutostartModule.ts
@@ -11,7 +11,7 @@ export class AutostartModule {
         const fileSystemUtility = moduleRegistry.get("FileSystemUtility");
 
         const autostartManager = process.windowsStore
-            ? new WindowsStoreAutostartManager(app, shell, process, fileSystemUtility, logger)
+            ? new WindowsStoreAutostartManager(app, shell, fileSystemUtility, logger)
             : new DefaultAutostartManager(app, process);
 
         const setAutostartOptions = (openAtLogin: boolean) => {
diff --git a/src/main/Core/Autostart/WindowsStoreAutostartManager.ts b/src/main/Core/Autostart/WindowsStoreAutostartManager.ts
index 4a9835da..541213d0 100644
--- a/src/main/Core/Autostart/WindowsStoreAutostartManager.ts
+++ b/src/main/Core/Autostart/WindowsStoreAutostartManager.ts
@@ -5,10 +5,12 @@ import { join } from "path";
 import type { AutostartManager } from "./AutostartManager";
 
 export class WindowsStoreAutostartManager implements AutostartManager {
+    private readonly appId = "1915OliverSchwendener.Ueli_a397x08q5x7rp!OliverSchwendener.Ueli";
+    private readonly shortcutTarget = `shell:AppsFolder\\${this.appId}`;
+
     public constructor(
         private readonly app: App,
         private readonly shell: Shell,
-        private readonly process: NodeJS.Process,
         private readonly fileSystemUtility: FileSystemUtility,
         private readonly logger: Logger,
     ) {}
@@ -31,8 +33,8 @@ export class WindowsStoreAutostartManager implements AutostartManager {
         }
 
         try {
-            const shortcutLink = this.shell.readShortcutLink(shortcutFilePath);
-            return shortcutLink.target === this.process.execPath;
+            const shortcutFileContent = this.fileSystemUtility.readTextFileSync(shortcutFilePath, "utf-16le");
+            return shortcutFileContent.includes(this.appId);
         } catch (error) {
             this.logger.error(`Failed to read shortcut link "${shortcutFilePath}". Reason: ${error}`);
             return false;
@@ -55,7 +57,7 @@ export class WindowsStoreAutostartManager implements AutostartManager {
         const shortcutFileExists = this.fileSystemUtility.existsSync(shortcutFilePath);
 
         this.shell.writeShortcutLink(shortcutFilePath, shortcutFileExists ? "replace" : "create", {
-            target: this.process.execPath,
+            target: this.shortcutTarget,
         });
     }
 
diff --git a/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts b/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
index c3f4d032..50005e1d 100644
--- a/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
+++ b/src/main/Core/FileSystemUtility/Contract/FileSystemUtility.ts
@@ -4,8 +4,8 @@ export interface FileSystemUtility {
     pathExists(fileOrFolderPath: string): Promise<boolean>;
     readFileSync(filePath: string): Buffer;
     readFile(filePath: string): Promise<Buffer>;
-    readTextFileSync(filePath: string): string;
-    readTextFile(filePath: string): Promise<string>;
+    readTextFileSync(filePath: string, encoding?: BufferEncoding): string;
+    readTextFile(filePath: string, encoding?: BufferEncoding): Promise<string>;
     readJsonFile<T>(filePath: string): Promise<T>;
     readJsonFileSync<T>(filePath: string): T;
     readDirectory(folderPath: string, recursive?: boolean): Promise<string[]>;
diff --git a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
index 3ddda88b..b725e125 100644
--- a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
+++ b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.ts
@@ -88,12 +88,12 @@ export class NodeJsFileSystemUtility implements FileSystemUtility {
         return readFileSync(filePath);
     }
 
-    public readTextFileSync(filePath: string): string {
-        return this.readFileSync(filePath).toString();
+    public readTextFileSync(filePath: string, encoding?: BufferEncoding): string {
+        return this.readFileSync(filePath).toString(encoding);
     }
 
-    public async readTextFile(filePath: string): Promise<string> {
-        return (await this.readFile(filePath)).toString();
+    public async readTextFile(filePath: string, encoding?: BufferEncoding): Promise<string> {
+        return (await this.readFile(filePath)).toString(encoding);
     }
 
     public async readDirectory(folderPath: string, recursive?: boolean): Promise<string[]> {


Test patch:
diff --git a/src/main/Core/Autostart/WindowsStoreAutostartManager.test.ts b/src/main/Core/Autostart/WindowsStoreAutostartManager.test.ts
index fb90e268..c262c521 100644
--- a/src/main/Core/Autostart/WindowsStoreAutostartManager.test.ts
+++ b/src/main/Core/Autostart/WindowsStoreAutostartManager.test.ts
@@ -8,40 +8,38 @@ import { WindowsStoreAutostartManager } from "./WindowsStoreAutostartManager";
 describe(WindowsStoreAutostartManager, () => {
     describe(WindowsStoreAutostartManager.prototype.autostartIsEnabled, () => {
         const testAutostartIsEnabled = ({
-            execPath,
             shortcutFileExists,
             shortcutFileReadError,
-            shortcutTarget,
+            shortcutFileContent,
             expected,
         }: {
-            execPath: string;
             shortcutFileExists: boolean;
             shortcutFileReadError?: string;
-            shortcutTarget?: string;
+            shortcutFileContent?: string;
             expected: boolean;
         }) => {
             const getPathMock = vi.fn().mockReturnValue("AppData");
             const app = <App>{ getPath: (p) => getPathMock(p) };
 
             const existsSyncMock = vi.fn().mockReturnValue(shortcutFileExists);
-            const fileSystemUtility = <FileSystemUtility>{ existsSync: (p) => existsSyncMock(p) };
-
-            const readShortcutLinkMock = shortcutFileReadError
+            const readTextFileSync = shortcutFileReadError
                 ? vi.fn().mockImplementationOnce(() => {
                       throw new Error(shortcutFileReadError);
                   })
-                : vi.fn().mockReturnValue({ target: shortcutTarget });
+                : vi.fn().mockReturnValue(shortcutFileContent);
+            const fileSystemUtility = <FileSystemUtility>{
+                existsSync: (p) => existsSyncMock(p),
+                readTextFileSync: (filePath, encoding) => readTextFileSync(filePath, encoding),
+            };
 
-            const shell = <Shell>{ readShortcutLink: (p) => readShortcutLinkMock(p) };
+            const shell = <Shell>{};
 
             const logErrorMock = vi.fn();
             const logger = <Logger>{ error: (m) => logErrorMock(m) };
 
-            const process = <NodeJS.Process>{ execPath };
-
-            expect(
-                new WindowsStoreAutostartManager(app, shell, process, fileSystemUtility, logger).autostartIsEnabled(),
-            ).toBe(expected);
+            expect(new WindowsStoreAutostartManager(app, shell, fileSystemUtility, logger).autostartIsEnabled()).toBe(
+                expected,
+            );
 
             expect(getPathMock).toHaveBeenCalledOnce();
             expect(getPathMock).toHaveBeenCalledWith("appData");
@@ -71,7 +69,6 @@ describe(WindowsStoreAutostartManager, () => {
             testAutostartIsEnabled({
                 shortcutFileExists: false,
                 expected: false,
-                execPath: "execPath",
             });
         });
 
@@ -80,24 +77,22 @@ describe(WindowsStoreAutostartManager, () => {
                 shortcutFileExists: true,
                 expected: false,
                 shortcutFileReadError: "some error",
-                execPath: "execPath",
             });
         });
 
-        it("should return false when shortcut file exists but target is not current process exec path", () => {
+        it("should return false when shortcut file exists and content does not contain app id", () => {
             testAutostartIsEnabled({
                 shortcutFileExists: true,
-                shortcutTarget: "other target",
-                execPath: "execPath",
+                shortcutFileContent: "content without app id",
                 expected: false,
             });
         });
 
-        it("should return true when shortcut file exists and target is same as current process exec path", () => {
+        it("should return true when shortcut file exists and content contains app id", () => {
             testAutostartIsEnabled({
                 shortcutFileExists: true,
-                shortcutTarget: "execPath",
-                execPath: "execPath",
+                shortcutFileContent:
+                    "some content with app id 1915OliverSchwendener.Ueli_a397x08q5x7rp!OliverSchwendener.Ueli",
                 expected: true,
             });
         });
@@ -130,8 +125,6 @@ describe(WindowsStoreAutostartManager, () => {
                 writeShortcutLink: (path, operation, options) => writeShortcutLinkMock(path, operation, options),
             };
 
-            const process = <NodeJS.Process>{ execPath: "execPath" };
-
             const shortcutFilePath = join(
                 "AppData",
                 "Microsoft",
@@ -142,7 +135,7 @@ describe(WindowsStoreAutostartManager, () => {
                 "Ueli.lnk",
             );
 
-            new WindowsStoreAutostartManager(app, shell, process, fileSystemUtility, <Logger>{}).setAutostartOptions(
+            new WindowsStoreAutostartManager(app, shell, fileSystemUtility, <Logger>{}).setAutostartOptions(
                 openAtLogin,
             );
 
@@ -160,7 +153,7 @@ describe(WindowsStoreAutostartManager, () => {
             if (expectedShortcutOperation) {
                 expect(writeShortcutLinkMock).toHaveBeenCalledOnce();
                 expect(writeShortcutLinkMock).toHaveBeenCalledWith(shortcutFilePath, expectedShortcutOperation, {
-                    target: "execPath",
+                    target: "shell:AppsFolder\\1915OliverSchwendener.Ueli_a397x08q5x7rp!OliverSchwendener.Ueli",
                 });
             }
         };
diff --git a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
index a38d0ed3..6f036ebd 100644
--- a/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
+++ b/src/main/Core/FileSystemUtility/NodeJsFileSystemUtility.test.ts
@@ -1,4 +1,4 @@
-import { mkdir, rm } from "fs/promises";
+import { mkdir, rm, writeFile } from "fs/promises";
 import { join } from "path";
 import { afterEach, beforeEach, describe, expect, it } from "vitest";
 import { NodeJsFileSystemUtility } from "./NodeJsFileSystemUtility";
@@ -233,6 +233,18 @@ describe(NodeJsFileSystemUtility, () => {
 
             expect(actual).toBe(content);
         });
+
+        it("should read the contents of a utf-16le encoded file as a string", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+            const filePath = join(tempFolderPath, "file.txt");
+            const content = "test";
+
+            await writeFile(filePath, content, { encoding: "utf-16le" });
+
+            const actual = await fileSystemUtility.readTextFile(filePath, "utf-16le");
+
+            expect(actual).toBe(content);
+        });
     });
 
     describe(NodeJsFileSystemUtility.prototype.readTextFileSync, () => {
@@ -245,6 +257,16 @@ describe(NodeJsFileSystemUtility, () => {
 
             expect(fileSystemUtility.readTextFileSync(filePath)).toBe(content);
         });
+
+        it("should read the contents of a utf-16le encoded file synchronously as a string", async () => {
+            const fileSystemUtility = new NodeJsFileSystemUtility();
+            const filePath = join(tempFolderPath, "file.txt");
+            const content = "test";
+
+            await writeFile(filePath, content, { encoding: "utf-16le" });
+
+            expect(fileSystemUtility.readTextFileSync(filePath, "utf-16le")).toBe(content);
+        });
     });
 
     describe(NodeJsFileSystemUtility.prototype.removeFile, () => {