-
Notifications
You must be signed in to change notification settings - Fork 5
/
path-extender-windows.ts
161 lines (143 loc) · 6.23 KB
/
path-extender-windows.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { PnpmError } from '@pnpm/error'
import matchAll from 'string.prototype.matchall'
import { win32 as path } from 'path'
import execa from 'safe-execa'
class BadEnvVariableError extends PnpmError {
public envName: string
public wantedValue: string
public currentValue: string
constructor ({ envName, wantedValue, currentValue }: { envName: string, wantedValue: string, currentValue: string }) {
super('BAD_ENV_FOUND', `Currently '${envName}' is set to '${wantedValue}'`)
this.envName = envName
this.wantedValue = wantedValue
this.currentValue = currentValue
}
}
type IEnvironmentValueMatch = { groups: { name: string, type: string, data: string } } & RegExpMatchArray
const REG_KEY = 'HKEY_CURRENT_USER\\Environment'
export type AddingPosition = 'start' | 'end'
export interface AddDirToWindowsEnvPathOpts {
proxyVarName?: string
overwriteProxyVar?: boolean
position?: AddingPosition
}
export interface EnvVariableChange {
variable: string,
action: EnvVariableChangeAction
oldValue: string,
newValue: string,
}
export type PathExtenderWindowsReport = EnvVariableChange[]
export async function addDirToWindowsEnvPath (dir: string, opts?: AddDirToWindowsEnvPathOpts): Promise<PathExtenderWindowsReport> {
// Use `chcp` to make `reg` use utf8 encoding for output.
// Otherwise, the non-ascii characters in the environment variables will become garbled characters.
const chcpResult = await execa('chcp')
const cpMatch = /\d+/.exec(chcpResult.stdout) ?? []
const cpBak = parseInt(cpMatch[0])
if (chcpResult.failed || !(cpBak > 0)) {
throw new PnpmError('CHCP', `exec chcp failed: ${cpBak}, ${chcpResult.stderr}`)
}
await execa('chcp', ['65001'])
try {
const report = await _addDirToWindowsEnvPath(dir, opts)
await refreshEnvVars()
return report
} finally {
await execa('chcp', [cpBak.toString()])
}
}
export type EnvVariableChangeAction = 'skipped' | 'updated'
async function _addDirToWindowsEnvPath (dir: string, opts: AddDirToWindowsEnvPathOpts = {}): Promise<PathExtenderWindowsReport> {
const addedDir = path.normalize(dir)
const registryOutput = await getRegistryOutput()
const changes: PathExtenderWindowsReport = []
if (opts.proxyVarName) {
changes.push(await updateEnvVariable(registryOutput, opts.proxyVarName, addedDir, {
expandableString: false,
overwrite: opts.overwriteProxyVar,
}))
changes.push(await addToPath(registryOutput, `%${opts.proxyVarName}%`, opts.position))
} else {
changes.push(await addToPath(registryOutput, addedDir, opts.position))
}
return changes
}
async function updateEnvVariable (
registryOutput: string,
name: string,
value: string,
opts: {
expandableString: boolean
overwrite: boolean
}
): Promise<EnvVariableChange> {
const currentValue = await getEnvValueFromRegistry(registryOutput, name)
if (currentValue && !opts.overwrite) {
if (currentValue !== value) {
throw new BadEnvVariableError({ envName: name, currentValue, wantedValue: value })
}
return { variable: name, action: 'skipped', oldValue: currentValue, newValue: value }
} else {
await setEnvVarInRegistry(name, value, { expandableString: opts.expandableString })
return { variable: name, action: 'updated', oldValue: currentValue, newValue: value }
}
}
async function addToPath (registryOutput: string, addedDir: string, position: AddingPosition = 'start'): Promise<EnvVariableChange> {
const variable = 'Path'
const pathData = await getEnvValueFromRegistry(registryOutput, variable)
if (pathData === undefined || pathData == null || pathData.trim() === '') {
throw new PnpmError('NO_PATH', '"Path" environment variable is not found in the registry')
} else if (pathData.split(path.delimiter).includes(addedDir)) {
return { action: 'skipped', variable, oldValue: pathData, newValue: pathData }
} else {
const newPathValue = position === 'start'
? `${addedDir}${path.delimiter}${pathData}`
: `${pathData}${path.delimiter}${addedDir}`
await setEnvVarInRegistry('Path', newPathValue, { expandableString: true })
return { action: 'updated', variable, oldValue: pathData, newValue: newPathValue }
}
}
// `windowsHide` in `execa` is true by default, which will cause `chcp` to have no effect.
const EXEC_OPTS = { windowsHide: false }
/**
* We read all the registry values and then pick the keys that we need.
* This is done because if we would try to pick a key that is not in the registry, the command would fail.
* And it is hard to identify the real cause of the command failure.
*/
async function getRegistryOutput (): Promise<string> {
try {
const queryResult = await execa('reg', ['query', REG_KEY], EXEC_OPTS)
return queryResult.stdout
} catch (err: any) { // eslint-disable-line
throw new PnpmError('REG_READ', 'win32 registry environment values could not be retrieved')
}
}
async function getEnvValueFromRegistry (registryOutput: string, envVarName: string): Promise<string | undefined> {
const regexp = new RegExp(`^ {4}(?<name>${envVarName}) {4}(?<type>\\w+) {4}(?<data>.*)$`, 'gim')
const match = Array.from(matchAll(registryOutput, regexp))[0] as IEnvironmentValueMatch
return match?.groups.data
}
async function setEnvVarInRegistry (
envVarName: string,
envVarValue: string,
opts: {
expandableString: boolean
}
) {
const regType = opts.expandableString ? 'REG_EXPAND_SZ' : 'REG_SZ'
try {
await execa('reg', ['add', REG_KEY, '/v', envVarName, '/t', regType, '/d', envVarValue, '/f'], EXEC_OPTS)
} catch (err: any) { // eslint-disable-line
throw new PnpmError('FAILED_SET_ENV', `Failed to set "${envVarName}" to "${envVarValue}": ${err.stderr as string}`)
}
}
// When setting environment variables through the registry, they will not be recognized immediately.
// There is a workaround though, to set at least one environment variable with `setx`.
// We have some redundancy here because we run it for each env var.
// It would be enough also to run it only for the last changed env var.
// Read more at: https://bit.ly/39OlQnF
async function refreshEnvVars () {
const TEMP_ENV_VAR = 'REFRESH_ENV_VARS' // This is just a random env var name.
await execa('setx', [TEMP_ENV_VAR, '1'], EXEC_OPTS)
await execa('reg', ['delete', REG_KEY, '/v', TEMP_ENV_VAR, '/f'], EXEC_OPTS)
}