ACC SHELL
/**
* File: modules/AutoinstScripts.ycp
* Module: Auto-Installation
* Summary: Custom scripts
* Authors: Anas Nashif <nashif@suse.de>
*
* $Id: AutoinstScripts.ycp 61507 2010-03-26 09:44:13Z ug $
*/
{
module "AutoinstScripts";
textdomain "autoinst";
import "Mode";
import "AutoinstConfig";
import "Summary";
import "URL";
import "Service";
import "Popup";
import "Label";
import "Report";
include "autoinstall/io.ycp";
/* Pre scripts */
global list<map> pre = [];
/* Post scripts */
global list<map> post = [];
/* Chroot scripts */
global list<map> chroot = [];
/* Init scripts */
global list<map> init = [];
/* postpart scripts */
global list<map> postpart = [];
/* Merged scripts */
global list<map> merged = [];
/* default value of settings modified */
global boolean modified = false;
/**
* Function sets internal variable, which indicates, that any
* settings were modified, to "true"
*/
global define void SetModified ()
{
modified = true;
}
/**
* Functions which returns if the settings were modified
* @return boolean settings were modified
*/
global define boolean GetModified ()
{
return modified;
}
/**
* merge all types of scripts into one single list
* @param -
* @return merged list
*/
list<map> mergeScripts ()
{
list<map> result = maplist (map p, pre,
``{
p = add(p,"type","pre-scripts");
return p;
});
result = (list<map>)union(result, maplist (map p, post,
``{
p = add(p,"type","post-scripts");
return p;
})
);
result = (list<map>)union(result, maplist (map p, chroot,
``{
p = add(p,"type","chroot-scripts");
return p;
})
);
result = (list<map>)union(result, maplist (map p, init,
``{
p = add(p,"type","init-scripts");
return p;
})
);
result = (list<map>)union(result, maplist (map p, postpart,
``{
p = add(p,"type","postpartitioning-scripts");
return p;
})
);
return result;
}
/**
* Constructor
*/
define void AutoinstScripts()
{
if ( !Mode::autoinst () )
{
merged = mergeScripts();
}
}
/**
* Dump the settings to a map, for autoinstallation use.
* @return map
*/
global define map<string, list> Export()
{
pre = [];
post = [];
chroot = [];
init = [];
postpart = [];
y2milestone("Merged %1", merged);
// split
foreach(map s, merged, ``{
if (s["type"]:"" == "pre-scripts")
pre = add(pre,s);
else if (s["type"]:"" == "post-scripts")
post = add(post,s);
else if (s["type"]:"" == "init-scripts")
init = add(init,s);
else if (s["type"]:"" == "chroot-scripts")
chroot = add(chroot,s);
else if (s["type"]:"" == "postpartitioning-scripts")
postpart = add(postpart,s);
});
// clean
list<map> expre = maplist (map p, pre, ``{
return ($["filename":p["filename"]:"",
"interpreter": p["interpreter"]:"",
"source":p["source"]:"",
"notification":p["notification"]:"",
"location":p["location"]:"",
"feedback":p["feedback"]:false,
"feedback_type":p["feedback_type"]:"",
"debug":p["debug"]:true
]);
});
list<map> expost = maplist (map p, post, ``{
return ($["filename":p["filename"]:"",
"interpreter": p["interpreter"]:"",
"source":p["source"]:"",
"location":p["location"]:"",
"notification":p["notification"]:"",
"feedback":p["feedback"]:false,
"feedback_type":p["feedback_type"]:"",
"debug":p["debug"]:true,
"network_needed":p["network_needed"]:false
]
);
});
list<map> exchroot = maplist (map p, chroot, ``{
return ($["filename":p["filename"]:"",
"interpreter": p["interpreter"]:"",
"source":p["source"]:"",
"chrooted":p["chrooted"]:false,
"notification":p["notification"]:"",
"location":p["location"]:"",
"feedback":p["feedback"]:false,
"feedback_type":p["feedback_type"]:"",
"debug":p["debug"]:true
]);
});
list<map> exinit = maplist (map p, init, ``{
return ($["filename":p["filename"]:"",
"source":p["source"]:"",
"location":p["location"]:"",
"debug":p["debug"]:true
]);
});
list<map> expostpart = maplist (map p, postpart, ``{
return ($["filename":p["filename"]:"",
"interpreter": p["interpreter"]:"",
"source":p["source"]:"",
"location":p["location"]:"",
"notification":p["notification"]:"",
"feedback":p["feedback"]:false,
"feedback_type":p["feedback_type"]:"",
"debug":p["debug"]:true
]);
});
map<string, list> result = $[];
if (size(expre) > 0 )
result["pre-scripts"] = expre;
if (size(expost) > 0 )
result["post-scripts"] = expost;
if (size(exchroot) > 0 )
result["chroot-scripts"] = exchroot;
if (size(exinit) > 0 )
result["init-scripts"] = exinit;
if (size(expostpart) > 0 )
result["postpartitioning-scripts"] = expostpart;
return result;
}
global define list<map> Resolve_relurl( list<map> d ) {
d = maplist (map script , d, ``{
if( issubstring( script["location"]:"", "relurl://" ) ) {
string l = script["location"]:"";
l = substring ( l, 9 );
string newloc = "";
if( AutoinstConfig::scheme == "relurl" ) {
y2milestone("autoyast profile was relurl too");
newloc = (string)SCR::Read(.etc.install_inf.ayrelurl);
map tok = URL::Parse(newloc);
y2milestone("tok = %1", tok);
newloc = tok["scheme"]:"" + "://" + tok["host"]:"" + "/" + dirname(tok["path"]:"") + l;
} else {
newloc = AutoinstConfig::scheme + "://" + AutoinstConfig::host + "/" + AutoinstConfig::directory + l;
}
script["location"] = newloc;
y2milestone("changed relurl to %1 for script", newloc);
}
return script;
});
return d;
}
/**
* Get all the configuration from a map.
* When called by autoinst_<module name> (preparing autoinstallation data)
* the map may be empty.
* @param settings $[...]
* @return success
*/
global define boolean Import(map s)
{
y2debug("Calling AutoinstScripts::Import()");
pre = s["pre-scripts"]:[];
init = s["init-scripts"]:[];
post = s["post-scripts"]:[];
chroot = s["chroot-scripts"]:[];
postpart = s["postpartitioning-scripts"]:[];
pre = Resolve_relurl(pre);
init = Resolve_relurl(init);
post = Resolve_relurl(post);
chroot = Resolve_relurl(chroot);
postpart = Resolve_relurl(postpart);
merged = mergeScripts();
y2debug("merged: %1", merged);
return true;
}
/**
* Return Summary
* @return string summary
*/
global define string Summary()
{
string summary = "";
summary = Summary::AddHeader(summary, _("Preinstallation Scripts"));
if (size( pre) > 0 )
{
summary = Summary::OpenList(summary);
foreach(map script, pre, ``{
summary = Summary::AddListItem(summary, script["filename"]:"" );
});
summary = Summary::CloseList(summary);
}
else
{
summary = Summary::AddLine(summary, Summary::NotConfigured());
}
summary = Summary::AddHeader(summary, _("Postinstallation Scripts"));
if (size( post) > 0)
{
summary = Summary::OpenList(summary);
foreach(map script, post, ``{
summary = Summary::AddListItem(summary, script["filename"]:"" );
});
summary = Summary::CloseList(summary);
}
else
{
summary = Summary::AddLine(summary, Summary::NotConfigured());
}
summary = Summary::AddHeader(summary, _("Chroot Scripts"));
if (size( chroot) > 0 )
{
summary = Summary::OpenList(summary);
foreach(map script, chroot, ``{
summary = Summary::AddListItem(summary, script["filename"]:"" );
});
summary = Summary::CloseList(summary);
}
else
{
summary = Summary::AddLine(summary, Summary::NotConfigured());
}
summary = Summary::AddHeader(summary, _("Init Scripts"));
if (size( init) > 0 )
{
summary = Summary::OpenList(summary);
foreach(map script, init, ``{
summary = Summary::AddListItem(summary, script["filename"]:"" );
});
summary = Summary::CloseList(summary);
}
else
{
summary = Summary::AddLine(summary, Summary::NotConfigured());
}
summary = Summary::AddHeader(summary, _("Postpartitioning Scripts"));
if (size( postpart ) > 0 )
{
summary = Summary::OpenList(summary);
foreach(map script, postpart, ``{
summary = Summary::AddListItem(summary, script["filename"]:"" );
});
summary = Summary::CloseList(summary);
}
else
{
summary = Summary::AddLine(summary, Summary::NotConfigured());
}
return summary;
}
/**
* delete a script from a list
* @param script name
* @return void
*/
global define void deleteScript(string scriptName)
{
list<map> clean = filter(map s, merged, ``(s["filename"]:"" != scriptName));
merged = clean;
return;
}
/**
* Add or edit a script
* @param scriptName script name
* @param source source of script
* @param interpreter interpreter to be used with script
* @param type type of script
* @return void
*/
global define void AddEditScript(string scriptName,
string source,
string interpreter,
string type,
boolean chrooted,
boolean debug,
boolean feedback,
boolean network,
string feedback_type,
string location,
string notification
)
{
boolean mod = false;
merged = maplist (map script , merged, ``{
// Edit
if (script["filename"]:"" == scriptName)
{
map oldScript = $[];
oldScript=add(oldScript,"filename", scriptName);
oldScript=add(oldScript,"source", source);
oldScript=add(oldScript,"interpreter", interpreter);
oldScript=add(oldScript,"type", type);
oldScript=add(oldScript,"chrooted", chrooted);
oldScript=add(oldScript,"debug",debug);
oldScript=add(oldScript,"feedback",feedback);
oldScript=add(oldScript,"network_needed",network);
oldScript=add(oldScript,"feedback_type", feedback_type);
oldScript=add(oldScript,"location", location);
oldScript=add(oldScript,"notification", notification);
mod = true;
return oldScript;
}
else {
return script;
}
});
if (!mod)
{
map script = $[];
script=add(script,"filename", scriptName);
script=add(script,"source", source);
script=add(script,"interpreter", interpreter);
script=add(script,"type", type);
script=add(script,"chrooted", chrooted);
script=add(script,"debug",debug);
script=add(script,"feedback",feedback);
script=add(script,"network_needed",network);
script=add(script,"feedback_type", feedback_type);
script=add(script,"location", location);
script=add(script,"notification", notification);
merged=add(merged,script);
}
y2debug("Merged scripts: %1", merged);
return;
}
/**
* return type of script as formatted string
* @param script type
* @return string type as translated string
*/
global define string typeString(string type)
{
if (type == "pre-scripts")
{
return _("Pre");
}
else if (type == "post-scripts")
{
return _("Post");
}
else if (type == "init-scripts")
{
return _("Init");
}
else if (type == "chroot-scripts")
{
return _("Chroot");
}
else if (type == "postpartitioning-scripts")
{
return _("Postpartitioning");
}
return _("Unknown");
}
/*
bidirectional feedback during script execution
Experimental
*/
define map<string,string> splitParams( string s ) ``{
list<string> l = splitstring( s, "|" );
map<string,string> ret = $[];
l = remove( l, 0 );
foreach( string element, l, ``{
list<string> p = splitstring( element, "=" );
ret[ p[0]:"" ] = p[1]:"";
});
return ret;
}
define void interactiveScript( string shell, string debug, string scriptPath, string current_logdir, string scriptName ) ``{
map data = $[];
string widget = "";
SCR::Execute(.target.remove, "/tmp/ay_opipe");
SCR::Execute(.target.remove, "/tmp/ay_ipipe");
SCR::Execute(.target.bash, "mkfifo -m 660 /tmp/ay_opipe", $[]);
SCR::Execute(.target.bash, "mkfifo -m 660 /tmp/ay_ipipe", $[]);
string execute = sformat("%1 %2 %3 2&> %4/%5.log ", shell, debug, scriptPath, current_logdir, scriptName);
SCR::Execute(.target.bash_background, "OPIPE=/tmp/ay_opipe IPIPE=/tmp/ay_ipipe "+execute, $[]);
boolean run = true;
boolean ok_button = false;
term vbox = `VBox();
list<string> buffer = [];
while( SCR::Read(.target.lstat, "/tmp/ay_opipe") != $[] && run ) {
data = (map)SCR::Execute (.target.bash_output, "cat /tmp/ay_opipe", $[]);
buffer = splitstring( data["stdout"]:"", "\n" );
while( buffer != [] ) {
string line = buffer[0]:"";
buffer = remove( buffer, 0 );
if( size(line) == 0 )
continue;
data["stdout"] = line;
y2milestone("working on line %1", line);
if( substring( data["stdout"]:"", 0, 8 ) == "__EXIT__" ) {
if ( widget == "radiobutton" ) {
vbox = add( vbox, `PushButton(`id(`ok), Label::OKButton() ) );
UI::OpenDialog( `RadioButtonGroup(`id(`rb), vbox) );
}
if( ok_button == true ) {
UI::ChangeWidget(`id(`ok) , `Enabled, true);
any ret = UI::UserInput();
if( widget == "radiobutton" ) {
any val = UI::QueryWidget(`id(`rb), `CurrentButton);
SCR::Execute (.target.bash, sformat("echo \"%1\" > /tmp/ay_ipipe", AutoinstConfig::ShellEscape((string)val) ), $[]);
} else if( widget == "entry" ) {
any val = UI::QueryWidget(`id(`ay_entry), `Value);
SCR::Execute (.target.bash, sformat("echo \"%1\" > /tmp/ay_ipipe", AutoinstConfig::ShellEscape((string)(val)) ), $[]);
}
ok_button = false;
}
vbox = `VBox();
if( widget == "" )
run = false;
else
UI::CloseDialog();
widget = "";
} else if( substring( data["stdout"]:"", 0, 12 ) == "__PROGRESS__" ) {
map<string,string> params = splitParams( data["stdout"]:"" );
UI::OpenDialog(
`VBox( `ProgressBar(`id(`pr), params["label"]:"", tointeger(params["max"]:"100"), 0 ) )
);
widget = "progressbar";
} else if( substring( data["stdout"]:"", 0, 8 ) == "__TEXT__" ) {
map<string,string> params = splitParams( data["stdout"]:"" );
integer hspace = tointeger(params["width"]:"10");
integer vspace = tointeger(params["height"]:"20");
ok_button = haskey(params, "okbutton")?true:false;
vbox = `VBox( `HSpacing(hspace), `HBox( `VSpacing(vspace), `RichText( `id( `mle ), "" ) ) );
if( ok_button == true )
vbox = add( vbox, `PushButton(`id(`ok), Label::OKButton() ) );
UI::OpenDialog( vbox );
if( ok_button == true )
UI::ChangeWidget(`id(`ok) , `Enabled, false);
widget = "text";
} else if( substring( data["stdout"]:"", 0, 9 ) == "__ENTRY__" ) {
map<string,string> params = splitParams( data["stdout"]:"" );
if( haskey( params, "description" ) ) {
vbox = add( vbox, `HSpacing(40) );
vbox = add( vbox, `RichText( params["description"]:"" ) );
}
vbox = add( vbox, `TextEntry( `id(`ay_entry), params["label"]:"", params["default"]:"" ) );
vbox = add( vbox, `PushButton(`id(`ok), Label::OKButton() ) );
UI::OpenDialog( vbox );
widget = "entry";
ok_button = true;
} else if( substring( data["stdout"]:"", 0, 15 ) == "__RADIOBUTTON__" ) {
map<string,string> params = splitParams( data["stdout"]:"" );
if( haskey( params, "description" ) ) {
vbox = add( vbox, `HSpacing(60) );
vbox = add( vbox, `RichText( params["description"]:"" ) );
}
widget = "radiobutton";
ok_button = true;
} else {
if( widget == "progressbar" ) {
UI::ChangeWidget(`id(`pr), `Value, tointeger(data["stdout"]:"0") );
} else if( widget == "text" ) {
UI::ChangeWidget(`id(`mle), `Value, (string)UI::QueryWidget(`id(`mle), `Value)+data["stdout"]:"" );
} else if( widget == "radiobutton" ) {
if( substring( data["stdout"]:"", 0, 10 ) == "__BUTTON__" ) {
map<string,string> params = splitParams( data["stdout"]:"" );
vbox = add( vbox, `Left(`RadioButton(`id(params["val"]:""), params["label"]:"")) );
} else {
y2milestone("*urgs* received '%1' instead of '__BUTTON__' during RADIOBUTTON creation",data["stdout"]:"");
}
}
}
};
};
SCR::Execute (.target.remove, "/tmp/ay_opipe");
SCR::Execute (.target.remove, "/tmp/ay_ipipe");
}
/**
* Execute pre scripts
* @param string type of script
* @param boolean if script should be executed in chroot env.
* @return boolean true on success
*/
global define boolean Write( string type , boolean special)
{
if (!Mode::autoinst ())
return true;
list<map> scripts = [];
if (type == "pre-scripts")
{
scripts = pre;
}
else if (type == "init-scripts")
{
scripts = init;
}
else if ( type == "chroot-scripts" && !special)
{
scripts = filter(map s, chroot, ``(!s["chrooted"]:false));
}
else if ( type == "chroot-scripts" && special)
{
scripts = filter(map s, chroot, ``(s["chrooted"]:false));
}
else if (type == "post-scripts" && !special)
{
scripts = filter(map s, post, ``(!s["network_needed"]:false));
}
else if (type == "post-scripts" && special)
{
scripts = filter(map s, post, ``(s["network_needed"]:false));
}
else if (type == "postpartitioning-scripts" )
{
scripts = postpart;
}
else
{
y2error("Unsupported script type");
return false;
}
string tmpdirString = "";
string current_logdir = "";
if (type == "pre-scripts" || type == "postpartitioning-scripts")
{
tmpdirString = sformat("%1/%2", AutoinstConfig::tmpDir, type);
SCR::Execute (.target.mkdir, tmpdirString);
current_logdir = sformat("%1/logs", tmpdirString);
SCR::Execute (.target.mkdir, current_logdir);
}
else if (type == "chroot-scripts")
{
if( !special ) {
tmpdirString = sformat("%1%2", AutoinstConfig::destdir, AutoinstConfig::scripts_dir);
SCR::Execute (.target.mkdir, tmpdirString);
current_logdir = sformat("%1%2", AutoinstConfig::destdir, AutoinstConfig::logs_dir);
SCR::Execute (.target.mkdir, current_logdir);
} else {
tmpdirString = sformat("%1", AutoinstConfig::scripts_dir);
SCR::Execute (.target.mkdir, tmpdirString);
current_logdir = sformat("%1", AutoinstConfig::logs_dir);
SCR::Execute (.target.mkdir, current_logdir);
}
}
else
{
current_logdir = AutoinstConfig::logs_dir;
}
foreach( map s, scripts,
``{
string scriptInterpreter = s["interpreter"]:"shell";
string scriptName = s["filename"]:"";
if (scriptName=="")
{
map t = URL::Parse(s["location"]:"");
scriptName=basename(t["path"]:"");
if( scriptName == "" )
scriptName = type;
}
string scriptPath = "";
if ( type == "pre-scripts" || type == "postpartitioning-scripts" )
{
// y2milestone("doing /sbin/udevcontrol stop_exec_queue now to prevent trouble if one is doing partitioning in a pre-script");
// SCR::Execute( .target.bash, "/sbin/udevcontrol stop_exec_queue" );
scriptPath = sformat("%1/%2/%3", AutoinstConfig::tmpDir, type, scriptName);
y2milestone("Writing %1 script into %2", type, scriptPath);
if (s["location"]:"" != "")
{
y2debug("getting script: %1", s["location"]:"" );
if (!GetURL(s["location"]:"", scriptPath ) )
{
y2error("script %1 could not be retrieved", s["location"]:"");
}
} else {
SCR::Write(.target.string, scriptPath, s["source"]:"echo Empty script!");
}
}
else if (type == "init-scripts")
{
scriptPath = sformat("%1/%2", AutoinstConfig::initscripts_dir, scriptName);
y2milestone("Writing init script into %1", scriptPath);
if (s["location"]:""!="")
{
y2debug("getting script: %1", s["location"]:"" );
if (!GetURL(s["location"]:"", scriptPath ) )
{
y2error("script %1 could not be retrieved", s["location"]:"");
}
} else {
SCR::Write(.target.string, scriptPath, s["source"]:"echo Empty script!");
}
Service::Enable("autoyast");
}
else if (type == "chroot-scripts")
{
//scriptPath = sformat("%1%2/%3", (special) ? "" : AutoinstConfig::destdir, AutoinstConfig::scripts_dir, scriptName);
map toks = URL::Parse(s["location"]:"");
if( toks["scheme"]:"" == "nfs" || !special ) {
scriptPath = sformat("%1%2/%3",AutoinstConfig::destdir, AutoinstConfig::scripts_dir, scriptName);
} else {
scriptPath = sformat("%1/%2",AutoinstConfig::scripts_dir, scriptName);
}
y2milestone("Writing chroot script into %1", scriptPath);
if (s["location"]:""!="")
{
if (!GetURL(s["location"]:"", scriptPath ) )
{
y2error("script %1 could not be retrieved", s["location"]:"");
}
} else {
SCR::Write(.target.string, scriptPath, s["source"]:"echo Empty script!");
}
if( special && toks["scheme"]:"" == "nfs" ) {
scriptPath = substring (scriptPath, 4); // cut off the /mnt for later execution
}
}
else
{
// disable all sources and finish target - no clue what this is good for.
// triggers an error with post-script network_needed=true
// Pkg::SourceFinishAll();
// Pkg::TargetFinish();
scriptPath = sformat("%1/%2", AutoinstConfig::scripts_dir, scriptName);
y2milestone("Writing script into %1", scriptPath);
if (s["location"]:""!="")
{
if (!GetURL(s["location"]:"", scriptPath ) )
{
y2error("script %1 could not be retrieved", s["location"]:"");
}
} else {
SCR::Write(.target.string, scriptPath, s["source"]:"echo Empty script!");
}
}
if (type != "init-scripts")
{
// string message = sformat(_("Executing user supplied script: %1"), scriptName);
string executionString = "";
boolean showFeedback = s["feedback"]:false;
if( s["notification"]:"" != "" )
Popup::ShowFeedback( "", s["notification"]:"" );
if (scriptInterpreter == "shell")
{
string debug = ( s["debug"]:true ? "-x" : "" );
if( SCR::Read (.target.size, scriptPath+"-run" ) == -1 || s["rerun"]:false == true ) {
if( s["interactive"]:false == true ) {
interactiveScript( "/bin/sh", debug, scriptPath, current_logdir, scriptName );
} else {
executionString = sformat("/bin/sh %1 %2 2&> %3/%4.log ", debug, scriptPath, current_logdir, scriptName);
y2milestone("Script Execution command: %1", executionString );
SCR::Execute (.target.bash, executionString);
SCR::Execute (.target.bash, "/bin/touch $FILE", $["FILE":scriptPath+"-run"]);
}
}
}
else if (scriptInterpreter == "perl")
{
string debug = ( s["debug"]:true ? "-w" : "" );
if( SCR::Read (.target.size, scriptPath+"-run" ) == -1 || s["rerun"]:false == true ) {
if( s["interactive"]:false == true ) {
interactiveScript( "/usr/bin/perl", debug, scriptPath, current_logdir, scriptName );
} else {
executionString = sformat("/usr/bin/perl %1 %2 2&> %3/%4.log ", debug, scriptPath, current_logdir, scriptName);
y2milestone("Script Execution command: %1", executionString );
SCR::Execute (.target.bash, executionString);
SCR::Execute (.target.bash, "/bin/touch $FILE", $["FILE":scriptPath+"-run"]);
}
}
}
else if (scriptInterpreter == "python")
{
if( SCR::Read (.target.size, scriptPath+"-run" ) == -1 || s["rerun"]:false == true ) {
if( s["interactive"]:false == true ) {
interactiveScript( "/usr/bin/python", "", scriptPath, current_logdir, scriptName );
} else {
executionString = sformat("/usr/bin/python %1 2&> %2/%3.log ", scriptPath, current_logdir, scriptName );
y2milestone("Script Execution command: %1", executionString );
SCR::Execute (.target.bash, executionString);
SCR::Execute (.target.bash, "/bin/touch $FILE", $["FILE":scriptPath+"-run"]);
}
}
}
else
{
y2error("Unknown interpreter: %1", scriptInterpreter);
}
string feedback = "";
if( s["notification"]:"" != "" )
Popup::ClearFeedback();
if( executionString != "" ) {
if( showFeedback ) {
feedback = (string)SCR::Read(.target.string, current_logdir+"/"+scriptName+".log" );
}
if( size(feedback) > 0 ) {
if( s["feedback_type"]:"" == "" ) {
Popup::LongText("", `RichText(`opt(`plainText), feedback), 50, 20 );
} else if( s["feedback_type"]:"" == "message" ) {
Report::Message( feedback );
} else if( s["feedback_type"]:"" == "warning" ) {
Report::Warning( feedback );
} else if( s["feedback_type"]:"" == "error" ) {
Report::Error( feedback );
}
}
}
}
});
return true;
}
}
ACC SHELL 2018