openMSX
DiskChanger.cc
Go to the documentation of this file.
1 #include "DiskChanger.hh"
2 #include "DiskFactory.hh"
3 #include "DummyDisk.hh"
4 #include "RamDSKDiskImage.hh"
5 #include "DirAsDSK.hh"
6 #include "CommandController.hh"
7 #include "RecordedCommand.hh"
9 #include "Scheduler.hh"
10 #include "FilePool.hh"
11 #include "File.hh"
12 #include "MSXMotherBoard.hh"
13 #include "Reactor.hh"
14 #include "DiskManipulator.hh"
15 #include "FileContext.hh"
16 #include "FileOperations.hh"
17 #include "FileException.hh"
18 #include "CommandException.hh"
19 #include "CliComm.hh"
20 #include "TclObject.hh"
21 #include "EmuTime.hh"
22 #include "serialize.hh"
23 #include "serialize_stl.hh"
24 #include "serialize_constr.hh"
25 #include "memory.hh"
26 #include <functional>
27 #include <utility>
28 
29 using std::string;
30 using std::vector;
31 
32 namespace openmsx {
33 
34 class DiskCommand final : public Command // TODO RecordedCommand
35 {
36 public:
37  DiskCommand(CommandController& commandController,
38  DiskChanger& diskChanger);
39  void execute(array_ref<TclObject> tokens,
40  TclObject& result) override;
41  string help(const vector<string>& tokens) const override;
42  void tabCompletion(vector<string>& tokens) const override;
43  bool needRecord(array_ref<TclObject> tokens) const /*override*/;
44 private:
45  DiskChanger& diskChanger;
46 };
47 
49  string driveName_,
50  bool createCmd,
51  bool doubleSidedDrive_,
52  std::function<void()> preChangeCallback_)
53  : reactor(board.getReactor())
54  , controller(board.getCommandController())
55  , stateChangeDistributor(&board.getStateChangeDistributor())
56  , scheduler(&board.getScheduler())
57  , preChangeCallback(std::move(preChangeCallback_))
58  , driveName(std::move(driveName_))
59  , doubleSidedDrive(doubleSidedDrive_)
60 {
61  init(board.getMachineID() + "::", createCmd);
62 }
63 
64 DiskChanger::DiskChanger(Reactor& reactor_, string driveName_)
65  : reactor(reactor_)
66  , controller(reactor.getCommandController())
67  , stateChangeDistributor(nullptr)
68  , scheduler(nullptr)
69  , driveName(std::move(driveName_))
70  , doubleSidedDrive(true) // irrelevant, but needs a value
71 {
72  init({}, true);
73 }
74 
75 void DiskChanger::init(const string& prefix, bool createCmd)
76 {
77  if (createCmd) createCommand();
78  ejectDisk();
79  auto& manipulator = reactor.getDiskManipulator();
80  manipulator.registerDrive(*this, prefix);
81  if (stateChangeDistributor) {
82  stateChangeDistributor->registerListener(*this);
83  }
84 }
85 
87 {
88  if (diskCommand) return;
89  diskCommand = make_unique<DiskCommand>(controller, *this);
90 }
91 
93 {
94  if (stateChangeDistributor) {
95  stateChangeDistributor->unregisterListener(*this);
96  }
97  auto& manipulator = reactor.getDiskManipulator();
98  manipulator.unregisterDrive(*this);
99 }
100 
102 {
103  return disk->getName();
104 }
105 
107 {
108  bool ret = diskChangedFlag;
109  diskChangedFlag = false;
110  return ret;
111 }
112 
114 {
115  if (dynamic_cast<DummyDisk*>(disk.get())) {
116  return nullptr;
117  }
118  return dynamic_cast<SectorAccessibleDisk*>(disk.get());
119 }
120 
121 const std::string& DiskChanger::getContainerName() const
122 {
123  return getDriveName();
124 }
125 
126 void DiskChanger::sendChangeDiskEvent(array_ref<string> args)
127 {
128  // note: might throw MSXException
129  if (stateChangeDistributor) {
130  stateChangeDistributor->distributeNew(
131  std::make_shared<MSXCommandEvent>(
132  args, scheduler->getCurrentTime()));
133  } else {
134  signalStateChange(std::make_shared<MSXCommandEvent>(
135  args, EmuTime::zero));
136  }
137 }
138 
139 void DiskChanger::signalStateChange(const std::shared_ptr<StateChange>& event)
140 {
141  auto* commandEvent = dynamic_cast<MSXCommandEvent*>(event.get());
142  if (!commandEvent) return;
143 
144  auto& tokens = commandEvent->getTokens();
145  if (tokens[0] == getDriveName()) {
146  if (tokens[1] == "eject") {
147  ejectDisk();
148  } else {
149  insertDisk(tokens);
150  }
151  }
152 }
153 
154 void DiskChanger::stopReplay(EmuTime::param /*time*/)
155 {
156  // nothing
157 }
158 
160 {
161  TclObject args[] = { TclObject("dummy"), TclObject(filename) };
162  try {
163  insertDisk(args);
164  return 0;
165  } catch (MSXException&) {
166  return -1;
167  }
168 }
169 
171 {
172  const string& diskImage = FileOperations::getConventionalPath(args[1].getString());
173  auto& diskFactory = reactor.getDiskFactory();
174  std::unique_ptr<Disk> newDisk(diskFactory.createDisk(diskImage, *this));
175  for (unsigned i = 2; i < args.size(); ++i) {
176  newDisk->applyPatch(Filename(
177  args[i].getString().str(), userFileContext()));
178  }
179 
180  // no errors, only now replace original disk
181  changeDisk(std::move(newDisk));
182 }
183 
184 void DiskChanger::ejectDisk()
185 {
186  changeDisk(make_unique<DummyDisk>());
187 }
188 
189 void DiskChanger::changeDisk(std::unique_ptr<Disk> newDisk)
190 {
191  if (preChangeCallback) preChangeCallback();
192  disk = std::move(newDisk);
193  diskChangedFlag = true;
195  getDiskName().getResolved());
196 }
197 
198 
199 // class DiskCommand
200 
202  DiskChanger& diskChanger_)
203  : Command(commandController_, diskChanger_.driveName)
204  , diskChanger(diskChanger_)
205 {
206 }
207 
209 {
210  if (tokens.size() == 1) {
211  result.addListElement(diskChanger.getDriveName() + ':');
212  result.addListElement(diskChanger.getDiskName().getResolved());
213 
214  TclObject options;
215  if (dynamic_cast<DummyDisk*>(diskChanger.disk.get())) {
216  options.addListElement("empty");
217  } else if (dynamic_cast<DirAsDSK*>(diskChanger.disk.get())) {
218  options.addListElement("dirasdisk");
219  } else if (dynamic_cast<RamDSKDiskImage*>(diskChanger.disk.get())) {
220  options.addListElement("ramdsk");
221  }
222  if (diskChanger.disk->isWriteProtected()) {
223  options.addListElement("readonly");
224  }
225  if (options.getListLength(getInterpreter()) != 0) {
226  result.addListElement(options);
227  }
228 
229  } else if (tokens[1] == "ramdsk") {
230  string args[] = {
231  diskChanger.getDriveName(), tokens[1].getString().str()
232  };
233  diskChanger.sendChangeDiskEvent(args);
234  } else if (tokens[1] == "-ramdsk") {
235  string args[] = {diskChanger.getDriveName(), "ramdsk"};
236  diskChanger.sendChangeDiskEvent(args);
237  result.setString(
238  "Warning: use of '-ramdsk' is deprecated, instead use the 'ramdsk' subcommand");
239  } else if (tokens[1] == "-eject") {
240  string args[] = {diskChanger.getDriveName(), "eject"};
241  diskChanger.sendChangeDiskEvent(args);
242  result.setString(
243  "Warning: use of '-eject' is deprecated, instead use the 'eject' subcommand");
244  } else if (tokens[1] == "eject") {
245  string args[] = {diskChanger.getDriveName(), "eject"};
246  diskChanger.sendChangeDiskEvent(args);
247  } else {
248  int firstFileToken = 1;
249  if (tokens[1] == "insert") {
250  if (tokens.size() > 2) {
251  firstFileToken = 2; // skip this subcommand as filearg
252  } else {
253  throw CommandException("Missing argument to insert subcommand");
254  }
255  }
256  try {
257  vector<string> args = { diskChanger.getDriveName() };
258  for (unsigned i = firstFileToken; i < tokens.size(); ++i) {
259  string_view option = tokens[i].getString();
260  if (option == "-ips") {
261  if (++i == tokens.size()) {
262  throw MSXException(
263  "Missing argument for option \"", option, '\"');
264  }
265  args.push_back(tokens[i].getString().str());
266  } else {
267  // backwards compatibility
268  args.push_back(option.str());
269  }
270  }
271  diskChanger.sendChangeDiskEvent(args);
272  } catch (FileException& e) {
273  throw CommandException(e.getMessage());
274  }
275  }
276 }
277 
278 string DiskCommand::help(const vector<string>& /*tokens*/) const
279 {
280  const string& driveName = diskChanger.getDriveName();
281  return strCat(
282  driveName, " eject : remove disk from virtual drive\n",
283  driveName, " ramdsk : create a virtual disk in RAM\n",
284  driveName, " insert <filename> : change the disk file\n",
285  driveName, " <filename> : change the disk file\n",
286  driveName, " : show which disk image is in drive\n"
287  "The following options are supported when inserting a disk image:\n"
288  "-ips <filename> : apply the given IPS patch to the disk image");
289 }
290 
291 void DiskCommand::tabCompletion(vector<string>& tokens) const
292 {
293  if (tokens.size() >= 2) {
294  static const char* const extra[] = {
295  "eject", "ramdsk", "insert",
296  };
297  completeFileName(tokens, userFileContext(), extra);
298  }
299 }
300 
302 {
303  return tokens.size() > 1;
304 }
305 
306 static string calcSha1(SectorAccessibleDisk* disk, FilePool& filePool)
307 {
308  return disk ? disk->getSha1Sum(filePool).toString() : string{};
309 }
310 
311 // version 1: initial version
312 // version 2: replaced Filename with DiskName
313 template<typename Archive>
314 void DiskChanger::serialize(Archive& ar, unsigned version)
315 {
316  DiskName diskname = disk->getName();
317  if (ar.versionBelow(version, 2)) {
318  // there was no DiskName yet, just a plain Filename
319  Filename filename;
320  ar.serialize("disk", filename);
321  if (filename.getOriginal() == "ramdisk") {
322  diskname = DiskName(Filename(), "ramdisk");
323  } else {
324  diskname = DiskName(filename, {});
325  }
326  } else {
327  ar.serialize("disk", diskname);
328  }
329 
330  vector<Filename> patches;
331  if (!ar.isLoader()) {
332  patches = disk->getPatches();
333  }
334  ar.serialize("patches", patches);
335 
336  auto& filePool = reactor.getFilePool();
337  string oldChecksum;
338  if (!ar.isLoader()) {
339  oldChecksum = calcSha1(getSectorAccessibleDisk(), filePool);
340  }
341  ar.serialize("checksum", oldChecksum);
342 
343  if (ar.isLoader()) {
344  diskname.updateAfterLoadState();
345  string name = diskname.getResolved(); // TODO use Filename
346  if (!name.empty()) {
347  // Only when the original file doesn't exist on this
348  // system, try to search by sha1sum. This means we
349  // prefer the original file over a file with a matching
350  // sha1sum (the original file may have changed). An
351  // alternative is to prefer the exact sha1sum match.
352  // I'm not sure which alternative is better.
353  if (!FileOperations::exists(name)) {
354  assert(!oldChecksum.empty());
355  auto file = filePool.getFile(
356  FilePool::DISK, Sha1Sum(oldChecksum));
357  if (file.is_open()) {
358  name = file.getURL();
359  }
360  }
361  vector<TclObject> args =
362  { TclObject("dummy"), TclObject(name) };
363  for (auto& p : patches) {
364  p.updateAfterLoadState();
365  args.emplace_back(p.getResolved()); // TODO
366  }
367 
368  try {
369  insertDisk(args);
370  } catch (MSXException& e) {
371  throw MSXException(
372  "Couldn't reinsert disk in drive ",
373  getDriveName(), ": ", e.getMessage());
374  // Alternative: Print warning and continue
375  // without diskimage. Is this better?
376  }
377  }
378 
379  string newChecksum = calcSha1(getSectorAccessibleDisk(), filePool);
380  if (oldChecksum != newChecksum) {
381  controller.getCliComm().printWarning(
382  "The content of the diskimage ",
383  diskname.getResolved(),
384  " has changed since the time this savestate was "
385  "created. This might result in emulation problems "
386  "or even diskcorruption. To prevent the latter, "
387  "the disk is now write-protected (eject and "
388  "reinsert the disk if you want to override this).");
389  disk->forceWriteProtect();
390  }
391  }
392 
393  // This should only be restored after disk is inserted
394  ar.serialize("diskChanged", diskChangedFlag);
395 }
396 
397 // extra (local) constructor arguments for polymorphic de-serialization
399 {
400  using type = std::tuple<std::string>;
401 
402  template<typename Archive>
403  void save(Archive& ar, const DiskChanger& changer)
404  {
405  ar.serialize("driveName", changer.getDriveName());
406  }
407 
408  template<typename Archive> type load(Archive& ar, unsigned /*version*/)
409  {
410  string driveName;
411  ar.serialize("driveName", driveName);
412  return make_tuple(driveName);
413  }
414 };
415 
418  std::reference_wrapper<MSXMotherBoard>);
419 
420 } // namespace openmsx
Contains the main loop of openMSX.
Definition: Reactor.hh:63
const std::string & getContainerName() const override
Definition: DiskChanger.cc:121
const std::string getURL() const
Returns the URL of this file object.
Definition: File.cc:124
DiskChanger(MSXMotherBoard &board, std::string driveName, bool createCommand=true, bool doubleSidedDrive=true, std::function< void()> preChangeCallback={})
Definition: DiskChanger.cc:48
This class is used to for Tcl commands that directly influence the MSX state (e.g.
File getFile(FileType fileType, const Sha1Sum &sha1sum)
Search file with the given sha1sum.
Definition: FilePool.cc:313
const DiskName & getDiskName() const
Definition: DiskChanger.cc:101
void save(Archive &ar, const DiskChanger &changer)
Definition: DiskChanger.cc:403
Sha1Sum getSha1Sum(FilePool &filepool)
Calculate SHA1 of the content of this disk.
void unregisterListener(StateChangeListener &listener)
STL namespace.
unsigned getListLength(Interpreter &interp) const
Definition: TclObject.cc:152
void distributeNew(const EventPtr &event)
Deliver the event to all registered listeners MSX input devices should call the distributeNew() versi...
void registerListener(StateChangeListener &listener)
(Un)registers the given object to receive state change events.
void registerDrive(DiskContainer &drive, const std::string &prefix)
void execute(array_ref< TclObject > tokens, TclObject &result) override
Execute this command.
Definition: DiskChanger.cc:208
void serialize(Archive &ar, unsigned version)
Definition: Filename.cc:45
EmuTime::param getCurrentTime() const
Get the current scheduler time.
Definition: Scheduler.cc:93
REGISTER_POLYMORPHIC_CLASS_1(DiskContainer, DiskChanger, "DiskChanger", std::reference_wrapper< MSXMotherBoard >)
static void completeFileName(std::vector< std::string > &tokens, const FileContext &context, const RANGE &extra)
Definition: Completer.hh:122
const std::string & getMessage() const
Definition: MSXException.hh:23
void serialize(Archive &ar, unsigned version)
Definition: DiskChanger.cc:314
void changeDisk(std::unique_ptr< Disk > newDisk)
Definition: DiskChanger.cc:189
const std::string & getOriginal() const
Definition: Filename.hh:26
FileContext userFileContext(string_view savePath)
Definition: FileContext.cc:161
This class represents the result of a sha1 calculation (a 160-bit value).
Definition: sha1.hh:19
bool diskChanged() override
Definition: DiskChanger.cc:106
This class implements a subset of the proposal for std::array_ref (proposed for the next c++ standard...
Definition: array_ref.hh:19
const std::string & getMachineID() const
string getConventionalPath(string_view path)
Returns the path in conventional path-delimiter.
DiskFactory & getDiskFactory()
Definition: Reactor.hh:84
int insertDisk(string_view filename) override
Definition: DiskChanger.cc:159
This class represents a filename.
Definition: Filename.hh:17
Thanks to enen for testing this on a real cartridge:
Definition: Autofire.cc:5
bool exists(string_view filename)
Does this file (directory) exists?
void setString(string_view value)
Definition: TclObject.cc:14
CommandController & getCommandController() const
Definition: Command.hh:23
DiskManipulator & getDiskManipulator()
Definition: Reactor.hh:85
string help(const vector< string > &tokens) const override
Print help for this command.
Definition: DiskChanger.cc:278
size_type size() const
Definition: array_ref.hh:61
std::string toString() const
Definition: utils/sha1.cc:231
void tabCompletion(vector< string > &tokens) const override
Attempt tab completion for this command.
Definition: DiskChanger.cc:291
Interpreter & getInterpreter() const
Definition: Command.cc:41
This class implements a (close approximation) of the std::string_view class.
Definition: string_view.hh:15
std::string getResolved() const
Definition: DiskName.cc:24
#define INSTANTIATE_SERIALIZE_METHODS(CLASS)
Definition: serialize.hh:840
std::string str() const
Definition: string_view.cc:12
virtual CliComm & getCliComm()=0
const std::vector< TclObject > & getTokens() const
std::vector< Filename > getPatches() const
void addListElement(string_view element)
Definition: TclObject.cc:69
SectorAccessibleDisk * getSectorAccessibleDisk() override
Definition: DiskChanger.cc:113
void updateAfterLoadState()
Definition: DiskName.cc:29
virtual void update(UpdateType type, string_view name, string_view value)=0
std::string strCat(Ts &&...ts)
Definition: strCat.hh:577
bool needRecord(array_ref< TclObject > tokens) const
Definition: DiskChanger.cc:301
Serialize (local) constructor arguments.
DiskCommand(CommandController &commandController, DiskChanger &diskChanger)
Definition: DiskChanger.cc:201
void unregisterDrive(DiskContainer &drive)
const std::string & getDriveName() const
Definition: DiskChanger.hh:38