master
js 299 lines 11.2 KB
Raw
1 /*
2 Copyright 2022 Intel Corporation
3 @author Bryan Roe
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16 */
17
18 //
19 // win-deskutils is a utility module that exposes various desktop related features for Windows
20 // such as MouseTrails Accessability and Windows Desktop Background
21 //
22
23 //
24 // MSDN documention for the system call this module relies on can be found at:
25 // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfoa
26 //
27
28 var SPI_GETDESKWALLPAPER = 0x0073;
29 var SPI_SETDESKWALLPAPER = 0x0014;
30 var SPI_GETMOUSETRAILS = 0x005E;
31 var SPI_SETMOUSETRAILS = 0x005D;
32
33 var GM = require('_GenericMarshal');
34 var user32 = GM.CreateNativeProxy('user32.dll');
35 user32.CreateMethod('SystemParametersInfoA');
36 user32.CreateMethod('GetLastInputInfo');
37 var kernel32 = GM.CreateNativeProxy('kernel32.dll');
38 kernel32.CreateMethod('GetTickCount');
39
40 //
41 // This function is a helper method to dispatch method calls to different user sessions
42 //
43 function sessionDispatch(tsid, parent, method, args)
44 {
45 //
46 // Check to see if the process owner of the current processor is root
47 //
48 var sid = undefined;
49 var stype = require('user-sessions').getProcessOwnerName(process.pid).tsid == 0 ? 1 : 0;
50 /*
51 The following is the list of possible values for stype.
52 If the current process owner is root, we set the stype to user,
53 because we cannot set/get any properties from this user, we
54 must switch to a user session.. Default behavior for stype(1)
55 is that it will context switch to the logged in user. If
56 this is not intended, then an actual user TSID must be specified, using
57 ILibProcessPipe_SpawnTypes_SPECIFIED_USER and the actual TSID
58 ------------------------------------------------------------------------
59 ILibProcessPipe_SpawnTypes_DEFAULT = 0,
60 ILibProcessPipe_SpawnTypes_USER = 1,
61 ILibProcessPipe_SpawnTypes_WINLOGON = 2,
62 ILibProcessPipe_SpawnTypes_TERM = 3,
63 ILibProcessPipe_SpawnTypes_DETACHED = 4,
64 ILibProcessPipe_SpawnTypes_SPECIFIED_USER = 5,
65 ILibProcessPipe_SpawnTypes_POSIX_DETACHED = 0x8000
66 ------------------------------------------------------------------------
67 */
68 console.log('stype: ' + stype);
69 if (stype == 1)
70 {
71 if (tsid == null && require('MeshAgent')._tsid != null)
72 {
73 stype = 5; // ILibProcessPipe_SpawnTypes_SPECIFIED_USER
74 sid = require('MeshAgent')._tsid; // If this is set, it was set via user selection UI
75 }
76 else
77 {
78 sid = tsid; // Set the SID to be whatever was passed in
79 }
80 }
81
82 // Spawn a child process in the appropriate user session, and relay the response back via stdout
83 var mod = Buffer.from(getJSModule('win-deskutils')).toString('base64');
84 var prog = "try { addModule('win-deskutils', process.env['win_deskutils']);} catch (x) { } var x;try{x=require('win-deskutils').dispatch('" + parent + "', '" + method + "', " + JSON.stringify(args) + ");console.log(x);}catch(z){console.log(z);process.exit(1);}process.exit(0);";
85 var child = require('child_process').execFile(process.execPath, [process.execPath.split('\\').pop(), '-b64exec', Buffer.from(prog).toString('base64')], { type: stype, uid: sid, env: { win_deskutils: getJSModule('win-deskutils') } });
86
87 child.stdout.str = '';
88 child.stdout.on('data', function (c) { this.str += c.toString(); });
89 child.stderr.on('data', function (c) { });
90 child.on('exit', function (c) { this.exitCode = c; });
91 child.waitExit();
92 if (child.exitCode == 0)
93 {
94 return (child.stdout.str.trim()); // If the return code was 0, then relay the response from stdout
95 }
96 else
97 {
98 throw (child.stdout.str.trim()); // If the return code was nonzero, then the stdout response is the exception that should be bubbled
99 }
100 }
101
102 //
103 // This function gets the path of the windows desktop background of the specified user desktop session
104 //
105 function background_get(tsid)
106 {
107 if (tsid != null || tsid === null) // TSID is not undefined or is explicitly null
108 {
109 // Need to disatch to different session first
110 return (sessionDispatch(tsid, 'background', 'get', []));
111 }
112 var v = GM.CreateVariable(1024);
113 var ret = user32.SystemParametersInfoA(SPI_GETDESKWALLPAPER, v._size, v, 0);
114 if (ret.Val == 0)
115 {
116 throw ('Error occured trying to fetch wallpaper');
117 }
118 return (v.String);
119 }
120
121 //
122 // This function sets the path for the windows desktop background of the specified user desktop session
123 //
124 function background_set(path, tsid)
125 {
126 if (tsid != null || tsid === null) // TSID is not undefined or is explicitly null
127 {
128 // Need to disatch to different session first
129 return (sessionDispatch(tsid, 'background', 'set', [path]));
130 }
131 var nb = GM.CreateVariable(path);
132 var ret = user32.SystemParametersInfoA(SPI_SETDESKWALLPAPER, nb._size, nb, 0);
133 if (ret.Val == 0)
134 {
135 throw ('Error occured trying to set wallpaper');
136 }
137 return;
138 }
139
140 //
141 // This is a helper function that is called by the child process from sessionDispatch()
142 //
143 function dispatch(parent, method, args)
144 {
145 try
146 {
147 return (this[parent][method].apply(this, args));
148 }
149 catch (e)
150 {
151 console.log('ERROR: ' + e);
152 throw ('Error occured trying to dispatch: ' + method);
153 }
154 }
155
156 //
157 // This function sets the mousetrail accessibility feature, for the specified user desktop session.
158 // Setting value 0 or one disables this feature
159 // Otherwise, value is the number of cursors to render for this feature
160 //
161 function mousetrails_set(value, tsid)
162 {
163 if (tsid != null || tsid === null) // TSID is not undefined or is explicitly null
164 {
165 // Need to disatch to different session first
166 return (sessionDispatch(tsid, 'mouse', 'setTrails', [value]));
167 }
168 var ret = user32.SystemParametersInfoA(SPI_SETMOUSETRAILS, value, 0, 0);
169 if (ret.Val == 0)
170 {
171 throw ('Error occured trying to fetch wallpaper');
172 }
173 }
174
175 //
176 // This function returns the number of cursors the mousetrail accessibility feature will render
177 // A value of 0 or 1 means the feature is disabled, otherwise it is the number of cursors that will be rendered
178 //
179 function mousetrails_get(tsid)
180 {
181 if (tsid != null || tsid === null) // TSID is not undefined or is explicitly null
182 {
183 // Need to disatch to different session first
184 return (sessionDispatch(tsid, 'mouse', 'getTrails', []));
185 }
186 var v = GM.CreateVariable(4);
187 var ret = user32.SystemParametersInfoA(SPI_GETMOUSETRAILS, v._size, v, 0);
188 if (ret.Val == 0)
189 {
190 throw ('Error occured trying to fetch wallpaper');
191 }
192 return (v.toBuffer().readUInt32LE());
193 }
194
195 //
196 // This function returns the number of seconds since the last keyboard or mouse input from the user.
197 // It uses GetLastInputInfo from user32.dll to retrieve the last input tick count,
198 // then compares it against the current tick count from GetTickCount.
199 //
200 // Both GetTickCount and the dwTime field from GetLastInputInfo are 32-bit values that wrap
201 // around after approximately 49.7 days. This function handles the wraparound case correctly.
202 //
203 // MSDN documentation:
204 // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getlastinputinfo
205 //
206 function idle_getSeconds(tsid)
207 {
208 if (tsid != null || tsid === null) // TSID is not undefined or is explicitly null
209 {
210 // Need to dispatch to different session first
211 return (sessionDispatch(tsid, 'idle', 'getSeconds', []));
212 }
213
214 // Allocate 8 bytes for the LASTINPUTINFO struct:
215 // UINT cbSize (offset 0, 4 bytes) - must be set to 8 before calling
216 // DWORD dwTime (offset 4, 4 bytes) - tick count of last input event
217 var lii = GM.CreateVariable(8);
218
219 // cbSize must be set to the size of the struct (8) before calling
220 lii.toBuffer().writeUInt32LE(8, 0);
221
222 var ret = user32.GetLastInputInfo(lii);
223 if (ret.Val == 0)
224 {
225 throw ('Error occured trying to get last input info');
226 }
227
228 // Read dwTime from byte offset 4
229 var dwTime = lii.toBuffer().readUInt32LE(4);
230
231 // GetTickCount returns the number of milliseconds since system boot (32-bit, wraps after ~49.7 days)
232 var tickNow = kernel32.GetTickCount().Val;
233
234 // Handle 32-bit wraparound case
235 var idleMs;
236 if (tickNow >= dwTime)
237 {
238 // Normal case: no wraparound
239 idleMs = tickNow - dwTime;
240 }
241 else
242 {
243 // Wraparound occurred: tickNow wrapped to 0 while dwTime is still large
244 // Calculate the time from dwTime to the wrap point (0xFFFFFFFF) plus time since wrap
245 idleMs = (0xFFFFFFFF - dwTime) + tickNow + 1;
246 }
247
248 return Math.floor(idleMs / 1000);
249 }
250
251 //
252 // This function returns the minimum idle time across all active/connected user sessions.
253 // This is useful for detecting if ANY user (console or RDP) is actively using the machine.
254 // Returns a promise that resolves to the minimum idle seconds, or -1 if no users are logged in.
255 //
256 function idle_getSecondsAllSessions()
257 {
258 var promise = require('promise');
259 return new promise(function (resolve, reject)
260 {
261 require('user-sessions').enumerateUsers().then(function (sessions)
262 {
263 var minIdleSeconds = Infinity;
264 for (var sessionId in sessions)
265 {
266 var session = sessions[sessionId];
267
268 // Only check Active sessions with a logged-in user (Username is present)
269 // Skip "Connected" sessions (console when disconnected) and "Listening" sessions
270 if (session.State === 'Active' && session.Username && session.Username !== '')
271 {
272 try
273 {
274 var idleSeconds = parseFloat(sessionDispatch(session.SessionId, 'idle', 'getSeconds', []));
275 if (idleSeconds < minIdleSeconds)
276 {
277 minIdleSeconds = idleSeconds;
278 }
279 }
280 catch (e)
281 {
282 // Session might not support GetLastInputInfo, skip it
283 }
284 }
285 }
286
287 // If no active user sessions found, return -1 to indicate "no users logged in"
288 resolve(minIdleSeconds === Infinity ? -1 : Math.floor(minIdleSeconds));
289 }).catch(function (err)
290 {
291 reject(err);
292 });
293 });
294 }
295
296 module.exports = { background: { get: background_get, set: background_set } };
297 module.exports.mouse = { getTrails: mousetrails_get, setTrails: mousetrails_set };
298 module.exports.idle = { getSeconds: idle_getSeconds, getSecondsAllSessions: idle_getSecondsAllSessions };
299 module.exports.dispatch = dispatch;