1265 lines
33 KiB
C
1265 lines
33 KiB
C
/* -*- Mode: C ; indent-tabs-mode: t ; tab-width: 8 ; c-basic-offset: 8 -*- */
|
|
/*
|
|
* LASH
|
|
*
|
|
* Copyright (C) 2008 Juuso Alasuutari <juuso.alasuutari@gmail.com>
|
|
* Copyright (C) 2002 Robert Ham <rah@bash.sh>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#define _GNU_SOURCE
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <sys/types.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <assert.h>
|
|
#include <limits.h>
|
|
#include <stddef.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
#include <uuid/uuid.h>
|
|
#include <dbus/dbus.h>
|
|
#include <jack/jack.h>
|
|
|
|
#include "project.h"
|
|
#include "client.h"
|
|
#include "client_dependency.h"
|
|
#include "store.h"
|
|
#include "file.h"
|
|
#include "jack_patch.h"
|
|
#include "server.h"
|
|
#include "loader.h"
|
|
#include "dbus_iface_control.h"
|
|
#include "../common/safety.h"
|
|
#include "../common/debug.h"
|
|
|
|
#include "jackdbus_mgr.h"
|
|
|
|
static const char *
|
|
project_get_client_config_dir(project_t *project,
|
|
struct lash_client *client);
|
|
|
|
project_t *
|
|
project_new(void)
|
|
{
|
|
project_t *project;
|
|
|
|
project = lash_calloc(1, sizeof(project_t));
|
|
|
|
INIT_LIST_HEAD(&project->clients);
|
|
INIT_LIST_HEAD(&project->lost_clients);
|
|
|
|
return project;
|
|
}
|
|
|
|
static struct lash_client *
|
|
project_get_client_by_name(project_t *project,
|
|
const char *name)
|
|
{
|
|
struct lash_client *client;
|
|
if ((client = client_find_by_name(&project->clients, name))
|
|
|| (client = client_find_by_name(&project->lost_clients, name)))
|
|
return client;
|
|
return NULL;
|
|
}
|
|
|
|
static char *
|
|
project_get_unique_client_name(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
uint8_t i;
|
|
char *str = lash_malloc(1, strlen(client->class) + 4);
|
|
|
|
lash_debug("Creating a unique name for client %s based on its class",
|
|
client->id_str);
|
|
|
|
if (!project_get_client_by_name(project, client->class)) {
|
|
strcpy(str, client->class);
|
|
return str;
|
|
}
|
|
|
|
for (i = 1; i < 100; ++i) {
|
|
sprintf(str, "%s %02u", client->class, i);
|
|
if (!project_get_client_by_name(project, str)) {
|
|
return str;
|
|
}
|
|
}
|
|
|
|
lash_error("Could not create a unique name for client %s. Do you have "
|
|
"100 clients of class %s open?", client->id_str,
|
|
client->class);
|
|
|
|
lash_free(&str);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
struct lash_client *
|
|
project_get_client_by_id(struct list_head *client_list,
|
|
uuid_t id)
|
|
{
|
|
struct list_head *node;
|
|
struct lash_client *client;
|
|
|
|
list_for_each(node, client_list) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
|
|
if (uuid_compare(client->id, id) == 0)
|
|
return client;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Set modified_status to new_status, emit signal if status changed */
|
|
void
|
|
project_set_modified_status(project_t *project,
|
|
bool new_status)
|
|
{
|
|
if (project->modified_status == new_status)
|
|
return;
|
|
|
|
dbus_bool_t value = new_status;
|
|
project->modified_status = new_status;
|
|
|
|
signal_new_valist(g_server->dbus_service,
|
|
"/", "org.nongnu.LASH.Control",
|
|
"ProjectModifiedStatusChanged",
|
|
DBUS_TYPE_STRING, &project->name,
|
|
DBUS_TYPE_BOOLEAN, &value,
|
|
DBUS_TYPE_INVALID);
|
|
}
|
|
|
|
void
|
|
project_name_client(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
char *name;
|
|
|
|
if (!(name = project_get_unique_client_name(project, client))) {
|
|
lash_error("Cannot get unique client name, using empty string");
|
|
name = lash_strdup("");
|
|
}
|
|
|
|
if (client->name)
|
|
free(client->name);
|
|
client->name = name;
|
|
|
|
lash_info("Client %s set its name to '%s'", client->id_str, client->name);
|
|
}
|
|
|
|
void
|
|
project_new_client(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
uuid_generate(client->id);
|
|
uuid_unparse(client->id, client->id_str);
|
|
|
|
/* Set the client's data path */
|
|
lash_strset(&client->data_path,
|
|
project_get_client_dir(project, client));
|
|
|
|
lash_debug("New client now has id %s", client->id_str);
|
|
|
|
if (CLIENT_CONFIG_DATA_SET(client))
|
|
client_store_open(client,
|
|
project_get_client_config_dir(project,
|
|
client));
|
|
|
|
client->project = project;
|
|
list_add(&client->siblings, &project->clients);
|
|
|
|
lash_info("Added client %s of class '%s' to project '%s'",
|
|
client->id_str, client->class, project->name);
|
|
|
|
lash_create_dir(client->data_path);
|
|
|
|
/* Give the client a unique name */
|
|
project_name_client(project, client);
|
|
|
|
project_set_modified_status(project, true);
|
|
|
|
// TODO: Swap 2nd and 3rd parameter of this signal
|
|
lashd_dbus_signal_emit_client_appeared(client->id_str, project->name,
|
|
client->name);
|
|
}
|
|
|
|
void
|
|
project_satisfy_client_dependency(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
struct list_head *node;
|
|
struct lash_client *lost_client;
|
|
|
|
list_for_each(node, &project->lost_clients) {
|
|
lost_client = list_entry(node, struct lash_client, siblings);
|
|
|
|
if (!list_empty(&lost_client->unsatisfied_deps)) {
|
|
client_dependency_remove(&lost_client->unsatisfied_deps,
|
|
client->id);
|
|
|
|
if (list_empty(&lost_client->unsatisfied_deps)) {
|
|
lash_debug("Dependencies for client '%s' "
|
|
"are now satisfied",
|
|
lost_client->name);
|
|
project_launch_client(project, lost_client);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
project_load_file(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
lash_info("Requesting client '%s' to load data from disk",
|
|
client_get_identity(client));
|
|
|
|
client->pending_task = (++g_server->task_iter);
|
|
client->task_type = LASH_Restore_File;
|
|
client->task_progress = 0;
|
|
|
|
method_call_new_valist(g_server->dbus_service, NULL,
|
|
method_default_handler, false,
|
|
client->dbus_name,
|
|
"/org/nongnu/LASH/Client",
|
|
"org.nongnu.LASH.Client",
|
|
"Load",
|
|
DBUS_TYPE_UINT64, &client->pending_task,
|
|
DBUS_TYPE_STRING, &client->data_path,
|
|
DBUS_TYPE_INVALID);
|
|
}
|
|
|
|
/* Send a LoadDataSet method call to the client */
|
|
void
|
|
project_load_data_set(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
if (!client_store_open(client, project_get_client_config_dir(project,
|
|
client))) {
|
|
lash_error("Could not open client's store; "
|
|
"not sending data set");
|
|
return;
|
|
}
|
|
|
|
lash_info("Sending client '%s' its data set",
|
|
client_get_identity(client));
|
|
|
|
if (list_empty(&client->store->keys)) {
|
|
lash_debug("No data found in store");
|
|
return;
|
|
}
|
|
|
|
method_msg_t new_call;
|
|
DBusMessageIter iter, array_iter;
|
|
dbus_uint64_t task_id;
|
|
|
|
if (!method_call_init(&new_call, g_server->dbus_service,
|
|
NULL,
|
|
method_default_handler,
|
|
client->dbus_name,
|
|
"/org/nongnu/LASH/Client",
|
|
"org.nongnu.LASH.Client",
|
|
"LoadDataSet")) {
|
|
lash_error("Failed to initialise LoadDataSet method call");
|
|
return;
|
|
}
|
|
|
|
dbus_message_iter_init_append(new_call.message, &iter);
|
|
|
|
task_id = (++g_server->task_iter);
|
|
|
|
if (!dbus_message_iter_append_basic(&iter, DBUS_TYPE_UINT64, &task_id)) {
|
|
lash_error("Failed to write task ID");
|
|
goto fail;
|
|
}
|
|
|
|
if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &array_iter)) {
|
|
lash_error("Failed to open config array container");
|
|
goto fail;
|
|
}
|
|
|
|
if (!store_create_config_array(client->store, &array_iter)) {
|
|
lash_error("Failed to create config array");
|
|
goto fail;
|
|
}
|
|
|
|
if (!dbus_message_iter_close_container(&iter, &array_iter)) {
|
|
lash_error("Failed to close config array container");
|
|
goto fail;
|
|
}
|
|
|
|
if (!method_send(&new_call, false)) {
|
|
lash_error("Failed to send LoadDataSet method call");
|
|
/* method_send has unref'd the message for us */
|
|
return;
|
|
}
|
|
|
|
client->pending_task = task_id;
|
|
client->task_type = LASH_Restore_Data_Set;
|
|
client->task_progress = 0;
|
|
|
|
return;
|
|
|
|
fail:
|
|
dbus_message_unref(new_call.message);
|
|
}
|
|
|
|
void
|
|
project_launch_client(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
lash_debug("Launching client %s (flags 0x%08X)", client->id_str, client->flags);
|
|
loader_execute(client, client->flags & LASH_Terminal);
|
|
}
|
|
|
|
struct lash_client *
|
|
project_find_lost_client_by_class(project_t *project,
|
|
const char *class)
|
|
{
|
|
struct list_head *node;
|
|
list_for_each (node, &project->lost_clients) {
|
|
struct lash_client *client = list_entry(node, struct lash_client, siblings);
|
|
if (strcmp(client->class, class) == 0)
|
|
return client;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
const char *
|
|
project_get_client_dir(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
get_store_and_return_fqn(lash_get_fqn(project->directory,
|
|
PROJECT_ID_DIR),
|
|
client->id_str);
|
|
}
|
|
|
|
static const char *
|
|
project_get_client_config_dir(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
get_store_and_return_fqn(project_get_client_dir(project, client),
|
|
PROJECT_CONFIG_DIR);
|
|
}
|
|
|
|
// TODO: - Needs to check for errors so that we can
|
|
// report failures back to the control app
|
|
// - Needs to be less fugly
|
|
void
|
|
project_move(project_t *project,
|
|
const char *new_dir)
|
|
{
|
|
struct list_head *node;
|
|
struct lash_client *client;
|
|
DIR *dir = NULL;
|
|
|
|
if (!new_dir || !new_dir[0]
|
|
|| strcmp(new_dir, project->directory) == 0)
|
|
return;
|
|
|
|
/* Check to be sure directory is acceptable
|
|
* FIXME: thorough enough error checking? */
|
|
dir = opendir(new_dir);
|
|
|
|
if (dir || errno == ENOTDIR) {
|
|
lash_error("Cannot move project to %s: Target exists",
|
|
new_dir);
|
|
closedir(dir);
|
|
return;
|
|
} else if (errno == ENOENT) {
|
|
/* This is what we want... */
|
|
/*printf("Directory %s does not exist, creating.\n", new_dir);*/
|
|
}
|
|
|
|
/* close all the clients' stores */
|
|
list_for_each (node, &project->clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
client_store_close(client);
|
|
/* FIXME: check for errors */
|
|
}
|
|
|
|
/* move the directory */
|
|
|
|
if (rename(project->directory, new_dir)) {
|
|
lash_error("Cannot move project to %s: %s",
|
|
new_dir, strerror(errno));
|
|
} else {
|
|
lash_info("Project '%s' moved from '%s' to '%s'",
|
|
project->name, project->directory, new_dir);
|
|
|
|
lash_strset(&project->directory, new_dir);
|
|
lashd_dbus_signal_emit_project_path_changed(project->name, new_dir);
|
|
|
|
/* open all the clients' stores again */
|
|
list_for_each (node, &project->clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
client_store_open(client,
|
|
project_get_client_config_dir(project,
|
|
client));
|
|
/* FIXME: check for errors */
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
/* This is the handler to use when calling a client's Save method.
|
|
At the moment it isn't used but it will be, so don't delete. */
|
|
static void
|
|
project_save_client_handler(DBusPendingCall *pending,
|
|
void *data)
|
|
{
|
|
DBusMessage *msg = dbus_pending_call_steal_reply(pending);
|
|
struct lash_client *client = data;
|
|
|
|
if (msg) {
|
|
const char *err_str;
|
|
|
|
if (!method_return_verify(msg, &err_str)) {
|
|
lash_error("Client save request failed: %s", err_str);
|
|
client->pending_task = 0;
|
|
client->task_type = 0;
|
|
client->task_progress = 0;
|
|
} else {
|
|
lash_debug("Client save request succeeded");
|
|
++client->project->client_tasks_total;
|
|
/* Now we can start waiting for the client to send
|
|
a success or failure report of the save.
|
|
We will not reset pending_task until the report
|
|
arrives. */
|
|
}
|
|
dbus_message_unref(msg);
|
|
} else {
|
|
lash_error("Cannot get method return from pending call");
|
|
client->pending_task = 0;
|
|
client->task_type = 0;
|
|
client->task_progress = 0;
|
|
}
|
|
|
|
dbus_pending_call_unref(pending);
|
|
}
|
|
#endif
|
|
|
|
static __inline__ void
|
|
project_save_clients(project_t *project)
|
|
{
|
|
struct list_head *node;
|
|
struct lash_client *client;
|
|
|
|
list_for_each (node, &project->clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
if (client->pending_task) {
|
|
lash_error("Clients have pending tasks, not sending "
|
|
"save request");
|
|
return;
|
|
}
|
|
}
|
|
|
|
project->task_type = LASH_TASK_SAVE;
|
|
project->client_tasks_total = 0;
|
|
project->client_tasks_progress = 0;
|
|
++g_server->task_iter;
|
|
|
|
lash_debug("Signaling all clients of project '%s' to save (task %llu)",
|
|
project->name, g_server->task_iter);
|
|
signal_new_valist(g_server->dbus_service,
|
|
"/", "org.nongnu.LASH.Server", "Save",
|
|
DBUS_TYPE_STRING, &project->name,
|
|
DBUS_TYPE_UINT64, &g_server->task_iter,
|
|
DBUS_TYPE_INVALID);
|
|
|
|
list_for_each (node, &project->clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
|
|
if (CLIENT_HAS_INTERNAL_STATE(client))
|
|
{
|
|
client->pending_task = g_server->task_iter;
|
|
client->task_type = (CLIENT_CONFIG_FILE(client)) ? LASH_Save_File : LASH_Save_Data_Set;
|
|
client->task_progress = 0;
|
|
++project->client_tasks_total;
|
|
}
|
|
}
|
|
|
|
project->client_tasks_pending = project->client_tasks_total;
|
|
if (project->client_tasks_total == 0)
|
|
{
|
|
project->task_type = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
project_create_client_jack_patch_xml(project_t *project,
|
|
struct lash_client *client,
|
|
xmlNodePtr clientxml)
|
|
{
|
|
xmlNodePtr jack_patch_set;
|
|
struct list_head *node, *next;
|
|
jack_patch_t *patch;
|
|
|
|
LIST_HEAD(patches);
|
|
|
|
if (!lashd_jackdbus_mgr_get_client_patches(g_server->jackdbus_mgr,
|
|
client->id, &patches)) {
|
|
lash_info("client '%s' has no patches to save", client_get_identity(client));
|
|
return;
|
|
}
|
|
|
|
jack_patch_set =
|
|
xmlNewChild(clientxml, NULL, BAD_CAST "jack_patch_set", NULL);
|
|
|
|
list_for_each_safe (node, next, &patches) {
|
|
patch = list_entry(node, jack_patch_t, siblings);
|
|
|
|
lash_info("Saving client '%s' patch %s:%s -> %s:%s", client_get_identity(client), patch->src_client, patch->src_port, patch->dest_client, patch->dest_port);
|
|
jack_patch_create_xml(patch, jack_patch_set);
|
|
|
|
jack_patch_destroy(patch);
|
|
}
|
|
}
|
|
|
|
static void
|
|
project_create_client_dependencies_xml(struct lash_client *client,
|
|
xmlNodePtr parent)
|
|
{
|
|
struct list_head *node;
|
|
client_dependency_t *dep;
|
|
xmlNodePtr deps_xml;
|
|
char id_str[37];
|
|
|
|
deps_xml = xmlNewChild(parent, NULL, BAD_CAST "dependencies", NULL);
|
|
|
|
list_for_each (node, &client->dependencies) {
|
|
dep = list_entry(node, client_dependency_t, siblings);
|
|
uuid_unparse(dep->client_id, id_str);
|
|
xmlNewChild(deps_xml, NULL, BAD_CAST "id",
|
|
BAD_CAST id_str);
|
|
}
|
|
}
|
|
|
|
static xmlDocPtr
|
|
project_create_xml(project_t *project)
|
|
{
|
|
xmlDocPtr doc;
|
|
xmlNodePtr lash_project, clientxml, arg_set;
|
|
struct list_head *node;
|
|
struct lash_client *client;
|
|
char num[16];
|
|
int i;
|
|
|
|
doc = xmlNewDoc(BAD_CAST XML_DEFAULT_VERSION);
|
|
|
|
/* DTD */
|
|
xmlCreateIntSubset(doc, BAD_CAST "lash_project", NULL,
|
|
BAD_CAST "http://www.nongnu.org/lash/lash-project-1.0.dtd");
|
|
|
|
/* Root node */
|
|
lash_project = xmlNewDocNode(doc, NULL, BAD_CAST "lash_project", NULL);
|
|
xmlAddChild((xmlNodePtr) doc, lash_project);
|
|
|
|
xmlNewChild(lash_project, NULL, BAD_CAST "version",
|
|
BAD_CAST PROJECT_XML_VERSION);
|
|
|
|
xmlNewChild(lash_project, NULL, BAD_CAST "name",
|
|
BAD_CAST project->name);
|
|
|
|
xmlNewChild(lash_project, NULL, BAD_CAST "description",
|
|
BAD_CAST project->description);
|
|
|
|
list_for_each (node, &project->clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
|
|
clientxml = xmlNewChild(lash_project, NULL, BAD_CAST "client",
|
|
NULL);
|
|
|
|
xmlNewChild(clientxml, NULL, BAD_CAST "class",
|
|
BAD_CAST client->class);
|
|
|
|
xmlNewChild(clientxml, NULL, BAD_CAST "id",
|
|
BAD_CAST client->id_str);
|
|
|
|
xmlNewChild(clientxml, NULL, BAD_CAST "name",
|
|
BAD_CAST client->name);
|
|
|
|
sprintf(num, "%d", client->flags);
|
|
xmlNewChild(clientxml, NULL, BAD_CAST "flags", BAD_CAST num);
|
|
|
|
xmlNewChild(clientxml, NULL, BAD_CAST "working_directory",
|
|
BAD_CAST client->working_dir);
|
|
|
|
arg_set = xmlNewChild(clientxml, NULL, BAD_CAST "arg_set", NULL);
|
|
for (i = 0; i < client->argc; i++)
|
|
xmlNewChild(arg_set, NULL, BAD_CAST "arg",
|
|
BAD_CAST client->argv[i]);
|
|
|
|
if (client->jack_client_name)
|
|
project_create_client_jack_patch_xml(project, client,
|
|
clientxml);
|
|
else
|
|
{
|
|
lash_info("client '%s' (%p) has no jack client name", client_get_identity(client), client);
|
|
}
|
|
|
|
if (!list_empty(&client->dependencies))
|
|
project_create_client_dependencies_xml(client,
|
|
clientxml);
|
|
}
|
|
|
|
return doc;
|
|
}
|
|
|
|
static __inline__ bool
|
|
project_write_info(project_t *project)
|
|
{
|
|
xmlDocPtr doc;
|
|
const char *filename;
|
|
|
|
doc = project_create_xml(project);
|
|
|
|
if (project->doc)
|
|
xmlFree(project->doc);
|
|
project->doc = doc;
|
|
|
|
filename = lash_get_fqn(project->directory, PROJECT_INFO_FILE);
|
|
|
|
if (xmlSaveFormatFile(filename, doc, 1) == -1) {
|
|
lash_error("Cannot save project data to file %s: %s",
|
|
filename, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
project_clear_lost_clients(project_t *project)
|
|
{
|
|
struct list_head *head, *node;
|
|
struct lash_client *client;
|
|
|
|
head = &project->lost_clients;
|
|
node = head->next;
|
|
|
|
while (node != head) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
node = node->next;
|
|
|
|
if (lash_dir_exists(client->data_path))
|
|
lash_remove_dir(client->data_path);
|
|
|
|
list_del(&client->siblings);
|
|
client_destroy(client);
|
|
}
|
|
}
|
|
|
|
static __inline__ void
|
|
project_load_notes(project_t *project)
|
|
{
|
|
char *filename;
|
|
char *data;
|
|
|
|
filename = lash_dup_fqn(project->directory, PROJECT_NOTES_FILE);
|
|
|
|
if (!lash_read_text_file((const char *) filename, &data))
|
|
lash_error("Failed to read project notes from '%s'", filename);
|
|
else {
|
|
project->notes = data;
|
|
lash_debug("Project notes: \"%s\"", data);
|
|
}
|
|
|
|
free(filename);
|
|
}
|
|
|
|
static __inline__ bool
|
|
project_save_notes(project_t *project)
|
|
{
|
|
char *filename;
|
|
FILE *file;
|
|
size_t size;
|
|
bool ret;
|
|
|
|
ret = true;
|
|
|
|
filename = lash_dup_fqn(project->directory, PROJECT_NOTES_FILE);
|
|
|
|
file = fopen(filename, "w");
|
|
if (!file) {
|
|
lash_error("Failed to open '%s' for writing: %s",
|
|
filename, strerror(errno));
|
|
ret = false;
|
|
goto exit;
|
|
}
|
|
|
|
if (project->notes) {
|
|
size = strlen(project->notes);
|
|
|
|
if (fwrite(project->notes, size, 1, file) != 1) {
|
|
lash_error("Failed to write %ld bytes of data "
|
|
"to file '%s'", size, filename);
|
|
ret = false;
|
|
}
|
|
}
|
|
|
|
fclose(file);
|
|
|
|
exit:
|
|
free(filename);
|
|
return ret;
|
|
}
|
|
|
|
static
|
|
__inline__
|
|
bool
|
|
project_update_last_modify_time(
|
|
project_t * project_ptr)
|
|
{
|
|
struct stat st;
|
|
|
|
if (stat(project_ptr->directory, &st) != 0)
|
|
{
|
|
lash_error("failed to stat '%s', error is %d", project_ptr->directory, errno);
|
|
return false;
|
|
}
|
|
|
|
project_ptr->last_modify_time = st.st_mtime;
|
|
return true;
|
|
}
|
|
|
|
static
|
|
__inline__
|
|
void
|
|
project_clients_save_complete(
|
|
project_t * project_ptr)
|
|
{
|
|
bool success;
|
|
|
|
success = project_write_info(project_ptr);
|
|
|
|
/* Signal task completion */
|
|
lashd_dbus_signal_emit_progress(100);
|
|
lashd_dbus_signal_emit_project_saved(project_ptr->name);
|
|
|
|
project_update_last_modify_time(project_ptr);
|
|
|
|
if (success)
|
|
{
|
|
lash_info("Project '%s' saved.", project_ptr->name);
|
|
}
|
|
else
|
|
{
|
|
lash_error("Error writing info file for project '%s'", project_ptr->name);
|
|
}
|
|
|
|
project_set_modified_status(project_ptr, false);
|
|
}
|
|
|
|
static
|
|
__inline__
|
|
void
|
|
project_loaded(
|
|
project_t * project_ptr)
|
|
{
|
|
/* Signal task completion */
|
|
lashd_dbus_signal_emit_progress(100);
|
|
lashd_dbus_signal_emit_project_loaded(project_ptr->name);
|
|
|
|
lash_info("Project '%s' loaded.", project_ptr->name);
|
|
project_set_modified_status(project_ptr, false);
|
|
}
|
|
|
|
void
|
|
project_save(project_t *project)
|
|
{
|
|
if (project->task_type) {
|
|
lash_error("Another task (type %d) is in progress, cannot save right now", project->task_type);
|
|
lash_error("%" PRIu32 " pending client tasks.", project->client_tasks_pending);
|
|
return;
|
|
}
|
|
|
|
lash_info("Saving project '%s' ...", project->name);
|
|
|
|
/* Signal beginning of task */
|
|
lashd_dbus_signal_emit_progress(0);
|
|
|
|
if (list_empty(&project->siblings_all)) {
|
|
/* this is first save for new project, add it to available for loading list */
|
|
list_add_tail(&project->siblings_all, &g_server->all_projects);
|
|
}
|
|
|
|
if (!lash_dir_exists(project->directory)) {
|
|
lash_create_dir(project->directory);
|
|
lash_info("Created project directory %s", project->directory);
|
|
}
|
|
|
|
project_save_clients(project);
|
|
|
|
lashd_jackdbus_mgr_get_graph(g_server->jackdbus_mgr);
|
|
|
|
if (!project_save_notes(project))
|
|
lash_error("Error writing notes file for project '%s'", project->name);
|
|
|
|
project_clear_lost_clients(project);
|
|
|
|
/* in project_save_clients we tell clients to save and save completes when there are no pending tasks,
|
|
as detected in project_client_task_completed()
|
|
However, when there are not clients with internal state, project_client_task_completed() is not called at all.
|
|
OTOH save is complete at this point.
|
|
*/
|
|
if (project->client_tasks_total == 0) /* check for project with stateless clients only */
|
|
{
|
|
project_clients_save_complete(project);
|
|
}
|
|
}
|
|
|
|
bool
|
|
project_load(project_t *project)
|
|
{
|
|
xmlNodePtr projectnode, xmlnode;
|
|
xmlChar *content = NULL;
|
|
|
|
for (projectnode = project->doc->children; projectnode;
|
|
projectnode = projectnode->next) {
|
|
if (projectnode->type == XML_ELEMENT_NODE
|
|
&& strcmp((const char *) projectnode->name, "lash_project") == 0)
|
|
break;
|
|
}
|
|
|
|
if (!projectnode) {
|
|
lash_error("No root node in project XML document");
|
|
return false;
|
|
}
|
|
|
|
for (xmlnode = projectnode->children; xmlnode;
|
|
xmlnode = xmlnode->next) {
|
|
if (strcmp((const char *) xmlnode->name, "version") == 0) {
|
|
/* FIXME: check version */
|
|
} else if (strcmp((const char *) xmlnode->name, "name") == 0) {
|
|
content = xmlNodeGetContent(xmlnode);
|
|
lash_strset(&project->name, (const char *) content);
|
|
xmlFree(content);
|
|
} else if (strcmp((const char *) xmlnode->name, "client") == 0) {
|
|
struct lash_client *client;
|
|
|
|
client = client_new();
|
|
client->project = project;
|
|
client_parse_xml(project, client, xmlnode);
|
|
|
|
// TODO: reject client if its data doesn't contain
|
|
// the basic stuff (id, class, etc.)
|
|
|
|
list_add(&client->siblings, &project->lost_clients);
|
|
}
|
|
}
|
|
|
|
if (!project->name) {
|
|
lash_error("No name node in project XML document");
|
|
project_unload(project);
|
|
return false;
|
|
}
|
|
|
|
project_load_notes(project);
|
|
|
|
#ifdef LASH_DEBUG
|
|
{
|
|
struct list_head *node, *node2;
|
|
struct lash_client *client;
|
|
int i;
|
|
|
|
lash_debug("Restored project with:");
|
|
lash_debug(" directory: '%s'", project->directory);
|
|
lash_debug(" name: '%s'", project->name);
|
|
lash_debug(" clients:");
|
|
list_for_each (node, &project->lost_clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
|
|
lash_debug(" ------");
|
|
lash_debug(" id: '%s'", client->id_str);
|
|
lash_debug(" name: '%s'", client->name);
|
|
lash_debug(" working dir: '%s'", client->working_dir);
|
|
lash_debug(" flags: %d", client->flags);
|
|
lash_debug(" argc: %d", client->argc);
|
|
lash_debug(" args:");
|
|
for (i = 0; i < client->argc; i++) {
|
|
lash_debug(" %d: '%s'", i, client->argv[i]);
|
|
}
|
|
|
|
if (!list_empty(&client->jack_patches)) {
|
|
lash_debug(" jack patches:");
|
|
list_for_each (node2, &client->jack_patches) {
|
|
jack_patch_t *p = list_entry(node2, jack_patch_t, siblings);
|
|
lash_debug(" '%s' -> '%s'", p->src_desc, p->dest_desc);
|
|
}
|
|
} else
|
|
lash_debug(" no jack patches");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
project->modified_status = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
project_t *
|
|
project_new_from_disk(const char *parent_dir,
|
|
const char *project_dir)
|
|
{
|
|
project_t *project;
|
|
char *filename = NULL;
|
|
xmlNodePtr projectnode, xmlnode;
|
|
xmlChar *content = NULL;
|
|
|
|
project = project_new();
|
|
|
|
INIT_LIST_HEAD(&project->siblings_loaded);
|
|
|
|
lash_strset(&project->name, project_dir);
|
|
|
|
project->directory = lash_dup_fqn(parent_dir, project_dir);
|
|
|
|
if (!project_update_last_modify_time(project))
|
|
{
|
|
goto fail;
|
|
}
|
|
|
|
filename = lash_dup_fqn(project->directory, PROJECT_INFO_FILE);
|
|
if (!lash_file_exists(filename)) {
|
|
lash_error("Project '%s' has no info file", project->name);
|
|
goto fail;
|
|
}
|
|
|
|
project->doc = xmlParseFile(filename);
|
|
if (project->doc == NULL) {
|
|
lash_error("Could not parse file %s", filename);
|
|
goto fail;
|
|
}
|
|
|
|
lash_free(&filename);
|
|
|
|
for (projectnode = project->doc->children; projectnode;
|
|
projectnode = projectnode->next) {
|
|
if (projectnode->type == XML_ELEMENT_NODE
|
|
&& strcmp((const char *) projectnode->name, "lash_project") == 0)
|
|
break;
|
|
}
|
|
|
|
if (!projectnode) {
|
|
lash_error("No root node in project XML document");
|
|
goto fail;
|
|
}
|
|
|
|
for (xmlnode = projectnode->children; xmlnode;
|
|
xmlnode = xmlnode->next) {
|
|
if (strcmp((const char *) xmlnode->name, "version") == 0) {
|
|
/* FIXME: check version */
|
|
} else if (strcmp((const char *) xmlnode->name, "name") == 0) {
|
|
content = xmlNodeGetContent(xmlnode);
|
|
lash_strset(&project->name, (const char *) content);
|
|
xmlFree(content);
|
|
} else if (strcmp((const char *) xmlnode->name, "description") == 0) {
|
|
content = xmlNodeGetContent(xmlnode);
|
|
lash_strset(&project->description, (const char *)content);
|
|
xmlFree(content);
|
|
}
|
|
}
|
|
|
|
if (!project->name) {
|
|
lash_error("No name node in project XML document");
|
|
goto fail;
|
|
}
|
|
|
|
return project;
|
|
|
|
fail:
|
|
lash_free(&filename);
|
|
project_destroy(project);
|
|
return NULL;
|
|
}
|
|
|
|
bool
|
|
project_is_loaded(project_t *project)
|
|
{
|
|
return !list_empty(&project->siblings_loaded);
|
|
}
|
|
|
|
void
|
|
project_lose_client(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
LIST_HEAD(patches);
|
|
|
|
lash_info("Losing client '%s'", client_get_identity(client));
|
|
|
|
if (CLIENT_CONFIG_DATA_SET(client) && client->store) {
|
|
if (!list_empty(&client->store->keys))
|
|
store_write(client->store);
|
|
else if (lash_dir_exists(client->store->dir))
|
|
lash_remove_dir(client->store->dir);
|
|
}
|
|
|
|
if (CLIENT_CONFIG_DATA_SET(client) || CLIENT_CONFIG_FILE(client)) {
|
|
const char *dir = (const char *) client->data_path;
|
|
if (lash_dir_exists(dir) && lash_dir_empty(dir))
|
|
lash_remove_dir(dir);
|
|
|
|
dir = lash_get_fqn(project->directory, PROJECT_ID_DIR);
|
|
if (lash_dir_exists(dir) && lash_dir_empty(dir))
|
|
lash_remove_dir(dir);
|
|
}
|
|
|
|
if (client->jack_client_name) {
|
|
if (lashd_jackdbus_mgr_remove_client(g_server->jackdbus_mgr,
|
|
client->id, &patches)) {
|
|
list_splice(&patches, &client->jack_patches);
|
|
#ifdef LASH_DEBUG
|
|
lash_debug("Backed-up JACK patches:");
|
|
jack_patch_list(&client->jack_patches);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* Pid is only stored for clients who were recently launched so that
|
|
lashd can tell launched clients from recovering ones. All lost
|
|
clients must have valid project pointers. */
|
|
client->pid = 0;
|
|
client->project = project;
|
|
|
|
list_add(&client->siblings, &project->lost_clients);
|
|
project_set_modified_status(project, true);
|
|
lashd_dbus_signal_emit_client_disappeared(client->id_str, project->name);
|
|
}
|
|
|
|
void
|
|
project_unload(project_t *project)
|
|
{
|
|
struct list_head *node, *next;
|
|
struct lash_client *client;
|
|
|
|
LIST_HEAD(patches);
|
|
|
|
list_del_init(&project->siblings_loaded);
|
|
|
|
lash_debug("Signaling all clients of project '%s' to quit",
|
|
project->name);
|
|
|
|
signal_new_single(g_server->dbus_service,
|
|
"/", "org.nongnu.LASH.Server", "Quit",
|
|
DBUS_TYPE_STRING, &project->name);
|
|
|
|
list_for_each_safe (node, next, &project->clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
|
|
if (client->dbus_name == NULL && client->pid != 0)
|
|
{
|
|
lash_debug("Sending SIGTERM to raw client '%s' with PID %llu", client->name, (unsigned long long)client->pid);
|
|
kill(client->pid, SIGTERM);
|
|
}
|
|
|
|
if (client->jack_client_name) {
|
|
lashd_jackdbus_mgr_remove_client(g_server->jackdbus_mgr,
|
|
client->id, NULL);
|
|
}
|
|
|
|
list_del(&client->siblings);
|
|
client_destroy(client);
|
|
}
|
|
|
|
list_for_each_safe (node, next, &project->lost_clients) {
|
|
client = list_entry(node, struct lash_client, siblings);
|
|
list_del(&client->siblings);
|
|
// TODO: Do lost clients also need to have their patches destroyed?
|
|
client_destroy(client);
|
|
}
|
|
|
|
if (project->move_on_close)
|
|
{
|
|
char * project_dir;
|
|
char * char_ptr;
|
|
|
|
project_dir = lash_dup_fqn(g_server->projects_dir, project->name);
|
|
|
|
/* replace bad chars so we dont save in other project dir for example */
|
|
/* i.e. make sure we do rename, not move into other dir */
|
|
char_ptr = project_dir + strlen(g_server->projects_dir) + 1;
|
|
while ((char_ptr = strpbrk(char_ptr, "/")) != NULL)
|
|
{
|
|
*char_ptr = ' ';
|
|
char_ptr++;
|
|
}
|
|
|
|
project_move(project, project_dir);
|
|
free(project_dir);
|
|
}
|
|
|
|
lash_info("Project '%s' unloaded", project->name);
|
|
|
|
if (project->doc == NULL && lash_dir_exists(project->directory))
|
|
{
|
|
lash_info("Removing directory '%s' of closed newborn project '%s'", project->directory, project->name);
|
|
lash_remove_dir(project->directory);
|
|
}
|
|
}
|
|
|
|
void
|
|
project_destroy(project_t *project)
|
|
{
|
|
if (project) {
|
|
lash_debug("Destroying project '%s'", project->name);
|
|
|
|
if (project_is_loaded(project))
|
|
project_unload(project);
|
|
|
|
if (project->doc)
|
|
xmlFree(project->doc);
|
|
|
|
lash_free(&project->name);
|
|
lash_free(&project->directory);
|
|
lash_free(&project->description);
|
|
lash_free(&project->notes);
|
|
|
|
// TODO: Free client lists
|
|
|
|
free(project);
|
|
}
|
|
}
|
|
|
|
/** Update @a project 's and @a client 's progress readings based on @a client 's
|
|
* updated progress reading @a percentage. Signal @a project 's updated progress
|
|
* reading to the outside world.
|
|
* @param project Project the client belongs to.
|
|
* @param client Client that has progressed.
|
|
* @param percentage Percentage at which @a client is now.
|
|
*/
|
|
void
|
|
project_client_progress(project_t *project,
|
|
struct lash_client *client,
|
|
uint8_t percentage)
|
|
{
|
|
if (client->task_progress)
|
|
project->client_tasks_progress -= client->task_progress;
|
|
|
|
project->client_tasks_progress += percentage;
|
|
client->task_progress = percentage;
|
|
|
|
/* Prevent divide by 0 */
|
|
if (!project->client_tasks_total)
|
|
return;
|
|
|
|
uint8_t p = project->client_tasks_progress / project->client_tasks_total;
|
|
lashd_dbus_signal_emit_progress(p > 99 ? 99 : p);
|
|
}
|
|
|
|
/* Send the appropriate signal(s) to signify that a client completed a task */
|
|
void
|
|
project_client_task_completed(project_t *project,
|
|
struct lash_client *client)
|
|
{
|
|
lash_debug("----------- client '%s' task completed", client->name);
|
|
|
|
/* Calculate new progress reading and send Progress signal */
|
|
project_client_progress(project, client, 100);
|
|
|
|
/* If the project task is finished emit the appropriate signals */
|
|
if (project->client_tasks_pending && (--project->client_tasks_pending) == 0) {
|
|
lash_debug("----------- project '%s' tasks completed", project->name);
|
|
project->task_type = 0;
|
|
project->client_tasks_total = 0;
|
|
project->client_tasks_progress = 0;
|
|
|
|
/* Send ProjectSaved or ProjectLoaded signal, or return if the task was neither */
|
|
switch (client->task_type) {
|
|
case LASH_Save_Data_Set: case LASH_Save_File:
|
|
project_clients_save_complete(project);
|
|
break;
|
|
case LASH_Restore_File: case LASH_Restore_Data_Set:
|
|
project_loaded(project);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
project_rename(project_t *project,
|
|
const char *new_name)
|
|
{
|
|
char *old_name = project->name;
|
|
|
|
if (strcmp(project->name, new_name) != 0)
|
|
{
|
|
project->move_on_close = true;
|
|
}
|
|
|
|
project->name = lash_strdup(new_name);
|
|
|
|
lashd_dbus_signal_emit_project_name_changed(old_name, new_name);
|
|
|
|
project_set_modified_status(project, true);
|
|
|
|
lash_free(&old_name);
|
|
}
|
|
|
|
void
|
|
project_set_description(project_t *project,
|
|
const char *description)
|
|
{
|
|
lash_strset(&project->description, description);
|
|
lashd_dbus_signal_emit_project_description_changed(project->name, description);
|
|
|
|
project_set_modified_status(project, true);
|
|
}
|
|
|
|
void
|
|
project_set_notes(project_t *project,
|
|
const char *notes)
|
|
{
|
|
lash_strset(&project->notes, notes);
|
|
lashd_dbus_signal_emit_project_notes_changed(project->name, notes);
|
|
|
|
project_set_modified_status(project, true);
|
|
}
|
|
|
|
void
|
|
project_rename_client(project_t *project,
|
|
struct lash_client *client,
|
|
const char *name)
|
|
{
|
|
if (strcmp(client->name, name) == 0)
|
|
return;
|
|
|
|
lash_strset(&client->name, name);
|
|
lashd_dbus_signal_emit_client_name_changed(client->id_str, client->name);
|
|
}
|
|
|
|
void
|
|
project_clear_id_dir(project_t *project)
|
|
{
|
|
if (project) {
|
|
const char *dir = lash_get_fqn(project->directory,
|
|
PROJECT_ID_DIR);
|
|
if (lash_dir_exists(dir))
|
|
lash_remove_dir(dir);
|
|
}
|
|
}
|
|
|
|
/* EOF */
|