trace-chunk: Introduce chunk "path", relayd session "ongoing_rotation", sessiond...
[lttng-tools.git] / src / common / trace-chunk.c
index c549f46b33c855334814355ef8a6e37f6cacadf2..266a02ee68e3933ea2533c70572c714098c4df45 100644 (file)
@@ -55,18 +55,26 @@ enum trace_chunk_mode {
  * since only one thread may access a chunk during its destruction (the last
  * to release its reference to the chunk).
  */
-typedef void (*chunk_close_command)(struct lttng_trace_chunk *trace_chunk);
+typedef int (*chunk_command)(struct lttng_trace_chunk *trace_chunk);
 
 /* Move a completed trace chunk to the 'completed' trace archive folder. */
 static
-void lttng_trace_chunk_move_to_completed(struct lttng_trace_chunk *trace_chunk);
+int lttng_trace_chunk_move_to_completed_post_release(struct lttng_trace_chunk *trace_chunk);
+static
+enum lttng_trace_chunk_status lttng_trace_chunk_rename_path_no_lock(
+               struct lttng_trace_chunk *chunk, const char *path);
 
 struct chunk_credentials {
        bool use_current_user;
        struct lttng_credentials user;
 };
 
-/* NOTE: Make sure to update lttng_trace_chunk_copy if you modify this. */
+/*
+ * NOTE: Make sure to update:
+ * - lttng_trace_chunk_copy(),
+ * - lttng_trace_chunk_registry_element_create_from_chunk()
+ * if you modify this structure.
+ */
 struct lttng_trace_chunk {
        pthread_mutex_t lock;
        struct urcu_ref ref;
@@ -87,6 +95,7 @@ struct lttng_trace_chunk {
        bool in_registry_element;
        bool name_overridden;
        char *name;
+       char *path;
        /* An unset id means the chunk is anonymous. */
        LTTNG_OPTIONAL(uint64_t) id;
        LTTNG_OPTIONAL(time_t) timestamp_creation;
@@ -123,9 +132,9 @@ char *close_command_names[] = {
 };
 
 static const
-chunk_close_command close_command_funcs[] = {
+chunk_command close_command_post_release_funcs[] = {
        [LTTNG_TRACE_CHUNK_COMMAND_TYPE_MOVE_TO_COMPLETED] =
-                       lttng_trace_chunk_move_to_completed,
+                       lttng_trace_chunk_move_to_completed_post_release,
 };
 
 static
@@ -242,6 +251,8 @@ void lttng_trace_chunk_fini(struct lttng_trace_chunk *chunk)
        }
        free(chunk->name);
        chunk->name = NULL;
+       free(chunk->path);
+       chunk->path = NULL;
        lttng_dynamic_pointer_array_reset(&chunk->top_level_directories);
        lttng_dynamic_pointer_array_reset(&chunk->files);
        pthread_mutex_destroy(&chunk->lock);
@@ -271,7 +282,7 @@ struct lttng_trace_chunk *lttng_trace_chunk_create_anonymous(void)
 
 LTTNG_HIDDEN
 struct lttng_trace_chunk *lttng_trace_chunk_create(
-               uint64_t chunk_id, time_t chunk_creation_time)
+               uint64_t chunk_id, time_t chunk_creation_time, const char *path)
 {
        struct lttng_trace_chunk *chunk;
         char chunk_creation_datetime_buf[16] = {};
@@ -309,6 +320,19 @@ struct lttng_trace_chunk *lttng_trace_chunk_create(
                        goto error;
                }
         }
+       if (path) {
+               chunk->path = strdup(path);
+               if (!chunk->path) {
+                       goto error;
+               }
+       } else {
+               if (chunk->name) {
+                       chunk->path = strdup(chunk->name);
+                       if (!chunk->path) {
+                               goto error;
+                       }
+               }
+       }
 
         DBG("Chunk name set to \"%s\"", chunk->name ? : "(none)");
 end:
@@ -352,6 +376,13 @@ struct lttng_trace_chunk *lttng_trace_chunk_copy(
                        goto error_unlock;
                }
        }
+       if (source_chunk->path) {
+               new_chunk->path = strdup(source_chunk->path);
+               if (!new_chunk->path) {
+                       ERR("Failed to copy source trace chunk path in %s()",
+                                       __FUNCTION__);
+               }
+       }
        new_chunk->id = source_chunk->id;
        new_chunk->timestamp_creation = source_chunk->timestamp_creation;
        new_chunk->timestamp_close = source_chunk->timestamp_close;
@@ -520,9 +551,10 @@ enum lttng_trace_chunk_status lttng_trace_chunk_override_name(
                struct lttng_trace_chunk *chunk, const char *name)
 
 {
-       char *new_name;
        enum lttng_trace_chunk_status status = LTTNG_TRACE_CHUNK_STATUS_OK;
+       char *new_name, *new_path;
 
+       DBG("Override trace chunk name from %s to %s", chunk->name, name);
        if (!is_valid_chunk_name(name)) {
                ERR("Attempted to set an invalid name on a trace chunk: name = %s",
                                name ? : "NULL");
@@ -537,6 +569,7 @@ enum lttng_trace_chunk_status lttng_trace_chunk_override_name(
                status = LTTNG_TRACE_CHUNK_STATUS_INVALID_OPERATION;
                goto end_unlock;
        }
+
        new_name = strdup(name);
        if (!new_name) {
                ERR("Failed to allocate new trace chunk name");
@@ -545,13 +578,239 @@ enum lttng_trace_chunk_status lttng_trace_chunk_override_name(
        }
        free(chunk->name);
        chunk->name = new_name;
+
+       new_path = strdup(name);
+       if (!new_path) {
+               ERR("Failed to allocate new trace chunk path");
+               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+               goto end_unlock;
+       }
+       free(chunk->path);
+       chunk->path = new_path;
+
        chunk->name_overridden = true;
-end_unlock:    
+end_unlock:
        pthread_mutex_unlock(&chunk->lock);
 end:
        return status;
 }
 
+static
+enum lttng_trace_chunk_status lttng_trace_chunk_rename_path_no_lock(
+               struct lttng_trace_chunk *chunk, const char *path)
+
+{
+       enum lttng_trace_chunk_status status = LTTNG_TRACE_CHUNK_STATUS_OK;
+       struct lttng_directory_handle *rename_directory = NULL;
+       char *new_path, *old_path;
+       int ret;
+
+       if (chunk->name_overridden) {
+               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+               goto end;
+       }
+
+       old_path = chunk->path;
+       DBG("lttng_trace_chunk_rename_path from %s to %s", old_path, path);
+
+       if ((!old_path && !path) ||
+                       (old_path && path && !strcmp(old_path, path)))  {
+               goto end;
+       }
+       /*
+        * Use chunk name as path if NULL path is specified.
+        */
+       if (!path) {
+               path = chunk->name;
+       }
+
+       /* Renaming from "" to "" is not accepted. */
+       if (path[0] == '\0' && old_path[0] == '\0') {
+               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+               goto end;
+       }
+
+       /*
+        * If a rename is performed on a chunk for which the chunk_directory
+        * is not set (yet), or the session_output_directory is not set
+        * (interacting with a relay daemon), there is no rename to perform.
+        */
+       if (!chunk->chunk_directory ||
+                       !chunk->session_output_directory) {
+               goto skip_move;
+       }
+
+       if (old_path[0] != '\0' && path[0] != '\0') {
+               /* Rename chunk directory. */
+               ret = lttng_directory_handle_rename_as_user(
+                       chunk->session_output_directory,
+                       old_path,
+                       chunk->session_output_directory,
+                       path,
+                       LTTNG_OPTIONAL_GET(chunk->credentials).use_current_user ?
+                               NULL :
+                               &chunk->credentials.value.user);
+               if (ret) {
+                       PERROR("Failed to move trace chunk directory \"%s\" to \"%s\"",
+                                       old_path, path);
+                       status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                       goto end;
+               }
+               rename_directory = lttng_directory_handle_create_from_handle(
+                               path,
+                               chunk->session_output_directory);
+               if (!rename_directory) {
+                       ERR("Failed to get handle to trace chunk rename directory");
+                       status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                       goto end;
+               }
+
+               /* Release old handle. */
+               lttng_directory_handle_put(chunk->chunk_directory);
+               /*
+                * Transfer new handle reference to chunk as the current chunk
+                * handle.
+                */
+               chunk->chunk_directory = rename_directory;
+               rename_directory = NULL;
+       } else if (old_path[0] == '\0') {
+               size_t i, count = lttng_dynamic_pointer_array_get_count(
+                               &chunk->top_level_directories);
+
+               ret = lttng_directory_handle_create_subdirectory_as_user(
+                               chunk->session_output_directory,
+                               path,
+                               DIR_CREATION_MODE,
+                               LTTNG_OPTIONAL_GET(chunk->credentials).use_current_user ?
+                                       NULL :
+                                       &chunk->credentials.value.user);
+               if (ret) {
+                       PERROR("Failed to create trace chunk rename directory \"%s\"",
+                                       path);
+                       status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                       goto end;
+               }
+
+               rename_directory = lttng_directory_handle_create_from_handle(
+                               path, chunk->session_output_directory);
+               if (!rename_directory) {
+                       ERR("Failed to get handle to trace chunk rename directory");
+                       status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                       goto end;
+               }
+
+               /* Move toplevel directories. */
+               for (i = 0; i < count; i++) {
+                       const char *top_level_name =
+                               lttng_dynamic_pointer_array_get_pointer(
+                                       &chunk->top_level_directories, i);
+
+                       ret = lttng_directory_handle_rename_as_user(
+                                       chunk->chunk_directory,
+                                       top_level_name,
+                                       rename_directory,
+                                       top_level_name,
+                                       LTTNG_OPTIONAL_GET(chunk->credentials).use_current_user ?
+                                               NULL :
+                                               &chunk->credentials.value.user);
+                       if (ret) {
+                               PERROR("Failed to move \"%s\" to trace chunk rename directory",
+                                               top_level_name);
+                               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                               goto end;
+                       }
+               }
+               /* Release old handle. */
+               lttng_directory_handle_put(chunk->chunk_directory);
+               /*
+                * Transfer new handle reference to chunk as the current chunk
+                * handle.
+                */
+               chunk->chunk_directory = rename_directory;
+               rename_directory = NULL;
+       } else {
+               size_t i, count = lttng_dynamic_pointer_array_get_count(
+                               &chunk->top_level_directories);
+               const bool reference_acquired = lttng_directory_handle_get(
+                               chunk->session_output_directory);
+
+               assert(reference_acquired);
+               rename_directory = chunk->session_output_directory;
+
+               /* Move toplevel directories. */
+               for (i = 0; i < count; i++) {
+                       const char *top_level_name =
+                               lttng_dynamic_pointer_array_get_pointer(
+                                       &chunk->top_level_directories, i);
+
+                       ret = lttng_directory_handle_rename_as_user(
+                                       chunk->chunk_directory,
+                                       top_level_name,
+                                       rename_directory,
+                                       top_level_name,
+                                       LTTNG_OPTIONAL_GET(chunk->credentials).use_current_user ?
+                                               NULL :
+                                               &chunk->credentials.value.user);
+                       if (ret) {
+                               PERROR("Failed to move \"%s\" to trace chunk rename directory",
+                                               top_level_name);
+                               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                               goto end;
+                       }
+               }
+               /* Release old handle. */
+               lttng_directory_handle_put(chunk->chunk_directory);
+               /*
+                * Transfer new handle reference to chunk as the current chunk
+                * handle.
+                */
+               chunk->chunk_directory = rename_directory;
+               rename_directory = NULL;
+
+               /* Remove old directory. */
+               status = lttng_directory_handle_remove_subdirectory(
+                               chunk->session_output_directory,
+                               old_path);
+               if (status != LTTNG_TRACE_CHUNK_STATUS_OK) {
+                       ERR("Error removing subdirectory '%s' file when deleting chunk",
+                               old_path);
+                       ret = -1;
+                       goto end;
+               }
+       }
+
+skip_move:
+       if (path) {
+               new_path = strdup(path);
+               if (!new_path) {
+                       ERR("Failed to allocate new trace chunk path");
+                       status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                       goto end;
+               }
+       } else {
+               new_path = NULL;
+       }
+       free(chunk->path);
+       chunk->path = new_path;
+end:
+       lttng_directory_handle_put(rename_directory);
+       return status;
+}
+
+LTTNG_HIDDEN
+enum lttng_trace_chunk_status lttng_trace_chunk_rename_path(
+               struct lttng_trace_chunk *chunk, const char *path)
+
+{
+       enum lttng_trace_chunk_status status;
+
+       pthread_mutex_lock(&chunk->lock);
+       status = lttng_trace_chunk_rename_path_no_lock(chunk, path);
+       pthread_mutex_unlock(&chunk->lock);
+
+       return status;
+}
+
 LTTNG_HIDDEN
 enum lttng_trace_chunk_status lttng_trace_chunk_get_credentials(
                struct lttng_trace_chunk *chunk,
@@ -641,32 +900,39 @@ enum lttng_trace_chunk_status lttng_trace_chunk_set_as_owner(
                status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
                goto end;
        }
-
-       if (chunk->name) {
-               /*
-                * A nameless chunk does not need its own output directory.
-                * The session's output directory will be used.
-                */
+       if (chunk->path[0] != '\0') {
                ret = lttng_directory_handle_create_subdirectory_as_user(
                                session_output_directory,
-                               chunk->name,
+                               chunk->path,
                                DIR_CREATION_MODE,
                                !chunk->credentials.value.use_current_user ?
                                        &chunk->credentials.value.user : NULL);
                if (ret) {
                        PERROR("Failed to create chunk output directory \"%s\"",
-                               chunk->name);
+                               chunk->path);
                        status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
                        goto end;
                }
-       }
-       chunk_directory_handle = lttng_directory_handle_create_from_handle(
-                       chunk->name,
-                       session_output_directory);
-       if (!chunk_directory_handle) {
-               /* The function already logs on all error paths. */
-               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
-               goto end;
+               chunk_directory_handle =
+                               lttng_directory_handle_create_from_handle(
+                                       chunk->path,
+                                       session_output_directory);
+               if (!chunk_directory_handle) {
+                       /* The function already logs on all error paths. */
+                       status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+                       goto end;
+               }
+       } else {
+               /*
+                * A nameless chunk does not need its own output directory.
+                * The session's output directory will be used.
+                */
+               const bool reference_acquired =
+                               lttng_directory_handle_get(
+                                       session_output_directory);
+
+               assert(reference_acquired);
+               chunk_directory_handle = session_output_directory;
        }
        chunk->chunk_directory = chunk_directory_handle;
        chunk_directory_handle = NULL;
@@ -972,7 +1238,7 @@ int lttng_trace_chunk_unlink_file(struct lttng_trace_chunk *chunk,
        if (!chunk->credentials.is_set) {
                /*
                 * Fatal error, credentials must be set before a
-                * directory is created.
+                * file is unlinked.
                 */
                ERR("Credentials of trace chunk are unset: refusing to unlink file \"%s\"",
                                file_path);
@@ -999,12 +1265,50 @@ end:
        return status;
 }
 
-static
-void lttng_trace_chunk_move_to_completed(struct lttng_trace_chunk *trace_chunk)
+LTTNG_HIDDEN
+int lttng_trace_chunk_remove_subdirectory_recursive(struct lttng_trace_chunk *chunk,
+               const char *path)
 {
        int ret;
-       char *directory_to_rename = NULL;
-       bool free_directory_to_rename = false;
+       enum lttng_trace_chunk_status status = LTTNG_TRACE_CHUNK_STATUS_OK;
+
+       DBG("Recursively removing trace chunk directory \"%s\"", path);
+       pthread_mutex_lock(&chunk->lock);
+       if (!chunk->credentials.is_set) {
+               /*
+                * Fatal error, credentials must be set before a
+                * directory is removed.
+                */
+               ERR("Credentials of trace chunk are unset: refusing to recursively remove directory \"%s\"",
+                               path);
+               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+               goto end;
+       }
+       if (!chunk->chunk_directory) {
+               ERR("Attempted to recursively remove trace chunk directory \"%s\" before setting the chunk output directory",
+                               path);
+               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+               goto end;
+       }
+       ret = lttng_directory_handle_remove_subdirectory_recursive_as_user(
+                       chunk->chunk_directory, path,
+                       chunk->credentials.value.use_current_user ?
+                                       NULL : &chunk->credentials.value.user,
+                       LTTNG_DIRECTORY_HANDLE_SKIP_NON_EMPTY_FLAG);
+       if (ret < 0) {
+               status = LTTNG_TRACE_CHUNK_STATUS_ERROR;
+               goto end;
+       }
+end:
+       pthread_mutex_unlock(&chunk->lock);
+       return status;
+}
+
+static
+int lttng_trace_chunk_move_to_completed_post_release(
+               struct lttng_trace_chunk *trace_chunk)
+{
+       int ret = 0;
        char *archived_chunk_name = NULL;
        const uint64_t chunk_id = LTTNG_OPTIONAL_GET(trace_chunk->id);
        const time_t creation_timestamp =
@@ -1012,6 +1316,7 @@ void lttng_trace_chunk_move_to_completed(struct lttng_trace_chunk *trace_chunk)
        const time_t close_timestamp =
                        LTTNG_OPTIONAL_GET(trace_chunk->timestamp_close);
        struct lttng_directory_handle *archived_chunks_directory = NULL;
+       enum lttng_trace_chunk_status status;
 
        if (!trace_chunk->mode.is_set ||
                        trace_chunk->mode.value != TRACE_CHUNK_MODE_OWNER ||
@@ -1025,76 +1330,13 @@ void lttng_trace_chunk_move_to_completed(struct lttng_trace_chunk *trace_chunk)
 
        assert(trace_chunk->mode.value == TRACE_CHUNK_MODE_OWNER);
        assert(!trace_chunk->name_overridden);
-
-       /*
-        * The fist trace chunk of a session is directly output to the
-        * session's output folder. In this case, the top level directories
-        * must be moved to a temporary folder before that temporary directory
-        * is renamed to match the chunk's name.
-        */
-       if (chunk_id == 0) {
-               struct lttng_directory_handle *temporary_rename_directory =
-                               NULL;
-               size_t i, count = lttng_dynamic_pointer_array_get_count(
-                                         &trace_chunk->top_level_directories);
-
-               ret = lttng_directory_handle_create_subdirectory_as_user(
-                               trace_chunk->session_output_directory,
-                               DEFAULT_TEMPORARY_CHUNK_RENAME_DIRECTORY,
-                               DIR_CREATION_MODE,
-                               !trace_chunk->credentials.value.use_current_user ?
-                                       &trace_chunk->credentials.value.user : NULL);
-               if (ret) {
-                       PERROR("Failed to create temporary trace chunk rename directory \"%s\"",
-                                       DEFAULT_TEMPORARY_CHUNK_RENAME_DIRECTORY);
-               }
-
-               temporary_rename_directory = lttng_directory_handle_create_from_handle(
-                               DEFAULT_TEMPORARY_CHUNK_RENAME_DIRECTORY,
-                               trace_chunk->session_output_directory);
-               if (!temporary_rename_directory) {
-                       ERR("Failed to get handle to temporary trace chunk rename directory");
-                       goto end;
-               }
-
-               for (i = 0; i < count; i++) {
-                       const char *top_level_name =
-                                       lttng_dynamic_pointer_array_get_pointer(
-                                               &trace_chunk->top_level_directories, i);
-
-                       ret = lttng_directory_handle_rename_as_user(
-                                       trace_chunk->session_output_directory,
-                                       top_level_name,
-                                       temporary_rename_directory,
-                                       top_level_name,
-                                       LTTNG_OPTIONAL_GET(trace_chunk->credentials).use_current_user ?
-                                               NULL :
-                                               &trace_chunk->credentials.value.user);
-                       if (ret) {
-                               PERROR("Failed to move \"%s\" to temporary trace chunk rename directory",
-                                               top_level_name);
-                               lttng_directory_handle_put(
-                                               temporary_rename_directory);
-                               goto end;
-                       }
-               }
-               lttng_directory_handle_put(temporary_rename_directory);
-               directory_to_rename = DEFAULT_TEMPORARY_CHUNK_RENAME_DIRECTORY;
-               free_directory_to_rename = false;
-       } else {
-               directory_to_rename = generate_chunk_name(chunk_id,
-                               creation_timestamp, NULL);
-               if (!directory_to_rename) {
-                       ERR("Failed to generate initial trace chunk name while renaming trace chunk");
-                       goto end;
-               }
-               free_directory_to_rename = true;
-       }
+       assert(trace_chunk->path);
 
        archived_chunk_name = generate_chunk_name(chunk_id, creation_timestamp,
                        &close_timestamp);
        if (!archived_chunk_name) {
                ERR("Failed to generate archived trace chunk name while renaming trace chunk");
+               ret = -1;
                goto end;
        }
 
@@ -1116,12 +1358,29 @@ void lttng_trace_chunk_move_to_completed(struct lttng_trace_chunk *trace_chunk)
                        trace_chunk->session_output_directory);
        if (!archived_chunks_directory) {
                PERROR("Failed to get handle to archived trace chunks directory");
+               ret = -1;
                goto end;
        }
 
+       /*
+        * Make sure chunk is renamed to old directory if not already done by
+        * the creation of the next chunk. This happens if a rotation is
+        * performed while tracing is stopped.
+        */
+       if (!trace_chunk->path || strcmp(trace_chunk->path,
+                       DEFAULT_CHUNK_TMP_OLD_DIRECTORY)) {
+               status = lttng_trace_chunk_rename_path_no_lock(trace_chunk,
+                               DEFAULT_CHUNK_TMP_OLD_DIRECTORY);
+               if (status != LTTNG_TRACE_CHUNK_STATUS_OK) {
+                       ERR("Failed to rename chunk to %s", DEFAULT_CHUNK_TMP_OLD_DIRECTORY);
+                       ret = -1;
+                       goto end;
+               }
+       }
+
        ret = lttng_directory_handle_rename_as_user(
                        trace_chunk->session_output_directory,
-                       directory_to_rename,
+                       trace_chunk->path,
                        archived_chunks_directory,
                        archived_chunk_name,
                        LTTNG_OPTIONAL_GET(trace_chunk->credentials).use_current_user ?
@@ -1129,15 +1388,14 @@ void lttng_trace_chunk_move_to_completed(struct lttng_trace_chunk *trace_chunk)
                                &trace_chunk->credentials.value.user);
        if (ret) {
                PERROR("Failed to rename folder \"%s\" to \"%s\"",
-                               directory_to_rename, archived_chunk_name);
+                               trace_chunk->path,
+                               archived_chunk_name);
        }
 
 end:
        lttng_directory_handle_put(archived_chunks_directory);
        free(archived_chunk_name);
-       if (free_directory_to_rename) {
-               free(directory_to_rename);
-       }
+       return ret;
 }
 
 LTTNG_HIDDEN
@@ -1233,7 +1491,11 @@ void lttng_trace_chunk_release(struct urcu_ref *ref)
                        ref);
 
        if (chunk->close_command.is_set) {
-               close_command_funcs[chunk->close_command.value](chunk);
+               if (close_command_post_release_funcs[
+                               chunk->close_command.value](chunk)) {
+                       ERR("Trace chunk post-release command %s has failed.",
+                                       close_command_names[chunk->close_command.value]);
+               }
        }
 
        if (chunk->in_registry_element) {
@@ -1333,10 +1595,11 @@ lttng_trace_chunk_registry_element_create_from_chunk(
                chunk->chunk_directory = NULL;
        }
        /*
-        * The original chunk becomes invalid; the name attribute is transferred
-        * to the new chunk instance.
+        * The original chunk becomes invalid; the name and path attributes are
+        * transferred to the new chunk instance.
         */
        chunk->name = NULL;
+       chunk->path = NULL;
        element->chunk.in_registry_element = true;
 end:
        return element;
This page took 0.030453 seconds and 4 git commands to generate.