diff --git a/news/3190.bugfix.md b/news/3190.bugfix.md
new file mode 100644
index 0000000000..0ad5fb87b6
--- /dev/null
+++ b/news/3190.bugfix.md
@@ -0,0 +1 @@
+Fix the `pre_install` and `post_install` signals receiving an exhausted generator, instead of a list of packages.
diff --git a/src/pdm/cli/actions.py b/src/pdm/cli/actions.py
index 99be844680..be167a6bfa 100644
--- a/src/pdm/cli/actions.py
+++ b/src/pdm/cli/actions.py
@@ -281,7 +281,7 @@ def do_sync(
         selection.validate()
         for group in selection:
             requirements.extend(project.get_dependencies(group))
-    packages = resolve_from_lockfile(project, requirements, groups=list(selection))
+    packages = list(resolve_from_lockfile(project, requirements, groups=list(selection)))
     if tracked_names and dry_run:
         packages = [p for p in packages if p.candidate.identify() in tracked_names]
     synchronizer = project.get_synchronizer()(
diff --git a/tests/test_signals.py b/tests/test_signals.py
index 2d63c47e58..9e2c63ff04 100644
--- a/tests/test_signals.py
+++ b/tests/test_signals.py
@@ -1,8 +1,12 @@
+from itertools import chain
 from unittest import mock
 
 import pytest
 
 from pdm import signals
+from pdm.models.candidates import Candidate
+from pdm.models.repositories import Package
+from pdm.models.requirements import Requirement
 
 
 def test_post_init_signal(project_no_init, pdm):
@@ -26,3 +30,82 @@ def test_post_lock_and_install_signals(project, pdm):
     signals.post_install.disconnect(post_install)
     for mocker in (pre_lock, post_lock, pre_install, post_install):
         mocker.assert_called_once()
+
+
+@pytest.mark.usefixtures("working_set")
+def test_lock_and_install_signals_injection_with_add(project, pdm):
+    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)
+    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)
+    pre_install = signals.pre_install.connect(mock.Mock(), weak=False)
+    post_install = signals.post_install.connect(mock.Mock(), weak=False)
+    pdm(["add", "requests"], obj=project, strict=True)
+    signals.pre_lock.disconnect(pre_lock)
+    signals.post_lock.disconnect(post_lock)
+    signals.pre_install.disconnect(pre_install)
+    signals.post_install.disconnect(post_install)
+
+    assert isinstance(pre_lock.call_args.kwargs["requirements"], list)
+    assert all(isinstance(e, Requirement) for e in pre_lock.call_args.kwargs["requirements"])
+    assert len(pre_lock.call_args.kwargs["requirements"]) == 1
+
+    assert isinstance(post_lock.call_args.kwargs["resolution"], dict)
+    assert all(isinstance(e, Candidate) for e in chain.from_iterable(post_lock.call_args.kwargs["resolution"].values()))
+    assert len(post_lock.call_args.kwargs["resolution"]) == 5
+
+    assert isinstance(pre_install.call_args.kwargs["packages"], list)
+    assert all(isinstance(e, Package) for e in pre_install.call_args.kwargs["packages"])
+    assert len(pre_install.call_args.kwargs["packages"]) == 5
+
+    assert isinstance(post_install.call_args.kwargs["packages"], list)
+    assert all(isinstance(e, Package) for e in post_install.call_args.kwargs["packages"])
+    assert len(post_install.call_args.kwargs["packages"]) == 5
+
+
+@pytest.mark.usefixtures("working_set")
+def test_lock_and_install_signals_injection_with_install(project, pdm):
+    project.add_dependencies(["requests"])
+
+    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)
+    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)
+    pre_install = signals.pre_install.connect(mock.Mock(), weak=False)
+    post_install = signals.post_install.connect(mock.Mock(), weak=False)
+    pdm(["install"], obj=project, strict=True)
+    signals.pre_lock.disconnect(pre_lock)
+    signals.post_lock.disconnect(post_lock)
+    signals.pre_install.disconnect(pre_install)
+    signals.post_install.disconnect(post_install)
+
+    assert isinstance(pre_lock.call_args.kwargs["requirements"], list)
+    assert all(isinstance(e, Requirement) for e in pre_lock.call_args.kwargs["requirements"])
+    assert len(pre_lock.call_args.kwargs["requirements"]) == 1
+
+    assert isinstance(post_lock.call_args.kwargs["resolution"], dict)
+    assert all(isinstance(e, Candidate) for e in chain.from_iterable(post_lock.call_args.kwargs["resolution"].values()))
+    assert len(post_lock.call_args.kwargs["resolution"]) == 5
+
+    assert isinstance(pre_install.call_args.kwargs["packages"], list)
+    assert all(isinstance(e, Package) for e in pre_install.call_args.kwargs["packages"])
+    assert len(pre_install.call_args.kwargs["packages"]) == 5
+
+    assert isinstance(post_install.call_args.kwargs["packages"], list)
+    assert all(isinstance(e, Package) for e in post_install.call_args.kwargs["packages"])
+    assert len(post_install.call_args.kwargs["packages"]) == 5
+
+
+@pytest.mark.usefixtures("working_set")
+def test_lock_signals_injection_with_update(project, pdm):
+    project.add_dependencies(["requests"])
+
+    pre_lock = signals.pre_lock.connect(mock.Mock(), weak=False)
+    post_lock = signals.post_lock.connect(mock.Mock(), weak=False)
+    pdm(["update"], obj=project, strict=True)
+    signals.pre_lock.disconnect(pre_lock)
+    signals.post_lock.disconnect(post_lock)
+
+    assert isinstance(pre_lock.call_args.kwargs["requirements"], list)
+    assert all(isinstance(e, Requirement) for e in pre_lock.call_args.kwargs["requirements"])
+    assert len(pre_lock.call_args.kwargs["requirements"]) == 1
+
+    assert isinstance(post_lock.call_args.kwargs["resolution"], dict)
+    assert all(isinstance(e, Candidate) for e in chain.from_iterable(post_lock.call_args.kwargs["resolution"].values()))
+    assert len(post_lock.call_args.kwargs["resolution"]) == 5