Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
K
KWSys
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Deploy
Releases
Container Registry
Model registry
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
Repository analytics
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Sylvain Joubert
KWSys
Commits
7d56ef24
Commit
7d56ef24
authored
7 years ago
by
Adam Weisi
Committed by
Brad King
7 years ago
Browse files
Options
Downloads
Patches
Plain Diff
Process: Save results from all children internally
parent
8a799e36
No related branches found
Branches containing commit
No related tags found
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
ProcessUNIX.c
+83
-48
83 additions, 48 deletions
ProcessUNIX.c
ProcessWin32.c
+78
-41
78 additions, 41 deletions
ProcessWin32.c
with
161 additions
and
89 deletions
ProcessUNIX.c
+
83
−
48
View file @
7d56ef24
...
...
@@ -165,7 +165,8 @@ static kwsysProcessTime kwsysProcessTimeAdd(kwsysProcessTime in1,
kwsysProcessTime
in2
);
static
kwsysProcessTime
kwsysProcessTimeSubtract
(
kwsysProcessTime
in1
,
kwsysProcessTime
in2
);
static
void
kwsysProcessSetExitException
(
kwsysProcess
*
cp
,
int
sig
);
static
void
kwsysProcessSetExitExceptionByIndex
(
kwsysProcess
*
cp
,
int
sig
,
int
idx
);
static
void
kwsysProcessChildErrorExit
(
int
errorPipe
);
static
void
kwsysProcessRestoreDefaultSignalHandlers
(
void
);
static
pid_t
kwsysProcessFork
(
kwsysProcess
*
cp
,
...
...
@@ -183,6 +184,26 @@ static void kwsysProcessesSignalHandler(int signum, siginfo_t* info,
static
void
kwsysProcessesSignalHandler
(
int
signum
);
#endif
/* A structure containing results data for each process. */
typedef
struct
kwsysProcessResults_s
kwsysProcessResults
;
struct
kwsysProcessResults_s
{
/* The status of the child process. */
int
State
;
/* The exceptional behavior that terminated the process, if any. */
int
ExitException
;
/* The process exit code. */
int
ExitCode
;
/* The process return code, if any. */
int
ExitValue
;
/* Description for the ExitException. */
char
ExitExceptionString
[
KWSYSPE_PIPE_BUFFER_SIZE
+
1
];
};
/* Structure containing data used to implement the child's execution. */
struct
kwsysProcess_s
{
...
...
@@ -253,28 +274,18 @@ struct kwsysProcess_s
/* The number of children still executing. */
int
CommandsLeft
;
/* The
current
status of the
child
process. Must be atomic because
/* The status of the process
structure
. Must be atomic because
the signal handler checks this to avoid a race. */
volatile
sig_atomic_t
State
;
/* The exceptional behavior that terminated the child process, if
* any. */
int
ExitException
;
/* The exit code of the child process. */
int
ExitCode
;
/* The exit value of the child process, if any. */
int
ExitValue
;
/* Whether the process was killed. */
volatile
sig_atomic_t
Killed
;
/* Buffer for error message in case of failure. */
char
ErrorMessage
[
KWSYSPE_PIPE_BUFFER_SIZE
+
1
];
/*
Description for the ExitException
. */
char
ExitExceptionString
[
KWSYSPE_PIPE_BUFFER_SIZE
+
1
]
;
/*
process results
. */
kwsysProcessResults
*
ProcessResults
;
/* The exit codes of each child process in the pipeline. */
int
*
CommandExitCodes
;
...
...
@@ -350,6 +361,7 @@ void kwsysProcess_Delete(kwsysProcess* cp)
if
(
cp
->
CommandExitCodes
)
{
free
(
cp
->
CommandExitCodes
);
}
free
(
cp
->
ProcessResults
);
free
(
cp
);
}
...
...
@@ -652,17 +664,23 @@ int kwsysProcess_GetState(kwsysProcess* cp)
int
kwsysProcess_GetExitException
(
kwsysProcess
*
cp
)
{
return
cp
?
cp
->
ExitException
:
kwsysProcess_Exception_Other
;
return
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
?
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitException
:
kwsysProcess_Exception_Other
;
}
int
kwsysProcess_GetExitCode
(
kwsysProcess
*
cp
)
{
return
cp
?
cp
->
ExitCode
:
0
;
return
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
?
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitCode
:
0
;
}
int
kwsysProcess_GetExitValue
(
kwsysProcess
*
cp
)
{
return
cp
?
cp
->
ExitValue
:
-
1
;
return
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
?
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitValue
:
-
1
;
}
const
char
*
kwsysProcess_GetErrorString
(
kwsysProcess
*
cp
)
...
...
@@ -677,10 +695,10 @@ const char* kwsysProcess_GetErrorString(kwsysProcess* cp)
const
char
*
kwsysProcess_GetExceptionString
(
kwsysProcess
*
cp
)
{
if
(
!
cp
)
{
if
(
!
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
)
{
return
"GetExceptionString called with NULL process management structure"
;
}
else
if
(
cp
->
State
==
kwsysProcess_State_Exception
)
{
return
cp
->
ExitExceptionString
;
return
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitExceptionString
;
}
return
"No exception"
;
}
...
...
@@ -1263,7 +1281,6 @@ static int kwsysProcessWaitForPipe(kwsysProcess* cp, char** data, int* length,
int
kwsysProcess_WaitForExit
(
kwsysProcess
*
cp
,
double
*
userTimeout
)
{
int
status
=
0
;
int
prPipe
=
0
;
/* Make sure we are executing a process. */
...
...
@@ -1294,10 +1311,6 @@ int kwsysProcess_WaitForExit(kwsysProcess* cp, double* userTimeout)
cp
->
State
=
kwsysProcess_State_Error
;
return
1
;
}
/* Use the status of the last process in the pipeline. */
status
=
cp
->
CommandExitCodes
[
cp
->
NumberOfCommands
-
1
];
/* Determine the outcome. */
if
(
cp
->
Killed
)
{
/* We killed the child. */
...
...
@@ -1305,23 +1318,31 @@ int kwsysProcess_WaitForExit(kwsysProcess* cp, double* userTimeout)
}
else
if
(
cp
->
TimeoutExpired
)
{
/* The timeout expired. */
cp
->
State
=
kwsysProcess_State_Expired
;
}
else
if
(
WIFEXITED
(
status
))
{
/* The child exited normally. */
cp
->
State
=
kwsysProcess_State_Exited
;
cp
->
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ExitCode
=
status
;
cp
->
ExitValue
=
(
int
)
WEXITSTATUS
(
status
);
}
else
if
(
WIFSIGNALED
(
status
))
{
/* The child received an unhandled signal. */
cp
->
State
=
kwsysProcess_State_Exception
;
cp
->
ExitCode
=
status
;
kwsysProcessSetExitException
(
cp
,
(
int
)
WTERMSIG
(
status
));
}
else
{
/* Error getting the child return code. */
strcpy
(
cp
->
ErrorMessage
,
"Error getting child return code."
);
cp
->
State
=
kwsysProcess_State_Error
;
/* The children exited. Report the outcome of the child processes. */
for
(
prPipe
=
0
;
prPipe
<
cp
->
NumberOfCommands
;
++
prPipe
)
{
cp
->
ProcessResults
[
prPipe
].
ExitCode
=
cp
->
CommandExitCodes
[
prPipe
];
if
(
WIFEXITED
(
cp
->
ProcessResults
[
prPipe
].
ExitCode
))
{
/* The child exited normally. */
cp
->
ProcessResults
[
prPipe
].
State
=
kwsysProcess_State_Exited
;
cp
->
ProcessResults
[
prPipe
].
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ProcessResults
[
prPipe
].
ExitValue
=
(
int
)
WEXITSTATUS
(
cp
->
ProcessResults
[
prPipe
].
ExitCode
);
}
else
if
(
WIFSIGNALED
(
cp
->
ProcessResults
[
prPipe
].
ExitCode
))
{
/* The child received an unhandled signal. */
cp
->
ProcessResults
[
prPipe
].
State
=
kwsysProcess_State_Exception
;
kwsysProcessSetExitExceptionByIndex
(
cp
,
(
int
)
WTERMSIG
(
cp
->
ProcessResults
[
prPipe
].
ExitCode
),
prPipe
);
}
else
{
/* Error getting the child return code. */
strcpy
(
cp
->
ProcessResults
[
prPipe
].
ExitExceptionString
,
"Error getting child return code."
);
cp
->
ProcessResults
[
prPipe
].
State
=
kwsysProcess_State_Error
;
}
}
/* support legacy state status value */
cp
->
State
=
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
State
;
}
/* Normal cleanup. */
kwsysProcessCleanup
(
cp
,
0
);
return
1
;
...
...
@@ -1445,11 +1466,7 @@ static int kwsysProcessInitialize(kwsysProcess* cp)
#endif
cp
->
State
=
kwsysProcess_State_Starting
;
cp
->
Killed
=
0
;
cp
->
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ExitCode
=
1
;
cp
->
ExitValue
=
1
;
cp
->
ErrorMessage
[
0
]
=
0
;
strcpy
(
cp
->
ExitExceptionString
,
"No exception"
);
oldForkPIDs
=
cp
->
ForkPIDs
;
cp
->
ForkPIDs
=
(
volatile
pid_t
*
)
malloc
(
sizeof
(
volatile
pid_t
)
*
...
...
@@ -1475,6 +1492,23 @@ static int kwsysProcessInitialize(kwsysProcess* cp)
memset
(
cp
->
CommandExitCodes
,
0
,
sizeof
(
int
)
*
(
size_t
)(
cp
->
NumberOfCommands
));
/* Allocate process result information for each process. */
free
(
cp
->
ProcessResults
);
cp
->
ProcessResults
=
(
kwsysProcessResults
*
)
malloc
(
sizeof
(
kwsysProcessResults
)
*
(
size_t
)(
cp
->
NumberOfCommands
));
if
(
!
cp
->
ProcessResults
)
{
return
0
;
}
memset
(
cp
->
ProcessResults
,
0
,
sizeof
(
kwsysProcessResults
)
*
(
size_t
)(
cp
->
NumberOfCommands
));
for
(
i
=
0
;
i
<
cp
->
NumberOfCommands
;
i
++
)
{
cp
->
ProcessResults
[
i
].
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ProcessResults
[
i
].
State
=
kwsysProcess_State_Starting
;
cp
->
ProcessResults
[
i
].
ExitCode
=
1
;
cp
->
ProcessResults
[
i
].
ExitValue
=
1
;
strcpy
(
cp
->
ProcessResults
[
i
].
ExitExceptionString
,
"No exception"
);
}
/* Allocate memory to save the real working directory. */
if
(
cp
->
WorkingDirectory
)
{
#if defined(MAXPATHLEN)
...
...
@@ -2008,9 +2042,10 @@ static kwsysProcessTime kwsysProcessTimeSubtract(kwsysProcessTime in1,
}
#define KWSYSPE_CASE(type, str) \
cp->ExitException = kwsysProcess_Exception_##type; \
strcpy(cp->ExitExceptionString, str)
static
void
kwsysProcessSetExitException
(
kwsysProcess
*
cp
,
int
sig
)
cp->ProcessResults[idx].ExitException = kwsysProcess_Exception_##type; \
strcpy(cp->ProcessResults[idx].ExitExceptionString, str)
static
void
kwsysProcessSetExitExceptionByIndex
(
kwsysProcess
*
cp
,
int
sig
,
int
idx
)
{
switch
(
sig
)
{
#ifdef SIGSEGV
...
...
@@ -2196,8 +2231,8 @@ static void kwsysProcessSetExitException(kwsysProcess* cp, int sig)
#endif
#endif
default:
cp
->
ExitException
=
kwsysProcess_Exception_Other
;
sprintf
(
cp
->
ExitExceptionString
,
"Signal %d"
,
sig
);
cp
->
ProcessResults
[
idx
].
ExitException
=
kwsysProcess_Exception_Other
;
sprintf
(
cp
->
ProcessResults
[
idx
].
ExitExceptionString
,
"Signal %d"
,
sig
);
break
;
}
}
...
...
This diff is collapsed.
Click to expand it.
ProcessWin32.c
+
78
−
41
View file @
7d56ef24
...
...
@@ -118,6 +118,8 @@ static kwsysProcessTime kwsysProcessTimeAdd(kwsysProcessTime in1,
static
kwsysProcessTime
kwsysProcessTimeSubtract
(
kwsysProcessTime
in1
,
kwsysProcessTime
in2
);
static
void
kwsysProcessSetExitException
(
kwsysProcess
*
cp
,
int
code
);
static
void
kwsysProcessSetExitExceptionByIndex
(
kwsysProcess
*
cp
,
int
code
,
int
idx
);
static
void
kwsysProcessKillTree
(
int
pid
);
static
void
kwsysProcessDisablePipeThreads
(
kwsysProcess
*
cp
);
static
int
kwsysProcessesInitialize
(
void
);
...
...
@@ -180,6 +182,26 @@ struct kwsysProcessPipeData_s
HANDLE
Write
;
};
/* A structure containing results data for each process. */
typedef
struct
kwsysProcessResults_s
kwsysProcessResults
;
struct
kwsysProcessResults_s
{
/* The status of the process. */
int
State
;
/* The exceptional behavior that terminated the process, if any. */
int
ExitException
;
/* The process exit code. */
DWORD
ExitCode
;
/* The process return code, if any. */
int
ExitValue
;
/* Description for the ExitException. */
char
ExitExceptionString
[
KWSYSPE_PIPE_BUFFER_SIZE
+
1
];
};
/* Structure containing data used to implement the child's execution. */
struct
kwsysProcess_s
{
...
...
@@ -245,15 +267,6 @@ struct kwsysProcess_s
/* ------------- Data managed per call to Execute ------------- */
/* The exceptional behavior that terminated the process, if any. */
int
ExitException
;
/* The process exit code. */
DWORD
ExitCode
;
/* The process return code, if any. */
int
ExitValue
;
/* Index of last pipe to report data, if any. */
int
CurrentIndex
;
...
...
@@ -285,8 +298,8 @@ struct kwsysProcess_s
/* Buffer for error messages. */
char
ErrorMessage
[
KWSYSPE_PIPE_BUFFER_SIZE
+
1
];
/*
Description for the ExitException
. */
char
ExitExceptionString
[
KWSYSPE_PIPE_BUFFER_SIZE
+
1
]
;
/*
process results
. */
kwsysProcessResults
*
ProcessResults
;
/* Windows process information data. */
PROCESS_INFORMATION
*
ProcessInformation
;
...
...
@@ -513,6 +526,7 @@ void kwsysProcess_Delete(kwsysProcess* cp)
if
(
cp
->
CommandExitCodes
)
{
free
(
cp
->
CommandExitCodes
);
}
free
(
cp
->
ProcessResults
);
free
(
cp
);
}
...
...
@@ -839,17 +853,23 @@ int kwsysProcess_GetState(kwsysProcess* cp)
int
kwsysProcess_GetExitException
(
kwsysProcess
*
cp
)
{
return
cp
?
cp
->
ExitException
:
kwsysProcess_Exception_Other
;
return
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
?
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitException
:
kwsysProcess_Exception_Other
;
}
int
kwsysProcess_GetExitValue
(
kwsysProcess
*
cp
)
{
return
cp
?
cp
->
ExitValue
:
-
1
;
return
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
?
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitValue
:
-
1
;
}
int
kwsysProcess_GetExitCode
(
kwsysProcess
*
cp
)
{
return
cp
?
cp
->
ExitCode
:
0
;
return
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
?
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitCode
:
0
;
}
const
char
*
kwsysProcess_GetErrorString
(
kwsysProcess
*
cp
)
...
...
@@ -864,10 +884,10 @@ const char* kwsysProcess_GetErrorString(kwsysProcess* cp)
const
char
*
kwsysProcess_GetExceptionString
(
kwsysProcess
*
cp
)
{
if
(
!
cp
)
{
if
(
!
(
cp
&&
cp
->
ProcessResults
&&
(
cp
->
NumberOfCommands
>
0
))
)
{
return
"GetExceptionString called with NULL process management structure"
;
}
else
if
(
cp
->
State
==
kwsysProcess_State_Exception
)
{
return
cp
->
ExitExceptionString
;
return
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
ExitExceptionString
;
}
return
"No exception"
;
}
...
...
@@ -1312,19 +1332,24 @@ int kwsysProcess_WaitForExit(kwsysProcess* cp, double* userTimeout)
/* The timeout expired. */
cp
->
State
=
kwsysProcess_State_Expired
;
}
else
{
/* The children exited. Report the outcome of the last process. */
cp
->
ExitCode
=
cp
->
CommandExitCodes
[
cp
->
NumberOfCommands
-
1
];
if
((
cp
->
ExitCode
&
0xF0000000
)
==
0xC0000000
)
{
/* Child terminated due to exceptional behavior. */
cp
->
State
=
kwsysProcess_State_Exception
;
cp
->
ExitValue
=
1
;
kwsysProcessSetExitException
(
cp
,
cp
->
ExitCode
);
}
else
{
/* Child exited without exception. */
cp
->
State
=
kwsysProcess_State_Exited
;
cp
->
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ExitValue
=
cp
->
ExitCode
;
/* The children exited. Report the outcome of the child processes. */
for
(
i
=
0
;
i
<
cp
->
NumberOfCommands
;
++
i
)
{
cp
->
ProcessResults
[
i
].
ExitCode
=
cp
->
CommandExitCodes
[
i
];
if
((
cp
->
ProcessResults
[
i
].
ExitCode
&
0xF0000000
)
==
0xC0000000
)
{
/* Child terminated due to exceptional behavior. */
cp
->
ProcessResults
[
i
].
State
=
kwsysProcess_State_Exception
;
cp
->
ProcessResults
[
i
].
ExitValue
=
1
;
kwsysProcessSetExitExceptionByIndex
(
cp
,
cp
->
ProcessResults
[
i
].
ExitCode
,
i
);
}
else
{
/* Child exited without exception. */
cp
->
ProcessResults
[
i
].
State
=
kwsysProcess_State_Exited
;
cp
->
ProcessResults
[
i
].
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ProcessResults
[
i
].
ExitValue
=
cp
->
ProcessResults
[
i
].
ExitCode
;
}
}
/* support legacy state status value */
cp
->
State
=
cp
->
ProcessResults
[
cp
->
NumberOfCommands
-
1
].
State
;
}
return
1
;
...
...
@@ -1507,19 +1532,31 @@ void kwsysProcessPipeThreadWakePipe(kwsysProcess* cp, kwsysProcessPipeData* td)
/* Initialize a process control structure for kwsysProcess_Execute. */
int
kwsysProcessInitialize
(
kwsysProcess
*
cp
)
{
int
i
;
/* Reset internal status flags. */
cp
->
TimeoutExpired
=
0
;
cp
->
Terminated
=
0
;
cp
->
Killed
=
0
;
cp
->
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ExitCode
=
1
;
cp
->
ExitValue
=
1
;
/* Reset error data. */
cp
->
ErrorMessage
[
0
]
=
0
;
strcpy
(
cp
->
ExitExceptionString
,
"No exception"
);
free
(
cp
->
ProcessResults
);
/* Allocate process result information for each process. */
cp
->
ProcessResults
=
(
kwsysProcessResults
*
)
malloc
(
sizeof
(
kwsysProcessResults
)
*
(
cp
->
NumberOfCommands
));
if
(
!
cp
->
ProcessResults
)
{
return
0
;
}
ZeroMemory
(
cp
->
ProcessResults
,
sizeof
(
kwsysProcessResults
)
*
cp
->
NumberOfCommands
);
for
(
i
=
0
;
i
<
cp
->
NumberOfCommands
;
i
++
)
{
cp
->
ProcessResults
[
i
].
ExitException
=
kwsysProcess_Exception_None
;
cp
->
ProcessResults
[
i
].
State
=
kwsysProcess_State_Starting
;
cp
->
ProcessResults
[
i
].
ExitCode
=
1
;
cp
->
ProcessResults
[
i
].
ExitValue
=
1
;
strcpy
(
cp
->
ProcessResults
[
i
].
ExitExceptionString
,
"No exception"
);
}
/* Allocate process information for each process. */
free
(
cp
->
ProcessInformation
);
cp
->
ProcessInformation
=
(
PROCESS_INFORMATION
*
)
malloc
(
sizeof
(
PROCESS_INFORMATION
)
*
cp
->
NumberOfCommands
);
if
(
!
cp
->
ProcessInformation
)
{
...
...
@@ -1559,7 +1596,6 @@ int kwsysProcessInitialize(kwsysProcess* cp)
}
}
{
int
i
;
for
(
i
=
0
;
i
<
3
;
++
i
)
{
cp
->
PipeChildStd
[
i
]
=
INVALID_HANDLE_VALUE
;
}
...
...
@@ -1981,9 +2017,10 @@ kwsysProcessTime kwsysProcessTimeSubtract(kwsysProcessTime in1,
}
#define KWSYSPE_CASE(type, str) \
cp->ExitException = kwsysProcess_Exception_##type; \
strcpy(cp->ExitExceptionString, str)
static
void
kwsysProcessSetExitException
(
kwsysProcess
*
cp
,
int
code
)
cp->ProcessResults[idx].ExitException = kwsysProcess_Exception_##type; \
strcpy(cp->ProcessResults[idx].ExitExceptionString, str)
static
void
kwsysProcessSetExitExceptionByIndex
(
kwsysProcess
*
cp
,
int
code
,
int
idx
)
{
switch
(
code
)
{
case
STATUS_CONTROL_C_EXIT
:
...
...
@@ -2062,9 +2099,9 @@ static void kwsysProcessSetExitException(kwsysProcess* cp, int code)
case
STATUS_NO_MEMORY
:
default:
cp
->
ExitException
=
kwsysProcess_Exception_Other
;
_snprintf
(
cp
->
ExitExceptionString
,
KWSYSPE_PIPE_BUFFER_SIZE
,
"Exit code 0x%x
\n
"
,
code
);
cp
->
ProcessResults
[
idx
].
ExitException
=
kwsysProcess_Exception_Other
;
_snprintf
(
cp
->
ProcessResults
[
idx
].
ExitExceptionString
,
KWSYSPE_PIPE_BUFFER_SIZE
,
"Exit code 0x%x
\n
"
,
code
);
break
;
}
}
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment