const { any } = require('bbhverse');
const fs = require('fs')
var path = require('path');
var cli = require('./cliverse')
var nodeShellExec = cli.nodeShellExec;
function elevatedRunIPCWriteMessage( target, m ) {
fs.writeFileSync(target, ', ' + JSON.stringify( m ), { 'flag': 'a+' })
}
function toHTML(l, x, r, ol, or){
ol = ol || '
'
or = or || '
'
l = l || ''
r = r || '
'
if(Object.prototype.toString.call(x) === '[object Array]') {
var ahtml = []
x.forEach(xi => { ahtml.push( `${l}${xi}${r}` ) })
return `${ol}${ahtml.join('')}${or}`
}
}
var __isElevated = null;
var shell_verse = {
// getCommonTask is agnostic of whether we are running in an elevated shell or not. It runs in either case.
init( o ){ Object.assign(this, o) }
, downloadsdir : '../Downloads'
, getCommonTask( taskToRun ){ return ()=>{ return shell_verse.runTask(taskToRun) }}
, runTask : ( taskToRun ) => {
if (__isElevated) return shell_verse.elevatedRunner( taskToRun )
else return shell_verse.runNonElevated( taskToRun )
}
, elevatedRunner( taskToRun, inBatch ){
// PB : TODO -- Should be called only when we are in an elevated shell that was already requested from an unelevated shell with a batch of tasks.
try {
var runlogjson = `${taskToRun.selectedinstance.root}/.elxr/run-${taskToRun.runtimestamp}/run.log`
var __runasresult = null;
return taskToRun().then((r)=>{
// PB : TODO -- Every elevation should have its own messaging file. Async writes from multiple processes are a problem here...
elevatedRunIPCWriteMessage( runlogjson, { info : taskToRun.info, success: true } )
if(!inBatch) fs.writeFileSync('run.done', 'success') // PB : TODO -- This should be done conditionally if we are running inproc.
return __runasresult = r;
})
.catch((e) => {
elevatedRunIPCWriteMessage( runlogjson, e)
if(!inBatch)fs.writeFileSync('run.done', 'failure')
console.error(e)
})
.finally(() => {
// if(__runasresult && !__runasresult.skipped) fs.unlinkSync('run.done')
})
}
catch (e) {
console.error('Error Invalid command : ' + e)
if(!inBatch) fs.writeFileSync('run.done', 'error')
}
finally {
}
}
, getElevatedTask : function( taskToRun ){ return ()=>{ return shell_verse.runElevated(taskToRun) }}
, getElevatedTaskInBatch : function( taskToRun ){ return ()=>{ return shell_verse.runElevatedInBatch(taskToRun) }}
, runElevatedInBatch : ( taskToRun ) => {
if (__isElevated) return shell_verse.elevatedRunner(taskToRun, true)
else return shell_verse.requestElevation(shell_verse.elevatedRunner, taskToRun)
}
, runElevated : ( taskToRun ) => {
// Let shell_verse decide whether to Elevate Out of Proc or In Proc
// taskToRun by default is the launched command and args. Specially in windows out of proc.
// taskToRun = taskToRun || (()=>{ return op[processedArgs.label || processedArgs._[0] || 'undefined'](processedArgs) })
if(taskToRun.processedArgs.skipelevated) return Promise.resolve({ skipped : true });
if (__isElevated) {
return shell_verse.elevatedRunner(taskToRun)
}
else {
console.log('Requesting Elevated Privileges');
// requesteElevation is acutally request elevation and run. Both In Proc and Out of Proc.
// Linux doesnt require elevation for most commands...
return shell_verse.requestElevation(shell_verse.elevatedRunner, taskToRun)
}
}
, runElevatedBatch( batchToRun ){
// In windows we don't need to run each task. We hand over to another shell which in elevated state rebuilds the whole batch and runs.
// Irrespective of the batch we just call runElevated once.
if (__isElevated) {
return any(batchToRun).then((r)=>{
// PB : TODO -- Every elevation should have its own messaging file. Async writes from multiple processes are a problem here...
// fs.writeFileSync('run.log', ', ' + JSON.stringify( { info : taskToRun.info, success: true }), { 'flag': 'a+' })
fs.writeFileSync('run.done', 'success') // PB : TODO -- This should be done conditionally if we are running inproc.
return __runasresult = r;
})
.catch((e) => {
// fs.writeFileSync('run.log', ', ' + JSON.stringify(e), { 'flag': 'a+' })
fs.writeFileSync('run.done', 'failure')
console.error(e)
})
// .finally(() => {
// if(__runasresult && !__runasresult.skipped) fs.unlinkSync('run.done')
// });
}
else {
return this.runElevated(batchToRun[0])
}
}
, getNonElevatedTask : function( taskToRun ){ return ()=>{ return shell_verse.runNonElevated(taskToRun) } }
, runNonElevated : ( taskToRun ) => {
// Let shell_verse decide whether to Elevate Out of Proc or In Proc
if(__isElevated) {
return Promise.resolve( 'Skipping regular task in elevated shell.' ) // Regular tasks unless marked as common tasks should not run in elevated shell...
}
else {
// taskToRun by default is the launched command and args.
// taskToRun = taskToRun || (()=>{ return op[processedArgs.label || processedArgs._[0] || 'undefined'](processedArgs) })
return taskToRun().then(r=>{
taskToRun.statuslog.statuslog(null, taskToRun.info /*repo*/ )
return r;
}).catch((e) => {
e.info = taskToRun.info;
if(taskToRun.errHandler) throw taskToRun.errHandler(e)
taskToRun.statuslog.statuslog(e); //
// console.error(e)
throw e;
}).finally(()=>{})
}
}
, isElevated : ()=>{
return acquireElevationState().then( ()=>{
shell_verse.isElevated = () => {
return Promise.resolve(__isElevated)
}
return shell_verse.isElevated()
})
}
// , isElevationOutOfProc : ()=>{ return true }
, acquireElevationState : () => {
return nodeShellExec("fsutil", ["dirty", "query", "C:"], {
inherit: true
// , shell: true
, stdio: 'ignore'
, env: process.env
, title: `check privileged execution mode using "fsutil dirty query C:"`
}).then((exitcode) => {
console.log('Elevated')
__isElevated = true;
shell_verse.acquireElevationState = ()=> Promise.resolve(__isElevated);
shell_verse.isElevated = () => { return Promise.resolve(__isElevated)}
return __isElevated
}).catch((e) => {
__isElevated = false;
shell_verse.acquireElevationState = ()=> Promise.resolve(__isElevated);
shell_verse.isElevated = () => { return Promise.resolve(__isElevated)}
console.log('Not Elevated');
return __isElevated
})
// .finally(()=>{
// shell_verse.acquireElevationState = ()=> Promise.resolve(__isElevated);
// shell_verse.isElevated = () => { return Promise.resolve(__isElevated)}
// // return __isElevated; // Value returned from finally is not supported by node.
// })
}
, getTaskCheckExists : cli.createTask('getTaskCheckExists', 'where')
, getbash : ()=>{ return "C:\\Program Files\\Git\\bin\\sh.exe" }
, createJuntionOrLink : (dirOrFile, target, opts) =>{
return nodeShellExec('mklink', ['/J', dirOrFile, target], opts).catch((e) => { console.error(e) })
}
, removeJuncionOrLink : ( junctionOrLink )=>{
return nodeShellExec('rmdir', [junctionOrLink], { inherit: true, shell: true, env: process.env })
}
, requestElevation(elevatedRunner, taskToRun) {
// PB : TODO -- Multiple parallel request elevations should be queued into a batch and serialized as a single promise.
var processedArgs = taskToRun.processedArgs, statuslog = taskToRun.statuslog
// Wait for the runas to complete before we read it.
try {
fs.unlinkSync('run.done') // Need a unique file for aech elevated run.
}
catch (e) { } //Ignore
// Find node path to send to hta.
return nodeShellExec('where', ['node']).then(r => {
var args = taskToRun.args
var options = taskToRun.args ? taskToRun.args.pop() : {
inherit: true
, shell: true
, env: taskToRun.ENV
, runas: 'self'
, title: `runas`
}
options.env = Object.assign({}, taskToRun.ENV, { wd : options.cwd })
var spawntimestamp = (new Date()).getTime()
if(!args) {
var namedArgs = [];
console.log('result : ' + JSON.stringify(r))
Object.keys(processedArgs).forEach((v) => { v != '_' ? namedArgs.push('--' + v + '=' + processedArgs[v]) : null; })
// PB : TODO -- Convert all the cli args back to string.
args = [].concat(processedArgs._)
namedArgs.length > 0 ? args = args.concat(namedArgs.join(' ')) : null;
}
else {
// args = args.splice(-1) -- Already popped
args.push(`--root=${selectedinstance.root}`);
}
args.splice(0,0, path.normalize(`${taskToRun.selectedinstance.root}/.elxr/run-${taskToRun.runtimestamp}/windowselevate.hta`))
args.push('--runas=self');
// args.debug = true
if(args.debug) args.push(`--debug=${args.debug}`);
args.push(`--runtimestamp=${spawntimestamp}`);
// args.push(`--wd=${options.cwd}`);
// args.push('--nodepath=' + r.messages[r.messages.length - 1])
// if (!processedArgs.node_env) args.push('--node_env=' + ENV.NODE_ENV)
// if (processedArgs.debug) args.push('--debug=true') // Enable to debug elevated..
// console.dir(processedArgs._)
// console.dir(namedArgs.join(' '))
console.dir(args)
// throw 'test'
return nodeShellExec('MSHTA', [`"${args.join('" "')}"`], options ).then(() => {
// runas returned.
try {
// PB : TODO -- Log is comma prefixed. Needs to be proper JSON.
var runlogjson = `${taskToRun.selectedinstance.root}/.elxr/run-${spawntimestamp}/run.log`
var runaslog = JSON.parse('[' + fs.readFileSync(runlogjson, { flags: 'a+' }) + ']');
try { fs.unlinkSync(runlogjson) } catch(e){ } // PB : TODO -- Have a unique file for each elevated run.
// console.log( "runaslog : " + runaslog.length )
// Assemble elevated run results into the main run log
runaslog.forEach((logEntry) => {
statuslog.statuslog(logEntry.success ? null : logEntry, logEntry)
logEntry.success ? (console.log(['success :' + (logEntry.result || logEntry.success)]), console.log((logEntry.messages || []).join(' '))) : (console.error(['error :' + logEntry.result]), console.error((logEntry.messages || []).join(' ')))
})
}
catch (e) {
// We must have a runas log
statuslog.statuslog(e)
console.error('Run log error probably was not created by runas : ' + e)
}
})
.catch(err => console.error('Elevation failed : ' + JSON.stringify(err)));
})
}
, launchui() {
// PB : TODO -- Multiple parallel requests should be queued into a batch and serialized as a single promise.
// PB : TODO -- cleanup elevated execution code from here. This is a non elevated hta.
var processedArgs = this.processedArgs
// Wait for the runas to complete before we read it.
try {
fs.unlinkSync('run.done') // Need a unique file for aech elevated run.
}
catch (e) { } //Ignore
console.log(this.processedArgs.awaiturn)
// Find node path to send to hta.
return nodeShellExec('where', ['node']).then(r => {
var namedArgs = [];
console.log('result : ' + JSON.stringify(r))
Object.keys(processedArgs).forEach((v) => { v != '_' ? namedArgs.push('--' + v + '=' + processedArgs[v]) : null; })
// PB : TODO -- Convert all the cli args back to string.
var _args = []
if(processedArgs._[0] === 'launchui' ) { _args = processedArgs._.slice(1); _args.splice(0,0, 'pull') } // Default command.
else _args = processedArgs._;
var args = [ path.normalize(`${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/windowselevate.hta`) ].concat(_args)
namedArgs.length > 0 ? args = args.concat(namedArgs.join(' ')) : null;
// args.push('--runas=self');
var spawntimestamp = (new Date()).getTime()
args.push(`--runtimestamp=${spawntimestamp}`);
// args.push('--nodepath=' + r.messages[r.messages.length - 1])
// if (!processedArgs.node_env) args.push('--node_env=' + ENV.NODE_ENV)
// if (processedArgs.debug) args.push('--debug=true') // Enable to debug elevated..
// console.dir(processedArgs._)
// console.dir(namedArgs.join(' '))
console.dir(args)
// throw 'test'
return nodeShellExec('MSHTA', [`"${args.join('" "')}"`]
, {
inherit: true
, shell: true
, env: this.ENV
// , runas: 'self'
// , title: `runas`
}
).then(() => {
// runas returned.
try {
// PB : TODO -- Log is comma prefixed. Needs to be proper JSON.
var runlogjson = `${this.selectedinstance.root}/.elxr/run-${spawntimestamp}/run.log`
var runaslog = JSON.parse('[' + fs.readFileSync(runlogjson, { flags: 'a+' }) + ']');
try { fs.unlinkSync(runlogjson) } catch(e){ } // PB : TODO -- Have a unique file for each elevated run.
// console.log( "runaslog : " + runaslog.length )
// Assemble elevated run results into the main run log
runaslog.forEach((logEntry) => {
this.statuslog.statuslog(logEntry.success ? null : logEntry, logEntry)
logEntry.success ? (console.log(['success :' + (logEntry.result || logEntry.success)]), console.log((logEntry.messages || []).join(' '))) : (console.error(['error :' + logEntry.result]), console.error((logEntry.messages || []).join(' ')))
})
}
catch (e) {
// We must have a runas log
this.statuslog.statuslog(e)
console.error('Run log error probably was not created by runas : ' + e)
}
})
.catch(err => console.error('Elevation failed : ' + err));
})
}
, ensureDirectoryExistence(filePath) {
var dirname = path.dirname(filePath);
if (fs.existsSync(dirname)) {
return filePath;
}
this.ensureDirectoryExistence(dirname);
fs.mkdirSync(dirname);
return filePath;
}
, generateDependencies(){
// PB : TODO -- Keep only the last n runs...
// Currently it retains 2*n when proc needs to be relaunched in elevated mode !!!
this.ensureDirectoryExistence(`${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/download.bat`)
fs.writeFileSync(this.ensureDirectoryExistence(path.normalize(`${this.selectedinstance.root}/${this.downloadsdir}/readme.txt`)), `${this.getVersion()} Your local downloads for this instance`)
// PB : TODO include and build from files... using rollup..
var downloadbatch =
`::**************************************************************************
:Download_
Powershell.exe ^
$AllProtocols = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'; ^
[System.Net.ServicePointManager]::SecurityProtocol = $AllProtocols; ^
(New-Object System.Net.WebClient).DownloadFile('%1','%2')
exit /b
::**************************************************************************`
fs.writeFileSync(`${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/download.bat`, downloadbatch)
var windowselevate =
`
`
fs.writeFileSync(`${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/windowselevate.hta`, windowselevate)
}
, getchrome(){
// Chrome install location from registry.
return shell_verse.regread("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe\\").then(chromepath => {
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe
console.log(chromepath)
return chromepath
});
}
, regread(s){
var uniquerun = (new Date()).getTime()
var out = `${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/${uniquerun}-regread.out`
var outescaped = out.replace(/\\/g,"\\\\")
// console.log('out ::: ' + out)
fs.writeFileSync(`${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/${uniquerun}-regread.js`,
`
// WScript.Echo('------------UNNAMED-----------')
// WScript.Echo(WScript.Arguments.Item(0))
argEnumerator = new Enumerator(WScript.Arguments.Unnamed)
var args = []
for(; !argEnumerator.atEnd(); argEnumerator.moveNext()){
args.push('' + argEnumerator.item() + '')
// WScript.Echo('-arg-' + argEnumerator.item() + '-arg-') // Value
}
// WScript.Echo(args[0])
var objShell = WScript.createobject("wscript.shell")
if(!args[0]) {
WScript.Echo( '' )
}
else {
WScript.Echo(args[0])
var read = objShell.RegRead(args[0])
try {
var fs = new ActiveXObject('Scripting.FileSystemObject');
var fh = fs.CreateTextFile('${outescaped}', true);
fh.WriteLine(read);
fh.Close();
}
catch(e){ WScript.Echo(e.message) }
// WScript.Echo( read )
}
`
)
return nodeShellExec('cscript', [path.normalize(`${this.selectedinstance.root}/.elxr/run-${this.runtimestamp}/${uniquerun}-regread.js`), `${s}`])
.then(read => {
// console.log("REGREAD RESULT : " + read)
// console.dir(read)
if(read.success) {
return (fs.readFileSync(out) + "").trim()
}
else throw 'regread failed'
})
}
, iswin(){ return true}
, islin(){ return false}
}
module.exports = shell_verse