Outlook_Addin_LLM/node_modules/applicationinsights/out/Library/Sender.js

472 lines
23 KiB
JavaScript

"use strict";
var fs = require("fs");
var os = require("os");
var path = require("path");
var zlib = require("zlib");
var child_process = require("child_process");
var Logging = require("./Logging");
var AutoCollectHttpDependencies = require("../AutoCollection/HttpDependencies");
var Util = require("./Util");
var Sender = (function () {
function Sender(config, onSuccess, onError) {
this._config = config;
this._onSuccess = onSuccess;
this._onError = onError;
this._enableDiskRetryMode = false;
this._resendInterval = Sender.WAIT_BETWEEN_RESEND;
this._maxBytesOnDisk = Sender.MAX_BYTES_ON_DISK;
this._numConsecutiveFailures = 0;
this._resendTimer = null;
if (!Sender.OS_PROVIDES_FILE_PROTECTION) {
// Node's chmod levels do not appropriately restrict file access on Windows
// Use the built-in command line tool ICACLS on Windows to properly restrict
// access to the temporary directory used for disk retry mode.
if (Sender.USE_ICACLS) {
// This should be async - but it's currently safer to have this synchronous
// This guarantees we can immediately fail setDiskRetryMode if we need to
try {
Sender.OS_PROVIDES_FILE_PROTECTION = fs.existsSync(Sender.ICACLS_PATH);
}
catch (e) { }
if (!Sender.OS_PROVIDES_FILE_PROTECTION) {
Logging.warn(Sender.TAG, "Could not find ICACLS in expected location! This is necessary to use disk retry mode on Windows.");
}
}
else {
// chmod works everywhere else
Sender.OS_PROVIDES_FILE_PROTECTION = true;
}
}
}
/**
* Enable or disable offline mode
*/
Sender.prototype.setDiskRetryMode = function (value, resendInterval, maxBytesOnDisk) {
this._enableDiskRetryMode = Sender.OS_PROVIDES_FILE_PROTECTION && value;
if (typeof resendInterval === 'number' && resendInterval >= 0) {
this._resendInterval = Math.floor(resendInterval);
}
if (typeof maxBytesOnDisk === 'number' && maxBytesOnDisk >= 0) {
this._maxBytesOnDisk = Math.floor(maxBytesOnDisk);
}
if (value && !Sender.OS_PROVIDES_FILE_PROTECTION) {
this._enableDiskRetryMode = false;
Logging.warn(Sender.TAG, "Ignoring request to enable disk retry mode. Sufficient file protection capabilities were not detected.");
}
};
Sender.prototype.send = function (payload, callback) {
var _this = this;
var endpointUrl = this._config.endpointUrl;
// todo: investigate specifying an agent here: https://nodejs.org/api/http.html#http_class_http_agent
var options = {
method: "POST",
withCredentials: false,
headers: {
"Content-Type": "application/x-json-stream"
}
};
zlib.gzip(payload, function (err, buffer) {
var dataToSend = buffer;
if (err) {
Logging.warn(err);
dataToSend = payload; // something went wrong so send without gzip
options.headers["Content-Length"] = payload.length.toString();
}
else {
options.headers["Content-Encoding"] = "gzip";
options.headers["Content-Length"] = buffer.length;
}
Logging.info(Sender.TAG, options);
// Ensure this request is not captured by auto-collection.
options[AutoCollectHttpDependencies.disableCollectionRequestOption] = true;
var requestCallback = function (res) {
res.setEncoding("utf-8");
//returns empty if the data is accepted
var responseString = "";
res.on("data", function (data) {
responseString += data;
});
res.on("end", function () {
_this._numConsecutiveFailures = 0;
Logging.info(Sender.TAG, responseString);
if (typeof _this._onSuccess === "function") {
_this._onSuccess(responseString);
}
if (typeof callback === "function") {
callback(responseString);
}
if (_this._enableDiskRetryMode) {
// try to send any cached events if the user is back online
if (res.statusCode === 200) {
if (!_this._resendTimer) {
_this._resendTimer = setTimeout(function () {
_this._resendTimer = null;
_this._sendFirstFileOnDisk();
}, _this._resendInterval);
_this._resendTimer.unref();
}
// store to disk in case of burst throttling
}
else if (res.statusCode === 408 ||
res.statusCode === 429 ||
res.statusCode === 439 ||
res.statusCode === 500 ||
res.statusCode === 503) {
// TODO: Do not support partial success (206) until _sendFirstFileOnDisk checks payload age
_this._storeToDisk(payload);
}
}
});
};
var req = Util.makeRequest(_this._config, endpointUrl, options, requestCallback);
req.on("error", function (error) {
// todo: handle error codes better (group to recoverable/non-recoverable and persist)
_this._numConsecutiveFailures++;
// Only use warn level if retries are disabled or we've had some number of consecutive failures sending data
// This is because warn level is printed in the console by default, and we don't want to be noisy for transient and self-recovering errors
// Continue informing on each failure if verbose logging is being used
if (!_this._enableDiskRetryMode || _this._numConsecutiveFailures > 0 && _this._numConsecutiveFailures % Sender.MAX_CONNECTION_FAILURES_BEFORE_WARN === 0) {
var notice = "Ingestion endpoint could not be reached. This batch of telemetry items has been lost. Use Disk Retry Caching to enable resending of failed telemetry. Error:";
if (_this._enableDiskRetryMode) {
notice = "Ingestion endpoint could not be reached " + _this._numConsecutiveFailures + " consecutive times. There may be resulting telemetry loss. Most recent error:";
}
Logging.warn(Sender.TAG, notice, error);
}
else {
var notice = "Transient failure to reach ingestion endpoint. This batch of telemetry items will be retried. Error:";
Logging.info(Sender.TAG, notice, error);
}
_this._onErrorHelper(error);
if (typeof callback === "function") {
var errorMessage = "error sending telemetry";
if (error && (typeof error.toString === "function")) {
errorMessage = error.toString();
}
callback(errorMessage);
}
if (_this._enableDiskRetryMode) {
_this._storeToDisk(payload);
}
});
req.write(dataToSend);
req.end();
});
};
Sender.prototype.saveOnCrash = function (payload) {
if (this._enableDiskRetryMode) {
this._storeToDiskSync(payload);
}
};
Sender.prototype._runICACLS = function (args, callback) {
var aclProc = child_process.spawn(Sender.ICACLS_PATH, args, { windowsHide: true });
aclProc.on("error", function (e) { return callback(e); });
aclProc.on("close", function (code, signal) {
return callback(code === 0 ? null : new Error("Setting ACL restrictions did not succeed (ICACLS returned code " + code + ")"));
});
};
Sender.prototype._runICACLSSync = function (args) {
// Some very old versions of Node (< 0.11) don't have this
if (child_process.spawnSync) {
var aclProc = child_process.spawnSync(Sender.ICACLS_PATH, args, { windowsHide: true });
if (aclProc.error) {
throw aclProc.error;
}
else if (aclProc.status !== 0) {
throw new Error("Setting ACL restrictions did not succeed (ICACLS returned code " + aclProc.status + ")");
}
}
else {
throw new Error("Could not synchronously call ICACLS under current version of Node.js");
}
};
Sender.prototype._getACLIdentity = function (callback) {
if (Sender.ACL_IDENTITY) {
return callback(null, Sender.ACL_IDENTITY);
}
var psProc = child_process.spawn(Sender.POWERSHELL_PATH, ["-Command", "[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"], {
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'] // Needed to prevent hanging on Win 7
});
var data = "";
psProc.stdout.on("data", function (d) { return data += d; });
psProc.on("error", function (e) { return callback(e, null); });
psProc.on("close", function (code, signal) {
Sender.ACL_IDENTITY = data && data.trim();
return callback(code === 0 ? null : new Error("Getting ACL identity did not succeed (PS returned code " + code + ")"), Sender.ACL_IDENTITY);
});
};
Sender.prototype._getACLIdentitySync = function () {
if (Sender.ACL_IDENTITY) {
return Sender.ACL_IDENTITY;
}
// Some very old versions of Node (< 0.11) don't have this
if (child_process.spawnSync) {
var psProc = child_process.spawnSync(Sender.POWERSHELL_PATH, ["-Command", "[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"], {
windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'] // Needed to prevent hanging on Win 7
});
if (psProc.error) {
throw psProc.error;
}
else if (psProc.status !== 0) {
throw new Error("Getting ACL identity did not succeed (PS returned code " + psProc.status + ")");
}
Sender.ACL_IDENTITY = psProc.stdout && psProc.stdout.toString().trim();
return Sender.ACL_IDENTITY;
}
else {
throw new Error("Could not synchronously get ACL identity under current version of Node.js");
}
};
Sender.prototype._getACLArguments = function (directory, identity) {
return [directory,
"/grant", "*S-1-5-32-544:(OI)(CI)F",
"/grant", identity + ":(OI)(CI)F",
"/inheritance:r"]; // Remove all inherited permissions
};
Sender.prototype._applyACLRules = function (directory, callback) {
var _this = this;
if (!Sender.USE_ICACLS) {
return callback(null);
}
// For performance, only run ACL rules if we haven't already during this session
if (Sender.ACLED_DIRECTORIES[directory] === undefined) {
// Avoid multiple calls race condition by setting ACLED_DIRECTORIES to false for this directory immediately
// If batches are being failed faster than the processes spawned below return, some data won't be stored to disk
// This is better than the alternative of potentially infinitely spawned processes
Sender.ACLED_DIRECTORIES[directory] = false;
// Restrict this directory to only current user and administrator access
this._getACLIdentity(function (err, identity) {
if (err) {
Sender.ACLED_DIRECTORIES[directory] = false; // false is used to cache failed (vs undefined which is "not yet tried")
return callback(err);
}
else {
_this._runICACLS(_this._getACLArguments(directory, identity), function (err) {
Sender.ACLED_DIRECTORIES[directory] = !err;
return callback(err);
});
}
});
}
else {
return callback(Sender.ACLED_DIRECTORIES[directory] ? null :
new Error("Setting ACL restrictions did not succeed (cached result)"));
}
};
Sender.prototype._applyACLRulesSync = function (directory) {
if (Sender.USE_ICACLS) {
// For performance, only run ACL rules if we haven't already during this session
if (Sender.ACLED_DIRECTORIES[directory] === undefined) {
this._runICACLSSync(this._getACLArguments(directory, this._getACLIdentitySync()));
Sender.ACLED_DIRECTORIES[directory] = true; // If we get here, it succeeded. _runIACLSSync will throw on failures
return;
}
else if (!Sender.ACLED_DIRECTORIES[directory]) {
throw new Error("Setting ACL restrictions did not succeed (cached result)");
}
}
};
Sender.prototype._confirmDirExists = function (directory, callback) {
var _this = this;
fs.lstat(directory, function (err, stats) {
if (err && err.code === 'ENOENT') {
fs.mkdir(directory, function (err) {
if (err && err.code !== 'EEXIST') {
callback(err);
}
else {
_this._applyACLRules(directory, callback);
}
});
}
else if (!err && stats.isDirectory()) {
_this._applyACLRules(directory, callback);
}
else {
callback(err || new Error("Path existed but was not a directory"));
}
});
};
/**
* Computes the size (in bytes) of all files in a directory at the root level. Asynchronously.
*/
Sender.prototype._getShallowDirectorySize = function (directory, callback) {
// Get the directory listing
fs.readdir(directory, function (err, files) {
if (err) {
return callback(err, -1);
}
var error = null;
var totalSize = 0;
var count = 0;
if (files.length === 0) {
callback(null, 0);
return;
}
// Query all file sizes
for (var i = 0; i < files.length; i++) {
fs.stat(path.join(directory, files[i]), function (err, fileStats) {
count++;
if (err) {
error = err;
}
else {
if (fileStats.isFile()) {
totalSize += fileStats.size;
}
}
if (count === files.length) {
// Did we get an error?
if (error) {
callback(error, -1);
}
else {
callback(error, totalSize);
}
}
});
}
});
};
/**
* Computes the size (in bytes) of all files in a directory at the root level. Synchronously.
*/
Sender.prototype._getShallowDirectorySizeSync = function (directory) {
var files = fs.readdirSync(directory);
var totalSize = 0;
for (var i = 0; i < files.length; i++) {
totalSize += fs.statSync(path.join(directory, files[i])).size;
}
return totalSize;
};
/**
* Stores the payload as a json file on disk in the temp directory
*/
Sender.prototype._storeToDisk = function (payload) {
var _this = this;
// tmpdir is /tmp for *nix and USERDIR/AppData/Local/Temp for Windows
var directory = path.join(os.tmpdir(), Sender.TEMPDIR_PREFIX + this._config.instrumentationKey);
// This will create the dir if it does not exist
// Default permissions on *nix are directory listing from other users but no file creations
Logging.info(Sender.TAG, "Checking existence of data storage directory: " + directory);
this._confirmDirExists(directory, function (error) {
if (error) {
Logging.warn(Sender.TAG, "Error while checking/creating directory: " + (error && error.message));
_this._onErrorHelper(error);
return;
}
_this._getShallowDirectorySize(directory, function (err, size) {
if (err || size < 0) {
Logging.warn(Sender.TAG, "Error while checking directory size: " + (err && err.message));
_this._onErrorHelper(err);
return;
}
else if (size > _this._maxBytesOnDisk) {
Logging.warn(Sender.TAG, "Not saving data due to max size limit being met. Directory size in bytes is: " + size);
return;
}
//create file - file name for now is the timestamp, a better approach would be a UUID but that
//would require an external dependency
var fileName = new Date().getTime() + ".ai.json";
var fileFullPath = path.join(directory, fileName);
// Mode 600 is w/r for creator and no read access for others (only applies on *nix)
// For Windows, ACL rules are applied to the entire directory (see logic in _confirmDirExists and _applyACLRules)
Logging.info(Sender.TAG, "saving data to disk at: " + fileFullPath);
fs.writeFile(fileFullPath, payload, { mode: 384 }, function (error) { return _this._onErrorHelper(error); });
});
});
};
/**
* Stores the payload as a json file on disk using sync file operations
* this is used when storing data before crashes
*/
Sender.prototype._storeToDiskSync = function (payload) {
// tmpdir is /tmp for *nix and USERDIR/AppData/Local/Temp for Windows
var directory = path.join(os.tmpdir(), Sender.TEMPDIR_PREFIX + this._config.instrumentationKey);
try {
Logging.info(Sender.TAG, "Checking existence of data storage directory: " + directory);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory);
}
// Make sure permissions are valid
this._applyACLRulesSync(directory);
var dirSize = this._getShallowDirectorySizeSync(directory);
if (dirSize > this._maxBytesOnDisk) {
Logging.info(Sender.TAG, "Not saving data due to max size limit being met. Directory size in bytes is: " + dirSize);
return;
}
//create file - file name for now is the timestamp, a better approach would be a UUID but that
//would require an external dependency
var fileName = new Date().getTime() + ".ai.json";
var fileFullPath = path.join(directory, fileName);
// Mode 600 is w/r for creator and no access for anyone else (only applies on *nix)
Logging.info(Sender.TAG, "saving data before crash to disk at: " + fileFullPath);
fs.writeFileSync(fileFullPath, payload, { mode: 384 });
}
catch (error) {
Logging.warn(Sender.TAG, "Error while saving data to disk: " + (error && error.message));
this._onErrorHelper(error);
}
};
/**
* Check for temp telemetry files
* reads the first file if exist, deletes it and tries to send its load
*/
Sender.prototype._sendFirstFileOnDisk = function () {
var _this = this;
var tempDir = path.join(os.tmpdir(), Sender.TEMPDIR_PREFIX + this._config.instrumentationKey);
fs.exists(tempDir, function (exists) {
if (exists) {
fs.readdir(tempDir, function (error, files) {
if (!error) {
files = files.filter(function (f) { return path.basename(f).indexOf(".ai.json") > -1; });
if (files.length > 0) {
var firstFile = files[0];
var filePath = path.join(tempDir, firstFile);
fs.readFile(filePath, function (error, payload) {
if (!error) {
// delete the file first to prevent double sending
fs.unlink(filePath, function (error) {
if (!error) {
_this.send(payload);
}
else {
_this._onErrorHelper(error);
}
});
}
else {
_this._onErrorHelper(error);
}
});
}
}
else {
_this._onErrorHelper(error);
}
});
}
});
};
Sender.prototype._onErrorHelper = function (error) {
if (typeof this._onError === "function") {
this._onError(error);
}
};
Sender.TAG = "Sender";
Sender.ICACLS_PATH = process.env.systemdrive + "/windows/system32/icacls.exe";
Sender.POWERSHELL_PATH = process.env.systemdrive + "/windows/system32/windowspowershell/v1.0/powershell.exe";
Sender.ACLED_DIRECTORIES = {};
Sender.ACL_IDENTITY = null;
// the amount of time the SDK will wait between resending cached data, this buffer is to avoid any throttling from the service side
Sender.WAIT_BETWEEN_RESEND = 60 * 1000;
Sender.MAX_BYTES_ON_DISK = 50 * 1000 * 1000;
Sender.MAX_CONNECTION_FAILURES_BEFORE_WARN = 5;
Sender.TEMPDIR_PREFIX = "appInsights-node";
Sender.OS_PROVIDES_FILE_PROTECTION = false;
Sender.USE_ICACLS = os.type() === "Windows_NT";
return Sender;
}());
module.exports = Sender;
//# sourceMappingURL=Sender.js.map