#444: Fixed basic pieces for user input trigger to run open source standalone

This commit is contained in:
frikky
2021-12-11 02:43:31 +01:00
parent 5532296f74
commit 001e1df08e
8 changed files with 271 additions and 50 deletions
+13 -8
View File
@@ -65,17 +65,19 @@ class AppBase:
# Checks output for whether it should be automatically parsed or not
def run_magic_parser(self, input_data):
if not isinstance(input_data, str):
self.logger.info("[DEBUG] Not string. Returning from magic")
return input_data
# Don't touch existing JSON/lists
if (input_data.startswith("[") and input_data.endswith("]")) or (input_data.startswith("{") and input_data.endswith("}")):
self.logger.info("[DEBUG] Already JSON-like. Returning from magic")
return input_data
# Don't touch large data.
if len(input_data) > 100000:
self.logger.info("[DEBUG] Value too large. Returning from magic")
return input_data
new_input = input_data
try:
#new_input.strip()
@@ -91,6 +93,7 @@ class AppBase:
splititem = ","
else:
new_return.append(item)
index += 1
continue
@@ -102,6 +105,7 @@ class AppBase:
# Prevent large data or infinite loops
if index > 10000:
self.logger.info(f"[DEBUG] Infinite loop. Returning default data.")
return input_data
fixed_return = []
@@ -120,13 +124,13 @@ class AppBase:
new_input = fixed_return
except Exception as e:
self.logger.info(f"[DEBUG] Failed to run magic parser (2): {e}")
self.logger.info(f"[ERROR] Failed to run magic parser (2): {e}")
return input_data
try:
new_input = input_data.split()
except Exception as e:
self.logger.info(f"[DEBUG] Failed to run magic parser (1): {e}")
self.logger.info(f"[ERROR] Failed to run magic parser (1): {e}")
return input_data
# Won't ever touch this one?
@@ -134,7 +138,7 @@ class AppBase:
try:
return json.dumps(new_input)
except Exception as e:
self.logger.info(f"[DEBUG] Failed to run magic parser: {e}")
self.logger.info(f"[ERROR] Failed to run magic parser: {e}")
return new_input
@@ -145,13 +149,14 @@ class AppBase:
action_result["status"] = "FAILURE"
try:
if self.original_action["run_magic_output"] == True:
self.logger.warning("[INFO] Action result ran with Magic parser output.")
#self.logger.info(f"[DEBUG] ACTION: {self.action}")
if self.action["run_magic_output"] == True:
self.logger.warning(f"[INFO] Action result ran with Magic parser output.")
action_result["result"] = self.run_magic_parser(action_result["result"])
else:
self.logger.warning("[ERROR] Magic output not defined.")
self.logger.warning(f"[ERROR] Magic output not defined.")
except Exception as e:
self.logger.warning("[ERROR] Failed to run magic autoparser: {e}")
self.logger.warning(f"[ERROR] Failed to run magic autoparser: {e}")
pass
# Try it with some magic
+42 -17
View File
@@ -366,7 +366,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
ctx := context.Background()
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId)
if err != nil {
log.Printf("Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err)
log.Printf("[WARNING] Failed getting execution (streamresult) %s: %s", actionResult.ExecutionId, err)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`)))
return
@@ -494,7 +494,9 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
err := handleUserInput(trigger, orgId, workflowExecution.Workflow.ID, workflowExecution.ExecutionId)
if err != nil {
log.Printf("[WARNING] Failed userinput handler: %s", err)
actionResult.Result = fmt.Sprintf("Cloud error: %s", err)
actionResult.Result = fmt.Sprintf(`{"success": False, "reason": "%s"}`, err)
workflowExecution.Results = append(workflowExecution.Results, actionResult)
workflowExecution.Status = "ABORTED"
err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true)
@@ -506,12 +508,13 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %s"}`, err)))
return
} else {
log.Printf("[INFO] Successful userinput handler")
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "CLOUD IS DONE"}`)))
actionResult.Result = "Waiting for user feedback based on configuration"
actionResult.Result = `{"success": True, "reason": "Waiting for user feedback based on configuration"}`
workflowExecution.Results = append(workflowExecution.Results, actionResult)
workflowExecution.Status = actionResult.Status
@@ -519,7 +522,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
if err != nil {
log.Printf("[WARNING] Failed setting userinput: %s", err)
} else {
log.Printf("Successfully set the execution to waiting.")
log.Printf("[DEBUG] Successfully set the execution to waiting.")
}
}
@@ -1035,13 +1038,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
return
}
user, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("[INFO] Api authentication failed in execute workflow: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
user, userErr := shuffle.HandleApiAuthentication(resp, request)
if user.Role == "org-reader" {
log.Printf("[WARNING] Org-reader doesn't have access to run workflow: %s (%s)", user.Username, user.Id)
@@ -1079,14 +1076,38 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
return
}
if user.Id != workflow.Owner && user.Role != "scheduler" && user.Role != fmt.Sprintf("workflow_%s", fileId) {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
log.Printf("[AUDIT] Letting user %s execute %s because they're admin of the same org", user.Username, workflow.ID)
} else {
log.Printf("[AUDIT] Wrong user (%s) for workflow %s (execute)", user.Username, workflow.ID)
executionAuthValid := false
newOrgId := ""
if userErr != nil {
// Check if the execution data has correct info in it! Happens based on subflows.
// 1. Parent workflow contains this workflow ID in the source trigger?
// 2. Parent workflow's owner is same org?
// 3. Parent execution auth is correct
executionAuthValid, newOrgId = shuffle.RunExecuteAccessValidation(request, workflow)
if !executionAuthValid {
log.Printf("[INFO] Api authentication failed in execute workflow: %s", userErr)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
} else {
log.Printf("[DEBUG] Execution of %s successfully validated and started based on subflow or user input execution", workflow.ID)
user.ActiveOrg = shuffle.OrgMini{
Id: newOrgId,
}
}
}
if !executionAuthValid {
if user.Id != workflow.Owner && user.Role != "scheduler" && user.Role != fmt.Sprintf("workflow_%s", fileId) {
if workflow.OrgId == user.ActiveOrg.Id && user.Role == "admin" {
log.Printf("[AUDIT] Letting user %s execute %s because they're admin of the same org", user.Username, workflow.ID)
} else {
log.Printf("[AUDIT] Wrong user (%s) for workflow %s (execute)", user.Username, workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
}
}
@@ -2419,6 +2440,7 @@ func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId
// E.g. check email
sms := ""
email := ""
subflow := ""
triggerType := ""
triggerInformation := ""
for _, item := range trigger.Parameters {
@@ -2430,11 +2452,14 @@ func handleUserInput(trigger shuffle.Trigger, organizationId string, workflowId
email = item.Value
} else if item.Name == "sms" {
sms = item.Value
} else if item.Name == "subflow" {
subflow = item.Value
}
}
_ = subflow
if len(triggerType) == 0 {
log.Printf("No type specified for user input node")
log.Printf("[WARNING] No type specified for user input node")
return errors.New("No type specified for user input node")
}
+61 -2
View File
@@ -8705,6 +8705,11 @@ const AngularWorkflow = (props) => {
name: "sms",
value: "0000000",
};
workflow.triggers[selectedTriggerIndex].parameters[5] = {
name: "subflow",
value: "",
};
setWorkflow(workflow);
}
@@ -8807,7 +8812,7 @@ const AngularWorkflow = (props) => {
}}
/>
<div style={{ flex: "10" }}>
<b>Information: </b>
<b>Information</b>
</div>
</div>
<TextField
@@ -8853,13 +8858,33 @@ const AngularWorkflow = (props) => {
}}
/>
<div style={{ flex: "10" }}>
<b>Contact options: </b>
<b>Contact options</b>
</div>
</div>
<FormGroup
style={{ paddingLeft: 10, backgroundColor: inputColor }}
row
>
<FormControlLabel
control={
<Checkbox
checked={
workflow.triggers[selectedTriggerIndex].parameters[2] !==
undefined &&
workflow.triggers[
selectedTriggerIndex
].parameters[2].value.includes("subflow")
}
onChange={() => {
setTriggerOptionsWrapper("subflow");
}}
color="primary"
value="subflow"
disabled
/>
}
label={<div style={{ color: "white" }}>Subflow</div>}
/>
<FormControlLabel
control={
<Checkbox
@@ -8916,6 +8941,7 @@ const AngularWorkflow = (props) => {
}}
fullWidth
color="primary"
required
placeholder={"mail1@company.com,mail2@company.com"}
defaultValue={
workflow.triggers[selectedTriggerIndex].parameters[3].value
@@ -8960,6 +8986,39 @@ const AngularWorkflow = (props) => {
setUpdate(Math.random());
}}
/>
) : null}
{workflow.triggers[selectedTriggerIndex].parameters[2] !==
undefined &&
workflow.triggers[
selectedTriggerIndex
].parameters[2].value.includes("subflow") ? (
<TextField
style={{
backgroundColor: inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
color: "white",
marginLeft: "5px",
maxWidth: "95%",
height: 50,
fontSize: "1em",
},
}}
fullWidth
color="primary"
placeholder={"ID of another workflow"}
defaultValue={
workflow.triggers[selectedTriggerIndex].parameters[5].value
}
onBlur={(event) => {
workflow.triggers[selectedTriggerIndex].parameters[5].value =
event.target.value;
setWorkflow(workflow);
setUpdate(Math.random());
}}
/>
) : null}
</div>
</div>
+3 -3
View File
@@ -400,7 +400,7 @@ export const validateJson = (showResult) => {
try {
result = jsonvalid ? JSON.parse(showResult) : showResult;
} catch (e) {
//console.log("Failed parsing JSON even though its valid: ", e)
////console.log("Failed parsing JSON even though its valid: ", e)
jsonvalid = false;
}
@@ -413,12 +413,12 @@ export const validateJson = (showResult) => {
try {
var newstr = showResult.replaceAll("'", '"')
console.log("Try replacements and trimming with new value: ", newstr)
//console.log("Try replacements and trimming with new value: ", newstr)
result = JSON.parse(newstr)
jsonvalid = true
} catch (e) {
console.log("Failed parsing JSON even though its valid (2): ", e)
//console.log("Failed parsing JSON even though its valid (2): ", e)
jsonvalid = false
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
NAME=shuffle-worker
VERSION=0.9.36
VERSION=0.9.40
echo "Running docker build with $NAME:$VERSION"
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
+1 -1
View File
@@ -10,6 +10,6 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/gorilla/mux v1.8.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/shuffle/shuffle-shared v0.1.35
github.com/shuffle/shuffle-shared v0.1.55
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
)
+4
View File
@@ -574,6 +574,10 @@ github.com/shuffle/shuffle-shared v0.1.33 h1:1U0yKWNfW7K7EKOj2aqSmd20UIA+nJeIurG
github.com/shuffle/shuffle-shared v0.1.33/go.mod h1:0QrK51T12CpCj/be8hXduj/RtDnoeaZ3rfogELZE2IU=
github.com/shuffle/shuffle-shared v0.1.35 h1:CoCur/G+TaM2xiLgDCVdVxPhFffNK/4YRWTtzRBprvg=
github.com/shuffle/shuffle-shared v0.1.35/go.mod h1:2ndjLm4ZOvY6arGFwOgGnkQ457Ke7gka9HDF/EkdIxQ=
github.com/shuffle/shuffle-shared v0.1.54 h1:dHpwot+5RPX8k9EC/8Yd+QYsFqCsqsv+J1wC+EtGxzI=
github.com/shuffle/shuffle-shared v0.1.54/go.mod h1:2ndjLm4ZOvY6arGFwOgGnkQ457Ke7gka9HDF/EkdIxQ=
github.com/shuffle/shuffle-shared v0.1.55 h1:feHtTN7Uhr1aMxkMIo3xZbr97599VB2eLekogTj/9Z4=
github.com/shuffle/shuffle-shared v0.1.55/go.mod h1:2ndjLm4ZOvY6arGFwOgGnkQ457Ke7gka9HDF/EkdIxQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+146 -18
View File
@@ -82,6 +82,12 @@ var executedIds = []string{}
var portMappings map[string]int
var baseport = 33333
type UserInputSubflow struct {
Argument string `json:"execution_argument"`
ContinueUrl string `json:"continue_url"`
CancelUrl string `json:"cancel_url"`
}
// removes every container except itself (worker)
func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason string, handleResultSend bool) {
log.Printf("[INFO] Shutdown (%s) started with reason %#v. Result amount: %d. ResultsSent: %d, Send result: %#v", workflowExecution.Status, reason, len(workflowExecution.Results), requestsSent, handleResultSend)
@@ -642,6 +648,11 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
ctx := context.Background()
startAction, extra, children, parents, visited, executed, nextActions, environments := shuffle.GetExecutionVariables(ctx, workflowExecution.ExecutionId)
log.Printf("[DEBUG] Getting info for %s. Extra: %d", workflowExecution.ExecutionId, extra)
dockercli, err := dockerclient.NewEnvClient()
if err != nil {
log.Printf("[ERROR] Unable to create docker client (3): %s", err)
return
}
log.Printf("[INFO] Inside execution results with %d / %d results", len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra)
@@ -865,6 +876,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
}
// FIXME: Add startnode from frontend
action.Label = trigger.Label
action.Parameters = []shuffle.WorkflowAppActionParameter{}
for _, parameter := range trigger.Parameters {
parameter.Variant = "STATIC_VALUE"
@@ -900,15 +912,15 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
//}
//continue
} else if action.AppName == "User Input" {
log.Printf("USER INPUT!")
log.Printf("[DEBUG] RUNNING USER INPUT!")
if action.ID == workflowExecution.Start {
log.Printf("Skipping because it's the startnode")
log.Printf("[DEBUG] Skipping user input because it's the startnode")
visited = append(visited, action.ID)
executed = append(executed, action.ID)
continue
} else {
log.Printf("Should stop after this iteration because it's user-input based. %#v", action)
log.Printf("[DEBUG] Should stop after this iteration because it's user-input based. %#v", action)
trigger := shuffle.Trigger{}
for _, innertrigger := range workflowExecution.Workflow.Triggers {
if innertrigger.ID == action.ID {
@@ -917,14 +929,23 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
}
}
action.Label = action.Label
action.Parameters = []shuffle.WorkflowAppActionParameter{}
for _, parameter := range trigger.Parameters {
action.Parameters = append(action.Parameters, shuffle.WorkflowAppActionParameter{
Name: parameter.Name,
Value: parameter.Value,
})
}
trigger.LargeImage = ""
triggerData, err := json.Marshal(trigger)
if err != nil {
log.Printf("Failed unmarshalling action: %s", err)
log.Printf("[WARNING] Failed unmarshalling action: %s", err)
triggerData = []byte("Failed unmarshalling. Cancel execution!")
}
err = runUserInput(topClient, action, workflowExecution.Workflow.ID, workflowExecution.ExecutionId, workflowExecution.Authorization, string(triggerData))
err = runUserInput(topClient, action, workflowExecution.Workflow.ID, workflowExecution, workflowExecution.Authorization, string(triggerData), dockercli)
if err != nil {
log.Printf("[ERROR] Failed launching backend magic: %s", err)
os.Exit(3)
@@ -1025,12 +1046,6 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
//executed = append(executed, action.ID)
// FIXME - check whether it's running locally yet too
dockercli, err := dockerclient.NewEnvClient()
if err != nil {
log.Printf("[ERROR] Unable to create docker client (2): %s", err)
//return err
continue
}
stats, err := dockercli.ContainerInspect(context.Background(), identifier)
if err != nil || stats.ContainerJSONBase.State.Status != "running" {
@@ -1328,7 +1343,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
// FIXME - new request here
// FIXME - clean up stopped (remove) containers with this execution id
err := shuffle.UpdateExecutionVariables(ctx, workflowExecution.ExecutionId, startAction, children, parents, visited, executed, nextActions, environments, extra)
err = shuffle.UpdateExecutionVariables(ctx, workflowExecution.ExecutionId, startAction, children, parents, visited, executed, nextActions, environments, extra)
if err != nil {
log.Printf("\n\n[ERROR] Failed to update exec variables for execution %s: %s (2)\n\n", workflowExecution.ExecutionId, err)
}
@@ -1661,31 +1676,32 @@ func runSkipAction(client *http.Client, action shuffle.Action, workflowId, workf
)
if err != nil {
log.Printf("[WARNING] Error building test request (0): %s", err)
log.Printf("[WARNING] Error building skip request (0): %s", err)
return err
}
newresp, err := client.Do(req)
if err != nil {
log.Printf("[WARNING] Error running test request (0): %s", err)
log.Printf("[WARNING] Error running skip request (0): %s", err)
return err
}
body, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("[WARNING] Failed reading body when waiting (0): %s", err)
log.Printf("[WARNING] Failed reading body when skipping (0): %s", err)
return err
}
log.Printf("[INFO] User Input Body: %s", string(body))
log.Printf("[INFO] Skip Action Body: %s", string(body))
return nil
}
func runUserInput(client *http.Client, action shuffle.Action, workflowId, workflowExecutionId, authorization string, configuration string) error {
// Sends request back to backend to handle the node
func runUserInput(client *http.Client, action shuffle.Action, workflowId string, workflowExecution shuffle.WorkflowExecution, authorization string, configuration string, dockercli *dockerclient.Client) error {
timeNow := time.Now().Unix()
result := shuffle.ActionResult{
Action: action,
ExecutionId: workflowExecutionId,
ExecutionId: workflowExecution.ExecutionId,
Authorization: authorization,
Result: configuration,
StartedAt: timeNow,
@@ -1693,6 +1709,118 @@ func runUserInput(client *http.Client, action shuffle.Action, workflowId, workfl
Status: "WAITING",
}
// Checking for userinput to deploy subflow for it
subflow := false
subflowId := ""
argument := ""
continueUrl := "testing continue"
cancelUrl := "testing cancel"
for _, item := range action.Parameters {
if item.Name == "subflow" {
subflow = true
subflowId = item.Value
} else if item.Name == "alertinfo" {
argument = item.Value
}
}
if subflow {
log.Printf("[DEBUG] Should run action with subflow app with argument %#v", argument)
newAction := shuffle.Action{
AppName: "shuffle-subflow",
Name: "run_subflow",
AppVersion: "1.0.0",
Label: "User Input Subflow Execution",
}
identifier := fmt.Sprintf("%s_%s_%s_%s", newAction.AppName, newAction.AppVersion, action.ID, workflowExecution.ExecutionId)
if strings.Contains(identifier, " ") {
identifier = strings.ReplaceAll(identifier, " ", "-")
}
inputValue := UserInputSubflow{
Argument: argument,
ContinueUrl: continueUrl,
CancelUrl: cancelUrl,
}
parsedArgument, err := json.Marshal(inputValue)
if err != nil {
log.Printf("[ERROR] Failed to parse arguments: %s", err)
parsedArgument = []byte(argument)
}
newAction.Parameters = []shuffle.WorkflowAppActionParameter{
shuffle.WorkflowAppActionParameter{
Name: "user_apikey",
Value: workflowExecution.Authorization,
},
shuffle.WorkflowAppActionParameter{
Name: "workflow",
Value: subflowId,
},
shuffle.WorkflowAppActionParameter{
Name: "argument",
Value: string(parsedArgument),
},
}
newAction.Parameters = append(newAction.Parameters, shuffle.WorkflowAppActionParameter{
Name: "source_workflow",
Value: workflowExecution.Workflow.ID,
})
newAction.Parameters = append(newAction.Parameters, shuffle.WorkflowAppActionParameter{
Name: "source_execution",
Value: workflowExecution.ExecutionId,
})
newAction.Parameters = append(newAction.Parameters, shuffle.WorkflowAppActionParameter{
Name: "source_node",
Value: action.ID,
})
newAction.Parameters = append(newAction.Parameters, shuffle.WorkflowAppActionParameter{
Name: "source_auth",
Value: workflowExecution.Authorization,
})
newAction.Parameters = append(newAction.Parameters, shuffle.WorkflowAppActionParameter{
Name: "startnode",
Value: "",
})
// If cleanup is set, it should run for efficiency
//appName := strings.Replace(identifier, fmt.Sprintf("_%s", action.ID), "", -1)
//appName = strings.Replace(appName, fmt.Sprintf("_%s", workflowExecution.ExecutionId), "", -1)
actionData, err := json.Marshal(newAction)
if err != nil {
return err
}
env := []string{
fmt.Sprintf("ACTION=%s", string(actionData)),
fmt.Sprintf("EXECUTIONID=%s", workflowExecution.ExecutionId),
fmt.Sprintf("AUTHORIZATION=%s", workflowExecution.Authorization),
fmt.Sprintf("CALLBACK_URL=%s", baseUrl),
fmt.Sprintf("BASE_URL=%s", appCallbackUrl),
fmt.Sprintf("TZ=%s", timezone),
}
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
//log.Printf("APPENDING PROXY TO THE APP!")
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
}
err = deployApp(dockercli, "frikky/shuffle:shuffle-subflow_1.0.0", identifier, env, workflowExecution, newAction)
if err != nil {
log.Printf("[ERROR] Failed to deploy subflow for user input trigger %s: %s", action.ID, err)
}
} else {
log.Printf("[DEBUG] Running user input WITHOUT subflow")
}
resultData, err := json.Marshal(result)
if err != nil {
return err