Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[desktop] update registry entries for windows, depend on install mode #3715

Merged
merged 2 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
19 changes: 19 additions & 0 deletions buildSrc/windows-installer.nsh
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
; electron builder nsis code:
; https://github.com/electron-userland/electron-builder/tree/master/packages/app-builder-lib/templates/nsis

!macro disableAutoUpdates
${GetParameters} $R0
ClearErrors
Expand All @@ -7,10 +10,26 @@
${EndIf}
!macroend

!macro saveInstallMode
; to detect whether _this_ install was installed per-user or per-machine,
; we put a file next to the executable on a per-user install.
; this should prevent confusion with multiple installs or custom paths
; that could occur if we wrote to a common location in the registry or file system
${If} $installMode == 'CurrentUser'
ClearErrors
FileOpen $0 $INSTDIR\per_user w
IfErrors done
FileWrite $0 ""
FileClose $0
done:
${EndIf}
!macroend

!macro deleteUpdateFile
Delete $INSTDIR\resources\app-update.yml
!macroend

!macro customInstall
!insertMacro disableAutoUpdates
!insertMacro saveInstallMode
!macroend
5 changes: 3 additions & 2 deletions flow/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ declare module 'electron' {
declare export var shell: {
// Open the given external protocol URL in the desktop's default manner.
// (For example, mailto: URLs in the user's default mail agent).
openExternal(url: string): void;
openExternal(url: string): Promise<void>;
showItemInFolder(fullPath: string): void;
// Open the given file in the desktop's default manner.
openPath(fullPath: string): Promise<string>;
Expand Down Expand Up @@ -321,7 +321,7 @@ declare module 'electron' {
once(AppEvent, (Event, ...Array<any>) => any): App,
emit(AppEvent): App,
removeListener(AppEvent, Function): App,
requestSingleInstanceLock(): void,
requestSingleInstanceLock(): boolean,
quit(): void,
exit(code: number): void,
relaunch({args: Array<string>, execPath?: string}): void,
Expand Down Expand Up @@ -354,6 +354,7 @@ declare module 'electron' {
args?: string
}): void;
getAppPath(): string;
getName(): string;
getPath(name: 'home'
| 'appData' //Per-user application data directory
| 'userData' // directory for your app's configuration files, by default it is appData + app name.
Expand Down
58 changes: 38 additions & 20 deletions src/desktop/DesktopUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import {exec, spawn} from 'child_process'
import {promisify} from 'util'
import type {Rectangle} from "electron"
import {app} from 'electron'
import {defer} from '@tutao/tutanota-utils'
import {noOp} from "@tutao/tutanota-utils"
import {defer, delay, noOp, uint8ArrayToHex} from '@tutao/tutanota-utils'
import {log} from "./DesktopLog"
import {uint8ArrayToHex} from "@tutao/tutanota-utils"
import {delay} from "@tutao/tutanota-utils"
import {DesktopCryptoFacade} from "./DesktopCryptoFacade"
import {swapFilename} from "./PathUtils"
import {fileExists, swapFilename} from "./PathUtils"
import url from "url"
import {registerKeys, unregisterKeys} from "./reg-templater"

export class DesktopUtils {

Expand All @@ -30,6 +28,11 @@ export class DesktopUtils {
return Promise.resolve(app.isDefaultProtocolClient("mailto"))
}

checkIsPerUserInstall(): Promise<boolean> {
const markerPath = swapFilename(process.execPath, "per_user")
return fileExists(markerPath)
}

/**
* open and close a file to make sure it exists
* @param path: the file to touch
Expand Down Expand Up @@ -224,26 +227,41 @@ export class DesktopUtils {
}

async _registerOnWin(): Promise<void> {
// any app that wants to use tutanota over MAPI needs to know which dll to load.
// additionally, the DLL needs to know
// * which tutanota executable to start (per-user/per-machine/snapshot/test/release)
// * where to log (this depends on the current user -> %USERPROFILE%)
// * where to put tmp files (also user-dependent)
// all these must be set in the registry
const execPath = process.execPath
const dllPath = swapFilename(execPath, "mapirs.dll")
const logPath = path.join(app.getPath('userData'), 'logs')
const tmpPath = this.getTutanotaTempPath('attach')
const tmpRegScript = (await import('./reg-templater.js')).registerKeys(
execPath,
dllPath,
logPath,
tmpPath
)
return this._executeRegistryScript(tmpRegScript)
.then(() => {
app.setAsDefaultProtocolClient('mailto')
})
const dllPath = swapFilename(execPath, 'mapirs.dll')
// we may be a per-machine installation that's used by multiple users, so the dll will replace %USERPROFILE%
// with the value of the USERPROFILE env var.
const appData = path.join("%USERPROFILE%", 'AppData')
const logPath = path.join(appData, 'Roaming', app.getName(), 'logs')
const tmpPath = path.join(appData, "Local", "Temp", this._topLevelDownloadDir, "attach")
const isLocal = await this.checkIsPerUserInstall()
const tmpRegScript = registerKeys({execPath, dllPath, logPath, tmpPath}, isLocal)
await this._executeRegistryScript(tmpRegScript)
app.setAsDefaultProtocolClient('mailto')
await this._openDefaultAppsSettings()
}

async _unregisterOnWin(): Promise<void> {
app.removeAsDefaultProtocolClient('mailto')
const tmpRegScript = (await import('./reg-templater.js')).unregisterKeys()
return this._executeRegistryScript(tmpRegScript)
const isLocal = await this.checkIsPerUserInstall()
const tmpRegScript = unregisterKeys(isLocal)
await this._executeRegistryScript(tmpRegScript)
await this._openDefaultAppsSettings()
}

async _openDefaultAppsSettings(): Promise<void> {
try {
await this._electron.shell.openExternal('ms-settings:defaultapps')
} catch (e) {
// ignoring, this is just a convenience for the user
console.error("failed to open default apps settings page:", e.message)
}
}

/**
Expand Down
167 changes: 167 additions & 0 deletions src/desktop/integration/RegistryScriptGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// @flow

/*
* This file helps keep registry operations for application and removal in sync by generating
* an application registry script and a removal script from the same template.
*
* Windows Registry Scripts look like this:
* ```
* Windows Registry Editor Version 5.00
*
* [HKCU\SOFTWARE\CLIENTS\MAIL]
* @="default_value"
*
* [-HKLM\SOFTWARE\CLIENTS\MAIL\Mozilla Thunderbird]
*
* [HKCU\SOFTWARE\CLIENTS\MAIL\Section]
* @="default_value"
* "name"="named_value"
* "value_to_delete"=-
*
* [HKCU\SOFTWARE\CLIENTS\MAIL\Section2]
* @=-
* ```
*
* * there is a header line followed by a list of sections
* * each section starts with a path in square brackets []. if the path is prefixed with a dash (-),
* that path and all its subkeys will be removed.
* * sections can contain
* * default value assignments of the form @="VALUE"
* * default value nullifications of the form @=-
* * named value assignments of the form "NAME"="VALUE"
* * named value deletions of the form "NAME"=-
*
* The script generator uses JavaScript arrays of RegistryValueTemplates as templates.
* During application, RegistryValueTemplates will recursively written to the registry at their respective root path.
* The removal script preserves the root keys. this means that subkeys that were created in a root key will be recursively deleted,
* but string values that are assigned directly to a root key or a name in that subkey will only be nulled. Example:
*
* To generate this application script:
* ```
* Windows Registry Editor Version 5.00
* [HKLM\SOFTWARE\CLIENTS\MAIL]
* "named"="val"
* @="default_value_1"
*
* [HKLM\SOFTWARE\CLIENTS\MAIL\subkey]
* @="default_value_2"
* "DLLPath"="C:\dll\path\t.dll"
* ```
*
* and this removal script
*
* ```
* Windows Registry Editor Version 5.00
*
* [HKLM\SOFTWARE\CLIENTS\MAIL]
* "named"=-
* @=-
*
* [-HKLM\SOFTWARE\CLIENTS\MAIL\subkey]
* ```
*
* We would use this template. Note the values with empty keys which get expanded to @=<value> assignments.
*
* ```
* const template = [{
* root: "HKLM\\SOFTWARE\\CLIENTS\\MAIL",
* value: {"named": "val", subkey: {"": "default_value_2", "DLLPath": "C:\\dll\\path\\t.dll",}, "": "default_value_1"}
* }]
* ```
*
* Also note that "HKLM\SOFTWARE\CLIENTS\MAIL\subkey" was removed entirely while the values
* directly assigned to "HKLM\SOFTWARE\CLIENTS\MAIL" are only nulled, because that path was given as a root.
*
* Current Limitations:
* * only string values are supported
* * application can only write, removal will only remove
* */

export type RegistryTemplateDefinition = $ReadOnlyArray<RegistryValueTemplate>
export type RegistryValueTemplate = {value: RegistrySubKey, root: string}
export type RegistrySubKey = {[string]: RegistryValue}
export type RegistryValue = RegistrySubKey | string
type OperationBuffer = {[string]: Array<string>}

const header_line = "Windows Registry Editor Version 5.00"

function quote(s: string): string {
return `"${s}"`
}

function keyLine(path: string): string {
return `[${path}]`
}

function valueLine(path: string, value: ?string): string {
return `${path === "" ? "@" : quote(path)}=${value == null ? "-" : quote(value)}`
}

/**
* value expander for the script generators. if a value is not a string, it's another section
* that gets expanded recursively.
*
* remove is used to create value/key removal lines
*/
function expandValue(path: string, key: string, value: RegistryValue, buf: OperationBuffer, remove?: boolean): OperationBuffer {
if (typeof value === "string") {
buf[path].push(valueLine(key, remove ? null : value))
} else {
buf = expandSection(`${path}\\${key}`, value, buf, remove)
}
return buf
}

/**
* section expander for the script generator
*/
function expandSection(path: string, value: RegistrySubKey, buf: OperationBuffer, remove?: boolean): OperationBuffer {
if (buf[path] == null) buf[path] = []
for (const key in value) {
if (typeof value[key] !== "string" && remove) {
buf[`-${path}\\${key}`] = []
} else {
expandValue(path, key, value[key], buf, remove)
}
}
return buf
}

/**
* converts a map of registry paths to value setters into an executable registry script
* @param {OperationBuffer} buf List of operations the need to be done
* @returns {string} a windows registry script that can be imported by regex.exe to apply the operations
*/
function bufToScript(buf: OperationBuffer): string {
const lines = [header_line]
for (const key in buf) {
const next = buf[key]
if (next.length < 1 && !key.startsWith("-")) continue
lines.push("", keyLine(key))
lines.push(...next)
}
return lines.join("\r\n").trim()
}

/**
* the application and removal script generators are very similar in structure, this function abstracts over that.
*/
function scriptBuilder(remove: boolean, template: RegistryTemplateDefinition): string {
const buf = template.reduce((prev, {root, value}) => expandSection(root, value, prev, remove), {})
return bufToScript(buf)
}

/**
* create a windows registry script that can be executed to apply the given template
*/
export function applyScriptBuilder(template: RegistryTemplateDefinition): string {
return scriptBuilder(false, template)
}

/**
* create a windows registry script that can be executed to remove the values that have been
* created by executing the script generated from the template by applyScriptBuilder
*/
export function removeScriptBuilder(template: RegistryTemplateDefinition): string {
return scriptBuilder(true, template)
}