@@ -76,7 +76,8 @@ static QTAILQ_HEAD(, BlockBackend) blk_backends =
* Store an error through @errp on failure, unless it's null.
* Return the new BlockBackend on success, null on failure.
*/
-BlockBackend *blk_new(const char *name, Error **errp)
+BlockBackend *blk_new(const char *name, Notifier *close_all_notifier,
+ Error **errp)
{
BlockBackend *blk;
@@ -100,6 +101,9 @@ BlockBackend *blk_new(const char *name, Error **errp)
blk->name = g_strdup(name);
blk->refcnt = 1;
notifier_list_init(&blk->remove_bs_notifiers);
+ if (close_all_notifier) {
+ bdrv_add_close_all_notifier(close_all_notifier);
+ }
QTAILQ_INSERT_TAIL(&blk_backends, blk, link);
return blk;
}
@@ -108,12 +112,13 @@ BlockBackend *blk_new(const char *name, Error **errp)
* Create a new BlockBackend with a new BlockDriverState attached.
* Otherwise just like blk_new(), which see.
*/
-BlockBackend *blk_new_with_bs(const char *name, Error **errp)
+BlockBackend *blk_new_with_bs(const char *name, Notifier *close_all_notifier,
+ Error **errp)
{
BlockBackend *blk;
BlockDriverState *bs;
- blk = blk_new(name, errp);
+ blk = blk_new(name, close_all_notifier, errp);
if (!blk) {
return NULL;
}
@@ -138,12 +143,12 @@ BlockBackend *blk_new_with_bs(const char *name, Error **errp)
*/
BlockBackend *blk_new_open(const char *name, const char *filename,
const char *reference, QDict *options, int flags,
- Error **errp)
+ Notifier *close_all_notifier, Error **errp)
{
BlockBackend *blk;
int ret;
- blk = blk_new_with_bs(name, errp);
+ blk = blk_new_with_bs(name, close_all_notifier, errp);
if (!blk) {
QDECREF(options);
return NULL;
@@ -151,7 +156,7 @@ BlockBackend *blk_new_open(const char *name, const char *filename,
ret = bdrv_open(&blk->bs, filename, reference, options, flags, NULL, errp);
if (ret < 0) {
- blk_unref(blk);
+ blk_unref(blk, close_all_notifier);
return NULL;
}
@@ -191,9 +196,13 @@ static void drive_info_del(DriveInfo *dinfo)
* Increment @blk's reference count.
* @blk must not be null.
*/
-void blk_ref(BlockBackend *blk)
+void blk_ref(BlockBackend *blk, Notifier *close_all_notifier)
{
blk->refcnt++;
+
+ if (close_all_notifier) {
+ bdrv_add_close_all_notifier(close_all_notifier);
+ }
}
/*
@@ -201,8 +210,12 @@ void blk_ref(BlockBackend *blk)
* If this drops it to zero, destroy @blk.
* For convenience, do nothing if @blk is null.
*/
-void blk_unref(BlockBackend *blk)
+void blk_unref(BlockBackend *blk, Notifier *close_all_notifier)
{
+ if (close_all_notifier) {
+ notifier_remove(close_all_notifier);
+ }
+
if (blk) {
assert(blk->refcnt > 0);
if (!--blk->refcnt) {
@@ -352,6 +365,21 @@ void blk_insert_bs(BlockBackend *blk, BlockDriverState *bs)
bs->blk = blk;
}
+typedef struct DevCloseAllNotifier {
+ Notifier n;
+ BlockBackend *blk;
+ QTAILQ_ENTRY(DevCloseAllNotifier) next;
+} DevCloseAllNotifier;
+
+static QTAILQ_HEAD(, DevCloseAllNotifier) close_all_notifiers =
+ QTAILQ_HEAD_INITIALIZER(close_all_notifiers);
+
+static void dev_close_all_notifier(Notifier *n, void *data)
+{
+ DevCloseAllNotifier *can = DO_UPCAST(DevCloseAllNotifier, n, n);
+ blk_detach_dev(can->blk, can->blk->dev);
+}
+
/*
* Attach device model @dev to @blk.
* Return 0 on success, -EBUSY when a device model is attached already.
@@ -359,10 +387,19 @@ void blk_insert_bs(BlockBackend *blk, BlockDriverState *bs)
int blk_attach_dev(BlockBackend *blk, void *dev)
/* TODO change to DeviceState *dev when all users are qdevified */
{
+ DevCloseAllNotifier *can;
+
if (blk->dev) {
return -EBUSY;
}
- blk_ref(blk);
+
+ can = g_new0(DevCloseAllNotifier, 1);
+ can->n.notify = dev_close_all_notifier;
+ can->blk = blk;
+
+ QTAILQ_INSERT_TAIL(&close_all_notifiers, can, next);
+
+ blk_ref(blk, &can->n);
blk->dev = dev;
blk_iostatus_reset(blk);
return 0;
@@ -387,12 +424,24 @@ void blk_attach_dev_nofail(BlockBackend *blk, void *dev)
void blk_detach_dev(BlockBackend *blk, void *dev)
/* TODO change to DeviceState *dev when all users are qdevified */
{
+ DevCloseAllNotifier *can;
+
assert(blk->dev == dev);
blk->dev = NULL;
blk->dev_ops = NULL;
blk->dev_opaque = NULL;
blk->guest_block_size = 512;
- blk_unref(blk);
+
+ QTAILQ_FOREACH(can, &close_all_notifiers, next) {
+ if (can->blk == blk) {
+ break;
+ }
+ }
+ assert(can);
+
+ blk_unref(blk, &can->n);
+ QTAILQ_REMOVE(&close_all_notifiers, can, next);
+ g_free(can);
}
/*
@@ -47,6 +47,15 @@
#include "trace.h"
#include "sysemu/arch_init.h"
+typedef struct BlockdevCloseAllNotifier {
+ Notifier n;
+ BlockBackend *blk;
+ QTAILQ_ENTRY(BlockdevCloseAllNotifier) next;
+} BlockdevCloseAllNotifier;
+
+static QTAILQ_HEAD(, BlockdevCloseAllNotifier) close_all_notifiers =
+ QTAILQ_HEAD_INITIALIZER(close_all_notifiers);
+
static const char *const if_name[IF_COUNT] = {
[IF_NONE] = "none",
[IF_IDE] = "ide",
@@ -78,6 +87,37 @@ static int if_max_devs[IF_COUNT] = {
[IF_SCSI] = 7,
};
+static void monitor_blk_unref_with_can(BlockBackend *blk,
+ BlockdevCloseAllNotifier *can)
+{
+ if (can) {
+ assert(can->blk == blk);
+ } else {
+ QTAILQ_FOREACH(can, &close_all_notifiers, next) {
+ if (can->blk == blk) {
+ break;
+ }
+ }
+ assert(can);
+ }
+
+ blk_unref(blk, &can->n);
+ QTAILQ_REMOVE(&close_all_notifiers, can, next);
+ g_free(can);
+}
+
+void monitor_blk_unref(BlockBackend *blk)
+{
+ monitor_blk_unref_with_can(blk, NULL);
+}
+
+static void blockdev_close_all_notifier(Notifier *n, void *data)
+{
+ BlockdevCloseAllNotifier *can = DO_UPCAST(BlockdevCloseAllNotifier, n, n);
+
+ monitor_blk_unref_with_can(can->blk, can);
+}
+
/**
* Boards may call this to offer board-by-board overrides
* of the default, global values.
@@ -140,7 +180,7 @@ void blockdev_auto_del(BlockBackend *blk)
DriveInfo *dinfo = blk_legacy_dinfo(blk);
if (dinfo && dinfo->auto_del) {
- blk_unref(blk);
+ monitor_blk_unref(blk);
}
}
@@ -460,6 +500,7 @@ static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
bool has_driver_specific_opts;
BlockdevDetectZeroesOptions detect_zeroes =
BLOCKDEV_DETECT_ZEROES_OPTIONS_OFF;
+ BlockdevCloseAllNotifier *can;
/* Check common options by copying from bs_opts to opts, all other options
* stay in bs_opts for processing by bdrv_open(). */
@@ -536,14 +577,21 @@ static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
}
/* init */
+ can = g_new0(BlockdevCloseAllNotifier, 1);
+ can->n.notify = blockdev_close_all_notifier;
+
if ((!file || !*file) && !has_driver_specific_opts) {
BlockBackendRootState *blk_rs;
- blk = blk_new(qemu_opts_id(opts), errp);
+ blk = blk_new(qemu_opts_id(opts), &can->n, errp);
if (!blk) {
+ g_free(can);
goto early_err;
}
+ can->blk = blk;
+ QTAILQ_INSERT_TAIL(&close_all_notifiers, can, next);
+
blk_rs = blk_get_root_state(blk);
blk_rs->open_flags = bdrv_flags;
blk_rs->read_only = !(bdrv_flags & BDRV_O_RDWR);
@@ -561,12 +609,16 @@ static BlockBackend *blockdev_init(const char *file, QDict *bs_opts,
}
blk = blk_new_open(qemu_opts_id(opts), file, NULL, bs_opts, bdrv_flags,
- errp);
+ &can->n, errp);
if (!blk) {
+ g_free(can);
goto err_no_bs_opts;
}
bs = blk_bs(blk);
+ can->blk = blk;
+ QTAILQ_INSERT_TAIL(&close_all_notifiers, can, next);
+
bs->detect_zeroes = detect_zeroes;
/* disk I/O throttling */
@@ -2246,7 +2298,7 @@ int do_drive_del(Monitor *mon, const QDict *qdict, QObject **ret_data)
blk_set_on_error(blk, BLOCKDEV_ON_ERROR_REPORT,
BLOCKDEV_ON_ERROR_REPORT);
} else {
- blk_unref(blk);
+ monitor_blk_unref(blk);
}
aio_context_release(aio_context);
@@ -3174,7 +3226,7 @@ void qmp_blockdev_add(BlockdevOptions *options, Error **errp)
if (bs && bdrv_key_required(bs)) {
if (blk) {
- blk_unref(blk);
+ monitor_blk_unref(blk);
} else {
bdrv_unref(bs);
}
@@ -77,6 +77,6 @@ void drive_hot_add(Monitor *mon, const QDict *qdict)
err:
if (dinfo) {
- blk_unref(blk_by_legacy_dinfo(dinfo));
+ monitor_blk_unref(blk_by_legacy_dinfo(dinfo));
}
}
@@ -908,8 +908,10 @@ static int blk_connect(struct XenDevice *xendev)
/* setup via xenbus -> create new block driver instance */
xen_be_printf(&blkdev->xendev, 2, "create new bdrv (xenbus setup)\n");
+ /* No need to give a close_all_notifier because blk_attach_dev_nofail()
+ * and blk_detach_dev() will keep track of our reference */
blkdev->blk = blk_new_open(blkdev->dev, blkdev->filename, NULL, options,
- qflags, &local_err);
+ qflags, NULL, &local_err);
if (!blkdev->blk) {
xen_be_printf(&blkdev->xendev, 0, "error: %s\n",
error_get_pretty(local_err));
@@ -925,11 +927,16 @@ static int blk_connect(struct XenDevice *xendev)
blkdev->blk = NULL;
return -1;
}
- /* blkdev->blk is not create by us, we get a reference
- * so we can blk_unref() unconditionally */
- blk_ref(blkdev->blk);
}
+
blk_attach_dev_nofail(blkdev->blk, blkdev);
+ if (!blkdev->dinfo) {
+ /* blkdev->blk has just been created; blk_attach_dev_nofail() counts
+ * the reference blkdev->blk, so we do not have to keep track (which
+ * allows us to ignore bdrv_close_all()) */
+ blk_unref(blkdev->blk, NULL);
+ }
+
blkdev->file_size = blk_getlength(blkdev->blk);
if (blkdev->file_size < 0) {
BlockDriverState *bs = blk_bs(blkdev->blk);
@@ -1032,7 +1039,6 @@ static void blk_disconnect(struct XenDevice *xendev)
if (blkdev->blk) {
blk_detach_dev(blkdev->blk, blkdev);
- blk_unref(blkdev->blk);
blkdev->blk = NULL;
}
xen_be_unbind_evtchn(&blkdev->xendev);
@@ -184,7 +184,6 @@ int pci_piix3_xen_ide_unplug(DeviceState *dev)
blk_detach_dev(blk, ds);
}
pci_ide->bus[di->bus].ifs[di->unit].blk = NULL;
- blk_unref(blk);
}
}
qdev_reset_all(DEVICE(dev));
@@ -13,6 +13,7 @@
#ifndef BLOCK_BACKEND_H
#define BLOCK_BACKEND_H
+#include "qemu/notify.h"
#include "qemu/typedefs.h"
#include "qapi/error.h"
@@ -60,13 +61,15 @@ typedef struct BlockDevOps {
void (*resize_cb)(void *opaque);
} BlockDevOps;
-BlockBackend *blk_new(const char *name, Error **errp);
-BlockBackend *blk_new_with_bs(const char *name, Error **errp);
+BlockBackend *blk_new(const char *name, Notifier *close_all_notifier,
+ Error **errp);
+BlockBackend *blk_new_with_bs(const char *name, Notifier *close_all_notifier,
+ Error **errp);
BlockBackend *blk_new_open(const char *name, const char *filename,
const char *reference, QDict *options, int flags,
- Error **errp);
-void blk_ref(BlockBackend *blk);
-void blk_unref(BlockBackend *blk);
+ Notifier *close_all_notifier, Error **errp);
+void blk_ref(BlockBackend *blk, Notifier *close_all_notifier);
+void blk_unref(BlockBackend *blk, Notifier *close_all_notifier);
const char *blk_name(BlockBackend *blk);
BlockBackend *blk_by_name(const char *name);
BlockBackend *blk_next(BlockBackend *blk);
@@ -67,4 +67,7 @@ DriveInfo *add_init_drive(const char *opts);
void do_commit(Monitor *mon, const QDict *qdict);
int do_drive_del(Monitor *mon, const QDict *qdict, QObject **ret_data);
+
+void monitor_blk_unref(BlockBackend *blk);
+
#endif
@@ -973,6 +973,9 @@ typedef struct NBDCloseNotifier {
static QTAILQ_HEAD(, NBDCloseNotifier) eject_notifiers =
QTAILQ_HEAD_INITIALIZER(eject_notifiers);
+static QTAILQ_HEAD(, NBDCloseNotifier) close_notifiers =
+ QTAILQ_HEAD_INITIALIZER(close_notifiers);
+
static void nbd_close_notifier(Notifier *n, void *data)
{
NBDCloseNotifier *cn = DO_UPCAST(NBDCloseNotifier, n, n);
@@ -983,6 +986,7 @@ NBDExport *nbd_export_new(BlockBackend *blk, off_t dev_offset, off_t size,
uint32_t nbdflags, void (*close)(NBDExport *))
{
NBDCloseNotifier *eject_notifier = g_new0(NBDCloseNotifier, 1);
+ NBDCloseNotifier *close_notifier = g_new0(NBDCloseNotifier, 1);
NBDExport *exp = g_malloc0(sizeof(NBDExport));
exp->refcount = 1;
QTAILQ_INIT(&exp->clients);
@@ -992,7 +996,12 @@ NBDExport *nbd_export_new(BlockBackend *blk, off_t dev_offset, off_t size,
exp->size = size == -1 ? blk_getlength(blk) : size;
exp->close = close;
exp->ctx = blk_get_aio_context(blk);
- blk_ref(blk);
+
+ close_notifier->n.notify = nbd_close_notifier;
+ close_notifier->exp = exp;
+ QTAILQ_INSERT_TAIL(&close_notifiers, close_notifier, next);
+
+ blk_ref(blk, &close_notifier->n);
blk_add_aio_context_notifier(blk, blk_aio_attached, blk_aio_detach, exp);
eject_notifier->n.notify = nbd_close_notifier;
@@ -1045,7 +1054,7 @@ void nbd_export_set_name(NBDExport *exp, const char *name)
void nbd_export_close(NBDExport *exp)
{
- NBDCloseNotifier *eject_notifier;
+ NBDCloseNotifier *eject_notifier, *close_notifier;
NBDClient *client, *next;
nbd_export_get(exp);
@@ -1054,6 +1063,7 @@ void nbd_export_close(NBDExport *exp)
}
nbd_export_set_name(exp, NULL);
nbd_export_put(exp);
+
if (exp->blk) {
QTAILQ_FOREACH(eject_notifier, &eject_notifiers, next) {
if (eject_notifier->exp == exp) {
@@ -1066,10 +1076,20 @@ void nbd_export_close(NBDExport *exp)
QTAILQ_REMOVE(&eject_notifiers, eject_notifier, next);
g_free(eject_notifier);
+ QTAILQ_FOREACH(close_notifier, &close_notifiers, next) {
+ if (close_notifier->exp == exp) {
+ break;
+ }
+ }
+ assert(close_notifier);
+
blk_remove_aio_context_notifier(exp->blk, blk_aio_attached,
blk_aio_detach, exp);
- blk_unref(exp->blk);
+ blk_unref(exp->blk, &close_notifier->n);
exp->blk = NULL;
+
+ QTAILQ_REMOVE(&close_notifiers, close_notifier, next);
+ g_free(close_notifier);
}
}
@@ -302,7 +302,7 @@ static BlockBackend *img_open(const char *id, const char *filename,
qdict_put(options, "driver", qstring_from_str(fmt));
}
- blk = blk_new_open(id, filename, NULL, options, flags, &local_err);
+ blk = blk_new_open(id, filename, NULL, options, flags, NULL, &local_err);
if (!blk) {
error_report("Could not open '%s': %s", filename,
error_get_pretty(local_err));
@@ -324,7 +324,7 @@ static BlockBackend *img_open(const char *id, const char *filename,
}
return blk;
fail:
- blk_unref(blk);
+ blk_unref(blk, NULL);
return NULL;
}
@@ -713,7 +713,7 @@ static int img_check(int argc, char **argv)
fail:
qapi_free_ImageCheck(check);
- blk_unref(blk);
+ blk_unref(blk, NULL);
return ret;
}
@@ -880,7 +880,7 @@ unref_backing:
done:
qemu_progress_end();
- blk_unref(blk);
+ blk_unref(blk, NULL);
if (local_err) {
qerror_report_err(local_err);
@@ -1292,9 +1292,9 @@ static int img_compare(int argc, char **argv)
out:
qemu_vfree(buf1);
qemu_vfree(buf2);
- blk_unref(blk2);
+ blk_unref(blk2, NULL);
out2:
- blk_unref(blk1);
+ blk_unref(blk1, NULL);
out3:
qemu_progress_end();
return ret;
@@ -1862,11 +1862,11 @@ out:
qemu_opts_free(create_opts);
qemu_vfree(buf);
qemu_opts_del(sn_opts);
- blk_unref(out_blk);
+ blk_unref(out_blk, NULL);
g_free(bs);
if (blk) {
for (bs_i = 0; bs_i < bs_n; bs_i++) {
- blk_unref(blk[bs_i]);
+ blk_unref(blk[bs_i], NULL);
}
g_free(blk);
}
@@ -2001,7 +2001,7 @@ static ImageInfoList *collect_image_info_list(const char *filename,
if (err) {
error_report("%s", error_get_pretty(err));
error_free(err);
- blk_unref(blk);
+ blk_unref(blk, NULL);
goto err;
}
@@ -2010,7 +2010,7 @@ static ImageInfoList *collect_image_info_list(const char *filename,
*last = elem;
last = &elem->next;
- blk_unref(blk);
+ blk_unref(blk, NULL);
filename = fmt = NULL;
if (chain) {
@@ -2298,7 +2298,7 @@ static int img_map(int argc, char **argv)
dump_map_entry(output_format, &curr, NULL);
out:
- blk_unref(blk);
+ blk_unref(blk, NULL);
return ret < 0;
}
@@ -2422,7 +2422,7 @@ static int img_snapshot(int argc, char **argv)
}
/* Cleanup */
- blk_unref(blk);
+ blk_unref(blk, NULL);
if (ret) {
return 1;
}
@@ -2546,7 +2546,7 @@ static int img_rebase(int argc, char **argv)
bdrv_get_backing_filename(bs, backing_name, sizeof(backing_name));
blk_old_backing = blk_new_open("old_backing", backing_name, NULL,
- options, src_flags, &local_err);
+ options, src_flags, NULL, &local_err);
if (!blk_old_backing) {
error_report("Could not open old backing file '%s': %s",
backing_name, error_get_pretty(local_err));
@@ -2563,7 +2563,8 @@ static int img_rebase(int argc, char **argv)
}
blk_new_backing = blk_new_open("new_backing", out_baseimg, NULL,
- options, src_flags, &local_err);
+ options, src_flags, NULL,
+ &local_err);
if (!blk_new_backing) {
error_report("Could not open new backing file '%s': %s",
out_baseimg, error_get_pretty(local_err));
@@ -2736,11 +2737,11 @@ out:
qemu_progress_end();
/* Cleanup */
if (!unsafe) {
- blk_unref(blk_old_backing);
- blk_unref(blk_new_backing);
+ blk_unref(blk_old_backing, NULL);
+ blk_unref(blk_new_backing, NULL);
}
- blk_unref(blk);
+ blk_unref(blk, NULL);
if (ret) {
return 1;
}
@@ -2863,7 +2864,7 @@ static int img_resize(int argc, char **argv)
break;
}
out:
- blk_unref(blk);
+ blk_unref(blk, NULL);
if (ret) {
return 1;
}
@@ -3001,7 +3002,7 @@ static int img_amend(int argc, char **argv)
out:
qemu_progress_end();
- blk_unref(blk);
+ blk_unref(blk, NULL);
qemu_opts_del(opts);
qemu_opts_free(create_opts);
g_free(options);
@@ -37,7 +37,7 @@ static ReadLineState *readline_state;
static int close_f(BlockBackend *blk, int argc, char **argv)
{
- blk_unref(qemuio_blk);
+ blk_unref(qemuio_blk, NULL);
qemuio_blk = NULL;
return 0;
}
@@ -59,7 +59,7 @@ static int openfile(char *name, int flags, QDict *opts)
return 1;
}
- qemuio_blk = blk_new_open("hda", name, NULL, opts, flags, &local_err);
+ qemuio_blk = blk_new_open("hda", name, NULL, opts, flags, NULL, &local_err);
if (!qemuio_blk) {
fprintf(stderr, "%s: can't open%s%s: %s\n", progname,
name ? " device " : "", name ?: "",
@@ -474,7 +474,7 @@ int main(int argc, char **argv)
*/
bdrv_drain_all();
- blk_unref(qemuio_blk);
+ blk_unref(qemuio_blk, NULL);
g_free(readline_state);
return 0;
}
@@ -694,7 +694,8 @@ int main(int argc, char **argv)
}
srcpath = argv[optind];
- blk = blk_new_open("hda", srcpath, NULL, options, flags, &local_err);
+ /* the reference will be passed on nbd_export_new() */
+ blk = blk_new_open("hda", srcpath, NULL, options, flags, NULL, &local_err);
if (!blk) {
errx(EXIT_FAILURE, "Failed to blk_new_open '%s': %s", argv[optind],
error_get_pretty(local_err));
@@ -728,7 +729,9 @@ int main(int argc, char **argv)
}
}
+ /* nbd_export_new() takes the reference to blk */
exp = nbd_export_new(blk, dev_offset, fd_size, nbdflags, nbd_export_closed);
+ blk_unref(blk, NULL);
if (sockpath) {
fd = unix_socket_incoming(sockpath);
@@ -773,7 +776,6 @@ int main(int argc, char **argv)
}
} while (state != TERMINATED);
- blk_unref(blk);
if (sockpath) {
unlink(sockpath);
}
Every time a reference to a BlockBackend is taken, a notifier for bdrv_close_all() has to be deposited so the reference holder can relinquish its reference when bdrv_close_all() is called. That notifier should be revoked on a bdrv_unref() call. Add a Notifier * parameter to all the functions changing the reference count of a BlockBackend: blk_new(), blk_new_with_bs(), blk_new_open(), blk_ref() and blk_unref(). By dropping the manual reference handling in hw/block/xen_disk.c, the blk_unref() in hw/ide/piix.c can be dropped as well. Signed-off-by: Max Reitz <mreitz@redhat.com> --- block/block-backend.c | 69 ++++++++++++++++++++++++++++++++++++------ blockdev.c | 62 ++++++++++++++++++++++++++++++++++--- device-hotplug.c | 2 +- hw/block/xen_disk.c | 16 +++++++--- hw/ide/piix.c | 1 - include/sysemu/block-backend.h | 13 +++++--- include/sysemu/blockdev.h | 3 ++ nbd.c | 26 ++++++++++++++-- qemu-img.c | 39 ++++++++++++------------ qemu-io.c | 6 ++-- qemu-nbd.c | 6 ++-- 11 files changed, 189 insertions(+), 54 deletions(-)