| 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; |